Ga naar inhoud
getnextpdf.com

De economie van PDF-bestandsgrootte

Spec: ISO 32000-2, §7.5.7

Twee PDF’s kunnen op het scherm pixel voor pixel identiek lijken en op schijf tienvoudig verschillen. Het verschil is bijna nooit de inhoud die je ziet; het is hoe het bestand eronder is samengesteld. Deze pagina is een grootte-economische rondleiding: waar de bytes van een PDF werkelijk heen gaan, en de vier hendels waaraan een auteur een bytebudget besteedt.

Het is de waarom bestanden groot zijn-metgezel van Streams en filters, die behandelt hoe een filter decodeert. Deze blijft bij het budget.

Bestandsgrootte is zelden een ijdelheidsmaat. Het is bandbreedte bij elke download, opslag bij elk archief, en latency bij elke voorvertoning. Een factuur van 12 MB die 400 KB had moeten zijn, is geen cosmetisch probleem wanneer je er een miljoen per maand genereert — het is een dertigvoudige rekening.

Het frustrerende is dat opgeblazenheid meestal onzichtbaar is. Het document rendert correct, opent prima, print prima. Niets vertelt je dat hetzelfde artefact een fractie van de grootte had kunnen zijn, omdat de verspilde bytes structureel zijn, niet visueel. Om ze te vinden moet je naar het budget kijken, niet naar de pagina.

Zie een PDF als een budget dat je aan vier posten besteedt.

  • Overhead per object. Elk indirect object draagt een N G obj / endobj-envelop, en wordt bijgehouden door één cross-reference-vermelding. Een pagina-zwaar document heeft duizenden kleine objecten, en de enveloppen tellen op. Object streams laten die envelop voor een hele groep objecten vallen; elk ingepakt object houdt nog steeds zijn eigen cross-reference-vermelding, maar als een compacte type-2-vermelding.
  • Beeldbytes. Voor elk document met foto’s of scans domineren de afbeeldingen, en de allergrootste hendel is de filterkeuze — een lossless codec versus een lossy beeldcodec is het verschil tussen megabytes en kilobytes.
  • Fontbytes. Een volledig ingebed lettertype is honderden kilobytes aan glyphs die je nooit gebruikt. Subsetting houdt alleen de glyphs die het document daadwerkelijk tekent.
  • De index. De cross-reference-tabel waarmee een reader elk object kan vinden, kan zelf een gecomprimeerde stream zijn in plaats van platte tekst.

Krijg alle vier goed en het bestand is klein. Mis er een en het domineert al het andere dat je goed deed.

NextPDF’s writer is een single-pass streaming serializer: hij voegt de bytes van elk object toe zodra ze worden geproduceerd en legt voor elk een klassieke in-use cross-reference-vermelding vast. Die standaard is snel, voorspelbaar en produceert een byte-stabiel bestand — maar het is niet de kleinst mogelijke layout, en NextPDF is daar eerlijk over.

Een PDF is een graaf van indirecte objecten. De meeste ervan zijn kleine dictionaries: pagina-nodes, annotatie-dictionaries, structure-tree-elementen, outline-vermeldingen. Elk daarvan betaalt een vaste belasting — de obj / endobj-sleutelwoorden, de object- en generatienummers, en een cross-reference-vermelding die het lokaliseert. Op een document met duizenden kleine objecten is die belasting een betekenisvol deel van het bestand.

Een object stream verzamelt veel van die kleine niet-stream-objecten in één stream en comprimeert ze samen (Spec: ISO 32000-2, §7.5.7). De obj / endobj-envelop wordt voor de hele groep gedropt; de waarden binnenin worden achter elkaar opgeslagen zonder sleutelwoorden per object, en vervolgens als één blok gedeflateerd — wat ook beter comprimeert, omdat de dedupliceerende compressor nu al die gelijksoortige dictionaries in één keer ziet. De cross-reference-vermelding verdwijnt niet — elk ingepakt object heeft er nog steeds een nodig — maar krimpt tot een compacte binaire type-2-vermelding in de cross-reference stream (meer daarover in Hendel 2).

In NextPDF wordt dit geleverd door ObjectStreamPacker, een op zichzelf staande post-processor die een voltooide cross-reference-stream-PDF neemt en de in aanmerking komende objecten herschrijft tot één /Type /ObjStm. De regels die hij volgt komen rechtstreeks uit de standaard: een in aanmerking komend object is een niet-stream-object van generatie nul, na uitsluiting van de speciale objecten die direct adresseerbaar moeten blijven. §7.5.7 verbiedt het opslaan van een stream-object binnen een object stream, dus stream-objecten — inhoud, lettertypen, afbeeldingen — houden hun eigen vermeldingen; en ObjectStreamPacker weigert daarnaast het eigen cross-reference-stream-object van het document (dat wordt herschreven) en de /Encrypt-dictionary, die beide direct adresseerbaar moeten blijven. Al het andere van generatie nul en niet-stream wordt ingepakt.

Object stream packing (ObjStm) — edition availability
EditionAvailability
CoreFull support in the open-source core via ObjectStreamPacker. It is opt-in: the single-pass writer’s default emits classic in-use entries, so output stays byte-identical unless you enable packing. The packer is deterministic, with its own reproducible golden baseline.
ProNot in this edition
EnterpriseNot in this edition

Opt-in is een bewuste houding, geen beperking die zich verstopt. De standaarduitvoer is byte-stabiel en komt overeen met de bestaande golden baselines; packing inschakelen kiest voor een andere, kleinere, even deterministische layout. Jij kiest de afweging, en de engine maakt die nooit achter je rug om.

Zodra objecten binnen een object stream leven, verandert de index die naar hen wijst van vorm. Een reader vindt een ingepakt object via een gecomprimeerde cross-reference-vermelding — een type-2-vermelding die de object stream en de index daarbinnen benoemt (Spec: ISO 32000-2, §7.5.8.3). Omdat de hele cross-reference zelf een /Type /XRef-stream is, wordt de index voor duizenden objecten binair ingepakt en gedeflateerd in plaats van als platte-tekst-rijen te worden geschreven. De kaart krimpt mee met het gebied.

ObjectStreamPacker herbouwt precies dit: het stuurt de enkele object stream uit, daarna een herschreven cross-reference stream die een compacte type-1-vermelding draagt voor elk behouden object en een type-2-vermelding voor elk ingepakt, waarbij elk objectnummer behouden blijft zodat bestaande referenties geldig blijven.

Voor elk document met echte afbeeldingen overschaduwt deze hendel de andere. De bytes zijn hetzelfde beeld; de codec is het budget. De filternamen leven in de standaardfilterset (Spec: ISO 32000-2, §7.4), en de keuze ertussen is een groottebeslissing:

  • FlateDecode is lossless. Perfect voor lijntekeningen, screenshots en alles met vlakke kleur — en rampzalig voor een foto, waar lossless elke byte van het origineel betekent.
  • DCTDecode is JPEG: lossy, en voor foto’s veruit de juiste keuze, vaak een tienvoudige reductie voor een kwaliteitsverlies dat niemand opmerkt.
  • JPXDecode is JPEG 2000: wavelet-compressie met een andere kwaliteit/grootte-curve, maar ongelijke ondersteuning en niet toegestaan door sommige archiveringsprofielen.

Dit is precies waar hoe een filter decodeert ertoe doet, en dat is de taak van het artikel Streams en filters. Het economische punt is smaller: een foto die lossless is opgeslagen, is de meest voorkomende enkele oorzaak van een onnodig enorme PDF, en geen hoeveelheid object-stream-packing zal een bestand redden waarvan het echte gewicht één niet-ge-JPEG’de scan is.

Een ingebed lettertype is een programma. Een volledig exemplaar kan enkele honderden kilobytes zijn, omdat het elke glyph draagt die de typeface-ontwerper ooit heeft getekend — duizenden tekens over scripts die je in dit document nooit zult gebruiken. Subsetting bedt alleen de glyphs in die het document daadwerkelijk tekent, en verandert dat programma in een klein deel van zichzelf. Een brief van één pagina heeft niet het geheel van een CJK-capabel lettertype nodig; het heeft de enkele tientallen glyphs nodig die het zet. De mechaniek van hoe glyphs worden geselecteerd en opnieuw geïndexeerd is een eigen onderwerp — zie Lettertypen: het lastige deel.

  1. Image bytesFor media-heavy files, the largest line item. The filter choice — lossless Flate vs a lossy image codec like DCT (or JPX in a lossy mode) — is the dominant size lever.
  2. Font bytesA full embedded font is mostly glyphs you never draw. Subsetting keeps only the used glyphs, often shrinking the program by an order of magnitude.
  3. Per-object overheadThousands of small dictionaries each pay an obj/endobj envelope plus a cross-reference entry. Object streams (ObjStm) drop the envelope for the whole group; each object keeps a compact type-2 cross-reference entry.
  4. The indexA compressed cross-reference stream with type-2 entries binary-packs and deflates the map of every object, replacing plaintext index rows.
Waar de bytes heen gaan: de vier hendels waaraan een auteur het groottebudget van een PDF besteedt, in de volgorde waarin ze doorgaans een echt bestand domineren.

Er is hier geen verzonnen API om te tonen, omdat de belangrijkste groottebeslissingen voordat de bytes de writer bereiken worden gemaakt — en de ene structurele hendel die NextPDF blootstelt, is een enkele opt-in. Conceptueel leest het budget zo:

  • Voer foto’s als JPEG aan zodat ze onder DCTDecode landen, niet lossless opnieuw gecodeerd. De grootste winst is een keuze over de brongegevens, geen writer-vlag.
  • Laat de writer ingebedde lettertypen subsetten zodat alleen getekende glyphs worden geleverd.
  • Kies voor een document met veel kleine objecten voor object-stream-packing, dat het voltooide bestand door ObjectStreamPacker leidt om de kleine niet-stream-objecten te groeperen en de cross-reference stream te herschrijven.

Het standaardpad — klassieke in-use-vermeldingen, geen ObjStm — is de juiste basislijn: deterministisch, byte-stabiel en makkelijk te verifiëren. Packing is de overwogen upgrade wanneer het objectaantal, niet het beeldgewicht, is wat het bestand opblaast.

De valkuil is grijpen naar een “comprimeer de PDF”-knop en verwachten dat die alles oplost. Compressie is niet één hendel; het zijn er vier, en ze vervangen elkaar niet. Object-stream-packing kan een foto niet verkleinen — dat is de taak van het beeldfilter. Een perfecte JPEG kan een lettertype dat je vergat te subsetten niet compenseren. En niets daarvan helpt als de echte opgeblazenheid een scan van 10 MB is die lossless is opgeslagen omdat niemand er DCTDecode voor koos.

Het tweede misverstand is dat kleiner altijd strikt beter is. Dat is niet zo. Object streams zijn incompatibel met linearisatie — de fast-web-view-layout die absolute objectplaatsing vastpint zodat de eerste pagina vroeg binnenstreamt. En sommige archiveringsprofielen beperken welke beeldfilters überhaupt zijn toegestaan. Grootte is één as. Het ruilt tegen streaming, archiveringsconformiteit en reproduceerbaarheid, en het juiste punt op de curve hangt af van waar het document voor dient.

De vier hendels zijn de structurele economie van bestandsgrootte. Ze zijn geen universele “maak het kleiner”-garantie, en NextPDF doet niet alsof het een re-optimizer is voor willekeurige binnenkomende PDF’s.

ObjectStreamPacker is een best-effort-optimalisatie die nooit correctheid riskeert. Het weigert — en geeft de invoer ongewijzigd terug — wanneer het bestand geen cross-reference-stream-PDF is, wanneer het versleuteld is, wanneer het een digitale handtekening draagt (objecten opnieuw leggen zou de bytebereiken verschuiven die een handtekening beschermt), wanneer het al object streams bevat, of wanneer er geen in aanmerking komend object is om in te pakken. Het stuurt precies één object stream uit voor het hele document; het splitst niet in een /Extends-collectie, wat buiten scope valt en conform is voor de documentgroottes die NextPDF produceert.

De beeld- en fonthendels zijn grotendeels beslissingen over de invoer. NextPDF’s writer transcodeert niet impliciet een lossless bitmap naar een lossy beeldstream — een foto opnieuw coderen naar DCTDecode of JPXDecode is een expliciete upstream-beeldcoderingsbeslissing, niet iets wat de serializer achter je rug om doet — noch wint het glyphs terug van een lettertype dat de aanroeper volledig liet inbedden. De grootste groottewinsten worden upstream van de byte-serializer gemaakt; de taak van de engine is om het budget dat je hem brengt niet te verspillen.

  • Object stream (ObjStm) — een enkele stream die veel kleine niet-stream-indirecte objecten bevat, samen gecomprimeerd, zodat de obj / endobj-envelop voor de groep wordt gedropt. Elk ingepakt object houdt nog steeds zijn eigen cross-reference-vermelding, als een compacte type-2-vermelding. Opt-in in NextPDF.
  • Indirect object — een genummerd object in de graaf van een PDF, verpakt in een obj / endobj-envelop en bijgehouden door de cross-reference-index. De envelop is de overhead per object.
  • Cross-reference stream — de /Type /XRef-stream die elk object indexeert, binair ingepakt en gedeflateerd in plaats van geschreven als platte-tekst-rijen.
  • Gecomprimeerde (type-2-)vermelding — een cross-reference-vermelding die wijst naar een object dat binnen een object stream leeft, en de stream en de index daarbinnen benoemt.
  • Font-subsetting — alleen de glyphs inbedden die een document daadwerkelijk tekent, in plaats van de volledige typeface, wat het ingebedde fontprogramma verkleint.
  • Lossless versus lossy filter — een lossless codec (FlateDecode) reproduceert elke byte; een lossy beeldcodec (DCTDecode, of JPXDecode in een lossy modus) verwerpt onmerkbaar detail voor een veel kleiner resultaat. (JPEG 2000 / JPX kan ook lossless worden geconfigureerd.) De keuze is de dominante hendel op een media-zwaar bestand.