跳至页脚内容
使用 IRONOCR

.NET OCR SDK:一个用于 C# 的文本识别库

.NET OCR SDK是一个软件开发工具包,它允许 C# 和.NET应用程序以编程方式从图像、扫描的 PDF 和其他文档格式中提取文本。 IronOCR是一款可用于生产的.NET OCR SDK,它封装了一个经过优化的 Tesseract 5 引擎,具有预处理过滤器、条形码读取、可搜索的 PDF 输出以及对 125 多种语言的支持——所有这些都可以通过简洁的 C# API 访问,该 API 可在 Windows、Linux、macOS 和云平台上运行。

为什么IronOCR是您项目的理想.NET OCR SDK?

从零开始构建文本识别意味着管理图像预处理流程、语言数据文件、线程模型和输出解析——在提取第一个单词之前,需要数月的工作。 IronOCR通过提供经过实战检验的引擎来消除这种额外开销,您的团队可以在几分钟内将其添加到项目中。

它与原始 Tesseract 绑定之间的主要区别在于以下几个方面:

  • 可识别125 种以上语言和文字,包括手写文本
  • 内置滤镜:降噪、去斜、二值化、分辨率增强和对比度校正
  • 在同一次读取过程中检测条形码和二维码
  • 生成带有不可见文本层的可搜索 PDF,适用于归档工作流程
  • 用于高通量流水线的异步和并行批处理
  • 采用区域OCR技术,针对特定页面区域进行识别,以缩短处理时间
  • 支持Windows、Linux、macOS、Docker 和 Azure 等跨平台系统

根据Tesseract OCR 项目文档,原始 Tesseract 需要手动配置语言包、DPI 设置和输出模式。 IronOCR会自动处理所有这些操作,让您可以专注于提取的文本的含义,而不是如何提取它。

IronOCR与 Raw Tesseract 相比如何?

通过 P/Invoke 包装器或 Tesseract NuGet包使用原始 Tesseract 会让您负责:下载和放置 tessdata 语言文件、选择正确的页面分割模式、自行处理多页 TIFF 和 PDF 分割,以及如果您想要并行处理,则需要连接线程。 这些细节并非贵公司独有的问题。

IronOCR包裹了所有这些管道。 您将获得类型化的 API 接口、自动 tessdata 管理、内置 PDF 分割和重组功能,以及可在多个请求中重用的线程安全引擎。 权衡之下,生产用途需要付费许可——许可页面显示了当前的定价层级,其中包括免费的开发许可。

对于只需要开源依赖项的团队来说,原始的 Tesseract 加上自定义预处理是一种可行的方法。 对于需要快速交付可靠 OCR 的团队来说, IronOCR将集成界面简化为几行 C# 代码。

如何安装IronOCR .NET SDK?

安装通过NuGet进行,NuGet 是标准的.NET包管理器。 在您的项目目录中运行以下命令:

Install-Package IronOcr

对于 Visual Studio 用户,请在NuGet包管理器 GUI 中搜索 IronOcr,然后从那里安装。 有关包括手动 DLL 引用在内的完整安装选项,请参阅IronOCR安装文档

安装完成后,将许可证密钥添加到您的应用程序启动或 appsettings.json。 您可以开始免费试用,获取试用密钥,在评估期间解锁所有功能。

验证安装

安装完成后快速检查一下,确认所有线路连接正确。 创建一个面向.NET 10 的控制台应用程序:

using IronOcr;

// Minimal smoke test -- reads a single image and prints extracted text
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadImage("sample.png");
var result = ocr.Read(input);
Console.WriteLine(result.Text);
using IronOcr;

// Minimal smoke test -- reads a single image and prints extracted text
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadImage("sample.png");
var result = ocr.Read(input);
Console.WriteLine(result.Text);
$vbLabelText   $csharpLabel

如果控制台中出现文本,则表示 SDK 已安装且许可证密钥有效。 您已准备好构建生产工作流程。

如何在 C# 中从图像和 PDF 中提取文本?

核心提取模式在所有输入类型中都是一致的。您创建一个 IronTesseract 实例,将内容加载到 OcrInput 对象中,然后调用 Read()。 IronOCR可以根据文件扩展名自动检测文件格式,因此相同的代码路径可以处理 JPEG、PNG、TIFF、BMP 和多页 PDF 文件。

using IronOcr;

// Reusable OCR service encapsulating the IronTesseract engine
public class OcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public string ExtractText(string filePath)
    {
        using var input = new OcrInput();

        // LoadPdf for PDF files; LoadImage for raster formats
        if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(filePath);
        else
            input.LoadImage(filePath);

        return _ocr.Read(input).Text;
    }

    public async Task<string> ExtractTextAsync(string filePath)
    {
        using var input = new OcrInput();

        if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(filePath);
        else
            input.LoadImage(filePath);

        var result = await _ocr.ReadAsync(input);
        return result.Text;
    }
}
using IronOcr;

// Reusable OCR service encapsulating the IronTesseract engine
public class OcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public string ExtractText(string filePath)
    {
        using var input = new OcrInput();

        // LoadPdf for PDF files; LoadImage for raster formats
        if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(filePath);
        else
            input.LoadImage(filePath);

        return _ocr.Read(input).Text;
    }

    public async Task<string> ExtractTextAsync(string filePath)
    {
        using var input = new OcrInput();

        if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(filePath);
        else
            input.LoadImage(filePath);

        var result = await _ocr.ReadAsync(input);
        return result.Text;
    }
}
$vbLabelText   $csharpLabel

使用该服务的最高入口点:

using IronOcr;

var service = new OcrService();
string text = await service.ExtractTextAsync("invoice.pdf");
Console.WriteLine(text);
using IronOcr;

var service = new OcrService();
string text = await service.ExtractTextAsync("invoice.pdf");
Console.WriteLine(text);
$vbLabelText   $csharpLabel

IronTesseract 实例是线程安全的,并且设计用于重用。 在应用程序启动时创建一次(例如,通过ASP.NET Core中的依赖注入),而不是每次请求都实例化它。

对于多页 PDF,result.Pages 可让您逐页访问文本、置信度分数和边界框。 有关逐页迭代的详细信息,请参阅多页 PDF OCR 指南

如何利用预处理滤波器提高OCR准确率?

平板扫描仪、智能手机摄像头或传真机扫描出的原始图像经常存在噪点、旋转、对比度低和分辨率不足等问题。 IronOCR 的图像质量校正流程通过在读取调用之前链接有针对性的过滤器来解决每个问题。

using IronOcr;

public class AccuracyOptimizedOcr
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public string ProcessLowQualityDocument(string filePath)
    {
        using var input = new OcrInput();

        if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(filePath);
        else
            input.LoadImage(filePath);

        // Chain preprocessing filters in order of operation
        input.DeNoise();              // Remove scan artifacts and speckling
        input.Deskew();               // Correct page tilt up to 35 degrees
        input.Scale(150);             // Enlarge small text for better recognition
        input.Binarize();             // Convert to black/white for cleaner edges
        input.EnhanceResolution(300); // Sharpen blurry or low-DPI input

        var result = _ocr.Read(input);

        // Confidence below 70 often signals a preprocessing mismatch
        if (result.Confidence < 70)
            Console.WriteLine($"Warning: low confidence ({result.Confidence:F1}%)");

        return result.Text;
    }
}
using IronOcr;

public class AccuracyOptimizedOcr
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public string ProcessLowQualityDocument(string filePath)
    {
        using var input = new OcrInput();

        if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(filePath);
        else
            input.LoadImage(filePath);

        // Chain preprocessing filters in order of operation
        input.DeNoise();              // Remove scan artifacts and speckling
        input.Deskew();               // Correct page tilt up to 35 degrees
        input.Scale(150);             // Enlarge small text for better recognition
        input.Binarize();             // Convert to black/white for cleaner edges
        input.EnhanceResolution(300); // Sharpen blurry or low-DPI input

        var result = _ocr.Read(input);

        // Confidence below 70 often signals a preprocessing mismatch
        if (result.Confidence < 70)
            Console.WriteLine($"Warning: low confidence ({result.Confidence:F1}%)");

        return result.Text;
    }
}
$vbLabelText   $csharpLabel

筛选器选择指南:

  • DeNoise() -- 用于扫描过程中出现严重斑点或压缩伪影的情况
  • Deskew() -- 当以一定角度拍摄文档时使用; 请参阅页面旋转检测以实现自动检测
  • Scale() -- 用于小字或低于 150 DPI 的输入; 通常情况下,数值在 150-200 之间能获得最佳结果
  • Binarize() -- 用于彩色或渐变背景; 将图像转换为纯黑白图像
  • EnhanceResolution() -- 用于模糊或低对比度文本; Tesseract 的最佳 DPI 为 300 DPI。

《国际文档分析与识别杂志》上发表的研究一致表明,二值化和去斜是提高字符识别率的两个影响最大的预处理步骤。 将两者作为任何生产流程的基准。

IronOCR预处理滤波器及其主要应用案例
筛选 问题已解决 何时申请
`DeNoise()` 扫描仪伪影、散斑噪声 任何平板扫描仪或传真扫描仪
`Deskew()` 页面倾斜和旋转 拍摄或错位的文件
`Scale()` 小字或低DPI 输入分辨率低于 150 DPI
`Binarize()` 彩色背景,渐变 彩色纸张或带水印的表格
`EnhanceResolution()` 模糊和低对比度 相机拍摄并压缩成JPEG格式

如何构建生产批量处理流水线?

单个文档的提取很简单,但生产场景涉及成百上千个文件到达队列、共享文件夹或云存储中。 IronOCR 的异步 API 和线程安全引擎使其适用于并行工作负载。

using IronOcr;
using Microsoft.Extensions.Logging;

public class ProductionOcrService
{
    private readonly IronTesseract _ocr;
    private readonly ILogger<ProductionOcrService> _logger;

    public ProductionOcrService(ILogger<ProductionOcrService> logger)
    {
        _logger = logger;
        _ocr = new IronTesseract
        {
            Configuration =
            {
                RenderSearchablePdfsAndHocr = true,
                ReadBarCodes = true
            }
        };
    }

    public async Task<IReadOnlyList<string>> ProcessBatchAsync(
        IEnumerable<string> filePaths,
        int maxDegreeOfParallelism = 4)
    {
        var results = new System.Collections.Concurrent.ConcurrentBag<string>();

        var options = new ParallelOptions
        {
            MaxDegreeOfParallelism = maxDegreeOfParallelism
        };

        await Parallel.ForEachAsync(filePaths, options, async (filePath, ct) =>
        {
            try
            {
                using var input = new OcrInput();

                if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
                    input.LoadPdf(filePath);
                else
                    input.LoadImage(filePath);

                var result = await _ocr.ReadAsync(input);
                results.Add(result.Text);
                _logger.LogInformation("Processed {FilePath} at {Confidence:F1}% confidence",
                    filePath, result.Confidence);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "OCR failed for {FilePath}", filePath);
                results.Add(string.Empty);
            }
        });

        return results.ToList();
    }

    public void CreateSearchablePdf(string inputPath, string outputPath)
    {
        using var input = new OcrInput();

        if (inputPath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(inputPath);
        else
            input.LoadImage(inputPath);

        _ocr.Read(input).SaveAsSearchablePdf(outputPath);
        _logger.LogInformation("Searchable PDF written to {OutputPath}", outputPath);
    }
}
using IronOcr;
using Microsoft.Extensions.Logging;

public class ProductionOcrService
{
    private readonly IronTesseract _ocr;
    private readonly ILogger<ProductionOcrService> _logger;

    public ProductionOcrService(ILogger<ProductionOcrService> logger)
    {
        _logger = logger;
        _ocr = new IronTesseract
        {
            Configuration =
            {
                RenderSearchablePdfsAndHocr = true,
                ReadBarCodes = true
            }
        };
    }

    public async Task<IReadOnlyList<string>> ProcessBatchAsync(
        IEnumerable<string> filePaths,
        int maxDegreeOfParallelism = 4)
    {
        var results = new System.Collections.Concurrent.ConcurrentBag<string>();

        var options = new ParallelOptions
        {
            MaxDegreeOfParallelism = maxDegreeOfParallelism
        };

        await Parallel.ForEachAsync(filePaths, options, async (filePath, ct) =>
        {
            try
            {
                using var input = new OcrInput();

                if (filePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
                    input.LoadPdf(filePath);
                else
                    input.LoadImage(filePath);

                var result = await _ocr.ReadAsync(input);
                results.Add(result.Text);
                _logger.LogInformation("Processed {FilePath} at {Confidence:F1}% confidence",
                    filePath, result.Confidence);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "OCR failed for {FilePath}", filePath);
                results.Add(string.Empty);
            }
        });

        return results.ToList();
    }

    public void CreateSearchablePdf(string inputPath, string outputPath)
    {
        using var input = new OcrInput();

        if (inputPath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(inputPath);
        else
            input.LoadImage(inputPath);

        _ocr.Read(input).SaveAsSearchablePdf(outputPath);
        _logger.LogInformation("Searchable PDF written to {OutputPath}", outputPath);
    }
}
$vbLabelText   $csharpLabel

MaxDegreeOfParallelism 限制可防止文件过大时内存耗尽。 在四核服务器上,数值 4 效果很好; 只有在分析内存使用情况后才能增加该值。 对于Azure Functions 或 AWS Lambda 部署,请将每个函数实例的并发数设置为 1,并改为水平扩展。

CreateSearchablePdf 生成一个 PDF,其中原始图像保留为可见图层,识别出的文本以不可见的方式嵌入在其下方。 这使得 PDF 查看器能够进行全文搜索,搜索引擎能够对其进行索引——这是文档管理系统的常见要求。

生产环境中的置信度评分监控

每个 OcrResult 都公开一个 Confidence 属性(0-100),该属性反映引擎对识别文本的确定程度。 在日志记录基础架构中跟踪此指标,可以在文档质量下降时发出预警信号——例如,如果扫描仪的校准发生漂移,或者新的文档供应商发送的 DPI 低于预期。

一个实用的阈值策略:置信度低于 80 时记录警告,低于 70 时触发预处理重试,低于 60 时标记文档以供人工审核。这种分层方法可以在质量问题导致下游系统出现隐性数据损坏之前将其捕获。

Microsoft .NET日志记录文档涵盖了上述批处理服务中使用的 ILogger 模式,适用于与ASP.NET Core 内置 DI 容器集成的团队。

如何从扫描文档中提取结构化数据?

文本提取是第一步。第二步是将文本解析成应用程序可以操作的类型字段。 此模式结合了 IronOCR 的读取过程和 .NET 的 Regex,以从发票、表单和报告中提取结构化数据。

using IronOcr;
using System.Text.RegularExpressions;

public record Invoice(
    string? InvoiceNumber,
    DateOnly? Date,
    decimal? TotalAmount,
    string RawText
);

public class InvoiceOcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public Invoice ExtractInvoiceData(string invoicePath)
    {
        using var input = new OcrInput();

        if (invoicePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(invoicePath);
        else
            input.LoadImage(invoicePath);

        input.DeNoise();
        input.Deskew();

        var result = _ocr.Read(input);
        string text = result.Text;

        return new Invoice(
            InvoiceNumber: ExtractInvoiceNumber(text),
            Date: ExtractDate(text),
            TotalAmount: ExtractAmount(text),
            RawText: text
        );
    }

    private static string? ExtractInvoiceNumber(string text)
    {
        var match = Regex.Match(text, @"Invoice\s*#?:?\s*(\S+)", RegexOptions.IgnoreCase);
        return match.Success ? match.Groups[1].Value : null;
    }

    private static DateOnly? ExtractDate(string text)
    {
        // Numeric format: MM/DD/YYYY
        var numeric = Regex.Match(text, @"\b(\d{1,2}/\d{1,2}/\d{2,4})\b");
        if (numeric.Success && DateTime.TryParse(numeric.Groups[1].Value, out var d1))
            return DateOnly.FromDateTime(d1);

        // Written format: January 15, 2025
        var written = Regex.Match(text,
            @"\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})\b",
            RegexOptions.IgnoreCase);
        if (written.Success && DateTime.TryParse(written.Value, out var d2))
            return DateOnly.FromDateTime(d2);

        return null;
    }

    private static decimal? ExtractAmount(string text)
    {
        var match = Regex.Match(text, @"\$\s*(\d+(?:\.\d{2})?)");
        return match.Success && decimal.TryParse(match.Groups[1].Value, out var amt)
            ? amt
            : null;
    }
}
using IronOcr;
using System.Text.RegularExpressions;

public record Invoice(
    string? InvoiceNumber,
    DateOnly? Date,
    decimal? TotalAmount,
    string RawText
);

public class InvoiceOcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public Invoice ExtractInvoiceData(string invoicePath)
    {
        using var input = new OcrInput();

        if (invoicePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            input.LoadPdf(invoicePath);
        else
            input.LoadImage(invoicePath);

        input.DeNoise();
        input.Deskew();

        var result = _ocr.Read(input);
        string text = result.Text;

        return new Invoice(
            InvoiceNumber: ExtractInvoiceNumber(text),
            Date: ExtractDate(text),
            TotalAmount: ExtractAmount(text),
            RawText: text
        );
    }

    private static string? ExtractInvoiceNumber(string text)
    {
        var match = Regex.Match(text, @"Invoice\s*#?:?\s*(\S+)", RegexOptions.IgnoreCase);
        return match.Success ? match.Groups[1].Value : null;
    }

    private static DateOnly? ExtractDate(string text)
    {
        // Numeric format: MM/DD/YYYY
        var numeric = Regex.Match(text, @"\b(\d{1,2}/\d{1,2}/\d{2,4})\b");
        if (numeric.Success && DateTime.TryParse(numeric.Groups[1].Value, out var d1))
            return DateOnly.FromDateTime(d1);

        // Written format: January 15, 2025
        var written = Regex.Match(text,
            @"\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})\b",
            RegexOptions.IgnoreCase);
        if (written.Success && DateTime.TryParse(written.Value, out var d2))
            return DateOnly.FromDateTime(d2);

        return null;
    }

    private static decimal? ExtractAmount(string text)
    {
        var match = Regex.Match(text, @"\$\s*(\d+(?:\.\d{2})?)");
        return match.Success && decimal.TryParse(match.Groups[1].Value, out var amt)
            ? amt
            : null;
    }
}
$vbLabelText   $csharpLabel

当您确切知道表单上每个字段的位置时,这种方法与区域 OCR结合使用效果很好。 通过提供边界矩形,您可以跳过整页识别,仅针对包含发票号码或总额的区域,从而大大缩短固定布局文档的处理时间。

对于包括表格和结构化表单在内的更高级的提取场景,请查看产品网站上的IronOCR数据提取示例

如何在.NET中处理多语言OCR?

许多组织处理的文件使用多种语言——进出口表格、国际合同或多语言客户提交的文件。 IronOCR通过允许您在读取调用之前配置语言包来解决这个问题。

using IronOcr;

// Configure multi-language recognition
var ocr = new IronTesseract();
ocr.Language = OcrLanguage.EnglishBest;  // Swap for any of 125+ supported languages

// For mixed-language documents, combine language packs
ocr.AddSecondaryLanguage(OcrLanguage.German);

using var input = new OcrInput();
input.LoadPdf("multilingual-contract.pdf");
var result = ocr.Read(input);
Console.WriteLine(result.Text);
using IronOcr;

// Configure multi-language recognition
var ocr = new IronTesseract();
ocr.Language = OcrLanguage.EnglishBest;  // Swap for any of 125+ supported languages

// For mixed-language documents, combine language packs
ocr.AddSecondaryLanguage(OcrLanguage.German);

using var input = new OcrInput();
input.LoadPdf("multilingual-contract.pdf");
var result = ocr.Read(input);
Console.WriteLine(result.Text);
$vbLabelText   $csharpLabel

IronOCR语言支持页面列出了所有 125 多个可用的语言包以及下载说明。 语言包以NuGet包的形式发布(例如,IronOcr.Languages.German),因此它们可以与您已经使用的相同包管理工作流程集成。

对于拉丁字母以外的字符集(例如阿拉伯语、中文、日语、韩语), IronOCR提供了优化的模型,可以处理从右到左的文本方向和表意文字。有关配置详情,请参阅CJK OCR 指南

下一步计划是什么?

现在您拥有了将生产级 OCR 添加到任何.NET 10 应用程序所需的模式:基本文本提取、困难扫描的预处理、异步批量处理、结构化数据解析和多语言支持。

接下来,您可以根据项目需求探索以下领域:

条形码和二维码读取——从同一图像中提取机器可读代码

先使用免费试用许可证,在自己的文档上评估全部功能,然后再决定购买哪个级别。

NuGet 使用 NuGet 安装

PM >  Install-Package IronOcr

IronOCR 上查看 NuGet 快速安装。超过 1000 万次下载,它正以 C# 改变 PDF 开发。 您也可以下载 DLLWindows 安装程序

常见问题解答

.NET OCR SDK是什么?

IronOCR的.NET OCR SDK是一个设计用于将光学字符识别功能集成到C#应用中的库,使开发人员能够从图像、PDF和扫描文档中提取文本。

IronOCR的.NET SDK的关键特性是什么?

IronOCR的.NET SDK提供简单的API、支持多种语言、跨平台兼容性,以及处理各种文件格式和低质量扫描的高级功能。

IronOCR如何处理不同语言?

IronOCR的.NET SDK支持多种语言,从而能够从各种语言的文档中提取和识别文本,而无需额外配置。

IronOCR能处理低质量扫描吗?

是的,IronOCR设计用于有效处理低质量扫描,采用高级算法来增强文本识别精度,即使在具有挑战性的情况下也是如此。

IronOCR的.NET SDK是跨平台的吗?

IronOCR的.NET SDK是跨平台的,这意味着它可以在不同的操作系统上使用,使其适用于各种开发环境。

IronOCR支持哪些文件格式?

IronOCR支持包括图像、PDF和扫描文档在内的多种文件格式,为不同媒体的文本识别任务提供灵活性。

开发人员如何将IronOCR集成到他们的项目中?

开发人员可以使用 IronOCR 的类型化 API 将IronOCR集成到他们的 C# 项目中,从而简化向应用程序添加 OCR 功能的过程。

IronOCR有哪些使用案例?

IronOCR可以用于文档管理系统、自动数据录入、内容数字化,以及任何需要从图像或PDF中提取文本的应用程序。

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

钢铁支援团队

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