PDF Library for Delphi usuwa operator jednostkowej macierzy tekstu, 1 0 0 1 0 0 Tm, podczas zapisowej optymalizacji peephole strumieni treści tylko wtedy, gdy macierz tekstu i macierz linii tekstu są już jednostkowe: tuż po BT albo tuż po wcześniejszym jednostkowym Tm. Jednostkowy cm jest nadal zawsze wyrzucany, bo cm mnoży CTM, podczas gdy Tm zastępuje obie macierze tekstu w całości. Od v3.539.28 każde inne jednostkowe Tm zostaje w strumieniu
Błąd, który to naprawia, jest cichego gatunku. Generator raportów emituje BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET i polega na jednostkowym Tm, żeby odesłać drugi łańcuch do początku przestrzeni tekstu, zanim zastosuje własną logikę pozycjonowania. Starszy optymalizator widział sześć liczb układających się w macierz jednostkową, uznał, że operator nie może niczego zmienić, i go skasował. Nic nie zawiodło, nic nie zapisało ostrzeżenia, a zapisana strona rysowała „Total" natychmiast po „Invoice" na tej samej linii bazowej — czyli dokładnie tę klasę defektu, której nikt nie zauważa, dopóki klient nie wydrukuje PDF-a
Dlaczego 1 0 0 1 0 0 Tm nie zawsze jest no-op?
Jednostkowe Tm jest no-opem tylko wtedy, gdy miałoby zastąpić dwie macierze, które już trzymają jednostkową, a to własność operatorów przed nim, nie jego własnych argumentów. ISO 32000-1 §9.4.1 mówi, że BT inicjalizuje zarówno macierz tekstu (Tm), jak i macierz linii tekstu (Tlm) na jednostkową, a §9.4.2 definiuje Tm jako ustawienie obu na podane wartości, nie doklejanie do nich. Porównaj z cm (§8.4.4), które mnoży aktualną macierz transformacji z prawej: mnożenie przez jednostkową nie zmienia żadnego CTM, więc 1 0 0 1 0 0 cm można bezpiecznie usuwać wszędzie. Wewnątrz obiektu tekstu obraz jest inny. Td, TD, T* i niejednostkowe Tm przesuwają Tlm, a każdy operator pokazujący tekst (Tj, TJ, ', ") posuwa Tm o szerokość namalowanych glifów. Po każdym z nich jednostkowe Tm jest prawdziwym resetem do początku. Jeśli kiedykolwiek śledziłeś pozycje tekstu ręcznie za pomocą trackera stanu CTM i macierzy tekstu strumienia treści, to dokładnie to samo rozróżnienie między doklejaniem stanu a jego zastępowaniem
Jak skan wstecz decyduje, które jednostkowe Tm wyrzucić
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices idzie teraz wstecz od każdego jednostkowego Tm i wyrzuca je tylko, jeśli skan najpierw dojdzie do BT albo innego jednostkowego Tm. Wcześniejsze jednostkowe Tm liczy się niezależnie od tego, czy jest zachowywane, czy samo właśnie trafiło do kolejki usunięcia, bo i tak zostawiło obie macierze jednostkowe, dokładnie jak robi to BT. Reguła sortuje każdego operatora, jakiego może spotkać, do jednej z dwóch grup:
- Stop i zachowaj Tm:
Td,TD,T*, niejednostkoweTm,Tj,TJ,',",ET, każdy operator nierozpoznany przez parser albo początek strumienia - Przejdź obok i skanuj dalej: operatory, które nigdy nie dotykają Tm ani Tlm, jak
Tf,Tc, ustawiacze koloru,gs, operatory marked-content icm
Konserwatywne przypadki są zamierzone. Nieznany operator może być czegokolwiek, więc skan odmawia wnioskowania poza nim. ET zamyka obiekt tekstu, więc Tm po nim nie ma BT, które rękuje za wartości macierzy. Skan pracuje też na jednym strumieniu treści naraz, co ma znaczenie dla stron, których /Contents jest tablicą: warstwa startująca w środku obiektu tekstu, bez własnego BT, zachowuje swoje jednostkowe Tm, nawet gdy poprzednia warstwa uczyniłaby je zbędnym. To kosztuje kilka bajtów na dziwacznych plikach i nigdy nie rusza glifa. Jeśli edytujesz tekst strony na poziomie instrukcji, jak w przewodniku po mapowaniu znak-bajt treści, ten sam sparsowany model TPDFContentProgram jest tym, co przepisuje optymalizator
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; // uszkodzony strumień: bajty zostają jak są
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // zwraca liczbę usuniętych instrukcji
finally
Optimizer.Free;
end;
Result := Prog.Emit; // jedna instrukcja na linię
finally
Prog.Free;
end;
end;
// Usuwane: Tm bezpośrednio po BT, drugie z dwóch identity Tm pod rząd
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Zachowywane: Tm po Td, po Tj, po nie-jednostkowym Tm albo poza BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Uruchom helper na strumieniu faktury z otwarcia, a jednostkowe Tm przeżyje, bo skan wstecz wpada na Tj, zanim dojdzie do BT. Wstaw /F1 12 Tf, 2 Tc i 0 g między BT a jednostkowym Tm, a ono nadal poleci, bo żadne z nich nie dotyka macierzy tekstu. Sekwencja taka jak BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm traci dokładnie jeden operator: pierwsze jednostkowe Tm resetuje macierz, którą przesunęło Td, a zbędne jest tylko drugie
Kiedy optymalizator peephole faktycznie się uruchamia?
Optymalizator rusza tylko podczas przebiegu kompresji, wewnątrz TPDFPageTree.Compress, i tylko na strumieniach treści, które nie są już skompresowane Flate'em. TPDFlib.SetOptimizeContentStreams(1) jest domyślne, a ten sam przełącznik występuje jako pole OptimizeContentStreams w TPDFlibSaveOptions; honorują go zarówno CompressContent, jak i CompressPage. Strumień, którego /Filter to już /FlateDecode, jest pomijany w całości, więc wczytanie istniejącego skompresowanego PDF-a i ponowny zapis nie przepisuje jego operatorów. Gdy strumień nie da się sparsować, oryginalne zdekodowane bajty są kompresowane bez zmian. TPDFlib.NormalizeContentStreams parsuje i wypuszcza treść z kanonicznymi odstępami i liczbami, ale nigdy nie woła optymalizatora, co czyni go użytecznym punktem odniesienia, gdy chcesz zobaczyć, ile wielkości dokłada sama reguła peephole, obok większych zysków opisanych w artykule o optymalizacji rozmiaru plików PDF z subsetowaniem fontów
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Nieskompresowane strumienie przechodzą reguły peephole, potem Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Ten sam wybór przez dołączone opcje zapisu; False rezygnuje
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;
Co stary test regresji naprawdę gwarantował?
Stary test regresji gwarantował jeden kształt: jednostkowe Tm bezpośrednio po BT jest usuwane. Peephole_RemovesIdentityTextMatrix podaje BT 1 0 0 1 0 0 Tm (hello) Tj ET optymalizatorowi i asertuje, że nie zostaje żadne Tm. Wcześniejsze wydanie odnotowało już, że wyrzucanie jednostkowego Tm jest niebezpieczne, gdy Tlm nie jest jednostkowe, a mimo to zostawiło zachowanie, bo test je „zablokował". Czytany uważnie test nie mówi nic o jednostkowym Tm po Td ani po pokazanym tekście; potraktowanie pokrycia jednej próbki jako kontraktu całej reguły było właściwym błędem. Poprawka utrzymuje oryginalny przypadek na zielono i dodaje sześć przypadków przypinających zarówno kształty usuwalne, jak i zachowywane, łącznie z Tm poza jakimkolwiek obiektem tekstu i takim po ET
Kompromis jest łatwy do zaakceptowania, gdy spisze się go na papierze. Generatory owijające każdy obiekt tekstu w BT 1 0 0 1 0 0 Tm ... nadal dostają usunięty ten zbędny operator, i stąd pochodziła niemal cała oszczędność. Czego optymalizator się wyrzeka, to sporadyczne jednostkowe Tm w środku obiektu tekstu, garść bajtów na stronę, zanim Flate w ogóle je zobaczy, w zamian za gwarancję, którą nagłówek modułu stawia czarno na białym: każda transformacja jest równoważna wyjściowo i nigdy nie zmienia widocznej strony. Optymalizator rozmiaru, który rusza tekst, to nie optymalizator, tylko błąd renderowania z dobrymi współczynnikami kompresji
Parser strumieni treści, optymalizator peephole i zapisowe opcje kompresji opisane tutaj są częścią PDF Library for Delphi and C++Builder, która wystawia też NormalizeContentStreams, CompressContent i TPDFlibSaveOptions do strojenia tego, jak każdy dokument jest zapisywany