Articol tehnic

Tm identitate în stream-uri PDF: eliminare peephole sigură

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

PDFlibPas tratează diferit 1 0 0 1 0 0 cm și 1 0 0 1 0 0 Tm: cm înmulțește la dreapta CTM-ul și e un no-op oriunde, în timp ce Tm înlocuiește dintr-una Tm și Tlm, iar fiecare Tj înaintează Tm cu lățimea desenată, deci un Tm identitate după text afișat e un reset autentic
Generatorul de rapoarte se baza pe acel reset: ștergerea Tm-ului identitate desena Total imediat după Invoice pe aceeași baseline, iar nimic nu a eșuat, logat sau avertizat pe drumul spre imprimanta clientului

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*, un Tm non-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 și cm
PDFlibPas RemoveIdentityMatrices merge înapoi de la fiecare Tm identitate: peste Tf, Tc, setter-ele de culoare, gs și cm se sare, în timp ce Td, TD, T*, un Tm non-identitate, Tj, TJ, un operator necunoscut sau ET oprește scanarea și păstrează Tm-ul, iar BT garantează pentru aruncarea lui
Un Tm identitate anterior oprește și el scanarea, pentru că păstrat sau deja programat pentru ștergere a lăsat ambele matrice la identitate — oricum ar fi, optimizerul nu mută niciodată un glyph

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

PDFlibPas rulează optimizerul peephole doar în pasul de compresie de la salvare: TPDFPageTree.Compress onorează SetOptimizeContentStreams, un stream deja filtrat cu /FlateDecode e sărit cu desăvârșire, un stream neparsabil e compresat cu octeții originali neschimbați, iar NormalizeContentStreams nu apelează deloc optimizerul
Stream-urile compresate sărite sunt partea tăcută: încărcați un PDF existent, salvați-l din nou, iar operatorii lui ies neatingși, pentru că optimizerul rescrie doar stream-urile pe care le-a decodat întâi
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