PDF Library for Delphi tar bort en identity-textmatrisoperator, 1 0 0 1 0 0 Tm, under sin peephole-optimering av innehållsströmmar vid sparande bara när textmatrisen och textradsmatrisen redan är identiteten: direkt efter BT, eller direkt efter en tidigare identity-Tm. En identity-cm tas fortfarande alltid bort, eftersom cm multiplicerar CTM:en medan Tm ersätter båda textmatriserna rakt av. Sedan v3.539.28 stannar varje annan identity-Tm kvar i strömmen
Buggen som detta fixar är av den tysta sorten. En rapportgenerator sänder ut BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET och förlitar sig på identity-Tm:en för att skicka tillbaka den andra strängen till textrummet origo innan den tillämpar sin egen positioneringslogik. Den äldre optimeraren såg sex tal som stavar identitetsmatrisen, beslöt att operatorn omöjligt kunde ändra något, och raderade den. Inget fallerade, inget loggade en varning, och den sparade sidan ritade "Total" direkt efter "Invoice" på samma baslinje, vilket är precis den klass av defekt som ingen lägger märke till förrän en kund skriver ut PDF:en
Varför är 1 0 0 1 0 0 Tm inte alltid en no-op?
En identity-Tm är en no-op bara när den skulle ersätta två matriser som redan innehåller identiteten, och det är en egenskap hos operatorerna före den, inte hos dess egna operander. ISO 32000-1 §9.4.1 säger att BT initierar både textmatrisen (Tm) och textradsmatrisen (Tlm) till identiteten, och §9.4.2 definierar Tm som att sätta båda till de givna värdena, inte att konkatenera på dem. Jämför med cm (§8.4.4), som högermultiplicerar den aktuella transformationsmatrisen: att multiplicera med identiteten lämnar varje CTM oförändrad, så 1 0 0 1 0 0 cm är säker att radera var som helst. Inuti ett textobjekt ser bilden annorlunda ut. Td, TD, T* och en icke-identity-Tm flyttar alla Tlm, och varje textvisande operator (Tj, TJ, ', ") flyttar fram Tm med bredden av de glyfer den ritade. Efter någon av dem är en identity-Tm en äkta återställning till origo. Om du någon gång har spårat textpositioner för hand med tillståndsspåraren för innehållsströmmars CTM och textmatris är det samma distinktion mellan att konkatenera tillstånd och att ersätta det
Hur den bakåtgående skanningen avgör vilka identity-Tm som tas bort
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices går nu baklänges från varje identity-Tm och tar bort den bara om skanningen når BT eller en annan identity-Tm först. Den tidigare identity-Tm:en räknas oavsett om den behålls eller själv just har schemalagts för radering, för på båda sätten lämnade den båda matriserna vid identiteten, exakt som BT gör. Regeln sorterar varje operator den kan träffa på i en av två grupper:
- Stanna och behåll Tm:en:
Td,TD,T*, en icke-identity-Tm,Tj,TJ,',",ET, varje operator parsaren inte känner igen, eller strömmens början - Stega förbi och fortsätt skanna: operatorer som aldrig rör Tm eller Tlm, som
Tf,Tc, färgsättare,gs, marked-content-operatorer ochcm
De konservativa fallen är medvetna val. En okänd operator kan vara vad som helst, så skanningen vägrar resonera förbi den. ET stänger textobjektet, så en Tm efter den har ingen BT som intygar matrisvärdena. Skanningen arbetar också med en innehållsström i taget, vilket spelar roll för sidor vars /Contents är en array: ett lager som börjar mitt i ett textobjekt, utan egen BT, behåller sin identity-Tm även när föregående lager hade gjort den redundant. Det kostar några byte på udda filer och flyttar aldrig en glyf. Om du redigerar sidtext på instruktionsnivå, som i genomgången av mappningen från tecken till innehållsbyte, är samma parsade TPDFContentProgram-modell det optimeraren skriver om
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; // skadad ström: lämna bytena i fred
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // returnerar antalet borttagna instruktioner
finally
Optimizer.Free;
end;
Result := Prog.Emit; // en instruktion per rad
finally
Prog.Free;
end;
end;
// Borttagna: Tm direkt efter BT, den andra av två identity-Tm i rad
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Behållna: Tm efter Td, efter Tj, efter ett icke-identity Tm, eller utanför BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Kör hjälparen på fakturaströmmen från inledningen så överlever identity-Tm:en, eftersom den bakåtgående skanningen träffar Tj innan den når BT. Lägg /F1 12 Tf, 2 Tc och 0 g mellan BT och identity-Tm:en så går den ändå, eftersom ingen av dem rör textmatriserna. En sekvens som BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm förlorar exakt en operator: den första identity-Tm:en återställer matrisen som Td flyttade, och bara den andra är redundant
När körs peephole-optimeraren egentligen?
Optimeraren körs bara under komprimeringspasset, inuti TPDFPageTree.Compress, och bara på innehållsströmmar som inte redan är Flate-komprimerade. TPDFlib.SetOptimizeContentStreams(1) är standard, och samma omkopplare exponeras som fältet OptimizeContentStreams i TPDFlibSaveOptions; både CompressContent och CompressPage respekterar den. En ström vars /Filter redan är /FlateDecode hoppas över helt, så att inläsning av en befintlig komprimerad PDF som sparas igen inte skriver om dess operatorer. Om strömmen inte går att parsa komprimeras de ursprungliga avkodade bytena oförändrade. TPDFlib.NormalizeContentStreams parsar och sänder ut innehåll med kanoniska mellanrum och tal men anropar aldrig optimeraren, vilket gör den till en användbar baslinje när du vill se hur stor del av storleksskillnaden peephole-reglerna bidrar med, jämte de större vinsterna som täcks i PDF-filsoptimering med teckensnitts-subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Okomprimerade strömmar går genom peephole-reglerna, sedan Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Samma val via de medföljande sparalternativen; False tackar nej
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;
Vad garanterade det gamla regressionstestet egentligen?
Det gamla regressionstestet garanterade bara en form: en identity-Tm direkt efter BT tas bort. Peephole_RemovesIdentityTextMatrix matar BT 1 0 0 1 0 0 Tm (hello) Tj ET till optimeraren och gör påståendet att ingen Tm återstår. En tidigare release hade redan konstaterat att det är otryggt att ta bort en identity-Tm när Tlm inte är identiteten, och behöll ändå beteendet eftersom testet "låste" det. Läst noggrant säger testet ingenting om en identity-Tm efter Td eller efter visad text; att behandla ett enskilt provs täckning som hela regelns kontrakt var själva misstaget. Fixen låter det ursprungliga fallet passera och lägger till sex fall som låser både de borttagningsbara formerna och de behållna, inklusive en Tm utanför varje textobjekt och en som följer ET
Avvägningen är lätt att acceptera när den väl är nedskriven. Generatorer som slår in varje textobjekt som BT 1 0 0 1 0 0 Tm ... får fortfarande den redundanta operatorn borttagen, och det är där nästan all besparing kom ifrån. Det optimeraren ger upp är den enstaka identity-Tm:en mitt i ett textobjekt, en handfull byte per sida innan Flate ens ser dem, i utbyte mot en garanti som modulhuvudet säger rakt ut: varje transform är utdataekvivalent och ändrar aldrig den synliga sidan. En storleksoptimerare som flyttar text är ingen optimerare, det är en renderingsbugg med bra komprimeringsgrad
Innehållsström-parsaren, peephole-optimeraren och sparalternativen för komprimering som beskrivs här följer alla med i PDF Library for Delphi and C++Builder, som också exponerar NormalizeContentStreams, CompressContent och TPDFlibSaveOptions för att ställa in hur varje dokument skrivs