PDF Library for Delphi haalt een identity text matrix-operator, 1 0 0 1 0 0 Tm, tijdens zijn save-time peephole-contentstream-optimalisatie alleen weg als de text matrix en de text line matrix al de identity zijn: direct na BT, of direct na een eerdere identity Tm. Een identity cm wordt nog steeds altijd weggegooid, want cm vermenigvuldigt de CTM terwijl Tm beide text matrices ronduit vervangt. Sinds v3.539.28 blijft elke andere identity Tm in de stream staan
De bug die hiermee gerepareerd wordt is van het stille soort. Een rapportgenerator zendt BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET uit, erop vertrouwend dat de identity Tm de tweede string terugstuurt naar de oorsprong van de text space voordat de generator zijn eigen positioneringslogica toepast. De oudere optimizer zag zes getallen die de identity matrix spellen, besloot dat de operator onmogelijk iets kon veranderen, en verwijderde hem. Niets faalde, niets logde een waarschuwing, en de opgeslagen pagina tekende "Total" direct na "Invoice" op dezelfde baseline, precies de klasse defecten waar niemand iets van merkt totdat een klant de PDF print
Waarom is 1 0 0 1 0 0 Tm niet altijd een no-op?
Een identity Tm is alleen een no-op als hij twee matrices zou vervangen die al de identity bevatten, en dat is een eigenschap van de operators ervoor, niet van zijn eigen operanden. ISO 32000-1 §9.4.1 zegt dat BT zowel de text matrix (Tm) als de text line matrix (Tlm) op de identity initialiseert, en §9.4.2 definieert Tm als het op de gegeven waarden zetten van beide, niet als daarop samenvoegen. Vergelijk dat met cm (§8.4.4), die de current transformation matrix van rechts vermenigvuldigt: vermenigvuldigen met de identity laat elke CTM onveranderd, dus 1 0 0 1 0 0 cm kan overal veilig weg. Binnen een text object ligt het anders. Td, TD, T* en een non-identity Tm verplaatsen allemaal Tlm, en elke text-showing operator (Tj, TJ, ', ") schuift Tm op met de breedte van de glyphs die hij tekende. Na een van die operators is een identity Tm een echte reset naar de oorsprong. Als u tekstposities ooit met de hand hebt nagelopen met de content-stream CTM- en text matrix state tracker, is dit hetzelfde onderscheid tussen state samenvoegen en state vervangen
Hoe de achterwaartse scan beslist welke identity Tm er afgaat
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices loopt nu achterwaarts vanaf elke identity Tm en laat hem alleen vallen als de scan eerst BT of een andere identity Tm bereikt. De eerdere identity Tm telt of hij behouden blijft of zelf net voor verwijdering is aangemeld, want in beide gevallen liet hij beide matrices op de identity achter, precies zoals BT doet. De regel sorteert elke operator die hij kan tegenkomen in een van twee groepen:
- Stoppen en de Tm behouden:
Td,TD,T*, een non-identityTm,Tj,TJ,',",ET, elke operator die de parser niet herkent, of het begin van de stream - Voorbijstappen en door blijven scannen: operators die Tm of Tlm nooit aanraken, zoals
Tf,Tc, color setters,gs, marked-content operators encm
De conservatieve gevallen zijn een bewuste keuze. Een onbekende operator kan van alles zijn, dus de scan weigert er voorbij te redeneren. ET sluit het text object af, dus een Tm erna heeft geen BT die voor de matrixwaarden instaat. De scan werkt ook per keer op één content stream, wat telt voor pagina's waarvan /Contents een array is: een laag die midden in een text object begint, zonder eigen BT, behoudt zijn identity Tm ook als de vorige laag hem overbodig zou hebben gemaakt. Dat kost een paar bytes op vreemde bestanden en verplaatst nooit een glyph. Als u paginatekst op instructieniveau bewerkt, zoals in de character-to-content-byte mapping walkthrough, is hetzelfde geparsde TPDFContentProgram-model wat de optimizer herschrijft
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; // beschadigde stream: laat de bytes met rust
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // geeft het aantal verwijderde instructies terug
finally
Optimizer.Free;
end;
Result := Prog.Emit; // één instructie per regel
finally
Prog.Free;
end;
end;
// Verwijderd: Tm direct na BT, de tweede van twee identity Tm op een rij
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Behouden: Tm na Td, na Tj, na een non-identity Tm, of buiten BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Zet de helper op de factuurstream uit de inleiding los en de identity Tm overleeft het, want de achterwaartse scan botst op Tj voordat hij BT bereikt. Zet /F1 12 Tf, 2 Tc en 0 g tussen BT en de identity Tm, en hij gaat nog steeds weg, want geen daarvan raakt de text matrices aan. Een reeks als BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm verliest precies één operator: de eerste identity Tm reset de matrix die Td had verschoven, en alleen de tweede is overbodig
Wanneer draait de peephole optimizer eigenlijk?
De optimizer draait alleen tijdens de compressiepass, binnenin TPDFPageTree.Compress, en alleen op content streams die nog niet Flate-gecomprimeerd zijn. TPDFlib.SetOptimizeContentStreams(1) is de standaard, en dezelfde schakelaar is ook beschikbaar als het veld OptimizeContentStreams van TPDFlibSaveOptions; zowel CompressContent als CompressPage houden zich eraan. Een stream waarvan /Filter al /FlateDecode is, wordt volledig overgeslagen, dus een bestaande gecomprimeerde PDF laden en opnieuw opslaan herschrijft zijn operators niet. Als de stream niet te parsen is, worden de oorspronkelijke gedecodeerde bytes onveranderd gecomprimeerd. TPDFlib.NormalizeContentStreams parst content opnieuw en zendt hem uit met canonieke spatiëring en getallen, maar roept de optimizer nooit aan, wat hem een nuttige baseline maakt als u wilt zien hoeveel verschil de peephole-regels in grootte bijdragen, naast de grotere winsten uit PDF-grootte-optimalisatie met font subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// ongecomprimeerde streams gaan door de peephole-regels, daarna Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// dezelfde keuze via de meegeleverde save options; False meldt zich af
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;
Wat garandeerde de oude regression test eigenlijk?
De oude regression test garandeerde precies één vorm: een identity Tm direct na BT wordt verwijderd. Peephole_RemovesIdentityTextMatrix voert BT 1 0 0 1 0 0 Tm (hello) Tj ET uit op de optimizer en asserteert dat er geen Tm meer over is. Een eerdere release had al opgemerkt dat het weggooien van een identity Tm onveilig is als Tlm niet de identity is, en hield het gedrag desondanks aan omdat de test het "vastlegde". Teruggelezen met aandacht zegt de test niets over een identity Tm na Td of na getoonde tekst; de dekking van één monster zien als het contract van de hele regel was de eigenlijke fout. De fix laat dat oorspronkelijke geval slagen en voegt zes gevallen toe die zowel de verwijderbare vormen als de behouden vastzetten, waaronder een Tm buiten elk text object en een die op ET volgt
De afweging is makkelijk te accepteren zodra hij op papier staat. Generators die elk text object inpakken als BT 1 0 0 1 0 0 Tm ... krijgen die redundante operator nog steeds weggehaald, en daar kwam vrijwel alle besparing vandaan. Wat de optimizer opgeeft is de nu en dan voorkomende identity Tm midden in een text object, een handvol bytes per pagina voordat Flate ze zelfs maar ziet, in ruil voor een garantie die de moduleheader ronduit stelt: elke transformatie is output-equivalent en verandert nooit de zichtbare pagina. Een size optimizer die tekst verplaatst is geen optimizer, het is een rendering bug met goede compressieverhoudingen
De content-stream parser, de peephole optimizer en de save-time compressieopties die hier beschreven worden, zitten allemaal in de PDF Library for Delphi and C++Builder, die ook NormalizeContentStreams, CompressContent en TPDFlibSaveOptions beschikbaar stelt om per document af te stemmen hoe het weggeschreven wordt