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ý
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
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á
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
/FontFilea CFF nebo OpenType/FontFile3, hlášené jakoOnly TrueType FontFile2 programs are supported - Identity CIDFonts bez
/W,/W2ani/CIDSeta 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í
CIDFontType2ani prostýTrueType - Fonty, jejichž subset minul
SubsetOptions.MinimumSavingPercent, a každý font, když znovu postavený soubor minulMinimumDocumentSavingPercent; v tom případě jeReport.AppliedFalsea 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