PDF Library for Delphi elimină un operator de matrice de text identitate, 1 0 0 1 0 0 Tm, în timpul optimizării peephole de content stream de la salvare, doar când matricea de text și matricea de linie de text sunt deja identitate: imediat după BT sau imediat după un Tm identitate anterior. Un cm identitate continuă să fie întotdeauna aruncat, pentru că cm înmulțește CTM-ul, în vreme ce Tm înlocuiește ambele matrice de text dintr-una. Din v3.539.28, orice alt Tm identitate rămâne în stream
Bug-ul reparat aici e de soiul tăcut. Un generator de rapoarte emite BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, bazându-se pe Tm-ul identitate să trimită al doilea șir înapoi la originea spațiului de text înainte să-și aplice propria logică de poziționare. Optimizerul mai vechi vedea șase numere care alcătuiau matricea identitate, a decis că operatorul nu are cum să schimbe ceva și l-a șters. Nimic nu a eșuat, nimic nu a logat un avertisment, iar pagina salvată desena „Total” imediat după „Invoice” pe aceeași baseline, exact clasa de defect pe care nimeni nu o observă până când un client tipărește PDF-ul
De ce 1 0 0 1 0 0 Tm nu e întotdeauna un no-op?
Un Tm identitate e un no-op doar când ar înlocui două matrice care țin deja identitatea, iar asta e o proprietate a operatorilor dinaintea lui, nu a operanzilor lui proprii. ISO 32000-1 §9.4.1 spune că BT inițializează atât matricea de text (Tm), cât și matricea de linie de text (Tlm) la identitate, iar §9.4.2 definește Tm ca setându-le pe ambele la valorile date, nu concatenându-le. Comparați cu cm (§8.4.4), care înmulțește la dreapta matricea de transformare curentă: înmulțirea cu identitatea lasă orice CTM neschimbat, deci 1 0 0 1 0 0 cm poate fi șters oriunde. În interiorul unui text object tabloul e altul. Td, TD, T* și un Tm non-identitate mută toate Tlm, iar fiecare operator de afișare a textului (Tj, TJ, ', ") înaintează Tm cu lățimea glyph-urilor pe care le-a desenat. După oricare dintre ei, un Tm identitate e un reset autentic la origine. Dacă ați urmărit vreodată pozițiile textului de mână cu tracker-ul de stare CTM și matrice de text din content stream, aceeași e distincția dintre concatenarea stării și înlocuirea ei
Cum decide scanarea înapoi care Tm identitate se aruncă
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices merge acum înapoi de la fiecare Tm identitate și îl aruncă doar dacă scanarea ajunge întâi la BT sau la un alt Tm identitate. Tm-ul identitate anterior contează indiferent dacă e păstrat sau dacă a fost el însuși chiar programat pentru ștergere, pentru că oricum ar fi a lăsat ambele matrice la identitate, exact cum face BT. Regula sortează fiecare operator pe care îl poate întâlni într-unul din două grupuri:
- Oprire și păstrează Tm-ul:
Td,TD,T*, unTmnon-identitate,Tj,TJ,',",ET, orice operator pe care parser-ul nu-l recunoaște, sau începutul stream-ului - Sar peste și continuă scanarea: operatori care nu ating niciodată Tm sau Tlm, precum
Tf,Tc, setter-ele de culoare,gs, operatorii marked-content șicm
Cazurile conservatoare sunt deliberate. Un operator necunoscut ar putea fi orice, deci scanarea refuză să raționeze dincolo de el. ET închide text object-ul, deci un Tm de după el nu are niciun BT care să garanteze pentru valorile matricei. Scanarea lucrează de asemenea pe un singur content stream odată, ceea ce contează pentru paginile ale căror /Contents e un tablou: un strat care pornește în mijlocul unui text object, fără BT propriu, își păstrează Tm-ul identitate chiar și când stratul anterior l-ar fi făcut redundant. Asta costă câțiva octeți pe fișierele ciudate și nu mută niciodată un glyph. Dacă editați textul paginii la nivel de instrucțiune, ca în turul mapării caracter-către-octet-de-conținut, același model parsat TPDFContentProgram e cel pe care îl rescrie optimizerul
uses
PDFlibContentModel, PDFlibContentOptimize;
function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
Prog: TPDFContentProgram;
Optimizer: TPDFContentPeepholeOptimizer;
begin
Result := Source;
Prog := TPDFContentProgram.Create;
try
if not Prog.Parse(Source) then
Exit; // stream avariat: lasă octeții în pace
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // întoarce numărul de instrucțiuni eliminate
finally
Optimizer.Free;
end;
Result := Prog.Emit; // o instrucțiune pe linie
finally
Prog.Free;
end;
end;
// Eliminat: Tm imediat după BT, al doilea din două Tm identitate la rând
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Păstrat: Tm după Td, după Tj, după un Tm non-identitate, sau în afara BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Rulați helper-ul pe stream-ul de factură din deschidere și Tm-ul identitate supraviețuiește, pentru că scanarea înapoi lovește Tj înainte să ajungă la BT. Puneți /F1 12 Tf, 2 Tc și 0 g între BT și Tm-ul identitate și tot merge, fiindcă niciunul nu atinge matricele de text. O secvență precum BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm pierde exact un operator: primul Tm identitate resetează matricea pe care a mutat-o Td, și doar al doilea e redundant
Când rulează de fapt optimizerul peephole?
Optimizerul rulează doar în pasul de compresie, în interiorul TPDFPageTree.Compress, și doar pe content stream-uri care nu sunt deja compresate Flate. TPDFlib.SetOptimizeContentStreams(1) e implicit, iar același switch e expus și ca câmpul OptimizeContentStreams din TPDFlibSaveOptions; atât CompressContent, cât și CompressPage îl onorează. Un stream al cărui /Filter e deja /FlateDecode e sărit cu desăvârșire, deci încărcarea unui PDF compresat existent și salvarea lui din nou nu-i rescrie operatorii. Dacă stream-ul nu poate fi parsat, octeții decodați originali sunt compresați neschimbați. TPDFlib.NormalizeContentStreams parsează și re-emite conținut cu spațiere și numere canonice, dar nu apelează niciodată optimizerul, ceea ce îl face un baseline util când vreți să vedeți cât din diferența de dimensiune vin regulile peephole, alături de câștigurile mai mari acoperite în optimizarea dimensiunii fișierelor PDF cu subsetting de fonturi
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Stream-urile necompresate trec prin regulile peephole, apoi prin Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Aceeași alegere prin opțiunile de salvare incluse; False renunță
Options.CompressContent := True;
Options.CompressFonts := True;
Options.CompressImages := True;
Options.Linearize := False;
Options.KeepModDate := False;
Options.OptimizeContentStreams := False;
Options.GarbageCollect := False;
Options.PackObjectStreams := True;
Lib.SaveToFileOptions('report-plain.pdf', Options);
finally
Lib.Free;
end;
end;
Ce garanta de fapt vechiul test de regresie?
Vechiul test de regresie garanta o singură formă: un Tm identitate imediat după BT e eliminat. Peephole_RemovesIdentityTextMatrix dă optimizerului BT 1 0 0 1 0 0 Tm (hello) Tj ET și assertează că nu rămâne niciun Tm. Un release anterior observase deja că aruncarea unui Tm identitate e nesigură când Tlm nu e identitate, apoi a păstrat oricum comportamentul pentru că testul îl „încuia”. Citit cu atenție, testul nu spune nimic despre un Tm identitate după Td sau după text afișat; a trata acoperirea unui singur exemplu ca fiind contractul întregii reguli era greșeala propriu-zisă. Corecția păstrează cazul original trecând și adaugă șase cazuri care fixează atât formele eliminabile, cât și cele păstrate, inclusiv un Tm în afara oricărui text object și unul care urmează după ET
Compromisul e ușor de acceptat odată scris pe hârtie. Generatoarele care învăluie fiecare text object ca BT 1 0 0 1 0 0 Tm ... primesc în continuare eliminarea operatorului redundant, și de acolo a venit aproape tot câștigul. Ce cedează optimizerul e un Tm identitate ocazional în mijlocul unui text object, câțiva octeți pe pagină înainte chiar să-i vadă Flate, în schimbul unei garanții pe care header-ul modulului o spune limpede: fiecare transformare e echivalentă la output și nu schimbă niciodată pagina vizibilă. Un optimizer de dimensiune care mută textul nu e un optimizer, ci un bug de randare cu rapoarte de compresie bune
Parser-ul de content stream, optimizerul peephole și opțiunile de compresie la salvare descrise aici se livrează toate în PDF Library for Delphi and C++Builder, care expune de asemenea NormalizeContentStreams, CompressContent și TPDFlibSaveOptions pentru a regla felul în care se scrie fiecare document