Articol tehnic

Reducerea dimensiunii fișierului PDF în Delphi: Fonturi, Imagini, LZW

Pentru a reduce dimensiunea fișierului PDF în Delphi, losLab PDF Library oferă trei API-uri care atacă cele mai mari trei surse de supradimensionare: SubsetEmbeddedFonts rescrie fiecare program de font TrueType încorporat limitându-l la glifele pe care documentul le afișează de fapt, DownsampleImages reeșantionează imaginile raster care depășesc o rezoluție DPI țintă, iar NormalizeLZWStreams înlocuiește compresia veche LZWDecode cu FlateDecode. Fiecare returnează numărul de obiecte modificate, astfel încât valoarea zero vă indică faptul că etapa a fost o operațiune nulă, nu o eroare silențioasă

De ce este PDF-ul meu îmbinat mai mare decât fișierele sale sursă?

Un PDF îmbinat sau generat programatic este de obicei supradimensionat din trei motive: fonturi complet încorporate, imagini eșantionate mult peste rezoluția lor de afișare și fluxuri comprimate încă cu filtrul LZW vechi. ISO 32000-1 §9.9 permite unui producător să încorporeze programul complet de font, iar majoritatea producătorilor fac exact acest lucru deoarece este valoarea implicită sigură. Un program FontFile2 complet de tip Arial ajunge la sute de kiloocteți; încorporați-l în o duzină de fișiere sursă, îmbinați-le și veți transporta o duzină de copii ale contururilor de glife pentru caractere pe care nu le-a introdus nimeni. Îmbinarea în sine nu creează deșeuri, ci doar le concentrează într-un singur fișier unde totalul devine în cele din urmă vizibil

Imaginile sunt a doua problemă. O scanare cu lățimea de 4800 de pixeli plasată într-un cadru de un sfert de pagină trimite de aproximativ 40 de ori mai multe date de pixeli decât poate utiliza o linie de tipărire de 300 DPI. A treia problemă este mai puțin vizibilă: fluxurile filtrate cu LZWDecode. ISO 32000-1 §7.4.4 specifică atât LZWDecode, cât și FlateDecode, menționând că Flate comprimă de obicei cel puțin la fel de bine; în practică, rezultatul Flate este constant mai mic pe aceleași date, iar LZW supraviețuiește mai ales în fișiere care au trecut prin instrumente din anii 1990 la un moment dat în istoricul lor. Restul acestui articol prezintă cele trei etape din losLab PDF Library care remediază fiecare problemă, apoi le combină într-o singură linie de procesare

Subcetarea fonturilor cu SubsetEmbeddedFonts

SubsetEmbeddedFonts reduce fiecare font TrueType încorporat dintr-un document încărcat la caracterele pe care documentul le utilizează de fapt, și nu necesită argumente deoarece derivă lista de păstrare direct din fluxurile de conținut. Intern, etapa parcurge fluxul de conținut al fiecărei pagini cu GetTextRuns, colectează codurile de caractere la care se face referire sub fiecare resursă de font, construiește o listă de păstrare și trimite programul de font original către motorul Windows FontSub (CreateFontPackage) pentru a produce un subset. Programul rescris înlocuiește fluxul FontFile2 pe loc, iar numele BaseFont primește o etichetă LOSABC+, convenția de șase litere majuscule plus semnul plus definită de ISO 32000-1 §9.6.4 pentru fonturile subsetate. Acest prefix este de asemenea cel care face ca apelul să fie idempotent: rulați etapa de două ori, iar fonturile deja subsetate sunt recunoscute și omise, făcând ca integrarea această într-o sarcină pe loturi care poate vizita din nou fișierele să fie sigură

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = number of FontFile2 programs rewritten;
      // 0 means nothing embedded, or everything already subsetted
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

Merită știute două detalii de implementare, deoarece explică limitele API-ului. În primul rând, etapa vizează FontFile2, acoperind astfel programele TrueType încorporate; fonturile încorporate ca Type 1 sau CFF simplu sunt lăsate neatinse pentru a evita riscurile. În al doilea rând, aceasta se bazează pe FontSub, ceea ce face ca SubsetEmbeddedFonts să fie disponibil doar pe Windows. Un aspect mai subtil al implementării: dacă un font este eligibil se stabilește prin rezolvarea efectivă a lanțului de referințe FontDescriptorFontFile2, nu prin încrederea într-o euristică a marcajului de încorporare, deoarece fonturile dintr-un document încărcat nu au trecut niciodată prin evidența de la nivel de creare care setează astfel de marcaje. Dacă fluxul rezolvat există, fontul este un candidat; în caz contrar, este omis fără erori

Compromisul real: un font subsetat conține doar glifele prezente în momentul subsetării. Dacă un instrument din aval sau propriul cod adaugă ulterior text în același font, orice caracter din afara subsetului nu va avea contur și va fi redat ca o glifă lipsă. Subsetarea trebuie realizată ca ultimul pas de modificare a conținutului, niciodată înaintea unei etape de editare. Aceeași precauție se aplică dacă intenționați să extrageți ulterior fontul pentru reutilizare; articolul despre extragerea textului, imaginilor și fonturilor cu PDFlibPas acoperă ceea ce vă poate oferi și ce nu vă poate oferi un program de subset extras

Cum decide DownsampleImages ce imagini să micșoreze?

DownsampleImages(MaxDPI, Quality, Filter) reeșantionează doar imaginile pe care le poate considera cu certitudine ca fiind supraeșantionate, utilizând o estimare DPI deliberat conservatoare. Un obiect imagine PDF XObject stochează dimensiunile în pixeli, dar nicio rezoluție fizică de încredere, iar orice etichetă DPI din imaginea sursă supraviețuiește rar unui ciclu de încărcare-editare-salvare. Astfel, etapa estimează SrcDPI = PixelWidth / 8.5, întrebând practic: dacă această imagine ar acoperi întreaga lățime a unei pagini Letter, care ar fi rezoluția sa? Sunt afectate doar imaginile a căror estimare depășește MaxDPI. Această tendință este intenționată: o imagine plasată la dimensiuni mici pe pagină are un DPI real mai mare decât estimarea, așa că etapa se activează mai greu în loc să degradeze o resursă de calitate pentru tipărire pe care nu o poate măsura

Quality de la 1 la 100 selectează calitatea de re-codificare JPEG, în timp ce 0 păstrează rezultatul ca Flate fără pierderi în stil PNG; Filter alege nucleul de reeșantionare, 0 pentru media casetei și 1 pentru biliniar. Pentru documentele de birou scanate, DownsampleImages(150, 75, 1) este un punct de pornire rezonabil; pentru orice material care ar putea fi retipărit, măriți MaxDPI la 300 sau omiteți complet etapa. Reducerea rezoluției este singurul pas cu pierderi dintre cele trei, așa că ar trebui plasată în spatele unei setări pe care utilizatorii dvs. o pot dezactiva

Convertirea fluxurilor LZW vechi cu NormalizeLZWStreams

NormalizeLZWStreams este o optimizare simplă: decomperează fără pierderi fiecare flux LZWDecode și îl recomprimă cu FlateDecode, pe loc, returnând numărul de fluxuri convertite. Gestionează atât o singură intrare /Filter /LZWDecode, cât și prezența LZW într-un tablou de filtre în lanț, unde doar veriga LZW este înlocuită, iar restul lanțului este păstrat. Parametrii predictorului (Predictor, Columns, Colors, BitsPerComponent) are read from DecodeParms al fluxului și transmiși mai departe către decompresor, astfel încât datele de imagine codificate cu predictor să fie procesate corect. Deoarece ambele filtre sunt codecuri exacte la nivel de bit, octeții decodificați sunt identici înainte și după; se modifică doar compresia containerului, motiv pentru care această etapă este sigură pentru a fi rulată necondiționat pe fiecare fișier

Pe un document fără fluxuri LZW, apelul returnează pur și simplu 0 și nu atinge nimic, lucru pe care suita de teste de regresie a bibliotecii îl verifică în mod explicit: un fișier doar cu Flate proaspăt creat trebuie să raporteze zero conversii. Această garanție de funcționare nulă (no-op) contează atunci când etapa se află într-o linie de procesare care procesează mii de fișiere eterogene, unele din 2024 și altele din 1998

Linia completă de optimizare a dimensiunii în Delphi

function OptimizePDF(const Src, Dst: string): Boolean;
var
  Lib: TPDFlib;
  Fonts, Images, Streams: Integer;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      Exit;
    Fonts   := Lib.SubsetEmbeddedFonts;        // TrueType FontFile2 -> subset
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Log Fonts/Images/Streams: three zeros mean the file was already lean
  finally
    Lib.Free;
  end;
end;

Verificați linia de procesare în modul în care biblioteca se verifică pe ea însăși: prin procesul complet (round-trip). Testele de regresie v3.130 creează un document, îl salvează, îl reîncarcă, rulează optimizarea, salvează din nou, iar apoi afirmă trei lucruri: rezultatul este mai mic, numărul returnat corespunde așteptărilor și o reîncărcare a fișierului optimizat poate fi analizată și redată în continuare. Reproducerea acestei bucle de creare-optimizare-reîncărcare pe un eșantion din propriile fișiere de producție și compararea textului extras înainte și după este o investiție de o oră care depistează erorile de integrare cu mult înainte ca un client să deschidă o factură deteriorată

// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Unde se încadrează linia de optimizare într-un flux de lucru de îmbinare? După îmbinare, nu în timpul acesteia. Îmbinarea mai întâi și optimizarea rezultatului unic înseamnă că fiecare font încorporat este subsetat o singură dată în raport cu uniunea tuturor caracterelor utilizate, în loc de a fi subsetat per fișier sursă. Dacă performanța îmbinării este blocajul, PDFlibPas oferă o cale rapidă la nivel de octet care evită analiza completă a obiectelor, descrisă în articolul despre îmbinarea rapidă a PDF-urilor cu deplasarea referințelor de octeți; și pentru intrările prea mari pentru a fi păstrate complet în memorie, îmbinarea și divizarea cu acces direct pentru PDF-uri mari acoperă arhitectura I/O aferentă pentru fișierele care nu încap confortabil în memoria RAM. Ambele se potrivesc în mod natural cu o etapă finală de optimizare a rezultatului îmbinat

Ce nu vor face cele trei etape

Trio-ul de optimizare al losLab PDF Library exclude în mod deliberat orice modifică semantica documentului. SubsetEmbeddedFonts nu unifică fonturile duplicate din surse îmbinate într-un singur program, ci le reduce pe fiecare în mod independent; deduplicarea este o transformare diferită și mai riscantă. DownsampleImages va trece peste o imagine a cărei estimare DPI conservatoare rămâne sub prag, chiar și atunci când un om ar putea spune că este supradimensionată pentru cadrul ei. De asemenea, niciuna dintre etape nu atinge structura documentului, așa că un fișier supradimensionat de mii de obiecte orfane necesită o salvare de tip rescriere, mai degrabă decât aceste etape la nivel de flux. În aceste limite, combinația dintre subsetarea fonturilor, reducerea rezoluției imaginilor și normalizarea LZW-to-Flate elimină cele trei surse clasice de supradimensionare a PDF-urilor cu câte un apel API predictibil fiecare. Cele trei funcții sunt livrate ca parte a losLab PDF Library pentru Delphi, C# și VB.NET, alături de API-urile de îmbinare, extragere și afișare discutate mai sus