Technisch artikel

Ingebedde fonts in een PDF subsetten vanuit Delphi

PDFium Component maakt subsets van fonts die al in een bestaande PDF zijn ingebed via SubsetPdfDocumentFonts in de unit FPdfDocumentFontSubset. De functie vervangt een ingebed TrueType-programma /FontFile2 door een kleiner alleen wanneer de fontresources zelf elk glyph bewijzen dat het document kan refereren; een font waarvan het glyph-domein niet kan worden bewezen blijft exact zoals hij was

De gebruikelijke reden om dit te willen is een archief dat u niet hebt geproduceerd. Facturen, afschriften en rapporten van oudere generators dragen vaak een complete Arial of een volledig CJK-font in elk bestand, en een paar honderd kilobyte font per document van twee pagina's telt snel op over een paar miljoen documenten. Opnieuw renderen vanuit de bron is zelden een optie. Wat u hebt is de PDF, en de enige veilige vraag is hoeveel van elk ingebed font kan zonder dat één pagina een teken verliest

Waarom loopt het subsetten van een bestaande PDF het risico glyphs te verliezen?

Subsetten van een bestaande PDF loopt het risico glyphs te verliezen, want de postprocessor weet niet welke glyphs de originele writer gebruikte. De producent die het font inbedde kende zijn eigen tekst; een tool die daarna draait moet die kennis reconstrueren, en elk glyph dat hij mist wordt een leeg vierkant of een vorm .notdef op een pagina waar niemand naar kijkt totdat een klant dat doet

De voor de hand liggende aanpak is elke content stream, elk Form XObject, elke annotatieappearance en elke default appearance van velden parsen, de codes verzamelen die bij elk font worden getoond, en die glyphs houden. Het werkt totdat hij een fontresource tegenkomt die over pagina's en forms wordt gedeeld, een formulierveld waarvan een viewer de appearance regenereert vanuit /DA, of een content stream met een operatorreeks die de scanner niet modelleert. SubsetPdfDocumentFonts scant bewust geen content streams. Hij leidt het glyph-domein af uit de metadata die de PDF toch al over elk font moet dragen: de /CIDToGIDMap, de breedtearrays /W en /W2, de bitmap /CIDSet, de CMap /ToUnicode, de array /Widths van het simple font en de /Encoding. Begrenst die metadata het domein niet, dan blijft het font onaangeroerd

Hoe PDFium Component het glyph-domein bewijst bij het subsetten van ingebedde TrueType-fonts vanuit Delphi: /CIDToGIDMap, /W- en /W2-breedtes, /CIDSet, /ToUnicode, /Widths en /Encoding begrenzen het domein zonder content streams te scannen, en een niet bewijsbaar font blijft onaangeroerd bewaard
SubsetPdfDocumentFonts maakt van fontmetadata een bewezen glyph-domein voordat er ook maar iets wordt gesneden, en een font waarvan de resources het domein niet kunnen begrenzen wordt gemeld en onaangeroerd gelaten

Originele glyph-ID's houden zodat content streams onaangeroerd blijven

De subset-backend achter SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, houdt elke originele glyph-ID-sleuf vast, zodat er nooit een content stream, CID of /CIDToGIDMap hoeft te worden herschreven. Hij bouwt de tabellen glyf en loca opnieuw op, geeft elk ongebruikt glyph een loca-bereik van nul lengte en laat maxp.numGlyphs onveranderd. Glyph 4711 is na het subsetten nog steeds glyph 4711; hij heeft alleen outlines als het document hem kan bereiken

Die keuze haalt het riskantste deel van het subsetten van een afgerond document weg. Glyphs hernummeren zou betekenen dat elke string in elke content stream die het font gebruikt wordt herschreven, plus de mappingstream, plus alles wat glyph-ID's cacheert. Vastgehouden ID's kosten wat grootte, want loca houdt één entry per origineel glyph, maar het gros van een TrueType-programma is outlinedata, en dat is wat eruit gaat. Samengestelde glyphs worden tot hun componenten gevolgd, dus een letter met accent houdt de basisletter en het accent waar hij van is gebouwd; cyclische of te diepe composite-verwijzingen laten het font falen in plaats van een gebroken afsluiting op te leveren

Vastgehouden glyph-ID's na het subsetten van ingebedde TrueType-fonts met PDFium Component in Delphi: glyph 4711 houdt zijn nummer en outlines, ongebruikte glyphs krijgen loca-bereiken van nul lengte terwijl maxp.numGlyphs onveranderd blijft, dus content streams, CIDs en /CIDToGIDMap worden nooit herschreven
Omdat glyph-ID's de subset overleven, hoeven content streams, CIDs en de /CIDToGIDMap nooit te worden herschreven - de groottewinst komt van weggegooide outlines, niet van hernummering

Het hernoemde font volgt ook ISO 32000-1 §9.6.4, dat vereist dat een subset-fontnaam begint met zes hoofdletters en een plusteken. De component leidt de zes letters af uit een SHA-256-digest van het nieuwe fontprogramma, haalt eerst een eventuele bestaande prefix ABCDEF+ eraf, lost botsingen tussen fonts in hetzelfde document op en schrijft de nieuwe naam naar zowel de descriptor /FontName als de fontdictionary /BaseFont, inbegrepen de Type0-ouder van een CIDFont

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;
  // De output is altijd een complete PDF: het herbouwde bestand wanneer Report.Applied
  // True is, anders een byte-voor-byte-kopie van de bron
  Target := TFileStream.Create(TargetName, fmCreate);
  try
    if Length(Output) > 0 then
      Target.WriteBuffer(Output[0], Length(Output));
  finally
    Target.Free;
  end;
end;

Hoe bewijst SubsetPdfDocumentFonts het glyph-domein van een CIDFont?

SubsetPdfDocumentFonts bewijst het glyph-domein van een font CIDFontType2 vanuit zijn CID-naar-glyph-mapping, en de mapping beslist hoeveel extra bewijs nodig is. Een expliciete stream /CIDToGIDMap (ISO 32000-1 §9.7.4.2) is al een lijst van elk glyph dat het font kan adresseren, dus elke GID in de stream vervoegt het domein. Een mappingstream groter dan 65536 entries, of een met een oneven aantal bytes, faalt het begrensde profiel en het font wordt bewaard

/CIDToGIDMap /Identity is het moeilijke geval, en het is ook het meest voorkomende in moderne bestanden. Identity zegt dat CID gelijk is aan GID, wat u niets vertelt over welke CIDs het document gebruikt. De component eist daarom primair bewijs: een expliciete breedtearray /W of /W2 (§9.7.4.3), waarvan de CID-entries glyphs markeren waarvoor de producent metrics declareerde, of een stream /CIDSet in de fontdescriptor (§9.8.3), een bitmap met één bit per CID, meest significante bit eerst. Een kale Identity-font met geen van beide wordt bewaard met als reden Identity CIDToGIDMap requires an explicit W array or CIDSet stream

De valkuil in het gebruik van /W als bewijs is /DW. Een producent mag elke CID weglaten waarvan de breedte gelijk is aan de defaultbreedte, en een goed geoptimaliseerde writer doet precies dat voor de meest voorkomende glyphbreedtes. Kwam het domein alleen uit /W, dan zouden die glyphs zwijgend uit de subset verdwijnen. Om die kloof te dichten loopt de component elke Type0-font af waarvan /DescendantFonts naar de CIDFont wijst, eist hij dat diens /Encoding /Identity-H of /Identity-V is zodat een 2-byte code de CID is, en voegt hij de broncodes van elke entry beginbfchar en beginbfrange uit diens CMap /ToUnicode (§9.10.3) in het domein samen. Elke andere encoding, of een stream /ToUnicode die niet binnen budget kan worden gedecodeerd, bewaart het font

Bewijsbeslissing voor een Identity CIDToGIDMap in het fontsubsetten van PDFium Component voor Delphi: een expliciete mappingstream somt al elk glyph op, Identity vereist een breedtearray /W of /W2 of een bitmap /CIDSet, en /ToUnicode-broncodes dichten de blinde vlek van de /DW-defaultbreedte
Een Identity CIDToGIDMap zegt op zichzelf niets over gebruik, dus eist de subsetter /W- of /CIDSet-bewijs en voegt /ToUnicode-codes samen om de glyphs te dekken die /W bij /DW weglaat

Simpele TrueType-fonts: van Widths naar glyph-ID's

Fonts van één byte /Subtype /TrueType worden bewezen via hun coderange en hun encoding, en daarna opgelost naar glyph-ID's via de eigen cmap van het fontprogramma. De component vereist 0 <= FirstChar <= LastChar <= 255 en een array /Widths waarvan de lengte exact LastChar - FirstChar + 1 is; een korte, lange of niet-numerieke array betekent dat het codedomein niet bewezen is, en het font blijft zoals hij is

Elke code in dat bereik, plus elke code genoemd in /Differences of gedekt door /ToUnicode, wordt daarna gemapped zoals ISO 32000-1 §9.6.6 beschrijft. Codes onder /WinAnsiEncoding, /MacRomanEncoding of /StandardEncoding gaan via de matchende tabel naar Unicode. Glyphnamen uit /Differences worden opgelost via de Adobe Glyph List, namen uniXXXX en uXXXX, de 258 standaard Macintosh-glyphnamen en de naamtabel post formaat 2.0 van het font zelf. De resulterende waarden worden opgezocht in cmap-subtabellen van formaat 0, 4, 6 en 12, inbegrepen Unicode (3,1), (3,10) en (0,*), Mac Roman (1,0), en Windows Symbol (3,0) op F000 plus de code

Heeft een font helemaal geen basisencoding, dan markeert de component de vereniging van elke plausibele interpretatie in plaats van er een te gokken. Dat houdt een paar glyphs vast die waarschijnlijk ongebruikt zijn, en dat is de goede richting om in te vergissen. Het tegenovergestelde geval is streng: een naam /Differences die naar geen bewezen glyph-ID mapt wijst het hele font af, want één onopgeloste naam betekent dat het domein incompleet is

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

Wat laat SubsetPdfDocumentFonts met rust?

SubsetPdfDocumentFonts laat elk font met rust dat hij niet kan bewijzen, en hij meldt elk in Report.Issues met het objectnummer en een reden in plaats van het document te laten falen. Report.Complete is alleen True wanneer geen enkel font is bewaard. De bewaarde gevallen zijn het kennen waard voordat u een verkleining belooft:

  • Programma's Type 1 /FontFile en CFF of OpenType /FontFile3, gemeld als Only TrueType FontFile2 programs are supported
  • Identity CIDFonts zonder /W, /W2 of /CIDSet, en CIDFonts met noch een mappingstream noch /Identity
  • Fontprogramma's gerefereerd van ergens anders dan hun fontdescriptors, en descriptors gedeeld met een consument die niet CIDFontType2 of simpel TrueType is
  • Fonts waarvan de subset SubsetOptions.MinimumSavingPercent mist, en elk font wanneer het herbouwde bestand MinimumDocumentSavingPercent mist; in dat geval is Report.Applied False en is de output de originele bytes

Twee invoeren falen ronduit in plaats van bewaard te worden. Ondertekende documenten en documenten met handtekeningvelden worden geweigerd, want het vervangen van fontobjecten zou het bytebereik van elke handtekening ongeldig maken, en versleutelde bestanden hebben eerst een ontsleutelde bron nodig. Een geslaagde run schrijft de vervangende objecten als een sparse update en bouwt het bestand daarna opnieuw op via dezelfde bereikbaarheidspas als beschreven in de gids voor het snoeien van weesobjecten en dedupliceren van resources, zodat de oude fontprogramma's niet als onbereikbare objecten overleven en het resultaat één schone revisie is

Nieuwe tekst subsetten bij opslaan met saSubsetNewFonts

Fonts die PDFium Component tijdens de huidige sessie inbedt zijn een apart geval, afgehandeld door de save-optie saSubsetNewFonts in plaats van door SubsetPdfDocumentFonts. saSubsetNewFonts mapt op de PDFium-vlag FPDF_SUBSET_NEW_FONTS, die de native HarfBuzz-subsetter draait over fonts die native aanroepen zoals TPdf.AddText vanuit het systeem hebben geladen, inbegrepen OpenType CFF-faces. Upstream PDFium labelt de vlag nog steeds experimenteel. Hij raakt fonts die al in het bestand zaten niet aan

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;

In de probe van de component zakte een document met Arial-tekst van 590 KB naar 22 KB onder saSubsetNewFonts, en tekstextractie uit het opgeslagen bestand matchte nog steeds exact. Een kleurfont COLR v0, Segoe UI Emoji op 7.0 MB, ging hetzelfde pad naar 10.5 KB, gerenderd als zijn basisoutlines omdat PDF geen notie van COLR-lagen heeft. Variabele fonts zijn de uitzondering, en het blok staat vóór de subsetter: GDI GetFontData gaf een vervangende stream van 18.3 MB terug voor het bestand Segoe UI Variable van 1.85 MB, en het aanmaken van tekstobjecten faalde voordat er ook maar iets werd gesubset. De walkthrough van CID Type 2-fontinbedding vanuit Delphi behandelt het inbeddingspad die fonts eerst doorlopen

Waar het bewijs stopt

Het bewijs binnen SubsetPdfDocumentFonts is precies zo sterk als de fontmetadata die de producent schreef. Een Identity CIDFont waarvan het bewijs alleen /W is heeft nog één blinde vlek: een glyph waarvan de breedte gelijk is aan /DW, die geen entry /ToUnicode heeft, en die de descriptor niet in een /CIDSet opsomt. Zo'n glyph is niet bewijsbaar vanuit de resources, en de component kan niet weten dat hij bestaat zonder de content stream-scan die hij opzettelijk vermijdt. Producenten die zowel /ToUnicode als /CIDSet weglaten terwijl ze /W comprimeren zijn zeldzaam, maar ze bestaan, dus render voor een archief van een onbekende generator een staal voor en na en vergelijk pixels en geëxtraheerde tekst

Vóór het draaien van een batch helpt het ook te weten waar u mee te maken heeft. De aanpak in PDF-fonteigenschappen lezen met PDFium Component vertelt u welke fonts er überhaupt zijn ingebed, en Report.Issues uit een droge run vertelt u welke daarvan zullen worden bewaard en waarom. De defaultopties begrenzen ook het werk: 1024 fonts, ongeveer een miljoen glyphs in totaal, 128 MB aan bron en een vast werkbudget, met een optionele CancellationToken voor lange batchtaken

FAQ

Verandert SubsetPdfDocumentFonts de tekst of lay-out van de PDF?

Nee. Originele glyph-ID's, CIDs, breedtes en content streams worden vastgehouden, dus tekstextractie en rendering gebruiken dezelfde glyphs als voorheen; alleen onbereikbare outlines worden verwijderd en de fontnamen krijgen een nieuw subset-prefix

Waarom werd een ingebed font niet gesubset?

Lees Report.Issues. Elk bewaard font draagt een reden zoals een ontbrekende /W of /CIDSet op een Identity CIDFont, een niet-TrueType-programma, een misvormde array /Widths of een besparing onder de drempel

Is saSubsetNewFonts hetzelfde als SubsetPdfDocumentFonts?

Nee. saSubsetNewFonts maakt subsets van fonts die in de huidige sessie zijn ingebed wanneer u opslaat, terwijl SubsetPdfDocumentFonts op een bestaande PDF-stream werkt en fonts vervangt die al door een andere producent waren ingebed

Fontsubsetten van documenten, fontsubsetten bij opslaan en de weesobjecten-snoeipas erachter worden allemaal geleverd met de PDFium Component for Delphi and C++Builder, die PDFium omwikkelt voor Delphi-, C++Builder- en Lazarus-projecten