Techninis straipsnis

Identiteto Tm turinio srautuose: saugus peephole šalinimas

PDF Library for Delphi identiteto teksto matricos operatorių 1 0 0 1 0 0 Tm savo išsaugojimo metu vykdomoje peephole turinio srauto optimizacijoje šalina tik tada, kai teksto matrica ir teksto eilutės matrica jau yra identitetas: iškart po BT arba iškart po ankstesnio identiteto Tm. Identiteto cm vis tiek šalinamas visuomet, nes cm daugina CTM, o Tm abi teksto matricas pakeičia iš karto. Nuo v3.539.28 kiekvienas kitas identiteto Tm lieka sraute

Klaida, kurią tai taiso, yra tylioji rūšis. Ataskaitų generatorius išduoda BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, pasikliaudamas, kad identiteto Tm antrą eilutę grąžins į teksto erdvės pradžią, ir tik tada pritaikys savo pozicionavimo logiką. Senasis optimizuotojas matė šešis skaičius, dėstančius identitetinę matricą, nusprendė, kad operatorius negali nieko pakeisti, ir jį ištrynė. Nieka nepavyko, niekas neįrašė įspėjimo, o išsaugotas puslapis nupiešė „Total“ iškart po „Invoice“ ta pačia pagrindo linija – būtent tos rūšies defektas, kurio niekas nepastebi, kol klientas neatspausdina PDF

Kodėl 1 0 0 1 0 0 Tm ne visada yra no-op?

Identiteto Tm yra no-op tik tada, kai jis pakeistų dvi matricas, kurios jau laiko identitetą, ir tai yra prieš jį esančių operatorių savybė, o ne jo paties operandų. ISO 32000-1 §9.4.1 sako, kad BT abi matricas – teksto (Tm) ir teksto eilutės (Tlm) – inicializuoja į identitetą, o §9.4.2 apibrėžia Tm kaip jų abiejų nustatymą į duotas reikšmes, o ne sudėjimą prie jų. Palyginkite su cm (§8.4.4), kuris dešinėja daugina dabartinę transformavimo matricą: dauginimas iš identiteto palieka bet kurią CTM nepakitusią, tad 1 0 0 1 0 0 cm saugu ištrinti bet kur. Teksto objekto viduje vaizdas kitoks. Td, TD, T* ir ne identiteto Tm visi pajudina Tlm, o kiekvienas tekstą rodantis operatorius (Tj, TJ, ', ") pastumia Tm per nupieštų glifų plotį. Po bet kurio iš jų identiteto Tm yra tikras grąžinimas į pradžią. Jei kada nors esate sekę teksto pozicijas rankomis su turinio srauto CTM ir teksto matricos būsenos sekiku, tai tas pats skirtumas tarp būsenos sudėjimo ir jos pakeitimo

PDFlibPas su 1 0 0 1 0 0 cm ir 1 0 0 1 0 0 Tm elgiasi skirtingai: cm dešinėja daugina CTM ir yra no-op bet kur, o Tm Tm ir Tlm pakeičia iš karto, ir kiekvienas Tj pastumia Tm per nupieštą plotį, tad identiteto Tm po parodyto teksto yra tikras grąžinimas į pradžią
Ataskaitų generatorius ant to grąžinimo ir rėmėsi: ištrynus identiteto Tm, Total nupiešdavo iškart po Invoice ta pačia pagrindo linija, ir niekas nesugriuvo, nebuvo užregistruota ar įspėta kelyje iki kliento spausdintuvo

Kaip atgalinė paieška nusprendžia, kurį identiteto Tm šalinti

TPDFContentPeepholeOptimizer.RemoveIdentityMatrices dabar eina atgal nuo kiekvieno identiteto Tm ir jį šalina tik jei paieška pirmiau pasiekia BT ar kitą identiteto Tm. Ankstesnis identiteto Tm galioja nesvarbu, ar jis paliktas, ar pats ką tik pažymėtas šalinimui, nes abiem atvejais jis abi matricas paliko identitete, lygiai kaip daro BT. Taisyklė kiekvieną sutiktą operatorių priskiria vienai iš dviejų grupių:

  • Sustoti ir palikti Tm: Td, TD, T*, ne identiteto Tm, Tj, TJ, ', ", ET, bet koks analizatoriaus neatpažįstamas operatorius arba srauto pradžia
  • Peržengti ir skenuoti toliau: operatoriai, kurie niekada neliečia Tm ar Tlm, pvz. Tf, Tc, spalvos nustatytojai, gs, marked-content operatoriai ir cm
PDFlibPas RemoveIdentityMatrices eina atgal nuo kiekvieno identiteto Tm: Tf, Tc, spalvos nustatytojai, gs ir cm peržengiami, o Td, TD, T*, ne identiteto Tm, Tj, TJ, neatpažintas operatorius arba ET sustabdo skenavimą ir palieka Tm, o BT liudija už jo šalinimą
Ankstesnis identiteto Tm taip pat sustabdo skenavimą, nes paliktas ar jau pažymėtas šalinimui jis abi matricas paliko identitete – abiem atvejais optimizuotojas nepajudina nė vieno glifo

Konservatyvūs atvejai yra sąmoningi. Neatpažintas operatorius gali būti bet kas, tad paieška atsisako mąstyti už jo. ET užveria teksto objektą, tad Tm po jo neturi BT, kuris liudytų matricos reikšmes. Skenavimas taip pat dirba po vieną turinio srautą, kas svarbu puslapiams, kurių /Contents yra masyvas: sluoksnis, prasidedantis teksto objekto viduryje, be savo BT, laiko savo identiteto Tm net tada, kai ankstesnis sluoksnis jį būtų padaręs nereikalingu. Tai kainuoja kelis baitus keistuose failuose ir niekada nepajudina glifo. Jei redaguojate puslapio tekstą instrukcijų lygyje, kaip simbolių ir turinio baitų atitikimo apraše, tas pats išanalysuotas TPDFContentProgram modelis yra tai, ką optimizuotojas perrašo

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; // sugadintas srautas: paliekame baitus ramybėje
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // grąžina pašalintų instrukcijų skaičių
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // po vieną instrukciją eilutei
  finally
    Prog.Free;
  end;
end;

// Pašalinta: Tm iškart po BT, antrasis iš dviejų iš eilės einančių identiteto Tm
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Palikta: Tm po Td, po Tj, po ne identiteto Tm arba už BT ribų
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Paleiskite pagalbinę funkciją ant įžangos sąskaitos srauto ir identiteto Tm išgyvena, nes atgalinė paieška į Tj užkliūva anksčiau, negu pasiekia BT. Įstatykite /F1 12 Tf, 2 Tc ir 0 g tarp BT ir identiteto Tm – jis vis tiek dingsta, nes niekas iš jų teksto matricų neliečia. Seka, tokia kaip BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm, praranda lygiai vieną operatorių: pirmasis identiteto Tm atstato matricą, kurią pajudino Td, o tik antrasis yra nereikalingas

Kada peephole optimizuotojas iš tikrųjų veikia?

Optimizuotojas veikia tik glaudinimo etapo metu, TPDFPageTree.Compress viduje, ir tik turinio srautams, kurie dar nėra Flate suspausti. TPDFlib.SetOptimizeContentStreams(1) yra numatytasis, o tas pats jungiklis atsiskleidžia kaip TPDFlibSaveOptions laukas OptimizeContentStreams; abu CompressContent ir CompressPage jo laikosi. Srautas, kurio /Filter jau /FlateDecode, praleidžiamas visas, tad egzistuojančio suspausto PDF pakrovimas ir pakartotinis išsaugojimas jo operatorių nerašo iš naujo. Jei srautas nepasiduoda analizei, originalūs iškoduoti baitai suspaudžiami nepakitę. TPDFlib.NormalizeContentStreams analizuoja ir iš naujo išduoda turinį su kanoniniais tarpais ir skaičiais, bet optimizuotojo niekada nekviečia, kas jį daro naudingu etalonu, kai norite matyti, kiek dydžio skirtumo duoda peephole taisyklės, greta didesnių laimėjimų, aprašytų PDF failo dydžio mažinime Delphi aplinkoje

PDFlibPas peephole optimizuotoją paleidžia tik išsaugojimo metu vykstančiame glaudinimo etape: TPDFPageTree.Compress gerbia SetOptimizeContentStreams, /FlateDecode jau naudojęs srautas praleidžiamas visas, neįskaitomas srautas suspaudžiamas su originaliais nepakitusiais baitais, o NormalizeContentStreams optimizuotojo apskritai nekviečia
Praleisti suspausti srautai yra tylioji dalis: pakraukite egzistuojantį PDF, išsaugokite dar kartą, ir jo operatoriai išeina nepaliesti, nes optimizuotojas perrašo tik tuos srautus, kuriuos pats pirmiausiai iškodavo
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // Nesuspausti srautai eina pro peephole taisykles, paskui Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Tas pats pasirinkimas per paketuotas išsaugojimo parinktis; False atsisako
    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;

Ką senasis regresijos testas iš tikrųjų garantavo?

Senasis regresijos testas garantavo vieną formą: identiteto Tm iškart po BT yra pašalinamas. Peephole_RemovesIdentityTextMatrix paduoda BT 1 0 0 1 0 0 Tm (hello) Tj ET optimizuotojui ir teigia, kad nelieka jokio Tm. Ankstesnė laida jau buvo pastebėjusi, kad identiteto Tm šalinimas nesaugus, kai Tlm nėra identitetas, bet elgesį vis tiek paliko, nes testas jį „užrakino“. Atidžiai perskaitytas testas nieko nesako apie identiteto Tm po Td ar po parodyto teksto; vieno pavyzdžio aprėpties traktavimas kaip visos taisyklės sutartis buvo ta tikroji klaida. Pataisa palieka tą pirminį atvejį teigiamu ir prideda šešis atvejus, kurie užkalė ir šalinamas, ir paliekamas formas, įskaitant Tm už bet kokio teksto objekto ribų ir tokį, kuris seka po ET

Kompromisą lengva priimti, kai jis užrašytas. Generatoriai, kurie kiekvieną teksto objektą apgaubia kaip BT 1 0 0 1 0 0 Tm ..., vis tiek sulaukia to nereikalingo operatoriaus šalinimo – būtent iš ten atėjo beveik visos taupomos lėšos. Ko optimizuotojas atsisako, tai pavienio identiteto Tm teksto objekto viduryje – saujos baitų puslapiui, kol Flate jų dar net nesumatė – mainais už garantiją, kurią modulio antraštė sako aiškiai: kiekviena transformacija išvestimi ekvivalentiška ir niekada nekeičia matomo puslapio. Dydžio optimizuotojas, judinantis tekstą, nėra optimizuotojas – tai atvaizdavimo klaida su geromis suspaudimo statistikomis

Turinio srautų analizatorius, peephole optimizuotojas ir išsaugojimo metu veikiančios glaudinimo parinktys, aprašyti čia, keliauja kartu su PDF Library for Delphi ir C++Builder, kuri taip pat atskleidžia NormalizeContentStreams, CompressContent ir TPDFlibSaveOptions norintiems derinti, kaip rašomas kiekvienas dokumentas