Technisch artikel

CID Type 2-lettertypen insluiten in PDF vanuit Delphi

PDFium Component schrijft tekst op glyphniveau naar een PDF-pagina met AddCidType2Text, waarbij het door u geleverde TrueType-data wordt ingesloten als een CID Type 2-lettertype. Het kent elke glyphinstantie een opeenvolgende CID toe, genereert de expliciete CID-naar-GID-map en de ToUnicode-CMap, en voegt glyphs op dezelfde baseline samen tot één native tekstobject. Optionele subsetting houdt het bestand klein, met een expliciet beleid voor wat er gebeurt wanneer subsetting mislukt

Schrijven op glyphniveau is wat u nodig hebt wanneer de tekst al is geshaped. Arabisch, Devanagari en elk schrift met contextuele vormen produceren een reeks glyph-identifiers die niet meer één-op-één op tekens afbeeldt, zodat een API die een string en een lettertypenaam neemt, het resultaat niet kan uitdrukken. Glyphs en posities rechtstreeks doorgeven is de enige manier om correct geshapete tekst in complexe schriften in een PDF te krijgen

AddCidType2Text in Delphi wijst elke gevormde glyph-instantie een sequentiele CID toe, schrijft de CIDToGIDMap en ToUnicode CMap, en voegt de run samen in één pagina-tekstobject dat netjes extraheert
Elke glyph-instantie neemt zijn eigen CID, de font dictionary brengt CIDs in kaart naar glyph-id's en terug naar Unicode, en de run landt op de pagina als één tekstobject dat weer extraheert naar de beoogde tekst

Waarom elke glyphinstantie zijn eigen CID krijgt

De verleidelijke optimalisatie is deduplicatie: één CID per unieke glyph-identifier, hergebruikt overal waar die glyph voorkomt. Dat levert een kleiner lettertype op en het breekt tekstextractie, want dezelfde glyph kan op verschillende plekken legitiem overeenkomen met andere Unicode-inhoud

Een ligatuurglyph is het duidelijke voorbeeld. Dezelfde "ffi"-glyph kan op de ene plek staan voor de drie tekens in één woord en, na een andere shapingbeslissing, elders voor iets anders. De ToUnicode-map is geïndexeerd op CID, dus een gedeelde CID kan maar één mapping dragen, en welke tekst die strijd ook verliest, wordt niet-extraheerbaar

Dus neemt elke glyphinstantie zijn eigen CID, en elke mapping mag uit meerdere UTF-16-code-units bestaan. Een glyph zonder betekenisvolle tekst — een decoratief element, een glyph die alleen voor positionering dient — wordt expliciet gemapt op U+200B, een zero-width space, zodat elke CID een waarneembaar extractieresultaat heeft in plaats van een gat

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;

    // Eén entry per geshapete glyph: het glyph-id, de tekst die het vertegenwoordigt,
    // en de advance en offsets ervan in text space
    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 gewenst
    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, en de controles die een returncode u niet geeft

Een lettertype subsetten tot de daadwerkelijk gebruikte glyphs is het verschil tussen 300 KB insluiten en 12 KB insluiten, en bij een document met meerdere lettertypen bepaalt dat of het bestand e-mailbaar is. Het subsettingtraject van het platform accepteert rechtstreeks een glyphlijst, wat perfect aansluit bij deze API, omdat de aanroeper al elke gebruikte glyph-identifier kent

Wat het u niet geeft, is zekerheid. Een subsettingaanroep kan succes rapporteren en toch output teruggeven die onbruikbaar is, dus valideert de component drie eigenschappen voordat het resultaat wordt geaccepteerd: de output moet kleiner zijn dan het origineel, het moet opnieuw parsen als een geldige sfnt met een leesbare maxp-tabel, en het moet glyphs behouden tot en met de hoogste gevraagde oorspronkelijke identifier. Elke mislukking betekent dat de subset wordt afgewezen

Wat er daarna gebeurt, is het beleid van de aanroeper. Onder pcfemSubsetPreferred, de standaard, valt een afgewezen subset terug op het insluiten van het volledige lettertype, zodat de pagina correct is en alleen groter. Onder pcfemSubsetRequired mislukt de bewerking voordat de pagina wordt gewijzigd, wat is wat een pijplijn met een grootte-beperking wil. pcfemFull slaat subsetting helemaal over. Het rapport vertelt u welk traject is gevolgd via SubsetAttempted, SubsetApplied, UsedFullFontFallback en SubsetErrorCode

CID Type 2 fontsubsetting in Delphi doorloopt drie validatiecontroles voor grootte, sfnt-integriteit en glyphdekking vóór acceptatie, en een geweigerd subset volgt het pcfem-beleid van volledige fontterugval of harde mislukking
Een subset wordt pas aanvaard nadat de grootte-, sfnt- en glyph-dekkingscontroles slagen, en een geweigerde subset volgt dan het aanroeper-beleid — terugvallen op het volledige lettertype of falen voordat de pagina wordt gewijzigd

Verificatie die geen vals-positief resultaat kan opleveren

Met VerifyExtraction ingeschakeld bevestigt de component dat wat het heeft geschreven, kan worden teruggelezen. De naïeve manier om dat te doen, is de hele pagina extraheren en zoeken naar de verwachte string, en dat is fout: een pagina die die tekst al bevatte, doorstaat de controle zelfs wanneer de nieuwe tekst incorrect is geschreven

In plaats daarvan wordt de tekstpagina herbouwd en worden de objecthandles die door deze aanroep zijn ingevoegd, afzonderlijk gelezen, in invoegvolgorde, en samengevoegd. Het resultaat wordt vergeleken met de verwachte tekst, en het rapport toont beide strings samen met de boolean, zodat een mismatch kan worden gediagnosticeerd in plaats van alleen gedetecteerd

Schakel het in tijdens ontwikkeling en in elke pijplijn waar extraheerbaarheid een vereiste is — doorzoekbare archieven, toegankelijkheidscompliance, downstream text mining. De kosten zijn één herbouw van de tekstpagina per aanroep, wat de reden is dat het standaard niet aanstaat in krappe lussen

Budgetten en atomair falen

Lettertypebytes, glyphinstanties en Unicode-code-units worden elk begrensd vóór allocatie, en het lettertypeformaat, de TTC-index, het bereik van glyph-identifiers en geometriewaarden worden gevalideerd voordat er iets wordt geschreven. Geometrie moet eindig zijn, wat vanzelfsprekend klinkt totdat een shapingengine u een NaN-advance uit een misvormd lettertype geeft

Falen is atomair op paginaniveau. Als het laden van het lettertype, het schrijven van objecten, het genereren van content of de extractieverificatie mislukt, wordt elk object dat door die aanroep is ingevoegd in omgekeerde volgorde verwijderd en wordt de paginacontent opnieuw gegenereerd. Een mislukte aanroep laat de pagina zoals ze was, niet met een halve tekstrun erop

Waar dit past in een tekstpijplijn

De taakverdeling is het waard om duidelijk te benoemen. Shaping — tekens omzetten in gepositioneerde glyphs — is niet de taak van deze API; dat is de taak van een shapingengine, en de eigen bidirectionele en complexe-schriftondersteuning van de component wordt behandeld in de afhandeling van emoji, CJK en surrogaatparen. Deze API is wat u daarna aanroept, met de glyphrun die de shaper heeft geproduceerd

Voor gewone Latijnse tekst zonder contextuele shaping zijn de eenvoudigere op strings gebaseerde tekst-API's het juiste gereedschap en leveren ze kleinere code op. Grijp naar CID Type 2-writing wanneer u geshapete output hebt, wanneer u exacte controle over glyph-identifiers nodig hebt, of wanneer het lettertype moet worden ingesloten vanuit bytes die u zelf beheert in plaats van opgelost via een naam — het alternatief voor lettertyperesolutie is het providermechanisme beschreven in het beheersen van PDF-lettertypesubstitutie

Eén implementatie-opmerking: een lettertype insluiten is net zozeer een licentiekwestie als een technische. Lettertypen verschillen erin of insluiten überhaupt is toegestaan, alleen is toegestaan voor weergave, of is toegestaan voor bewerking. De library sluit in wat voor bytes u haar ook geeft, en het controleren van de licentie is uw verantwoordelijkheid, niet die van het bestandsformaat

Tekst schrijven op glyphniveau, lettertypebeheer en tekstextractie delen hetzelfde paginamodel in Delphi, C++Builder en Lazarus; de volledige API wordt beschreven op de PDFium Component voor Delphi-pagina