Articol tehnic

HotPDF CompressDocument: subseturi font compacte în Delphi

HotPDF THotPDF.CompressDocument e un singur switch care face ca BeginDoc să producă cel mai mic PDF lossless pe care componenta îl poate scrie: FlateDecode la nivelul maxim, un cross-reference stream cu object stream-uri, font subsetting și subseturi de fonturi compacte care renumără glyph-urile păstrate în spatele unui /CIDToGIDMap explicit. EndDoc vă pune apoi înapoi setările proprii. Un document de test de trei pagini cu Arial și SimSun a scăzut de la 10.2 MB la 20 KB cu randare identică

Ce activează de fapt CompressDocument?

CompressDocument suprascrie șase setări de scriere, plus plafonul de object stream-uri, pentru un singur document și le restituie pe toate după. La BeginDoc, înainte ca versiunea PDF să se așeze, HotPDF își notează valorile voastre și setează Compression pe cmFlateDecode, CompressionLevel pe clMaximum, pornește EnableFontSubsetting și CompactFontSubsetting, și activează UseXRefStream plus UseObjectStreams (ISO 32000-1 §7.5.7 și §7.5.8). Object stream-urile cer PDF 1.5, deci un Version mai vechi e ridicat la 1.5 când nu e încuiat. PDF/A-1 interzice ambele structuri, deci un document PDF/A-1 își păstrează tabelul clasic de cross-reference și primește doar partea de Flate și de fonturi. Imaginile rămân întocmai cum le-ați încorporat

Diagrama ciclului de viață HotPDF CompressDocument în Delphi: BeginDoc notează valorile proprii ale scriitorului, suprascrie șase setări printre care Compression și UseObjectStreams pentru un singur document, iar EndDoc restituie fiecare valoare împrumutată în finally-ul lui cel mai exterior, în timp ce proprietatea CompressDocument rămâne ea însăși True
Șase setări de scriere și plafonul de object stream-uri sunt împrumutate pentru exact un document și date înapoi când rulează EndDoc, deci un raport eșuat nu lasă niciodată componenta blocată la compresie maximă
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // aplicat de BeginDoc, anulat de EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Restituirea are loc în finally-ul cel mai exterior al lui EndDoc, deci o excepție la jumătatea unui raport nu lasă o componentă de lungă durată blocată pe compresie maximă pentru jobul următor. Proprietatea CompressDocument rămâne ea însăși True; doar cele șase setări împrumutate se întorc. Versiunea e tratată cu mai multă grijă. HotPDF anulează propria lui ridicare la 1.5 doar dacă documentul se termină în continuare la 1.5, deci când o altă funcționalitate a împins fișierul la 1.6 în timpul rulării (un font OpenType încorporat, să zicem), versiunea mai mare rămâne, exact cum ar fi rămas fără compresie

De ce subseturile de fonturi rămân mari fără compactare?

Un subset TrueType clasic aruncă contururile pe care nu le desenați vreodată, dar păstrează fiecare glyph ID acolo unde era, iar numerotarea aceea e cea care-l ține greu. Content stream-ul arată CID-uri egale cu GID-urile originale, deci subsetul trebuie să țină un offset loca și o intrare hmtx pentru fiecare slot până la cel mai mare glyph pe care îl reține, gol sau nu. Pentru un font latin cheltuiala asta e zgomot. Pentru un font CJK precum SimSun, ale cărui ideograme stau adânc într-un tabel de glyph-uri foarte mare, două caractere chinezești trag după ele tabele dimensionate pentru tot fontul. Regulile de închidere a subseturilor de fonturi pentru glyph-uri modelate decid ce glyph-uri supraviețuiesc; compactarea ține de cât costă supraviețuitorii

CompactFontSubsetting renumără glyph-urile păstrate într-un interval dens pornind de la zero și scrie un stream /CIDToGIDMap pe CIDFont, pe care ISO 32000-1 §9.7.4.2 îl definește ca un tabel de GID-uri pe doi octeți indexate după CID. Tabelul acela e tot trucul. Content stream-urile, tabloul de lățimi /W și CMap-ul ToUnicode păstrează cu toții CID-urile originale, deci nimic din ce e deja scris nu trebuie schimbat; doar căutarea de la CID la glyph se mută în hartă. În testul care a motivat funcționalitatea, SimSun cu două caractere a coborât de la 24.8 KB de date de font la 3.1 KB

Comparație între un subset de font HotPDF rar, care reține intrări loca și hmtx pentru fiecare glyph ID original până la cel mai înalt GID reținut, și output-ul CompactFontSubsetting, care renumără dens glyph-urile păstrate de la zero și mapează CID-urile printr-un stream CIDToGIDMap, în timp ce content stream-urile, /W și ToUnicode rămân neschimbate
Renumărarea mută cheltuiala din programul fontului într-un singur stream de hartă mic — două caractere SimSun au coborât de la 24.8 KB la 3.1 KB fără să atingă un octet din conținutul deja scris

Compactarea are limite ferme și se degradează în tăcere în loc să eșueze. HotPDF construiește subseturi compacte doar pentru fonturi Type 0 TrueType, atât cele setate prin SetFont cu subsetting pornit, cât și fontul înregistrat prin RegisterUnicodeTTF. Un font TrueType simplu își găsește glyph-urile prin cmap-ul din interiorul programului de font, pe care renumărarea l-ar rupe, deci păstrează subsetul rar. Fonturile OpenType-CFF n-au nici ele cale compactă. O construcție compactă care eșuează cade pe subsetul rar în loc să ridice. Proprietatea e oprită din fabrică, deci output-ul existent rămâne identic octet cu octet, în timp ce sub PDF/A fontul Unicode înregistrat primește oricum un subset compact

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

Cum strânge scriitorul împachetat structura fișierului?

Odată ce fonturile și stream-urile sunt mici, dicționarele și datele de cross-reference devin cea mai mare cheltuială rămasă, deci scriitorul de object stream-uri din spatele lui CompressDocument le rade și pe acestea. Ghidul object stream-uri și actualizări incrementale acoperă formatul de container în sine; calea de compresie adaugă peste el patru rafinări:

  • Sintaxă compactă conform ISO 32000-1 §7.2.2: un spațiu e scris doar între două token-uri care altfel s-ar lipi ca caractere regulate, deci /Type /Page devine /Type/Page
  • Câmpurile stream-ului de cross-reference iau orice lățime pe care §7.5.8.2 o permite, deci un fișier sub 16 MB stochează fiecare offset pe 3 octeți în loc de 4
  • Până la 250 de obiecte intră în fiecare object stream în loc de cele obișnuite 100, cu excepția cazului în care vă setați propriul plafon prin ConfigureAdaptiveObjectStreamPacking
  • Când fișierul nu e criptat, Catalogul și dicționarul Info sunt împachetate și ele în object stream-uri; output-ul criptat le ține la nivelul de sus

Sintaxa compactă a venit cu o capcană care merită știută dacă extindeți scriitorul. Semnarea completează semnătura după ce fișierul e scris, căutând în octeți placeholder-ele literale /ByteRange ( și /Contents <, iar ortografia compactă le-ar transforma în /ByteRange( și /Contents<, pe care căutarea nu le găsește niciodată. Dicționarele de semnătură (Type Sig sau DocTimeStamp, FT Sig) și dicționarul de criptare păstrează deci aranjamentul cu spații. Un defect înrudit a afectat build-urile de dinainte de v2.766.41: fiecare salvare cu object stream-uri, CompressDocument inclusă, începea cu două linii de header %PDF-, deci faceți upgrade dacă un validator strict vă semnalează output-ul

Puteți comprima un PDF deja încărcat?

Da, prin overload-ul cu opțiuni CompressLoadedDocument(Options, Info), care rulează aceleași pași lossless pe un fișier existent. Cu THPDFLoadedDocumentCompressionOptions.Default, el elimină resursele de pagină nefolosite, îmbină fonturile și formularele identice, subsetează fonturile încorporate cu subseturi compacte pornite, recompresă stream-urile nefiltrate, Flate, LZW, ASCII și RunLength cu Flate când rezultatul e mai mic, și face ca următoarea salvare să folosească object stream-uri. HighRatioFlate e oprit din fabrică, iar object stream-urile sunt sărite pentru PDF/A-1 și salvările incrementale. Overload-ul fără parametri CompressLoadedDocument e apelul mai vechi și mai îngust care doar comprima Flate stream-urile necompresate

Fluxul HotPDF CompressLoadedDocument în Delphi: apelul elimină resursele de pagină nefolosite, îmbină fonturile și formularele identice, subsetează fonturile încorporate cu subseturi compacte, recompresă stream-urile cu Flate doar când rezultatul e mai mic și pornește object stream-urile pentru următoarea salvare, în timp ce câmpurile de semnătură declanșează RefusedBySignaturePolicy și lasă fișierul neatins
Fiecare pas rescrie octeți pe care o semnătură îi acoperă, deci documentul întreg e refuzat cu excepția cazului în care permiteți explicit invalidarea — Info.BytesSaved însumează atunci doar lucrarea de resurse, fonturi și stream-uri
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;

Două limite contează pe calea documentului încărcat. Fiecare pas rescrie octeți pe care o semnătură îi acoperă, deci un document cu câmpuri de semnătură e refuzat ca întreg: apelul întoarce 0, setează RefusedBySignaturePolicy și nu schimbă nimic, cu excepția cazului în care setați AllowSignatureInvalidation, după care Info.SignaturesInvalidated vă spune la ce ați renunțat. Compactarea e de asemenea mai conservatoare aici decât pe calea de creare. HotPDF compactează doar programele de font folosite exclusiv de fonturi CIDFontType2 cu /CIDToGIDMap Identity, unde CID egalează GID, și sare peste programele cu un stream de hartă existent, un /CIDSet sau tabele de glyph-uri colorate precum COLR, sbix, CBDT sau SVG, pentru că reconstrucția compactă ar arunca straturile de culoare. Rețineți de asemenea că Info.BytesSaved însumează doar pașii de resurse, fonturi și stream-uri; câștigul de object stream-uri apare când fișierul e scris

Ce rezultate trebuie să vă așteptați în practică?

Câștigurile urmăresc cât dintr-un fișier e structură necompresată și date de font supradimensionate, nu câte pagini are. Eșantionul de trei pagini cu Arial și SimSun s-a micșorat de la 10.2 MB la 20 KB când a fost generat cu CompressDocument, și de la 10.2 MB la 19.8 KB când originalul necompresat a fost încărcat și trecut prin CompressLoadedDocument, cu randare identică pe ambele căi. Un PDF deja compact abia se mișcă: în setul de regresie, asemenea fișiere s-au salvat între -0.07% și +0.06% din dimensiunea lor originală. Fișierele pline de poze câștigă puțin, pentru că niciuna dintre căi nu atinge datele de imagine

Dacă generați în fiecare noapte aceleași rapoarte CJK, combinați subseturile compacte cu cache-ul persistent de subseturi de fonturi pe disc, ca munca de subsetting să nu se repete per rulare, și faceți diff la output-urile compresate după conținutul de obiect, nu după octeți, fiindcă un singur câmp schimbat re-Flatează un object stream întreg. Referințele complete de proprietăți și înregistrări sunt pe pagina de produs a componentei HotPDF Delphi PDF