오픈소스 라이브러리를 사용하여 C#에서 OCR을 구현하는 방법
이 가이드는 Klippa의 REST API를 통합한 후 온프레미스 문서 처리를 위해 IronOCR 로 전환하는 .NET 개발자를 위한 것입니다. 이 문서에서는 HTTP 클라이언트 인프라를 제거하고, JSON 역직렬화를 없애고, 클라우드에 의존하는 문서 업로드를 네트워크에 전혀 연결하지 않는 로컬 OCR 호출로 대체하는 실질적인 단계를 다룹니다.
Klippa OCR에서 마이그레이션해야 하는 이유는 무엇일까요?
Klippa는 .NET SDK를 제공하지 않는 클라우드 전용 문서 인텔리전스 서비스입니다. 모든 통합은 수작업으로 개발된 REST 클라이언트입니다. 그러한 구조적 현실은 생산 시스템의 수명 주기 동안 누적되는 연쇄적인 결과를 초래합니다.
NuGet 패키지가 없다는 것은 통합 레이어를 직접 소유하고 있다는 의미입니다. 설치할 것이 아무것도 없습니다. 입장 비용은 HttpClient 래퍼 작성, X-Auth-Key 인증 헤더 구성, MultipartFormDataContent 요청 본문 작성, Klippa의 JSON 응답 스키마 역직렬화, 그리고 일시적인 오류에 대한 재시도 로직 연결입니다. 첫 번째 문서가 프로덕션 환경에서 안정적으로 처리되기까지 2~4일 정도의 파이프라인 구축 작업이 필요합니다. Klippa가 API 스키마를 업데이트하면 역직렬화 코드가 제대로 작동하지 않아 수동으로 수정해야 합니다.
모든 문서 업로드는 네트워크 연결에 의존합니다. Klippa는 EU 내 서버에서만 문서를 처리합니다. Klippa 측의 생산 중단, 지연 시간 증가 또는 애플리케이션 서버에서 외부 인터넷 액세스 중단이 발생하면 문서 처리가 완전히 중단됩니다. 클라우드 서비스 이용 불가 시 대체 기능, 로컬 모드 또는 재시도 기능이 제공되지 않습니다.
민감한 문서가 귀사의 인프라를 벗어납니다. 결제 내역이 포함된 영수증, 부가가치세 번호와 금액이 기재된 송장, 여권 정보가 포함된 신분증 등 재무 문서는 모든 API 호출 시 제3자 서버로 전송됩니다. GDPR의 데이터 전송 조항은 EU 기반 처리와 관련하여 이러한 사항 중 일부를 다루지만, 감사 범위는 여전히 Klippa의 인프라, 데이터 보존 정책 및 하위 처리업체까지 확장됩니다. 의료, 법률, 금융 서비스 또는 정부 계약을 체결한 팀의 경우 "EU 호스팅"은 데이터가 조직 외부로 유출되지 않아야 한다는 요건을 충족하지 못합니다.
문서별 가격 상한선 없음. Klippa는 가격 정보를 공개하지 않습니다. 월 1만 건의 영수증(경비 관리 시스템)이나 하루 500건의 송장(AP 자동화 워크플로)과 같이 의미 있는 문서 처리량이 발생할 경우, 문서별 요금 청구 모델은 영구 라이선스로는 절대 발생하지 않는 비용을 초래합니다. 비용 추이는 기업 성장과 직접적으로 연관되어 있는데, 이는 인프라 투자의 취지와는 정반대입니다.
요구사항이 확장됨에 따라 전문가의 업무 범위가 제한됩니다. 클리파는 영수증, 송장 및 신분증에 대한 교육을 받았습니다. 경비 관리 용도로 시작한 애플리케이션은 그 용도로만 머무르는 경우가 드뭅니다. 스캔한 고용 계약서, 의료 양식, 기술 도면, 비표준 레이아웃의 구매 주문서와 같이 앞서 언급한 세 가지 범주 이외의 문서 유형이 처음 나타나면 Klippa는 유용한 결과를 반환하지 않습니다.IronOCR범주 제한 없이 텍스트가 포함된 모든 문서를 처리합니다.
비동기 전용 REST 호출은 동기 컨텍스트에서 지연 시간을 추가합니다. 모든 Klippa 호출은 비동기 HTTP 작업입니다. 단일 문서의 왕복 전송은 네트워크를 통해 500ms에서 2000ms가 소요됩니다.IronOCR동기식 처리가 아키텍처에 더 적합한 시나리오에서 비동기 오버헤드 없이 동일한 문서를 로컬에서 100~400ms 내에 처리합니다.
근본적인 문제
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
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
IronOCR과 Klippa OCR: 기능 비교
아래 표는 프로덕션 환경으로의 마이그레이션 결정에 가장 중요한 요소들을 기준으로 두 라이브러리를 비교합니다.
| 기능 | 클리파 OCR | IronOCR |
|---|---|---|
| 배포 모델 | 클라우드 전용(EU 서버) | 온프레미스, 완전 로컬 |
| .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에 구애받지 않는 | 윈도우, 리눅스, 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
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
단계 3: 라이선스 초기화
라이선스 초기화를 응용 프로그램 시작 시 한 번 설정 — Program.cs, Startup.cs, 또는 첫 번째 OCR 호출 전에:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
코드 마이그레이션 예제
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
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
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
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
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
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
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
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
전체 재시도 인프라 — 루프, 지연 계산, CancellationTokenSource, HTTP 상태 코드 분기, 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
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
IronOCR 변환 과정 없이 PDF 파일을 바로 읽을 수 있습니다. 각 페이지는 전체 줄, 단어 및 문자 계층 구조를 유지하며 개별적으로 접근할 수 있습니다. SaveAsSearchablePdf() 호출은 스캔한 문서에서 텍스트 레이어 PDF를 생성합니다 — Klippa가 제공하지 않는 기능입니다. PDF 입력 가이드는 로딩 옵션을 다루고, 검색 가능한 PDF 가이드는 보관 규정 준수를 위한 PDF/A를 포함한 출력 옵션을 다룹니다.
클리파 OCR API와IronOCR매핑 참조
Klippa는 타입이 지정된 SDK가 아니라 REST API입니다. 아래 매핑은 Klippa의 적분 표면을IronOCR에 해당하는 값으로 변환합니다.
| 클리파 컨셉 | IronOCR에 상응하는 |
|---|---|
HttpClient와 X-Auth-Key 헤더 포함 |
IronTesseract 인스턴스 — 인증 설정 필요 없음 |
MultipartFormDataContent |
OcrInput.LoadImage(path) 혹은 OcrInput.LoadPdf(path) |
POST /api/v1/parseDocument |
IronTesseract.Read(input) |
await _client.PostAsync(...) |
ocr.Read(input) — 동기식, await 필요 없음 |
response.EnsureSuccessStatusCode() |
필요 없음 - HTTP 응답 없음 |
JsonSerializer.Deserialize<KlippaResponse>(json) |
타입화된 OcrResult — 역직렬화 없음 |
KlippaResponse.Data.ParsedDocument.Text |
OcrResult.Text |
KlippaResponse.Data.ParsedDocument.Amount |
사용자 정의 정규 표현식 OcrResult.Text 또는 OcrResult.Lines |
KlippaResponse.Data.ParsedDocument.Merchant |
OcrResult.Pages[0].Lines[0].Text |
Task.Delay을 가진 재시도 루프 |
필요 없음 - 네트워크 장애 모드가 없음 |
CancellationTokenSource(TimeSpan.FromSeconds(30)) |
필요 없음 - 로컬 실행 |
| 속도 제한 처리(HTTP 429) | 필요 없음 - 요금 제한 없음 |
| 클라우드 문서를 EU 서버로 라우팅 | 로컬 프로세스 내 실행 |
KlippaService.Dispose() / HttpClient.Dispose() |
OcrInput를 using 문을 통해 처리 |
| 구조화된 JSON 응답 필드 | OcrResult.Text + OcrResult.Pages + OcrResult.Words |
| SaaS API 구독 | IronOcr.License.LicenseKey 문자열 — 영구적 |
일반적인 마이그레이션 문제와 해결책
문제 1: HTTP 제거 후 비동기 전용 호출 사이트
Klippa: HTTP 호출에 비동기 방식이 필요하기 때문에 모든 Klippa 통합은 비동기 방식으로 이루어집니다. 컨트롤러, 서비스, 그리고 코드베이스 전반에 걸친 백그라운드 작업자들은 await ProcessDocumentAsync(...)을 호출합니다. HTTP 호출을 제거하면 await이 더 이상 필요 없지만, async 메서드 서명은 동일하게 유지됩니다.
해결책: IronOCR는 동기 및 비동기 API를 모두 제공합니다. 비동기 상태를 유지해야 하는 호출 사이트(ASP.NET Core 컨트롤러, CancellationToken을 가진 백그라운드 서비스)의 경우 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
비동기 OCR 가이드는 ASP.NET Core 및 호스트된 서비스 패턴에 대한 ReadAsync 및 CancellationToken 통합을 다룹니다.
문제 2: 의존성 주입 등록
Klippa: KlippaService 클래스가 DI에서 싱글톤 또는 스코프된 서비스로 등록되며 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
하나의 IronTesseract 인스턴스가 싱글톤으로 등록되어 동시 요청을 처리합니다. 각 Read() 호출은 스레드 안전합니다.
문제 3: JSON 사전 파싱 없이 구조화된 필드 추출
Klippa: Klippa는 amount, merchant, date, 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
영역 기반 OCR 가이드는 CropRectangle 사용법을 자세히 다룹니다. 영수증 및 송장 레이아웃 전반에 걸친 전체 패턴 추출을 위해, 영수증 스캔 튜토리얼은 완벽하게 작동하는 코드를 제공합니다.
문제 4: 상위 서비스에서 스트림 형태로 도착하는 문서
Klippa: Klippa는 파일을 HTTP 폼 콘텐츠로 래핑한 멀티파트 폼 업로드 방식으로 문서를 수신합니다. 애플리케이션이 S3, Azure Blob Storage 또는 내부 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
ReadAllBytes 없고, MultipartFormDataContent 구성도 없으며, HTTP POST도 없습니다. 스트림은 바로 OcrInput으로 들어갑니다. 하천 유입 지침에는 하천 유형 및 처리 방식이 설명되어 있습니다.
문제 5: HTTP 모킹에 의존하는 통합 테스트
Klippa: Klippa 코드 통합 테스트는 HttpClient를 모의하거나 HTTP 인터셉터(e.g., WireMock, MockHttp)를 사용하여 API 응답을 시뮬레이션합니다. 해당 테스트는 OCR 로직이 아닌 HTTP 계층을 모킹하는 것입니다.
해결책: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
이전에는 실제 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
이미지 품질 보정 가이드는 모든 전처리 필터와 다양한 문서 손상 유형에 대한 필터 적용 순서를 다룹니다.
클리파 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" .
재고 관련 참고 사항:
- Klippa 호출을 위한
HttpClient을 래핑하는 모든 클래스를 기록하세요 - 모든 JSON 역직렬화 모델 클래스(
KlippaResponse,KlippaParsedDocument등)를 나열하세요 - Klippa의 미리 파싱된 JSON 속성을 사용하는 모든 필드 매핑을 문서화합니다.
- Klippa용으로 구축된 Polly 재시도 정책이나 사용자 지정 재시도 루프를 참고하십시오.
코드 마이그레이션
IronOcrNuGet 패키지 (dotnet add package IronOcr) 설치- 응용 프로그램 시작에
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"추가 - Klippa 서비스 파일에서
System.Net.Http,System.Text.Json,Newtonsoft.Json불러오기를 제거하세요 KlippaService클래스를 삭제하거나 그 본문을IronTesseract호출로 대체하여 인터페이스를 유지하세요IronTesseract을 DI 컨테이너에 싱글톤으로 등록하세요MultipartFormDataContent업로드 블록을OcrInput.LoadImage()또는OcrInput.LoadPdf()으로 교체하세요- JSON 응답 모델 클래스(
KlippaResponse,KlippaData,KlippaParsedDocument) 삭제 - 널 가능 JSON 탐색 체인(
.Data?.ParsedDocument?.Text)을result.Text으로 교체하세요 - 재시도 루프와 Klippa 호출 사이트의
CancellationTokenSource타임아웃을 제거하세요 - 속도 제한 처리 제거 (HTTP 429 차단)
await ProcessDocumentAsync(...)을await ocr.ReadAsync(...)또는 동기식ocr.Read(...)로 교체하세요- 저급 문서 입력에 대한
OcrInput전처리 필터 (Deskew,DeNoise,Contrast) 추가 - HTTP 모의 테스트 인프라를 실제 문서 픽스처 테스트로 교체
- Klippa 호출에 적용되는 Polly 재시도 정책 또는 사용자 지정 재시도 미들웨어를 삭제합니다.
마이그레이션 이후
- 텍스트 추출 결과가 알려진 테스트 문서의 예상 내용과 일치하는지 확인합니다.
- 프로덕션 문서 유형에 대해 신뢰도 점수가 허용 가능한 임계값(일반적으로 70% 이상)을 초과하는지 확인합니다.
- PDF 입력 테스트: 여러 페이지 PDF를 네이티브로 로드하고
result.Pages을 통한 페이지별 텍스트 접근을 확인하세요 - 스트림 입력 테스트:
MemoryStream을 전달하고OcrInput.LoadImage(stream)이 올바른 출력을 생성하는지 확인하세요 - 전처리 필터가 처리되지 않은 기준선에 비해 저품질 스캔의 정확도를 향상시키는지 확인합니다.
- DI가 주입된
IronTesseract싱글톤이 경쟁 없이 동시 요청을 처리하는지 확인 - 통합 테스트는 오프라인(네트워크 연결 없이)에서 실행해야 합니다. 모든 테스트는 클라우드 액세스 없이도 통과해야 합니다.
- 스캔된 문서 흐름을 위한
result.SaveAsSearchablePdf("output.pdf")첨부물로 검색 가능한 PDF 출력 확인 ReadAsync을 ASP.NET Core 컨트롤러 컨텍스트에서CancellationToken전파와 함께 테스트- 지속적인 부하 하에서 메모리 누수가 없음을 확인하는
using var input = new OcrInput()처리 패턴 확인
IronOCR로 마이그레이션할 때의 주요 이점
데이터 주권은 처음부터 보장됩니다. 마이그레이션 후에도 민감한 금융 문서, 신원 확인 이미지, 기밀 청구서는 귀사 인프라 외부로 유출되지 않습니다. 감사 범위에 제3자 처리업체가 없으며, 검토해야 할 데이터 보존 정책도 없고, 유지 관리해야 할 데이터 전송 계약도 없습니다. 이전에는 Klippa 사용에 문제를 일으켰던 HIPAA, ITAR, CMMC 및 FedRAMP 제약 조건이 기본적으로 충족됩니다. Docker , AWS 또는 Azure 에 배포하면 모든 것이 자체 인프라 경계 내에 유지됩니다.
인프라 복잡성이 제거되었습니다. 서비스 클래스, HTTP 클라이언트, 폼 업로드 코드, JSON 모델, 재시도 정책, 타임아웃 구성 등 이 모든 것은 네트워크 호출을 래핑하기 위해 존재했습니다. 네트워크 호출을 제거하면 모든 것이 함께 사라집니다. 결과적으로 코드베이스는 더 작아지고, 읽기 쉬워지며, 오류 발생 가능성이 줄어듭니다. DI를 통해 주입된 단일 IronTesseract 인스턴스가 전체 HTTP 통합 레이어를 대체합니다.
예측 가능한 비용: 볼륨에 무관합니다. $999 (Lite), $1,499 (Professional), 또는 $2,999 (Enterprise)로 제공되는 영구IronOCR라이선스는 무제한 문서 처리를 보장합니다. 한 달에 500건의 문서를 처리하든 50만 건을 처리하든 비용은 동일합니다. Klippa를 대규모로 사용할 때 비용을 증가시켰던 문서별 청구 방식은 이제 구조적으로 사라졌습니다. IronOCR 라이선스 페이지에는 모든 등급과 각 등급에 포함되는 내용이 자세히 나와 있습니다.
문서 범위에 제한이 없습니다.IronOCR텍스트가 포함된 모든 문서를 처리합니다. 스캔된 계약서, 기술 도면, 의료 양식, 구매 주문서, 수기로 작성된 노트, 스크린샷, TIFF 아카이브 — 동일한 API로 동일한 Read() 호출을 통해 처리됩니다. Klippa에서 교육받은 범주 외의 문서에 대해 별도의 시스템이 필요했던 전문 분야 제한이 사라졌습니다. 하나의 라이브러리, 하나의 통합 지점, 모든 문서 유형.
오프라인 및 제한된 네트워크 환경이 이제 지원됩니다. 은행 네트워크, 정부 시스템, 엣지 환경 또는 외부 트래픽 전송이 제한된 모든 인프라에 배포된 애플리케이션은 개방형 환경에서와 동일하게 실행됩니다. 연결 상태 확인이나 클라우드 엔드포인트에 대한 상태 점검 기능이 없으며, 인터넷 연결이 불가능할 때 성능 저하 모드도 지원하지 않습니다. 에어갭 방식으로 구축하면 수정 없이 작동합니다. Linux 배포 가이드 와 Docker 배포 가이드는 이러한 환경에 대한 컨테이너 기반 배포 경로와 서버 측 배포 경로를 다룹니다.
이미지 향상에 대한 완벽한 제어. 클라우드 전처리는 블랙박스 방식이었습니다. Klippa가 전처리를 적용하면 사용자는 결과를 확인하고 조정할 매개변수가 없었습니다. IronOCR의 전처리 파이프라인은 명시적이고 조합 가능: Deskew(), DeNoise(), Contrast(), Binarize(), Sharpen(), Scale(), Dilate(), DeepCleanBackgroundNoise(). 각 필터는 선택 사항이며 주문해야 합니다. 정확도 향상은 측정 가능하고, 재현 가능하며, 사용자가 제어할 수 있습니다. 이미지 품질 보정 가이드 및 전처리 기능 페이지 에서는 전체 필터 카탈로그와 각 필터 적용 시점에 대한 지침을 제공합니다.
자주 묻는 질문
Klippa OCR API에서 IronOCR로 마이그레이션해야 하는 이유는 무엇인가요?
일반적인 동인으로는 COM 상호 운용의 복잡성 제거, 파일 기반 라이선스 관리 대체, 페이지당 과금 방지, Docker/컨테이너 배포 활성화, 표준 .NET 툴링과 통합되는 NuGet 네이티브 워크플로 채택 등이 있습니다.
Klippa OCR API에서 IronOCR로 마이그레이션할 때 주요 코드 변경 사항은 무엇인가요?
Klippa 초기화 시퀀스를 IronTesseract 인스턴스화로 대체하고, COM 수명 주기 관리(명시적 생성/로드/폐쇄 패턴)를 제거하고, 결과 속성 이름을 업데이트합니다. 그 결과 상용구 줄이 현저히 줄어듭니다.
마이그레이션을 시작하려면 IronOCR을 어떻게 설치하나요?
패키지 관리자 콘솔에서 '설치-패키지 IronOcr'을 실행하거나 CLI에서 '닷넷 추가 패키지 IronOcr'을 실행하세요. 언어 팩은 별도의 패키지입니다: 예를 들어 프랑스어의 경우 '닷넷 추가 패키지 IronOcr.Languages.French'를 실행합니다.
IronOCR은 표준 비즈니스 문서에 대한 Klippa OCR API의 OCR 정확도와 일치하나요?
IronOCR은 송장, 계약서, 영수증, 타이핑된 양식 등 표준 비즈니스 콘텐츠에 대해 높은 정확도를 달성합니다. 이미지 전처리 필터(데스큐, 노이즈 제거, 대비 향상)는 품질이 저하된 입력에 대한 인식률을 더욱 향상시킵니다.
Klippa OCR API가 별도로 설치하는 언어 데이터는 어떻게 처리하나요?
IronOCR의 언어 데이터는 NuGet 패키지로 배포됩니다. '닷넷 추가 패키지 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를 모두 읽습니다. IronTesseract를 인스턴스화하고, 입력이 PDF 경로 또는 OcrPdfInput인 경우 ocr.Read(input)를 호출하고, OcrResult 페이지를 반복하면 됩니다. 별도의 PDF 렌더링 파이프라인이 필요하지 않습니다.
IronOCR은 대량 처리에서 스레딩을 어떻게 처리하나요?
IronTesseract는 스레드별로 인스턴스화해도 안전합니다. Parallel.ForEach 또는 Task 풀에서 스레드당 하나의 인스턴스를 스핀업하고, OCR을 동시에 실행하고, 완료되면 각 인스턴스를 폐기하면 됩니다. 전역 상태나 잠금이 필요하지 않습니다.
IronOCR은 텍스트 추출 후 어떤 출력 형식을 지원하나요?
IronOCR은 텍스트, 단어 좌표, 신뢰도 점수, 페이지 구조를 포함한 구조화된 결과를 반환합니다. 내보내기 옵션에는 일반 텍스트, 검색 가능한 PDF, 다운스트림 처리를 위한 구조화된 결과 개체가 포함됩니다.
작업량 확장을 위해 Klippa OCR API보다 IronOCR 가격이 더 예측 가능한가요?
IronOCR은 페이지당 또는 볼륨당 요금이 없는 정액제 영구 라이선스를 사용합니다. 10,000페이지를 처리하든 1,000만 페이지를 처리하든 라이선스 비용은 일정하게 유지됩니다. 볼륨 및 팀 라이선스 옵션은 IronOCR 가격 페이지에서 확인할 수 있습니다.
Klippa OCR API에서 IronOCR로 마이그레이션한 후 기존 테스트는 어떻게 되나요?
추출된 텍스트 콘텐츠에 대해 어설트하는 테스트는 마이그레이션 후에도 계속 통과해야 합니다. API 호출 패턴 또는 COM 개체 수명 주기의 유효성을 검사하는 테스트는 IronOCR의 더 간단한 초기화 및 결과 모델을 반영하도록 업데이트해야 합니다.

