PDFlibPas, natywna biblioteka komponentu VCL PDF dla Delphi i C++Buildera, odtwarza strumień zawartości strony przez swoją klasę TPDFContentStateTracker w ogóle nie dotykając płótna renderowania. Podawanie trackerowi po jednym sparsowanym operatorze naraz utrzymuje bieżący rekord stanu grafiki — bieżącą macierz transformacji, macierz tekstu, granice przycinania i stos zapisu q/Q — dostępny do zrzutu przed lub po wykonaniu każdego operatora
Zapytaj, gdzie faktycznie ląduje przebieg tekstu na wydrukowanej stronie, a same surowe liczby ze strumienia zawartości będą cię za każdym razem wprowadzać w błąd. TPDFContentProgram.GetTextRuns już zgłasza punkt zakotwiczenia każdej instrukcji pokazującej tekst przez pola OriginX i OriginY na TPDFTextRun, a komentarze pól wyraźnie stwierdzają, że ten punkt siedzi w przestrzeni tekstu, już złożony przez Tm, Td, TD i T*. Wciąż brakuje, i to komentarze mówią, że wywołujący musi to dostarczyć, aktywnego CTM w tej dokładnej instrukcji — iloczynu każdego cm skonkatenowanego dotychczas, zagnieżdżonego w tylu parach q/Q, ile akurat jest otwartych w tym punkcie strumienia
Dlaczego odtwarzać strumień zawartości zamiast go renderować?
PDFlibPas utrzymuje dwa osobne pojęcia stanu grafiki dla dwóch osobnych zadań, i ten podział jest celowy. Wewnętrzny rekord stanu renderera niesie żywy uchwyt płótna urządzenia, uchwyt regionu przycinania i pamięci podręczne rasteryzacji czcionek — prawdziwe zasoby powiązane z jakąkolwiek powierzchnią aktualnie malowaną, i bezsensowne, gdy ta powierzchnia zniknie. TPDFContentGraphicsState nie niesie niczego z tego: to zwykły rekord ograniczony do wartości, które ISO 32000-1 §8.4 definiuje jako osiągalne wyłącznie z operatorów strumienia zawartości — CTM, styl linii, kolor, stan tekstu oraz wyprowadzone granice przycinania i ścieżki. Ponieważ rekord nie niesie żadnego odwołania do płótna ani żadnego otwartego uchwytu pliku, wywołujący może sparsować strumień zawartości, przejść przez niego za pomocą TPDFContentStateTracker i wciąż korzystać z wynikowych zrzutów długo po tym, jak cokolwiek wygenerowało te bajty już zniknęło
Jak TPDFContentStateTracker buduje CTM
TPDFContentStateTracker.Apply konkatenuje sześć operandów operatora cm do CTM trackera, używając tego samego premnożenia, jakie sam PDF specyfikuje: nowa macierz M2 łączy się z bieżącym CTM jako M2 × CTM, w konwencji wektora wierszowego, gdzie punkt transformuje się jako P′ = P × M (ISO 32000-1 §8.4). Część łatwa do pomylenia siedzi w członie translacji, nie w części liniowej: własna translacja M2 musi przejść przez komponent rotacji-i-skali bieżącego CTM, zanim doda się na wierzchu translację bieżącego CTM. Pomiń ten krok i zaszyj na sztywno naiwną kombinację komponent-po-komponencie zamiast tego, a pierwszy izolowany cm, który przetestujesz, będzie wyglądał poprawnie, podczas gdy każda współrzędna niżej w potoku po drugim czy trzecim zagnieżdżonym cm po cichu dryfuje, co jest dokładnie tym rodzajem błędu, który przetrwa przegląd kodu, ponieważ test jednostkowy, który by go wychwycił, potrzebuje przynajmniej dwóch połączonych transformacji, żeby zawieść
var
Prog: TPDFContentProgram;
Runs: TPDFTextRunArray;
States: TPDFContentGraphicsStateArray;
DeviceX, DeviceY: Double;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
try
Prog.Parse(ContentBytes);
Runs := Prog.GetTextRuns;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (ISO 32000-1 8.4)
with States[Runs[I].InstructionIndex].CTM do
begin
DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
end;
LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
end;
finally
Prog.Free;
end;
end;
Powyższa pętla odpowiada na problem z otwarcia: TPDFContentProgram.GetTextRuns oddaje OriginX i OriginY już złożone przez Tm, Td, TD i T*, a TraceGraphicsStates(nil, False) dostarcza ten jeden pozostały element, CTM sprzed instrukcji w dokładnym indeksie, w którym każdy przebieg został przechwycony, w jednym liniowym przejściu przez cały program. Podanie nil pozwala metodzie posiadać własny prywatny tracker dla tego wywołania i zwolnić go wewnętrznie, co jest właściwym wyborem dla jednorazowego skanu; podanie zamiast tego istniejącej instancji TPDFContentStateTracker jest tym, co utrzymuje ciągłość stanu na stronie zbudowanej z więcej niż jednego strumienia zawartości, ponieważ ISO 32000-1 traktuje tablicę /Contents strony jako jeden logiczny strumień, a stos q/Q musi się zgadzać
Macierz tekstu przetrwa Q; stan grafiki nie
ISO 32000-1 §9.4.2 definiuje Td, TD, Tm i T* jako operatory budujące macierz tekstu i macierz linii tekstu wewnątrz bloku BT/ET, a PDFlibPas utrzymuje to rozróżnienie ostro: Td i TD konkatenują czystą translację do macierzy linii tekstu, T* robi to samo, używając ujemnego bieżącego wiodącego (leading), a tylko Tm zastępuje obie macierze wprost sześcioma podanymi liczbami. BT resetuje obie macierze do tożsamości, dokładnie raz, na początku obiektu tekstowego — ale q i Q wcale ich nie dotykają. TPDFContentStateTracker.Apply traktuje coRestoreState jako przypadek specjalny dokładnie z tego powodu: zanim zdejmie zapisany stan ze stosu, przechwytuje bieżącą macierz tekstu, macierz linii tekstu i flagę BT/ET, i ponownie je stosuje na tym, co akurat trzymał zdjęty stan, ponieważ para q/Q owinięta wokół przebiegu tekstu nie ma cofać pozycji tekstu
var
Tracker: TPDFContentStateTracker;
Prog: TPDFContentProgram;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
Tracker := TPDFContentStateTracker.Create;
try
Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
for I := 0 to Prog.Count - 1 do
begin
Tracker.Apply(Prog[I]);
if Prog[I].Op in [coShowText, coRestoreState] then
LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
end;
finally
Tracker.Free;
Prog.Free;
end;
end;
Uruchom tę sekwencję, a CTM zgłoszone przy drugim Tj wraca do skali tożsamości, jaką miało przed q — 2 0 0 2 0 0 cm wewnątrz pary zapisz/przywróć zniknęło, tak jak wymaga tego q/Q. TextMatrix.DX w tej samej instrukcji jednak wciąż wynosi 100: Td, które je ustawiło, uruchomiło się przed q, więc to nie stan grafiki, do którego Q kiedykolwiek miało prawo się dobrać, a narzędzie, które założyłoby inaczej, zgłosiłoby drugi przebieg glifów zaczynający się od niewłaściwej pozycji poziomej na stronie
Co się dzieje, gdy uruchamia się operator ścieżki przycinającej?
Operator W lub W* nie zmniejsza natychmiast przycinania; jedynie zapisuje, jakiej reguły wypełniania użyć, a faktyczne przecięcie czeka na dowolny operator malowania ścieżki, który po nim następuje, w tym operator no-op n, którego autorzy PDF rutynowo używają dokładnie po to, by przyciąć bez rysowania czegokolwiek. TPDFContentStateTracker odzwierciedla dokładnie ten dwustopniowy czas: coClip i coClipEvenOdd tylko ustawiają oczekującą flagę reguły przycinania, a EndCurrentPath — wywoływane przez każdy operator malowania ścieżki — to to, co faktycznie przecina granice oczekującej ścieżki do ClipMinX, ClipMinY, ClipMaxX i ClipMaxY. Poprawne ustawienie tego etapowania ma znaczenie dla samego kontraktu zrzutu przed/po: zrzut sprzed instrukcji wzięty dokładnie przy instrukcji W wciąż musi pokazywać stare, szersze przycinanie, ponieważ przycinanie jeszcze nie zaczęło obowiązywać w tym punkcie strumienia, a zwinięcie dwóch kroków w jeden po cichu zepsułoby każdego wywołującego polegającego na tym, że stan-przed znaczy to, co mówi
ClipBoundsExact mówi wywołującemu, na którą z dwóch sytuacji patrzy, i jest True tylko wtedy, gdy to pojedynczy prostokąt osiowo wyrównany zbudowany przez re na skądinąd pustej ścieżce — jedyny kształt, który PDFlibPas potrafi przedstawić dokładnie jako cztery liczby. Wszystko inne — obrócony prostokąt, zakrzywiony kontur, ścieżka złożona z kilkoma podścieżkami, czy przycinanie zbudowane z trybu renderowania tekstu — wciąż produkuje ClipMinX do ClipMaxY, ale z ClipBoundsExact wyczyszczonym na False, uczciwym sygnałem, że cztery liczby to bezpieczna granica zewnętrzna, a nie prawdziwy kształt przycinania; wywołujący, którzy potrzebują tylko tej granicy, tacy jak izolowanie prostokątnego podregionu przed konwersją półtonową GDI w dół opisaną w renderowaniu stron PDF do 1-bitowej monochromii, mogą odczytać ją bezpośrednio zamiast wyprowadzać ją ponownie z geometrii strony
Krzywe Béziera: granica dokładna albo bezpieczna
Najtańszym sposobem ograniczenia segmentu krzywej sześciennej Béziera jest wzięcie otoczki wypukłej jej czterech punktów kontrolnych, i to zawsze jest bezpieczne, ponieważ krzywa nigdy jej nie opuszcza — ale płytka, szeroka krzywa może zgłosić prostokąt otaczający znacznie większy niż obszar, który krzywa faktycznie zajmuje, co osłabia filtrowanie oparte na przycinaniu dokładnie wtedy, gdy ma to największe znaczenie, na dużych ścieżkach dekoracyjnych. PDFlibPas rozwiązuje zamiast tego ciaśniejszy problem: dla każdej osi rozwiązuje pochodną krzywej sześciennej dla pierwiastków wewnątrz otwartego przedziału (0, 1) i ocenia krzywą przy dowolnych znalezionych pierwiastkach, razem z obydwoma końcami, co jest standardowym sposobem w postaci zamkniętej na uzyskanie prawdziwego zasięgu osiowo wyrównanego krzywej, zamiast przeszacowania. Precyzja na krzywą nie przenosi się jednak na samo przycinanie: gdy tylko zakrzywiony kontur staje się ścieżką przycinającą, ClipBoundsExact wciąż spada do False dla niej, ponieważ prostokąt otaczający, choćby najciaśniejszy, wciąż nie jest tym samym kształtem co krzywa, którą otacza, a tracker stanu woli to powiedzieć, niż pozwolić wywołującemu założyć prostokąt tam, gdzie w rzeczywistości jest krzywa
Odczyt stanu przed i po każdym operatorze
To, czy wywołujący chce stanu przed czy po, zależy całkowicie od tego, co robi operator: pytanie rysunkowe lub test trafienia dotyczące ścieżki lub przebiegu tekstu chce stanu takiego, jaki był w chwili tuż przed uruchomieniem tego operatora, ponieważ to on faktycznie determinował, jak operator malował, podczas gdy pytanie diagnostyczne o operator ustawiający stan, taki jak gs, zwykle chce zobaczyć to, co on właśnie zmienił. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) udostępnia dokładnie ten wybór jako pojedynczą wartość logiczną, obliczając jeden TPDFContentGraphicsState na instrukcję w jednym liniowym przejściu przez cały program, niezależnie od tego, która chwila jest żądana. GetGraphicsState(InstructionIndex, AfterInstruction, State) oferuje ten sam wybór przed/po dla pojedynczej instrukcji zamiast całego programu, ale odtwarza od instrukcji zero przy każdym wywołaniu, aby tam dotrzeć, więc skanowanie wielu indeksów przez wywoływanie jej w pętli kosztuje O(n²) wobec pojedynczego wywołania O(n) TraceGraphicsStates na tym samym programie
var
Before, After: TPDFContentGraphicsState;
begin
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
end;
Życie z zniekształconymi strumieniami zawartości
Dwa rodzaje zniekształconych danych wejściowych są na tyle powszechne u rzeczywistych producentów PDF, że TPDFContentStateTracker musi je tolerować, a nie na nich zawodzić. Pierwszy to ścieżka rozciągająca się przez granicę q/Q: bieżąca ścieżka, bieżący punkt i liczba podścieżek nie są parametrami stanu grafiki — ISO 32000-1 §8.4 obejmuje to, co q i Q zapisują i przywracają, a budowana bieżąca ścieżka nie jest tego częścią — więc TPDFContentStateTracker śledzi te dane całkowicie poza zapisanym stanem, a podścieżka rozpoczęta przed q wciąż tam jest, niepomalowana, natychmiast po pasującym Q. Drugi to gołe Q bez żadnego pasującego q gdziekolwiek wcześniej w strumieniu, niereadkie w wyniku z generatorów, które składają fragmenty strumienia zawartości przez konkatenację i psują księgowanie. TPDFContentStateTracker.RestoreUnderflowCount liczy każde takie zdarzenie zamiast zgłaszać wyjątek albo uszkadzać stan: niepasujące Q po prostu pozostawia bieżący stan grafiki dokładnie takim, jaki był, jakby ta instrukcja była no-opem, więc reszta strumienia dalej odtwarza się na rozsądnym stanie, a wywołujący może później zdecydować, na podstawie tej liczby, czy wejście warte jest zgłoszenia z powrotem temu, kto je wygenerował
Kompozycja CTM, niezależność macierzy tekstu od q/Q i etapowana realizacja ścieżki przycinającej nie zależą od tego, jak ani czy strumień zawartości zostanie kiedykolwiek pomalowany, co jest dokładnie clou: ten sam zrzut TPDFContentStateTracker jest poprawny niezależnie od tego, czy strona nigdy nie jest renderowana, czy właśnie ma zostać przekazana dowolnemu backendowi, który PDFlibPas wybierze dla tego pliku, w tym przełączanie silnika w czasie działania omówione w przewodniku po renderowaniu PDF w wielu silnikach w PDFlibPas. Analiza zawartości, mapowanie współrzędnych i narzędzia redakcji mogą wszystkie działać całkowicie na wyjściu trackera, długo przed lub całkowicie bez proszenia kiedykolwiek renderera o zaangażowanie się
Odtwarzanie strumienia zawartości przez TPDFContentStateTracker jest częścią frameworka strukturalnej edycji zawartości wbudowanego w PDFlibPas, natywną bibliotekę komponentu VCL PDF dla Delphi i C++Buildera