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
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
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
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
/FontFilesamt CFF- eller OpenType/FontFile3-program, rapporterade somOnly TrueType FontFile2 programs are supported - Identity-CIDFonts utan
/W,/W2eller/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
CIDFontType2eller enkelTrueType - Typernitt vars subset missar
SubsetOptions.MinimumSavingPercent, och varenda typsnitt när den ombyggda filen missarMinimumDocumentSavingPercent; i så fall ärReport.AppliedFalseoch 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