Teknisk artikel

Skapa subset av typsnitt redan inbäddade i en PDF i Delphi

PDFium Component subsättar typsnitt som redan är inbäddade i en befintlig PDF via SubsetPdfDocumentFonts i uniten FPdfDocumentFontSubset. Funktionen ersätter ett inbäddat TrueType /FontFile2-program med ett mindre bara när typsnittsresurserna själva bevisar varje glyf dokumentet kan referera; ett typsnitt vars glyfdomän inte kan bevisas lämnas exakt som det var

Det vanliga skälet att vilja det här är ett arkiv du inte producerat själv. Fakturor, utdrag och rapporter från äldre generatorer bär ofta en komplett Arial eller en hel CJK-stil i varje fil, och några hundra kilobyte typsnitt per tvåsidigt dokument hopar sig snabbt över några miljoner dokument. Att rendera om från källan är sällan ett alternativ. Det du har är PDF:en, och den enda säkra frågan är hur mycket av varje inbäddat typsnitt som kan gå utan att en enda sida tappar ett tecken

Varför riskerar subsetting av en befintlig PDF att tappa glyfer?

Subsetting av en befintlig PDF riskerar att tappa glyfer för att efterbehandlaren inte vet vilka glyfer originalskrivaren använde. Producenten som bäddade in typsnittet kände sin egen text; ett verktyg som körs efteråt måste rekonstruera den kunskapen, och varje glyf den missar blir en tom ruta eller en .notdef-form på en sida ingen tittar på förrän en kund gör det

Det uppenbara angreppssättet är att parsa varje innehållsström, varje Form XObject, varje annoteringsutseende och varje fälts standardutseende, samla in koderna som visas med varje typsnitt, och behålla de glyferna. Det fungerar tills det möter en typsnittsresurs delad över sidor och former, ett formulärfält vars utseende en visare regenererar från /DA, eller en innehållsström med en operatorsekvens skannaren inte modellerar. SubsetPdfDocumentFonts skannar med flit inte innehållsströmmar. Den härleder glyfdomänen ur metadatan PDF:en ändå måste bära om varje typsnitt: /CIDToGIDMap, /W- och /W2-breddarrayerna, /CIDSet-bitmappen, /ToUnicode-CMap:en, enkeltalet /Widths och /Encoding. När den metadatan inte avgränsar domänen lämnas typsnittet orört

Hur PDFium Component bevisar glyfdomänen vid subsetting av inbäddade TrueType-typsnitt från Delphi: /CIDToGIDMap, /W- och /W2-bredder, /CIDSet, /ToUnicode, /Widths och /Encoding avgränsar domänen utan att skanna innehållsströmmar, och ett obevisbart typsnitt bevaras orört
SubsetPdfDocumentFonts gör om typsnittsmetadata till en bevisad glyfdomän innan något skärs, och ett typsnitt vars resurser inte kan avgränsa domänen rapporteras och lämnas orört

Att behålla ursprungliga glyf-ID:n så att innehållsströmmar lämnas orörda

Subset-backend bakom SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, behåller varje ursprunglig glyf-ID-plats, så att ingen innehållsström, inget CID och ingen /CIDToGIDMap någonsin behöver skrivas om. Den bygger om tabellerna glyf och loca, ger varje oanvänd glyf ett nollängds-loca-intervall och lämnar maxp.numGlyphs oförändrat. Glyf 4711 är fortfarande glyf 4711 efter subsettingen; den har bara konturer om dokumentet kan nå den

Det valet tar bort den riskfylldaste delen av att subsätta ett färdigt dokument. Att omnumrera glyfer skulle betyda att skriva om varje sträng i varje innehållsström som använder typsnittet, plus mappningsströmmen, plus allt som cachar glyf-ID:n. Behållna ID:n kostar lite storlek, eftersom loca behåller en post per originalglyf, men huvuddelen av ett TrueType-program är konturdata, och det är det som går. Sammansatta glyfer följs till sina komponenter, så en accenterad bokstav behåller basbokstaven och accenten den bygger på; cykliska eller överdjupa sammansatta referenser fäller typsnittet i stället för att producera en trasig slutenhet

Behållna glyf-ID:n efter subsetting av inbäddade TrueType-typsnitt med PDFium Component i Delphi: glyf 4711 behåller sitt nummer och sina konturer, oanvända glyfer får nollängds-loca-intervall medan maxp.numGlyphs stannar oförändrat, så innehållsströmmar, CID:er och /CIDToGIDMap skrivs aldrig om
Eftersom glyf-ID:na överlever subsetten behöver innehållsströmmar, CID:er och /CIDToGIDMap aldrig skrivas om - storleksvinsten kommer från borttagna konturer, inte från omnumrering

Det omdöpta typsnittet följer också ISO 32000-1 §9.6.4, som kräver att ett subset-typsnittsnamn börjar med sex versaler och ett plustecken. Komponenten härleder de sex bokstäverna från en SHA-256-digest av det nya typsnittsprogrammet, stryker eventuell befintlig ABCDEF+-prefix först, löser kollisioner mellan typsnitt i samma dokument, och skriver det nya namnet till både beskrivarens /FontName och typsnittsordbokens /BaseFont, inklusive en CIDFonts Type0-förä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;
  // Utdata är alltid en komplett PDF: den ombyggda filen när Report.Applied
  // är True, annars en kopia av källan byte för byte
  Target := TFileStream.Create(TargetName, fmCreate);
  try
    if Length(Output) > 0 then
      Target.WriteBuffer(Output[0], Length(Output));
  finally
    Target.Free;
  end;
end;

Hur bevisar SubsetPdfDocumentFonts glyfdomänen hos en CIDFont?

SubsetPdfDocumentFonts bevisar glyfdomänen hos ett CIDFontType2-typsnitt utifrån dess CID-till-glyf-mappning, och mappningen avgör hur mycket extra bevis som behövs. En explicit /CIDToGIDMap-ström (ISO 32000-1 §9.7.4.2) är redan en lista över varje glyf typsnittet kan adressera, så varje GID i strömmen ansluter till domänen. En mappningsström större än 65536 poster, eller en med udda byteantal, fäller den avgränsade profilen och typsnittet bevaras

/CIDToGIDMap /Identity är det svåra fallet, och det är också det vanligaste i moderna filer. Identity säger att CID är lika med GID, vilket inte säger något om vilka CID:er dokumentet använder. Komponenten kräver därför primärbevis: en explicit /W- eller /W2-breddarray (§9.7.4.3), vars CID-poster markerar glyfer producenten deklarerat mått för, eller en /CIDSet-ström i typsnittsbeskrivaren (§9.8.3), en bitmap med en bit per CID, mest signifikant bit först. Ett bart Identity-typsnitt utan något av dem bevaras med skälet Identity CIDToGIDMap requires an explicit W array or CIDSet stream

Fällan i att använda /W som bevis är /DW. En producent får lämna ut varje CID vars bredd är lika med standardbredden, och en väl optimerad skrivare gör exakt det för de vanligaste glyfbredderna. Kom domänen från /W ensamt skulle de glyferna tyst försvinna ur subsetten. För att stänga det hållet vandrar komponenten igenom varje Type0-typsnitt vars /DescendantFonts pekar på CIDFont:en, kräver att dess /Encoding är /Identity-H eller /Identity-V så att en 2-bytes-kod är CID:en, och fogar källkoderna hos varje beginbfchar- och beginbfrange-post från dess /ToUnicode-CMap (§9.10.3) in i domänen. Någon annan kodning, eller en /ToUnicode-ström som inte kan avkodas inom budgeten, bevarar typsnittet

Beslut om bevis för en Identity CIDToGIDMap i PDFium Components typsnittssubsetting för Delphi: en explicit mappningsström listar redan varje glyf, Identity kräver en /W- eller /W2-breddarray eller en /CIDSet-bitmap, och /ToUnicode-källkoder stänger /DW-standardbreddens blinda fläck
En Identity CIDToGIDMap säger ingenting om användning på egen hand, så subsettern kräver /W- eller /CIDSet-bevis och fogar /ToUnicode-koder för att täcka glyferna som /W utelämnar vid /DW

Enkla TrueType-typsnitt: från Widths till glyf-ID:n

En-byte /Subtype /TrueType-typsnitt bevisas genom sitt kodintervall och sin kodning, och löses sedan till glyf-ID:n genom typsnittsprogramets egen cmap. Komponenten kräver 0 <= FirstChar <= LastChar <= 255 och en /Widths-array vars längd är exakt LastChar - FirstChar + 1; en kort, lång eller icke-numerisk array betyder att koddomänen inte är bevisad, och typsnittet blir kvar som det är

Varje kod i det intervallet, plus varje kod namngiven i /Differences eller täckt av /ToUnicode, mappas sedan på sätt ISO 32000-1 §9.6.6 beskriver. Koder under /WinAnsiEncoding, /MacRomanEncoding eller /StandardEncoding går genom matchande tabell till Unicode. /Differences-glyfnamn löses genom Adobe Glyph List, uniXXXX- och uXXXX-namn, de 258 standardiserade Macintosh-glyfnamnen och typsnittets egen post format 2.0-namntabell. De resulterande värdena slås upp i format 0, 4, 6 och 12 cmap-undertabeller, inklusive Unicode (3,1), (3,10) och (0,*), Mac Roman (1,0), och Windows Symbol (3,0) vid F000 plus koden

När ett typsnitt saknar all bas-kodning markerar komponenten unionen av varje rimlig tolkning i stället för att gissa en. Det behåller några glyfer som sannolikt är oanvända, vilket är rätt håll att missta sig i. Motsatt fall är strikt: ett /Differences-namn som mappar till inget bevisat glyf-ID avvisar hela typsnittet, eftersom ett olöst namn betyder att domänen är ofullständig

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

Vad lämnar SubsetPdfDocumentFonts orört?

SubsetPdfDocumentFonts lämnar varje typsnitt den inte kan bevisa orört, och rapporterar vart och ett i Report.Issues med objektnumret och ett skäl i stället för att fälla dokumentet. Report.Complete är True bara när inget typsnitt bevarades. De bevarade fallen är värda att känna innan du lovar en storleksminskning:

  • Type 1 /FontFile samt CFF- eller OpenType /FontFile3-program, rapporterade som Only TrueType FontFile2 programs are supported
  • Identity-CIDFonts utan /W, /W2 eller /CIDSet, och CIDFonts utan varken mappningsström eller /Identity
  • Typsnittsprogram refererade från något annat än sina typsnittsbeskrivare, och beskrivare delade med en konsument som inte är CIDFontType2 eller enkel TrueType
  • Typernitt vars subset missar SubsetOptions.MinimumSavingPercent, och varenda typsnitt när den ombyggda filen missar MinimumDocumentSavingPercent; i så fall är Report.Applied False och utdata är originalbytena

Två indatan fäller rakt av i stället för att bevaras. Signerade dokument och dokument med signaturfält avvisas, eftersom att ersätta typsnittsobjekt skulle ogiltigförklara byteintervallet hos varje signatur, och krypterade filer behöver en dekrypterad källa först. En lyckad körning skriver ersättningsobjekten som en gles uppdatering och bygger sedan om filen genom samma räckviddskontroll som beskrivs i guiden till att beskära föräldralösa objekt och deduplicera resurser, så att de gamla typsnittsprogrammen inte överlever som onåbara objekt och resultatet är en enda ren revision

Att subsätta ny text vid sparande med saSubsetNewFonts

Typernitt som PDFium Component bäddar in under den aktuella sessionen är ett separat fall, hanterat av sparalternativet saSubsetNewFonts snarare än av SubsetPdfDocumentFonts. saSubsetNewFonts mappar till PDFium-flaggan FPDF_SUBSET_NEW_FONTS, som kör den nativa HarfBuzz-subsettern över typsnitt som nativa anrop som TPdf.AddText laddat från systemet, inklusive OpenType CFF-stilar. Uppströms PDFium märker fortfarande flaggan experimentell. Den rör inte typsnitt som redan fanns 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 prob sjönk ett dokument med Arial-text från 590 KB till 22 KB under saSubsetNewFonts, och textextraktionen ur den sparade filen matchade fortfarande exakt. En COLR v0-färgstil, Segoe UI Emoji på 7,0 MB, gick genom samma väg till 10,5 KB, renderad som sina bas-konturer eftersom PDF saknar begreppet COLR-lager. Variabla typsnitt är undantaget, och blocket sitter framför subsettern: GDI GetFontData returnerade en ersättningsström på 18,3 MB för den 1,85 MB stora Segoe UI Variable-filen, och skapandet av textobjekt misslyckades innan någon subsetting börjat. Genomgången av CID Type 2-inbäddning av typsnitt från Delphi täcker inbäddningsvägen de typsnitten går genom först

Där beviset stannar

Beviset inne i SubsetPdfDocumentFonts är exakt lika starkt som den typsnittsmetadata producenten skrev. En Identity-CIDFont vars bevis är /W ensamt har fortfarande en blind fläck: en glyf vars bredd är lika med /DW, som saknar /ToUnicode-post, och som beskrivaren inte listar i någon /CIDSet. En sådan glyf är inte bevisbar från resurserna, och komponenten kan inte veta att den existerar utan den innehållsströmsskanning den med flit undviker. Producenter som utelämnar både /ToUnicode och /CIDSet medan de komprimerar /W är sällsynta, men de finns, så för ett arkiv från en okänd generator, rendera ett urval före och efter och jämför pixlar och extraherad text

Innan du kör en batch hjälper det också att veta vad du har att göra med. Angreppssättet i att läsa PDF-typsnittsegenskaper med PDFium Component talar om vilka typsnitt som alls är inbäddade, och Report.Issues från en torrkörning talar om vilka av dem som bevaras och varför. Standardalternativen avgränsar också arbetet: 1024 typsnitt, omkring en miljon glyfer totalt, 128 MB källa och en fast arbetsbudget, med en valfri CancellationToken för långa batchjobb

FAQ

Ändrar SubsetPdfDocumentFonts texten eller layouten i PDF:en?

Nej. Ursprungliga glyf-ID:n, CID:er, bredder och innehållsströmmar behålls, så textextraktion och rendering använder samma glyfer som tidigare; bara onåbara konturer tas bort och typsnittsnamnen får ett nytt subset-prefix

Varför blev ett inbäddat typsnitt inte subsatt?

Läs Report.Issues. Varje bevarat typsnitt bär ett skäl som en saknad /W eller /CIDSet hos en Identity-CIDFont, ett icke-TrueType-program, en felformad /Widths-array eller en besparing under tröskeln

Är saSubsetNewFonts detsamma som SubsetPdfDocumentFonts?

Nej. saSubsetNewFonts subsättar typsnitt inbäddade under aktuell session när du sparar, medan SubsetPdfDocumentFonts arbetar på en befintlig PDF-ström och ersätter typsnitt som redan bäddats in av en annan producent

Dokumenttypsnittssubsetting, subsetting av typsnitt vid sparande och beskärningspasset för föräldralösa objekt bakom dem levereras alla med PDFium Component for Delphi and C++Builder, som wrappar PDFium för Delphi-, C++Builder- och Lazarus-projekt