PDFlibPas ไลบรารีคอมโพเนนต์ PDF แบบ VCL เนทีฟสำหรับ Delphi และ C++Builder เล่นซ้ำ content stream ของหน้าผ่านคลาส TPDFContentStateTracker ของมัน โดยไม่แตะ rendering canvas เลย การป้อน operator ที่ parse แล้วให้ตัว tracker ทีละตัวจะรักษา record graphics-state ที่กำลังทำงานอยู่ ได้แก่ current transformation matrix, text matrix, ขอบเขต clip และ save stack q/Q ให้พร้อมสำหรับการ snapshot ก่อนหรือหลังทุก operator ที่ทำงาน
ลองถามว่าข้อความบรรทัดหนึ่งลงเอยที่ตำแหน่งไหนจริงๆ บนหน้าที่พิมพ์ออกมา แล้วตัวเลขดิบของ content stream เพียงอย่างเดียวจะทำให้คุณเข้าใจผิดทุกครั้ง TPDFContentProgram.GetTextRuns รายงานจุด anchor ของแต่ละคำสั่งแสดงข้อความอยู่แล้วผ่านฟิลด์ OriginX และ OriginY บน TPDFTextRun และ comment ของฟิลด์ระบุชัดว่าจุดนี้อยู่ใน text space ที่พับผ่าน Tm, Td, TD และ T* แล้ว สิ่งที่ยังขาดอยู่ และสิ่งที่ comment เหล่านั้นบอกว่าผู้เรียกต้องจัดหาเอง คือ CTM ที่ทำงานอยู่ ณ คำสั่งนั้นเป๊ะ ผลคูณของทุก cm ที่ concatenate ไปแล้วจนถึงตอนนั้น ซ้อนอยู่ภายในคู่ q/Q กี่คู่ก็ตามที่บังเอิญเปิดอยู่ ณ จุดนั้นในสตรีม
ทำไมต้องเล่นซ้ำ content stream แทนที่จะ render มัน
PDFlibPas เก็บแนวคิดของ graphics state แยกกันสองแบบสำหรับงานที่แยกกันสองอย่าง และการแยกนี้จงใจ state record ภายในของ renderer พก device-canvas handle ที่มีชีวิต, clipping-region handle และ font-rasterisation cache ซึ่งเป็น resource จริงที่ผูกกับพื้นผิวใดก็ตามที่กำลังถูกวาดอยู่ในขณะนั้น และไม่มีความหมายเลยเมื่อพื้นผิวนั้นหายไป TPDFContentGraphicsState ไม่พกอะไรแบบนั้นเลย มันเป็น record ธรรมดาที่จำกัดอยู่แค่ค่าที่ ISO 32000-1 §8.4 นิยามว่าเข้าถึงได้จาก content-stream operator เพียงอย่างเดียว คือ CTM, สไตล์เส้น, สี, text state และขอบเขต clip กับ path ที่ได้มา เพราะ record นี้ไม่ถือ canvas reference และไม่มี file handle ที่เปิดอยู่ ผู้เรียกจึงสามารถ parse content stream, เดินผ่านมันด้วย TPDFContentStateTracker และใช้ snapshot ที่ได้ต่อไปได้นานหลังจากอะไรก็ตามที่สร้างไบต์เหล่านั้นหายไปแล้ว
TPDFContentStateTracker สร้าง CTM อย่างไร
TPDFContentStateTracker.Apply concatenate operand ทั้งหกของ operator cm เข้าไปใน CTM ของ tracker ด้วยการ premultiply เดียวกับที่ PDF เองระบุไว้ matrix ใหม่ M2 รวมกับ CTM ปัจจุบันเป็น M2 × CTM ในข้อตกลง row-vector ที่จุดหนึ่งแปลงเป็น P′ = P × M (ISO 32000-1 §8.4) ส่วนที่ผิดได้ง่ายอยู่ที่ term การเลื่อนตำแหน่ง ไม่ใช่ส่วนเชิงเส้น การเลื่อนตำแหน่งของ M2 เองต้องผ่าน component การหมุน-และ-สเกลของ CTM ปัจจุบันก่อนที่การเลื่อนตำแหน่งของ CTM ปัจจุบันจะถูกบวกเข้าไปทับ ข้ามขั้นตอนนั้นไปและ hard-code การรวมแบบ component-wise ไร้เดียงสาแทน แล้ว cm เดี่ยวตัวแรกที่คุณทดสอบจะดูถูกต้อง ในขณะที่ทุกพิกัดที่อยู่ปลายทางของ cm ที่ซ้อนกันตัวที่สองหรือสามเลื่อนไปอย่างเงียบๆ ซึ่งเป็นบั๊กประเภทพอดีที่รอดผ่าน code review ได้ เพราะ unit test ที่จะจับมันต้องการ transform ที่เชื่อมกันอย่างน้อยสองตัวถึงจะล้มเหลว
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 ข้างบนตอบจุดปวดหัวจากตอนเปิดเรื่อง TPDFContentProgram.GetTextRuns ส่ง OriginX และ OriginY ที่พับผ่าน Tm, Td, TD และ T* แล้วกลับมา และ TraceGraphicsStates(nil, False) จัดหาชิ้นส่วนที่เหลืออีกชิ้น คือ CTM ก่อน-คำสั่งที่ดัชนีตรงที่แต่ละ run ถูกจับไว้ ในรอบเชิงเส้นเดียวข้ามทั้งโปรแกรม การส่ง nil ให้ method เป็นเจ้าของ tracker ส่วนตัวสำหรับการเรียกนั้นและ free มันภายใน ซึ่งเป็นตัวเลือกที่ถูกต้องสำหรับการสแกนครั้งเดียว การส่ง instance TPDFContentStateTracker ที่มีอยู่แล้วแทนคือสิ่งที่รักษา state ให้ต่อเนื่องข้ามหน้าที่ประกอบจาก content stream มากกว่าหนึ่งตัว เพราะ ISO 32000-1 ปฏิบัติต่อ array /Contents ของหน้าเป็นสตรีมเชิงตรรกะเดียว และ stack q/Q ต้องตรงกัน
text matrix อยู่รอดผ่าน Q; graphics state ไม่
ISO 32000-1 §9.4.2 นิยาม Td, TD, Tm และ T* เป็น operator ที่สร้าง text matrix และ text line matrix ภายใน block BT/ET และ PDFlibPas รักษาความแตกต่างนั้นไว้อย่างชัดเจน Td และ TD concatenate การเลื่อนตำแหน่งล้วนๆ เข้ากับ text line matrix, T* ทำแบบเดียวกันโดยใช้ค่าลบของ leading ปัจจุบัน และมีแค่ Tm เท่านั้นที่แทนที่ทั้งสอง matrix ตรงๆ ด้วยตัวเลขหกตัวที่ได้รับมา BT รีเซ็ตทั้งสอง matrix เป็น identity หนึ่งครั้งเป๊ะ ที่จุดเริ่มต้นของ text object แต่ q กับ Q ไม่แตะมันเลย TPDFContentStateTracker.Apply แยกกรณี coRestoreState ด้วยเหตุผลนี้พอดี ก่อนที่มันจะ pop saved state ออกจาก stack มันจับ text matrix ปัจจุบัน, text line matrix และ flag BT/ET ไว้ก่อน แล้วใช้มันซ้ำทับสิ่งที่ state ที่ popped ถืออยู่ เพราะคู่ q/Q ที่ห่อรอบ text run ไม่ควรย้ายตำแหน่งข้อความกลับ
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;
รันลำดับนั้นแล้ว CTM ที่รายงานที่ Tj ตัวที่สองกลับไปเป็นสเกล identity ที่มันมีก่อน q แล้ว 2 0 0 2 0 0 cm ภายในคู่ save/restore หายไปแล้ว ตามที่ q/Q กำหนด TextMatrix.DX ที่คำสั่งเดียวกันนั้น ยังคงเป็น 100 อยู่ Td ที่ตั้งมันรันก่อน q ดังนั้นมันจึงไม่ใช่ graphics state ที่ Q เคยมีสิทธิ์แตะเลย และเครื่องมือที่สมมติเป็นอย่างอื่นจะรายงาน glyph run ที่สองว่าเริ่มจากตำแหน่งแนวนอนที่ผิดบนหน้ากระดาษ
เกิดอะไรขึ้นเมื่อ operator ของ clipping path ทำงาน
operator W หรือ W* ไม่หด clip ทันที มันแค่บันทึกว่าจะใช้กฎการเติมสีแบบไหน และการตัดกันจริงรอ path-painting operator ใดก็ตามที่ตามมา รวมถึง n ตัววาดแบบ no-op ที่ผู้เขียน PDF ใช้กันเป็นประจำเพื่อ clip โดยไม่วาดอะไรเลย TPDFContentStateTracker สะท้อนจังหวะเวลาสองขั้นตอนนั้นตรงเป๊ะ coClip และ coClipEvenOdd แค่ตั้ง flag กฎ clip ที่รอดำเนินการเท่านั้น และ EndCurrentPath ซึ่งถูกเรียกโดยทุก path-painting operator คือสิ่งที่ตัดกันขอบเขตของ path ที่รอดำเนินการเข้าไปใน ClipMinX, ClipMinY, ClipMaxX และ ClipMaxY จริงๆ การทำให้จังหวะนี้ถูกต้องสำคัญต่อสัญญา before/after snapshot เอง before-snapshot ที่จับไว้ตรงคำสั่ง W เป๊ะยังคงต้องแสดง clip เก่าที่กว้างกว่า เพราะ clip ยังไม่มีผลจนถึงจุดนั้นในสตรีม และการยุบสองขั้นตอนเป็นหนึ่งจะทำลายผู้เรียกทุกตัวที่พึ่งพา before-state ว่าหมายถึงสิ่งที่มันบอกอย่างเงียบๆ
ClipBoundsExact บอกผู้เรียกว่ากำลังดูสองสถานการณ์ไหน และมันเป็น True ก็ต่อเมื่อสี่เหลี่ยมที่ตรงแกนเดียวที่สร้างโดย re บน path ที่ว่างเปล่าอยู่แล้วเท่านั้น เป็นรูปทรงเดียวที่ PDFlibPas แสดงออกได้เป๊ะเป็นตัวเลขสี่ตัว ทุกอย่างอื่น สี่เหลี่ยมที่หมุน, เส้นขอบโค้ง, path ผสมที่มีหลาย subpath หรือ clip ที่สร้างจากโหมด text-rendering ยังคงสร้าง ClipMinX ถึง ClipMaxY แต่ ClipBoundsExact ถูกล้างเป็น False เป็นสัญญาณที่ตรงไปตรงมาว่าตัวเลขสี่ตัวนั้นเป็นขอบเขตนอกที่ปลอดภัยและไม่ใช่รูปทรง clip ที่แท้จริง ผู้เรียกที่ต้องการแค่ขอบเขตนั้น เช่น การแยกพื้นที่ย่อยแบบสี่เหลี่ยมก่อนการแปลงลงเป็น GDI halftone ที่อธิบายไว้ในการ render หน้า PDF เป็นขาวดำ 1 บิต สามารถอ่านมันได้โดยตรงแทนที่จะสร้างมันใหม่จาก geometry ของหน้า
เส้นโค้งเบซิเยร์: ขอบเขตที่แม่นยำหรือขอบเขตที่ปลอดภัย
วิธีที่ถูกที่สุดในการหาขอบเขตของส่วนโค้ง Bézier แบบ cubic คือการหา convex hull ของจุดควบคุมทั้งสี่ของมัน และมันปลอดภัยเสมอเพราะเส้นโค้งไม่เคยออกนอกมันเลย แต่เส้นโค้งที่ตื้นและกว้างสามารถรายงาน bounding box ที่ใหญ่กว่าที่เส้นโค้งครอบครองจริงมาก ซึ่งทำให้การกรองแบบอิง clip อ่อนแอลงพอดีตอนที่มันสำคัญที่สุด บน path ตกแต่งขนาดใหญ่ PDFlibPas แก้ปัญหาที่แคบกว่าแทน สำหรับแต่ละแกน มันแก้อนุพันธ์ของเส้นโค้ง cubic หา root ภายในช่วงเปิด (0, 1) แล้วประเมินเส้นโค้งที่ root ใดก็ตามที่พบ พร้อมกับจุดปลายทั้งสอง ซึ่งเป็นวิธีมาตรฐานแบบ closed-form ในการหาขอบเขตที่ตรงแกนจริงของเส้นโค้ง แทนที่จะเป็นการประมาณเกิน ความแม่นยำต่อเส้นโค้งไม่ได้ส่งต่อไปยัง clip เองอยู่ดี เมื่อเส้นขอบโค้งกลายเป็น clipping path แล้ว ClipBoundsExact ยังคงตกเป็น False สำหรับมัน เพราะ bounding box ไม่ว่าจะแม่นยำแค่ไหน ก็ยังไม่ใช่รูปทรงเดียวกับเส้นโค้งที่มันครอบไว้ และตัว state tracker ยอมบอกแบบนั้นดีกว่าปล่อยให้ผู้เรียกสมมติว่าเป็นสี่เหลี่ยมในที่ที่จริงๆ เป็นเส้นโค้ง
การอ่าน state ก่อนและหลังแต่ละ operator
ผู้เรียกต้องการ state ก่อนหรือหลังขึ้นอยู่กับว่า operator ทำอะไรทั้งหมด คำถามเรื่องการวาดหรือ hit-testing เกี่ยวกับ path หรือ text run ต้องการ state ตามที่มันเป็นทันทีก่อนที่ operator นั้นจะรัน เพราะนั่นคือสิ่งที่ตัดสินจริงๆ ว่า operator วาดอย่างไร ในขณะที่คำถามเชิงวินิจฉัยเกี่ยวกับ state-setting operator อย่าง gs มักต้องการเห็นสิ่งที่มันเพิ่งเปลี่ยนไป TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) เปิดตัวเลือกนั้นพอดีเป็น Boolean เดียว คำนวณ TPDFContentGraphicsState หนึ่งตัวต่อคำสั่งในรอบเชิงเส้นเดียวข้ามทั้งโปรแกรม ไม่ว่าขณะไหนจะถูกร้องขอ GetGraphicsState(InstructionIndex, AfterInstruction, State) เสนอตัวเลือกก่อน/หลังเดียวกันสำหรับคำสั่งเดียวแทนที่จะเป็นทั้งโปรแกรม แต่มันเล่นซ้ำจากคำสั่งศูนย์ทุกครั้งที่เรียกเพื่อไปถึงตรงนั้น ดังนั้นการสแกนหลายดัชนีด้วยการเรียกมันใน loop มีต้นทุน O(n²) เทียบกับการเรียก TraceGraphicsStates ครั้งเดียวที่ O(n) ข้ามโปรแกรมเดียวกัน
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;
การอยู่ร่วมกับ content stream ที่ผิดรูปแบบ
อินพุตที่ผิดรูปแบบสองแบบพบได้บ่อยพอในตัวสร้าง PDF จริงจนที่ TPDFContentStateTracker ต้องอดทนกับมัน แทนที่จะล้มเหลวไปเลย แบบแรกคือ path ที่ครอบคลุมข้ามขอบเขต q/Q path ปัจจุบัน, จุดปัจจุบัน และจำนวน subpath ไม่ใช่พารามิเตอร์ graphics-state ISO 32000-1 §8.4 ครอบคลุมสิ่งที่ q และ Q บันทึกและกู้คืน และ path ปัจจุบันที่กำลังสร้างไม่ได้อยู่ในนั้น ดังนั้น TPDFContentStateTracker จึงติดตามข้อมูลนั้นแยกจาก saved state โดยสิ้นเชิง และ subpath ที่เริ่มก่อน q ยังคงอยู่ที่นั่น ยังไม่ถูกวาด ทันทีหลัง Q ที่ตรงกัน แบบที่สองคือ Q เปล่าๆ ที่ไม่มี q ที่ตรงกันเลยก่อนหน้าในสตรีม ไม่ใช่เรื่องหายากในเอาต์พุตจาก generator ที่ประกอบชิ้นส่วน content-stream ด้วยการต่อกันและทำบัญชีผิด TPDFContentStateTracker.RestoreUnderflowCount นับทุกเหตุการณ์เหล่านั้นแทนที่จะยก exception หรือทำให้ state เสียหาย Q ที่ไม่ตรงกันแค่ทิ้ง graphics state ปัจจุบันไว้เหมือนเดิมทุกประการ เสมือนว่าคำสั่งนั้นเป็น no-op ดังนั้นส่วนที่เหลือของสตรีมจึงยังคงเล่นซ้ำต่อบน state ที่สมเหตุสมผล และผู้เรียกยังคงตัดสินใจได้ภายหลังจากจำนวนนั้นว่าอินพุตควรค่าแก่การ flag กลับไปยังผู้สร้างมันหรือไม่
การประกอบ CTM, ความเป็นอิสระของ text matrix จาก q/Q และการเกิดขึ้นเป็นขั้นตอนของ clipping path ไม่ขึ้นอยู่กับว่า content stream จะถูกวาดหรือไม่เลย ซึ่งเป็นประเด็นพอดี snapshot ของ TPDFContentStateTracker เดียวกันถูกต้องไม่ว่าหน้าจะไม่เคย render เลย หรือกำลังจะถูกส่งให้ back end ใดก็ตามที่ PDFlibPas เลือกสำหรับไฟล์นั้น รวมถึงการสลับ engine ขณะทำงานที่ครอบคลุมในคู่มือการ render PDF แบบ multi-engine ใน PDFlibPas การวิเคราะห์เนื้อหา, การแม็ปพิกัด และเครื่องมือ redaction ล้วนรันได้ทั้งหมดบนเอาต์พุตของ tracker ก่อนหรือโดยไม่ต้องขอให้ renderer เข้ามาเกี่ยวข้องเลย
การเล่นซ้ำ content-stream ผ่าน TPDFContentStateTracker เป็นส่วนหนึ่งของ framework การแก้ไขเนื้อหาแบบมีโครงสร้างที่สร้างเข้าไปในPDFlibPas ไลบรารีคอมโพเนนต์ PDF แบบ VCL เนทีฟสำหรับ Delphi และ C++Builder