using System.IO;
using System.Windows.Media.Imaging;
using Tesseract;
namespace MmdPdf.Services
{
/// A single recognized word with its confidence and pixel box (top-left origin, OCR image space).
internal sealed class OcrWord
{
public string Text { get; set; } = "";
public float Confidence { get; set; }
public int Left { get; set; }
public int Top { get; set; }
public int Right { get; set; }
public int Bottom { get; set; }
}
/// Result of recognizing one image/page: full text, mean confidence, and per-word boxes.
internal sealed class OcrResult
{
public string Text { get; set; } = "";
public float MeanConfidence { get; set; }
public List Words { get; } = [];
}
///
/// Local Tesseract OCR. The tessdata folder (with at least eng.traineddata) must sit next to the
/// exe; the native engine loads language data by path. A TesseractEngine is NOT thread-safe, so run
/// OCR off the UI thread and create a fresh OcrService per operation (or serialize calls). Dispose when done.
///
internal sealed class OcrService : IDisposable
{
private readonly TesseractEngine _engine;
/// Folder holding *.traineddata. Defaults to the self-extracted cache (OcrNativeBootstrap).
/// Tesseract language code(s), e.g. "eng" or "eng+ben".
public OcrService(string? tessDataPath = null, string language = "eng")
{
// EnsureReady() extracts the embedded natives + language data and configures the native
// loader, so it must run before the engine is constructed.
string dataPath = tessDataPath ?? OcrNativeBootstrap.EnsureReady();
_engine = new TesseractEngine(dataPath, language, EngineMode.Default);
}
/// OCR an image file on disk (PNG, TIFF, JPEG, BMP).
public OcrResult RecognizeImageFile(string imagePath)
{
using var pix = Pix.LoadFromFile(imagePath);
return Run(pix);
}
/// OCR an encoded image already in memory (e.g. PNG bytes).
public OcrResult RecognizeImageBytes(byte[] encodedImage)
{
using var pix = Pix.LoadFromMemory(encodedImage);
return Run(pix);
}
///
/// OCR a rendered page straight from the render pipeline (raw BGRA, 4 bytes/pixel).
/// Encodes to PNG via WPF first so we avoid a System.Drawing dependency.
///
public OcrResult RecognizeBgra(byte[] bgra, int width, int height)
=> RecognizeImageBytes(EncodePng(bgra, width, height));
private OcrResult Run(Pix pix)
{
using var page = _engine.Process(pix);
var res = new OcrResult
{
Text = page.GetText() ?? "",
MeanConfidence = page.GetMeanConfidence(),
};
using var iter = page.GetIterator();
iter.Begin();
do
{
if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out var r))
{
string w = iter.GetText(PageIteratorLevel.Word) ?? "";
if (!string.IsNullOrWhiteSpace(w))
{
res.Words.Add(new OcrWord
{
Text = w,
Confidence = iter.GetConfidence(PageIteratorLevel.Word),
Left = r.X1,
Top = r.Y1,
Right = r.X2,
Bottom = r.Y2,
});
}
}
}
while (iter.Next(PageIteratorLevel.Word));
return res;
}
private static byte[] EncodePng(byte[] bgra, int width, int height)
{
var bmp = BitmapSource.Create(
width, height, 96, 96,
System.Windows.Media.PixelFormats.Bgra32, null,
bgra, width * 4);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
using var ms = new MemoryStream();
encoder.Save(ms);
return ms.ToArray();
}
public void Dispose() => _engine.Dispose();
}
}