Technický článek

Vkládání fontů CID Type 2 do PDF z Delphi

PDFium Component zapisuje text do stránky PDF na úrovni glyfů pomocí AddCidType2Text a vkládá data TrueType, která dodáte, jako font CID Type 2. Přiřadí každé instanci glyfu sekvenční CID, vygeneruje explicitní mapu CID-to-GID a CMap ToUnicode a sloučí glyfy na stejné základní čáře do jednoho nativního textového objektu. Volitelný subsetting udržuje soubor malý, s explicitní zásadou pro to, co se stane, když subsetting selže

Zápis na úrovni glyfů potřebujete, když už byl text tvarovaný. Arabština, dévanágarí a jakékoli písmo s kontextovými formami produkují posloupnost identifikátorů glyfů, která už se nemapuje jedna ku jedné na znaky, takže API, které přebírá řetězec a název fontu, výsledek vyjádřit nedokáže. Předání glyfů a pozic přímo je jediný způsob, jak dostat správně tvarovaný text komplexního písma do PDF

AddCidType2Text v Delphi přiřadí každé instanci tvarovaného glyfu sekvenční CID, zapíše CIDToGIDMap a CMap ToUnicode a slije běh do jednoho textového objektu strany, který se extrahuje čistě
Každá instance glyfu dostane vlastní CID, slovník písma mapuje CID na id glyfů a zpět na Unicode a úsek dopadne na stránku jako jeden textový objekt, který se extrahuje zpět na zamýšlený text

Proč každá instance glyfu dostane vlastní CID

Lákavá optimalizace je deduplikace: jedno CID na odlišný identifikátor glyfu, znovu použité všude, kde se glyf objeví. Vyprodukuje to menší font a rozbije to extrakci textu, protože stejný glyf může legitimně odpovídat různému unicodovému obsahu na různých místech

Jasným případem je ligaturní glyf. Stejný glyf "ffi" může zastupovat tři znaky v jednom slově a po jiném tvarovacím rozhodnutí i něco jiného jinde. Mapa ToUnicode je klíčovaná podle CID, takže sdílené CID smí nést jen jedno mapování, a ať už jedna z těchto dvou textových instancí tento souboj prohraje, stane se neextrahovatelnou

Proto si každá instance glyfu bere vlastní CID a každé mapování smí obsahovat libovolný počet jednotek kódu UTF-16. Glyf bez smysluplného textu — dekorativní prvek, glyf jen pro pozicování — se mapuje explicitně na U+200B, mezeru nulové šířky, takže má každé CID pozorovatelný výsledek extrakce místo mezery

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;

    // Jedna položka na tvarovaný glyf: id glyfu, text, který zastupuje,
    // a jeho posun a offsety v textovém prostoru
    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;   // preferuje subset
    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 a kontroly, které vám návratový kód nedá

Subsetting fontu na skutečně použité glyfy je rozdíl mezi vložením 300 KB a vložením 12 KB, a u dokumentu s několika fonty rozhoduje o tom, zda je soubor odeslatelný e-mailem. Platformní cesta subsettingu přijímá seznam glyfů přímo, což se k tomuto API dokonale hodí, protože volající už zná každý použitý identifikátor glyfu

Co vám nedá, je jistota. Volání subsettingu může nahlásit úspěch a vrátit nepoužitelný výstup, takže komponenta před přijetím výsledku ověří tři vlastnosti: výstup musí být menší než originál, musí se znovu naparsovat jako platný sfnt s čitelnou tabulkou maxp a musí zachovat glyfy až po nejvyšší požadovaný původní identifikátor. Jakékoli selhání znamená, že se subset odmítne

Co se stane dál, je zásada volajícího. Pod pcfemSubsetPreferred, výchozí volbou, se odmítnutý subset propadne k vložení celého fontu, takže stránka je správná, jen větší. Pod pcfemSubsetRequired operace selže dřív, než se stránka upraví, což chce pipeline omezená velikostí. pcfemFull subsetting úplně přeskočí. Report vám řekne, kterou cestu si vzal, přes SubsetAttempted, SubsetApplied, UsedFullFontFallback a SubsetErrorCode

Podmnožinování fontu CID Type 2 v Delphi projde třemi validačními kontrolami na velikost, integritu sfnt a pokrytí glyfů před přijetím a zamítnutá podmnožina následuje politiku pcfem plné fontové zálohy nebo tvrdého selhání
Subset se přijme, až projdou kontroly velikosti, sfnt i pokrytí glyfů, a odmítnutý subset pak následuje politiku volajícího — spadnout k celému písmu, nebo selhat, dřív než se stránka změní

Verifikace, která nemůže vyprodukovat falešně pozitivní výsledek

Se zapnutým VerifyExtraction komponenta potvrdí, že to, co zapsala, lze přečíst zpátky. Naivní způsob, jak to udělat, je extrahovat celou stránku a hledat očekávaný řetězec, a je to špatně: stránka, která už daný text obsahovala, projde kontrolou i tehdy, když byl nový text zapsán chybně

Místo toho se textová stránka znovu sestaví a rukojeti objektů vložené tímto voláním se čtou jednotlivě, v pořadí vložení, a spojí se dohromady. Výsledek se porovná s očekávaným textem a report vystaví oba řetězce spolu s booleanem, takže lze neshodu diagnostikovat, ne jen odhalit

Zapněte to při vývoji a v jakémkoli pipeline, kde je extrahovatelnost požadavkem — prohledávatelné archivy, shoda s přístupností, navazující text mining. Cena je jedno znovusestavení textové stránky na volání, což je důvod, proč to není zapnuté ve výchozím stavu v úzkých smyčkách

Rozpočty a atomické selhání

Bajty fontu, instance glyfů a jednotky kódu Unicode jsou každé omezeny stropem před alokací a formát fontu, index TTC, rozsah identifikátoru glyfu a hodnoty geometrie se validují dřív, než se cokoli zapíše. Geometrie musí být konečná, což zní zjevně, dokud vám tvarovací engine nedodá NaN v posunu z poškozeného fontu

Selhání je atomické na úrovni stránky. Pokud selže načtení fontu, zápis objektů, generování obsahu nebo verifikace extrakce, každý objekt vložený tímto voláním se odstraní v opačném pořadí a obsah stránky se znovu vygeneruje. Neúspěšné volání zanechá stránku takovou, jaká byla, ne s napůl hotovým textovým během

Kam to zapadá v textovém pipeline

Rozdělení práce stojí za jasné vyslovení. Tvarování — přeměna znaků na umístěné glyfy — není úkolem tohoto API; patří tvarovacímu enginu a vlastní obousměrnou podporu komponenty a podporu komplexních písem popisuje článek zpracování emoji, CJK a náhradních párů. Toto API je to, co voláte poté, s během glyfů, který tvarovač vyprodukoval

Pro obyčejný latinský text bez kontextového tvarování jsou správným nástrojem jednodušší API pro text založená na řetězcích a produkují menší kód. Po zápisu CID Type 2 sáhněte, když máte tvarovaný výstup, když potřebujete přesnou kontrolu nad identifikátory glyfů, nebo když musí být font vložen z bajtů, které držíte, ne rozřešen podle jména — alternativou rozřešení fontu je mechanismus poskytovatele popsaný v článku řízení náhrady fontu PDF

Jedna poznámka k nasazení: vkládání fontu je stejnou měrou licenční otázka jako technická. Fonty se liší v tom, zda je vkládání vůbec povoleno, povoleno jen pro zobrazení, nebo povoleno pro úpravy. Knihovna vloží jakékoli bajty, které jí dáte, a kontrola licence je vaše odpovědnost, ne odpovědnost souborového formátu

Zápis textu na úrovni glyfů, poskytování fontů a extrakce textu sdílejí v Delphi, C++Builder a Lazarus stejný model stránky; úplné API popisuje stránka PDFium Component pro Delphi