Technischer Artikel

HotPDF-OCR-Textebenen-Offset: Mapping durch die CropBox

OCR-Textebenen, Barcode-Grenzen und Schwärzungsboxen für Gesichter driften auf beschnittenen PDF-Seiten, wenn Bitmap-Pixel durch die MediaBox zurückgemappt wurden statt durch die Box, die der Renderer tatsächlich gerastert hat: die auf die MediaBox zugeschnittene CropBox (ISO 32000-1 §14.11.2). HotPDF hat das für ApplyLoadedOCRTextLayer in v2.770.153 gefixt und für DecodeLoadedPageBarcodes und DetectLoadedRedactionFindings in v2.770.154

Der Bugreport, der üblicherweise ankommt, sieht so aus. Ein gescannter Vertragsarchiv läuft durch OCR, die Ausgabe ist durchsuchbar, und der Suchtreffer auf eine Klauselnummer wird einen halben Zoll unterhalb und links der gedruckten Nummer hervorgehoben. Die meisten Dateien im Batch sind fein. Die kaputten stammen alle von einer Scanstation, die eine /CropBox schreibt, um den Auflagerand wegzuschneiden. Dieses eine Detail trennt das Bild, das die OCR-Engine sah, von dem Rahmen, in den die Textebene platziert wurde, und dieselbe Diskrepanz verschiebt Barcode-Grenzen und, ernster, Schwärzungsboxen für Gesichter

Warum driftet die OCR-Textebene von den gescannten Wörtern weg?

Die Textebene driftet, weil zwei Hälften der Pipeline sich uneinig waren, welches Rechteck die Bitmap abdeckt. In v2.766.64 stellte HotPDF Rendering, SVG-Export, Viewer und Druck auf die CropBox um: Eine Seite wird durch ihre auf die MediaBox zugeschnittene CropBox angezeigt, was ISO 32000-1 §14.11.2 vorschreibt, und GetLoadedPageVisibleBox wurde hinzugefügt, um diese sichtbare Box zurückzugeben. Die Erkennungs-Features bauten ihre Device-zu-Seite-Transformation weiterhin aus GetLoadedPageBox(PageIndex, pbMediaBox, ...). Das Raster deckte jetzt die sichtbare Box ab, die Transformation nahm weiterhin die MediaBox an, und jede erkannte Position kam um die Lücke zwischen beiden versetzt zurück

Das betroffene Fenster ist daher präzise. ApplyLoadedOCRTextLayer platzierte Text von v2.766.64 bis v2.770.152 falsch. Ganzseitiges DecodeLoadedPageBarcodes und die Gesichtserkennung innerhalb von DetectLoadedRedactionFindings blieben einen Build länger falsch, bis v2.770.153. Vor v2.766.64 zeichnete der Renderer die ganze MediaBox, Mapping und Raster stimmten also überein, um den Preis, Inhalte zu erkennen, die Viewer nie zeigen. Die Fixes änderten für jedes Feature drei Dinge zusammen: die Transformation, die Pixelbudget-Schätzung und die Seitenbox, die im Request-Record an eine Custom-Engine übergeben wird

Mehrere Fälle waren nie betroffen:

  • Seiten ohne /CropBox oder deren CropBox gleich der MediaBox ist, mappen vor und nach dem Fix identisch
  • DecodeLoadedPageBarcodes mit gesetztem HasRegion rendert exakt die Region, die Sie übergeben, und mappt durch dieselbe Region, explizites Region-Decoding war also durchgängig korrekt; die Prüfung, dass die Region innerhalb der Seite liegt, nutzt weiterhin die MediaBox
  • Musterbasierte Redaction-Findings (E-Mails, Kartennummern und so weiter) kommen aus der Textextraktion im User Space, nicht aus einem Raster, nur die Face-Detection-Findings haben sich also bewegt

Drei Koordinatenrahmen — und welche HotPDF-APIs welchen nutzen

HotPDF-Code, der Erkennung anfasst, hat es mit drei Rahmen zu tun, und die meisten Mapping-Bugs kommen daher, zwei davon zu vermischen

  • Bitmap-Pixel: Ursprung oben links, Y wächst abwärts, Einheit sind Pixel bei der angefragten DPI. THPDFOCRWord.Left, Top, Right und Bottom liegen in diesem Rahmen, ebenso die optionalen Baseline-Punkte, die Ergebnisse, die ein Custom-IHPDFBarcodeDecoder zurückgibt, und die Boxen eines Custom-IHPDFFaceDetector
  • PDF-User Space einer geladenen Seite: Ursprung unten links, Y wächst aufwärts, Einheit sind Punkte, mit Bottom < Top. GetLoadedPageBox und GetLoadedPageVisibleBox liefern Left, Bottom, Right, Top in diesem Rahmen, ebenso die Felder PageLeft, PageBottom, PageRight und PageTop von THPDFOCRRequest, die Grenzen in THPDFDecodedBarcode und die Rechtecke in THPDFRedactionFinding
  • HotPDF-Seitenzeichenkoordinaten: Die API, mit der Sie neue Seiten bauen (Textausgabe, Formen, Barcodes, Links, Formfelder), arbeitet mit Ursprung oben links und Y abwärts wachsend. Dieser Rahmen gehört zur Dokumentgenerierung und hat mit den geladenen-Dokument-APIs oben nichts zu tun, füttern Sie ihn also nie unverändert mit einem User-Space-Rechteck einer geladenen Seite

Der OCR-Word-Record ist bewusst pixelbasiert: Eine Engine meldet, was sie im Bild gesehen hat, und ApplyLoadedOCRTextLayer besitzt die Konversion. Diese Arbeitsteilung funktioniert nur, wenn die Konversion die richtige Box nutzt, was v2.770.153 wiederhergestellt hat

HotPDF-Erkennungskoordinatenrahmen: Bitmap-Pixel mit Ursprung oben links, genutzt von THPDFOCRWord-Boxen und Custom-Decodern, PDF-User Space mit Ursprung unten links, geliefert von GetLoadedPageBox und GetLoadedPageVisibleBox, und die Seitenzeichen-API mit Ursprung oben links, die nie unverändert ein Rechteck einer geladenen Seite erhalten darf
Engines melden Pixel, weil das ist, was sie sahen, HotPDF mappt sie, und die beiden Rahmen zu vermischen ist der Weg, auf dem Ebenen und Schwärzungsboxen driften

Die Device-zu-Seite-Transformation hinter OCR, Barcodes und Gesichtern

HotPDF mappt Bitmap-Pixel auf die Seite mit einer einzigen affinen Matrix, gebaut aus fünf Eingaben: der Rotation, dem Maßstab DPI / 72, der Bitmap-Höhe und Left, Bottom, Right und Top der gerenderten Box. OCR, Barcode-Decoding und Gesichtserkennung teilen sich dafür eine Routine — deshalb brach eine falsche Box-Eingabe alle drei auf dieselbe Weise. Für eine unrotierte Seite ist die Seite-zu-Device-Matrix [A B C D E F]:

  • A = Scale und D = -Scale, wobei Scale = DPI / 72; das negative D kippt den User Space (Y aufwärts) in den Bitmap-Raum (Y abwärts)
  • B = C = 0, denn eine unrotierte Seite hat keine Scherung und keinen Achstausch
  • E = -Left * Scale, was die linke Kante der Box auf Pixelspalte 0 schiebt
  • F = BitmapHeight + Bottom * Scale, was die untere Kante der Box auf y = BitmapHeight mappt, die Unterkante der Bitmap, die Oberkante landet also auf Zeile 0

Zurück auf die Seite gehen Pixel über die Inverse dieser Matrix. Request.PageRotation trägt das /Rotate der Seite, normalisiert auf 0, 90, 180 oder 270 (jeder Wert, der kein Vielfaches von 90 ist, gilt als 0), und der Renderer dreht die Seite im Uhrzeigersinn, wie ISO 32000-1 §7.7.3.3 es verlangt. Unter Rotation tauschen die Achsen, und ein anderes Paar Box-Kanten wird an den Bitmap-Ursprung genagelt. Als inverse Formeln ausgeschrieben, mit S = DPI / 72, x und y in Pixeln und H der Bitmap-Höhe:

/RotatePage XPage YBox-Kanten, von denen das Mapping abhängt
0Left + x / SBottom + (H - y) / SLeft, Bottom
90Left + y / SBottom + x / SLeft, Bottom
180Right - x / SBottom + y / SRight, Bottom
270Right - y / STop - x / SRight, Top

Die letzte Spalte erklärt, warum der Bug in der Produktion zufällig wirkte. Eine CropBox, die nur oben an der Seite abschneidet, lässt Left und Bottom unberührt, aufrechte Seiten kamen also perfekt heraus, und nur Seiten mit /Rotate 270 drifteten. Rotation tauscht zudem die Bitmap-Dimensionen: Bei 90 und 270 ist die Bitmap (Top - Bottom) * S Pixel breit und (Right - Left) * S Pixel hoch

HotPDF-Inverse-Mapping-Tabelle für Seitenrotation: Bei /Rotate 0 und 90 nagelt die Transformation die Left- und Bottom-Kanten der gerenderten Box, bei 180 Right und Bottom, bei 270 Right und Top — deshalb driftet eine beschnittene Seite in eine andere Richtung für jede Ausrichtung in einem gemischten Dokument
Derselbe halbe Zoll Beschnitt sieht aus wie drei verschiedene Bugs, sobald Seiten unterschiedliche /Rotate-Werte tragen, denn jede Ausrichtung nagelt ein anderes Paar Box-Kanten fest

Was läuft schief mit MediaBox [0 0 612 792] und CropBox [36 36 576 756]?

Mit einem halben Zoll Beschnitt an jeder Seite landet die Textebene einer unrotierten Seite exakt 36 Punkte links von und 36 Punkte unter den gescannten Wörtern, wenn die MediaBox benutzt wird. Nehmen Sie eine US-Letter-Seite, deren CropBox an jeder Kante 36 Punkte (0,5 Zoll) abschneidet. Die sichtbare Box ist 540 mal 720 Punkte, bei der Default-OCR-Auflösung von 300 DPI ist der Maßstab also 300 / 72 ≈ 4,1667, und die Bitmap ist 2250 mal 3000 Pixel

Angenommen, die Engine meldet ein Wort mit Pixelbox Left 450, Top 600, Right 900, Bottom 660 und ohne Baseline. HotPDF legt die Baseline dann 20 Prozent der Worthöhe über die Unterkante, auf Pixelzeile 648, und mappt den Startpunkt (450, 648):

  • Durch die sichtbare Box: x = 36 + 450 / 4,1667 = 144,0 und y = 36 + (3000 - 648) / 4,1667 = 600,48 — genau dort, wo das Wort gedruckt ist
  • Durch die MediaBox: x = 0 + 108,0 = 108,0 und y = 0 + 564,48 = 564,48 — eine gleichmäßige Verschiebung um (-36, -36) Punkte
HotPDF-CropBox-Drift-Anatomie auf einer US-Letter-Seite mit MediaBox 0 0 612 792 und CropBox 36 36 576 756: Der Renderer rastert die sichtbare Box bei 300 DPI, das Mappen von Wortpixel 450 durch GetLoadedPageVisibleBox liefert also 144,0 und 600,48, während die MediaBox-Transformation bei 108,0 und 564,48 landet
Das Raster deckt die CropBox ab, jede aus der MediaBox gebaute Transformation verschiebt also jedes erkannte Wort exakt um den Beschnittrand

Dieselbe Seite zu rotieren ändert die Fehlrichtung, weil andere Kanten im Spiel sind. Bei /Rotate 180 nutzt der X-Term Right, und 612 statt 576 schiebt die Ebene 36 Punkte nach rechts, während Bottom sie weiterhin 36 Punkte nach unten zieht. Bei /Rotate 270 sind sowohl Right als auch Top zu groß, die Ebene rückt also 36 Punkte nach rechts und 36 Punkte nach oben. Ein Dokument mit gemischten Ausrichtungen kann das Driften in drei Richtungen zeigen — ein verlässlicher Fingerabdruck dieses Bugs. Handgeschriebener Code, der den Maßstab aus der Box ableitet, etwa Bitmap.Width / (Right - Left), streckt zudem jede Koordinate um 612 / 540, rund 13 Prozent, obendrauf

Welche Ihrer PDF-Dokumente sind betroffen?

Ein PDF-Dokument ist exponiert, wenn mindestens eine Seite eine sichtbare Box hat, die von ihrer MediaBox abweicht, und HotPDF sagt Ihnen das in ein paar Zeilen. Vergleichen Sie GetLoadedPageBox mit pbMediaBox gegen GetLoadedPageVisibleBox für jede Seite und drucken Sie GetLoadedPageRotation daneben, sodass Sie die Driftrichtung aus der Tabelle oben vorhersagen können. THPDFPageBoundary bietet auch pbCropBox, pbBleedBox, pbTrimBox und pbArtBox, aber GetLoadedPageBox(pbCropBox) fällt ohne vorhandene CropBox auf die MediaBox zurück und beschneidet nicht, die sichtbare Box ist also das richtige Vergleichsobjekt

uses
  System.SysUtils, HPDFDoc;

procedure ReportCroppedPages(const FileName: string);
var
  Pdf: THotPDF;
  I: Integer;
  ML, MB, MR, MT, VL, VB, VR, VT, Tmp: Single;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) < 1 then
      raise Exception.Create('Cannot load ' + FileName);
    for I := 0 to Pdf.LoadedPageCount - 1 do
    begin
      if not Pdf.GetLoadedPageBox(I, pbMediaBox, ML, MB, MR, MT) then
        Continue;
      // das gespeicherte Array darf seine Ecken in beliebiger Reihenfolge listen
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // bereits normalisiert und auf die MediaBox zugeschnitten
      if not Pdf.GetLoadedPageVisibleBox(I, VL, VB, VR, VT) then
        Continue;
      if (Abs(VL - ML) > 0.01) or (Abs(VB - MB) > 0.01) or
         (Abs(VR - MR) > 0.01) or (Abs(VT - MT) > 0.01) then
        Writeln(Format('Page %d  MediaBox [%g %g %g %g]  visible [%g %g %g %g]  /Rotate %d',
          [I + 1, ML, MB, MR, MT, VL, VB, VR, VT,
           Pdf.GetLoadedPageRotation(I)]));
    end;
  finally
    Pdf.Free;
  end;
end;

Zwei Details von GetLoadedPageVisibleBox zählen für Skripte wie dieses. Die Funktion lässt ihre out-Parameter bei Misserfolg unangetastet, sie vorher auf eine Default-Seitengröße zu setzen ist also ein sicheres Muster. Und wenn eine missgebildete CropBox die MediaBox überhaupt nicht schneidet, liefert die Funktion die MediaBox statt eines leeren Rechtecks. Listet der Report Seiten auf und Ihr ausgelieferter Build ist älter als v2.770.153 für OCR bzw. v2.770.154 für Barcodes und Gesichter, fahren Sie die Erkennung auf diesen Seiten nach dem Upgrade erneut. Eine von einem betroffenen Build eingebrachte OCR-Ebene bleibt in der gespeicherten Datei, und die Default-Option SkipPagesWithText überspringt diese Seiten in einem zweiten Durchlauf, es sei denn, Sie schalten sie aus oder entfernen die alte Ebene zuerst

Wie soll eine Custom-IHPDFOCREngine Pixel zurück in den PDF-Raum mappen?

Eine Custom-IHPDFOCREngine sollte Wortboxen in Bitmap-Pixeln zurückgeben und HotPDF das Mappen überlassen; konvertieren Sie nur für Ihre eigenen Entscheidungen in den User Space, und dann mit der Box aus dem Request, nie mit der MediaBox. Seit v2.770.153 beschreiben PageLeft, PageBottom, PageRight und PageTop des Requests die gerenderte sichtbare Box, sie passen also exakt zu Request.Bitmap. Der Helfer unten ist die Inverse der Bibliotheks-Transformation, eingeschlossen ihrer Benutzung der tatsächlichen Bitmap-Höhe für aufrechte Seiten, er stimmt also pixelgenau mit HotPDF überein

uses
  System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;

// Bitmap-Pixel (Ursprung oben links, Y abwärts) in den PDF-User Space
// (Ursprung unten links, Y aufwärts), durch die Box, aus der die Bitmap gerendert wurde
procedure HotPixelToPage(Rotation, DPI, BitmapHeight: Integer;
  Left, Bottom, Right, Top: Single; X, Y: Double;
  out PageX, PageY: Double);
var
  S: Double;
begin
  S := DPI / 72.0;
  case Rotation of
    90:  begin PageX := Left + Y / S;  PageY := Bottom + X / S; end;
    180: begin PageX := Right - X / S; PageY := Bottom + Y / S; end;
    270: begin PageX := Right - Y / S; PageY := Top - X / S; end;
  else
    PageX := Left + X / S;
    PageY := Bottom + (BitmapHeight - Y) / S;
  end;
end;

Ein realistischer Grund, innerhalb einer Engine den User Space zu brauchen, ist eine Zonenregel: Rechnungen, deren Briefkopf Sie nie durchsuchbar wollen, oder eine Stempelfläche, die den Recognizer verwirrt. Die Engine unten, geschrieben mit TInterfacedObject, sodass die Referenzzählung ihre Lebensdauer übernimmt, filtert Wörter danach, wo ihre Zentren auf der Seite fallen, und liefert die Überlebenden unverändert in Pixelkoordinaten zurück. RunRecognizer steht stellvertretend für Ihren eigenen Recognizer-Aufruf

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // User Space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // Ihr Recognizer, Pixelboxen
  public
    constructor Create(SkipLeft, SkipBottom, SkipRight, SkipTop: Single);
    function GetName: AnsiString;
    function Recognize(const Request: THPDFOCRRequest;
      out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
  end;

function TZoneFilterOCREngine.Recognize(const Request: THPDFOCRRequest;
  out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
var
  Raw: THPDFOCRWords;
  I, Count: Integer;
  CX, CY: Double;
begin
  Diagnostic := '';
  SetLength(Words, 0);
  if not RunRecognizer(Request.Bitmap, Request.MaxWords, Raw) then
  begin
    Diagnostic := 'recognizer failed';
    Exit(False);
  end;
  SetLength(Words, Length(Raw));
  Count := 0;
  for I := 0 to High(Raw) do
  begin
    HotPixelToPage(Request.PageRotation, Request.DPI,
      Request.Bitmap.Height, Request.PageLeft, Request.PageBottom,
      Request.PageRight, Request.PageTop,
      (Raw[I].Left + Raw[I].Right) / 2, (Raw[I].Top + Raw[I].Bottom) / 2,
      CX, CY);
    if (CX >= FSkipLeft) and (CX <= FSkipRight) and
       (CY >= FSkipBottom) and (CY <= FSkipTop) then
      Continue;
    Words[Count] := Raw[I];  // weiterhin Pixel: HotPDF mappt sie selbst
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

Reichen Sie die Engine an ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) weiter wie jede andere Engine auch. Die Bibliothek validiert, was zurückkommt, bevor sie ihm vertraut: Ein Wort wird verworfen und in Info.DroppedWordCount gezählt, wenn seine Box die Bitmap verlässt, wenn Right <= Left oder Bottom <= Top gilt oder wenn Confidence außerhalb 0..1 oder unter MinimumConfidence liegt. Mehr Wörter zurückzugeben als MaxWordsPerPage oder die laufende Summe über MaxTotalWords zu schieben, lässt den ganzen Aufruf mit einem Budgetfehler scheitern, ehren Sie also Request.MaxWords in der Engine. Konvertieren Sie Wortboxen nicht in den User Space, bevor Sie sie zurückgeben; HotPDF würde die Punktwerte als Pixel behandeln, und die Ebene würde zum Bitmap-Ursprung kollabieren

Die Ausgabe Ihres eigenen Detectors mappen

Derselbe Helfer bedient eine selbstgebaute Pipeline auf RenderLoadedPageToBitmap, die die sichtbare Box rendert und /Rotate anwendet, genau wie die Erkennungs-Features. Lesen Sie die Box mit GetLoadedPageVisibleBox, normalisieren Sie die Rotation so wie HotPDF es tut, und mappen Sie zwei gegenüberliegende Ecken jeder Pixelbox. Die Y-Achse kippt, und bei 90 und 270 Grad tauschen die Achsen, die gemappten Ecken kommen also in keiner festen Reihenfolge heraus; nehmen Sie das Minimum und Maximum der gemappten Punkte, so baut auch HotPDF seine Barcode-Grenzen

const
  DPI = 200;
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  VL, VB, VR, VT: Single;
  Rotation: Integer;
  PxL, PxT, PxR, PxB, X1, Y1, X2, Y2: Double;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-ids.pdf');
    if not Pdf.GetLoadedPageVisibleBox(0, VL, VB, VR, VT) then Exit;
    Rotation := Pdf.GetLoadedPageRotation(0) mod 360;
    if Rotation < 0 then Inc(Rotation, 360);
    if (Rotation <> 90) and (Rotation <> 180) and (Rotation <> 270) then
      Rotation := 0;
    Bmp := Pdf.RenderLoadedPageToBitmap(0, DPI);
    if Bmp = nil then Exit;
    try
      MyDetector(Bmp, PxL, PxT, PxR, PxB);  // Ihr Code, Pixelbox
      HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
        PxL, PxT, X1, Y1);
      HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
        PxR, PxB, X2, Y2);
      Writeln(Format('User-space box [%.2f %.2f %.2f %.2f]',
        [Min(X1, X2), Min(Y1, Y2), Max(X1, X2), Max(Y1, Y2)]));
    finally
      Bmp.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Das Rotationsverhalten ist tiefer behandelt in Seitenrotation glätten, ohne die Seitenboxen zu brechen, und die Barcode-Decoding-Pipeline, die dieselbe Transformation konsumiert, in Rotierte QR-Codes aus PDF-Seiten dekodieren. Wenn Ihre Engine einen externen Recognizer einwickelt, zeigt der Tesseract-OCR-Adapter für durchsuchbare PDFs die Prozessisolation und Abbruchseite derselben Schnittstelle

Kurzreferenz: CropBox-sicheres Koordinaten-Mapping

  • Der Renderer rastert die sichtbare Box, die auf die MediaBox zugeschnittene CropBox (ISO 32000-1 §14.11.2); jedes Pixel-zu-Seite-Mapping muss diese Box nutzen, gelesen mit GetLoadedPageVisibleBox
  • HotPDF v2.770.153 fixte ApplyLoadedOCRTextLayer; v2.770.154 fixte ganzseitiges DecodeLoadedPageBarcodes und Face-Findings aus DetectLoadedRedactionFindings; Builds von v2.766.64 bis zu diesen Versionen sind betroffen
  • THPDFOCRWord-Boxen sind Bitmap-Pixel mit Ursprung oben links; GetLoadedPageBox und GetLoadedPageVisibleBox liefern PDF-User Space mit Ursprung unten links und Bottom < Top
  • Der Maßstab ist DPI / 72; leiten Sie ihn aus der DPI ab, nie aus einer in die Bitmap-Breite geteilten Seitenbox
  • /Rotate entscheidet, welche Kanten zählen: Left und Bottom bei 0 und 90, Right und Bottom bei 180, Right und Top bei 270
  • Liefern Sie OCR-Wörter in Pixeln und lassen Sie HotPDF sie mappen; konvertieren Sie nur für Ihre eigene Filterlogik
  • Fahren Sie OCR auf beschnittenen Seiten erneut, die ein betroffener Build verarbeitet hat, und denken Sie daran, dass SkipPagesWithText Seiten überspringt, die die alte Ebene bereits tragen

Die hier genutzten Erkennungs-Features, Seitenbox-Abfragen und das Rendern geladener Dokumente kommen alle in der HotPDF-Komponente für Delphi und C++Builder; Lizenzierung, Testdownloads und die vollständige Feature-Liste finden Sie auf der HotPDF Delphi PDF component page