跳至頁尾內容
與其他組件的比較

ABBYY FineReader vs Tesseract:OCR 比較

OCR.space沒有NuGet套件。 這個事實決定了.NET開發者在選擇它時所做的每一個整合決策:您撰寫自訂的https://api.ocr.space/parse/image,手動解析JSON回應,捕捉HTTP 429,實施指數性退避,構建速率限制記帳,並定義自己的例外型別——所有這些都在一個單一的文字到達您的應用程式之前。 本文檢討了DIY所需的額外成本,涉及程式碼時間和生產可靠性,並直接與IronOCR進行比較,這是以單一NuGet套件形式提供的原生.NET程式庫。

瞭解OCR.space

OCR.space是一個增值REST API,用於在OCR.space運行的遠端伺服器上處理圖像和PDF。 該服務圍繞其免費層次定位:每月25,000次請求,不需要信用卡,這對於正在嘗試OCR或建立個人專案的開發者具有吸引力。 沒有NuGet套件,沒有官方.NET SDK,也沒有任何語言中強型別的客戶端程式庫。 每個.NET整合都是在HttpClient之上手動構建的。

對於.NET開發者的架構現實:

  • 沒有NuGet套件:零一流的.NET支援。 沒有IntelliSense、沒有型別模型,沒有整合錯誤處理。
  • 僅限雲端處理:每個文件都離開您的基礎結構。 沒有本地部署方案,也沒有自我託管部署路徑。
  • 手動REST整合:開發者將文件編碼為base64,構建MultipartFormDataContent,並解析原始JSON響應。
  • DIY速率限制:免費層實施每分鐘60次請求以及每IP每天500次請求的嚴格限制。生產應用程式必須自行構建此邏輯。
  • 文件大小限制:免費層拒絕大小超過5MB的文件。 多頁PDF通常超過這個限制。
  • PDF輸出加水印:免費層次上的可搜尋PDF生成嵌入了OCR.space水印,這使得輸出文件無法用於客戶交付或文件歸檔。
  • 沒有SLA:免費層次不提供正常運行時間承諾。 付費計畫提供更高的限制,但仍然是相同的架構限制。

開發者必須撰寫的REST整合

OCR.space的文件將開發者指向REST端點,剩下的都由他們自己構建完成。 最低可行的.NET客戶端——在考慮生產硬化之前——看起來是這樣的:

// OCR.space: what you build before the first line of your actual application
public class OcrSpaceApiClient : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly SemaphoreSlim _rateLimiter;
    private const string ApiEndpoint = "https://api.ocr.space/parse/image";
    private const int MaxFileSizeFree = 5 * 1024 * 1024; // 5MB — hard limit on free tier
    private const int RateLimitPerMinute = 60;

    public OcrSpaceApiClient(string apiKey)
    {
        _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
        _httpClient = new HttpClient();
        _httpClient.Timeout = TimeSpan.FromSeconds(120);
        _rateLimiter = new SemaphoreSlim(RateLimitPerMinute, RateLimitPerMinute);
    }

    public async Task<OcrResult> ExtractTextFromFileAsync(
        string filePath,
        string language = "eng",
        CancellationToken cancellationToken = default)
    {
        if (!File.Exists(filePath))
            throw new FileNotFoundException("Image file not found", filePath);

        var fileInfo = new FileInfo(filePath);
        if (fileInfo.Length > MaxFileSizeFree)
        {
            throw new InvalidOperationException(
                $"File size {fileInfo.Length / 1024 / 1024}MB exceeds free tier limit of 5MB.");
        }

        await _rateLimiter.WaitAsync(cancellationToken);

        try
        {
            // Document leaves your infrastructure here
            byte[] imageBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
            string base64Image = Convert.ToBase64String(imageBytes);
            string mimeType = GetMimeType(filePath);

            var formContent = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("apikey", _apiKey),
                new KeyValuePair<string, string>("base64Image", $"data:{mimeType};base64,{base64Image}"),
                new KeyValuePair<string, string>("language", language),
                new KeyValuePair<string, string>("isOverlayRequired", "false"),
                new KeyValuePair<string, string>("filetype", Path.GetExtension(filePath).TrimStart('.')),
                new KeyValuePair<string, string>("detectOrientation", "true"),
                new KeyValuePair<string, string>("scale", "true"),
                new KeyValuePair<string, string>("OCREngine", "2")
            });

            var response = await _httpClient.PostAsync(ApiEndpoint, formContent, cancellationToken);

            if (!response.IsSuccessStatusCode)
            {
                string errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
                throw new OcrSpaceException(
                    $"OCR.space API returned {response.StatusCode}: {errorBody}",
                    (int)response.StatusCode);
            }

            string jsonResponse = await response.Content.ReadAsStringAsync(cancellationToken);
            return ParseOcrResponse(jsonResponse);
        }
        finally
        {
            _ = Task.Run(async () =>
            {
                await Task.Delay(60000 / RateLimitPerMinute);
                _rateLimiter.Release();
            });
        }
    }

    private OcrResult ParseOcrResponse(string jsonResponse)
    {
        using var doc = JsonDocument.Parse(jsonResponse);
        var root = doc.RootElement;

        if (root.TryGetProperty("IsErroredOnProcessing", out var isErrored) && isErrored.GetBoolean())
        {
            string errorMessage = "OCR processing failed";
            if (root.TryGetProperty("ErrorMessage", out var errorMessages))
            {
                // Parse the array of error strings manually
                var messages = new List<string>();
                if (errorMessages.ValueKind == JsonValueKind.Array)
                {
                    foreach (var msg in errorMessages.EnumerateArray())
                        messages.Add(msg.GetString() ?? "Unknown error");
                }
                errorMessage = string.Join("; ", messages);
            }
            throw new OcrSpaceException(errorMessage, 500);
        }

        var result = new OcrResult();

        if (root.TryGetProperty("ParsedResults", out var parsedResults))
        {
            foreach (var parsedResult in parsedResults.EnumerateArray())
            {
                if (parsedResult.TryGetProperty("ParsedText", out var parsedText))
                    result.ParsedText += parsedText.GetString();

                if (parsedResult.TryGetProperty("FileParseExitCode", out var exitCode))
                    result.ExitCodes.Add(exitCode.GetInt32());
            }
        }

        return result;
    }

    private string GetMimeType(string filePath) =>
        Path.GetExtension(filePath).ToLowerInvariant() switch
        {
            ".png"          => "image/png",
            ".jpg" or ".jpeg" => "image/jpeg",
            ".bmp"          => "image/bmp",
            ".tiff" or ".tif" => "image/tiff",
            ".pdf"          => "application/pdf",
            _               => "application/octet-stream"
        };

    public void Dispose()
    {
        _httpClient?.Dispose();
        _rateLimiter?.Dispose();
    }
}

// Custom models — no SDK means you define these yourself
public class OcrResult
{
    public string ParsedText { get; set; } = string.Empty;
    public List<string> Warnings { get; } = new();
    public List<int> ExitCodes { get; } = new();
}

public class OcrSpaceException : Exception
{
    public int StatusCode { get; }
    public OcrSpaceException(string message, int statusCode) : base(message)
        => StatusCode = statusCode;
}
// OCR.space: what you build before the first line of your actual application
public class OcrSpaceApiClient : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly SemaphoreSlim _rateLimiter;
    private const string ApiEndpoint = "https://api.ocr.space/parse/image";
    private const int MaxFileSizeFree = 5 * 1024 * 1024; // 5MB — hard limit on free tier
    private const int RateLimitPerMinute = 60;

    public OcrSpaceApiClient(string apiKey)
    {
        _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
        _httpClient = new HttpClient();
        _httpClient.Timeout = TimeSpan.FromSeconds(120);
        _rateLimiter = new SemaphoreSlim(RateLimitPerMinute, RateLimitPerMinute);
    }

    public async Task<OcrResult> ExtractTextFromFileAsync(
        string filePath,
        string language = "eng",
        CancellationToken cancellationToken = default)
    {
        if (!File.Exists(filePath))
            throw new FileNotFoundException("Image file not found", filePath);

        var fileInfo = new FileInfo(filePath);
        if (fileInfo.Length > MaxFileSizeFree)
        {
            throw new InvalidOperationException(
                $"File size {fileInfo.Length / 1024 / 1024}MB exceeds free tier limit of 5MB.");
        }

        await _rateLimiter.WaitAsync(cancellationToken);

        try
        {
            // Document leaves your infrastructure here
            byte[] imageBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
            string base64Image = Convert.ToBase64String(imageBytes);
            string mimeType = GetMimeType(filePath);

            var formContent = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("apikey", _apiKey),
                new KeyValuePair<string, string>("base64Image", $"data:{mimeType};base64,{base64Image}"),
                new KeyValuePair<string, string>("language", language),
                new KeyValuePair<string, string>("isOverlayRequired", "false"),
                new KeyValuePair<string, string>("filetype", Path.GetExtension(filePath).TrimStart('.')),
                new KeyValuePair<string, string>("detectOrientation", "true"),
                new KeyValuePair<string, string>("scale", "true"),
                new KeyValuePair<string, string>("OCREngine", "2")
            });

            var response = await _httpClient.PostAsync(ApiEndpoint, formContent, cancellationToken);

            if (!response.IsSuccessStatusCode)
            {
                string errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
                throw new OcrSpaceException(
                    $"OCR.space API returned {response.StatusCode}: {errorBody}",
                    (int)response.StatusCode);
            }

            string jsonResponse = await response.Content.ReadAsStringAsync(cancellationToken);
            return ParseOcrResponse(jsonResponse);
        }
        finally
        {
            _ = Task.Run(async () =>
            {
                await Task.Delay(60000 / RateLimitPerMinute);
                _rateLimiter.Release();
            });
        }
    }

    private OcrResult ParseOcrResponse(string jsonResponse)
    {
        using var doc = JsonDocument.Parse(jsonResponse);
        var root = doc.RootElement;

        if (root.TryGetProperty("IsErroredOnProcessing", out var isErrored) && isErrored.GetBoolean())
        {
            string errorMessage = "OCR processing failed";
            if (root.TryGetProperty("ErrorMessage", out var errorMessages))
            {
                // Parse the array of error strings manually
                var messages = new List<string>();
                if (errorMessages.ValueKind == JsonValueKind.Array)
                {
                    foreach (var msg in errorMessages.EnumerateArray())
                        messages.Add(msg.GetString() ?? "Unknown error");
                }
                errorMessage = string.Join("; ", messages);
            }
            throw new OcrSpaceException(errorMessage, 500);
        }

        var result = new OcrResult();

        if (root.TryGetProperty("ParsedResults", out var parsedResults))
        {
            foreach (var parsedResult in parsedResults.EnumerateArray())
            {
                if (parsedResult.TryGetProperty("ParsedText", out var parsedText))
                    result.ParsedText += parsedText.GetString();

                if (parsedResult.TryGetProperty("FileParseExitCode", out var exitCode))
                    result.ExitCodes.Add(exitCode.GetInt32());
            }
        }

        return result;
    }

    private string GetMimeType(string filePath) =>
        Path.GetExtension(filePath).ToLowerInvariant() switch
        {
            ".png"          => "image/png",
            ".jpg" or ".jpeg" => "image/jpeg",
            ".bmp"          => "image/bmp",
            ".tiff" or ".tif" => "image/tiff",
            ".pdf"          => "application/pdf",
            _               => "application/octet-stream"
        };

    public void Dispose()
    {
        _httpClient?.Dispose();
        _rateLimiter?.Dispose();
    }
}

// Custom models — no SDK means you define these yourself
public class OcrResult
{
    public string ParsedText { get; set; } = string.Empty;
    public List<string> Warnings { get; } = new();
    public List<int> ExitCodes { get; } = new();
}

public class OcrSpaceException : Exception
{
    public int StatusCode { get; }
    public OcrSpaceException(string message, int statusCode) : base(message)
        => StatusCode = statusCode;
}
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading
Imports System.Threading.Tasks

' OCR.space: what you build before the first line of your actual application
Public Class OcrSpaceApiClient
    Implements IDisposable

    Private ReadOnly _httpClient As HttpClient
    Private ReadOnly _apiKey As String
    Private ReadOnly _rateLimiter As SemaphoreSlim
    Private Const ApiEndpoint As String = "https://api.ocr.space/parse/image"
    Private Const MaxFileSizeFree As Integer = 5 * 1024 * 1024 ' 5MB — hard limit on free tier
    Private Const RateLimitPerMinute As Integer = 60

    Public Sub New(apiKey As String)
        _apiKey = If(apiKey, Throw New ArgumentNullException(NameOf(apiKey)))
        _httpClient = New HttpClient()
        _httpClient.Timeout = TimeSpan.FromSeconds(120)
        _rateLimiter = New SemaphoreSlim(RateLimitPerMinute, RateLimitPerMinute)
    End Sub

    Public Async Function ExtractTextFromFileAsync(filePath As String, Optional language As String = "eng", Optional cancellationToken As CancellationToken = Nothing) As Task(Of OcrResult)
        If Not File.Exists(filePath) Then
            Throw New FileNotFoundException("Image file not found", filePath)
        End If

        Dim fileInfo = New FileInfo(filePath)
        If fileInfo.Length > MaxFileSizeFree Then
            Throw New InvalidOperationException($"File size {fileInfo.Length \ 1024 \ 1024}MB exceeds free tier limit of 5MB.")
        End If

        Await _rateLimiter.WaitAsync(cancellationToken)

        Try
            ' Document leaves your infrastructure here
            Dim imageBytes As Byte() = Await File.ReadAllBytesAsync(filePath, cancellationToken)
            Dim base64Image As String = Convert.ToBase64String(imageBytes)
            Dim mimeType As String = GetMimeType(filePath)

            Dim formContent = New FormUrlEncodedContent(New KeyValuePair(Of String, String)() {
                New KeyValuePair(Of String, String)("apikey", _apiKey),
                New KeyValuePair(Of String, String)("base64Image", $"data:{mimeType};base64,{base64Image}"),
                New KeyValuePair(Of String, String)("language", language),
                New KeyValuePair(Of String, String)("isOverlayRequired", "false"),
                New KeyValuePair(Of String, String)("filetype", Path.GetExtension(filePath).TrimStart("."c)),
                New KeyValuePair(Of String, String)("detectOrientation", "true"),
                New KeyValuePair(Of String, String)("scale", "true"),
                New KeyValuePair(Of String, String)("OCREngine", "2")
            })

            Dim response = Await _httpClient.PostAsync(ApiEndpoint, formContent, cancellationToken)

            If Not response.IsSuccessStatusCode Then
                Dim errorBody = Await response.Content.ReadAsStringAsync(cancellationToken)
                Throw New OcrSpaceException($"OCR.space API returned {response.StatusCode}: {errorBody}", CInt(response.StatusCode))
            End If

            Dim jsonResponse = Await response.Content.ReadAsStringAsync(cancellationToken)
            Return ParseOcrResponse(jsonResponse)
        Finally
            _ = Task.Run(Async Function()
                             Await Task.Delay(60000 \ RateLimitPerMinute)
                             _rateLimiter.Release()
                         End Function)
        End Try
    End Function

    Private Function ParseOcrResponse(jsonResponse As String) As OcrResult
        Using doc = JsonDocument.Parse(jsonResponse)
            Dim root = doc.RootElement

            If root.TryGetProperty("IsErroredOnProcessing", ByRef isErrored) AndAlso isErrored.GetBoolean() Then
                Dim errorMessage As String = "OCR processing failed"
                If root.TryGetProperty("ErrorMessage", ByRef errorMessages) Then
                    ' Parse the array of error strings manually
                    Dim messages = New List(Of String)()
                    If errorMessages.ValueKind = JsonValueKind.Array Then
                        For Each msg In errorMessages.EnumerateArray()
                            messages.Add(msg.GetString() OrElse "Unknown error")
                        Next
                    End If
                    errorMessage = String.Join("; ", messages)
                End If
                Throw New OcrSpaceException(errorMessage, 500)
            End If

            Dim result = New OcrResult()

            If root.TryGetProperty("ParsedResults", ByRef parsedResults) Then
                For Each parsedResult In parsedResults.EnumerateArray()
                    If parsedResult.TryGetProperty("ParsedText", ByRef parsedText) Then
                        result.ParsedText &= parsedText.GetString()
                    End If

                    If parsedResult.TryGetProperty("FileParseExitCode", ByRef exitCode) Then
                        result.ExitCodes.Add(exitCode.GetInt32())
                    End If
                Next
            End If

            Return result
        End Using
    End Function

    Private Function GetMimeType(filePath As String) As String
        Return Path.GetExtension(filePath).ToLowerInvariant() Select Case
            Case ".png" : Return "image/png"
            Case ".jpg", ".jpeg" : Return "image/jpeg"
            Case ".bmp" : Return "image/bmp"
            Case ".tiff", ".tif" : Return "image/tiff"
            Case ".pdf" : Return "application/pdf"
            Case Else : Return "application/octet-stream"
        End Select
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        _httpClient?.Dispose()
        _rateLimiter?.Dispose()
    End Sub
End Class

' Custom models — no SDK means you define these yourself
Public Class OcrResult
    Public Property ParsedText As String = String.Empty
    Public ReadOnly Property Warnings As New List(Of String)()
    Public ReadOnly Property ExitCodes As New List(Of Integer)()
End Class

Public Class OcrSpaceException
    Inherits Exception

    Public ReadOnly Property StatusCode As Integer

    Public Sub New(message As String, statusCode As Integer)
        MyBase.New(message)
        Me.StatusCode = statusCode
    End Sub
End Class
$vbLabelText   $csharpLabel

這是超過90行的基礎設施程式碼,存在於第一個業務邏輯方法之前。 每個OCR.space整合都附帶這些樣板(或類似的),撰寫它的每個團隊都在解決OCR.space選擇不為他們解決的相同問題。

理解 IronOCR

IronOCR 是一個商業用的OCR程式庫,用於.NET,透過IronOcr NuGet套件安裝。 它包含一個優化的Tesseract 5引擎,具有自動預處理功能、原生的PDF輸入、可搜尋的PDF輸出、超過125種語言包及結構化結果提取——完全不依賴於雲端、API金鑰或每次請求的費用。 文件在執行.NET應用程式的任何基礎結構上本地處理。

關鍵特徵:

  • 單一NuGet套件: dotnet add package IronOcr 是完整安裝。 沒有需要管理的原生二進制檔,沒有tessdata目錄,沒有環境變數。
  • 強型別API: OcrResultCropRectangle 是一流的.NET型別,擁有完整的IntelliSense支援。
  • 自動預處理:Deskew、DeNoise、Contrast、Binarize和EnhanceResolution過濾器自動或按需應用,無需外部程式庫。
  • 原生PDF支援:同時內建讀取掃描PDF和產生可搜尋的PDF功能。 沒有超越可供記憶體之外的大小限制。
  • 本地執行:在OCR過程中不發生網路呼叫。 程式庫完全以內建方式運行。 氣隙環境在不更改設置的情況下運行良好。
  • 執行緒安全:多個IronTesseract實例可無鎖定或競爭管理的情況下並行運行。
  • 永久授權: $999 Lite / $1,499 Plus / $2,999 Professional。 任何音量都不收取每次請求的費用。

功能比較

功能 OCR.space IronOCR
NuGet套件 無——僅限REST API IronOcr——完整.NET支援
處理位置 OCR.space雲端伺服器 本地——您的基礎架構
免費層 25,000 req/月(有限制) 提供試用
付費定價 聯繫OCR.space以獲取當前定價 $2,999 一次性永久
速率限制 60 req/min,500/day(免費) None
離線能力 不是 是——完全氣隙
資料隱私 文件發送給第三方 文件保留在您的伺服器上

詳細功能比較

功能 OCR.space IronOCR
整合
NuGet套件 None 是(IronOcr
強型別模型 無——手動JSON解析 是(OcrResult, OcrInput
IntelliSense支援 None 全部
自訂例外型別 必須自行定義 內建
重試邏輯 必須自行構建 內建
處理
處理地點 雲端伺服器 本地計程中
需要網際網路 是——每次呼叫 不是
氣隙支援 不是
速率限制 是——所有計畫 None
文件大小限制 5 MB(免費層) 僅限記憶體
OCR能力
圖像OCR
PDF OCR 是(有條件) 是(原生,無大小限制)
可搜尋的 PDF 輸出 免費層的加水印PDF輸出 乾淨輸出,無水印
自動預處理 無(僅限伺服器端) 去斜、降噪、對比度、二值化、增強解析度
手動預處理控制 None 完整過濾器API
語言支持 ~25種語言 125+經由NuGet語言包
每個文件支持多語言 有限 是——主要+輔助語言
結構化輸出(單詞/行/頁面) 否——僅限純文字 是——頁、段落、行,單詞及坐標
置信分數 不是 是——每個結果與每個單詞
基於區域的OCR 不是 是——CropRectangle
OCR期間的條碼讀取 不是 是——ReadBarCodes = true
部署
Docker 需要對外部網路連接 開箱即用
Linux 需要對外部網路連接 全面支持
HIPAA相容 風險——文件離開您的控制 是——無外部傳輸
GDPR相容 風險——歐盟資料轉讓不明確 是——無第三方資料處理
價格
定價模型 每月訂閱 一次性永久授權
入門價格 聯繫OCR.space以獲取當前定價 $999 一次性
每文件文件成本在規模上 None
SLA 無(免費),隨付費不同 企業SLA可用

SDK深度 vs. 原始HTTP:整合成本

OCR.space和一個原生SDK之間的差距不是一個風格偏好。 以開發時長來衡量,錯誤的面積,以及每次部署的維護負擔。

OCR.space方法

所有使用OCR.space的.NET開發者都撰寫自己的HTTP客戶端。 最簡單可能的實施——基本文字提取,沒有重試邏輯,沒有批處理支持,也沒有預處理——仍然超過50行:

// OCR.space: minimum viable extraction —50+lines before business logic
public class OcrSpaceBasicExtraction
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;

    public OcrSpaceBasicExtraction(string apiKey)
    {
        _apiKey = apiKey;
        _httpClient = new HttpClient();
    }

    public async Task<string> ExtractText(string imagePath)
    {
        // Read file and encode — document leaves your infrastructure
        byte[] imageBytes = File.ReadAllBytes(imagePath);
        string base64 = Convert.ToBase64String(imageBytes);

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("apikey", _apiKey),
            new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}"),
            new KeyValuePair<string, string>("language", "eng")
        });

        // Send to cloud
        var response = await _httpClient.PostAsync(
            "https://api.ocr.space/parse/image", content);

        if (!response.IsSuccessStatusCode)
            throw new Exception($"API error: {response.StatusCode}");

        // Parse JSON manually — no typed models
        string json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);

        return doc.RootElement
            .GetProperty("ParsedResults")[0]
            .GetProperty("ParsedText")
            .GetString() ?? string.Empty;
    }
}
// OCR.space: minimum viable extraction —50+lines before business logic
public class OcrSpaceBasicExtraction
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;

    public OcrSpaceBasicExtraction(string apiKey)
    {
        _apiKey = apiKey;
        _httpClient = new HttpClient();
    }

    public async Task<string> ExtractText(string imagePath)
    {
        // Read file and encode — document leaves your infrastructure
        byte[] imageBytes = File.ReadAllBytes(imagePath);
        string base64 = Convert.ToBase64String(imageBytes);

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("apikey", _apiKey),
            new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}"),
            new KeyValuePair<string, string>("language", "eng")
        });

        // Send to cloud
        var response = await _httpClient.PostAsync(
            "https://api.ocr.space/parse/image", content);

        if (!response.IsSuccessStatusCode)
            throw new Exception($"API error: {response.StatusCode}");

        // Parse JSON manually — no typed models
        string json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);

        return doc.RootElement
            .GetProperty("ParsedResults")[0]
            .GetProperty("ParsedText")
            .GetString() ?? string.Empty;
    }
}
Imports System.Net.Http
Imports System.IO
Imports System.Threading.Tasks
Imports System.Text.Json

Public Class OcrSpaceBasicExtraction
    Private ReadOnly _httpClient As HttpClient
    Private ReadOnly _apiKey As String

    Public Sub New(apiKey As String)
        _apiKey = apiKey
        _httpClient = New HttpClient()
    End Sub

    Public Async Function ExtractText(imagePath As String) As Task(Of String)
        ' Read file and encode — document leaves your infrastructure
        Dim imageBytes As Byte() = File.ReadAllBytes(imagePath)
        Dim base64 As String = Convert.ToBase64String(imageBytes)

        Dim content = New FormUrlEncodedContent(New KeyValuePair(Of String, String)() {
            New KeyValuePair(Of String, String)("apikey", _apiKey),
            New KeyValuePair(Of String, String)("base64Image", $"data:image/png;base64,{base64}"),
            New KeyValuePair(Of String, String)("language", "eng")
        })

        ' Send to cloud
        Dim response = Await _httpClient.PostAsync("https://api.ocr.space/parse/image", content)

        If Not response.IsSuccessStatusCode Then
            Throw New Exception($"API error: {response.StatusCode}")
        End If

        ' Parse JSON manually — no typed models
        Dim json As String = Await response.Content.ReadAsStringAsync()
        Using doc = JsonDocument.Parse(json)
            Return doc.RootElement _
                .GetProperty("ParsedResults")(0) _
                .GetProperty("ParsedText") _
                .GetString() OrElse String.Empty
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

新增批處理時,行數上升到80多行,因為每個批處理請求需要速率限制節流,每請求重試邏輯帶有指數性倒退,並且HTTP 429回應的明確處理來進行程式。 新增PDF處理時,您要在每次呼叫前增加5MB文件大小檢查,因為免費層會拒絕較大的文件並且會引發錯誤而不是警告。

IronOCR 方法

從NuGet安裝IronOCR將所有這些基礎設施程式碼替換為已經存在的方法呼叫:

// IronOCR: complete implementation
var result = new IronTesseract().Read("invoice.png");
Console.WriteLine(result.Text);
// IronOCR: complete implementation
var result = new IronTesseract().Read("invoice.png");
Console.WriteLine(result.Text);
$vbLabelText   $csharpLabel

IronTesseract類別處理文件讀取、預處理、引擎執行和結果構造。 沒有HTTP客戶端。 沒有base64編碼。 沒有JSON解析。 沒有API金鑰。 沒有速率限制器。 閱讀文字從圖像中教學維習覆蓋輸入變數——文件路徑、位元組陣列、流、位圖——所有密切關聯在一起的一式一套調用模式。

從源文件中的程式碼整體複雜性數字不是假想的:

方案 OCR.space行數 IronOCR行數
基本提取 50+ 3
PDF處理 60+ 12
批量處理 80+ 15
錯誤處理 70+ 15
完整客戶端基礎設施 200+ 0(NuGet提供的)

批處理與速率限制

速率限制問題是OCR.space免費層在現實生產條件最明顯崩潰的地方。

OCR.space方法

在OCR.space下進行批處理需要建立和管理一個SemaphoreSlim,以保持在每分鐘60次請求的約束內,加上指數退避的重試邏輯,針對當信號量邏輯不精確時或共享基礎設施共享IP地址時到達的HTTP 429回應:

// OCR.space batch:80+lines of rate-limit plumbing before actual work
public class OcrSpaceBatchProcessing
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly SemaphoreSlim _rateLimiter;

    public OcrSpaceBatchProcessing(string apiKey)
    {
        _apiKey = apiKey;
        _httpClient = new HttpClient();
        _rateLimiter = new SemaphoreSlim(60, 60); // Free tier: 60/minute
    }

    public async Task<Dictionary<string, string>> ProcessBatch(string[] imagePaths)
    {
        var results = new Dictionary<string, string>();

        foreach (var imagePath in imagePaths)
        {
            await _rateLimiter.WaitAsync();

            try
            {
                string text = await ProcessWithRetry(imagePath);
                results[imagePath] = text;
            }
            finally
            {
                _ = Task.Run(async () =>
                {
                    await Task.Delay(1000); // 1 second spacing
                    _rateLimiter.Release();
                });
            }
        }

        return results;
    }

    private async Task<string> ProcessWithRetry(string imagePath, int maxRetries = 3)
    {
        for (int attempt = 0; attempt < maxRetries; attempt++)
        {
            try
            {
                byte[] imageBytes = File.ReadAllBytes(imagePath);
                string base64 = Convert.ToBase64String(imageBytes);

                var content = new FormUrlEncodedContent(new[]
                {
                    new KeyValuePair<string, string>("apikey", _apiKey),
                    new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}"),
                    new KeyValuePair<string, string>("language", "eng")
                });

                var response = await _httpClient.PostAsync(
                    "https://api.ocr.space/parse/image", content);

                if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
                {
                    // Rate limited — exponential backoff
                    await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
                    continue;
                }

                response.EnsureSuccessStatusCode();

                string json = await response.Content.ReadAsStringAsync();
                using var doc = JsonDocument.Parse(json);

                return doc.RootElement
                    .GetProperty("ParsedResults")[0]
                    .GetProperty("ParsedText")
                    .GetString() ?? string.Empty;
            }
            catch (HttpRequestException) when (attempt < maxRetries - 1)
            {
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
            }
        }

        throw new Exception($"Failed to process {imagePath} after {maxRetries} attempts");
    }
}
// OCR.space batch:80+lines of rate-limit plumbing before actual work
public class OcrSpaceBatchProcessing
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly SemaphoreSlim _rateLimiter;

    public OcrSpaceBatchProcessing(string apiKey)
    {
        _apiKey = apiKey;
        _httpClient = new HttpClient();
        _rateLimiter = new SemaphoreSlim(60, 60); // Free tier: 60/minute
    }

    public async Task<Dictionary<string, string>> ProcessBatch(string[] imagePaths)
    {
        var results = new Dictionary<string, string>();

        foreach (var imagePath in imagePaths)
        {
            await _rateLimiter.WaitAsync();

            try
            {
                string text = await ProcessWithRetry(imagePath);
                results[imagePath] = text;
            }
            finally
            {
                _ = Task.Run(async () =>
                {
                    await Task.Delay(1000); // 1 second spacing
                    _rateLimiter.Release();
                });
            }
        }

        return results;
    }

    private async Task<string> ProcessWithRetry(string imagePath, int maxRetries = 3)
    {
        for (int attempt = 0; attempt < maxRetries; attempt++)
        {
            try
            {
                byte[] imageBytes = File.ReadAllBytes(imagePath);
                string base64 = Convert.ToBase64String(imageBytes);

                var content = new FormUrlEncodedContent(new[]
                {
                    new KeyValuePair<string, string>("apikey", _apiKey),
                    new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}"),
                    new KeyValuePair<string, string>("language", "eng")
                });

                var response = await _httpClient.PostAsync(
                    "https://api.ocr.space/parse/image", content);

                if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
                {
                    // Rate limited — exponential backoff
                    await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
                    continue;
                }

                response.EnsureSuccessStatusCode();

                string json = await response.Content.ReadAsStringAsync();
                using var doc = JsonDocument.Parse(json);

                return doc.RootElement
                    .GetProperty("ParsedResults")[0]
                    .GetProperty("ParsedText")
                    .GetString() ?? string.Empty;
            }
            catch (HttpRequestException) when (attempt < maxRetries - 1)
            {
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
            }
        }

        throw new Exception($"Failed to process {imagePath} after {maxRetries} attempts");
    }
}
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading
Imports System.Threading.Tasks

Public Class OcrSpaceBatchProcessing
    Private ReadOnly _httpClient As HttpClient
    Private ReadOnly _apiKey As String
    Private ReadOnly _rateLimiter As SemaphoreSlim

    Public Sub New(apiKey As String)
        _apiKey = apiKey
        _httpClient = New HttpClient()
        _rateLimiter = New SemaphoreSlim(60, 60) ' Free tier: 60/minute
    End Sub

    Public Async Function ProcessBatch(imagePaths As String()) As Task(Of Dictionary(Of String, String))
        Dim results As New Dictionary(Of String, String)()

        For Each imagePath In imagePaths
            Await _rateLimiter.WaitAsync()

            Try
                Dim text As String = Await ProcessWithRetry(imagePath)
                results(imagePath) = text
            Finally
                _ = Task.Run(Async Function()
                                 Await Task.Delay(1000) ' 1 second spacing
                                 _rateLimiter.Release()
                             End Function)
            End Try
        Next

        Return results
    End Function

    Private Async Function ProcessWithRetry(imagePath As String, Optional maxRetries As Integer = 3) As Task(Of String)
        For attempt As Integer = 0 To maxRetries - 1
            Try
                Dim imageBytes As Byte() = File.ReadAllBytes(imagePath)
                Dim base64 As String = Convert.ToBase64String(imageBytes)

                Dim content = New FormUrlEncodedContent(New KeyValuePair(Of String, String)() {
                    New KeyValuePair(Of String, String)("apikey", _apiKey),
                    New KeyValuePair(Of String, String)("base64Image", $"data:image/png;base64,{base64}"),
                    New KeyValuePair(Of String, String)("language", "eng")
                })

                Dim response = Await _httpClient.PostAsync("https://api.ocr.space/parse/image", content)

                If response.StatusCode = System.Net.HttpStatusCode.TooManyRequests Then
                    ' Rate limited — exponential backoff
                    Await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)))
                    Continue For
                End If

                response.EnsureSuccessStatusCode()

                Dim json As String = Await response.Content.ReadAsStringAsync()
                Using doc = JsonDocument.Parse(json)
                    Return doc.RootElement _
                        .GetProperty("ParsedResults")(0) _
                        .GetProperty("ParsedText") _
                        .GetString() OrElse String.Empty
                End Using
            Catch ex As HttpRequestException When attempt < maxRetries - 1
                Await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)))
            End Try
        Next

        Throw New Exception($"Failed to process {imagePath} after {maxRetries} attempts")
    End Function
End Class
$vbLabelText   $csharpLabel

免費層每天500次請求的上限進一步限制了這一點。 單日處理600個文件的應用程式會在中途失敗,直到下個日曆日才會自動轉移。

IronOCR 方法

IronOCR沒有限速。 批處理是一個迴圈:

//IronOCRbatch: 8 lines, no rate limits, no retries needed
public Dictionary<string, string> ProcessBatch(string[] imagePaths)
{
    var results = new Dictionary<string, string>();
    var ocr = new IronTesseract();

    foreach (var imagePath in imagePaths)
    {
        //不是rate limits, no retries, no cloud dependency
        var result = ocr.Read(imagePath);
        results[imagePath] = result.Text;
    }

    return results;
}
//IronOCRbatch: 8 lines, no rate limits, no retries needed
public Dictionary<string, string> ProcessBatch(string[] imagePaths)
{
    var results = new Dictionary<string, string>();
    var ocr = new IronTesseract();

    foreach (var imagePath in imagePaths)
    {
        //不是rate limits, no retries, no cloud dependency
        var result = ocr.Read(imagePath);
        results[imagePath] = result.Text;
    }

    return results;
}
Imports System.Collections.Generic

Public Class IronOCRBatch
    ' IronOCRbatch: 8 lines, no rate limits, no retries needed
    Public Function ProcessBatch(imagePaths As String()) As Dictionary(Of String, String)
        Dim results As New Dictionary(Of String, String)()
        Dim ocr As New IronTesseract()

        For Each imagePath As String In imagePaths
            ' 不是rate limits, no retries, no cloud dependency
            Dim result = ocr.Read(imagePath)
            results(imagePath) = result.Text
        Next

        Return results
    End Function
End Class
$vbLabelText   $csharpLabel

跨批次使用IronTesseract例子避免了重複的引擎初始化開銷。 處理速度受限於本地CPU和磁碟I/O,而不是由於遠端API的節流政策。 多執行緒範例展示如何在當吞吐量事宜時進行多核平行化。

PDF 處理

PDF支援證明了第二大結構性差距。

OCR.space方法

OCR.space接受付費計畫中的PDF上傳並部分在免費層份上,但免費帳戶的5MB文件大小上限使得大多數多頁文件無法進行。 免費層的可搜尋PDF輸出嵌入了水印。 實施需要每頁結果提取和在每次請求前進行明確的文件大小檢查:

// OCR.space PDF: size check required, watermarks on free tier
public async Task<List<string>> ExtractTextFromPdf(string pdfPath)
{
    var pageTexts = new List<string>();

    // Reject before wasting a request quota entry
    var fileInfo = new FileInfo(pdfPath);
    if (fileInfo.Length > 5 * 1024 * 1024)
        throw new InvalidOperationException("File exceeds 5MB free tier limit. Upgrade or split PDF.");

    byte[] pdfBytes = File.ReadAllBytes(pdfPath);
    string base64 = Convert.ToBase64String(pdfBytes);

    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("apikey", _apiKey),
        new KeyValuePair<string, string>("base64Image", $"data:application/pdf;base64,{base64}"),
        new KeyValuePair<string, string>("language", "eng"),
        new KeyValuePair<string, string>("filetype", "PDF"),
        new KeyValuePair<string, string>("isCreateSearchablePdf", "false") // Watermarked on free tier
    });

    var response = await _httpClient.PostAsync("https://api.ocr.space/parse/image", content);

    if (!response.IsSuccessStatusCode)
        throw new Exception($"API error: {response.StatusCode}");

    string json = await response.Content.ReadAsStringAsync();
    using var doc = JsonDocument.Parse(json);

    if (doc.RootElement.TryGetProperty("ParsedResults", out var results))
    {
        foreach (var result in results.EnumerateArray())
        {
            if (result.TryGetProperty("ParsedText", out var text))
                pageTexts.Add(text.GetString() ?? string.Empty);
        }
    }

    return pageTexts;
}
// OCR.space PDF: size check required, watermarks on free tier
public async Task<List<string>> ExtractTextFromPdf(string pdfPath)
{
    var pageTexts = new List<string>();

    // Reject before wasting a request quota entry
    var fileInfo = new FileInfo(pdfPath);
    if (fileInfo.Length > 5 * 1024 * 1024)
        throw new InvalidOperationException("File exceeds 5MB free tier limit. Upgrade or split PDF.");

    byte[] pdfBytes = File.ReadAllBytes(pdfPath);
    string base64 = Convert.ToBase64String(pdfBytes);

    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("apikey", _apiKey),
        new KeyValuePair<string, string>("base64Image", $"data:application/pdf;base64,{base64}"),
        new KeyValuePair<string, string>("language", "eng"),
        new KeyValuePair<string, string>("filetype", "PDF"),
        new KeyValuePair<string, string>("isCreateSearchablePdf", "false") // Watermarked on free tier
    });

    var response = await _httpClient.PostAsync("https://api.ocr.space/parse/image", content);

    if (!response.IsSuccessStatusCode)
        throw new Exception($"API error: {response.StatusCode}");

    string json = await response.Content.ReadAsStringAsync();
    using var doc = JsonDocument.Parse(json);

    if (doc.RootElement.TryGetProperty("ParsedResults", out var results))
    {
        foreach (var result in results.EnumerateArray())
        {
            if (result.TryGetProperty("ParsedText", out var text))
                pageTexts.Add(text.GetString() ?? string.Empty);
        }
    }

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

Public Class OCRSpaceClient
    Private ReadOnly _apiKey As String
    Private ReadOnly _httpClient As HttpClient

    Public Sub New(apiKey As String, httpClient As HttpClient)
        _apiKey = apiKey
        _httpClient = httpClient
    End Sub

    Public Async Function ExtractTextFromPdf(pdfPath As String) As Task(Of List(Of String))
        Dim pageTexts As New List(Of String)()

        ' Reject before wasting a request quota entry
        Dim fileInfo As New FileInfo(pdfPath)
        If fileInfo.Length > 5 * 1024 * 1024 Then
            Throw New InvalidOperationException("File exceeds 5MB free tier limit. Upgrade or split PDF.")
        End If

        Dim pdfBytes As Byte() = File.ReadAllBytes(pdfPath)
        Dim base64 As String = Convert.ToBase64String(pdfBytes)

        Dim content As New FormUrlEncodedContent(New KeyValuePair(Of String, String)() {
            New KeyValuePair(Of String, String)("apikey", _apiKey),
            New KeyValuePair(Of String, String)("base64Image", $"data:application/pdf;base64,{base64}"),
            New KeyValuePair(Of String, String)("language", "eng"),
            New KeyValuePair(Of String, String)("filetype", "PDF"),
            New KeyValuePair(Of String, String)("isCreateSearchablePdf", "false") ' Watermarked on free tier
        })

        Dim response As HttpResponseMessage = Await _httpClient.PostAsync("https://api.ocr.space/parse/image", content)

        If Not response.IsSuccessStatusCode Then
            Throw New Exception($"API error: {response.StatusCode}")
        End If

        Dim json As String = Await response.Content.ReadAsStringAsync()
        Using doc As JsonDocument = JsonDocument.Parse(json)
            If doc.RootElement.TryGetProperty("ParsedResults", results) Then
                For Each result In results.EnumerateArray()
                    If result.TryGetProperty("ParsedText", text) Then
                        pageTexts.Add(If(text.GetString(), String.Empty))
                    End If
                Next
            End If
        End Using

        Return pageTexts
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 方法

IronOCR原生讀取PDF。 限制是可用記憶體,而不是任意的文件大小門檻。 可搜尋PDF輸出在任何授權層次上都產生乾淨文件,無水印:

//IronOCRPDF: native support, no size limit, no watermarks
public List<string> ExtractTextFromPdf(string pdfPath)
{
    var pageTexts = new List<string>();

    using var input = new OcrInput();
    input.LoadPdf(pdfPath); //不是5MB ceiling

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

    foreach (var page in result.Pages)
        pageTexts.Add(page.Text);

    return pageTexts;
}

// Generate searchable PDF — no watermarks
public void CreateSearchablePdf(string inputPath, string outputPath)
{
    var result = new IronTesseract().Read(inputPath);
    result.SaveAsSearchablePdf(outputPath);
}
//IronOCRPDF: native support, no size limit, no watermarks
public List<string> ExtractTextFromPdf(string pdfPath)
{
    var pageTexts = new List<string>();

    using var input = new OcrInput();
    input.LoadPdf(pdfPath); //不是5MB ceiling

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

    foreach (var page in result.Pages)
        pageTexts.Add(page.Text);

    return pageTexts;
}

// Generate searchable PDF — no watermarks
public void CreateSearchablePdf(string inputPath, string outputPath)
{
    var result = new IronTesseract().Read(inputPath);
    result.SaveAsSearchablePdf(outputPath);
}
Imports System.Collections.Generic

'IronOCRPDF: native support, no size limit, no watermarks
Public Function ExtractTextFromPdf(ByVal pdfPath As String) As List(Of String)
    Dim pageTexts As New List(Of String)()

    Using input As New OcrInput()
        input.LoadPdf(pdfPath) '不是5MB ceiling

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

        For Each page In result.Pages
            pageTexts.Add(page.Text)
        Next
    End Using

    Return pageTexts
End Function

' Generate searchable PDF — no watermarks
Public Sub CreateSearchablePdf(ByVal inputPath As String, ByVal outputPath As String)
    Dim result = New IronTesseract().Read(inputPath)
    result.SaveAsSearchablePdf(outputPath)
End Sub
$vbLabelText   $csharpLabel

含密碼保護的PDF是一個參數:input.LoadPdf("encrypted.pdf", Password: "secret")PDF OCR做法指南詳細說明頁面範圍選擇和密碼處理。 有關可搜尋PDF生成模式,請查看可搜尋PDF怎麼做法。

錯誤處理

OCR.space透過兩個獨立渠道傳送錯誤:HTTP狀態碼和JSON IsErroredOnProcessing 標誌。 生產程式碼必須檢查雙方,當IsErroredOnProcessing 為真時解析JSON錯誤陣列,並檢查每頁的FileParseExitCode 值以了解局部故障。 這一切都沒有型別化——所有東西都是從JsonDocument解析字串:

// OCR.space: two error layers, all string-based
public async Task<string> SafeExtract(HttpClient client, string apiKey, string imagePath)
{
    try
    {
        byte[] imageBytes = File.ReadAllBytes(imagePath);
        string base64 = Convert.ToBase64String(imageBytes);

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("apikey", apiKey),
            new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}")
        });

        var response = await client.PostAsync("https://api.ocr.space/parse/image", content);

        if (!response.IsSuccessStatusCode)
        {
            switch (response.StatusCode)
            {
                case System.Net.HttpStatusCode.Unauthorized:
                    throw new Exception("Invalid API key");
                case System.Net.HttpStatusCode.TooManyRequests:
                    throw new Exception("Rate limit exceeded — wait and retry");
                case System.Net.HttpStatusCode.PaymentRequired:
                    throw new Exception("Quota exceeded — upgrade plan");
                default:
                    throw new Exception($"API error: {response.StatusCode}");
            }
        }

        // Second error layer: JSON-level failures
        string json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);

        if (doc.RootElement.TryGetProperty("IsErroredOnProcessing", out var isError)
            && isError.GetBoolean())
        {
            var messages = new List<string>();
            if (doc.RootElement.TryGetProperty("ErrorMessage", out var errors))
            {
                foreach (var e in errors.EnumerateArray())
                    messages.Add(e.GetString() ?? "Unknown error");
            }
            throw new Exception(string.Join("; ", messages));
        }

        // Third layer: per-page exit codes
        if (doc.RootElement.TryGetProperty("ParsedResults", out var results))
        {
            foreach (var result in results.EnumerateArray())
            {
                if (result.TryGetProperty("FileParseExitCode", out var exitCode)
                    && exitCode.GetInt32() != 1)
                    throw new Exception($"Page parse failed: exit code {exitCode.GetInt32()}");
            }
        }

        return doc.RootElement
            .GetProperty("ParsedResults")[0]
            .GetProperty("ParsedText")
            .GetString() ?? string.Empty;
    }
    catch (JsonException ex)
    {
        throw new Exception($"Invalid API response: {ex.Message}");
    }
    catch (HttpRequestException ex)
    {
        throw new Exception($"Network error: {ex.Message}");
    }
}
// OCR.space: two error layers, all string-based
public async Task<string> SafeExtract(HttpClient client, string apiKey, string imagePath)
{
    try
    {
        byte[] imageBytes = File.ReadAllBytes(imagePath);
        string base64 = Convert.ToBase64String(imageBytes);

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("apikey", apiKey),
            new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}")
        });

        var response = await client.PostAsync("https://api.ocr.space/parse/image", content);

        if (!response.IsSuccessStatusCode)
        {
            switch (response.StatusCode)
            {
                case System.Net.HttpStatusCode.Unauthorized:
                    throw new Exception("Invalid API key");
                case System.Net.HttpStatusCode.TooManyRequests:
                    throw new Exception("Rate limit exceeded — wait and retry");
                case System.Net.HttpStatusCode.PaymentRequired:
                    throw new Exception("Quota exceeded — upgrade plan");
                default:
                    throw new Exception($"API error: {response.StatusCode}");
            }
        }

        // Second error layer: JSON-level failures
        string json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);

        if (doc.RootElement.TryGetProperty("IsErroredOnProcessing", out var isError)
            && isError.GetBoolean())
        {
            var messages = new List<string>();
            if (doc.RootElement.TryGetProperty("ErrorMessage", out var errors))
            {
                foreach (var e in errors.EnumerateArray())
                    messages.Add(e.GetString() ?? "Unknown error");
            }
            throw new Exception(string.Join("; ", messages));
        }

        // Third layer: per-page exit codes
        if (doc.RootElement.TryGetProperty("ParsedResults", out var results))
        {
            foreach (var result in results.EnumerateArray())
            {
                if (result.TryGetProperty("FileParseExitCode", out var exitCode)
                    && exitCode.GetInt32() != 1)
                    throw new Exception($"Page parse failed: exit code {exitCode.GetInt32()}");
            }
        }

        return doc.RootElement
            .GetProperty("ParsedResults")[0]
            .GetProperty("ParsedText")
            .GetString() ?? string.Empty;
    }
    catch (JsonException ex)
    {
        throw new Exception($"Invalid API response: {ex.Message}");
    }
    catch (HttpRequestException ex)
    {
        throw new Exception($"Network error: {ex.Message}");
    }
}
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading.Tasks

Public Class OCRSpace
    Public Async Function SafeExtract(client As HttpClient, apiKey As String, imagePath As String) As Task(Of String)
        Try
            Dim imageBytes As Byte() = File.ReadAllBytes(imagePath)
            Dim base64 As String = Convert.ToBase64String(imageBytes)

            Dim content As New FormUrlEncodedContent(New KeyValuePair(Of String, String)() {
                New KeyValuePair(Of String, String)("apikey", apiKey),
                New KeyValuePair(Of String, String)("base64Image", $"data:image/png;base64,{base64}")
            })

            Dim response As HttpResponseMessage = Await client.PostAsync("https://api.ocr.space/parse/image", content)

            If Not response.IsSuccessStatusCode Then
                Select Case response.StatusCode
                    Case System.Net.HttpStatusCode.Unauthorized
                        Throw New Exception("Invalid API key")
                    Case System.Net.HttpStatusCode.TooManyRequests
                        Throw New Exception("Rate limit exceeded — wait and retry")
                    Case System.Net.HttpStatusCode.PaymentRequired
                        Throw New Exception("Quota exceeded — upgrade plan")
                    Case Else
                        Throw New Exception($"API error: {response.StatusCode}")
                End Select
            End If

            ' Second error layer: JSON-level failures
            Dim json As String = Await response.Content.ReadAsStringAsync()
            Using doc As JsonDocument = JsonDocument.Parse(json)
                If doc.RootElement.TryGetProperty("IsErroredOnProcessing", ByRef isError) AndAlso isError.GetBoolean() Then
                    Dim messages As New List(Of String)()
                    If doc.RootElement.TryGetProperty("ErrorMessage", ByRef errors) Then
                        For Each e In errors.EnumerateArray()
                            messages.Add(e.GetString() ?? "Unknown error")
                        Next
                    End If
                    Throw New Exception(String.Join("; ", messages))
                End If

                ' Third layer: per-page exit codes
                If doc.RootElement.TryGetProperty("ParsedResults", ByRef results) Then
                    For Each result In results.EnumerateArray()
                        If result.TryGetProperty("FileParseExitCode", ByRef exitCode) AndAlso exitCode.GetInt32() <> 1 Then
                            Throw New Exception($"Page parse failed: exit code {exitCode.GetInt32()}")
                        End If
                    Next
                End If

                Return doc.RootElement _
                    .GetProperty("ParsedResults")(0) _
                    .GetProperty("ParsedText") _
                    .GetString() ?? String.Empty
            End Using
        Catch ex As JsonException
            Throw New Exception($"Invalid API response: {ex.Message}")
        Catch ex As HttpRequestException
            Throw New Exception($"Network error: {ex.Message}")
        End Try
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR引發標準.NET例外並帶有明確資訊。 沒有JSON解析,沒有HTTP狀態碼切換,沒有分層錯誤提取:

// IronOCR: standard .NET exceptions
public string SafeExtract(string imagePath)
{
    try
    {
        var result = new IronTesseract().Read(imagePath);
        return result.Text;
    }
    catch (FileNotFoundException)
    {
        throw; // File not found — straightforward
    }
    catch (Exception ex) when (ex.Message.Contains("corrupt"))
    {
        throw new Exception($"Image file is corrupt: {imagePath}");
    }
    //不是JSON errors.不是HTTP errors.不是rate limit errors.
}
// IronOCR: standard .NET exceptions
public string SafeExtract(string imagePath)
{
    try
    {
        var result = new IronTesseract().Read(imagePath);
        return result.Text;
    }
    catch (FileNotFoundException)
    {
        throw; // File not found — straightforward
    }
    catch (Exception ex) when (ex.Message.Contains("corrupt"))
    {
        throw new Exception($"Image file is corrupt: {imagePath}");
    }
    //不是JSON errors.不是HTTP errors.不是rate limit errors.
}
Imports IronOcr

Public Function SafeExtract(imagePath As String) As String
    Try
        Dim result = New IronTesseract().Read(imagePath)
        Return result.Text
    Catch ex As FileNotFoundException
        Throw ' File not found — straightforward
    Catch ex As Exception When ex.Message.Contains("corrupt")
        Throw New Exception($"Image file is corrupt: {imagePath}")
    End Try
    '不是JSON errors.不是HTTP errors.不是rate limit errors.
End Function
$vbLabelText   $csharpLabel

IronTesseract API參考文件OcrResult API參考文件記錄了型別化結果結構,Confidence屬性可評估擷取質量。

API 地圖參考

OCR.space概念 IronOCR 等效
POST https://api.ocr.space/parse/image new IronTesseract().Read(path)
FormUrlEncodedContent / MultipartFormDataContent OcrInput
base64Image 參數 input.LoadImage(path)input.LoadImage(bytes)
language 參數 ocr.Language = OcrLanguage.English
OCREngine 參數 引擎內部管理
isOverlayRequired 參數 result.Words / result.Lines (始終可用)
isCreateSearchablePdf 參數 result.SaveAsSearchablePdf(outputPath)
filetype=PDF 參數 input.LoadPdf(path)
ParsedResults[0].ParsedText result.Text
ParsedResults[n] (每頁) result.Pages[n].Text
IsErroredOnProcessing JSON標誌 標準.NET例外
FileParseExitCode 每頁標誌 標準.NET例外
ProcessingTimeInMilliseconds 隱含 — 無需額外解析
HTTP 429 /速率限制 不適用 — 無速率限制
自訂OcrResult POCO (使用者自定義) IronOcr.OcrResult (由NuGet提供)
自訂OcrSpaceException (使用者自定義) 標準.NET例外型別
SemaphoreSlim 速限制器(使用者建) 不需要
每次請求中的API金鑰 IronOcr.License.LicenseKey (在啟動時一次)

團隊考慮將從OCR.space移至IronOCR時

進行負載測試時觸碰到免費層

OCR.space的每IP每天500次請求上限是一道硬牆不是軟建議。 團隊在負載測試或階段部署期間發現了這一點,多個開發者共用辦公室IP地址。 運行針對OCR.space整合測試的CI/CD流水線會在業務日終結前耗盡每日配額。 那時應用程式的失敗不是因為程式碼錯誤,而是因為一個第三方計數器到達了一個任意閾值。 轉向本地處理完全消除了這類故障——因為處理過程都是在程中執行,並沒有超越配額。

合規和資料分類審查

將文件歸類為PII、PHI或金融敏感內容的安全審查創造了OCR.space的二元問題:該服務沒有本地部署路徑。 缺少HIPAA場景的商業夥伴協議(BAA)等效文件,沒有明確規範歐盟資料傳輸於GDPR框架的資料處理協議結構,並且即使有合同控件也無法防止文件傳輸的技術機制。 在雲端處理的文件上收到相應合規審核結果的團隊很需要一個本地解決方案。 IronOCR通過設計滿足這一需求——文件從未離開應用程式伺服器。 授權頁面記錄了永久授權結構,這也透過從範圍中消除雲端供應商審核來簡化合規文件。

整合程式碼維護負擔

OCR.space的整合隨其功能需求積累技術債務。 最初的HTTP客戶端是可控的。 然後團隊新增重試邏輯。 接著他們新增了每IP速率限制跟蹤,因為多個作業共用一個地址。 然後他們再加入文件大小預先驗證步驟,然後發現引擎1與引擎2的回應JSON錯誤結構不同,且再加一個分支。 六個月後,每位新團隊成員在深入了解與OCR有關的任何東西之前,必須先要了解其OCR.space整合的400行基礎設施程式碼。 當該團隊以$999為基礎來評估IronOCR一次性許可的維護成本時,算術上是一目了然。

結構化輸出要求

OCR.space僅返回純文字,沒有其他。 回應中沒有單詞座標,沒有線界,沒有段落分段,沒有對字信心得分。 需要從發票中提取特定欄位(例如已知區域的供應商名稱、右下角的總金額)的應用程式在OCR.space的輸出中沒有結構化基礎構建的基礎。 IronOCR揭露出以坐標及信心水準在每個細化層級上展示完整的文件層級:頁、段落、行和單詞。 基於區域處理可以允許針對特定文件區域進行定位,而無需後處理整頁輸出。 讀取結果指南區域基於OCR指南詳細涵蓋這些模式。

超出免費層級的成本增長

在每月25,000請求上下,免費層對低容量情境是可行的。 超過這一點,適用於付費層——聯繫OCR.space以獲取當前報價。 這些成本隨時間累積,直到比較IronOCR一次性存在的$999 永久許可,永不會有每次請求費用。 計劃的文件量增長過25,000每月的團隊,有一個顯而易見的本地處理成本案例。

常見的遷移考量

用方法調用替換HTTP客戶端

從OCR.space遷移到IronOCR在架構上很簡單,因為兩者都從文件返回文字。 HTTP客戶端、JSON解析器、速率限制器以及自訂例外型別消失。 取而代之的是一個NuGet包和方法呼叫。 在服務邊界的前後轉換:

// Before: OCR.space service (simplified — actual implementation is200+lines)
public class OcrSpaceService : IDisposable
{
    private readonly HttpClient _client = new();
    private readonly string _apiKey;

    public OcrSpaceService(string apiKey) => _apiKey = apiKey;

    public async Task<string> ProcessDocument(string path)
    {
        byte[] bytes = File.ReadAllBytes(path);
        string base64 = Convert.ToBase64String(bytes);

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("apikey", _apiKey),
            new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}")
        });

        var response = await _client.PostAsync("https://api.ocr.space/parse/image", content);
        response.EnsureSuccessStatusCode();

        string json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);
        return doc.RootElement.GetProperty("ParsedResults")[0].GetProperty("ParsedText").GetString()!;
    }

    public void Dispose() => _client.Dispose();
}

// After:IronOCRservice — no HttpClient, no JSON, no API key
public class IronOcrService
{
    private readonly IronTesseract _ocr = new();

    public string ProcessDocument(string path)
    {
        return _ocr.Read(path).Text;
    }
    //不是Dispose — no external connections to close
}
// Before: OCR.space service (simplified — actual implementation is200+lines)
public class OcrSpaceService : IDisposable
{
    private readonly HttpClient _client = new();
    private readonly string _apiKey;

    public OcrSpaceService(string apiKey) => _apiKey = apiKey;

    public async Task<string> ProcessDocument(string path)
    {
        byte[] bytes = File.ReadAllBytes(path);
        string base64 = Convert.ToBase64String(bytes);

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("apikey", _apiKey),
            new KeyValuePair<string, string>("base64Image", $"data:image/png;base64,{base64}")
        });

        var response = await _client.PostAsync("https://api.ocr.space/parse/image", content);
        response.EnsureSuccessStatusCode();

        string json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);
        return doc.RootElement.GetProperty("ParsedResults")[0].GetProperty("ParsedText").GetString()!;
    }

    public void Dispose() => _client.Dispose();
}

// After:IronOCRservice — no HttpClient, no JSON, no API key
public class IronOcrService
{
    private readonly IronTesseract _ocr = new();

    public string ProcessDocument(string path)
    {
        return _ocr.Read(path).Text;
    }
    //不是Dispose — no external connections to close
}
Imports System
Imports System.Net.Http
Imports System.IO
Imports System.Threading.Tasks
Imports System.Collections.Generic
Imports System.Text.Json

' Before: OCR.space service (simplified — actual implementation is 200+ lines)
Public Class OcrSpaceService
    Implements IDisposable

    Private ReadOnly _client As HttpClient = New HttpClient()
    Private ReadOnly _apiKey As String

    Public Sub New(apiKey As String)
        _apiKey = apiKey
    End Sub

    Public Async Function ProcessDocument(path As String) As Task(Of String)
        Dim bytes As Byte() = File.ReadAllBytes(path)
        Dim base64 As String = Convert.ToBase64String(bytes)

        Dim content As New FormUrlEncodedContent(New KeyValuePair(Of String, String)() {
            New KeyValuePair(Of String, String)("apikey", _apiKey),
            New KeyValuePair(Of String, String)("base64Image", $"data:image/png;base64,{base64}")
        })

        Dim response As HttpResponseMessage = Await _client.PostAsync("https://api.ocr.space/parse/image", content)
        response.EnsureSuccessStatusCode()

        Dim json As String = Await response.Content.ReadAsStringAsync()
        Using doc As JsonDocument = JsonDocument.Parse(json)
            Return doc.RootElement.GetProperty("ParsedResults")(0).GetProperty("ParsedText").GetString()
        End Using
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        _client.Dispose()
    End Sub
End Class

' After: IronOCR service — no HttpClient, no JSON, no API key
Public Class IronOcrService
    Private ReadOnly _ocr As New IronTesseract()

    Public Function ProcessDocument(path As String) As String
        Return _ocr.Read(path).Text
    End Function
    ' 不是Dispose — no external connections to close
End Class
$vbLabelText   $csharpLabel

因為沒有需要管理的IDisposable實施消失。

API金鑰移除

OCR.space每個HTTP請求都需要一個API金鑰。該鑰必須安全儲存、暴露時進行旋轉,並從源程式碼控管中排除。 IronOCR使用在應用程式啟動時設置任一次的許可金鑰,通常來自環境變數或配置系統:

// At application startup — once, not per request
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
// At application startup — once, not per request
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
' At application startup — once, not per request
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
$vbLabelText   $csharpLabel

遷移之後,API金鑰的輪替過程、機密管理配置和每次請求的金鑰注入邏輯都變得不起必要。

遷移後的預處理

OCR.space在返回結果之前應用了伺服器端處理,但開發者無法控制這些處理包含或不包含什麼。 IronOCR揭露了明確的預處理方法。 如果文件質量是一個問題——偏斜的掃描、低對比度的影印件、嘈雜的傳真輸出——預處理流水線包括三到五個方法調用:

using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");
input.Deskew();
input.DeNoise();
input.Contrast();
input.Binarize();
input.EnhanceResolution(300);
var result = new IronTesseract().Read(input);
using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");
input.Deskew();
input.DeNoise();
input.Contrast();
input.Binarize();
input.EnhanceResolution(300);
var result = new IronTesseract().Read(input);
Imports IronOcr

Using input As New OcrInput()
    input.LoadImage("low-quality-scan.jpg")
    input.Deskew()
    input.DeNoise()
    input.Contrast()
    input.Binarize()
    input.EnhanceResolution(300)
    Dim result = New IronTesseract().Read(input)
End Using
$vbLabelText   $csharpLabel

圖像質量矯正指南圖像顏色矯正指南記錄了每個可用過濾器,並通過範例顯示前/後準確性的差異。

異步 vs. 同步

因為涉及 HTTP 來往過程,OCR.space 的呼叫有其固有的異步特性。 IronOCR的呼叫預設為同步,這在非網頁上下文中簡化了程式碼。 對於需要非阻塞執行的ASP.NET和伺服器場景,IronOCR通過Task.Run 包裝或直接的異步API支援異步操作。 異步OCR指南涵蓋了這些模式。 從以前等待雲端呼叫的服務方法中移除await,在僅因OCR.space要求的情況下採用的異步環境簡化了呼叫棧。

其他IronOCR功能

上面部分未涵蓋的功能,每一項都代表OCR.space中無可比擬的功能:

  • hOCR匯出: result.SaveAsHocrFile("output.hocr") 生成符合標準的hOCR輸出,適合下游文件處理流水線。
  • 進度跟踪: 為執行時間較長的多頁批次操作訂閱進度事件,使進度欄和自詆降詞由於於使用者介面應用程式。
  • 表格提取: 結構化表格資料通過結果模型存取,支持需要列對準的帳單行項目資料提取等應用案例。
  • 專業文件閱讀 護照 MRZ 區塊、牌照車牌、MICR 支票線及手寫識別是目的建能力,可以透過相同的IronTesseract API得到。

.NET 相容性和未來準備

IronOCR 將目標設置為 .NET 6、.NET 7、.NET 8 和 .NET 9,並為每個 LTS 和 STS 版本提供積極支援。 程式庫可可在 Windows x64、Windows x86、Linux x64、macOS、Docker、AWS Lambda 和 Azure App Service 上運行,——所有這一切在同一個 NuGet 程式包中,沒有平台特定的配置。 OCR.space 從每個部署環境需要對外部 HTTPS 的需求,這排除此氣隙網路、限制性企業代理和其政策封鎖對外部 API 的基礎設施。IronOCR沒有運行時網路要求。 其完全在程中執行。 隨著.NET 10將於2026年末發行,IronOCR維持跨.NET主版本的相容性的記錄而未進行整合變化提供了一個持續性。

結論

OCR.space 擁有一個真實且有用的利基:需要在週末上手的開發者,探索文字提取概念的學生,或無需合規要求、每月少於25,000文件的個人工具。 對於那個觀眾,免費層提供了真正的價值。

問題是.NET開發者通常不構建周末原型。他們正在構建發票系統、醫療記錄處理器、文件歸檔流水線及合規敏感的商業應用程式。 對於這些上下文而言,OCR.space缺乏的NuGet套件不是一種不便證它是結構上的不相容性。 每小時花在構建HTTP客戶端,速率限制器,JSON解析器及重試基礎設施上的是時間未用於應用程式真正需求的。 這一成本是前期的,但由於整合生命週期而持續存在維護成本。

IronOCR特別針對定義了每個OCR.space整合之特定失敗模式:缺少真實SDK。 一個NuGet套件,一個方法呼叫,無HTTP水道,無速率限製,無文件被傳輸到外部伺服器。 $999 的入門價格是一個一次性費用,隨著體量增長,OCR.space訂閱費用會隨著時間的推移超過該費用。 對於具有合規要求的團隊而言,無論成本多少,本地處理是唯一的選擇。

最終比對縮減為一個問題:應用程式需要OCR作為外部REST依賴,還是作為程式庫功能? 對於生產.NET應用程式,答案幾乎總是後者。

請注意OCR.space和Tesseract是其各自所有者的註冊商標。 本網站不與谷歌或OCR.space關聯,不受其認可或協贊。 所有產品名稱、標誌和品牌均為其各自所有者的財產。 比較僅供資訊用途,並反映撰寫時獲得的公開資訊。

常見問題

什麼是OCR.space API?

OCR.space API是一個由開發者和企業用來從圖像和文件中提取文字的OCR解決方案。在.NET應用程式開發中,它是多個與IronOCR一起評估的OCR選項之一。

IronOCR相較於OCR.space API對於.NET開發者如何?

IronOCR是一個NuGet原生的.NET OCR程式庫,使用IronTesseract作為其核心引擎。相比OCR.space API,它提供更簡單的部署(無SDK安裝程式)、統一的定價,以及無COM互操作或雲端依賴的乾淨C# API。

IronOCR是否比OCR.space API更容易設置?

IronOCR 通過一個單一的 NuGet 套件安裝。無需 SDK 安裝程式、複製授權文件、註冊 COM 元件或管理單獨的運行時二進位檔案。整個 OCR 引擎都打包在套件中。

OCR.space API和IronOCR之間的準確性有哪些差異?

IronOCR 在標準商業文件、發票、收據和掃描表單上達到了高識別準確度。對於極度退化的文件或罕見文字,準確度會根據來源品質變化。IronOCR 包含影像預處理篩選器,以改善低品質輸入的識別。

IronOCR 支援 PDF 文字提取嗎?

是的。IronOCR 能從原生 PDF 和掃描的 PDF 圖像中一次性提取文字。它還支持多頁 TIFF 檔案、影像和流。對於掃描的 PDF,OCR 會逐頁應用並生成每頁的結果物件。

OCR.space API與IronOCR的授權如何比較?

IronOCR 採用固定費率的永久授權,無須為每頁或每次掃描支付費用。處理大量文件的組織無論數量多少都支付相同的授權費用。詳細資訊和批量定價在 IronOCR 授權頁面上。

IronOCR 支援哪些語言?

IronOCR 透過單獨的 NuGet 語言包支持 127 種語言。新增一種語言只需一個 'dotnet add package IronOcr.Languages.{Language}' 命令。無需手動放置文件或配置路徑。

如何在.NET專案中安裝IronOCR?

通過 NuGet 安裝:在套件管理員控制台或 CLI 中分別使用 'Install-Package IronOcr' 或 'dotnet add package IronOcr'。其他語言包也是這樣安裝的。無需本地 SDK 安裝程式。

IronOCR是否適用於Docker和容器化部署,不像OCR.space?

是的。IronOCR 通過其 NuGet 載於 Docker 容器中運行。授權金鑰通過環境變數設置。無需授權文件、SDK 路徑或量掛載來執行 OCR 引擎。

與OCR.space相比,我可以在購買前試用IronOCR嗎?

是的。IronOCR 試用模式中處理文件並在輸出上附上浮水印結果。您可以在購買授權前驗證自己文件的準確性。

IronOCR 支援條碼閱讀與文字提取嗎?

IronOCR 專注於文字提取和 OCR。對於條碼閱讀,Iron Software 提供了額外的 IronBarcode 程式庫。兩者均可個別使用或作為 Iron Suite 套件的一部分。

從OCR.space API遷移到IronOCR是否容易?

從OCR.space API遷移到IronOCR通常涉及用IronTesseract實例化替換初始化序列,移除COM生命週期管理,並更新API調用。大多數的遷移能顯著減少程式碼複雜性。

Kannaopat Udonpant
軟體工程師
在成為軟體工程師之前,Kannapat在日本北海道大學完成了環境資源博士學位。在攻讀學位期間,Kannapat還成為車輛機器人實驗室的一員,該實驗室隸屬於生產工程系。在2022年,他憑藉C#技能加入了Iron Software的工程團隊,專注於IronPDF。Kannapat珍視他的工作,因為他能直接向撰寫大部分IronPDF程式碼的開發者學習。除了同儕學習,Kannapat還喜歡在Iron Software工作的社交方面。不寫程式碼或文件時,Kannapat通常在他的PS5上玩遊戲或重看The Last of Us。

Iron 支援團隊

我們線上24小時,每週5天。
聊天
電子郵件
給我打電話