Files
mmd-pdf/Services/OcrService.cs
kua-agent 6610acfa44 feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation
  from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc.
  Fixes edited values staying recoverable by text extraction.
- Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode.
- Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon.
- Remove the upstream author credit and the in-app install button.
2026-08-27 07:37:09 +02:00

118 lines
4.4 KiB
C#

using System.IO;
using System.Windows.Media.Imaging;
using Tesseract;
namespace MmdPdf.Services
{
/// <summary>A single recognized word with its confidence and pixel box (top-left origin, OCR image space).</summary>
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; }
}
/// <summary>Result of recognizing one image/page: full text, mean confidence, and per-word boxes.</summary>
internal sealed class OcrResult
{
public string Text { get; set; } = "";
public float MeanConfidence { get; set; }
public List<OcrWord> Words { get; } = [];
}
/// <summary>
/// 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.
/// </summary>
internal sealed class OcrService : IDisposable
{
private readonly TesseractEngine _engine;
/// <param name="tessDataPath">Folder holding *.traineddata. Defaults to the self-extracted cache (OcrNativeBootstrap).</param>
/// <param name="language">Tesseract language code(s), e.g. "eng" or "eng+ben".</param>
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);
}
/// <summary>OCR an image file on disk (PNG, TIFF, JPEG, BMP).</summary>
public OcrResult RecognizeImageFile(string imagePath)
{
using var pix = Pix.LoadFromFile(imagePath);
return Run(pix);
}
/// <summary>OCR an encoded image already in memory (e.g. PNG bytes).</summary>
public OcrResult RecognizeImageBytes(byte[] encodedImage)
{
using var pix = Pix.LoadFromMemory(encodedImage);
return Run(pix);
}
/// <summary>
/// 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.
/// </summary>
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();
}
}