Technický článek

Subsetting fontů už zabudovaných v PDF z Delphi

PDFium Component subsetuje fonty, které už jsou zabudované v existujícím PDF, přes SubsetPdfDocumentFonts v unitu FPdfDocumentFontSubset. Funkce vymění zabudovaný program TrueType /FontFile2 za menší jen tehdy, když samotné fontové zdroje dokládají každý glyf, na který dokument může odkazovat; font, jehož doménu glyfů nelze dokázat, zůstane přesně takový, jaký byl

Obvyklý důvod to chtít je archiv, který jste nevyprodukovali. Faktury, výpisy a reporty ze starších generátorů často nesou kompletní Arial nebo kompletní CJK face v každém souboru a pár stovek kilobajtů fontu na dvoustránkový dokument se rychle sčítá napříč miliony dokumentů. Přerenderovat ze zdroje bývá zřídka volba. To, co máte, je PDF, a jediná bezpečná otázka zní, kolik každého zabudovaného fontu může pryč, aniž by jediná stránka přišla o znak

Proč hrozí u subsettingu existujícího PDF ztráta glyfů?

Subsetting existujícího PDF hrozí ztrátou glyfů, protože post-procesor neví, které glyfy původní writer použil. Producent, který font zembeddedoval, znal svůj vlastní text; nástroj běžící poté musí tu znalost rekonstruovat a jakýkoli glyf, který mu unikne, se změní v prázdný box nebo tvar .notdef na stránce, na kterou se nikdo nepodívá, dokud se nepodívá zákazník

Zjevný přístup je proparsovat každý content stream, každý Form XObject, každou anotační appearance a každý defaultní appearance pole, sesbírat kódy ukázané s každým fontem a ty glyfy ponechat. Funguje to, dokud nenarazí na fontový zdroj sdílený napříč stránkami a formy, na formulářové pole, jehož appearance regeneruje prohlížeč z /DA, nebo na content stream se sekvencí operátorů, kterou skener nemodeluje. SubsetPdfDocumentFonts záměrně content streamy neprohledává. Doménu glyfů odvozuje z metadat, která o každém fontu musí PDF stejně nést: z /CIDToGIDMap, polí šířek /W a /W2, bitmapy /CIDSet, CMap /ToUnicode, pole /Widths prostého fontu a /Encoding. Neomezuje-li ta metadata doménu, font zůstane netknutý

Jak PDFium Component dokládá doménu glyfů při subsettingu zabudovaných fontů TrueType z Delphi: /CIDToGIDMap, šířky /W a /W2, /CIDSet, /ToUnicode, /Widths a /Encoding omezují doménu bez prohledávání content streamů a nedokazatelný font se zachová netknutý
SubsetPdfDocumentFonts mění metadata fontu na dokázanou doménu glyfů, než cokoli ořeže, a font, jehož zdroje doménu nemůžou omezit, se nahlásí a nechá netknutý

Držení původních ID glyfů, takže content streamy zůstávají netknuté

Subset backend za SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, drží každý původní slot ID glyfu, takže žádný content stream, CID ani /CIDToGIDMap se nikdy nemusí přepisovat. Znovu postaví tabulky glyf a loca, dá každému nepoužitému glyfu rozsah loca nulové délky a nechá maxp.numGlyphs nezměněné. Glyf 4711 je po subsettingu pořád glyf 4711; obrysy má jen tehdy, když k němu dokument dosáhne

Ta volba odstraňuje nejrizikovější část subsettingu hotového dokumentu. Přenumčíslování glyfů by znamenalo přepsat každý řetězec v každém content streamu, který font používá, plus mapovací stream, plus cokoli, co si ID glyfů cachuje. Zachované ID stojí trochu velikosti, protože loca drží jednu položku na původní glyf, ale hmotu programu TrueType tvoří data obrysů a právě to odejde. Kompozitní glyfy se sledují ke svým komponentám, takže písmeno s diakritikou si drží základní písmeno i diakritiku, ze které je poskládané; cyklické nebo příliš hluboké kompozitní reference selže font místo produkce rozbitého closure

Zachované ID glyfů po subsettingu zabudovaných fontů TrueType přes PDFium Component v Delphi: glyf 4711 si drží číslo i obrysy, nepoužité glyfy dostanou rozsahy loca nulové délky, zatímco maxp.numGlyphs zůstává nezměněné, takže content streamy, CIDs ani /CIDToGIDMap se nikdy nepřepisují
Protože ID glyfů subset přežijí, content streamy, CIDs ani /CIDToGIDMap nikdy nepotřebují přepis - zisk velikosti pochází ze zahozených obrysů, ne z přenumčíslování

Přejmenovaný font taky následuje ISO 32000-1 §9.6.4, které vyžaduje, aby název subset fontu začínal šesti velkými písmeny a znaménkem plus. Komponenta odvozuje šest písmen z digestu SHA-256 nového fontového programu, nejdřív odstraní jakoukoli existující předponu ABCDEF+, řeší kolize mezi fonty ve stejném dokumentu a zapisuje nový název do /FontName deskriptoru i do /BaseFont slovníku fontu, včetně rodiče Type0 CIDFontu

uses
  System.SysUtils, System.Classes, FPdfDocumentFontSubset;

procedure SubsetEmbeddedFonts(const SourceName, TargetName: string);
var
  Source, Target: TFileStream;
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Source := TFileStream.Create(SourceName, fmOpenRead or fmShareDenyWrite);
  try
    if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
      raise Exception.Create('Font subsetting failed: ' + Report.ErrorMessage);
  finally
    Source.Free;
  end;
  // Výstup je vždy kompletní PDF: znovu postavený soubor, když je Report.Applied
  // True, jinak bajt za bajtem kopie zdroje
  Target := TFileStream.Create(TargetName, fmCreate);
  try
    if Length(Output) > 0 then
      Target.WriteBuffer(Output[0], Length(Output));
  finally
    Target.Free;
  end;
end;

Jak SubsetPdfDocumentFonts dokládá doménu glyfů CIDFontu?

SubsetPdfDocumentFonts dokládá doménu glyfů fontu CIDFontType2 z jeho mapování CID-na-glyf a mapování rozhoduje, kolik další evidence je potřeba. Explicitní stream /CIDToGIDMap (ISO 32000-1 §9.7.4.2) je už sám o sobě seznamem každého glyfu, na který font může mířit, takže každý GID ve streamu se přidá k doméně. Mapovací stream větší než 65536 položek, nebo s lichým počtem bajtů, propadne ohraničenému profilu a font se zachová

/CIDToGIDMap /Identity je těžký případ a zároveň nejběžnější v moderních souborech. Identity říká, že CID rovná se GID, což neřekne nic o tom, které CIDs dokument používá. Komponenta proto vyžaduje primární důkaz: explicitní pole šířek /W nebo /W2 (§9.7.4.3), jehož položky CID značí glyfy, pro které producent deklaroval metriky, nebo stream /CIDSet ve fontovém deskriptoru (§9.8.3), bitmapu s jedním bitem na CID, nejvýznamnějším bitem první. Holý Identity font s ani jedním z nich se zachová s důvodem Identity CIDToGIDMap requires an explicit W array or CIDSet stream

Pastí v používání /W jako důkazu je /DW. Producent smí vynechat každý CID, jehož šířka se rovná defaultní šířce, a dobře optimalizovaný writer to dělá přesně u nejčastějších šířek glyfů. Pocházela-li by doména jen z /W, ty glyfy by se tiše ztratily ze subsetu. Aby tu mezeru zavřela, projde komponenta každý font Type0, jehož /DescendantFonts míří na CIDFont, vyžaduje jeho /Encoding /Identity-H nebo /Identity-V, takže 2bajtový kód je CID, a sloučí zdrojové kódy každé položky beginbfchar a beginbfrange z jeho CMap /ToUnicode (§9.10.3) do domény. Jakékoli jiné kódování, nebo stream /ToUnicode, který se nedá dekódovat v rámci rozpočtu, font zachová

Rozhodnutí o důkazu pro Identity CIDToGIDMap při subsettingu fontů PDFium Component pro Delphi: explicitní mapovací stream už vypisuje každý glyf, Identity vyžaduje pole šířek /W nebo /W2 nebo bitmapu /CIDSet a zdrojové kódy /ToUnicode zavírají slepé místo defaultní šířky /DW
Identity CIDToGIDMap sama o sobě o použití neřekne nic, takže subsetter vyžaduje důkaz /W nebo /CIDSet a sloučí kódy /ToUnicode, aby pokryl glyfy, které /W vynechává u /DW

Prosté fonty TrueType: od Widths k ID glyfů

Jednobajtové fonty /Subtype /TrueType se dokládají přes jejich rozsah kódů a jejich kódování a pak se řeší na ID glyfů přes vlastní cmap fontového programu. Komponenta vyžaduje 0 <= FirstChar <= LastChar <= 255 a pole /Widths, jehož délka je přesně LastChar - FirstChar + 1; krátké, dlouhé nebo nečíselné pole znamená, že doména kódů není dokázaná a font zůstává, jak je

Každý kód v tom rozsahu, plus každý kód pojmenovaný v /Differences nebo pokrytý /ToUnicode, se pak mapuje způsobem, který popisuje ISO 32000-1 §9.6.6. Kódy pod /WinAnsiEncoding, /MacRomanEncoding nebo /StandardEncoding jdou přes odpovídající tabulku na Unicode. Názvy glyfů /Differences se řeší přes Adobe Glyph List, názvy uniXXXX a uXXXX, 258 standardních názvů glyfů Macintosh a vlastní tabulku názvů post formát 2.0 fontu. Výsledné hodnoty se hledají v subtabulkách cmap formát 0, 4, 6 a 12, včetně Unicode (3,1), (3,10) a (0,*), Mac Roman (1,0) a Windows Symbol (3,0) na F000 plus kód

Nemá-li font vůbec žádné základní kódování, komponenta označí sjednocení každé věrohodné interpretace místo hádání jedné. To ponechá pár glyfů, které jsou nejspíš nepoužité, což je správný směr, do kterého se mýlit. Opačný případ je striktní: název /Differences, který se nemapuje na žádné dokázané ID glyfu, odmítne celý font, protože jeden nevyřešený název znamená, že doména není kompletní

procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
  I: Integer;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Options.SubsetOptions.MinimumSavingPercent := 10;  // na font
  Options.MinimumDocumentSavingPercent := 5;         // celý soubor
  if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
  begin
    Log.Add('Rejected: ' + Report.ErrorMessage);
    Exit;
  end;
  Log.Add(Format('Fonts %d, subset %d, preserved %d, glyphs %d',
    [Report.FontCount, Report.SubsetFontCount, Report.PreservedFontCount,
     Report.GlyphCount]));
  Log.Add(Format('Font bytes %d -> %d, file bytes %d -> %d',
    [Report.OriginalFontBytes, Report.SubsetFontBytes,
     Report.SourceByteCount, Report.OutputByteCount]));
  for I := 0 to High(Report.Issues) do
    Log.Add(Format('  object %d kept: %s',
      [Report.Issues[I].ObjectNumber, Report.Issues[I].Reason]));
end;

Co SubsetPdfDocumentFonts nechává na pokoji?

SubsetPdfDocumentFonts nechává na pokoji každý font, který nedokáže dokázat, a hlásí každý z nich v Report.Issues s číslem objektu a důvodem místo selhání dokumentu. Report.Complete je True jen tehdy, když se žádný font nezachoval. Zachované případy stojí za znalost, než slíbíte zmenšení velikosti:

  • Programy Type 1 /FontFile a CFF nebo OpenType /FontFile3, hlášené jako Only TrueType FontFile2 programs are supported
  • Identity CIDFonts bez /W, /W2 ani /CIDSet a CIDFonts bez mapovacího streamu i bez /Identity
  • Fontové programy odkazované odkudkoli jiného než jejich fontové deskriptory a deskriptory sdílené s konzumentem, který není CIDFontType2 ani prostý TrueType
  • Fonty, jejichž subset minul SubsetOptions.MinimumSavingPercent, a každý font, když znovu postavený soubor minul MinimumDocumentSavingPercent; v tom případě je Report.Applied False a výstupem jsou původní bajty

Dva vstupy selhávají rovnou místo zachování. Podepsané dokumenty a dokumenty s podpisovými poli se odmítají, protože výměna fontových objektů by zneplatnila byte rozsah každého podpisu, a šifrované soubory potřebují nejdřív dešifrovaný zdroj. Úspěšný běh zapisuje náhradní objekty jako řídký update a pak znovu postaví soubor přes tentýž průchod dosažitelností popsaný v průvodci prořezáváním sirotků a deduplikací zdrojů, takže staré fontové programy nepřežijí jako nedosažitelné objekty a výsledkem je jediná čistá revize

Subsetting nového textu při uložení přes saSubsetNewFonts

Fonty, které PDFium Component embedduje během aktuální session, jsou samostatný případ, obsloužený volbou uložení saSubsetNewFonts místo SubsetPdfDocumentFonts. saSubsetNewFonts se mapuje na flag PDFium FPDF_SUBSET_NEW_FONTS, který spustí nativní subsetter HarfBuzz nad fonty, které nativní volání jako TPdf.AddText načetla ze systému, včetně OpenType CFF face. Upstream PDFium flag pořád označuje jako experimentální. Netýká se fontů, které už v souboru byly

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);
    Pdf.AddText('Quarterly statement', 'Arial', 14, 72, 770, clBlack, $FF, 0.0);
    if not Pdf.SaveAs('statement.pdf', saSubsetNewFonts) then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

V sondě komponenty dokument s textem Arial klesl pod saSubsetNewFonts z 590 KB na 22 KB a extrakce textu z uloženého souboru stále seděla přesně. Barevný font COLR v0, Segoe UI Emoji o 7,0 MB, prošel tou samou cestou na 10,5 KB, vyrenderovaný jako základní obrysy, protože PDF nemá ponětí o vrstvách COLR. Výjimkou jsou variable fonty a strop stojí před subsetterem: GDI GetFontData vrátil pro 1,85 MB soubor Segoe UI Variable náhradní stream 18,3 MB a vytváření textového objektu selhalo dřív, než začal jakýkoli subsetting. Průchod embeddingem fontu CID Type 2 z Delphi popisuje embedding cestu, kterou ty fonty nejdřív procházejí

Kde důkaz končí

Důkaz uvnitř SubsetPdfDocumentFonts je přesně tak silný, jaká je metadata fontu, která producent zapsala. Identity CIDFont, jehož důkazem je jen /W, má pořád jedno slepé místo: glyf, jehož šířka se rovná /DW, který nemá položku /ToUnicode a který deskriptor neuvádí v /CIDSet. Takový glyf se ze zdrojů nedokáže a komponenta nemůže vědět, že existuje, bez prohledání content streamů, které se záměrně vyhýbá. Producenti, kteří vynechají obojí /ToUnicode i /CIDSet, zatímco stlačují /W, jsou vzácní, ale existují, takže u archivu od neznámého generátoru vyrenderujte vzorek před i po a srovnejte pixely i extrahovaný text

Před spuštěním dávky také pomáhá vědět, s čím máte co do činění. Přístup v čtení vlastností fontů PDF s PDFium Component vám řekne, které fonty jsou vůbec zabudované, a Report.Issues ze suchého běhu řekne, které z nich se zachovají a proč. Defaultní volby také ohraničují práci: 1024 fontů, zhruba milion glyfů celkem, 128 MB zdroje a pevný pracovní rozpočet, s volitelným CancellationToken pro dlouhé dávkové úlohy

FAQ

Mění SubsetPdfDocumentFonts text nebo layout PDF?

Ne. Původní ID glyfů, CIDs, šířky i content streamy se zachovávají, takže extrakce textu i renderování používá tytéž glyfy jako dřív; odstraní se jen nedosažitelné obrysy a názvy fontů dostanou novou subset předponu

Proč se zabudovaný font nesubsetoval?

Čtěte Report.Issues. Každý zachovaný font nese důvod, jako chybějící /W nebo /CIDSet u Identity CIDFontu, ne-TrueType program, poškozené pole /Widths nebo úsporu pod prahem

Je saSubsetNewFonts totéž co SubsetPdfDocumentFonts?

Ne. saSubsetNewFonts subsetuje fonty embeddované v aktuální session při uložení, zatímco SubsetPdfDocumentFonts pracuje na existujícím PDF streamu a mění fonty, které už zembeddedoval jiný producent

Subsetting fontů dokumentu, subsetting fontů při uložení i prořez pass sirotků za nimi všechny dodává PDFium Component for Delphi and C++Builder, který obaluje PDFium pro projekty v Delphi, C++Builderu a Lazarusu