Tekninen artikkeli

Identity Tm sisältöstreameissa: turvallinen peephole-poisto

PDF Library for Delphi poistaa identity-tekstimatriisioperaattorin 1 0 0 1 0 0 Tm tallennusaikaisen peephole-sisältöstreamioptimoinnin yhteydessä vain silloin, kun tekstimatriisi ja tekstirivimatriisi ovat jo identityä: heti BT:n jälkeen tai heti aiemman identity-Tm:n jälkeen. Identity-cm pudotetaan yhä aina, koska cm kertoo CTM:n, kun taas Tm korvaa molemmat tekstimatriisit suoraan. Versiosta v3.539.28 alkaen jokainen muu identity-Tm jää streamiin

Tämä korjaus koskee hiljaista vikaa. Raporttigeneraattori emittoi rivin BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET ja nojaa identity-Tmihin siinä, että se lähettää toisen merkkijonon takaisin tekstitilan alkupisteeseen ennen kuin soveltaa omaa sijoituslogiikkaansa. Vanhempi optimoija näki kuusi lukua, jotka muodostavat identiteettimatriisin, päätti, ettei operaattori voi mitenkään muuttaa mitään, ja poisti sen. Mikään ei kaatunut, mikään ei kirjannut varoitusta, ja tallennettu sivu piirsi sanan "Total" heti sanan "Invoice" perään samalle peruslinjalle, mikä on täsmälleen sitä vikaluokkaa, jota kukaan ei huomaa, ennen kuin asiakas tulostaa PDF:n

Miksi 1 0 0 1 0 0 Tm ei aina ole no-op?

Identity-Tm on no-op vain silloin, kun se korvaisi kaksi matriisia, jotka jo pitävät identityä, ja tuo on ominaisuus sitä edeltävistä operaattoreista, ei sen omista operandeista. ISO 32000-1 §9.4.1 sanoo, että BT alustaa sekä tekstimatriisin (Tm) että tekstirivimatriisin (Tlm) identityksi, ja §9.4.2 määrittelee Tmin asettavaksi molemmat annettuihin arvoihin, ei ketjutettavaksi niiden päälle. Vertaa cm:een (§8.4.4), joka kertoo oikealta nykyisen muunnosmatriisin: kertominen identityllä jättää minkä tahansa CTM:n ennalleen, joten 1 0 0 1 0 0 cm on turvallista poistaa mistä tahansa. Tekstiobjektin sisällä tilanne on toinen. Td, TD, T* ja ei-identity-Tm siirtävät kaikkia Tlm:ää, ja jokainen tekstiä näyttävä operaattori (Tj, TJ, ', ") etenee Tm:ää piirtämien glyyfiensä leveydellä. Minkä tahansa niistä jälkeen identity-Tm on aito palautus alkupisteeseen. Jos olet joskus jäljittänyt tekstipaikkoja käsin artikkelin sisältöstreamin CTM- ja tekstimatriisin tilaseurannan avulla, tässä on sama ero tilan ketjuttamisen ja korvaamisen välillä

PDFlibPas kohtelee arvoja 1 0 0 1 0 0 cm ja 1 0 0 1 0 0 Tm eri tavalla: cm kertoo CTM:n oikealta ja on no-op missä tahansa, kun taas Tm korvaa Tm:n ja Tlm:n suoraan, ja jokainen Tj etenee Tm:ää piirtämänsä leveyden verran, joten identity-Tm näytetyn tekstin jälkeen on aito palautus
Raporttigeneraattori nojasi tuohon palautukseen: identity-Tm:n poistaminen piirsi sanan Total heti sanan Invoice perään samalle peruslinjalle, eikä matkalla asiakkaan tulostimelle kaatunut mitään, kirjautunut mitään tai varoitettu mistään

Miten taaksepäin kulkeva skannaus päättää, mitkä identity-Tm pudotetaan

TPDFContentPeepholeOptimizer.RemoveIdentityMatrices kävelee nyt taaksepäin jokaisesta identity-Tmstä ja pudottaa sen vain, jos skannaus saavuttaa ensin BT:n tai toisen identity-Tmn. Aiempi identity-Tm kelpaa riippumatta siitä, säilytetäänkö se vai onko se itse juuri merkitty poistettavaksi, koska kummassakin tapauksessa se jätti molemmat matriisit identityksi, täsmälleen kuten BT tekee. Sääntö lajittelee jokaisen kohtaamansa operaattorin kahteen ryhmään:

  • Pysähdy ja säilytä Tm: Td, TD, T*, ei-identity-Tm, Tj, TJ, ', ", ET, mikä tahansa operaattori, jota jäsennin ei tunnista, tai streamin alku
  • Astu yli ja jatka skannausta: operaattorit, jotka eivät koskaan kosketa Tm:tä tai Tlm:ää, kuten Tf, Tc, värin asettajat, gs, marked-content-operaattorit ja cm
PDFlibPas RemoveIdentityMatrices kävelee taaksepäin jokaisesta identity-Tm:stä: Tf, Tc, värin asettajat, gs ja cm yli astutaan, kun taas Td, TD, T*, ei-identity Tm, Tj, TJ, tuntematon operaattori tai ET pysäyttää skannauksen ja säilyttää Tm:n, ja BT takaa pudotuksen
Myös aiempi identity-Tm pysäyttää skannauksen, koska säilytettynä tai jo poistoon merkittynä se jätti molemmat matriisit identityksi — kummalla tavalla optimoija ei koskaan siirrä glyyfiä

Konservatiiviset tapaukset ovat tahallisia. Tuntematon operaattori voi olla mitä tahansa, joten skannaus kieltäytyy päättelemästä sen ohi. ET sulkee tekstiobjektin, joten Tm sen jälkeen ei saa BTltä takuuta matriisiarvoille. Skannaus toimii lisäksi yhden sisältöstreamin kerrallaan, mikä merkitsee sivuja, joiden /Contents on taulukko: kerros, joka alkaa tekstiobjektin keskeltä ilman omaa BTään, pitää identity-Tmnsä, vaikka edellinen kerros olisi tehnyt sen tarpeettomaksi. Se maksaa muutaman tavun oudoissa tiedostoissa eikä koskaan siirrä glyyfiä. Jos muokkaat sivun tekstiä käskytasolla, kuten artikkelin merkkien ja sisältötavujen mappauksen läpikäynti tekee, sama jäsennetty TPDFContentProgram-malli on se, jonka optimoija kirjoittaa uudelleen

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; // vioittunut stream: jätä tavut rauhaan
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // palauttaa poistettujen käskyjen määrän
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // yksi käsky per rivi
  finally
    Prog.Free;
  end;
end;

// Poistettu: Tm suoraan BT:n jälkeen, kahden peräkkäisen identity Tm:n jälkimmäinen
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Säilytetty: Tm Td:n jälkeen, Tj:n jälkeen, ei-identity-Tm:n jälkeen tai BT:n ulkopuolella
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Aja apufunktio aloituksen laskutusskriptille ja identity-Tm selviää hengissä, koska taaksepäin kulkeva skannaus osuu Tj:hin ennen kuin se saavuttaa BT:n. Laita /F1 12 Tf, 2 Tc ja 0 g BT:n ja identity-Tmn väliin, ja se katoaa yhä, koska yksikään niistä ei kosketa tekstimatriiseja. Sekvenssi kuten BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm menettää täsmälleen yhden operaattorin: ensimmäinen identity-Tm nollaa matriisin, jonka Td siirsi, ja vain jälkimmäinen on tarpeeton

Milloin peephole-optimoija oikeasti ajaa?

Optimoija ajaa vain pakkausvaiheen sisällä, TPDFPageTree.Compressin sisällä, ja vain sisältöstreameissa, jotka eivät ole jo Flate-pakattuja. TPDFlib.SetOptimizeContentStreams(1) on oletus, ja sama kytkin on näkyvillä myös TPDFlibSaveOptions-rakenteen OptimizeContentStreams-kenttänä; sekä CompressContent että CompressPage noudattavat sitä. Stream, jonka /Filter on jo /FlateDecode, ohitetaan kokonaan, joten olemassa olevan pakatun PDF:n lataaminen ja uudelleentallennus ei kirjoita sen käskyjä uudelleen. Jos streamin jäsentäminen epäonnistuu, alkuperäiset dekoodatut tavut pakataan muuttumattomina. TPDFlib.NormalizeContentStreams jäsentää ja emittoi sisällön kanonisella välilyönnityksellä ja luvuilla, mutta ei koskaan kutsu optimoijaa, mikä tekee siitä hyödyllisen vertailukohdan, kun haluat nähdä, kuinka suuren kokoeron peephole-säännöt tuovat, rinnalla suuremmat säästöt, jotka on käsitelty kirjoituksessa PDF-tiedostokoon optimointi fonttisubsettingilla

PDFlibPas ajaa peephole-optimoijan vain tallennusaikaisen pakkausvaiheen sisällä: TPDFPageTree.Compress noudattaa SetOptimizeContentStreamia, /FlateDecode-suodatettu stream ohitetaan kokonaan, jäsentymätön stream pakataan alkuperäisin tavuin muuttumattomana, eikä NormalizeContentStreams koskaan kutsu optimoijaa lainkaan
Ohitetut pakatut streamit ovat hiljainen osa: lataa olemassa oleva PDF, tallenna se uudelleen, ja sen käskyt tulevat ulos koskemattomina, koska optimoija kirjoittaa uudelleen vain streamit, jotka se ensin dekoodasi
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // Pakkaamattomat streamit kulkevat peephole-sääntöjen läpi, sitten Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Sama valinta mukana toimitettavien tallennusasetusten kautta; False kieltäytyy
    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ä vanha regressiotesti oikeasti takasi?

Vanha regressiotesti takasi täsmälleen yhden muodon: identity-Tm suoraan BT:n jälkeen poistetaan. Peephole_RemovesIdentityTextMatrix syöttää optimoijalle rivin BT 1 0 0 1 0 0 Tm (hello) Tj ET ja assertoi, ettei yhtään Tmä jää jäljelle. Aiempi julkaisu oli jo todennut, että identity-Tmn pudottaminen on turvatonta, kun Tlm ei ole identity, ja piti silti käyttäytymisen ennallaan, koska testi "lukitsi" sen. Huolella uudelleen luettuna testi ei sanonut mitään identity-Tmstä Td:n jälkeen tai näytetyn tekstin jälkeen; yhden otannan kattavuuden käsittely koko säännön sopimuksena oli varsinainen virhe. Korjaus pitää alkuperäisen tapauksen läpäisyn ja lisää kuusi tapausta, jotka lukitsevat sekä poistettavat muodot että säilytettävät, mukaanlukien Tm tekstiobjektin ulkopuolella ja yksi, joka seuraa ETä

Kompromissi on helppo hyväksyä, kun se on kirjoitettu ylös. Generaattorit, jotka käärittävät jokaisen tekstiobjektin muotoon BT 1 0 0 1 0 0 Tm ..., saavat yhä tuon tarpeettoman operaattorin poistetuksi, ja juuri siinä tuli lähes kaikki säästöt. Se, mistä optimoija luopuu, on satunnainen identity-Tm tekstiobjektin keskellä, kourallinen tavuja sivua kohti ennen kuin Flate edes näkee ne, vastineeksi takuusta, jonka moduulin otsikko sanoo suoraan: jokainen muunnos on tulosteekvivalentti eikä koskaan muuta näkyvää sivua. Koon optimoija, joka siirtää tekstiä, ei ole optimoija, vaan renderöintivika hyvillä pakkaussuhteilla

Sisältöstreamin jäsennin, peephole-optimoija ja tässä kuvatut tallennusaikaiset pakkausasetukset toimitetaan mukana tuotteessa PDF Library for Delphi and C++Builder, joka näyttää myös NormalizeContentStreams, CompressContent ja TPDFlibSaveOptions kunkin asiakirjan kirjoitustavan säätöön