跳至页脚内容
使用IRONPDF

如何用OCR.net和IronPDF在C#中构建一个PDF OCR工作流程

显示OCRNet处理流程的图表,包含五个阶段:输入图像、预处理、特征提取、OCRNet的序列建模,以及解码以生成提取的文本内容。

OCR.net 是一个用于光学字符识别的深度学习框架,与IronPDF配对,从PDF中提取文本并在.NET应用程序中生成可搜索的文档。 本教程将向您展示如何连接这两个工具,以便您的应用程序可以处理扫描文件、光栅化PDF页面以进行OCR处理,并将识别的文本重新组装成新的可搜索PDF。

OCR.net模型在场景文本检测和复杂环境中的字符识别方面表现出色。 将其与IronPDF的渲染引擎结合使用,您将获得一个完整的管道:生成或加载PDF,将其页面导出为高分辨率图像,将这些图像发送到OCR.net,并将结果重建为完全可搜索的文档。

立即开始使用 IronPDF。
green arrow pointer

如何开始使用 IronPDF?

在构建OCR工作流程之前,您需要在项目中安装IronPDF。 最快的路径是使用NuGet包管理器控制台:

Install-Package IronPDF

或者通过NuGet UI直接搜索IronPdf添加它。 安装完成后,在应用程序启动时应用您的许可证密钥:

using IronPdf;

IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
using IronPdf;

IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
$vbLabelText   $csharpLabel

免费试用许可证可用,因此您可以在没有任何限制的情况下测试完整功能。 IronPDF支持.NET 6、7、8和10,在Windows、Linux和macOS上运行,这意味着相同的代码在桌面应用程序、ASP.NET Core Web服务和容器化部署中运行。

对于Docker环境,IronPDF提供了预配置的Linux部署指南瘦包变体,以减少镜像大小。如果您更喜欢远程渲染架构,IronPDF Engine可以作为独立服务运行,支持任何平台上的客户端。

什么是 OCRNet,光学字符识别如何工作?

OCR.net 是一种深度学习光学字符识别(OCR)方法,能够识别不同字体风格的字母数字字符。 该模型使用优化的神经网络架构从输入图像中捕获空间特征。 结合PDF生成功能,这些训练模型在常见文档类型上提供出色的识别精度。

OCR.net背后的识别框架结合了门控循环单元(GRU),以改进特征学习并处理基于图像的序列识别任务。 这种混合模型通过连接时序分类(CTC)实现显著的准确性,这是一种最初引入用于序列标注的技术,非常适用于文档OCR。 正在进行的改进持续扩展了OCR.net的语言支持,特别是当与PDF文本提取工具集成时。

现代OCR流程的关键组件包括:

  • 文本检测:使用训练模型识别图像中的文本内容区域
  • 场景文本检测:在复杂背景和动态环境中定位文本
  • 字母数字字符识别:使用训练模型高精度识别字符
  • 模式识别:应用图像处理技术进行轻量级场景文本识别

基于GRU的架构和连接时序分类在容器化环境中实现了高效的资源使用,使OCR.net成为<Kubernetes部署的实际选择,其中内存和CPU限制很重要。 轻量级架构保持Docker镜像大小可控,同时保持强大的识别准确性。

何时应该使用OCR.net而非传统OCR库?

在处理复杂场景文本、手写文档或模板为基础的OCR失败的多语言内容时,OCR.net是更好的选择。 它在需要在没有外部依赖的不同硬件配置上提供一致性能的容器化应用程序中特别出色。 该模型干净地处理UTF-8编码,这对于国际语言支持来说很重要。

传统的基于正则表达式或模板匹配的OCR系统在可变字体、手写文字或照度不均的图像上不起作用。 OCR.net的神经方法在这些场景中表现更好,因为它学习特征而不是匹配固定模板。 不过,如果您的文档是干净的、机器输入的文本且格式一致,则可能轻量级库更快且足够。

生产中OCR.net的常见资源要求是什么?

生产部署通常需要2-4个CPU核心和4-8 GB的RAM以确保稳定性能。 GPU加速在使用NVIDIA Docker运行时的容器化环境中的批量处理上提供了显著的加速。这些要求非常适合与Azure应用服务AWS Lambda部署,但Lambda的内存上限意味着您在承诺之前应衡量您的特定文档大小。

IronPDF如何为OCR处理创建PDF文档?

IronPDF使.NET开发人员能够完全控制PDF生成。 该库可以通过其基于Chrome的渲染引擎渲染HTML字符串URLs和文件输入为完善的PDF。对于OCR工作流,关键功能是RasterizeToImageFiles(),它将PDF页面导出为适合识别的高分辨率图像。

using IronPdf;

// Create a PDF document with IronPDF
var renderer = new ChromePdfRenderer();

// Set 300 DPI for OCR accuracy -- higher DPI preserves text sharpness
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.DPI = 300;
renderer.RenderingOptions.MarginTop = 50;
renderer.RenderingOptions.MarginBottom = 50;

var pdf = renderer.RenderHtmlAsPdf(@"
    <h1>Document Report</h1>
    <p>Scene text integration for computer vision analysis.</p>
    <p>Text detection results for dataset and model analysis.</p>");

// Tag the document with searchable metadata
pdf.MetaData.Author = "OCR Processing Pipeline";
pdf.MetaData.Keywords = "OCR, Text Recognition, Computer Vision";
pdf.MetaData.ModifiedDate = DateTime.Now;

pdf.SaveAs("document-for-ocr.pdf");

// Export pages as PNG images for OCR.net -- 300 DPI is the recommended minimum
pdf.RasterizeToImageFiles("page-*.png", IronPdf.Imaging.ImageType.Png, 300);
using IronPdf;

// Create a PDF document with IronPDF
var renderer = new ChromePdfRenderer();

// Set 300 DPI for OCR accuracy -- higher DPI preserves text sharpness
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.DPI = 300;
renderer.RenderingOptions.MarginTop = 50;
renderer.RenderingOptions.MarginBottom = 50;

var pdf = renderer.RenderHtmlAsPdf(@"
    <h1>Document Report</h1>
    <p>Scene text integration for computer vision analysis.</p>
    <p>Text detection results for dataset and model analysis.</p>");

// Tag the document with searchable metadata
pdf.MetaData.Author = "OCR Processing Pipeline";
pdf.MetaData.Keywords = "OCR, Text Recognition, Computer Vision";
pdf.MetaData.ModifiedDate = DateTime.Now;

pdf.SaveAs("document-for-ocr.pdf");

// Export pages as PNG images for OCR.net -- 300 DPI is the recommended minimum
pdf.RasterizeToImageFiles("page-*.png", IronPdf.Imaging.ImageType.Png, 300);
$vbLabelText   $csharpLabel

方法RasterizeToImageFiles()以指定的DPI将PDF页面转换为PNG图像。 在300 DPI时,文本边缘保持足够清晰,使OCR模型能够区分类似字符。 在150 DPI或更低时,识别精度在衬线字体和小号字体上明显下降。 导出后,将PNG文件上传到OCR.net或直接传递给本地模型。

显示在图像查看器中的PDF文档截图,展示标题

为什么DPI设置会影响OCR准确性?

更高的DPI设置(300-600)保留了文本的清晰度,这是OCR模型准确区分字符所需的。 权衡是文件大小和处理时间。在300 DPI时,单个A4页面大约会产生一个2-3 MB的PNG。 在600 DPI时,这一大小增长到8-12 MB。 对于大多数文档,300 DPI是合适的平衡点。 渲染选项可让您根据文档类型调整此设置,压缩技术则帮助在OCR完成后优化文件大小。

IronPDF如何处理容器化环境?

IronPDF的本地引擎确保了在LinuxWindowsmacOS容器中一致的渲染。 对于高可用性服务,IronPDF与ASP.NET Core健康检查终点集成,这样您可以实现准备和存活性探测,验证PDF渲染在向容器实例路由流量之前是否可用。

using IronPdf;

// Kubernetes-compatible health check endpoint
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/health/ready", async () =>
{
    try
    {
        var renderer = new ChromePdfRenderer();
        var testPdf = await renderer.RenderHtmlAsPdfAsync("<p>Health check</p>");
        return testPdf.PageCount > 0 ? Results.Ok() : Results.Problem();
    }
    catch
    {
        return Results.Problem("PDF rendering unavailable");
    }
});

await app.RunAsync();
using IronPdf;

// Kubernetes-compatible health check endpoint
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/health/ready", async () =>
{
    try
    {
        var renderer = new ChromePdfRenderer();
        var testPdf = await renderer.RenderHtmlAsPdfAsync("<p>Health check</p>");
        return testPdf.PageCount > 0 ? Results.Ok() : Results.Problem();
    }
    catch
    {
        return Results.Problem("PDF rendering unavailable");
    }
});

await app.RunAsync();
$vbLabelText   $csharpLabel

在此终点旁使用自定义日志记录,以捕捉渲染时间并识别在完全失败之前就已开始恶化的容器。

OCR.net 如何从 PDF 图像中提取文本?

一旦您从IronPDF获取了PNG导出,就可以将其上传到OCR.net进行文本识别。 OCR.net管道流程处理图像并返回跨不同字体风格的标准化文本输出。 它处理已打印和手写文本,并支持超过60种文档语言。

使用 OCR.net 在线:

1.导航至 https://ocr.net/

  1. 上传从IronPDF导出的PNG或JPG图像(最大2 MB)
  2. 从60多种可用选项中选择文档语言
  3. 选择输出格式:纯文本或可搜索的PDF
  4. 点击"立即转换"以使用OCR.net模型处理图像

OCR.net网页界面显示page-1.png的文件上传,语言选择设为英语,输出格式设为文本

OCR.net还提供用于自动化处理的API。 免费帐户限制为每小时50个请求,这对自动化管道来说是个重要约束。 设计您的集成以通过指数回退优雅地处理速率限制响应,而不是硬性失败:

using System;
using System.Net.Http;
using System.Threading.Tasks;

// Queue-based OCR processing with exponential backoff retry
async Task<string> ProcessOcrWithRetry(string imagePath, int maxRetries = 3)
{
    for (int attempt = 0; attempt < maxRetries; attempt++)
    {
        try
        {
            // Replace with your actual OCR.net API call
            return await CallOcrNetApi(imagePath);
        }
        catch (HttpRequestException ex) when (ex.Message.Contains("429"))
        {
            if (attempt == maxRetries - 1) throw;
            var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
            await Task.Delay(delay);
        }
    }
    throw new InvalidOperationException("OCR processing failed after all retries");
}
using System;
using System.Net.Http;
using System.Threading.Tasks;

// Queue-based OCR processing with exponential backoff retry
async Task<string> ProcessOcrWithRetry(string imagePath, int maxRetries = 3)
{
    for (int attempt = 0; attempt < maxRetries; attempt++)
    {
        try
        {
            // Replace with your actual OCR.net API call
            return await CallOcrNetApi(imagePath);
        }
        catch (HttpRequestException ex) when (ex.Message.Contains("429"))
        {
            if (attempt == maxRetries - 1) throw;
            var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
            await Task.Delay(delay);
        }
    }
    throw new InvalidOperationException("OCR processing failed after all retries");
}
$vbLabelText   $csharpLabel

对可达性工作流来说,OCR文本提取允许视力受损的用户从以前仅有图像的文档中收到音频反馈。 通过IronPDF将OCR.net输出与PDF/UA合规性相结合,创建辅助技术可以有效导航的文档。

如何构建完整的IronPDF与OCR.net工作流程?

将IronPDF与OCR.net连接起来可以生成端到端的文档解决方案。 工作流程有三个阶段:将PDF页面导出为图像,将图像发送到OCR.net进行文本提取,并将识别的文本重新构建为新的可搜索PDF。

using IronPdf;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

// --- Stage 1: Export PDF pages as images for OCR ---
var scannedPdf = PdfDocument.FromFile("input-document.pdf");
var imageFiles = scannedPdf.RasterizeToImageFiles(
    "scan-page-{0}.png",
    IronPdf.Imaging.ImageType.Png,
    300 // 300 DPI -- minimum for reliable OCR accuracy
);

// --- Stage 2: Process each image through OCR.net ---
var ocrResults = new List<string>();
foreach (var imageFile in imageFiles)
{
    // Replace this placeholder with your actual OCR.net API integration
    string ocrText = await SendImageToOcrNet(imageFile);
    ocrResults.Add(ocrText);
}

// --- Stage 3: Reassemble recognized text as a searchable PDF ---
var htmlBuilder = new StringBuilder();
htmlBuilder.Append(@"<!DOCTYPE html><html><head>
    <style>body{font-family:Arial,sans-serif;margin:40px;}
    .page{page-break-after:always;} pre{white-space:pre-wrap;}</style>
    </head><body>");

for (int i = 0; i < ocrResults.Count; i++)
{
    htmlBuilder.AppendFormat(
        "<div class='page'><h2>Page {0}</h2><pre>{1}</pre></div>",
        i + 1,
        System.Web.HttpUtility.HtmlEncode(ocrResults[i])
    );
}
htmlBuilder.Append("</body></html>");

var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.DPI = 300;
renderer.RenderingOptions.EnableJavaScript = false;

var searchablePdf = await renderer.RenderHtmlAsPdfAsync(htmlBuilder.ToString());
searchablePdf.MetaData.Title = "OCR Processed Document";
searchablePdf.MetaData.Subject = "Searchable PDF from OCR";
searchablePdf.MetaData.CreationDate = DateTime.UtcNow;
searchablePdf.SecuritySettings.AllowUserPrinting = true;
searchablePdf.SecuritySettings.AllowUserCopyPasteContent = true;

searchablePdf.SaveAs("searchable-document.pdf");
using IronPdf;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

// --- Stage 1: Export PDF pages as images for OCR ---
var scannedPdf = PdfDocument.FromFile("input-document.pdf");
var imageFiles = scannedPdf.RasterizeToImageFiles(
    "scan-page-{0}.png",
    IronPdf.Imaging.ImageType.Png,
    300 // 300 DPI -- minimum for reliable OCR accuracy
);

// --- Stage 2: Process each image through OCR.net ---
var ocrResults = new List<string>();
foreach (var imageFile in imageFiles)
{
    // Replace this placeholder with your actual OCR.net API integration
    string ocrText = await SendImageToOcrNet(imageFile);
    ocrResults.Add(ocrText);
}

// --- Stage 3: Reassemble recognized text as a searchable PDF ---
var htmlBuilder = new StringBuilder();
htmlBuilder.Append(@"<!DOCTYPE html><html><head>
    <style>body{font-family:Arial,sans-serif;margin:40px;}
    .page{page-break-after:always;} pre{white-space:pre-wrap;}</style>
    </head><body>");

for (int i = 0; i < ocrResults.Count; i++)
{
    htmlBuilder.AppendFormat(
        "<div class='page'><h2>Page {0}</h2><pre>{1}</pre></div>",
        i + 1,
        System.Web.HttpUtility.HtmlEncode(ocrResults[i])
    );
}
htmlBuilder.Append("</body></html>");

var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.DPI = 300;
renderer.RenderingOptions.EnableJavaScript = false;

var searchablePdf = await renderer.RenderHtmlAsPdfAsync(htmlBuilder.ToString());
searchablePdf.MetaData.Title = "OCR Processed Document";
searchablePdf.MetaData.Subject = "Searchable PDF from OCR";
searchablePdf.MetaData.CreationDate = DateTime.UtcNow;
searchablePdf.SecuritySettings.AllowUserPrinting = true;
searchablePdf.SecuritySettings.AllowUserCopyPasteContent = true;

searchablePdf.SaveAs("searchable-document.pdf");
$vbLabelText   $csharpLabel

该管道故意设计为简单明了。 阶段1生成编号的PNG文件。 阶段2将每个文件发送到OCR.net并收集返回的文本字符串。 阶段3将这些字符串包装进HTML中,并使用IronPDF渲染最终的PDF,其中的文本完全可选择和搜索。 您可以扩展阶段3以应用PDF元数据进行文档管理或安全设置来控制访问。

比较两个PDF查看器窗口并排截图,左侧显示有关

此工作流程的最佳Docker配置是什么?

多阶段Docker构建在包含IronPDF在Linux上所需的所有运行时依赖项时保持最终镜像小巧:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app

# IronPDF Linux runtime dependencies
RUN apt-get update && apt-get install -y \
    libgdiplus \
    libc6-dev \
    libx11-dev \
    && rm -rf /var/lib/apt/lists/*

COPY --from=build /app/out .

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health/ready || exit 1

ENTRYPOINT ["dotnet", "OcrWorkflow.dll"]

对于生产规模,考虑Kubernetes Jobs来进行批量OCR操作。 Kubernetes Jobs提供自动重试、并行控制和资源隔离,以确保失败的文档任务不会影响其他服务。 设置parallelism以匹配您的OCR.net API层级,并设置backoffLimit以控制一个失败的pod在任务被标记为失败之前重试的次数。

如何在生产中监控性能指标?

跟踪OCR处理时间和成功率有助于在它们影响用户之前识别瓶颈。 Prometheus与自定义指标是一个实际方法:

using Prometheus;
using System;
using System.Threading.Tasks;

// Prometheus metrics for OCR pipeline observability
var ocrRequestsTotal = Metrics
    .CreateCounter("ocr_requests_total", "Total OCR requests processed");

var ocrDuration = Metrics
    .CreateHistogram("ocr_duration_seconds", "OCR processing duration in seconds",
        new HistogramConfiguration
        {
            Buckets = Histogram.LinearBuckets(0.1, 0.1, 10)
        });

var activeOcrJobs = Metrics
    .CreateGauge("ocr_active_jobs", "Currently active OCR jobs");

// Wrapper that tracks every OCR operation automatically
async Task<t> TrackOcrOperation<t>(Func<Task<t>> operation)
{
    using (ocrDuration.NewTimer())
    {
        activeOcrJobs.Inc();
        try
        {
            var result = await operation();
            ocrRequestsTotal.Inc();
            return result;
        }
        finally
        {
            activeOcrJobs.Dec();
        }
    }
}
using Prometheus;
using System;
using System.Threading.Tasks;

// Prometheus metrics for OCR pipeline observability
var ocrRequestsTotal = Metrics
    .CreateCounter("ocr_requests_total", "Total OCR requests processed");

var ocrDuration = Metrics
    .CreateHistogram("ocr_duration_seconds", "OCR processing duration in seconds",
        new HistogramConfiguration
        {
            Buckets = Histogram.LinearBuckets(0.1, 0.1, 10)
        });

var activeOcrJobs = Metrics
    .CreateGauge("ocr_active_jobs", "Currently active OCR jobs");

// Wrapper that tracks every OCR operation automatically
async Task<t> TrackOcrOperation<t>(Func<Task<t>> operation)
{
    using (ocrDuration.NewTimer())
    {
        activeOcrJobs.Inc();
        try
        {
            var result = await operation();
            ocrRequestsTotal.Inc();
            return result;
        }
        finally
        {
            activeOcrJobs.Dec();
        }
    }
}
$vbLabelText   $csharpLabel

将这些指标与IronPDF的日志能力配合起来,以将渲染时间与OCR持续时间相关联。 当OCR时长飙升但渲染时间没有相应增长时,瓶颈在于OCR.net API调用或到该调用的网络路径,而不是在PDF生成步骤中。

您的下一步是什么?

结合IronPDF的OCR.net为您在.NET中提供了一条实用的文本提取和可搜索PDF生成路径。 该管道涵盖了核心用例:从HTML创建PDF,以OCR兼容分辨率导出页面,将图像发送到OCR.net,并将结果重新组装成完全可搜索的文档。

将此移至生产时需考虑的关键点:

  • 容器设置:使用IronPDF Slim包和多阶段Docker构建以保持镜像大小可控
  • 资源规划:依据文档大小和并发目标配置内存限制
  • 监控:实现在IronPDF日志旁的Prometheus指标以早期捕获降级
  • 吞吐量:使用异步操作和批量队列管理在OCR.net的速率限制内工作
  • 可靠性:针对OCR.net API调用构建指数回退重试逻辑和断路器

在承诺生产许可证之前,先使用免费试用许可证测试完整工作流程端到端。 该试用版移除水印并解锁所有功能,因此您的基准测试结果准确反映生产行为。 准备好部署后,请查看IronPDF许可选项以找到符合使用模式的层级。

常见问题解答

OCR.net有什么作用,以及它如何与IronPDF连接?

OCR.net是一个深度学习OCR服务,接受图像输入并返回识别的文本。IronPDF生成PDF并将其页面导出为图像。这两个工具在图像层连接:IronPDF通过RasterizeToImageFiles()导出页面,这些图像用于OCR.net进行文本提取,然后IronPDF将结果重新组装为可搜索的PDF。

导出PDF页面以进行OCR时应使用哪种DPI?

300 DPI是可靠OCR准确性的标准最低值。在300 DPI时,文本边缘足够清晰以便模型区分相似字符。在150 DPI或以下,准确性在衬线字体和小字上下降。仅当源文档包含非常小或退化的文本时才使用600 DPI,因为600 DPI下每页生成的文件大小要大4-5倍。

在生产中如何处理OCR.net API速率限制?

OCR.net免费账户每小时允许50次请求。在您的OCR调用中构建指数退避重试逻辑:捕获429响应,等待Math.Pow(2, attempt)秒,然后重试直到配置的最大次数。为了更高的吞吐量,升级到付费的OCR.net计划或使用后台工作服务排队请求。

IronPDF可以在Linux的Docker容器内运行吗?

可以。在您的Dockerfile的运行时阶段添加libgdipluslibc6-devlibx11-dev。使用多阶段构建以保持最终映像小。IronPDF Slim包变体通过在您将IronPDF Engine作为单独的服务运行时排除捆绑的浏览器二进制文件进一步减小映像大小。

如何从OCR结果创建可搜索的PDF?

收集OCR.net返回的文本字符串,用每个文档页面一个换页类将它们包装在HTML中,然后将HTML传递给ChromePdfRenderer.RenderHtmlAsPdfAsync()。生成的PDF包含用户和搜索引擎可以索引的可选择和可搜索的文本。

此工作流程支持多语言文档吗?

支持。OCR.net支持60多种语言。在处理前在OCR.net界面或API调用中选择目标语言。IronPDF原生支持UTF-8输出,因此使用非拉丁文的语言在重建的可搜索PDF中正确呈现。

在生产中如何监控OCR管道性能?

为您的处理服务添加Prometheus计数器、直方图和仪表盘,以跟踪总请求、持续时间分布和活动作业。将Prometheus指标与IronPDF的自定义日志记录配对,以关联渲染时间与OCR API延迟,并识别瓶颈所在。

OCR.net与IronOCR的区别是什么?

OCR.net是一个外部Web服务,处理您通过API上传的图像。IronOCR是Iron Software的.NET库,在您的应用程序中本地运行OCR处理,无需外部API调用。IronOCR更适合离线环境,或者当您需要更低的延迟和对OCR引擎的更多控制时。

Curtis Chau
技术作家

Curtis Chau 拥有卡尔顿大学的计算机科学学士学位,专注于前端开发,精通 Node.js、TypeScript、JavaScript 和 React。他热衷于打造直观且美观的用户界面,喜欢使用现代框架并创建结构良好、视觉吸引力强的手册。

除了开发之外,Curtis 对物联网 (IoT) 有浓厚的兴趣,探索将硬件和软件集成的新方法。在空闲时间,他喜欢玩游戏和构建 Discord 机器人,将他对技术的热爱与创造力相结合。

Iron Support Team

We're online 24 hours, 5 days a week.
Chat
Email
Call Me