Műszaki cikk

Identity Tm a PDF streamben: biztonságos peephole törlés

A PDF Library for Delphi a mentéskori peephole tartalomstream-optimalizálása során akkor távolítja el az identity text matrix operátort, az 1 0 0 1 0 0 Tm-et, ha a text matrix és a text line matrix már identity: közvetlenül BT után, vagy egy korábbi identity Tm után. Az identity cm-et továbbra is mindig eldobja, mert a cm a CTM-et szorozza, a Tm pedig mindkét text mátrixot egy az egyben lecseréli. A v3.539.28 óta minden más identity Tm a streamben marad

Az a hiba, amit ez javít, a csendes fajta. Egy riportgenerátor kiadja a BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET sort, és az identity Tm-re támaszkodik, hogy a második stringet a szövegtér origójába küldje vissza, mielőtt a saját pozicionálási logikája lépne életbe. A régebbi optimizer hat számot látott, amik az identity mátrixot írják le, eldöntötte, hogy az operátor biztosan nem változtat semmin, és törölte. Semmi nem bukott el, semmi nem írt figyelmeztetést, és a mentett oldal a Total-t közvetlenül az Invoice után rajzolta ugyanazon az alapvonalon, ami pontosan az a hibaosztály, amit senki nem vesz észre, amíg egy ügyfél ki nem nyomtatja a PDF-et

Miért nem mindig no-op az 1 0 0 1 0 0 Tm?

Egy identity Tm csak akkor no-op, ha két, már identity-t tartó mátrixot cserélne le, és ez az őt megelőző operátorok tulajdonsága, nem a saját operandusaié. Az ISO 32000-1 §9.4.1-e szerint a BT mind a text matrixot (Tm), mind a text line matrixot (Tlm) identity-re inicializálja, a §9.4.2 pedig a Tm-et úgy definiálja, hogy mindkettőt a megadott értékekre állítja, nem rájuk fűzi őket. Hasonlítsd össze a cm-mel (§8.4.4), ami jobbról szorozza az aktuális transzformációs mátrixot: identity-vel szorozni bármilyen CTM-et változatlanul hagy, így az 1 0 0 1 0 0 cm bármhol biztonságosan törölhető. Egy text objektumon belül más a kép. A Td, a TD, a T* és egy nem identity Tm mind mozgatja a Tlm-et, és minden szöveget megjelenítő operátor (Tj, TJ, ', ") a kirajzolt glyphok szélességével lépteti a Tm-et. Ezek bármelyike után egy identity Tm valódi visszállítás az origóra. Ha valaha kézzel követtél nyomon szövegpozíciókat a tartalomstream CTM és text matrix állapotkövetőjével, ez ugyanaz a különbség az állapot fűzése és cseréje közt

A PDFlibPas másképp kezeli az 1 0 0 1 0 0 cm-et és az 1 0 0 1 0 0 Tm-et: a cm jobbról szorozza a CTM-et, és bárhol no-op, a Tm viszont a Tm-et és a Tlm-et egy az egyben lecseréli, és minden Tj a kirajzolt szélességgel lépteti a Tm-et, így megjelenített szöveg utáni identity Tm valódi visszaállítás
A riportgenerátor erre a visszaállításra számított: az identity Tm törlése a Total-t közvetlenül az Invoice után tette ugyanarra az alapvonalra, és semmi nem bukott el, nem írt logot és nem figyelmeztetett az ügyfél nyomtatójáig vezető úton

Hogyan dönti el a visszafelé scannelés, melyik identity Tm-et dobjuk el

A TPDFContentPeepholeOptimizer.RemoveIdentityMatrices mostantól minden identity Tm-től visszafelé jár, és csak akkor dobja el, ha a scan előbb BT-t vagy egy másik identity Tm-et ér el. A korábbi identity Tm akkor is számít, ha megtartották, vagy ha épp törlésre jelölték, mert mindkét esetben mindkét mátrixot identity-en hagyta, pontosan úgy, mint a BT. A szabály minden találkozható operátort a két csoport egyikébe sorol:

  • Állj meg és tartsd meg a Tm-et: Td, TD, T*, egy nem identity Tm, Tj, TJ, ', ", ET, bármilyen operátor, amit a parser nem ismer fel, vagy a stream eleje
  • Lépj át és folytasd a scannelést: operátorok, amik sosem nyúlnak a Tm-hez vagy a Tlm-hez, mint a Tf, a Tc, a színbeállítók, a gs, a marked-content operátorok és a cm
A PDFlibPas RemoveIdentityMatrices minden identity Tm-től visszafelé jár: a Tf, a Tc, a színbeállítók, a gs és a cm fölött átlép, míg a Td, a TD, a T*, egy nem identity Tm, a Tj, a TJ, egy ismeretlen operátor vagy az ET megállítja a scannelést, és megtartja a Tm-et, a BT pedig a törlést igazolja
Egy korábbi identity Tm szintén megállítja a scannelést, mert megtartva vagy már törlésre jelölve is identity-en hagyta mindkét mátrixot — akárhogy is, az optimizer sosem mozdít glyphot

A konzervatív esetek szándékosak. Egy ismeretlen operátor bármi lehet, így a scan nem hajlandó rajta túl gondolkodni. Az ET lezárja a text objektumot, így utána egy Tm-nek nincs BT-je, ami igazolná a mátrixértékeket. A scan egy content streamen dolgozik egyszerre, ami azoknál az oldalaknál számít, amiknek a /Contents-e tömb: egy olyan réteg, ami egy text objektum közepén indul, saját BT nélkül, megtartja az identity Tm-ét akkor is, ha az előző réteg redundánssá tette volna. Ez néhány bájtot költségez furcsa fájlokon, és sosem mozdít glyphot. Ha utasításszinten szerkeszted az oldalszöveget, mint a karakter-content bájt leképezési bemutatóban, ugyanaz a TPDFContentProgram modell az, amit az optimizer átír

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; // sérült stream: a bájtokat békén hagyjuk
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // a törölt utasítások számát adja vissza
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // soronként egy utasítás
  finally
    Prog.Free;
  end;
end;

// Törölve: Tm közvetlenül BT után, két egymás utáni identity Tm közül a második
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Megtartva: Tm Td után, Tj után, nem identity Tm után, vagy BT-n kívül
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Futtasd a helpert a nyitó invoice streamen, és az identity Tm túléli, mert a visszafelé scannelés BT előtt Tj-be ütközik. Tegyél /F1 12 Tf-et, 2 Tc-t és 0 g-t a BT és az identity Tm közé, és az még mindig megy, mert egyik sem nyúl a text mátrixokhoz. Egy BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm sorozat pontosan egy operátort veszít: az első identity Tm visszaállítja azt a mátrixot, amit a Td elmozdított, és csak a második a redundáns

Mikor fut valójában a peephole optimizer?

Az optimizer csak a tömörítési fázisban fut, a TPDFPageTree.Compress-en belül, és csak olyan tartalomstreameken, amik még nem Flate-tömörítettek. A TPDFlib.SetOptimizeContentStreams(1) az alapértelmezett, és ugyanez a kapcsoló a TPDFlibSaveOptions OptimizeContentStreams mezőjeként is elérhető; a CompressContent és a CompressPage is figyelembe veszi. Az a stream, aminek a /Filter-e már /FlateDecode, teljesen kimarad, így egy meglévő tömörített PDF betöltése és újramentése nem írja újra az operátorait. Ha a stream parseolása megbukik, az eredeti dekódolt bájtok változatlanul tömörülnek. A TPDFlib.NormalizeContentStreams kanonikus szóközökkel és számokkal parseol és bocsát ki tartalmat, de az optimizert sosem hívja meg, ami hasznos bázisvonal, ha látni akarod, mekkora méretkülönbséget hoznak a peephole szabályok, a font szubsettinges PDF fájlméret-optimalizálás nagyobb nyereségei mellett

A PDFlibPas a peephole optimizert csak a mentéskori tömörítési fázisban futtatja: a TPDFPageTree.Compress figyelembe veszi a SetOptimizeContentStreams-t, a /FlateDecode szűrésű stream teljesen kimarad, a nem parseolható stream az eredeti bájtjai változatlanul tömörödik, a NormalizeContentStreams pedig egyáltalán nem hívja az optimert
A kimaradó tömörített streamek a csendes rész: tölts be egy meglévő PDF-et, mentsd újra, és az operátorai érintetlenül jönnek ki, mert az optimizer csak olyan streameket ír át, amiket előbb dekódolt
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // A tömörítetlen streamek a peephole szabályokon mennek át, majd Flate-en
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Ugyanez a választás a mellékelt mentési opciókon át; a False kizárja magát
    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;

Mit garantált valójában a régi regressziós teszt?

A régi regressziós teszt egyetlen alakot garantált: egy identity Tm közvetlenül BT után törlődik. A Peephole_RemovesIdentityTextMatrix a BT 1 0 0 1 0 0 Tm (hello) Tj ET sort adja az optimizernak, és azt állítja, hogy Tm nem maradt. Egy korábbi kiadás már megjegyezte, hogy egy identity Tm eldobása nem biztonságos, ha a Tlm nem identity, majd ettől függetlenül megtartotta a viselkedést, mert a teszt „bezárta" azt. Alaposabban elolvasva a teszt semmit sem mond egy identity Tm-ről Td után vagy megjelenített szöveg után; egyetlen minta lefedettségét a teljes szabály szerződéseként kezelni volt a tényleges hiba. A javítás megtartja azt az eredeti esetet, és hat esetet ad hozzá, amik a törölhető és a megtartott alakokat is rögzítik, köztük egy Tm-et bármilyen text objektumon kívül, és egyet, ami ET után következik

A kompromisszum leírva könnyen elfogadható. Azok a generátorok, amik minden text objektumot BT 1 0 0 1 0 0 Tm ... alakban csomagolnak, továbbra is megkapják a redundáns operátor törlését, és a megtakarítás majdnem egésze onnan jött. Amiről az optimizer lemond, az a text objektum közepén bújkáló alkalmi identity Tm, oldalanként egy maroknyi bájt, még mielőtt a Flate látná őket, cserébe egy garantálásért, amit a modul fejléce egyenesen kimond: minden transzformáció kimenet-ekvivalens, és sosem változtatja meg a látható oldalt. A szöveget mozdító méretoptimalizáló nem optimizer, hanem jó tömörítési aránnyal bíró renderelési hiba

Az itt tárgyalt tartalomstream-parser, peephole optimizer és mentéskori tömörítési opciók a PDF Library for Delphi and C++Builder részét képezik, ami a NormalizeContentStreams-t, a CompressContent-et és a TPDFlibSaveOptions-t is feltárja, hogy minden dokumentumot ízlésed szerint írhass ki