PDFlibPas menyelesaikan pemanggilan Form XObject rekursif dalam content stream PDF Delphi dengan melacak rantai pemanggilan aktif, bukan sebuah set visited global, sehingga TPDFlib.EnumPageContentStatesEx bisa menelusuri Form yang sama yang dipanggil beberapa kali pada satu halaman tanpa salah mengira penggunaan-ulang yang sah sebagai sebuah siklus. Sebuah Form XObject stempel dalam sebuah template invoice adalah kasus tipikal: objek yang sama dipanggil dari header, footer, dan sebuah lapisan watermark pada satu halaman, dan hanya sebuah rantai pemanggilan yang berputar kembali ke dirinya sendiri yang merupakan siklus sejati
ISO 32000-1 §8.10 mendefinisikan sebuah Form XObject sebagai sebuah content stream mandiri yang dipanggil sebuah halaman, atau Form lain, dengan operator Do, lengkap dengan sistem koordinatnya sendiri di /Matrix, sebuah batas clipping dalam sistem koordinat itu di /BBox, dan secara opsional dictionary resource-nya sendiri. Tidak ada apa pun dalam spesifikasi yang membatasi berapa kali satu Form bisa dipanggil atau seberapa dalam Form bisa saling memanggil, sehingga sebuah parser yang sesuai standar harus menerima penggunaan-ulang yang sah dan nesting yang sah sambil tetap mempertahankan dirinya terhadap satu pengaturan yang memang dilarang spesifikasi: sebuah Form yang content stream-nya, secara langsung atau transitif, memanggil dirinya sendiri. PDFlibPas melaporkan perbedaan itu lewat nilai TPDFlibContentFormTraversalStatus yang dilampirkan pada setiap snapshot Do, paling menonjol ftsEnumerated untuk sebuah penurunan yang berhasil dan ftsCycle untuk satu kasus yang benar-benar sebuah loop
Mengapa Menggunakan Ulang Form XObject yang Sama Tidak Memicu Siklus Palsu?
Sebuah referensi Form XObject yang berulang bukan, dengan sendirinya, bukti apa pun yang salah. ISO 32000-1 mengizinkan objek Form yang sama dipanggil dari sebanyak apa pun tempat dalam sebuah content stream yang diinginkan penulis, yang persis merupakan cara sebuah stempel logo, sebuah template kop surat, atau sebuah footer nomor-halaman digunakan ulang di seluruh halaman tanpa menduplikasi content stream-nya berkali-kali. Penjaga naif terhadap rekursi tak terkendali adalah sebuah set visited tunggal yang dikunci berdasarkan nomor objek: kali pertama sebuah walker melihat objek Form 12, ia menandai 12 sebagai terlihat dan menolak memasukinya lagi di mana pun lagi dalam pohon tersebut. Pendekatan itu rusak begitu stempel yang sama muncul di dua sudut yang tidak berkaitan pada satu halaman, karena pemanggilan kedua yang sepenuhnya sah tiba setelah nomor objek itu sudah ditandai terlihat dan ditolak seolah itu sebuah loop
PDFlibPas menghindari false positive itu dengan men-scope deteksi siklus ke rantai pemanggilan saat ini alih-alih seluruh dokumen. EnumPageContentStatesEx mendorong stream Form yang terselesaikan ke rantai pemanggilan aktif tepat sebelum turun ke dalamnya, lalu mem-pop entri yang sama itu keluar lagi begitu penurunan itu kembali, berhasil atau tidak. Sebuah pemanggilan sibling atas stream identik hanya dimulai setelah yang pertama sudah di-pop, sehingga rantai pemanggilan itu bersih dari stream tersebut pada saat pemanggilan sibling memeriksanya, dan walker itu meng-enumerasinya persis seperti Form lain mana pun. Sebuah siklus sejati terlihat berbeda pada rantai yang sama itu: Form A memanggil Form B, B masih terbuka pada rantai ketika kontennya sendiri memanggil kembali ke A, dan A masih duduk pada rantai dari pemanggilan luar yang belum kembali — itulah satu-satunya bentuk yang dilaporkan ftsCycle, sebuah stream Form yang masih terbuka di suatu tempat lebih awal pada rantai pemanggilan saat ini, bukan sekadar hadir di suatu tempat lain pada halaman tersebut
Seberapa Dalam Rekursi Form XObject Bisa Berjalan Sebelum PDFlibPas Menghentikannya?
Deteksi siklus dan pembatasan kedalaman menyelesaikan dua masalah berbeda, dan PDFlibPas menjaga keduanya sebagai dua hasil TPDFlibContentFormTraversalStatus berbeda persis karena alasan itu. Sebuah rantai dua puluh Form berbeda, masing-masing memanggil berikutnya dan tidak satu pun berulang, bukan sebuah siklus menurut definisi mana pun — pemeriksaan rantai-aktif tidak pernah menemukan sebuah stream yang berulang — tetapi dua puluh level nesting yang jujur tetap dua puluh level parsing, konkatenasi matriks, dan penyelesaian resource yang bisa didorong sebuah PDF cacat atau adversarial secara sewenang-wenang lebih tinggi jika tidak ada apa pun lain yang menghentikannya. EnumPageContentStatesEx menerima sebuah parameter MaxFormDepth persis karena alasan ini dan menjepit nilai apa pun yang diserahkan ke maksimum 64, terlepas dari apa yang diminta pemanggil. Sebuah kedalaman nol adalah sebuah kasus khusus yang layak diketahui sendiri: itu menonaktifkan rekursi Form sepenuhnya dan mereproduksi perilaku datar, halaman-saja dari metode EnumPageContentStates yang lebih lama, itulah sebabnya setiap snapshot Do dalam mode itu melaporkan ftsNotRequested alih-alih mencoba apa pun
var
Lib: TPDFlib;
States: array of TPDFlibContentGraphicsState;
Count, I: Integer;
begin
Lib:= TPDFlib.Create;
try
if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
Exit;
Lib.SelectPage(1);
Count:= Lib.EnumPageContentStatesEx(True, 8, States); // count only
SetLength(States, Count);
Lib.EnumPageContentStatesEx(True, 8, States); // fill
for I:= 0 to Count- 1 do
if States[I].FormTraversalStatus= ftsCycle then
LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
finally
Lib.Free;
end;
end;
Satu Sub-Tracker Per Pemanggilan: Mengisolasi Graphics State
Setiap penurunan ke dalam sebuah Form XObject mendapat tracker graphics-state-nya sendiri alih-alih berbagi yang sudah menelusuri halaman, karena content stream sebuah Form diwajibkan meninggalkan graphics state persis seperti ditemukannya, dan PDFlibPas tidak bisa mengasumsikan setiap PDF yang dibukanya benar-benar menghormati persyaratan itu. Tracker anak dimulai dari sebuah snapshot CTM, state warna, dan parameter teks apa pun yang aktif pada instruksi Do pemanggil, lalu mereset stack save-and-restore-nya sendiri dan pelacakan current-path menjadi kosong sebelum mengeksekusi satu instruksi pun dari Form tersebut. Sebuah q yang tidak seimbang tanpa Q yang cocok di dalam sebuah Form yang ceroboh atau rusak, bukan hal yang jarang ditemukan dalam PDF yang diproduksi tooling lebih lama, tetap terkurung di dalam tracker satu pemanggilan itu dan tidak pernah bocor ke tracker halaman atau ke sebuah pemanggilan sibling dari stempel yang sama itu yang duduk satu baris kemudian dalam content stream
/Matrix Form dikombinasikan dengan CTM yang berlaku pada Do dengan cara yang sama seperti operator cm, dikalikan-kiri terhadap transform saat ini alih-alih menggantikannya, dan PDFlibPas secara sengaja menggunakan kembali satu jalur kode itu alih-alih memelihara sebuah rumus kedua, karena dua implementasi independen dari aljabar matriks yang sama persis merupakan jenis duplikasi yang diam-diam melenceng setelah beberapa ronde komposisi skala, rotasi, dan shear. /BBox kemudian melakukan clip dalam ruang koordinat Form itu sendiri setelah matriks sudah diterapkan, dan keempat sudut kotak itu ditransformasi secara individual alih-alih hanya sudut yang berlawanan, karena sebuah Form yang diputar atau di-shear jika tidak bisa melaporkan sebuah bounding box yang melewatkan konten sungguhan yang duduk di apa yang dulunya sebuah sudut ekstrem sebelum transform memindahkannya ke tempat lain. Memperluas loop dari contoh sebelumnya di atas array States yang sama membaca field-field itu secara langsung
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
States[I].FormBBoxKnown then
Writeln('Form ', States[I].XObjectResource, ' matrix ',
States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);
Apakah Dua Form dengan Nama Resource yang Sama Berbagi Satu Font?
Tidak. Sebuah nama resource seperti /F1 hanya berarti sesuatu relatif terhadap dictionary resource yang aktif pada titik penggunaannya, dan dua Form XObject yang berbeda bebas mendefinisikan dua font yang sama sekali berbeda di bawah nama identik itu. PDFlibPas menyelesaikan ini dengan melacak sebuah scope resource berdampingan dengan setiap nama resource: ketika sebuah Form membawa dictionary /Resources-nya sendiri, dictionary itu menjadi scope resource lengkap untuk segala sesuatu di dalamnya, tanpa fallback per-key ke dictionary halaman atau pemanggil untuk apa pun yang kebetulan dihilangkan dictionary Form itu sendiri. Hanya sebuah Form tanpa key /Resources sama sekali, sebuah pola yang masih dihasilkan beberapa generator PDF yang lebih lama, yang mewarisi dictionary pemanggil secara utuh, dan itu adalah sebuah pengecualian kompatibilitas yang disengaja alih-alih sebuah aturan umum yang layak diandalkan dalam output baru. Identitas font dalam sebuah snapshot TPDFlibContentGraphicsState karenanya adalah pasangan FontResource dan FontResourceScope, bukan namanya saja, dengan FontObjectNumber tersedia untuk mengonfirmasi persis objek indirect mana yang diselesaikan sebuah /F1 tertentu dalam scope itu secara spesifik
Scoping yang sama berlaku pada setiap resource bernama lain yang bisa dibawa sebuah Form, entry ExtGState dan entry XObject bersarang termasuk, karena mekanisme penyelesaian yang mendasarinya tidak mengistimewakan font — kasus font kebetulan paling penting, karena sebuah identitas font yang tidak cocok diam-diam menghasilkan glyph yang salah alih-alih sebuah kegagalan yang jelas. Kode ekstraksi yang mengelompokkan run teks hanya berdasarkan nama font, tanpa juga mengelompokkan berdasarkan scope resource, akan menggabungkan dua font yang secara visual berbeda yang kebetulan berbagi sebuah nama, dan kesalahan itu tidak akan muncul sampai seseorang memperhatikan angka dari typeface yang salah duduk di dalam apa yang seharusnya terbaca sebagai satu font yang konsisten
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
States[I].FontObjectNumber, States[I].ContentDepth);
Membaca FormTraversalStatus dalam Pipeline Anda Sendiri
FormTraversalStatus mengubah setiap snapshot Do menjadi sebuah laporan diagnostik kecil dengan sendirinya, dan sebuah pipeline yang mengabaikannya membuang persis informasi yang akan menjelaskan sebuah ekstraksi yang tidak lengkap. ftsNotApplicable berarti instruksi itu tidak pernah sebuah pemanggilan Form yang terselesaikan sejak awal; ftsNotRequested berarti rekursi dimatikan untuk pemanggilan ini; ftsEnumerated berarti Form itu di-parse dan ditelusuri dengan berhasil; ftsDepthLimit dan ftsCycle menandai dua cara sebuah penurunan dipotong dengan sengaja; dan ftsMalformed mencakup segala sesuatu lainnya yang menghentikan penelusuran itu — sebuah referensi stream yang tidak bisa diselesaikan, sebuah /Matrix atau /BBox yang gagal di-parse, atau sebuah exception yang dimunculkan selagi mengeksekusi konten Form itu sendiri. Kasus terakhir itu penting secara operasional, karena sebuah penelusuran bersarang yang gagal mengembalikan apa pun output parsial yang sudah dihasilkannya untuk cabang itu, sehingga seorang pemanggil tidak pernah perlu menebak apakah sebuah Form benar-benar kosong atau sekadar meledak dua instruksi ke dalam content stream-nya
var
Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
Status: TPDFlibContentFormTraversalStatus;
begin
for Status:= Low(Tally) to High(Tally) do
Tally[Status]:= 0;
for I:= 0 to Count- 1 do
Inc(Tally[States[I].FormTraversalStatus]);
if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;
Batasan, Biaya, dan Di Mana Ini Cocok
Content stream sebuah Form didekode dan di-parse persis satu kali per pemanggilan enumerasi tidak peduli berapa kali Form itu dipanggil, karena PDFlibPas meng-cache daftar instruksi ter-parse terhadap objek stream yang mendasarinya alih-alih mem-parse ulang pada setiap pemanggilan sibling — stempel tiga-sudut dari contoh pembuka didekode sekali dan ditelusuri tiga kali, bukan didekode tiga kali. Yang memang dibangun ulang pada setiap pemanggilan tunggal adalah segala sesuatu yang secara sah berbeda antara satu titik pemanggilan dan berikutnya: tracker anak, CTM yang dirangkai, clip yang diintersect, dan scope resource. Pembukuan CTM dan clip per-pemanggilan itu adalah mesin yang sama di balik tracker state CTM dan clipping content-stream milik PDFlibPas, layak dibaca berdampingan dengan yang ini untuk penelusuran content-stream apa pun yang melampaui rekursi Form itu sendiri
Dua batas layak menetapkan ekspektasi sebelum API ini masuk ke sebuah pipeline yang lebih besar. Batas atas kedalaman 64-level bukan sebuah kenop tuning untuk dokumen yang secara sah dalam, karena invoice, statement, dan template laporan sungguhan pada dasarnya tidak pernah membungkus Form lebih dari tiga atau empat level dalam — sebuah dokumen yang benar-benar menabrak ftsDepthLimit jauh lebih mungkin cacat atau adversarial daripada tidak biasa rumit, dan layak dicatat sebagai sebuah sinyal kualitas-data alih-alih diam-diam dicoba ulang dengan angka lebih besar. EnumPageContentStatesEx juga sebuah API analisis sisi-baca: ia melaporkan apa yang dilakukan sebuah content stream, bukan apakah sebuah Form seharusnya terlihat sama sekali, yang merupakan pertanyaan terpisah yang dijawab state visibilitas Optional Content Group ketika sebuah Form stempel atau watermark duduk di belakang sebuah layer yang mungkin sudah dimatikan sebuah viewer. Deteksi siklus rantai-pemanggilan, isolasi per-pemanggilan, dan resource scoping bersama-sama membentuk satu sudut permukaan inspeksi content-stream dalam komponen PDFlibPas untuk Delphi dan C++Builder