跳至页脚内容
视频

如何使用开源库在C#中实现OCR

本指南面向已集成 Klippa 的 REST API 并正在迁移到IronOCR进行本地文档处理的.NET开发人员。 它涵盖了移除 HTTP 客户端基础架构、消除 JSON 反序列化以及用从不接触网络的本地 OCR 调用替换依赖云的文档上传的实际步骤。

为什么要从 剪切 OCR 迁移?

Klippa 是一款纯云端文档智能服务,不包含.NET SDK。 每个集成都是手工构建的 REST 客户端。 这种架构现实会产生下游影响,并在生产系统的整个生命周期内不断累积。

没有NuGet包意味着您拥有集成层。无需安装任何内容。 进入成本是编写一个MultipartFormDataContent请求体,反序列化Klippa的JSON响应架构,并为瞬时故障安装重试逻辑。 这意味着在第一个文档能够可靠地在生产环境中处理之前,需要 2-4 天的准备工作。 当 Klippa 更新其 API 架构时,您的反序列化代码会失效,需要手动维护。

每次文档上传都依赖于网络。Klippa仅在欧盟境内的服务器上处理文档。 Klippa 端生产中断、延迟升高或应用程序服务器出站互联网访问中断都会导致文档处理完全停止。 如果云服务不可用,则没有回退机制、本地模式或重试机制来解决该问题。

敏感文件离开您的基础设施。财务文件——包含付款详情的收据、包含增值税号和金额的发票、包含护照信息的身份证件——在每次 API 调用时都会传输到第三方服务器。 GDPR 的数据传输条款解决了欧盟境内处理的部分问题,但审计范围仍然扩展到 Klippa 的基础设施、数据保留政策和子处理者。 对于拥有医疗保健、法律、金融服务或政府合同的团队而言,"欧盟托管"并不符合数据不得离开组织的要求。

按文档计费,无上限。Klippa不公开定价。 在任何有意义的文档量下——例如费用管理系统中每月 10,000 张收据,或应付账款自动化工作流程中每天 500 张发票——按文档计费模式都会产生永久许可证永远不会产生的成本。 成本走势与业务增长直接相关,这与基础设施支出的初衷背道而驰。

当需求扩大时,专家的职责范围就会受到限制。Klippa接受过收据、发票和身份证明文件方面的培训。 一款最初定位为费用管理的应用,很少会一直停留在费用管理领域。 如果首次出现这三类文件之外的文件类型(例如扫描的雇佣协议、医疗表格、技术图纸、非标准布局的采购订单),Klippa 将不会返回任何有用的信息。 IronOCR可以处理任何包含文本的文档,没有类别限制。

仅支持异步的 REST 调用会增加同步上下文中的延迟。每个 Klippa 调用都是一个异步 HTTP 操作。 单个文档在网络上的往返传输时间为 500 毫秒至 2000 毫秒。 IronOCR在本地处理同一文档只需 100-400 毫秒,无需异步开销,这在同步处理更适合架构的场景中尤为如此。

基本问题

Klippa没有SDK。 OCR 指的是构建并发送 HTTP 请求,然后反序列化 JSON:

// Klippa: 15+ lines of HTTP plumbing before you read a single character
var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes(imagePath)), "document", "receipt.jpg");
_client.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey); // auth header — rotates, breaks, leaks

var response = await _client.PostAsync(
    "https://custom-ocr.klippa.com/api/v1/parseDocument", content);
response.EnsureSuccessStatusCode(); // throws on 4xx/5xx — no retry, document lost

var json = await response.Content.ReadAsStringAsync();
var parsed = JsonSerializer.Deserialize<KlippaResponse>(json); // your schema, your maintenance
var text = parsed?.Data?.ParsedDocument?.Text; // nullable chain — breaks when schema changes
// Klippa: 15+ lines of HTTP plumbing before you read a single character
var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes(imagePath)), "document", "receipt.jpg");
_client.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey); // auth header — rotates, breaks, leaks

var response = await _client.PostAsync(
    "https://custom-ocr.klippa.com/api/v1/parseDocument", content);
response.EnsureSuccessStatusCode(); // throws on 4xx/5xx — no retry, document lost

var json = await response.Content.ReadAsStringAsync();
var parsed = JsonSerializer.Deserialize<KlippaResponse>(json); // your schema, your maintenance
var text = parsed?.Data?.ParsedDocument?.Text; // nullable chain — breaks when schema changes
Imports System.Net.Http
Imports System.IO
Imports System.Text.Json
Imports System.Threading.Tasks

' Klippa: 15+ lines of HTTP plumbing before you read a single character
Dim content As New MultipartFormDataContent()
content.Add(New ByteArrayContent(File.ReadAllBytes(imagePath)), "document", "receipt.jpg")
_client.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey) ' auth header — rotates, breaks, leaks

Dim response As HttpResponseMessage = Await _client.PostAsync("https://custom-ocr.klippa.com/api/v1/parseDocument", content)
response.EnsureSuccessStatusCode() ' throws on 4xx/5xx — no retry, document lost

Dim json As String = Await response.Content.ReadAsStringAsync()
Dim parsed As KlippaResponse = JsonSerializer.Deserialize(Of KlippaResponse)(json) ' your schema, your maintenance
Dim text As String = parsed?.Data?.ParsedDocument?.Text ' nullable chain — breaks when schema changes
$vbLabelText   $csharpLabel

IronOCR取代了所有这些:

// IronOCR: no HTTP, no auth headers, no JSON — just text
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
var text = new IronTesseract().Read(imagePath).Text;
// IronOCR: no HTTP, no auth headers, no JSON — just text
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
var text = new IronTesseract().Read(imagePath).Text;
' IronOCR: no HTTP, no auth headers, no JSON — just text
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
Dim text = New IronTesseract().Read(imagePath).Text
$vbLabelText   $csharpLabel

IronOCR与 Klippa OCR:功能对比

下表从对生产环境迁移决策最重要的几个方面对这两个库进行了比较。

特征 剪切 OCR IronOCR
部署模型 仅限云端(欧盟服务器) 本地化,完全本地化
.NET SDK / NuGet包 None IronOcr NuGet包
需要互联网连接 是的,每次通话都是这样 绝不
文档数据离开网络 总是 绝不
通用OCR 不(仅限收据、发票、身份证件) 是的(任何文档类型)
身份验证设置 X-Auth-Key HTTP头 IronOcr.License.LicenseKey字符串
需要 HTTP 客户端
响应反序列化 手动解析 JSON 类型化的OcrResult对象
重试/超时逻辑 手工卷制 无需拨打(本地电话)
离线/气隙支持
PDF 输入 是的(云) 是的(本地的)
多页 TIFF 输入 未知
图像输入格式 JPG、PNG(云) JPG、PNG、BMP、TIFF、GIF 等格式
流和字节数组输入 无SDK
自动图像预处理 云侧(不透明) 是的(桌面倾斜校正、降噪、对比度调整、二值化、锐化)
结构化输出:词坐标
每个词的置信度得分
可搜索的 PDF 输出
OCR过程中的条形码读取
多语言支持 仅限已训练的文档类型 125 种以上语言
螺纹安全 不适用(HTTP 调用) 是的(一对每个线程的IronTesseract
跨平台部署 与 REST 无关 Windows、Linux、macOS、Docker、Azure、AWS
HIPAA/ITAR/物理隔离合规性
定价模式 按文档计费的SaaS(未公开价格) 来自$999的永久许可证
规模化时的每页成本 是的,无限制。 None

快速入门:Klippa OCR 到IronOCR 的迁移

步骤 1:替换 NuGet 软件包

Klippa 没有官方的NuGet包。 移除仅为支持 Klippa 集成而存在的 HTTP 客户端依赖项:

# Remove Klippa-related packages (if installed for REST support)
dotnet remove package Newtonsoft.Json
dotnet remove package System.Net.Http.Json
# Remove Klippa-related packages (if installed for REST support)
dotnet remove package Newtonsoft.Json
dotnet remove package System.Net.Http.Json
SHELL

NuGet安装IronOCR :

dotnet add package IronOcr

步骤 2:更新命名空间

移除 Klippa 集成所需的 HTTP 和 JSON 命名空间。 添加单个IronOCR命名空间:

// Before (Klippa integration)
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;

// After (IronOCR)
using IronOcr;
// Before (Klippa integration)
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;

// After (IronOCR)
using IronOcr;
Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Text.Json
Imports System.Text.Json.Serialization

Imports IronOcr
$vbLabelText   $csharpLabel

步骤 3:初始化许可证

在应用程序启动时添加许可证初始化—在Startup.cs或首次OCR调用之前:

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

代码迁移示例

替换 HTTP 客户端服务类

Klippa 集成需要一个完整的服务类来封装 HTTP 基础架构。 由于没有SDK,所以无法避免这种情况。

克利帕方法:

// Klippa: entire service class just to send one HTTP request
public class KlippaOcrService : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl = "https://custom-ocr.klippa.com/api/v1";

    public KlippaOcrService(string apiKey)
    {
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("X-Auth-Key", apiKey);
        _httpClient.Timeout = TimeSpan.FromSeconds(30); // network timeout required
    }

    public async Task<string> ReadDocumentTextAsync(string filePath)
    {
        using var form = new MultipartFormDataContent();
        var fileBytes = await File.ReadAllBytesAsync(filePath);
        form.Add(new ByteArrayContent(fileBytes), "document", Path.GetFileName(filePath));

        var response = await _httpClient.PostAsync($"{_baseUrl}/parseDocument", form);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);
        // navigate Klippa's nested JSON schema
        return doc.RootElement
            .GetProperty("data")
            .GetProperty("parsed_document")
            .GetProperty("text")
            .GetString() ?? string.Empty;
    }

    public void Dispose() => _httpClient.Dispose();
}
// Klippa: entire service class just to send one HTTP request
public class KlippaOcrService : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl = "https://custom-ocr.klippa.com/api/v1";

    public KlippaOcrService(string apiKey)
    {
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("X-Auth-Key", apiKey);
        _httpClient.Timeout = TimeSpan.FromSeconds(30); // network timeout required
    }

    public async Task<string> ReadDocumentTextAsync(string filePath)
    {
        using var form = new MultipartFormDataContent();
        var fileBytes = await File.ReadAllBytesAsync(filePath);
        form.Add(new ByteArrayContent(fileBytes), "document", Path.GetFileName(filePath));

        var response = await _httpClient.PostAsync($"{_baseUrl}/parseDocument", form);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);
        // navigate Klippa's nested JSON schema
        return doc.RootElement
            .GetProperty("data")
            .GetProperty("parsed_document")
            .GetProperty("text")
            .GetString() ?? string.Empty;
    }

    public void Dispose() => _httpClient.Dispose();
}
Imports System
Imports System.IO
Imports System.Net.Http
Imports System.Threading.Tasks
Imports System.Text.Json

' Klippa: entire service class just to send one HTTP request
Public Class KlippaOcrService
    Implements IDisposable

    Private ReadOnly _httpClient As HttpClient
    Private ReadOnly _baseUrl As String = "https://custom-ocr.klippa.com/api/v1"

    Public Sub New(apiKey As String)
        _httpClient = New HttpClient()
        _httpClient.DefaultRequestHeaders.Add("X-Auth-Key", apiKey)
        _httpClient.Timeout = TimeSpan.FromSeconds(30) ' network timeout required
    End Sub

    Public Async Function ReadDocumentTextAsync(filePath As String) As Task(Of String)
        Using form As New MultipartFormDataContent()
            Dim fileBytes = Await File.ReadAllBytesAsync(filePath)
            form.Add(New ByteArrayContent(fileBytes), "document", Path.GetFileName(filePath))

            Dim response = Await _httpClient.PostAsync($"{_baseUrl}/parseDocument", form)
            response.EnsureSuccessStatusCode()

            Dim json = Await response.Content.ReadAsStringAsync()
            Using doc = JsonDocument.Parse(json)
                ' navigate Klippa's nested JSON schema
                Return doc.RootElement _
                    .GetProperty("data") _
                    .GetProperty("parsed_document") _
                    .GetProperty("text") _
                    .GetString() OrElse String.Empty
            End Using
        End Using
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        _httpClient.Dispose()
    End Sub
End Class
$vbLabelText   $csharpLabel

IronOCR方法:

// IronOCR: no HTTP, no JSON navigation, no Dispose plumbing
public class OcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public string ReadDocumentText(string filePath)
    {
        return _ocr.Read(filePath).Text;
    }
}

// At startup:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Usage — identical call site, different internals:
var service = new OcrService();
var text = service.ReadDocumentText("invoice.jpg"); // local, synchronous, zero network
// IronOCR: no HTTP, no JSON navigation, no Dispose plumbing
public class OcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public string ReadDocumentText(string filePath)
    {
        return _ocr.Read(filePath).Text;
    }
}

// At startup:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Usage — identical call site, different internals:
var service = new OcrService();
var text = service.ReadDocumentText("invoice.jpg"); // local, synchronous, zero network
Imports IronOcr

Public Class OcrService
    Private ReadOnly _ocr As New IronTesseract()

    Public Function ReadDocumentText(filePath As String) As String
        Return _ocr.Read(filePath).Text
    End Function
End Class

' At startup:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

' Usage — identical call site, different internals:
Dim service As New OcrService()
Dim text As String = service.ReadDocumentText("invoice.jpg") ' local, synchronous, zero network
$vbLabelText   $csharpLabel

Klippa 服务类的存在完全是因为 API 需要 HTTP 基础设施。 IronOCR等价于简化为单个Read()调用。 由于没有网络连接,超时、身份验证标头和处置模式都会消失。 有关初始化选项,请参阅IronTesseract 设置指南;有关可运行的代码,请参阅基本 OCR 示例

取消多部分表单上传

Klippa 接收以多部分表单上传形式提供的文档。 上传代码虽然是机械式的,但却很脆弱:文件读取、内容类型标头、边界构建和上传大小管理。

克利帕方法:

// Klippa: multipart upload — every document is an HTTP form POST
public async Task<KlippaResult> UploadAndParseAsync(
    string filePath, string documentType = "financial")
{
    using var form = new MultipartFormDataContent();

    // read file into memory — entire document in RAM before upload
    var fileBytes = await File.ReadAllBytesAsync(filePath);
    var byteContent = new ByteArrayContent(fileBytes);
    byteContent.Headers.ContentType =
        new System.Net.Http.Headers.MediaTypeHeaderValue("image/jpeg");

    form.Add(byteContent, "document", Path.GetFileName(filePath));
    form.Add(new StringContent(documentType), "DocumentType");

    // document leaves your server here
    var response = await _httpClient.PostAsync(
        "https://custom-ocr.klippa.com/api/v1/parseDocument", form);

    if (!response.IsSuccessStatusCode)
    {
        var error = await response.Content.ReadAsStringAsync();
        throw new InvalidOperationException($"Klippa API error: {response.StatusCode} — {error}");
    }

    var json = await response.Content.ReadAsStringAsync();
    return JsonSerializer.Deserialize<KlippaResult>(json,
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
// Klippa: multipart upload — every document is an HTTP form POST
public async Task<KlippaResult> UploadAndParseAsync(
    string filePath, string documentType = "financial")
{
    using var form = new MultipartFormDataContent();

    // read file into memory — entire document in RAM before upload
    var fileBytes = await File.ReadAllBytesAsync(filePath);
    var byteContent = new ByteArrayContent(fileBytes);
    byteContent.Headers.ContentType =
        new System.Net.Http.Headers.MediaTypeHeaderValue("image/jpeg");

    form.Add(byteContent, "document", Path.GetFileName(filePath));
    form.Add(new StringContent(documentType), "DocumentType");

    // document leaves your server here
    var response = await _httpClient.PostAsync(
        "https://custom-ocr.klippa.com/api/v1/parseDocument", form);

    if (!response.IsSuccessStatusCode)
    {
        var error = await response.Content.ReadAsStringAsync();
        throw new InvalidOperationException($"Klippa API error: {response.StatusCode} — {error}");
    }

    var json = await response.Content.ReadAsStringAsync();
    return JsonSerializer.Deserialize<KlippaResult>(json,
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
Imports System.IO
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading.Tasks

Public Class KlippaUploader
    Private ReadOnly _httpClient As HttpClient

    Public Sub New(httpClient As HttpClient)
        _httpClient = httpClient
    End Sub

    Public Async Function UploadAndParseAsync(filePath As String, Optional documentType As String = "financial") As Task(Of KlippaResult)
        Using form As New MultipartFormDataContent()
            ' read file into memory — entire document in RAM before upload
            Dim fileBytes = Await File.ReadAllBytesAsync(filePath)
            Dim byteContent = New ByteArrayContent(fileBytes)
            byteContent.Headers.ContentType = New System.Net.Http.Headers.MediaTypeHeaderValue("image/jpeg")

            form.Add(byteContent, "document", Path.GetFileName(filePath))
            form.Add(New StringContent(documentType), "DocumentType")

            ' document leaves your server here
            Dim response = Await _httpClient.PostAsync("https://custom-ocr.klippa.com/api/v1/parseDocument", form)

            If Not response.IsSuccessStatusCode Then
                Dim error = Await response.Content.ReadAsStringAsync()
                Throw New InvalidOperationException($"Klippa API error: {response.StatusCode} — {error}")
            End If

            Dim json = Await response.Content.ReadAsStringAsync()
            Return JsonSerializer.Deserialize(Of KlippaResult)(json, New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True})
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR方法:

// IronOCR: load from file path, byte array, or stream — no upload, no form
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// From file path
using var input = new OcrInput();
input.LoadImage("invoice.jpg");
var result = new IronTesseract().Read(input);

// From byte array (same bytes Klippa was uploading)
byte[] fileBytes = await File.ReadAllBytesAsync("invoice.jpg");
using var inputFromBytes = new OcrInput();
inputFromBytes.LoadImage(fileBytes);
var resultFromBytes = new IronTesseract().Read(inputFromBytes);

Console.WriteLine(result.Text);
// IronOCR: load from file path, byte array, or stream — no upload, no form
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// From file path
using var input = new OcrInput();
input.LoadImage("invoice.jpg");
var result = new IronTesseract().Read(input);

// From byte array (same bytes Klippa was uploading)
byte[] fileBytes = await File.ReadAllBytesAsync("invoice.jpg");
using var inputFromBytes = new OcrInput();
inputFromBytes.LoadImage(fileBytes);
var resultFromBytes = new IronTesseract().Read(inputFromBytes);

Console.WriteLine(result.Text);
Imports IronOcr
Imports System.IO

' IronOCR: load from file path, byte array, or stream — no upload, no form
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

' From file path
Using input As New OcrInput()
    input.LoadImage("invoice.jpg")
    Dim result = New IronTesseract().Read(input)

    ' From byte array (same bytes Klippa was uploading)
    Dim fileBytes As Byte() = Await File.ReadAllBytesAsync("invoice.jpg")
    Using inputFromBytes As New OcrInput()
        inputFromBytes.LoadImage(fileBytes)
        Dim resultFromBytes = New IronTesseract().Read(inputFromBytes)

        Console.WriteLine(result.Text)
    End Using
End Using
$vbLabelText   $csharpLabel

MultipartFormDataContent构造、内容类型头和上传本身都已消失。 IronOCR直接从文件路径读取,从字节数组中读取,或者从Stream中读取—Klippa传输到云端的相同数据保留在本地。 图像输入指南涵盖所有支持的输入格式,流输入指南涵盖从上游进程以字节数组形式到达的文档的内存流路径。

替换 JSON 响应反序列化

Klippa 返回一个嵌套的 JSON 结构。 导航这种结构需要匹配的C#模型或内联JsonDocument遍历—当Klippa更改其响应架构时,两者都会失败。

克利帕方法:

// Klippa: deserialization model — breaks when API schema changes
public class KlippaResponse
{
    [JsonPropertyName("data")]
    public KlippaData Data { get; set; }
}

public class KlippaData
{
    [JsonPropertyName("parsed_document")]
    public KlippaParsedDocument ParsedDocument { get; set; }
}

public class KlippaParsedDocument
{
    [JsonPropertyName("text")]
    public string Text { get; set; }

    [JsonPropertyName("amount")]
    public decimal? Amount { get; set; }

    [JsonPropertyName("merchant")]
    public string Merchant { get; set; }

    [JsonPropertyName("date")]
    public string Date { get; set; }
}

// Usage: navigate the nullable chain every time
public async Task<string> GetExtractedTextAsync(string imagePath)
{
    var klippaResult = await UploadAndParseAsync(imagePath);
    // every property access is nullable — schema drift breaks this silently
    return klippaResult?.Data?.ParsedDocument?.Text ?? string.Empty;
}
// Klippa: deserialization model — breaks when API schema changes
public class KlippaResponse
{
    [JsonPropertyName("data")]
    public KlippaData Data { get; set; }
}

public class KlippaData
{
    [JsonPropertyName("parsed_document")]
    public KlippaParsedDocument ParsedDocument { get; set; }
}

public class KlippaParsedDocument
{
    [JsonPropertyName("text")]
    public string Text { get; set; }

    [JsonPropertyName("amount")]
    public decimal? Amount { get; set; }

    [JsonPropertyName("merchant")]
    public string Merchant { get; set; }

    [JsonPropertyName("date")]
    public string Date { get; set; }
}

// Usage: navigate the nullable chain every time
public async Task<string> GetExtractedTextAsync(string imagePath)
{
    var klippaResult = await UploadAndParseAsync(imagePath);
    // every property access is nullable — schema drift breaks this silently
    return klippaResult?.Data?.ParsedDocument?.Text ?? string.Empty;
}
Imports System.Text.Json.Serialization

' Klippa: deserialization model — breaks when API schema changes
Public Class KlippaResponse
    <JsonPropertyName("data")>
    Public Property Data As KlippaData
End Class

Public Class KlippaData
    <JsonPropertyName("parsed_document")>
    Public Property ParsedDocument As KlippaParsedDocument
End Class

Public Class KlippaParsedDocument
    <JsonPropertyName("text")>
    Public Property Text As String

    <JsonPropertyName("amount")>
    Public Property Amount As Decimal?

    <JsonPropertyName("merchant")>
    Public Property Merchant As String

    <JsonPropertyName("date")>
    Public Property Date As String
End Class

' Usage: navigate the nullable chain every time
Public Async Function GetExtractedTextAsync(imagePath As String) As Task(Of String)
    Dim klippaResult = Await UploadAndParseAsync(imagePath)
    ' every property access is nullable — schema drift breaks this silently
    Return If(klippaResult?.Data?.ParsedDocument?.Text, String.Empty)
End Function
$vbLabelText   $csharpLabel

IronOCR方法:

// IronOCR: typed result object — no JSON schema, no nullable chains
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

var ocr = new IronTesseract();
var result = ocr.Read("invoice.jpg");

// Direct property access — no deserialization, no nullable navigation
string fullText   = result.Text;
double confidence = result.Confidence;
int pageCount     = result.Pages.Count();

// Structured data: lines and words with coordinates
foreach (var page in result.Pages)
{
    foreach (var line in page.Lines)
    {
        Console.WriteLine($"Line: '{line.Text}' at Y={line.Y}");
    }
}
// IronOCR: typed result object — no JSON schema, no nullable chains
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

var ocr = new IronTesseract();
var result = ocr.Read("invoice.jpg");

// Direct property access — no deserialization, no nullable navigation
string fullText   = result.Text;
double confidence = result.Confidence;
int pageCount     = result.Pages.Count();

// Structured data: lines and words with coordinates
foreach (var page in result.Pages)
{
    foreach (var line in page.Lines)
    {
        Console.WriteLine($"Line: '{line.Text}' at Y={line.Y}");
    }
}
Imports IronOcr

' IronOCR: typed result object — no JSON schema, no nullable chains
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

Dim ocr As New IronTesseract()
Dim result = ocr.Read("invoice.jpg")

' Direct property access — no deserialization, no nullable navigation
Dim fullText As String = result.Text
Dim confidence As Double = result.Confidence
Dim pageCount As Integer = result.Pages.Count()

' Structured data: lines and words with coordinates
For Each page In result.Pages
    For Each line In page.Lines
        Console.WriteLine($"Line: '{line.Text}' at Y={line.Y}")
    Next
Next
$vbLabelText   $csharpLabel

OcrResult是一个类型化的.NET对象。 无需解析 JSON,无需维护模型类,也不存在因模式漂移而导致生产环境反序列化失败的风险。 读取结果指南记录了完整的OcrResult对象模型,包括单词坐标、置信度分数和结构化页面层次。 对于基于OcrResult的发票特定字段提取模式,发票OCR教程涵盖了端到端提取逻辑。

移除错误处理和重试基础架构

通过 HTTP 集成 Klippa 需要对网络调用可能产生的每种故障模式进行错误处理:超时、4xx 响应、5xx 响应、速率限制和部分 JSON。 运行生产集成的团队使用 Polly 或自定义逻辑添加重试策略。 当网络呼叫消失时,该基础设施也会消失。

克利帕方法:

// Klippa: retry policy required — cloud calls fail unpredictably
public async Task<string> ReadWithRetryAsync(string filePath, int maxRetries = 3)
{
    var delay = TimeSpan.FromSeconds(1);

    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            using var form = new MultipartFormDataContent();
            form.Add(
                new ByteArrayContent(await File.ReadAllBytesAsync(filePath)),
                "document",
                Path.GetFileName(filePath));

            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
            var response = await _httpClient.PostAsync(
                "https://custom-ocr.klippa.com/api/v1/parseDocument", form, cts.Token);

            if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
            {
                // rate limited — back off and retry
                await Task.Delay(delay * attempt);
                continue;
            }

            response.EnsureSuccessStatusCode();
            var json = await response.Content.ReadAsStringAsync(cts.Token);
            var parsed = JsonSerializer.Deserialize<KlippaResponse>(json);
            return parsed?.Data?.ParsedDocument?.Text ?? string.Empty;
        }
        catch (HttpRequestException) when (attempt < maxRetries)
        {
            await Task.Delay(delay * attempt); // exponential backoff
        }
        catch (TaskCanceledException) when (attempt < maxRetries)
        {
            await Task.Delay(delay * attempt); // timeout — retry
        }
    }

    throw new InvalidOperationException($"Klippa API failed after {maxRetries} attempts");
}
// Klippa: retry policy required — cloud calls fail unpredictably
public async Task<string> ReadWithRetryAsync(string filePath, int maxRetries = 3)
{
    var delay = TimeSpan.FromSeconds(1);

    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            using var form = new MultipartFormDataContent();
            form.Add(
                new ByteArrayContent(await File.ReadAllBytesAsync(filePath)),
                "document",
                Path.GetFileName(filePath));

            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
            var response = await _httpClient.PostAsync(
                "https://custom-ocr.klippa.com/api/v1/parseDocument", form, cts.Token);

            if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
            {
                // rate limited — back off and retry
                await Task.Delay(delay * attempt);
                continue;
            }

            response.EnsureSuccessStatusCode();
            var json = await response.Content.ReadAsStringAsync(cts.Token);
            var parsed = JsonSerializer.Deserialize<KlippaResponse>(json);
            return parsed?.Data?.ParsedDocument?.Text ?? string.Empty;
        }
        catch (HttpRequestException) when (attempt < maxRetries)
        {
            await Task.Delay(delay * attempt); // exponential backoff
        }
        catch (TaskCanceledException) when (attempt < maxRetries)
        {
            await Task.Delay(delay * attempt); // timeout — retry
        }
    }

    throw new InvalidOperationException($"Klippa API failed after {maxRetries} attempts");
}
Imports System
Imports System.IO
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading
Imports System.Threading.Tasks

Public Class KlippaService
    Private ReadOnly _httpClient As HttpClient

    Public Sub New(httpClient As HttpClient)
        _httpClient = httpClient
    End Sub

    ' Klippa: retry policy required — cloud calls fail unpredictably
    Public Async Function ReadWithRetryAsync(filePath As String, Optional maxRetries As Integer = 3) As Task(Of String)
        Dim delay As TimeSpan = TimeSpan.FromSeconds(1)

        For attempt As Integer = 1 To maxRetries
            Try
                Using form As New MultipartFormDataContent()
                    form.Add(New ByteArrayContent(Await File.ReadAllBytesAsync(filePath)), "document", Path.GetFileName(filePath))

                    Using cts As New CancellationTokenSource(TimeSpan.FromSeconds(30))
                        Dim response As HttpResponseMessage = Await _httpClient.PostAsync("https://custom-ocr.klippa.com/api/v1/parseDocument", form, cts.Token)

                        If response.StatusCode = System.Net.HttpStatusCode.TooManyRequests Then
                            ' rate limited — back off and retry
                            Await Task.Delay(delay * attempt)
                            Continue For
                        End If

                        response.EnsureSuccessStatusCode()
                        Dim json As String = Await response.Content.ReadAsStringAsync(cts.Token)
                        Dim parsed As KlippaResponse = JsonSerializer.Deserialize(Of KlippaResponse)(json)
                        Return If(parsed?.Data?.ParsedDocument?.Text, String.Empty)
                    End Using
                End Using
            Catch ex As HttpRequestException When attempt < maxRetries
                Await Task.Delay(delay * attempt) ' exponential backoff
            Catch ex As TaskCanceledException When attempt < maxRetries
                Await Task.Delay(delay * attempt) ' timeout — retry
            End Try
        Next

        Throw New InvalidOperationException($"Klippa API failed after {maxRetries} attempts")
    End Function
End Class

Public Class KlippaResponse
    Public Property Data As KlippaData
End Class

Public Class KlippaData
    Public Property ParsedDocument As ParsedDocument
End Class

Public Class ParsedDocument
    Public Property Text As String
End Class
$vbLabelText   $csharpLabel

IronOCR方法:

// IronOCR: no network, no retry policy needed — local call either works or throws once
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

public string ReadDocument(string filePath)
{
    // 否 retry loop. 否 CancellationTokenSource. 否 HTTP status checks.
    // 否 rate limit handling. 否 partial-JSON guards.
    var result = new IronTesseract().Read(filePath);
    return result.Text;
}
// IronOCR: no network, no retry policy needed — local call either works or throws once
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

public string ReadDocument(string filePath)
{
    // 否 retry loop. 否 CancellationTokenSource. 否 HTTP status checks.
    // 否 rate limit handling. 否 partial-JSON guards.
    var result = new IronTesseract().Read(filePath);
    return result.Text;
}
Imports IronOcr

' IronOCR: no network, no retry policy needed — local call either works or throws once
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

Public Function ReadDocument(filePath As String) As String
    ' 否 retry loop. 否 CancellationTokenSource. 否 HTTP status checks.
    ' 否 rate limit handling. 否 partial-JSON guards.
    Dim result = New IronTesseract().Read(filePath)
    Return result.Text
End Function
$vbLabelText   $csharpLabel

整个重试基础设施—循环、延迟计算、TaskCanceledException捕获块—仅仅因为网络而存在。 移除网络通话后,所有问题都会消失。 如果输入文件缺失或无法读取,本地 OCR 调用会快速失败并抛出异常;否则,调用会成功。如果迁移后吞吐量成为问题,速度优化指南将介绍IronOCR 的性能调优方法。

无需云上传即可处理多页 PDF

Klippa通过相同的parseDocument端点接受PDF上传。 多页PDF文件仍然会从您的网络中传输出去。 IronOCR可直接读取 PDF 文件,并支持逐页访问结果。

克利帕方法:

// Klippa: PDF upload — entire document transmitted, results depend on cloud availability
public async Task<List<string>> ExtractPdfPagesAsync(string pdfPath)
{
    var pages = new List<string>();

    // Klippa parses the entire PDF server-side and returns combined results
    // You cannot control per-page processing or access raw page text
    using var form = new MultipartFormDataContent();
    form.Add(
        new ByteArrayContent(await File.ReadAllBytesAsync(pdfPath)),
        "document",
        Path.GetFileName(pdfPath));

    var response = await _httpClient.PostAsync(
        "https://custom-ocr.klippa.com/api/v1/parseDocument", form);
    response.EnsureSuccessStatusCode();

    var json = await response.Content.ReadAsStringAsync();
    var result = JsonSerializer.Deserialize<KlippaResponse>(json);
    // Klippa returns the combined parsed text — no per-page breakdown in basic API
    pages.Add(result?.Data?.ParsedDocument?.Text ?? string.Empty);

    return pages;
}
// Klippa: PDF upload — entire document transmitted, results depend on cloud availability
public async Task<List<string>> ExtractPdfPagesAsync(string pdfPath)
{
    var pages = new List<string>();

    // Klippa parses the entire PDF server-side and returns combined results
    // You cannot control per-page processing or access raw page text
    using var form = new MultipartFormDataContent();
    form.Add(
        new ByteArrayContent(await File.ReadAllBytesAsync(pdfPath)),
        "document",
        Path.GetFileName(pdfPath));

    var response = await _httpClient.PostAsync(
        "https://custom-ocr.klippa.com/api/v1/parseDocument", form);
    response.EnsureSuccessStatusCode();

    var json = await response.Content.ReadAsStringAsync();
    var result = JsonSerializer.Deserialize<KlippaResponse>(json);
    // Klippa returns the combined parsed text — no per-page breakdown in basic API
    pages.Add(result?.Data?.ParsedDocument?.Text ?? string.Empty);

    return pages;
}
Imports System.IO
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading.Tasks

Public Class PdfExtractor
    Private ReadOnly _httpClient As HttpClient

    Public Sub New(httpClient As HttpClient)
        _httpClient = httpClient
    End Sub

    ' Klippa: PDF upload — entire document transmitted, results depend on cloud availability
    Public Async Function ExtractPdfPagesAsync(pdfPath As String) As Task(Of List(Of String))
        Dim pages As New List(Of String)()

        ' Klippa parses the entire PDF server-side and returns combined results
        ' You cannot control per-page processing or access raw page text
        Using form As New MultipartFormDataContent()
            form.Add(New ByteArrayContent(Await File.ReadAllBytesAsync(pdfPath)), "document", Path.GetFileName(pdfPath))

            Dim response = Await _httpClient.PostAsync("https://custom-ocr.klippa.com/api/v1/parseDocument", form)
            response.EnsureSuccessStatusCode()

            Dim json = Await response.Content.ReadAsStringAsync()
            Dim result = JsonSerializer.Deserialize(Of KlippaResponse)(json)
            ' Klippa returns the combined parsed text — no per-page breakdown in basic API
            pages.Add(If(result?.Data?.ParsedDocument?.Text, String.Empty))
        End Using

        Return pages
    End Function
End Class

Public Class KlippaResponse
    Public Property Data As KlippaData
End Class

Public Class KlippaData
    Public Property ParsedDocument As ParsedDocument
End Class

Public Class ParsedDocument
    Public Property Text As String
End Class
$vbLabelText   $csharpLabel

IronOCR方法:

// IronOCR: native PDF OCR with per-page structured access — no upload
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

using var input = new OcrInput();
input.LoadPdf("multi-page-invoice.pdf"); // reads locally — no HTTP

var ocr = new IronTesseract();
var result = ocr.Read(input);

// Per-page access — not available from Klippa's combined response
foreach (var page in result.Pages)
{
    Console.WriteLine($"Page {page.PageNumber}: {page.Lines.Count()} lines");
    Console.WriteLine(page.Text);
}

// Or produce a searchable PDF from the scanned original
result.SaveAsSearchablePdf("searchable-output.pdf");
// IronOCR: native PDF OCR with per-page structured access — no upload
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

using var input = new OcrInput();
input.LoadPdf("multi-page-invoice.pdf"); // reads locally — no HTTP

var ocr = new IronTesseract();
var result = ocr.Read(input);

// Per-page access — not available from Klippa's combined response
foreach (var page in result.Pages)
{
    Console.WriteLine($"Page {page.PageNumber}: {page.Lines.Count()} lines");
    Console.WriteLine(page.Text);
}

// Or produce a searchable PDF from the scanned original
result.SaveAsSearchablePdf("searchable-output.pdf");
Imports IronOcr

' IronOCR: native PDF OCR with per-page structured access — no upload
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

Using input As New OcrInput()
    input.LoadPdf("multi-page-invoice.pdf") ' reads locally — no HTTP

    Dim ocr As New IronTesseract()
    Dim result = ocr.Read(input)

    ' Per-page access — not available from Klippa's combined response
    For Each page In result.Pages
        Console.WriteLine($"Page {page.PageNumber}: {page.Lines.Count()} lines")
        Console.WriteLine(page.Text)
    Next

    ' Or produce a searchable PDF from the scanned original
    result.SaveAsSearchablePdf("searchable-output.pdf")
End Using
$vbLabelText   $csharpLabel

IronOCR可直接读取 PDF 文件,无需任何转换步骤。每一页都可单独访问,包含完整的行、单词和字符层级结构。 SaveAsSearchablePdf()调用从扫描文档生成文本层PDF—这是Klippa不提供的功能。 PDF 输入指南涵盖加载选项,可搜索 PDF 指南涵盖输出选项,包括符合存档要求的 PDF/A。

剪切 OCR API 到IronOCR映射参考

Klippa 是一个 REST API,而不是一个类型化的 SDK。 下面这张映射图将 Klippa 的积分曲面转换为IronOCR等效曲面。

Klippa概念 IronOCR当量
X-Auth-Key IronTesseract实例—无需身份验证设置
MultipartFormDataContent OcrInput.LoadPdf(path)
POST /api/v1/parseDocument IronTesseract.Read(input)
await _client.PostAsync(...) await
response.EnsureSuccessStatusCode() 无需响应——无HTTP响应
JsonSerializer.Deserialize<KlippaResponse>(json) 类型化的OcrResult—无需反序列化
KlippaResponse.Data.ParsedDocument.Text OcrResult.Text
KlippaResponse.Data.ParsedDocument.Amount 自定义正则表达式作用于OcrResult.Lines
KlippaResponse.Data.ParsedDocument.Merchant OcrResult.Pages[0].Lines[0].Text
具有Task.Delay的重试循环 无需网络故障模式
CancellationTokenSource(TimeSpan.FromSeconds(30)) 无需执行——本地执行
速率限制处理(HTTP 429) 无需限制——无速率限制
云文档路由到欧盟服务器 本地进程内执行
KlippaService.Dispose() / HttpClient.Dispose() using语句处理清理
结构化 JSON 响应字段 OcrResult.Text + OcrResult.Pages + OcrResult.Words
SaaS API 订阅 IronOcr.License.LicenseKey字符串—永久

常见迁移问题和解决方案

问题 1:HTTP 移除后的仅异步调用站点

Klippa:所有 Klippa 集成都是异步的,因为 HTTP 调用需要异步操作。 控制器、服务和整个代码库中的后台工作线程调用await ProcessDocumentAsync(...)。 删除HTTP调用意味着不再需要async方法签名仍然存在。

解决方案:IronOCR提供同步和异步API。对于必须保持异步的调用点(ASP.NET Core控制器、具有ReadAsync

// Keep async method signatures — switch the implementation
public async Task<string> ProcessDocumentAsync(
    string filePath, CancellationToken cancellationToken = default)
{
    // Previously: await _httpClient.PostAsync(...)
    // Now: local call, same awaitable pattern
    var ocr = new IronTesseract();
    var result = await ocr.ReadAsync(filePath);
    return result.Text;
}
// Keep async method signatures — switch the implementation
public async Task<string> ProcessDocumentAsync(
    string filePath, CancellationToken cancellationToken = default)
{
    // Previously: await _httpClient.PostAsync(...)
    // Now: local call, same awaitable pattern
    var ocr = new IronTesseract();
    var result = await ocr.ReadAsync(filePath);
    return result.Text;
}
Imports System.Threading
Imports System.Threading.Tasks

Public Class DocumentProcessor
    Public Async Function ProcessDocumentAsync(filePath As String, Optional cancellationToken As CancellationToken = Nothing) As Task(Of String)
        Dim ocr As New IronTesseract()
        Dim result = Await ocr.ReadAsync(filePath)
        Return result.Text
    End Function
End Class
$vbLabelText   $csharpLabel

异步OCR指南涵盖了CancellationToken在ASP.NET Core和托管服务模式中的集成。

问题 2:依赖注入注册

Klippa:HttpClient。 移除它意味着更新 DI 注册和所有喷射点。

解决方案:IronTesseract注册为单例(它是线程安全的)并直接注入,或创建一个薄的包装器以镜像现有的服务接口:

// In Program.cs or Startup.cs
builder.Services.AddSingleton<IronTesseract>();

// Or wrap for interface compatibility
builder.Services.AddSingleton<IOcrService, IronOcrService>();

public class IronOcrService : IOcrService
{
    private readonly IronTesseract _ocr;
    public IronOcrService(IronTesseract ocr) => _ocr = ocr;

    public string ReadDocument(string path) => _ocr.Read(path).Text;
}
// In Program.cs or Startup.cs
builder.Services.AddSingleton<IronTesseract>();

// Or wrap for interface compatibility
builder.Services.AddSingleton<IOcrService, IronOcrService>();

public class IronOcrService : IOcrService
{
    private readonly IronTesseract _ocr;
    public IronOcrService(IronTesseract ocr) => _ocr = ocr;

    public string ReadDocument(string path) => _ocr.Read(path).Text;
}
Imports Microsoft.Extensions.DependencyInjection

' In Program.vb or Startup.vb
builder.Services.AddSingleton(Of IronTesseract)()

' Or wrap for interface compatibility
builder.Services.AddSingleton(Of IOcrService, IronOcrService)()

Public Class IronOcrService
    Implements IOcrService

    Private ReadOnly _ocr As IronTesseract

    Public Sub New(ocr As IronTesseract)
        _ocr = ocr
    End Sub

    Public Function ReadDocument(path As String) As String Implements IOcrService.ReadDocument
        Return _ocr.Read(path).Text
    End Function
End Class
$vbLabelText   $csharpLabel

一个注册为单例的IronTesseract实例处理并发请求。 每次调用Read()都是线程安全的。

问题 3:无需预解析 JSON 即可提取结构化字段

Klippa:Klippa返回vat_amount作为类型化的JSON属性。 迁移到IronOCR意味着这些字段不再预先解析。

解决方案:IronOCR的OcrResult提供原始文本和单词级坐标以构建等效提取。 对于布局可预测的文档,基于区域的 OCR 可以直接识别特定字段:

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Target specific layout regions instead of relying on pre-parsed cloud fields
var totalRegion   = new CropRectangle(350, 580, 250, 50); // bottom-right total area
var merchantRegion = new CropRectangle(50, 30, 400, 60);  // top header area

using var merchantInput = new OcrInput();
merchantInput.LoadImage("receipt.jpg", merchantRegion);
var merchantName = new IronTesseract().Read(merchantInput).Text.Trim();

using var totalInput = new OcrInput();
totalInput.LoadImage("receipt.jpg", totalRegion);
var totalText = new IronTesseract().Read(totalInput).Text.Trim();
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Target specific layout regions instead of relying on pre-parsed cloud fields
var totalRegion   = new CropRectangle(350, 580, 250, 50); // bottom-right total area
var merchantRegion = new CropRectangle(50, 30, 400, 60);  // top header area

using var merchantInput = new OcrInput();
merchantInput.LoadImage("receipt.jpg", merchantRegion);
var merchantName = new IronTesseract().Read(merchantInput).Text.Trim();

using var totalInput = new OcrInput();
totalInput.LoadImage("receipt.jpg", totalRegion);
var totalText = new IronTesseract().Read(totalInput).Text.Trim();
Imports IronOcr

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

' Target specific layout regions instead of relying on pre-parsed cloud fields
Dim totalRegion As New CropRectangle(350, 580, 250, 50) ' bottom-right total area
Dim merchantRegion As New CropRectangle(50, 30, 400, 60) ' top header area

Using merchantInput As New OcrInput()
    merchantInput.LoadImage("receipt.jpg", merchantRegion)
    Dim merchantName As String = New IronTesseract().Read(merchantInput).Text.Trim()
End Using

Using totalInput As New OcrInput()
    totalInput.LoadImage("receipt.jpg", totalRegion)
    Dim totalText As String = New IronTesseract().Read(totalInput).Text.Trim()
End Using
$vbLabelText   $csharpLabel

基于区域的OCR指南详细介绍CropRectangle的用法。 收据扫描教程提供了完整的可运行代码,用于提取收据和发票布局的完整模式。

问题 4:来自上游服务的流式文档

Klippa: Klippa 接收以 multipart 表单上传形式提供的文档——文件字节封装在 HTTP 表单内容中。 如果您的应用程序从 S3、Azure Blob 存储或内部 API 接收流式文档,那么您之前是将流读取为字节,然后将这些字节上传到 Klippa。

解决方案:IronOCR直接接受Stream对象。 字节转换步骤消失了:

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Stream from S3, Azure Blob, or any upstream source
public async Task<string> ProcessDocumentStreamAsync(Stream documentStream)
{
    using var input = new OcrInput();
    input.LoadImage(documentStream); // accepts Stream directly

    var ocr = new IronTesseract();
    var result = ocr.Read(input);
    return result.Text;
}
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Stream from S3, Azure Blob, or any upstream source
public async Task<string> ProcessDocumentStreamAsync(Stream documentStream)
{
    using var input = new OcrInput();
    input.LoadImage(documentStream); // accepts Stream directly

    var ocr = new IronTesseract();
    var result = ocr.Read(input);
    return result.Text;
}
Imports System.IO
Imports System.Threading.Tasks
Imports IronOcr

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

Public Async Function ProcessDocumentStreamAsync(documentStream As Stream) As Task(Of String)
    Using input As New OcrInput()
        input.LoadImage(documentStream) ' accepts Stream directly

        Dim ocr As New IronTesseract()
        Dim result = ocr.Read(input)
        Return result.Text
    End Using
End Function
$vbLabelText   $csharpLabel

没有MultipartFormDataContent构造,没有HTTP POST。流直接进入OcrInput河流输入指南涵盖河流类型和排放模式。

问题 5:依赖 HTTP 模拟的集成测试

Klippa:Klippa代码的集成测试模拟MockHttp)来模拟API响应。 这些测试模拟的是 HTTP 层,而不是 OCR 逻辑。

解决方案: IronOCR测试使用具有已知预期输出的真实文档。 无需模拟基础设施。 测试离线运行:

[Fact]
public void ReadDocument_ReturnsExpectedText()
{
    IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
    var ocr = new IronTesseract();

    // Use a real test fixture — no HTTP mocking, runs fully offline
    var result = ocr.Read("test-fixtures/sample-invoice.jpg");

    Assert.Contains("Invoice", result.Text, StringComparison.OrdinalIgnoreCase);
    Assert.True(result.Confidence > 70);
}
[Fact]
public void ReadDocument_ReturnsExpectedText()
{
    IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
    var ocr = new IronTesseract();

    // Use a real test fixture — no HTTP mocking, runs fully offline
    var result = ocr.Read("test-fixtures/sample-invoice.jpg");

    Assert.Contains("Invoice", result.Text, StringComparison.OrdinalIgnoreCase);
    Assert.True(result.Confidence > 70);
}
<Fact>
Public Sub ReadDocument_ReturnsExpectedText()
    IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
    Dim ocr = New IronTesseract()

    ' Use a real test fixture — no HTTP mocking, runs fully offline
    Dim result = ocr.Read("test-fixtures/sample-invoice.jpg")

    Assert.Contains("Invoice", result.Text, StringComparison.OrdinalIgnoreCase)
    Assert.True(result.Confidence > 70)
End Sub
$vbLabelText   $csharpLabel

以前需要实时 Klippa 连接或复杂的 HTTP 模拟设置的测试,现在无需网络访问即可在 CI 中运行。

问题 6:Klippa 服务器端增强的低质量文档

Klippa:云处理在识别之前会应用图像增强技术。 开发者无需进行任何配置——这在 Klippa 的服务器上会自动完成。 迁移时,Klippa 静默处理的文档如果没有在IronOCR中进行显式预处理,可能会产生较低的准确率。

解决方案:显式应用IronOCR的预处理过滤器。 过滤器集反映了云服务在服务器端应用的过滤器集:

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");
input.Deskew();    // fix rotation from camera or scanner
input.DeNoise();   // remove compression noise
input.Contrast();  // boost faded ink
input.Binarize();  // clean background for clearer character edges

var result = new IronTesseract().Read(input);
Console.WriteLine($"Confidence: {result.Confidence}%");
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");
input.Deskew();    // fix rotation from camera or scanner
input.DeNoise();   // remove compression noise
input.Contrast();  // boost faded ink
input.Binarize();  // clean background for clearer character edges

var result = new IronTesseract().Read(input);
Console.WriteLine($"Confidence: {result.Confidence}%");
Imports IronOcr

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

Using input As New OcrInput()
    input.LoadImage("low-quality-scan.jpg")
    input.Deskew()    ' fix rotation from camera or scanner
    input.DeNoise()   ' remove compression noise
    input.Contrast()  ' boost faded ink
    input.Binarize()  ' clean background for clearer character edges

    Dim result = New IronTesseract().Read(input)
    Console.WriteLine($"Confidence: {result.Confidence}%")
End Using
$vbLabelText   $csharpLabel

图像质量校正指南涵盖所有预处理滤镜以及针对不同文档劣化类型应用这些滤镜的顺序。

剪切 OCR 迁移清单

迁移前

在删除任何内容之前,请先检查您的代码库,找出所有 Klippa 特有的代码:

# Find all files containing Klippa HTTP integration code
grep -r "X-Auth-Key" --include="*.cs" .
grep -r "klippa.com" --include="*.cs" .
grep -r "KlippaService\|KlippaResponse\|KlippaResult\|KlippaData" --include="*.cs" .

# Find all files with MultipartFormDataContent (likely Klippa upload code)
grep -r "MultipartFormDataContent" --include="*.cs" .

# Find all JSON deserialization models that map to Klippa response fields
grep -r "parsed_document\|vat_amount\|merchant\|X-Auth-Key" --include="*.cs" .

# Find all async methods that wrap Klippa calls
grep -r "ParseDocumentAsync\|ProcessReceiptAsync\|UploadAndParseAsync" --include="*.cs" .

# Find test files with HTTP mocks for Klippa
grep -r "MockHttp\|WireMock\|klippa" --include="*.cs" .
# Find all files containing Klippa HTTP integration code
grep -r "X-Auth-Key" --include="*.cs" .
grep -r "klippa.com" --include="*.cs" .
grep -r "KlippaService\|KlippaResponse\|KlippaResult\|KlippaData" --include="*.cs" .

# Find all files with MultipartFormDataContent (likely Klippa upload code)
grep -r "MultipartFormDataContent" --include="*.cs" .

# Find all JSON deserialization models that map to Klippa response fields
grep -r "parsed_document\|vat_amount\|merchant\|X-Auth-Key" --include="*.cs" .

# Find all async methods that wrap Klippa calls
grep -r "ParseDocumentAsync\|ProcessReceiptAsync\|UploadAndParseAsync" --include="*.cs" .

# Find test files with HTTP mocks for Klippa
grep -r "MockHttp\|WireMock\|klippa" --include="*.cs" .
SHELL

库存记录:

  • 记录每个包装HttpClient用于Klippa调用的类
  • 列出所有JSON反序列化模型类(KlippaParsedDocument等)
  • 记录所有使用 Klippa 预解析 JSON 属性的字段映射。
  • 注意任何 Polly 重试策略或为 Klippa 构建的自定义重试循环

代码迁移

  1. 安装IronOcr NuGet包(dotnet add package IronOcr
  2. IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"添加到应用程序启动中
  3. 从Klippa服务文件中移除Newtonsoft.Json导入
  4. 删除IronTesseract调用替换其主体,保留接口)
  5. 在DI容器中将IronTesseract注册为单例
  6. OcrInput.LoadPdf()
  7. 删除JSON响应模型类(KlippaParsedDocument
  8. 将可空JSON导航链(result.Text
  9. 移除Klippa调用点的重试循环和CancellationTokenSource超时
  10. 移除速率限制处理(HTTP 429 捕获块)
  11. ocr.Read(...)
  12. 为低质量的文档输入添加Contrast
  13. 用真实文档测试用例替换 HTTP 模拟测试基础设施
  14. 删除 Polly 重试策略或作用于 Klippa 调用的自定义重试中间件

后迁移

  • 验证文本提取输出是否与已知测试文档中的预期内容相符
  • 确认生产文档类型的置信度评分超过可接受阈值(通常为 70% 以上)
  • 测试PDF输入:本地加载多页PDF并通过result.Pages验证每页文本访问
  • 测试流输入:传递OcrInput.LoadImage(stream)产生正确的输出
  • 验证预处理滤波器是否能提高低质量扫描的准确率,与未经处理的基线相比。
  • 确认DI注入的IronTesseract单例处理并发请求而无争用
  • 离线运行集成测试(无网络连接)——所有测试都应在不访问云的情况下通过
  • 使用result.SaveAsSearchablePdf("output.pdf")验证扫描文档流程的可搜索PDF输出
  • 在ASP.NET Core控制器上下文中测试CancellationToken
  • 确认using var input = new OcrInput()处理模式在持续负载下不泄漏内存

迁移到IronOCR的主要优势

从一开始就保障数据主权。迁移后,敏感财务文件、身份扫描件和机密发票永远不会离开您的基础架构。 审计范围内没有第三方处理者,没有数据保留政策需要审查,也没有数据传输协议需要维护。 以前使 Klippa 出现问题的 HIPAA、ITAR、CMMC 和 FedRAMP 限制现在默认得到满足。 在DockerAWSAzure上部署可以将所有内容都控制在您自己的基础设施边界内。

基础设施复杂性得以消除。服务类、HTTP客户端、表单上传代码、JSON模型、重试策略、超时配置——所有这些都只是为了封装一个网络调用。 移除网络呼叫,所有相关数据也会随之消失。 生成的代码库更小、更易读,并且故障模式更少。 通过DI注入的单个IronTesseract实例替换整个HTTP集成层。

无论卷是多少,成本都是可预测的。一个在$999(Lite),$1,499(Professional)或$2,999(Enterprise)的永久IronOCR许可证涵盖无限量的文档处理。 每月处理 500 份文件和每月处理 50 万份文件的成本相同。 Klippa 曾经因按文档计费而导致规模化成本高昂,但这种计费方式在结构上已经不存在了。 IronOCR许可页面详细列出了所有级别以及每个级别包含的内容。

文档范围不受限制。IronOCR可以处理任何包含文本的文档。 扫描的合同、技术图纸、医疗表格、购买订单、手写便条、截图、TIFF存档—全都由相同的Read()调用使用相同的API处理。 之前针对 Klippa 培训类别之外的文件需要第二个系统的专业范围限制已经取消。 一个库,一个集成点,支持任何文档类型。

现在支持离线和受限网络环境。部署在银行网络、政府系统、边缘环境或任何出站流量受限的基础设施中的应用程序,其运行方式与在开放环境中完全相同。 没有连接性检查,没有对云端点进行健康检查,也没有在互联网不可用时进入降级模式。 物理隔离部署无需修改即可正常工作。 Linux 部署指南Docker 部署指南涵盖了这些环境的容器化和服务器端部署路径。

完全掌控图像增强。云端预处理就像一个黑匣子——Klippa 应用预处理,你只能观察结果,无法调整任何参数。 IronOCR的预处理管道是明确和可组合的:DeepCleanBackgroundNoise()。 每个过滤器都是可选的,需要按顺序排列。 准确率的提高是可衡量的、可重复的,并且在您的控制之下。 图像质量校正指南预处理功能页面涵盖了完整的滤镜目录,并指导何时应用每个滤镜。

请注意Klippa和Tesseract是其各自所有者的注册商标。 本网站与Google或Klippa无关系、未获授权或赞助。 所有产品名称、徽标和品牌均为各自所有者的财产。 比较仅供参考,反映撰写时公开可用的信息。

常见问题解答

我为什么要从Klippa OCR API迁移到IronOCR?

常见的驱动因素包括消除 COM 互操作的复杂性、取代基于文件的许可证管理、避免按页计费、启用 Docker/容器部署以及采用与标准 .NET 工具集成的 NuGet 原生工作流程。

从 Klippa OCR API 迁移到 IronOCR 时,主要的代码变更有哪些?

将 Klippa 初始化序列替换为 IronTesseract 实例化,移除 COM 生命周期管理(显式的创建/加载/关闭模式),并更新结果属性名称。这样可以显著减少样板代码行数。

我该如何安装 IronOCR 以开始迁移?

在软件包管理器控制台中运行“Install-Package IronOcr”,或在命令行界面中运行“dotnet add package IronOcr”。语言包是单独的软件包:例如,法语语言包需要运行“dotnet add package IronOcr.Languages.French”。

IronOCR 在标准商业文档的 OCR 准确率方面是否能与 Klippa OCR API 相媲美?

IronOCR 对标准商业内容(包括发票、合同、收据和打印表格)的识别准确率很高。图像预处理滤镜(例如去倾斜、去噪、对比度增强)可进一步提高对质量较差输入文件的识别率。

IronOCR 如何处理 Klippa OCR API 单独安装的语言数据?

IronOCR 中的语言数据以 NuGet 包的形式分发。“dotnet add package IronOcr.Languages.German”命令用于安装德语支持。无需手动放置文件或指定目录路径。

从 Klippa OCR API 迁移到 IronOCR 是否需要更改部署基础架构?

IronOCR 比 Klippa OCR API 需要的底层架构变更更少。它无需 SDK 二进制文件路径、许可证文件放置位置或许可证服务器配置。NuGet 包包含完整的 OCR 引擎,许可证密钥是应用程序代码中设置的字符串。

迁移后如何配置 IronOCR 许可?

在应用程序启动代码中,将 IronOcr.License.LicenseKey 赋值为 "YOUR-KEY"。在 Docker 或 Kubernetes 中,将此密钥存储为环境变量,并在启动时读取。使用 License.IsValidLicense 在接受流量之前进行验证。

IronOCR 能否像 Klippa 一样处理 PDF 文件?

是的。IronOCR 可以读取原生 PDF 和扫描版 PDF。实例化 IronTesseract,调用 ocr.Read(input) 方法(其中 input 可以是 PDF 路径或 OcrPdfInput),然后遍历 OcrResult 页面。无需单独的 PDF 渲染流程。

IronOCR 如何处理大批量处理中的线程问题?

IronTesseract 可以安全地按线程实例化。在 Parallel.ForEach 或 Task 池中为每个线程启动一个实例,并发运行 OCR,并在完成后释放每个实例。无需全局状态或锁定。

IronOCR在提取文本后支持哪些输出格式?

IronOCR 返回结构化结果,包括文本、词坐标、置信度评分和页面结构。导出选项包括纯文本、可搜索 PDF 和用于下游处理的结构化结果对象。

对于可扩展的工作负载而言,IronOCR 的定价是否比 Klippa OCR API 更具可预测性?

IronOCR采用永久统一费率许可模式,不收取任何页数或数量费用。无论您处理1万页还是1000万页,许可费用都保持不变。批量许可和团队许可选项请参见IronOCR定价页面。

从 Klippa OCR API 迁移到 IronOCR 后,我现有的测试会发生什么变化?

迁移后,对提取的文本内容进行断言的测试应该继续通过。验证 API 调用模式或 COM 对象生命周期的测试需要更新,以反映 IronOCR 更简化的初始化和结果模型。

Kannaopat Udonpant
软件工程师
在成为软件工程师之前,Kannapat 在日本北海道大学完成了环境资源博士学位。在攻读学位期间,Kannapat 还成为了车辆机器人实验室的成员,隶属于生物生产工程系。2022 年,他利用自己的 C# 技能加入 Iron Software 的工程团队,专注于 IronPDF。Kannapat 珍视他的工作,因为他可以直接从编写大多数 IronPDF 代码的开发者那里学习。除了同行学习外,Kannapat 还喜欢在 Iron Software 工作的社交方面。不撰写代码或文档时,Kannapat 通常可以在他的 PS5 上玩游戏或重温《最后生还者》。

钢铁支援团队

我们每周 5 天,每天 24 小时在线。
聊天
电子邮件
打电话给我