Technischer Artikel

HotPDF: Chinesische und mehrsprachige OCR mit RapidOCR

HotPDF fährt chinesische und mehrsprachige OCR in Delphi über seinen nativen RapidOCR-DLL-Adapter: THPDFRapidOCRDLLOptions.ForLanguage mappt einen Sprach-Tag wie 'zh-CN', 'zh-TW', 'ru' oder 'ar' auf ein zusammenpassendes Erkennungsmodell und Zeichwörterbuch, und THotPDF.ApplyLoadedOCRTextLayer macht aus den erkannten Zeilen eine unsichtbare, durchsuchbare Unicode-Textebene auf gescannten PDF-Seiten

Eine Demo in lateinischer Schrift zum Laufen zu bringen ist der leichte Teil. Die interessanten Ausfälle beginnen, wenn Sie auf Traditionelles Chinesisch oder Russisch umschalten und die Ausgabe in selbstbewussten, wohlgeformten Unsinn übergeht, oder wenn jede Zeile stillschweigend ihr letztes Zeichen verliert, oder wenn eine arabische Seite mit ihren Textboxen in falscher Reihenfolge zurückkommt. Keines davon wirft von sich aus eine Exception. Die in HotPDF v2.775.0 hinzugekommenen Sprachprofile existieren größtenteils, um diese Lücken zu schließen, und die vier Fallstricke unten sind es wert, verstanden zu werden, selbst wenn Sie den nativen Code nie anfassen — jeder erklärt ein Symptom, dem Sie sonst einen Tag lang hinterherjagen könnten

Wie wählt ForLanguage Modell und Wörterbuch?

THPDFRapidOCRDLLOptions.ForLanguage löst einen Tag auf eines von neun Profilen auf und liefert Optionen, die auf <profile>/recognition.onnx und <profile>/dictionary.txt unter Ihrem Modellverzeichnis zeigen, während der geteilte Detector, der optionale Winkelklassifikator und die Thread-, Pixel- und Timeout-Defaults von THPDFRapidOCRDLLOptions.Default erhalten bleiben. Die Methode kleinschreibt den Tag, macht aus Unterstrichen Bindestriche und trimmt umgebenden Whitespace, 'zh_TW', 'ZH-tw' und ' zh-tw ' landen also alle auf demselben Profil. Aliase sind eine explizite Liste statt eines Präfix-Abgleichs: 'zh-Hant-TW' wird akzeptiert, weil es gelistet ist, während eine willkürliche, nicht gelistete Regionalvariante EArgumentException wirft, bevor irgendein Modell geladen wird

HotPDF-ForLanguage-Profilauflösung für THPDFRapidOCRDLLOptions: Tags wie zh_TW, ZH-tw und zh-TW werden normalisiert und gegen neun gelistete Profile gematcht, von denen jedes ein Erkennungsmodell und ein Wörterbuch festnagelt, die stets gemeinsam gesetzt werden, während ein nicht gelisteter Tag EArgumentException wirft, bevor irgendein Modell lädt
Ein Tag wählt ein festgenageltes Modell-Wörterbuch-Paar; Detector, Klassifikator und Budgets bleiben geteilt, und ein unbekannter Tag scheitert schnell, statt irgendetwas zu laden
ProfilSprachenBeispiel-TagsFestgenageltes Modell
chVereinfachtes Chinesisch und Englischzh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtTraditionelles Chinesischzh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enEnglischen, en-US, en-GB, engPP-OCRv4
latinFranzösisch, Deutsch, Spanisch, Portugiesisch, Italienisch, Niederländisch, Türkischfr, de, es-419, pt-BR, trPP-OCRv3
japanJapanischja, ja-JP, jpnPP-OCRv4
koreanKoreanischko, ko-KR, korPP-OCRv4
cyrillicRussisch, Ukrainisch, Bulgarisch, Weißrussischru, ru-RU, uk, bgPP-OCRv3
arabicArabisch, Persisch, Urduar, ar-SA, fa, urPP-OCRv4
devanagariHindi, Marathi, Nepalesischhi, mr, nePP-OCRv4

Der Adapter selbst lädt nie etwas herunter. Sie stellen die Dateien einmal mit dem gebündelten Helfer bereit, etwa tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (oder -Language All für alle neun Profile), und der Helfer legt einen geteilten Detector und Klassifikator unter den Root-Dateinamen ab, die Default erwartet. Danach wird ein vereinfacht-chinesischer Scan mit ein paar Zeilen durchsuchbar. Die Engine-Verkabelung ist dieselbe IHPDFOCREngine-Naht aus dem Artikel zur In-Process-RapidOCR-DLL und ihrer ABI-Grenze, dieser hier bleibt also bei den Sprachen

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeChineseScanSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Models: THPDFRapidOCRDLLOptions;
  Layer: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // ch/recognition.onnx + ch/dictionary.txt, geteilter Detector und Klassifikator
  Models := THPDFRapidOCRDLLOptions.ForLanguage('zh-CN');
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Layer := THPDFOCRTextLayerOptions.Default;  // 300 DPI, MinimumConfidence 0.5
    // eine leere Seitenliste bedeutet jede Seite; Seiten mit vorhandenem Text werden übersprungen
    if not Doc.ApplyLoadedOCRTextLayer([], Engine, Layer, Info) then
      raise Exception.Create(string(Info.Diagnostic));
    Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
      ' lines, ', Info.UniqueScalarCount, ' distinct characters');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

Zwei Details in dieser Ausgabe verdienen eine Notiz. Die native Pipeline liefert ein Ergebnis pro erkannter Textzeile, nicht pro Wort, AcceptedWordCount zählt hier also Zeilen, und MinimumConfidence wird gegen die mittlere Zeichen-Confidence der ganzen Zeile verglichen: Eine Zeile mit Durchschnitt 0,45 wird als Einheit verworfen. UniqueScalarCount meldet, wie viele unterschiedliche Unicode-Skalare die Textebene in ihren Font und ihre ToUnicode-Tabelle mappen musste — ein nützlicher Sanity-Check, dass tatsächlich CJK-Text angekommen ist statt einer Handvoll lateinischer Fallbacks. Halten Sie die Engine-Schnittstelle dokumentübergreifend am Leben, denn die Modellinitialisierung passiert in der Factory und ist der teure Schritt

Warum produziert der Wechsel nur des Erkennungsmodells Datenmüll?

Ein CTC-Erkennungsmodell gibt nie Zeichen aus, nur Klassenindizes, und das Wörterbuch ist das einzige, das aus Index 1.204 ein Glyph macht. Tauschen Sie ch/recognition.onnx gegen cyrillic/recognition.onnx, behalten aber das chinesische Wörterbuch, gibt das Modell fröhlich gültige kyrillische Indizes aus, die das alte Wörterbuch in zufällige Han-Zeichen übersetzt. Das Ergebnis sieht aus wie Text, besteht die UTF-8-Validierung und ist durchsuchbar für exakt nichts. Deshalb setzt ForLanguage RecognitionModel und CharacterDictionary stets zusammen, und deshalb sollten handgebaute Optionen niemals das eine ohne das andere ändern

Die naheliegende Sicherheitsprüfung, die Wörterbuchgröße gegen die Modellausgabebreite zu vergleichen, ist nötig, aber nicht hinreichend. Zwei Wörterbücher können dieselbe Anzahl Einträge in unterschiedlicher Reihenfolge haben, und ein Off-by-one in der Reihenfolge verschiebt jedes Zeichen um einen Code Point. HotPDF prüft deshalb in zwei Stufen, wenn die Factory das Modell initialisiert. Erstens muss die Ausgabeklassenanzahl den Wörterbucheinträgen plus zwei entsprechen. Zweitens wird, wenn die ONNX-Datei eine character-Metadatenliste einbettet, jeder Wörterbucheintrag der Reihe nach damit verglichen, eine Diskrepanz lässt die Initialisierung mit EInvalidOperation und einer nativen Diagnose scheitern, statt später plausiblen Müll zu produzieren

Das „plus zwei“ kommt aus dem Klassenlayout. Klasse 0 ist das CTC-Blank, die Klassen 1 bis N sind die Wörterbuchzeilen in Dateireihenfolge, und die letzte Klasse ist ein Leerzeichen. Manche Wörterbücher tragen zudem einen eigenen Leerzeichen-Eintrag, und diese Zeile muss exakt so bleiben, wie sie ist. Hier richtet ein wohlmeinendes Trim echten Schaden an: Es macht aus einem Einzele-Leerzeichen-Eintrag einen leeren String und verschiebt oder bricht die Tabelle. Die einzige sichere Normalisierung ist, ein abschließendes Wagenrücklauf zu entfernen, ein mit CRLF-Zeilenenden gespeichertes Wörterbuch lädt also korrekt, während eine UTF-8-Byteordnungsmarke, eine leere Zeile oder ein Eintrag mit Tab abgewiesen wird. Die Skizze unten zeigt das Layout in Pascal; es ist erläuternder Code, keine HotPDF-API

HotPDF-CTC-Klassentabellen-Layout für RapidOCR-Wörterbücher: Klasse 0 ist das Blank, die Klassen 1 bis N sind die Wörterbuchzeilen in Dateireihenfolge, ein einzelner Leerzeichen-Eintrag bleibt erhalten, und die letzte Klasse ist ein Leerzeichen, was N plus 2 Ausgabeklassen ergibt, die die Factory gegen das Modell prüft, Metadaten eingeschlossen
Das Wörterbuch ist das einzige, das Klassenindizes in Zeichen verwandelt, seine Größe, Reihenfolge und sein Leerzeichen-Eintrag werden also geprüft, bevor eine einzige Seite erkannt wird
// Nur Illustration: die Klassentabelle, die ein CTC-Recognizer erwartet
uses
  SysUtils, IOUtils;

function BuildCTCClassTable(const FileName: string): TArray<string>;
var
  Text, Entry: string;
  Lines: TArray<string>;
  I, Last: Integer;
begin
  Text := TEncoding.UTF8.GetString(TFile.ReadAllBytes(FileName));
  if (Text <> '') and (Text[1] = #$FEFF) then
    raise EArgumentException.Create('Dictionary must be UTF-8 without a BOM');
  Lines := Text.Split([#10]);
  Last := High(Lines);
  if (Last >= 0) and (Lines[Last] = '') then
    Dec(Last);                                   // Zeilenumbruch am Dateiende
  SetLength(Result, Last + 3);
  Result[0] := '';                               // Klasse 0: CTC-Blank
  for I := 0 to Last do
  begin
    Entry := Lines[I];
    if (Entry <> '') and (Entry[Length(Entry)] = #13) then
      SetLength(Entry, Length(Entry) - 1);       // CRLF: nur das CR wegnehmen
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // nie Trim: ' ' ist eine Klasse
  end;
  Result[Last + 2] := ' ';                       // letzte Klasse: Leerzeichen
  // Length(Result) muss der Ausgabeklassenanzahl des Modells entsprechen
end;

Was macht Greedy-CTC-Decoding tatsächlich?

Greedy-CTC-Decoding wählt an jedem Zeitschritt die höchstbewertete Klasse, kollabiert aufeinanderfolgende Wiederholungen zu einem Zeichen und verwirft die Blank-Klasse; das Blank ist es, was echten verdoppelten Buchstaben das Überleben erlaubt. Ein Erkennungsmodell betrachtet eine Textzeile als Folge schmaler vertikaler Scheiben, und für jede Scheibe, jeden Zeitschritt, gibt es eine Wahrscheinlichkeit für jede Klasse aus. Eine Zeile mit AA中 könnte die Argmax-Folge A A blank A 中 space produzieren. Die ersten zwei A-Schritte zu einem A zu kollabieren, das Blank trennt es vom nächsten A, und das Ergebnis ist AA中 mit intaktem nachgestelltem Leerzeichen. Ohne die Blank-Regel wären book und bok ununterscheidbar

HotPDF-GreedyCTCDecode-Durchlauf: Sechs Zeitschritte wählen Argmax-Klassen A, A, blank, A, ein Han-Zeichen und Leerzeichen, aufeinanderfolgende Wiederholungen kollabieren, das Blank setzt den Wiederholungswächter zurück, sodass ein echtes Doppelzeichen überlebt, und drei Grenzfehler verlieren stillschweigend Wortabstände, das letzte Zeichen oder Doppelzeichen
Der Dekoder ist ein Dutzend Zeilen, und jede Grenze zählt: die letzte Klasse einschließen, den letzten Schritt einschließen, und nur ein Blank darf Wiederholungen trennen

Da der Dekoder nur ein Dutzend Zeilen hat, sind die Grenzen leicht falsch zu setzen, und die Ausfälle sind still. Hält die innere Argmax-Schleife eine Klasse zu früh an, kann die Leerzeichen-Klasse nie gewinnen, jede Zeile kommt ohne Wortabstände zurück, was die Phrasensuche auf englischen und lateinischen Seiten ruiniert. Hält die äußere Schleife einen Zeitschritt zu früh an, verschwindet das letzte Zeichen jeder Zeile, bei einer kurzen Zeile also ein Drittel des Texts. Und wird der Wiederholungswächter nicht durch ein Blank zurückgesetzt, kollabieren Doppelzeichen wie ll oder chinesische Reduplikationen wie 谢谢 zu einem. Der HotPDF-Dekoder schließt die letzte Klasse und den letzten Zeitschritt ein, behält durch Blanks getrennte Wiederholungen und weist zudem Bewertungen ab, die nicht endlich sind oder außerhalb 0 bis 1 fallen, sowie jede Klassenanzahl, die nicht zum Wörterbuch passt. Hier dieselbe Logik als Pascal-Illustration

// Nur Illustration: Greedy-CTC-Decoding mit korrekten Grenzen.
// Scores hält Steps * Classes Wahrscheinlichkeiten, eine Zeile pro Zeitschritt
function GreedyCTCDecode(const Scores: array of Single;
  Steps, Classes: Integer; const Characters: array of string): string;
var
  Step, C, Best, Previous: Integer;
  BestScore: Single;
begin
  if (Classes < 3) or (Length(Characters) <> Classes) or
    (Length(Scores) <> Steps * Classes) then
    raise EArgumentException.Create('Model output does not match the dictionary');
  Result := '';
  Previous := 0;                            // Klasse 0 ist das CTC-Blank
  for Step := 0 to Steps - 1 do             // den letzten Zeitschritt einschließen
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // die letzte Klasse einschließen (Leerzeichen)
      if Scores[Step * Classes + C] > BestScore then
      begin
        Best := C;
        BestScore := Scores[Step * Classes + C];
      end;
    if (Best <> 0) and (Best <> Previous) then
      Result := Result + Characters[Best];
    Previous := Best;                       // ein Blank setzt den Wiederholungswächter zurück
  end;
end;

Greedy-Decoding ist nicht die genaueste verfügbare CTC-Strategie; Beam Search mit einem Sprachmodell kann manche mehrdeutigen Scheiben retten. Für gedruckte Dokumente bei 300 DPI ist das Greedy-Ergebnis meist das, was das Modell zu bieten hat, und der Dekoder ist nicht der Ort, Modell-Schwächen zu kompensieren. Das lateinische PP-OCRv3-Modell liest etwa ein ñ als n, selbst auf sauberer Eingabe. HotPDF überklebt das nicht mit nachgeschalteten Zeichenersetzungen, denn eine Ersetzungstabelle, die Spanisch fixt, bricht etwas anderes, und ein falsches Zeichen in einer durchsuchbaren Ebene ist schlimmer als ein ehrlicher Fehlschlag

Wie ordnet HotPDF Textzeilen, eingeschlossen rechts-nach-links-Arabisch?

HotPDF sortiert erkannte Textboxen von oben nach unten, gruppiert Boxen zu einer Zeile, wenn sie vertikal um mindestens die halbe Höhe der kleineren Box überlappen, und ordnet jede Zeile von links nach rechts — oder von rechts nach links, wenn RightToLeft aktiviert ist; die Zeichen innerhalb jeder erkannten Zeile werden nie umgedreht. Die Gruppierung zählt, denn ein Detector zerschneidet eine visuelle Zeile oft in mehrere Boxen, etwa ein Label und ein Wert, getrennt durch eine breite Lücke, und eine reine Top-Koordinaten-Sortierung würde sie mit der Nachbarzeile verschachteln, sobald sich ihre Tops um ein, zwei Pixel unterscheiden

Das arabische Profil setzt RightToLeft := True, was der DLL sagt, die Boxen jeder Zeile an ihrer rechten Kante zu ordnen, vom rechten Rand nach innen. Das ist die gesamte Wirkung. Der Text, den das Modell für eine Zeile zurückgibt, ist bereits in Unicode-Logischer Reihenfolge — der Reihenfolge, in der ein arabischer Leser liest und tippt — und das ist auch die Reihenfolge, die PDF-Textextraktion und Suche erwarten. Den String mechanisch umzudrehen, damit er im Debugger „richtig aussieht“, würde Suche, Kopieren, Einfügen und Screenreader brechen. Bidirektionale Anzeige und Glyphenformung sind Sache des Viewers

Eine Engine bedient ein Sprachprofil. Es gibt keine automatische Schriftenerkennung, ein Dokument, das Schriften mischt, braucht also eine Engine pro Profil, angewandt auf die Seiten, die es nutzen. Weil ApplyLoadedOCRTextLayer eine explizite Seitenliste nimmt und jeden Aufruf als eigene Alles-oder-Nichts-Transaktion committet, ist das unkompliziert

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // wirft EArgumentException für einen unbekannten Tag, bevor irgendein Modell lädt
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // Raum für A3-Seiten bei 300 DPI
  Result := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
end;

procedure OCRMixedArchive(Doc: THotPDF);
var
  Chinese, Arabic: IHPDFOCREngine;
  Layer: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  Chinese := CreateRapidEngine('zh-TW');  // chinese_cht-Profil
  Arabic := CreateRapidEngine('ar-SA');   // arabic-Profil, RightToLeft = True
  Layer := THPDFOCRTextLayerOptions.Default;
  if not Doc.ApplyLoadedOCRTextLayer([0, 1, 2], Chinese, Layer, Info) then
    raise Exception.Create(string(Info.Diagnostic));
  if not Doc.ApplyLoadedOCRTextLayer([3], Arabic, Layer, Info) then
    raise Exception.Create(string(Info.Diagnostic));
end;

Die MaxPixels-Zeile steht da mit Grund. Die DLL-Optionen haben einen Default von 16.777.216 Pixeln pro Anfrage, was A4 und US Letter bei 300 DPI bequem abdeckt, aber eine A3-Seite bei 300 DPI ist etwa 3508 mal 4961 Pixel, grob 17,4 Millionen, und die Anfrage wird als über dem Budget abgewiesen. Heben Sie MaxPixels an (die Obergrenze ist 67.108.864) oder senken Sie THPDFOCRTextLayerOptions.DPI für große Formate. Die Rechts-nach-links-Ordnung nutzt den optionalen HPDFRapidOCRSetReadingDirection-Export der ABI-Version 1; der Adapter braucht ihn nur, wenn RightToLeft gesetzt ist, eine ältere DLL bedient also weiterhin Links-nach-rechts-Sprachen und scheitert bei der Engine-Erzeugung mit einer EArgumentException, die den fehlenden Export für Arabisch nennt

Warum lassen sich neuere OCR-Modelle nicht laden?

Die HotPDF-RapidOCR-DLL linkt eine statische ONNX Runtime 1.14, die keine Modelle lesen kann, die mit ONNX-IR-Version 10 gespeichert wurden, und neuere Exporte wie PP-OCRv5-Modelle können eine neuere Runtime brauchen als das; so ein Modell scheitert an der Engine-Erzeugung mit einer nativen Diagnose. Diese Einschränkung ist der Grund, warum die Sprachpakete auf spezifische PP-OCRv3- und PP-OCRv4-Recognizer- und Wörterbuch-Paare festgenagelt sind statt auf „latest“, und warum die Tabelle oben die beiden Generationen mischt: Jedes festgenagelte Paar ist eines, das unter dieser Runtime lädt und verifiziert

Der Installer erzwingt das Pairing. Jede Datei in seinem Manifest trägt einen SHA256-Hash, eine existierende Datei mit anderem Hash stoppt die Installation, statt überschrieben zu werden, und jeder Download landet unter einem temporären Namen und rückt erst an seinen Platz, wenn sein Hash passt. Das schützt vor der stillen Variante des Wörterbuchproblems: Jemand wirft von Hand ein neueres recognition.onnx in einen Profilordner, die Klassenanzahl passt zufällig, und nichts scheitert, bis ein Kunde meldet, dass die Suche Wörter nicht findet, die er klar sehen kann. Zur Laufzeit bleibt der Adapter offline und holt nie ein fehlendes Modell. Der Recognizer validiert zudem die Modellform beim Laden, akzeptiert NCHW-Eingabe mit fester Höhe 32 oder 48 Pixeln oder dynamischer Höhe, die er bei 48 fährt

Brauchen Sie eine Schrift, die keines der neun Profile abdeckt, können Sie RecognitionModel und CharacterDictionary weiterhin auf eigene Dateien zeigen lassen. Dieselben Prüfungen gelten, und das ist der Punkt: Ein unpassendes Paar scheitert an der Initialisierung, nicht im Archiv Ihres Kunden. Für Seiten, auf die keines der RapidOCR-Profile passt, dockt der Tesseract-Adapter für durchsuchbare PDFs an denselben ApplyLoadedOCRTextLayer-Aufruf an, und für maschinengedruckte ASCII-Formulare braucht die eingebaute Template-Matching-OCR-Engine überhaupt keine Modelle

Kurzreferenz: Mehrsprachige-RapidOCR-Checkliste

  • Optionen mit THPDFRapidOCRDLLOptions.ForLanguage erzeugen und EArgumentException als nicht unterstützten Tag behandeln, nicht als Runtime-Fehler
  • RecognitionModel und CharacterDictionary gemeinsam ändern, nie einzeln; gleiche Klassenanzahlen beweisen keine gleiche Zeichenreihenfolge
  • Wörterbücher als UTF-8 ohne BOM halten, Einträge nie trimmen und damit rechnen, dass das Modell N + 2 Klassen hat: Blank, N Einträge, Leerzeichen
  • Ein eigener CTC-Dekoder muss die letzte Klasse und den letzten Zeitschritt abdecken und Wiederholungen, die durch ein Blank getrennt sind, behalten
  • Eine Engine pro Sprachprofil nutzen und für gemischtschriftliche Dokumente explizite Seitenlisten übergeben
  • RightToLeft ändert nur die Boxreihenfolge; erkannter Text bleibt in Unicode-Logischer Reihenfolge
  • Modelle mit Install-RapidOCRModels.ps1 installieren, damit SHA256-Nagelungen das Modell-Wörterbuch-Pairing halten; UseAngleClassifier := False setzen, wenn Sie mit -SkipClassifier installiert haben
  • MaxPixels über den 16.777.216er Default heben, bevor Sie A3- oder größere Seiten bei 300 DPI fahren

Die RapidOCR-Sprachprofile, der native DLL-Adapter und die OCR-Textebenen-Pipeline sind Teil der HotPDF Delphi PDF Component für Delphi, C++Builder und Windows FPC/Lazarus, ab v2.775.0 für die mehrsprachigen Profile