Technisch artikel

HotPDF CompressDocument: compacte font subsets in Delphi

HotPDF THotPDF.CompressDocument is één schakelaar die BeginDoc de kleinst mogelijke verliesvrije PDF laat produceren die het component kan schrijven: FlateDecode op het maximale niveau, een cross-reference stream met object streams, font subsetting en compacte font subsets die de behouden glyphs hernummeren achter een expliciete /CIDToGIDMap. EndDoc zet daarna uw eigen instellingen terug. Een testdocument van drie pagina's met Arial en SimSun zakte van 10,2 MB naar 20 KB met identieke rendering

Wat schakelt CompressDocument eigenlijk aan?

CompressDocument overrulet zes writer-instellingen, plus de object-stream-cap, voor één document en zet ze daarna allemaal terug. Bij BeginDoc, voordat de PDF-versie vastligt, neemt HotPDF uw waarden op en zet Compression op cmFlateDecode, CompressionLevel op clMaximum, zet EnableFontSubsetting en CompactFontSubsetting aan, en activeert UseXRefStream plus UseObjectStreams (ISO 32000-1 §7.5.7 en §7.5.8). Object streams hebben PDF 1.5 nodig, dus een oudere Version wordt naar 1.5 verhoogd als hij niet vastgezet is. PDF/A-1 verbiedt beide structuren, dus een PDF/A-1-document houdt zijn klassieke cross-reference tabel en krijgt alleen de Flate- en fontwerkzaamheden. Afbeeldingen blijven precies zoals u ze insloot

Diagram van de HotPDF CompressDocument-levenscyclus in Delphi: BeginDoc neemt de eigen waarden van de writer op, overrulet zes instellingen waaronder Compression en UseObjectStreams voor één document, en EndDoc zet elke geleende waarde terug in zijn buitenste finally terwijl de property CompressDocument zelf True blijft
Zes writer-instellingen en de object-stream-cap worden geleend voor precies één document en teruggegeven zodra EndDoc draait, zodat een mislukte rapport het component nooit achterlaat vastgezet op maximale compressie
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // toegepast door BeginDoc, ongedaan gemaakt door EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

De terugzet gebeurt in de buitenste finally van EndDoc, dus een exceptie halverwege een rapport laat een langlevend component niet vastzitten op maximale compressie voor de volgende job. De property CompressDocument zelf blijft True; alleen de zes instellingen die hij leende gaan terug. Met de versie wordt voorzichtiger omgegaan. HotPDF draait zijn eigen verhoging naar 1.5 alleen terug als het document nog op 1.5 eindigt, dus als een andere functie het bestand tijdens de run naar 1.6 duwde (een ingesloten OpenType-font bijvoorbeeld), blijft de hogere versie staan, precies zoals zonder compressie

Waarom blijven font subsets groot zonder compactie?

Een klassieke TrueType-subset laat de outlines vallen die u nooit tekent maar houdt elke glyph ID op zijn plek, en juist die nummering houdt hem zwaar. De content stream toont CIDs die gelijk zijn aan de oorspronkelijke GIDs, dus de subset moet een loca-offset en een hmtx-entry vasthouden voor elke plek tot aan de hoogste glyph die hij behoudt, leeg of niet. Voor een Latijns lettertype is die overhead ruis. Voor een CJK-lettertype als SimSun, waarvan de ideografen diep in een zeer grote glyftabel zitten, slepen twee Chinese karakters tabellen mee die voor het hele font gemeten zijn. De font subset closure-regels voor gevormde glyphs bepalen welke glyphs overleven; compactie gaat over hoeveel de overlevenden kosten

CompactFontSubsetting hernummert de behouden glyphs in een dichte reeks beginnend bij nul en schrijft een /CIDToGIDMap-stream op de CIDFont, die ISO 32000-1 §9.7.4.2 definieert als een tabel van two-byte GIDs geïndexeerd op CID. Die tabel is de hele truc. Content streams, de /W-breedte-array en de ToUnicode CMap houden allemaal de oorspronkelijke CIDs aan, dus niets wat al geschreven is hoeft te veranderen; alleen de opslag van CID naar glyph verhuist naar de map. In de test die de functie motiveerde, ging SimSun met twee karakters van 24,8 KB fontgegevens naar 3,1 KB

Vergelijking van een sparse HotPDF font subset, die loca- en hmtx-entries vasthoudt voor elke oorspronkelijke glyph ID tot aan de hoogste behouden GID, met de uitvoer van CompactFontSubsetting, die behouden glyphs dicht hernummert vanaf nul en CIDs mapt via een CIDToGIDMap-stream terwijl content streams, /W en ToUnicode onveranderd blijven
Hernummeren verplaatst de kosten uit het fontprogramma naar één kleine map-stream — twee SimSun-karakters zakten van 24,8 KB naar 3,1 KB zonder ook maar een byte van al geschreven content aan te raken

Compactie heeft harde grenzen, en hij degradeert stilletjes in plaats van te falen. HotPDF bouwt compacte subsets alleen voor Type 0 TrueType-faces, zowel die gezet via SetFont met subsetting aan als het via RegisterUnicodeTTF geregistreerde face. Een simpel TrueType-font vindt zijn glyphs via de cmap binnen het fontprogramma, en hernummeren zou die breken, dus hij houdt de sparse subset. OpenType-CFF-faces hebben ook geen compact pad. Een compacte build die faalt, valt terug op de sparse subset in plaats van te gooien. De property staat standaard uit, dus bestaande uitvoer blijft byte-identiek, terwijl onder PDF/A het geregistreerde Unicode-face altijd een compacte subset krijgt

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // bruikbaar zonder CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Hoe perst de packed writer de bestandsstructuur uit?

Als fonts en streams eenmaal klein zijn, worden de dictionaries en de cross-reference data de grootste resterende kostenpost, dus de object-stream writer achter CompressDocument snoeit ook die. De gids over object streams en incremental updates behandelt het containerformaat zelf; het compressiepad legt daar bovenop vier verfijningen:

  • Compacte syntaxis volgens ISO 32000-1 §7.2.2: een spatie wordt alleen geschreven tussen twee tokens die anders als gewone tekens aan elkaar plakken, dus /Type /Page wordt /Type/Page
  • Cross-reference stream-velden krijgen elke breedte die §7.5.8.2 toelaat, dus een bestand onder 16 MB bewaart elke offset in 3 bytes in plaats van 4
  • Tot 250 objecten gaan in elke object stream in plaats van de gebruikelijke 100, tenzij u via ConfigureAdaptiveObjectStreamPacking uw eigen cap zet
  • Als het bestand niet versleuteld is, worden de Catalog en de Info-dictionary ook in object streams gepakt; versleutelde uitvoer houdt ze op het topniveau

De compacte syntaxis kwam met een valkuil die de moeite van kennen waard is als u de writer uitbreidt. Signing vult de handtekening in nadat het bestand geschreven is door de bytes te doorzoeken op de letterlijke placeholders /ByteRange ( en /Contents <, en compacte spelling zou die veranderen in /ByteRange( en /Contents<, wat de zoektocht nooit vindt. Signature-dictionaries (Type Sig of DocTimeStamp, FT Sig) en de encryption-dictionary houden daarom de gespatieerde lay-out. Een gerelateerd defect trof builds vóór v2.766.41: elke object-stream-save, CompressDocument inbegrepen, begon met twee %PDF--headerregels, dus upgrade als een strenge validator uw uitvoer vlagt

Kunt u een PDF comprimeren die al geladen is?

Ja, via de options-overload CompressLoadedDocument(Options, Info), die dezelfde verliesvrije stappen op een bestaand bestand zet. Met THPDFLoadedDocumentCompressionOptions.Default verwijdert hij ongebruikte paginabronnen, voegt hij identieke fonts en formulieren samen, subset hij ingesloten fonts met compacte subsets aan, hercomprimeert hij ongefilterde, Flate-, LZW-, ASCII- en RunLength-streams met Flate als het resultaat kleiner is, en laat hij de volgende save object streams gebruiken. HighRatioFlate staat standaard uit, en object streams worden overgeslagen voor PDF/A-1 en incremental saves. De overload van CompressLoadedDocument zonder parameters is de oudere, nauwere aanroep die alleen ongecomprimeerde streams met Flate comprimeert

Stroom van HotPDF CompressLoadedDocument in Delphi: de aanroep verwijdert ongebruikte paginabronnen, voegt identieke fonts en formulieren samen, subset ingesloten fonts met compacte subsets, hercomprimeert streams met Flate alleen als het resultaat kleiner is, en zet object streams aan voor de volgende save, terwijl handtekeningvelden RefusedBySignaturePolicy activeren en het bestand onaangeroerd laten
Elke stap herschrijft bytes die een handtekening dekt, dus het hele document wordt geweigerd tenzij u invalidatie expliciet toestaat — Info.BytesSaved somt daarna alleen de resource-, font- en streamwerk op
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Op het geladen pad doen twee grenzen ertoe. Elke stap herschrijft bytes die een handtekening dekt, dus een document met handtekeningvelden wordt als geheel geweigerd: de aanroep geeft 0 terug, zet RefusedBySignaturePolicy en verandert niets, tenzij u AllowSignatureInvalidation zet, waarna Info.SignaturesInvalidated u vertelt wat u hebt opgegeven. Compactie is hier ook conservatiever dan op het aanmaakpad. HotPDF compact alleen fontprogramma's die uitsluitend door CIDFontType2-fonts met een Identity-/CIDToGIDMap gebruikt worden, waar CID gelijk is aan GID, en slaat programma's over met een bestaande map-stream, een /CIDSet of kleurglyftabellen als COLR, sbix, CBDT of SVG, want de compacte herbouw zou de kleurlagen laten vallen. Merk ook op dat Info.BytesSaved alleen de resource-, font- en streamstappen somt; de object-stream-winst komt tevoorschijn zodra het bestand geschreven wordt

Welke resultaten mag u in de praktijk verwachten?

De winst volgt hoeveel van een bestand ongecomprimeerde structuur en oversized fontgegevens is, niet hoeveel pagina's het heeft. Het voorbeeld van drie pagina's met Arial en SimSun kromp van 10,2 MB naar 20 KB bij generatie met CompressDocument, en van 10,2 MB naar 19,8 KB toen het ongecomprimeerde origineel geladen en door CompressLoadedDocument gehaald werd, met identieke rendering beide keren. Een PDF die al compact is beweegt nauwelijks: in de regressieset bespaarden zulke bestanden binnen -0,07% tot +0,06% van hun oorspronkelijke grootte. Fotzware bestanden winnen weinig, want geen van beide paden raakt beeldgegevens aan

Als u elke nacht dezelfde CJK-rapporten genereert, combineer dan compacte subsets met de persistente font subset cache op schijf zodat het subsetwerk niet per run herhaald wordt, en diff gecomprimeerde uitvoer op objectcontent in plaats van op bytes, want één veranderd veld re-Flate een hele object stream. De volledige property- en recordreferenties staan op de productpagina van de HotPDF Delphi PDF component