PDFlibPas, library komponen PDF VCL native untuk Delphi dan C++Builder, memutar ulang content stream sebuah halaman lewat kelas TPDFContentStateTracker-nya tanpa menyentuh sebuah canvas rendering sama sekali. Memberi makan tracker satu operator ter-parse pada satu waktu menjaga sebuah record graphics-state yang berjalan — current transformation matrix, text matrix, batas clip, dan stack save q/Q — tersedia untuk snapshot sebelum atau sesudah setiap operator dieksekusi
Tanyakan di mana sebenarnya sebuah run teks mendarat pada halaman yang tercetak, dan angka mentah content stream saja akan menyesatkan Anda setiap saat. TPDFContentProgram.GetTextRuns sudah melaporkan titik anchor setiap instruksi penampil-teks lewat field OriginX dan OriginY pada TPDFTextRun, dan komentar field itu eksplisit bahwa titik ini berada dalam text space, sudah dilipat lewat Tm, Td, TD, dan T*. Yang masih hilang, dan yang menurut komentar itu harus disediakan seorang pemanggil, adalah CTM yang aktif pada instruksi eksak itu — hasil kali setiap cm yang dirangkai sejauh ini, bersarang di dalam berapa pun pasangan q/Q yang kebetulan terbuka pada titik itu dalam stream
Mengapa memutar ulang sebuah content stream alih-alih merendernya?
PDFlibPas menjaga dua notasi graphics state terpisah untuk dua pekerjaan terpisah, dan pemisahan itu disengaja. State internal renderer membawa sebuah handle device-canvas hidup, sebuah handle region-clipping, dan cache font-rasterisasi — resource sungguhan yang terikat ke permukaan apa pun yang sedang dicat, dan tidak berarti apa-apa begitu permukaan itu pergi. TPDFContentGraphicsState tidak membawa apa pun dari itu: ia adalah record polos yang terbatas pada nilai yang didefinisikan ISO 32000-1 §8.4 sebagai bisa dijangkau dari operator content-stream saja — CTM, gaya garis, warna, text state, dan batas clip dan path turunan. Karena record itu tidak memegang referensi canvas dan tidak ada file handle terbuka, seorang pemanggil bisa mem-parse sebuah content stream, menelusurinya dengan TPDFContentStateTracker, dan terus menggunakan snapshot hasilnya lama setelah apa pun yang menghasilkan byte tersebut pergi
Bagaimana TPDFContentStateTracker membangun CTM
TPDFContentStateTracker.Apply merangkai enam operand sebuah operator cm ke dalam CTM tracker menggunakan premultiply yang sama yang dispesifikasikan PDF sendiri: matriks baru M2 dikombinasikan dengan CTM saat ini sebagai M2 × CTM, dalam konvensi row-vector di mana sebuah titik ditransformasi sebagai P′ = P × M (ISO 32000-1 §8.4). Bagian yang mudah salah berada dalam term translasi, bukan bagian linear: translasi milik M2 sendiri harus melewati komponen rotasi-dan-skala CTM saat ini sebelum translasi CTM saat ini ditambahkan di atasnya. Lewati langkah itu dan hard-code sebuah kombinasi komponen-demi-komponen naif sebagai gantinya, dan cm terisolasi pertama yang Anda uji akan terlihat benar sementara setiap koordinat di hilir sebuah cm bersarang kedua atau ketiga diam-diam melenceng, yang persis merupakan jenis bug yang bertahan dari code review karena unit test yang akan menangkapnya membutuhkan setidaknya dua transform berantai untuk gagal
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;
Loop di atas menjawab titik masalah dari pembukaan: TPDFContentProgram.GetTextRuns menyerahkan kembali OriginX dan OriginY yang sudah dilipat lewat Tm, Td, TD, dan T*, dan TraceGraphicsStates(nil, False) menyediakan satu bagian yang tersisa, CTM sebelum-instruksi pada indeks eksak tempat setiap run ditangkap, dalam satu lintasan linear tunggal di atas seluruh program. Menyerahkan nil membiarkan metode itu memiliki sebuah tracker privat untuk pemanggilan itu dan membebaskannya secara internal, yang merupakan pilihan tepat untuk sebuah pemindaian sekali-pakai; menyerahkan sebuah instance TPDFContentStateTracker yang sudah ada sebagai gantinya adalah yang menjaga state tetap berkelanjutan di seluruh sebuah halaman yang dirakit dari lebih dari satu content stream, karena ISO 32000-1 memperlakukan array /Contents sebuah halaman sebagai satu stream logis dan stack q/Q harus sepakat
Text matrix bertahan dari Q; graphics state tidak
ISO 32000-1 §9.4.2 mendefinisikan Td, TD, Tm, dan T* sebagai operator yang membangun text matrix dan text line matrix di dalam sebuah blok BT/ET, dan PDFlibPas menjaga perbedaan itu tajam: Td dan TD merangkai sebuah translasi murni ke text line matrix, T* melakukan hal yang sama menggunakan negatif dari leading saat ini, dan hanya Tm yang mengganti kedua matriks sepenuhnya dengan enam angka yang diberikan padanya. BT mereset kedua matriks ke identity, tepat satu kali, di awal objek teks — tetapi q dan Q sama sekali tidak menyentuhnya. TPDFContentStateTracker.Apply mengistimewakan coRestoreState persis karena alasan ini: sebelum ia mem-pop state tersimpan dari stack, ia menangkap text matrix, text line matrix, dan flag BT/ET saat ini, dan menerapkannya kembali di atas apa pun yang kebetulan dipegang state yang di-pop itu, karena sebuah pasangan q/Q yang membungkus sebuah run teks tidak seharusnya memindahkan posisi teks kembali
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;
Jalankan urutan itu dan CTM yang dilaporkan pada Tj kedua kembali ke skala identity yang dimilikinya sebelum q — 2 0 0 2 0 0 cm di dalam pasangan save/restore sudah hilang, sebagaimana diwajibkan q/Q. TextMatrix.DX pada instruksi yang sama, meski begitu, tetap 100: Td yang mengaturnya berjalan sebelum q, sehingga itu bukan graphics state yang pernah berhak disentuh Q, dan sebuah tool yang mengasumsikan sebaliknya akan melaporkan run glyph kedua dimulai dari posisi horizontal yang salah pada halaman
Apa yang terjadi ketika sebuah operator clipping path berjalan?
Sebuah operator W atau W* tidak langsung menciutkan clip; ia hanya mencatat aturan fill mana yang digunakan, dan intersection sesungguhnya menunggu operator path-painting apa pun yang mengikutinya, termasuk painter no-op n yang secara rutin digunakan penulis PDF persis untuk melakukan clip tanpa menggambar apa pun. TPDFContentStateTracker mencerminkan timing dua-langkah itu secara persis: coClip dan coClipEvenOdd hanya mengatur sebuah flag pending clip-rule, dan EndCurrentPath — dipanggil oleh setiap operator path-painting — adalah yang benar-benar mengintersect batas path yang pending ke dalam ClipMinX, ClipMinY, ClipMaxX, dan ClipMaxY. Membuat staging ini benar penting untuk kontrak snapshot sebelum/sesudah itu sendiri: sebuah snapshot sebelum yang diambil tepat pada instruksi W tetap harus menunjukkan clip lama yang lebih lebar, karena clip itu belum berlaku pada titik itu dalam stream, dan menciutkan kedua langkah menjadi satu akan diam-diam merusak setiap pemanggil yang mengandalkan before-state untuk berarti apa yang dikatakannya
ClipBoundsExact memberi tahu seorang pemanggil situasi mana dari dua yang sedang dilihatnya, dan itu hanya pernah True untuk satu rectangle sejajar-sumbu tunggal yang dibangun re pada sebuah path yang sebaliknya kosong — bentuk tunggal yang bisa direpresentasikan PDFlibPas persis sebagai empat angka. Segala sesuatu yang lain — sebuah rectangle yang diputar, sebuah outline melengkung, sebuah compound path dengan beberapa subpath, atau sebuah clip yang dibangun dari sebuah text-rendering mode — tetap menghasilkan ClipMinX hingga ClipMaxY, tetapi dengan ClipBoundsExact dikosongkan ke False, sebuah sinyal jujur bahwa keempat angka itu adalah batas luar yang aman dan bukan bentuk clip sesungguhnya; pemanggil yang hanya membutuhkan batas itu, seperti mengisolasi sebuah sub-region persegi panjang sebelum konversi-turun halftone GDI yang dijelaskan di merender halaman PDF ke monokrom 1-bit, bisa membacanya langsung alih-alih menurunkannya ulang dari geometri halaman
Kurva Bézier: sebuah batas eksak atau yang aman
Cara termurah membatasi sebuah segmen Bézier kubik adalah mengambil convex hull dari empat titik kontrolnya, dan itu selalu aman karena kurva tersebut tidak pernah meninggalkannya — tetapi sebuah kurva dangkal dan lebar bisa melaporkan sebuah bounding box jauh lebih besar dari yang sebenarnya diduduki kurva itu, yang melemahkan filtering berbasis-clip persis ketika paling penting, pada path dekoratif besar. PDFlibPas menyelesaikan masalah yang lebih ketat sebagai gantinya: untuk setiap sumbu, ia menyelesaikan turunan kurva kubik tersebut untuk akar di dalam interval terbuka (0, 1) dan mengevaluasi kurva pada akar mana pun yang ditemukannya, bersama kedua endpoint, yang merupakan cara bentuk-tertutup standar untuk mendapatkan extent sejajar-sumbu sesungguhnya sebuah kurva alih-alih sebuah overestimate. Presisi per-kurva tidak berlanjut ke clip itu sendiri, meski begitu: begitu sebuah outline melengkung menjadi sebuah clipping path, ClipBoundsExact tetap jatuh ke False untuknya, karena sebuah bounding box, betapapun ketatnya, tetap bukan bentuk yang sama dengan kurva yang dibatasinya, dan state tracker itu lebih suka mengatakan begitu daripada membiarkan seorang pemanggil mengasumsikan sebuah rectangle di mana sebenarnya ada sebuah kurva
Membaca state sebelum dan sesudah setiap operator
Apakah seorang pemanggil menginginkan state sebelum atau sesudah sepenuhnya bergantung pada apa yang dilakukan operator itu: sebuah pertanyaan penggambaran atau hit-testing tentang sebuah path atau run teks menginginkan state sebagaimana adanya persis sebelum operator itu berjalan, karena itulah yang sebenarnya menentukan bagaimana operator itu mengecat, sementara sebuah pertanyaan diagnostik tentang sebuah operator pengatur-state seperti gs biasanya menginginkan melihat apa yang baru saja diubahnya. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) mengekspos persis pilihan itu sebagai sebuah Boolean tunggal, menghitung satu TPDFContentGraphicsState per instruksi dalam satu lintasan linear di atas seluruh program terlepas dari instan mana yang diminta. GetGraphicsState(InstructionIndex, AfterInstruction, State) menawarkan pilihan before/after yang sama untuk satu instruksi tunggal alih-alih seluruh program, tetapi ia memutar ulang dari instruksi nol pada setiap pemanggilan untuk sampai ke sana, sehingga memindai banyak indeks dengan memanggilnya dalam sebuah loop menelan biaya O(n²) dibanding satu pemanggilan O(n) tunggal ke TraceGraphicsStates di atas program yang sama
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;
Hidup dengan content stream yang cacat
Dua jenis input cacat cukup umum pada produser PDF sungguhan sehingga TPDFContentStateTracker harus mentolerirnya alih-alih gagal karenanya. Yang pertama adalah sebuah path yang membentang melintasi sebuah batas q/Q: path saat ini, titik saat ini, dan hitungan subpath bukan parameter graphics-state — ISO 32000-1 §8.4 membahas apa yang disimpan dan dipulihkan q dan Q, dan path yang sedang dibangun saat ini tidak termasuk di dalamnya — sehingga TPDFContentStateTracker melacak data itu sepenuhnya di luar state tersimpan, dan sebuah subpath yang dimulai sebelum sebuah q tetap ada di sana, tidak dicat, segera setelah Q yang cocok. Yang kedua adalah sebuah Q telanjang tanpa q yang cocok di mana pun sebelumnya dalam stream, tidak jarang dalam output dari generator yang merakit fragmen content-stream lewat concatenation dan salah dalam pembukuannya. TPDFContentStateTracker.RestoreUnderflowCount menghitung setiap satu dari event itu alih-alih memunculkan sebuah exception atau merusak state: sebuah Q yang tidak cocok sekadar meninggalkan graphics state saat ini persis seperti adanya, seolah instruksi itu adalah sebuah no-op, sehingga sisa stream terus diputar ulang pada sebuah state yang masuk akal dan seorang pemanggil masih bisa memutuskan setelahnya, dari hitungan itu, apakah input itu layak ditandai kembali kepada siapa pun yang menghasilkannya
Komposisi CTM, independensi text matrix dari q/Q, dan realisasi bertahap sebuah clipping path tidak bergantung pada bagaimana atau apakah content stream itu pernah dicat, yang persis merupakan intinya: snapshot TPDFContentStateTracker yang sama benar baik halaman itu tidak pernah dirender sama sekali atau akan diserahkan ke back end mana pun yang dipilih PDFlibPas untuk file itu, termasuk pergantian mesin runtime yang dibahas di panduan rendering PDF multi-mesin di PDFlibPas. Analisis konten, pemetaan koordinat, dan tooling redaksi semuanya bisa berjalan sepenuhnya di atas output tracker, jauh sebelum atau sepenuhnya tanpa pernah meminta sebuah renderer terlibat
Putar ulang content-stream lewat TPDFContentStateTracker adalah bagian dari framework editing konten terstruktur yang dibangun ke dalam PDFlibPas, library komponen PDF VCL native untuk Delphi dan C++Builder