Technischer Artikel

CID-Type-2-Schriften aus Delphi in PDF einbetten

PDFium Component schreibt mit AddCidType2Text Text auf Glyphenebene in eine PDF-Seite und bettet dabei von Ihnen bereitgestellte TrueType-Daten als CID-Type-2-Schrift ein. Es weist jeder Glyphen-Instanz eine fortlaufende CID zu, erzeugt die explizite CID-zu-GID-Zuordnung und die ToUnicode-CMap und verschmilzt Glyphen auf derselben Grundlinie zu einem einzigen nativen Textobjekt. Optionales Subsetting hält die Datei klein, mit einer expliziten Richtlinie dafür, was passiert, wenn das Subsetting fehlschlägt

Schreiben auf Glyphenebene brauchen Sie, wenn der Text bereits geshapt wurde. Arabisch, Devanagari und jede Schrift mit kontextabhängigen Formen erzeugen eine Folge von Glyphenkennungen, die nicht mehr eins-zu-eins auf Zeichen abbildet, sodass eine API, die einen String und einen Schriftnamen entgegennimmt, das Ergebnis nicht ausdrücken kann. Glyphen und Positionen direkt zu übergeben ist der einzige Weg, korrekt geshapten Text komplexer Schriftsysteme in ein PDF zu bringen

AddCidType2Text in Delphi weist jeder geformten Glypheninstanz eine fortlaufende CID zu, schreibt CIDToGIDMap und ToUnicode-CMap und verschmilzt den Lauf in ein Seitentextobjekt, das sich sauber extrahieren lässt
Jede Glypheninstanz nimmt ihre eigene CID, das Font-Dictionary mappt CIDs auf Glyph-IDs und zurück zu Unicode, und der Lauf landet als ein Textobjekt auf der Seite, das zum beabsichtigten Text zurückextrahiert

Warum jede Glyphen-Instanz eine eigene CID erhält

Die verlockende Optimierung ist die Deduplizierung: eine CID pro eindeutiger Glyphenkennung, wiederverwendet überall, wo diese Glyphe erscheint. Das erzeugt eine kleinere Schrift und bricht die Textextraktion, denn dieselbe Glyphe kann legitim an unterschiedlichen Stellen unterschiedlichem Unicode-Inhalt entsprechen

Eine Ligatur-Glyphe ist der klare Fall. Dieselbe „ffi“-Glyphe kann in einem Wort für die drei Zeichen stehen und andernorts, nach einer anderen Shaping-Entscheidung, für etwas anderes. Die ToUnicode-Zuordnung ist nach CID indiziert, sodass eine geteilte CID nur eine einzige Zuordnung tragen kann, und welcher Text diesen Wettstreit auch verliert, wird nicht mehr extrahierbar

Also erhält jede Glyphen-Instanz ihre eigene CID, und jede Zuordnung darf frei aus mehreren UTF-16-Codeeinheiten bestehen. Eine Glyphe ohne sinnvollen Text — ein dekoratives Element, eine reine Positionierungsglyphe — bildet explizit auf U+200B ab, ein Leerzeichen ohne Breite, sodass jede CID ein beobachtbares Extraktionsergebnis statt einer Lücke hat

uses
  PDFium;

var
  Pdf: TPdf;
  Glyphs: TPdfCidGlyphs;
  Options: TPdfCidFontOptions;
  Report: TPdfCidFontReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'label.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;

    // Ein Eintrag pro geshapter Glyphe: die Glyphen-ID, der Text, den sie
    // repräsentiert, sowie ihr Vorschub und ihre Offsets im Textraum
    SetLength(Glyphs, 3);
    Glyphs[0].GlyphID := 402;  Glyphs[0].UnicodeText := 'ffi';
    Glyphs[0].Advance := 18.4;
    Glyphs[1].GlyphID := 71;  Glyphs[1].UnicodeText := 'c';
    Glyphs[1].Advance := 9.8;
    Glyphs[2].GlyphID := 74;   Glyphs[2].UnicodeText := 'e';
    Glyphs[2].Advance := 9.2;

    Options := TPdfCidFontOptions.Default;   // subset preferred
    Options.VerifyExtraction := True;

    if Pdf.AddCidType2Text(LoadFileBytes('NotoSans.ttf'), Glyphs,
         11, 72, 700, Options, Report) then
      Writeln(Format('%d glyphs, %d unique, font %d -> %d bytes, match=%s',
        [Report.GlyphCount, Report.UniqueGlyphCount,
         Report.OriginalFontBytes, Report.EmbeddedFontBytes,
         BoolToStr(Report.ExtractionMatches, True)]));
  finally
    Pdf.Free;
  end;
end;

Subsetting, und die Prüfungen, die ein Rückgabecode Ihnen nicht liefert

Eine Schrift auf die tatsächlich verwendeten Glyphen zu subsetten ist der Unterschied zwischen 300 KB und 12 KB Einbettung, und bei einem Dokument mit mehreren Schriften entscheidet das darüber, ob die Datei per E-Mail versendbar ist. Der Plattform-Subsetting-Pfad akzeptiert eine Glyphenliste direkt, was perfekt zu dieser API passt, da der Aufrufer bereits jede verwendete Glyphenkennung kennt

Was er Ihnen nicht liefert, ist Sicherheit. Ein Subsetting-Aufruf kann Erfolg melden und trotzdem eine unbrauchbare Ausgabe zurückliefern, daher validiert die Komponente drei Eigenschaften, bevor sie das Ergebnis akzeptiert: Die Ausgabe muss kleiner als das Original sein, sie muss sich als gültiges sfnt mit lesbarer maxp-Tabelle erneut parsen lassen, und sie muss Glyphen bis zur höchsten angeforderten ursprünglichen Kennung behalten. Jeder Fehlschlag bedeutet, dass das Subset abgelehnt wird

Was als Nächstes passiert, ist die Richtlinie des Aufrufers. Unter pcfemSubsetPreferred, dem Standard, fällt ein abgelehntes Subset auf das Einbetten der vollständigen Schrift zurück, sodass die Seite korrekt und nur größer ist. Unter pcfemSubsetRequired schlägt die Operation fehl, bevor die Seite geändert wird, was eine größenbeschränkte Pipeline möchte. pcfemFull überspringt Subsetting vollständig. Der Bericht sagt Ihnen über SubsetAttempted, SubsetApplied, UsedFullFontFallback und SubsetErrorCode, welcher Pfad genommen wurde

CID-Type-2-Font-Subsetting in Delphi durchläuft drei Validierungsprüfungen für Größe, sfnt-Integrität und Glyphenabdeckung vor der Annahme; ein abgelehntes Subset folgt der pcfem-Richtlinie aus Vollschrift-Rückfall oder hartem Fehlschlag
Ein Subset wird erst akzeptiert, nachdem die Größen-, sfnt- und Glyphenabdeckungsprüfungen bestanden sind, und ein abgelehnter Subset folgt dann der Aufrufer-Richtlinie — auf die volle Schrift zurückfallen oder scheitern, bevor die Seite verändert wird

Verifikation, die kein falsches Positiv erzeugen kann

Mit aktiviertem VerifyExtraction bestätigt die Komponente, dass sich das Geschriebene zurücklesen lässt. Der naive Weg dafür ist, die ganze Seite zu extrahieren und nach dem erwarteten String zu suchen, und das ist falsch: Eine Seite, die diesen Text bereits enthielt, besteht die Prüfung auch dann, wenn der neue Text falsch geschrieben wurde

Stattdessen wird die Textseite neu aufgebaut, und die von diesem Aufruf eingefügten Objekt-Handles werden einzeln, in Einfügereihenfolge, gelesen und aneinandergereiht. Das Ergebnis wird mit dem erwarteten Text verglichen, und der Bericht legt beide Strings zusammen mit dem Boolean offen, sodass eine Abweichung diagnostiziert und nicht nur erkannt werden kann

Schalten Sie es während der Entwicklung ein und in jeder Pipeline, in der Extrahierbarkeit eine Anforderung ist — durchsuchbare Archive, Barrierefreiheits-Compliance, nachgelagertes Text-Mining. Die Kosten sind ein Textseiten-Rebuild pro Aufruf, weshalb es in engen Schleifen nicht standardmäßig aktiviert ist

Budgets und atomarer Fehlschlag

Schrift-Bytes, Glyphen-Instanzen und Unicode-Codeeinheiten sind jeweils vor der Zuweisung gedeckelt, und Schriftformat, TTC-Index, Glyphenkennungsbereich und Geometriewerte werden validiert, bevor irgendetwas geschrieben wird. Geometrie muss endlich sein, was offensichtlich klingt, bis eine Shaping-Engine Ihnen einen NaN-Vorschub aus einer fehlerhaften Schrift liefert

Fehlschlag ist auf Seitenebene atomar. Schlägt das Laden der Schrift, das Schreiben von Objekten, die Inhaltserzeugung oder die Extraktionsverifikation fehl, wird jedes von diesem Aufruf eingefügte Objekt in umgekehrter Reihenfolge entfernt und der Seiteninhalt neu erzeugt. Ein fehlgeschlagener Aufruf lässt die Seite so, wie sie war, nicht mit einem halben Textlauf darauf

Wo sich das in eine Text-Pipeline einfügt

Die Arbeitsteilung ist es wert, klar benannt zu werden. Shaping — Zeichen in positionierte Glyphen zu verwandeln — ist nicht Aufgabe dieser API; das gehört einer Shaping-Engine, und die eigene bidirektionale und Complex-Script-Unterstützung der Komponente ist unter Behandlung von Emoji, CJK und Surrogatpaaren behandelt. Diese API rufen Sie anschließend auf, mit dem vom Shaper erzeugten Glyphenlauf

Für gewöhnlichen lateinischen Text ohne kontextabhängiges Shaping sind die einfacheren stringbasierten Text-APIs das richtige Werkzeug und erzeugen kleineren Code. Greifen Sie zum CID-Type-2-Schreiben, wenn Sie geshapte Ausgabe haben, wenn Sie exakte Kontrolle über Glyphenkennungen benötigen, oder wenn die Schrift aus von Ihnen gehaltenen Bytes eingebettet werden muss statt namentlich aufgelöst zu werden — wobei die Alternative der Schriftauflösung der unter Steuerung der PDF-Schriftersetzung beschriebene Provider-Mechanismus ist

Ein Hinweis zur Bereitstellung: Das Einbetten einer Schrift ist ebenso eine Lizenzfrage wie eine technische. Schriften unterscheiden sich darin, ob Einbettung überhaupt erlaubt ist, nur zur Anzeige erlaubt ist oder zur Bearbeitung erlaubt ist. Die Bibliothek bettet ein, welche Bytes auch immer Sie ihr geben, und die Lizenz zu prüfen ist Ihre Verantwortung, nicht die des Dateiformats

Text-Schreiben auf Glyphenebene, Schriftbereitstellung und Textextraktion teilen sich in Delphi, C++Builder und Lazarus dasselbe Seitenmodell; die vollständige API ist auf der Seite der PDFium Component für Delphi beschrieben