Skip to footer content
COMPARE TO OTHER COMPONENTS

Tesseract OCR vs IronOCR: .NET OCR Library

Before you read a single OCR result from Tesseract, you have written a preprocessing pipeline — grayscale conversion, contrast enhancement, binarization, noise removal, deskewing, DPI scaling — roughly 180 lines of image manipulation code that has nothing to do with text recognition. That is the real cost of the charlesw Tesseract wrapper: not the zero-dollar license fee, but the 20-40 hours of engineering to make the engine work reliably on documents that were not scanned in ideal conditions. Then you discover that your application receives PDFs, and the pipeline needs to start all over with a PDF rendering library bolted on the front.

Understanding Tesseract

The Tesseract NuGet package (charlesw wrapper) is a P/Invoke bridge that exposes the native Tesseract OCR engine to .NET applications. It wraps the Leptonica image processing library and the Tesseract engine binaries, giving C# developers direct access to one of the most capable open-source OCR engines available.

Tesseract itself is the backbone of OCR in the open-source world. Originally developed at Hewlett-Packard in the 1980s and open-sourced by Google in 2005, the engine accumulated nearly 8 million NuGet downloads through the charlesw wrapper alone. That number reflects genuine usefulness, not a niche project. When images are clean and well-formatted, Tesseract reaches 95%+ accuracy with minimal configuration.

The critical architectural facts that shape every production use of this library:

  • Image-only input: Tesseract processes images. It has no internal PDF renderer. Every PDF workflow requires a separate library (PdfiumViewer, PDFtoImage, Docnet.Core, GhostScript) to convert each page to an image before Tesseract can touch it.
  • Manual preprocessing required: Tesseract expects clean, high-resolution, properly oriented images. It provides no built-in filters. Skew, noise, low DPI, and color backgrounds all degrade accuracy without correction, and that correction is entirely the developer's responsibility.
  • Tessdata file management: Language recognition depends on .traineddata files downloaded from GitHub and placed in a tessdata folder. Each language is 15-100 MB. Every environment — development, CI, staging, production, Docker — must have the correct files at the correct path.
  • Native binary deployment: The wrapper ships platform-specific native libraries (tesseract50.dll, leptonica-1.82.0.dll on Windows; .so files on Linux). These must be deployed alongside the application and matched to the target architecture.
  • Non-thread-safe engine: A TesseractEngine instance cannot be shared across threads. Parallel processing requires creating one engine per thread, multiplying the 40-100 MB memory footprint of engine initialization by the degree of parallelism.
  • Tesseract version pinned at 4.1.1: The charlesw wrapper tracks Tesseract 4.1.1, released in 2019. Tesseract 5.x with improved LSTM accuracy is not available through this package.

The Preprocessing Gap

The preprocessing requirement is not a configuration option you can skip. It is the difference between production accuracy and unusable output on real-world documents. The image-preprocessing-tesseract.cs source file for this wrapper documents the full manual pipeline:

// Tesseract requires every one of these steps to be written manually
public static string ExtractWithPreprocessing(string imagePath)
{
    using (var original = new Bitmap(imagePath))
    {
        // Step 2: Convert to grayscale (~25 lines)
        using (var grayscale = ConvertToGrayscale(original))
        {
            // Step 3: Apply contrast enhancement (~15 lines)
            using (var enhanced = EnhanceContrast(grayscale))
            {
                // Step 4: Binarize — convert to black and white (~15 lines)
                using (var binarized = Binarize(enhanced, 128))
                {
                    // Step 5: Remove noise (~25 lines)
                    using (var denoised = RemoveNoise(binarized))
                    {
                        // Step 6: Deskew if rotated (~50 lines, simplified)
                        using (var deskewed = Deskew(denoised))
                        {
                            // Step 7: Scale to 300 DPI (~20 lines)
                            using (var scaled = ScaleToDpi(deskewed, 300))
                            {
                                return RunTesseract(scaled);  // Save to temp file, load Pix, process
                            }
                        }
                    }
                }
            }
        }
    }
}
// Tesseract requires every one of these steps to be written manually
public static string ExtractWithPreprocessing(string imagePath)
{
    using (var original = new Bitmap(imagePath))
    {
        // Step 2: Convert to grayscale (~25 lines)
        using (var grayscale = ConvertToGrayscale(original))
        {
            // Step 3: Apply contrast enhancement (~15 lines)
            using (var enhanced = EnhanceContrast(grayscale))
            {
                // Step 4: Binarize — convert to black and white (~15 lines)
                using (var binarized = Binarize(enhanced, 128))
                {
                    // Step 5: Remove noise (~25 lines)
                    using (var denoised = RemoveNoise(binarized))
                    {
                        // Step 6: Deskew if rotated (~50 lines, simplified)
                        using (var deskewed = Deskew(denoised))
                        {
                            // Step 7: Scale to 300 DPI (~20 lines)
                            using (var scaled = ScaleToDpi(deskewed, 300))
                            {
                                return RunTesseract(scaled);  // Save to temp file, load Pix, process
                            }
                        }
                    }
                }
            }
        }
    }
}
Imports System.Drawing

Public Class ImageProcessor
    ' Tesseract requires every one of these steps to be written manually
    Public Shared Function ExtractWithPreprocessing(imagePath As String) As String
        Using original As New Bitmap(imagePath)
            ' Step 2: Convert to grayscale (~25 lines)
            Using grayscale As Bitmap = ConvertToGrayscale(original)
                ' Step 3: Apply contrast enhancement (~15 lines)
                Using enhanced As Bitmap = EnhanceContrast(grayscale)
                    ' Step 4: Binarize — convert to black and white (~15 lines)
                    Using binarized As Bitmap = Binarize(enhanced, 128)
                        ' Step 5: Remove noise (~25 lines)
                        Using denoised As Bitmap = RemoveNoise(binarized)
                            ' Step 6: Deskew if rotated (~50 lines, simplified)
                            Using deskewed As Bitmap = Deskew(denoised)
                                ' Step 7: Scale to 300 DPI (~20 lines)
                                Using scaled As Bitmap = ScaleToDpi(deskewed, 300)
                                    Return RunTesseract(scaled)  ' Save to temp file, load Pix, process
                                End Using
                            End Using
                        End Using
                    End Using
                End Using
            End Using
        End Using
    End Function

    ' Placeholder methods for conversion
    Private Shared Function ConvertToGrayscale(original As Bitmap) As Bitmap
        ' Implementation here
        Return Nothing
    End Function

    Private Shared Function EnhanceContrast(grayscale As Bitmap) As Bitmap
        ' Implementation here
        Return Nothing
    End Function

    Private Shared Function Binarize(enhanced As Bitmap, threshold As Integer) As Bitmap
        ' Implementation here
        Return Nothing
    End Function

    Private Shared Function RemoveNoise(binarized As Bitmap) As Bitmap
        ' Implementation here
        Return Nothing
    End Function

    Private Shared Function Deskew(denoised As Bitmap) As Bitmap
        ' Implementation here
        Return Nothing
    End Function

    Private Shared Function ScaleToDpi(deskewed As Bitmap, dpi As Integer) As Bitmap
        ' Implementation here
        Return Nothing
    End Function

    Private Shared Function RunTesseract(scaled As Bitmap) As String
        ' Implementation here
        Return Nothing
    End Function
End Class
$vbLabelText   $csharpLabel

That nested using structure is not boilerplate — each step is a real implementation: a color matrix for grayscale, pixel iteration for contrast, another pixel iteration for binarization, a median filter for noise, and a Hough transform substitute for deskewing. The image-preprocessing-tesseract.cs source notes directly: "Simplified deskew — real implementation needs Hough transform. This typically requires OpenCV or similar library."

The total: approximately 180 lines before a single word is read. The accuracy table in that same file shows why the investment is necessary — Tesseract without preprocessing on a 5-degree skewed document produces 60-70% accuracy, while properly preprocessed input reaches 90%+.

Understanding IronOCR

IronOCR is a commercial .NET OCR library that embeds an optimized Tesseract 5 LSTM engine alongside a built-in preprocessing pipeline, native PDF support, and a managed API that requires no native binary management. The library installs as a single NuGet package with no tessdata folders, no platform-specific DLL deployment steps, and no additional PDF rendering libraries.

Key characteristics that define IronOCR's design:

  • Automatic preprocessing: Deskew, DeNoise, Contrast, Binarize, and EnhanceResolution are one-line method calls on OcrInput. The engine also applies intelligent automatic preprocessing by default before OCR begins.
  • Native PDF input: input.LoadPdf() accepts scanned PDFs, digital PDFs, and mixed PDFs with no external dependency. Password-protected PDFs require one additional parameter.
  • 125+ languages via NuGet: Language packs install as standard NuGet packages — IronOcr.Languages.French, IronOcr.Languages.Arabic — and require no folder management or path configuration.
  • Thread-safe IronTesseract instance: A single instance serves all threads concurrently. Parallel batch processing does not require per-thread engine initialization.
  • Cross-platform single package: Windows, Linux, macOS, Docker, Azure, and AWS all deploy from the same NuGet package with no platform-specific configuration.
  • Searchable PDF output: OCR results convert to searchable PDF in one method call.
  • Pricing: $999 Lite perpetual / $1,499 Plus / $2,999 Professional / $5,999 Unlimited — one-time purchase, no per-document fees.

Feature Comparison

Feature Tesseract (charlesw) IronOCR
License Apache 2.0 (free) Commercial ($999+ perpetual)
PDF input None — requires external library Native built-in
Image preprocessing Manual — 100+ lines of code Automatic + one-line methods
Language management Manual tessdata file download NuGet package install
Thread safety Not thread-safe (per-thread engine) Thread-safe single instance
Deployment Native DLLs + tessdata folder Single NuGet package
Tesseract version 4.1.1 (2019) 5.x optimized

Detailed Feature Comparison

Category / Feature Tesseract (charlesw) IronOCR
Setup and Installation
NuGet install Install-Package Tesseract Install-Package IronOcr
Additional setup steps tessdata download + path config None
Native binary deployment Required Bundled
Docker setup apt-get + tessdata copy No additional steps
Setup time estimate 2-4 hours 5 minutes
Preprocessing
Deskew Manual (50+ lines) input.Deskew()
Denoise Manual (25+ lines) input.DeNoise()
Contrast enhancement Manual (15+ lines) input.Contrast()
Binarization Manual (15+ lines) input.Binarize()
Resolution scaling Manual (20+ lines) input.EnhanceResolution(300)
Total preprocessing LOC ~180 lines 1-10 lines
PDF Support
Read scanned PDFs Not supported natively Native
Read digital PDFs Not supported natively Native
Password-protected PDFs Requires decryption library One parameter
Page range selection Manual (via PDF library) input.LoadPdfPages()
Create searchable PDFs Not supported result.SaveAsSearchablePdf()
Language Support
English Included (file required) Included
Additional languages Manual .traineddata download NuGet package
Number of languages 100+ (manual management) 125+ (NuGet)
Multi-language in one call "eng+fra+deu" string AddSecondaryLanguage()
Threading
Thread-safe engine No Yes
Parallel processing Per-thread engine creation Shared single instance
Memory per thread 40-100 MB each Shared pool
Output and Results
Plain text page.GetText() result.Text
Word-level bounding boxes ResultIterator loop result.Words LINQ
Confidence score page.GetMeanConfidence() result.Confidence
Searchable PDF Not supported result.SaveAsSearchablePdf()
hOCR export page.GetHOCRText() result.SaveAsHocrFile()
Barcode detection Not supported ocr.Configuration.ReadBarCodes = true
Platform Support
Windows Yes Yes
Linux Requires compilation/apt-get Yes
macOS Requires manual setup Yes
Docker Multi-step setup Works out of the box

The Preprocessing Gap

The preprocessing requirement is where the 20-40 hour time estimate comes from. It is not exaggeration. Building a reliable preprocessing pipeline from scratch with Tesseract means implementing every transform that IronOCR ships built-in.

Tesseract Approach

The complete preprocessing pipeline shown in image-preprocessing-tesseract.cs requires System.Drawing.Common (Windows-only) or an additional cross-platform library like ImageSharp. The deskew implementation alone notes it is simplified — a production-grade skew detection algorithm requires a Hough line transform, which typically means pulling in OpenCvSharp4 as an additional dependency:

// image-preprocessing-tesseract.cs — the actual implementation pattern
private static Bitmap ConvertToGrayscale(Bitmap original)
{
    var result = new Bitmap(original.Width, original.Height);
    using (var graphics = Graphics.FromImage(result))
    {
        var colorMatrix = new ColorMatrix(new float[][]
        {
            new float[] { 0.299f, 0.299f, 0.299f, 0, 0 },
            new float[] { 0.587f, 0.587f, 0.587f, 0, 0 },
            new float[] { 0.114f, 0.114f, 0.114f, 0, 0 },
            new float[] { 0, 0, 0, 1, 0 },
            new float[] { 0, 0, 0, 0, 1 }
        });
        using (var attributes = new ImageAttributes())
        {
            attributes.SetColorMatrix(colorMatrix);
            graphics.DrawImage(original,
                new Rectangle(0, 0, original.Width, original.Height),
                0, 0, original.Width, original.Height,
                GraphicsUnit.Pixel, attributes);
        }
    }
    return result;
}

private static Bitmap EnhanceContrast(Bitmap image)
{
    var result = new Bitmap(image.Width, image.Height);
    float contrast = 1.5f;
    for (int y = 0; y < image.Height; y++)
    {
        for (int x = 0; x < image.Width; x++)
        {
            var pixel = image.GetPixel(x, y);
            int r = Clamp((int)((pixel.R - 128) * contrast + 128));
            int g = Clamp((int)((pixel.G - 128) * contrast + 128));
            int b = Clamp((int)((pixel.B - 128) * contrast + 128));
            result.SetPixel(x, y, Color.FromArgb(r, g, b));
        }
    }
    return result;
}

private static Bitmap RemoveNoise(Bitmap image)
{
    var result = new Bitmap(image.Width, image.Height);
    int kernelSize = 3;
    int radius = kernelSize / 2;
    for (int y = radius; y < image.Height - radius; y++)
    {
        for (int x = radius; x < image.Width - radius; x++)
        {
            var pixels = new List<int>();
            for (int ky = -radius; ky <= radius; ky++)
                for (int kx = -radius; kx <= radius; kx++)
                    pixels.Add(image.GetPixel(x + kx, y + ky).R);
            pixels.Sort();
            int median = pixels[pixels.Count / 2];
            result.SetPixel(x, y, Color.FromArgb(median, median, median));
        }
    }
    return result;
}

// After all preprocessing, save to temp file — Tesseract requires a file path
private static string RunTesseract(Bitmap preprocessed)
{
    string tempPath = Path.GetTempFileName() + ".png";
    try
    {
        preprocessed.Save(tempPath, ImageFormat.Png);
        using (var engine = new TesseractEngine(TessDataPath, "eng", EngineMode.Default))
        using (var img = Pix.LoadFromFile(tempPath))
        using (var page = engine.Process(img))
            return page.GetText();
    }
    finally
    {
        if (File.Exists(tempPath)) File.Delete(tempPath);
    }
}
// image-preprocessing-tesseract.cs — the actual implementation pattern
private static Bitmap ConvertToGrayscale(Bitmap original)
{
    var result = new Bitmap(original.Width, original.Height);
    using (var graphics = Graphics.FromImage(result))
    {
        var colorMatrix = new ColorMatrix(new float[][]
        {
            new float[] { 0.299f, 0.299f, 0.299f, 0, 0 },
            new float[] { 0.587f, 0.587f, 0.587f, 0, 0 },
            new float[] { 0.114f, 0.114f, 0.114f, 0, 0 },
            new float[] { 0, 0, 0, 1, 0 },
            new float[] { 0, 0, 0, 0, 1 }
        });
        using (var attributes = new ImageAttributes())
        {
            attributes.SetColorMatrix(colorMatrix);
            graphics.DrawImage(original,
                new Rectangle(0, 0, original.Width, original.Height),
                0, 0, original.Width, original.Height,
                GraphicsUnit.Pixel, attributes);
        }
    }
    return result;
}

private static Bitmap EnhanceContrast(Bitmap image)
{
    var result = new Bitmap(image.Width, image.Height);
    float contrast = 1.5f;
    for (int y = 0; y < image.Height; y++)
    {
        for (int x = 0; x < image.Width; x++)
        {
            var pixel = image.GetPixel(x, y);
            int r = Clamp((int)((pixel.R - 128) * contrast + 128));
            int g = Clamp((int)((pixel.G - 128) * contrast + 128));
            int b = Clamp((int)((pixel.B - 128) * contrast + 128));
            result.SetPixel(x, y, Color.FromArgb(r, g, b));
        }
    }
    return result;
}

private static Bitmap RemoveNoise(Bitmap image)
{
    var result = new Bitmap(image.Width, image.Height);
    int kernelSize = 3;
    int radius = kernelSize / 2;
    for (int y = radius; y < image.Height - radius; y++)
    {
        for (int x = radius; x < image.Width - radius; x++)
        {
            var pixels = new List<int>();
            for (int ky = -radius; ky <= radius; ky++)
                for (int kx = -radius; kx <= radius; kx++)
                    pixels.Add(image.GetPixel(x + kx, y + ky).R);
            pixels.Sort();
            int median = pixels[pixels.Count / 2];
            result.SetPixel(x, y, Color.FromArgb(median, median, median));
        }
    }
    return result;
}

// After all preprocessing, save to temp file — Tesseract requires a file path
private static string RunTesseract(Bitmap preprocessed)
{
    string tempPath = Path.GetTempFileName() + ".png";
    try
    {
        preprocessed.Save(tempPath, ImageFormat.Png);
        using (var engine = new TesseractEngine(TessDataPath, "eng", EngineMode.Default))
        using (var img = Pix.LoadFromFile(tempPath))
        using (var page = engine.Process(img))
            return page.GetText();
    }
    finally
    {
        if (File.Exists(tempPath)) File.Delete(tempPath);
    }
}
Imports System.Drawing
Imports System.Drawing.Imaging
Imports Tesseract
Imports System.IO

' image-preprocessing-tesseract.vb — the actual implementation pattern
Private Shared Function ConvertToGrayscale(original As Bitmap) As Bitmap
    Dim result As New Bitmap(original.Width, original.Height)
    Using graphics As Graphics = Graphics.FromImage(result)
        Dim colorMatrix As New ColorMatrix(New Single()() {
            New Single() {0.299F, 0.299F, 0.299F, 0, 0},
            New Single() {0.587F, 0.587F, 0.587F, 0, 0},
            New Single() {0.114F, 0.114F, 0.114F, 0, 0},
            New Single() {0, 0, 0, 1, 0},
            New Single() {0, 0, 0, 0, 1}
        })
        Using attributes As New ImageAttributes()
            attributes.SetColorMatrix(colorMatrix)
            graphics.DrawImage(original, New Rectangle(0, 0, original.Width, original.Height), 0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes)
        End Using
    End Using
    Return result
End Function

Private Shared Function EnhanceContrast(image As Bitmap) As Bitmap
    Dim result As New Bitmap(image.Width, image.Height)
    Dim contrast As Single = 1.5F
    For y As Integer = 0 To image.Height - 1
        For x As Integer = 0 To image.Width - 1
            Dim pixel As Color = image.GetPixel(x, y)
            Dim r As Integer = Clamp(CInt((pixel.R - 128) * contrast + 128))
            Dim g As Integer = Clamp(CInt((pixel.G - 128) * contrast + 128))
            Dim b As Integer = Clamp(CInt((pixel.B - 128) * contrast + 128))
            result.SetPixel(x, y, Color.FromArgb(r, g, b))
        Next
    Next
    Return result
End Function

Private Shared Function RemoveNoise(image As Bitmap) As Bitmap
    Dim result As New Bitmap(image.Width, image.Height)
    Dim kernelSize As Integer = 3
    Dim radius As Integer = kernelSize \ 2
    For y As Integer = radius To image.Height - radius - 1
        For x As Integer = radius To image.Width - radius - 1
            Dim pixels As New List(Of Integer)()
            For ky As Integer = -radius To radius
                For kx As Integer = -radius To radius
                    pixels.Add(image.GetPixel(x + kx, y + ky).R)
                Next
            Next
            pixels.Sort()
            Dim median As Integer = pixels(pixels.Count \ 2)
            result.SetPixel(x, y, Color.FromArgb(median, median, median))
        Next
    Next
    Return result
End Function

' After all preprocessing, save to temp file — Tesseract requires a file path
Private Shared Function RunTesseract(preprocessed As Bitmap) As String
    Dim tempPath As String = Path.GetTempFileName() & ".png"
    Try
        preprocessed.Save(tempPath, ImageFormat.Png)
        Using engine As New TesseractEngine(TessDataPath, "eng", EngineMode.Default)
            Using img As Pix = Pix.LoadFromFile(tempPath)
                Using page As Page = engine.Process(img)
                    Return page.GetText()
                End Using
            End Using
        End Using
    Finally
        If File.Exists(tempPath) Then File.Delete(tempPath)
    End Try
End Function

Private Shared Function Clamp(value As Integer) As Integer
    Return Math.Max(0, Math.Min(255, value))
End Function

Private Const TessDataPath As String = "path_to_tessdata" ' Define the path to tessdata directory
$vbLabelText   $csharpLabel

This is real code from the source files — not a contrived worst case. The pixel-iteration approach for contrast and noise removal runs in O(n²) over every pixel. The temp file save-and-load is not optional; Pix.LoadFromFile requires a file path on disk. For an application processing 1,000 scanned documents per day, this is measurable overhead on top of OCR time.

IronOCR Approach

The same preprocessing in IronOCR is a sequence of method calls on OcrInput:

// dotnet add package IronOcr
using IronOcr;

using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");

input.Deskew();           // Detects and corrects skew angle automatically
input.DeNoise();          // Removes scanner artifacts and specks
input.Contrast();         // Enhances contrast for character separation
input.Binarize();         // Converts to black and white with adaptive threshold
input.EnhanceResolution(300);  // Scales to 300 DPI for optimal recognition

var result = new IronTesseract().Read(input);
Console.WriteLine($"Confidence: {result.Confidence}%");
Console.WriteLine(result.Text);
// dotnet add package IronOcr
using IronOcr;

using var input = new OcrInput();
input.LoadImage("low-quality-scan.jpg");

input.Deskew();           // Detects and corrects skew angle automatically
input.DeNoise();          // Removes scanner artifacts and specks
input.Contrast();         // Enhances contrast for character separation
input.Binarize();         // Converts to black and white with adaptive threshold
input.EnhanceResolution(300);  // Scales to 300 DPI for optimal recognition

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

Dim input As New OcrInput()
input.LoadImage("low-quality-scan.jpg")

input.Deskew()           ' Detects and corrects skew angle automatically
input.DeNoise()          ' Removes scanner artifacts and specks
input.Contrast()         ' Enhances contrast for character separation
input.Binarize()         ' Converts to black and white with adaptive threshold
input.EnhanceResolution(300)  ' Scales to 300 DPI for optimal recognition

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

No temp files. No pixel iteration. No dependency on System.Drawing.Common or OpenCvSharp4. The image quality correction guide and image orientation correction guide cover the full filter catalog — there are over 15 available. The image filters example shows the low-quality scan pipeline end to end.

For most real-world documents, the default read applies intelligent automatic preprocessing with no explicit filter calls at all:

// Automatic preprocessing applied internally — no explicit filter calls needed
var text = new IronTesseract().Read("scanned-invoice.jpg").Text;
// Automatic preprocessing applied internally — no explicit filter calls needed
var text = new IronTesseract().Read("scanned-invoice.jpg").Text;
Imports IronTesseract

' Automatic preprocessing applied internally — no explicit filter calls needed
Dim text As String = New IronTesseract().Read("scanned-invoice.jpg").Text
$vbLabelText   $csharpLabel

On clean, high-DPI input this costs nothing. On a 72 DPI phone photograph, the engine scales, enhances, and normalizes before recognizing text.

The PDF Gap

PDF is the standard delivery format for business documents. Contracts, invoices, bank statements, medical records — they arrive as PDFs. Tesseract cannot open a PDF. Building the bridge costs another library, another native dependency, and another 50-150 lines of glue code.

Tesseract Approach

The pdf-ocr-processing-tesseract.cs file documents three separate PDF rendering library options — PdfiumViewer, PDFtoImage, and Docnet.Core — each with different dependency chains and trade-offs. The PdfiumViewer pattern shown in that file is representative:

// Tesseract PDF processing — from pdf-ocr-processing-tesseract.cs
// Requires: PdfiumViewer NuGet + pdfium native DLL deployed to application directory
// NuGet: PdfiumViewer, PdfiumViewer.Native.x64

using PdfiumViewer;

public static string ExtractFromPdfWithPdfium(string pdfPath)
{
    var results = new List<string>();

    using (var document = PdfDocument.Load(pdfPath))
    {
        using (var engine = new TesseractEngine(TessDataPath, "eng", EngineMode.Default))
        {
            for (int pageIndex = 0; pageIndex < document.PageCount; pageIndex++)
            {
                // Render page to image at 300 DPI
                using (var pageImage = document.Render(pageIndex, 300, 300,
                    PdfRenderFlags.CorrectFromDpi))
                {
                    // Tesseract requires a file path — must write to disk first
                    string tempPath = Path.GetTempFileName() + ".png";
                    try
                    {
                        pageImage.Save(tempPath);
                        using (var img = Pix.LoadFromFile(tempPath))
                        using (var page = engine.Process(img))
                            results.Add(page.GetText());
                    }
                    finally
                    {
                        File.Delete(tempPath);  // Must clean up or disk fills
                    }
                }
            }
        }
    }

    return string.Join("\n\n--- Page Break ---\n\n", results);
}
// Tesseract PDF processing — from pdf-ocr-processing-tesseract.cs
// Requires: PdfiumViewer NuGet + pdfium native DLL deployed to application directory
// NuGet: PdfiumViewer, PdfiumViewer.Native.x64

using PdfiumViewer;

public static string ExtractFromPdfWithPdfium(string pdfPath)
{
    var results = new List<string>();

    using (var document = PdfDocument.Load(pdfPath))
    {
        using (var engine = new TesseractEngine(TessDataPath, "eng", EngineMode.Default))
        {
            for (int pageIndex = 0; pageIndex < document.PageCount; pageIndex++)
            {
                // Render page to image at 300 DPI
                using (var pageImage = document.Render(pageIndex, 300, 300,
                    PdfRenderFlags.CorrectFromDpi))
                {
                    // Tesseract requires a file path — must write to disk first
                    string tempPath = Path.GetTempFileName() + ".png";
                    try
                    {
                        pageImage.Save(tempPath);
                        using (var img = Pix.LoadFromFile(tempPath))
                        using (var page = engine.Process(img))
                            results.Add(page.GetText());
                    }
                    finally
                    {
                        File.Delete(tempPath);  // Must clean up or disk fills
                    }
                }
            }
        }
    }

    return string.Join("\n\n--- Page Break ---\n\n", results);
}
Imports PdfiumViewer
Imports Tesseract

Public Shared Function ExtractFromPdfWithPdfium(pdfPath As String) As String
    Dim results As New List(Of String)()

    Using document = PdfDocument.Load(pdfPath)
        Using engine = New TesseractEngine(TessDataPath, "eng", EngineMode.Default)
            For pageIndex As Integer = 0 To document.PageCount - 1
                ' Render page to image at 300 DPI
                Using pageImage = document.Render(pageIndex, 300, 300, PdfRenderFlags.CorrectFromDpi)
                    ' Tesseract requires a file path — must write to disk first
                    Dim tempPath As String = Path.GetTempFileName() & ".png"
                    Try
                        pageImage.Save(tempPath)
                        Using img = Pix.LoadFromFile(tempPath)
                            Using page = engine.Process(img)
                                results.Add(page.GetText())
                            End Using
                        End Using
                    Finally
                        File.Delete(tempPath)  ' Must clean up or disk fills
                    End Try
                End Using
            Next
        End Using
    End Using

    Return String.Join(vbCrLf & vbCrLf & "--- Page Break ---" & vbCrLf & vbCrLf, results)
End Function
$vbLabelText   $csharpLabel

The pdf-ocr-processing-tesseract.cs source directly notes the dependency chain: "Tesseract: Apache 2.0, PdfiumViewer: BSD, iText: AGPL or commercial, GhostScript: AGPL or commercial." That last item matters in enterprise contexts — GhostScript's AGPL license requires your application to be open-source unless you purchase a commercial GhostScript license.

Password-protected PDFs add another layer. The PasswordProtectedPdf class in the same file throws NotImplementedException with the comment: "Requires PDF library with encryption support (iText, PDFSharp). Tesseract cannot decrypt PDFs." So password protection means a fourth dependency with its own licensing considerations.

IronOCR Approach

IronOCR reads PDFs natively, including scanned PDFs, digital text PDFs, mixed content PDFs, and password-protected PDFs:

// dotnet add package IronOcr
using IronOcr;

// Scanned PDF — direct load, no rendering library required
var result = new IronTesseract().Read("scanned-contract.pdf");
Console.WriteLine(result.Text);

// Password-protected PDF — one additional parameter
using var input = new OcrInput();
input.LoadPdf("encrypted.pdf", Password: "secret");
var protectedResult = new IronTesseract().Read(input);

// Specific page range from a 200-page document
using var rangeInput = new OcrInput();
rangeInput.LoadPdfPages("large-report.pdf", 1, 10);
var rangeResult = new IronTesseract().Read(rangeInput);

// Create searchable PDF with embedded text layer
var searchable = new IronTesseract().Read("scanned-invoice.pdf");
searchable.SaveAsSearchablePdf("searchable-invoice.pdf");
// dotnet add package IronOcr
using IronOcr;

// Scanned PDF — direct load, no rendering library required
var result = new IronTesseract().Read("scanned-contract.pdf");
Console.WriteLine(result.Text);

// Password-protected PDF — one additional parameter
using var input = new OcrInput();
input.LoadPdf("encrypted.pdf", Password: "secret");
var protectedResult = new IronTesseract().Read(input);

// Specific page range from a 200-page document
using var rangeInput = new OcrInput();
rangeInput.LoadPdfPages("large-report.pdf", 1, 10);
var rangeResult = new IronTesseract().Read(rangeInput);

// Create searchable PDF with embedded text layer
var searchable = new IronTesseract().Read("scanned-invoice.pdf");
searchable.SaveAsSearchablePdf("searchable-invoice.pdf");
Imports IronOcr

' Scanned PDF — direct load, no rendering library required
Dim result = New IronTesseract().Read("scanned-contract.pdf")
Console.WriteLine(result.Text)

' Password-protected PDF — one additional parameter
Using input As New OcrInput()
    input.LoadPdf("encrypted.pdf", Password:="secret")
    Dim protectedResult = New IronTesseract().Read(input)
End Using

' Specific page range from a 200-page document
Using rangeInput As New OcrInput()
    rangeInput.LoadPdfPages("large-report.pdf", 1, 10)
    Dim rangeResult = New IronTesseract().Read(rangeInput)
End Using

' Create searchable PDF with embedded text layer
Dim searchable = New IronTesseract().Read("scanned-invoice.pdf")
searchable.SaveAsSearchablePdf("searchable-invoice.pdf")
$vbLabelText   $csharpLabel

No PDF rendering library. No temp files. No AGPL license considerations. The PDF input how-to guide covers all PDF input variations. The searchable PDF how-to explains the text layer output. For teams building document processing pipelines, the PDF OCR use case page provides production architecture patterns.

The preprocessing methods work identically on PDF input, allowing the same input.Deskew(), input.DeNoise(), input.EnhanceResolution() calls on scanned PDFs without any intermediate conversion step.

Tessdata Management

Every Tesseract deployment includes a tessdata folder problem. The folder must exist, be populated with the correct .traineddata files, and be accessible at the path specified in TesseractEngine initialization. This creates deployment complexity that compounds with scale.

Tesseract Approach

The multi-language-tesseract.cs file documents the language file sizes and management process:

// Must exist before initialization:
// ./tessdata/eng.traineddata   (~15 MB)
// ./tessdata/fra.traineddata   (~15 MB)
// ./tessdata/deu.traineddata   (~15 MB)
// ./tessdata/chi_sim.traineddata  (~45 MB)
// ./tessdata/jpn.traineddata   (~40 MB)
// 10 languages = 200-300 MB to download and manage

public string SafeMultiLanguageOcr(string imagePath, string[] languages)
{
    // Check presence before attempting — runtime failures are worse
    foreach (var lang in languages)
    {
        if (!File.Exists(Path.Combine(TessDataPath, $"{lang}.traineddata")))
        {
            throw new FileNotFoundException(
                $"Missing {lang}.traineddata in {TessDataPath}. " +
                "Download from https://github.com/tesseract-ocr/tessdata");
        }
    }

    var langString = string.Join("+", languages);  // e.g., "eng+fra+deu"
    using var engine = new TesseractEngine(TessDataPath, langString, EngineMode.Default);
    using var img = Pix.LoadFromFile(imagePath);
    using var page = engine.Process(img);
    return page.GetText();
}
// Must exist before initialization:
// ./tessdata/eng.traineddata   (~15 MB)
// ./tessdata/fra.traineddata   (~15 MB)
// ./tessdata/deu.traineddata   (~15 MB)
// ./tessdata/chi_sim.traineddata  (~45 MB)
// ./tessdata/jpn.traineddata   (~40 MB)
// 10 languages = 200-300 MB to download and manage

public string SafeMultiLanguageOcr(string imagePath, string[] languages)
{
    // Check presence before attempting — runtime failures are worse
    foreach (var lang in languages)
    {
        if (!File.Exists(Path.Combine(TessDataPath, $"{lang}.traineddata")))
        {
            throw new FileNotFoundException(
                $"Missing {lang}.traineddata in {TessDataPath}. " +
                "Download from https://github.com/tesseract-ocr/tessdata");
        }
    }

    var langString = string.Join("+", languages);  // e.g., "eng+fra+deu"
    using var engine = new TesseractEngine(TessDataPath, langString, EngineMode.Default);
    using var img = Pix.LoadFromFile(imagePath);
    using var page = engine.Process(img);
    return page.GetText();
}
Imports System.IO
Imports Tesseract

Public Function SafeMultiLanguageOcr(imagePath As String, languages As String()) As String
    ' Check presence before attempting — runtime failures are worse
    For Each lang In languages
        If Not File.Exists(Path.Combine(TessDataPath, $"{lang}.traineddata")) Then
            Throw New FileNotFoundException(
                $"Missing {lang}.traineddata in {TessDataPath}. " &
                "Download from https://github.com/tesseract-ocr/tessdata")
        End If
    Next

    Dim langString = String.Join("+", languages)  ' e.g., "eng+fra+deu"
    Using engine As New TesseractEngine(TessDataPath, langString, EngineMode.Default)
        Using img As Pix = Pix.LoadFromFile(imagePath)
            Using page As Page = engine.Process(img)
                Return page.GetText()
            End Using
        End Using
    End Using
End Function
$vbLabelText   $csharpLabel

The defensive file-existence check is not paranoia — a missing .traineddata file throws TesseractException: Failed to initialise tesseract engine with a message that does not always clearly identify which file is missing. The basic-text-extraction-tesseract.cs source documents the common runtime exceptions: System.DllNotFoundException for missing Leptonica binaries, TesseractException for missing tessdata, BadImageFormatException for 32/64-bit mismatches.

In Docker deployments, the tessdata files must be copied into the container image. For three languages at 15 MB each plus the best models at 50-100 MB each, container images balloon by several hundred megabytes. CI/CD pipelines must either cache these downloads or accept slow build times when the cache is cold.

IronOCR Approach

Language support in IronOCR is a NuGet package reference:

// Install once: dotnet add package IronOcr.Languages.French
// Install once: dotnet add package IronOcr.Languages.German
using IronOcr;

var ocr = new IronTesseract();
ocr.Language = OcrLanguage.French;
ocr.AddSecondaryLanguage(OcrLanguage.German);

var result = ocr.Read("multilingual-document.jpg");
// Install once: dotnet add package IronOcr.Languages.French
// Install once: dotnet add package IronOcr.Languages.German
using IronOcr;

var ocr = new IronTesseract();
ocr.Language = OcrLanguage.French;
ocr.AddSecondaryLanguage(OcrLanguage.German);

var result = ocr.Read("multilingual-document.jpg");
Imports IronOcr

' Install once: dotnet add package IronOcr.Languages.French
' Install once: dotnet add package IronOcr.Languages.German

Dim ocr As New IronTesseract()
ocr.Language = OcrLanguage.French
ocr.AddSecondaryLanguage(OcrLanguage.German)

Dim result = ocr.Read("multilingual-document.jpg")
$vbLabelText   $csharpLabel

The language data is embedded in the NuGet package. No folder to create, no path to configure, no file to download from GitHub and verify. Adding a language to Docker means adding one PackageReference line to the .csproj. The multiple languages how-to guide covers the full 125+ language catalog, and the multi-language blog post walks through production multi-language pipelines including CJK character sets.

API Mapping Reference

Tesseract (charlesw) API IronOCR Equivalent
new TesseractEngine(tessDataPath, "eng", EngineMode.Default) new IronTesseract()
Pix.LoadFromFile(path) input.LoadImage(path) or ocr.Read(path)
Pix.LoadFromMemory(bytes) input.LoadImage(bytes)
engine.Process(img) ocr.Read(input)
page.GetText() result.Text
page.GetMeanConfidence() result.Confidence
page.GetHOCRText(0) result.SaveAsHocrFile(path)
engine.Process(img, tessRect) input.LoadImage(path, new CropRectangle(...))
iter.GetText(PageIteratorLevel.Word) result.Words[i].Text
iter.GetConfidence(PageIteratorLevel.Word) result.Words[i].Confidence
iter.TryGetBoundingBox(PageIteratorLevel.Word, out bounds) result.Words[i].X, .Y, .Width, .Height
"eng+fra+deu" language string ocr.AddSecondaryLanguage(OcrLanguage.French)
N/A — requires PdfiumViewer or similar input.LoadPdf(path)
N/A — requires PDF library input.LoadPdf(path, Password: "secret")
N/A — not supported result.SaveAsSearchablePdf(outputPath)
Manual preprocessing pipeline input.Deskew(), input.DeNoise(), input.Binarize()
Manual multi-thread engine per thread Thread-safe single IronTesseract instance

When Teams Consider Moving from Tesseract to IronOCR

The Preprocessing Milestone Arrives

Every Tesseract project starts with clean test images. Sample invoices, clearly scanned documents, PNG files at 300 DPI that work on the first read. The preprocessing question gets deferred. Then the first production batch arrives: faxed purchase orders at 150 DPI, scanned contracts with 3-degree skew, photographs of receipts taken under fluorescent lighting. Accuracy drops to 60-70%. The team now faces implementing the preprocessing pipeline that was deferred, discovering that grayscale conversion and contrast enhancement are manageable but deskew requires a Hough transform and denoise requires a median filter, and neither is a two-hour task. Teams at this milestone — where preprocessing debt becomes a sprint backlog item — frequently evaluate IronOCR because the license cost is cheaper than two developer-weeks of image processing work they were not hired to do.

The PDF Requirement Appears

Document processing applications almost always eventually need PDF support. The first response is usually "add PdfiumViewer" — it is well-documented and handles many cases well. The problems emerge in production: the native pdfium.dll must be present in the application directory at the correct bitness, container images require explicit COPY steps in Dockerfiles, Linux deployments need the corresponding .so file, and password-protected PDFs require a separate decryption library with its own license. Teams managing three separate dependency chains — Tesseract native libraries, Leptonica, and pdfium — in four environments (Windows, Linux, Docker, CI) reach a maintenance threshold where a single-package alternative becomes worth evaluating.

Parallel Processing at Scale

A batch OCR job processing 500 invoices benefits from parallelism. With the charlesw wrapper, the safe parallel processing pattern creates one engine instance per thread, loading 40-100 MB of language model data each. At eight threads, that is 320-800 MB of engine memory before any documents are loaded. Teams profiling their OCR services and finding memory pressure concentrated at engine initialization — rather than document content — find IronOCR's thread-safe single-instance model directly addresses the root cause. The multithreading example demonstrates the pattern.

Deployment Environments Multiply

A project that started on Windows adds a Linux container for cloud deployment. The Dockerfile now needs native library installation steps, the tessdata files must be copied into the container, and the tessdata path environment variable must be set correctly. Then a macOS developer joins the team. Then someone wants to deploy to AWS Lambda. Each platform adds another configuration surface that can fail silently — a missing native library at runtime in a production container is a worse outcome than a slightly higher NuGet package cost. The IronOCR Docker deployment guide shows the contrast: no system packages, no tessdata copy step, no environment variable.

Tesseract Version Currency Matters

Tesseract 5.x introduced improvements to LSTM accuracy that are measurable on certain document types. The charlesw wrapper targets Tesseract 4.1.1. For teams where OCR accuracy on difficult documents is a product quality metric, the version gap is a real consideration — particularly when the alternative is a commercially maintained package tracking the current engine release.

Common Migration Considerations

Tessdata Folder Removal

The first cleanup step after migrating to IronOCR is deleting the tessdata folder and removing the corresponding <Content Include="tessdata\**"> items from the project file. Any hardcoded path validation code — the Directory.Exists(TessDataPath) guard present in basic-text-extraction-tesseract.cs — also goes away. The using Tesseract; namespace references and TesseractEngine, Pix, and Page type references all need replacement with using IronOcr;, IronTesseract, OcrInput, and OcrResult.

PDF Library Removal

Any PDF rendering library added solely to support Tesseract PDF processing — PdfiumViewer, PDFtoImage, Docnet.Core — can be removed. The native binary dependencies those packages required (pdfium.dll, GhostScript binaries) also go away. Dockerfile COPY and apt-get lines for those dependencies are no longer needed. The IronOCR PDF input guide covers every PDF input variation that those libraries handled, including page range selection and password-protected documents. The entire render-then-OCR block — typically 50-80 lines spanning three libraries — collapses to input.LoadPdf(path) followed by a single Read() call.

Preprocessing Code Replacement

The existing preprocessing methods — ConvertToGrayscale, EnhanceContrast, Binarize, RemoveNoise, Deskew, ScaleToDpi — map directly to IronOCR filter methods. The temp file save-and-load pattern disappears entirely. Reference the image color correction guide for color-specific transforms and the DPI settings guide for resolution management.

Thread Model Change

Code that creates TesseractEngine inside a Parallel.ForEach loop — one per thread — changes to creating IronTesseract once before the loop and sharing it across all threads. This is a correctness change, not just a refactor: the old pattern was defensive programming around a thread-unsafe API; the new pattern is the intended usage of a thread-safe API. The per-thread engine initialization overhead — 40-100 MB of language model data per thread — disappears with the change because IronOCR maintains a shared internal pool rather than loading full model state per engine instance.

Additional IronOCR Capabilities

Beyond preprocessing and PDF support, IronOCR includes capabilities that extend well past the core comparison:

  • Region-based OCR: Extract text from a defined crop rectangle without processing the full image. The region OCR guide and crop example cover invoice header extraction and form field isolation.
  • Confidence-based quality routing: result.Confidence provides a document-level accuracy estimate that enables routing low-confidence results to a human review queue without running OCR twice. See the confidence scores guide.
  • Async OCR: The async OCR guide covers non-blocking OCR for ASP.NET Core applications where blocking the request thread on a CPU-bound operation is unacceptable.
  • Specialized document types: Passport reading, MICR/cheque reading, and license plate reading are available as targeted features without requiring custom trained models.
  • Speed configuration: The speed optimization guide and speed tuning example document configuration options for high-throughput batch processing where per-document latency matters.

.NET Compatibility and Future Readiness

IronOCR targets .NET 6, .NET 7, .NET 8, and .NET 9, with active support for .NET 10 upon its 2026 release. The library also supports .NET Standard 2.0 for projects that have not yet migrated to modern .NET. The charlesw Tesseract wrapper targets .NET Standard 2.0 and is pinned to Tesseract engine version 4.1.1 from 2019, with no announced roadmap for Tesseract 5.x support through that package. For greenfield projects and teams planning multi-year maintenance windows, the engine version gap and the wrapper's slowing maintenance cadence are factors worth weighing alongside the zero-cost license.

Conclusion

Tesseract through the charlesw NuGet wrapper is a genuine OCR engine, not a toy. The 8 million download count reflects real usage in real applications, and on clean, well-formatted images it reaches accuracy levels that justify its popularity. The honest comparison is not about OCR quality — it is about the surface area of engineering work required to make that quality available in production conditions.

The preprocessing gap is the central trade-off. Roughly 180 lines of image manipulation code separating good accuracy from poor accuracy on real-world documents is not a minor inconvenience. It is an engineering task that requires image processing knowledge, additional dependencies, and ongoing maintenance as new document types emerge. The PDF gap adds another layer: a second library, a second set of native binaries, another deployment surface, and potential license entanglements with GhostScript or iText. Together, these two gaps account for the 20-40 hour setup estimate that distinguishes a prototype from a production system.

IronOCR addresses both gaps directly: preprocessing is one-line method calls, PDF is a native input format, and the entire solution deploys as a single NuGet package. The $999 perpetual license is the cost of not spending two weeks on image processing code and dependency chain management. For teams where developer time costs more than the license price, the math is straightforward. For teams with open-source license requirements or zero budget, Tesseract remains the path forward — with clear eyes about the engineering investment that comes with it.

The decision maps cleanly to the document types and operational context: clean, controlled images in a single-environment deployment favor Tesseract's free license. Real-world scans, PDF workflows, multi-environment deployment, and parallel processing at scale each add friction that tilts the calculus toward IronOCR. Most production document processing systems encounter at least two of those conditions.

Please noteGhostscript, PDFium, PDFSharp, Tesseract, and iText are registered trademarks of their respective owners. This site is not affiliated with, endorsed by, or sponsored by Artifex Software, Chromium Project, Google, empira Software GmbH, or iText Group. All product names, logos, and brands are property of their respective owners. Comparisons are for informational purposes only and reflect publicly available information at the time of writing.

Frequently Asked Questions

What is Tesseract OCR?

Tesseract OCR is an OCR solution used by developers and enterprises to extract text from images and documents. It is one of several OCR options evaluated alongside IronOCR for .NET application development.

How does IronOCR compare to Tesseract OCR for .NET developers?

IronOCR is a NuGet-native .NET OCR library using IronTesseract as its core engine. Compared to Tesseract OCR, it offers simpler deployment (no SDK installers), flat-rate pricing, and a clean C# API without COM interop or cloud dependencies.

Is IronOCR easier to set up than Tesseract OCR?

IronOCR installs via a single NuGet package. There are no SDK installers, license files to copy, COM components to register, or separate runtime binaries to manage. The entire OCR engine is bundled in the package.

What accuracy differences exist between Tesseract OCR and IronOCR?

IronOCR achieves high recognition accuracy for standard business documents, invoices, receipts, and scanned forms. For highly degraded documents or uncommon scripts, accuracy varies by source quality. IronOCR includes image preprocessing filters to improve recognition on low-quality inputs.

Does IronOCR support PDF text extraction?

Yes. IronOCR extracts text from both native PDFs and scanned PDF images in a single call. It also supports multi-page TIFF files, images, and streams. For scanned PDFs, OCR is applied page-by-page with per-page result objects.

How does Tesseract OCR licensing compare to IronOCR?

IronOCR uses a flat-rate perpetual license with no per-page or per-scan charges. Organizations processing high document volumes pay the same license cost regardless of volume. Details and volume pricing are on the IronOCR licensing page.

What languages does IronOCR support?

IronOCR supports 127 languages via separate NuGet language packs. Adding a language requires a single 'dotnet add package IronOcr.Languages.{Language}' command. No manual file placement or path configuration is needed.

How do I install IronOCR in a .NET project?

Install via NuGet: 'Install-Package IronOcr' in Package Manager Console or 'dotnet add package IronOcr' in the CLI. Additional language packs are installed the same way. No native SDK installer is required.

Is IronOCR suitable for Docker and containerized deployments, unlike Tesseract OCR?

Yes. IronOCR works in Docker containers via its NuGet package. The license key is set via an environment variable. No license files, SDK paths, or volume mounts are required for the OCR engine itself.

Can I try IronOCR before purchasing, compared to Tesseract OCR?

Yes. IronOCR trial mode processes documents and returns OCR results with a watermark overlay on output. You can verify accuracy on your own documents before purchasing a license.

Does IronOCR support barcode reading alongside text extraction?

IronOCR focuses on text extraction and OCR. For barcode reading, Iron Software provides IronBarcode as a companion library. Both are available individually or as part of the Iron Suite bundle.

Is it easy to migrate from Tesseract OCR to IronOCR?

Migration from Tesseract OCR to IronOCR typically involves replacing initialization sequences with IronTesseract instantiation, removing COM lifecycle management, and updating API calls. Most migrations reduce code complexity significantly.

Kannaopat Udonpant
Software Engineer
Before becoming a Software Engineer, Kannapat completed a Environmental Resources PhD from Hokkaido University in Japan. While pursuing his degree, Kannapat also became a member of the Vehicle Robotics Laboratory, which is part of the Department of Bioproduction Engineering. In 2022, he leveraged his C# skills to join Iron Software's engineering ...
Read More

Iron Support Team

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