Teknisk artikel

Subset fonts, der allerede er indlejret i en PDF, fra Delphi

PDFium Component subsets fonts, der allerede er indlejret i en eksisterende PDF, gennem SubsetPdfDocumentFonts i FPdfDocumentFontSubset-uniten. Funktionen erstatter et indlejret TrueType /FontFile2-program med et mindre, kun når fontressourcerne selv beviser hver glyph, dokumentet kan referere; en font, hvis glyph-domæne ikke kan bevises, efterlades præcis, som den var

Den sædvanlige grund til at ønske dette er et arkiv, du ikke selv har produceret. Fakturaer, kontoudtog og rapporter fra ældre generatorer bærer ofte en komplet Arial eller en fuld CJK-face i hver fil, og nogle hundrede kilobytes font pr. to-siders dokument lægges hurtigt op over et par millioner dokumenter. At re-rendere fra kilden er sjældent en mulighed. Det, du har, er PDF'en, og det eneste sikre spørgsmål er, hvor meget af hver indlejret font der kan gå, uden at en eneste side mister et tegn

Hvorfor risikerer subsetting af en eksisterende PDF at tabe glyphs?

Subsetting af en eksisterende PDF risikerer at tabe glyphs, fordi efterprocessoren ikke ved, hvilke glyphs den oprindelige writer brugte. Producenten, der indlejrede fonten, kendte sin egen tekst; et værktøj, der kører bagefter, må rekonstruere den viden, og enhver glyph, den misser, bliver til en tom boks eller en .notdef-form på en side, ingen kigger på, indtil en kunde gør

Den oplagte tilgang er at parse hver content stream, hver Form XObject, hver annotationsappearance og hver feltdefault-appearance, indsamle koderne vist med hver font og beholde de glyphs. Det virker, indtil det møder en fontressource delt hen over sider og forms, et formularfelt, hvis appearance en viewer regenererer fra /DA, eller en content stream med en operatorsekvens, scanneren ikke modeller. SubsetPdfDocumentFonts scanner bevidst ikke content streams. Den udleder glyph-domænet fra de metadata, PDF'en alligevel skal bære om hver font: /CIDToGIDMap, /W- og /W2-breddearrayerne, /CIDSet-bitmappen, /ToUnicode-CMap'en, simple-font /Widths-arrayet og /Encoding. Afgrænser metadataet ikke domænet, forbliver fonten urørt

Hvordan PDFium Component beviser glyph-domænet ved subsetting af indlejrede TrueType-fonts fra Delphi: /CIDToGIDMap, /W- og /W2-bredder, /CIDSet, /ToUnicode, /Widths og /Encoding afgrænser domænet uden at scanne content streams, og en ubeviselig font bevares urørt
SubsetPdfDocumentFonts omsætter fontmetadata til et bevist glyph-domæne, før noget skæres, og en font, hvis ressourcer ikke kan afgrænse domænet, rapporteres og efterlades urørt

At bevare originale glyph-ID'er, så content streams forbliver urørte

Subset-backenden bag SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, beholder hver original glyph-ID-slot, så ingen content stream, CID eller /CIDToGIDMap nogensinde behøver omskrives. Den genopbygger glyf- og loca-tabellerne, giver hver ubrugt glyph et nul-længde loca-interval og lader maxp.numGlyphs stå uændret. Glyph 4711 er stadig glyph 4711 efter subsetting; den har blot outlines, kun hvis dokumentet kan nå den

Det valg fjerner den risikableste del af at subsette et færdigt dokument. At omnummerere glyphs ville betyde at omskrive hver streng i hver content stream, der bruger fonten, plus mapping-streamen, plus alt, der cacher glyph-ID'er. Beholder-ID'er koster lidt størrelse, fordi loca beholder én indgang pr. original glyph, men størstedelen af et TrueType-program er outlinedata, og dét er det, der går. Komposite glyphs følges til deres komponenter, så et accentbogstav beholder grundbogstavet og accenten, det er bygget af; cykliske eller for dybe kompositreferencer får fonten til at fejle i stedet for at producere en knækket lukning

Beholder-glyph-ID'er efter subsetting af indlejrede TrueType-fonts med PDFium Component i Delphi: glyph 4711 beholder sit nummer og sine outlines, ubrugte glyphs får nul-længde loca-intervaller, mens maxp.numGlyphs forbliver uændret, så content streams, CIDs og /CIDToGIDMap aldrig omskrives
Fordi glyph-ID'er overlever subsettet, behøver content streams, CIDs og /CIDToGIDMap aldrig omskrives - størrelsesgevinsten kommer fra droppede outlines, ikke fra omnummerering

Den omdøbte font følger også ISO 32000-1 §9.6.4, som kræver, at et subset-fontnavn starter med seks store bogstaver og et plus-tegn. Komponenten udleder de seks bogstaver fra en SHA-256-digest af det nye fontprogram, stripper eventuel eksisterende ABCDEF+-præfiks først, resolver kollisioner mellem fonts i samme dokument og skriver det nye navn til både deskriptorens /FontName og fontdictionaryens /BaseFont, inklusive en CIDFonts Type0-forælder

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;
  // Output er altid en komplet PDF: den genopbyggede fil, når Report.Applied
  // er True, ellers en byte-for-byte-kopi af kilden
  Target := TFileStream.Create(TargetName, fmCreate);
  try
    if Length(Output) > 0 then
      Target.WriteBuffer(Output[0], Length(Output));
  finally
    Target.Free;
  end;
end;

Hvordan beviser SubsetPdfDocumentFonts en CIDFonts glyph-domæne?

SubsetPdfDocumentFonts beviser en CIDFontType2-fonts glyph-domæne ud fra dens CID-til-glyph-mapping, og mappingen beslutter, hvor meget ekstra bevismateriale der behøves. En eksplicit /CIDToGIDMap-stream (ISO 32000-1 §9.7.4.2) er allerede en liste over hver glyph, fonten kan adressere, så hver GID i streamen går ind i domænet. En mapping-stream større end 65536 indgange, eller én med et ulige byteantal, fejler den afgrænsede profil, og fonten bevares

/CIDToGIDMap /Identity er det svære tilfælde, og det er også det mest almindelige i moderne filer. Identity siger, at CID er lig GID, hvilket ikke fortæller dig noget om, hvilke CIDs dokumentet bruger. Komponenten kræver derfor primært bevismateriale: et eksplicit /W- eller /W2-breddearray (§9.7.4.3), hvis CID-indgange markerer glyphs, producenten erklærede metrics for, eller en /CIDSet-stream i fontdeskriptoren (§9.8.3), en bitmap med én bit pr. CID, mest signifikante bit først. En nøgen Identity-font uden nogen af delene bevares med begrundelsen Identity CIDToGIDMap requires an explicit W array or CIDSet stream

Fælden ved at bruge /W som bevismateriale er /DW. En producent må udelade hver CID, hvis bredde er lig standardbredden, og en velloptimeret writer gør netop det for de mest almindelige glyph-bredder. Kom domænet fra /W alene, ville de glyphs stille forsvinde fra subsettet. For at lukke det hul gennemgår komponenten hver Type0-font, hvis /DescendantFonts peger på CIDFonten, kræver, at dens /Encoding er /Identity-H eller /Identity-V, så en 2-byte-kode er CID'en, og fletter kildekoderne fra hver beginbfchar- og beginbfrange-indgang fra dens /ToUnicode-CMap (§9.10.3) ind i domænet. Enhver anden encoding, eller en /ToUnicode-stream, der ikke kan dekodes inden for budgettet, bevarer fonten

Bevisbeslutning for en Identity CIDToGIDMap i PDFium Components fontsubsetting til Delphi: en eksplicit mapping-stream lister allerede hver glyph, Identity kræver et /W- eller /W2-breddearray eller en /CIDSet-bitmap, og /ToUnicode-kildekoder lukker /DW-standardbredde-blindvinklen
En Identity CIDToGIDMap siger intet om brug på egen hånd, så subsetteren kræver /W- eller /CIDSet-bevismateriale og fletter /ToUnicode-koder for at dække de glyphs, /W udelader ved /DW

Simple TrueType-fonts: fra Widths til glyph-ID'er

Enkelt-byte /Subtype /TrueType-fonts bevises gennem deres kodeinterval og deres encoding og resolves derefter til glyph-ID'er gennem fontprogrammets egen cmap. Komponenten kræver 0 <= FirstChar <= LastChar <= 255 og et /Widths-array, hvis længde er præcis LastChar - FirstChar + 1; et kort, langt eller ikke-numerisk array betyder, at kodedomænet ikke er bevist, og fonten forbliver, som den er

Hver kode i det interval plus enhver kode navngivet i /Differences eller dækket af /ToUnicode mappes derefter, som ISO 32000-1 §9.6.6 beskriver. Koder under /WinAnsiEncoding, /MacRomanEncoding eller /StandardEncoding går gennem den matchende tabel til Unicode. /Differences-glyphnavne resolves gennem Adobe Glyph List, uniXXXX- og uXXXX-navne, de 258 standardiserede Macintosh-glyphnavne og fontens egen post format 2.0-navnetabel. De resulterende værdier slås op i format 0, 4, 6 og 12 cmap-undertabeller, inklusive Unicode (3,1), (3,10) og (0,*), Mac Roman (1,0) og Windows Symbol (3,0) ved F000 plus koden

Har en font slet ingen base-encoding, markerer komponenten foreningen af enhver plausibel fortolkning i stedet for at gætte én. Det beholder nogle få glyphs, der sandsynligvis er ubrugte, hvilket er den rigtige retning at tage fejl i. Det modsatte tilfælde er strengt: et /Differences-navn, der mapper til intet bevist glyph-ID, afviser hele fonten, fordi ét uopløst navn betyder, at domænet er ufuldstændigt

procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
  I: Integer;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Options.SubsetOptions.MinimumSavingPercent := 10;  // pr. font
  Options.MinimumDocumentSavingPercent := 5;         // hele filen
  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;

Hvad lader SubsetPdfDocumentFonts i fred?

SubsetPdfDocumentFonts lader hver font, den ikke kan bevise, i fred og rapporterer hver enkelt i Report.Issues med objektnummeret og en begrundelse i stedet for at få dokumentet til at fejle. Report.Complete er True kun, når ingen font blev bevaret. De bevarede tilfælde er værd at kende, før du lover en størrelsesreduktion:

  • Type 1 /FontFile og CFF- eller OpenType /FontFile3-programmer, rapporteret som Only TrueType FontFile2 programs are supported
  • Identity CIDFonts uden /W, /W2 eller /CIDSet, og CIDFonts med hverken en mapping-stream eller /Identity
  • Fontprogrammer refereret fra et andet sted end deres fontdeskriptorer, og deskriptorer delt med en forbruger, der ikke er CIDFontType2 eller simple TrueType
  • Fonts, hvis subset misser SubsetOptions.MinimumSavingPercent, og alle fonts, når den genopbyggede fil misser MinimumDocumentSavingPercent; i det tilfælde er Report.Applied False, og outputtet er de originale bytes

To input fejler kategorisk i stedet for at blive bevaret. Signerede dokumenter og dokumenter med signaturfelter afvises, fordi udskiftning af fontobjekter ville invalidere byte-range'en af hver signatur, og krypterede filer behøver en dekrypteret kilde først. En vellykket kørsel skriver erstatningsobjekterne som en sparsom opdatering og genopbygger derefter filen gennem det samme reachability-pass, der er beskrevet i guiden til at beskære orphan-objekter og deduplikere ressourcer, så de gamle fontprogrammer ikke overlever som utilgængelige objekter, og resultatet er én enkelt ren revision

Subsetting af ny tekst ved gemning med saSubsetNewFonts

Fonts, som PDFium Component indlejrer i den aktuelle session, er et separat tilfælde, håndteret af gem-indstillingen saSubsetNewFonts frem for af SubsetPdfDocumentFonts. saSubsetNewFonts mapper til PDFium FPDF_SUBSET_NEW_FONTS-flagget, som kører den native HarfBuzz-subsetter hen over fonts, som native kald som TPdf.AddText loadede fra systemet, inklusive OpenType CFF-faces. Upstream PDFium markerer stadig flagget som eksperimentelt. Det rører ikke fonts, der allerede var i filen

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;

I komponentens probe faldt et dokument med Arial-tekst fra 590 KB til 22 KB under saSubsetNewFonts, og tekstudtrækning fra den gemte fil matchede stadig præcis. En COLR v0 color font, Segoe UI Emoji på 7,0 MB, gik samme vej til 10,5 KB, rendert som sine basisoutlines, da PDF intet begreb om COLR-lag har. Variable fonts er undtagelsen, og blokken sidder foran subsetteren: GDI GetFontData returnerede en 18,3 MB-erstatningsstream for den 1,85 MB Segoe UI Variable-fil, og tekstobjektoprettelse fejlede, før nogen subsetting begyndte. Gennemgangen af CID Type 2-fontindlejring fra Delphi dækker indlejrstien, de fonts går først

Hvor beviset stopper

Beviset inde i SubsetPdfDocumentFonts er præcis så stærkt som den fontmetadata, producenten skrev. En Identity CIDFont, hvis bevismateriale er /W alene, har stadig én blind vinkel: en glyph, hvis bredde er lig /DW, som ikke har nogen /ToUnicode-indgang, og som deskriptoren ikke lister i en /CIDSet. En sådan glyph er ikke beviselig fra ressourcerne, og komponenten kan ikke vide, at den eksisterer, uden den content stream-scanning, den bevidst undgår. Producenter, der udelader både /ToUnicode og /CIDSet, mens de komprimerer /W, er sjældne, men de findes, så for et arkiv fra en ukendt generator, render en prøve før og efter og sammenlign pixels og udtrukket tekst

Før du kører en batch, hjælper det også at vide, hvad du har med at gøre. Tilgangen i læsning af PDF-fontegenskaber med PDFium Component fortæller dig, hvilke fonts der overhovedet er indlejret, og Report.Issues fra en tørkørsel fortæller dig, hvilke af dem der bevares og hvorfor. Standardindstillingerne afgrænser også arbejdet: 1024 fonts, omkring en million glyphs i alt, 128 MB kilde og et fast arbejdsbudget med en valgfri CancellationToken til lange batchjobs

FAQ

Ændrer SubsetPdfDocumentFonts teksten eller layoutet i PDF'en?

Nej. Originale glyph-ID'er, CIDs, bredder og content streams beholdes, så tekstudtrækning og rendering bruger samme glyphs som før; kun utilgængelige outlines fjernes, og fontnavnene får et nyt subset-præfiks

Hvorfor blev en indlejret font ikke subsettet?

Læs Report.Issues. Hver bevaret font bærer en begrundelse som en manglende /W eller /CIDSet på en Identity CIDFont, et ikke-TrueType-program, et misdannet /Widths-array eller en besparelse under tærsklen

Er saSubsetNewFonts det samme som SubsetPdfDocumentFonts?

Nej. saSubsetNewFonts subsetter fonts indlejret i den aktuelle session, når du gemmer, mens SubsetPdfDocumentFonts arbejder på en eksisterende PDF-stream og erstatter fonts, der allerede var indlejret af en anden producent

Dokumentfont-subsetting, gemtidspunkts-fontsubsetting og orphan-beskæringspasset bag dem alle følger med PDFium Component for Delphi og C++Builder, som wrapper PDFium til Delphi-, C++Builder- og Lazarus-projekter