เมื่อ FPDFPage_TransFormWithClip เขียนหน้าใหม่ ทุก handle FPDF_PAGEOBJECT ที่คุณถืออยู่แล้วยังคงอธิบายผลการ parse จากก่อน transform PDFium Component สำหรับ Delphi และ C++Builder แก้ปัญหานี้ภายใน TransformPageContent ซึ่งยกเลิกการโหลด text page สร้าง content ใหม่ แล้วโหลดหน้าใหม่ เพื่อให้การสอบถามในภายหลังเห็นพิกัดใหม่
อาการนี้เงียบมาก คุณใช้ scale 0.9 เพื่อเพิ่มขอบพิมพ์ แล้วอ่าน PageObjectInfo แล้วได้ตัวเลขเดียวกันเป๊ะกับก่อนการเรียก ไม่มี exception ไม่มีรหัส error ไม่มีอะไรใน log นี่เป็นความล้มเหลวต่างจาก text page cache ที่ค้างซึ่งอธิบายไว้ใน บทความเรื่อง text page ที่ค้างหลังการแก้ไข ตรงนั้น cache คือ handle FPDF_TEXTPAGE เดียวที่คุณทิ้งและสร้างใหม่ได้ ตรงนี้ปัญหาคือทุก page object handle ในตัวแปรของคุณเอง บวกกับกลุ่ม getter ที่รายงานความล้มเหลวผ่าน return code ที่ caller ส่วนใหญ่ทิ้งไปโดยไม่สนใจ
ทำไมขอบเขตของ page object ถึงค้างโดยไม่มี error
เพราะ page object handle คือ pointer เข้าไปในตัวแทนที่ถูก parse ของ content stream หนึ่งตัวโดยเฉพาะ และ transform ระดับทั้งหน้าจะแทนที่ content stream นั้นด้วยตัวใหม่ PDFium ไม่เดินตาม call stack ของคุณเพื่อหา handle ที่ต้องแพตช์ มันสร้าง object graph ใหม่และปล่อยของเก่าไว้เหมือนเดิมทุกประการ ดังนั้นการอ่าน handle เก่าจึงเป็นการอ่านโครงสร้างที่ถูกต้องสมบูรณ์แบบที่ไม่ตรงกับสิ่งที่ไฟล์บอกอีกต่อไป
ISO 32000-1 §7.8.2 กำหนด content stream เป็นลำดับ operator ที่วาดหน้า และ §8.3.3 กำหนดว่า current transformation matrix แม็ป user space เข้าไปยัง device space อย่างไร transform ระดับหน้าถูกแสดงออกด้วยการห่อและเขียน operator เหล่านั้นใหม่ ไม่ใช่ด้วยการแก้พิกัดต่อ object ในที่เดิม ดังนั้นพิกัดที่ object พกอาจไม่เปลี่ยนแปลงเลย สิ่งที่เปลี่ยนคือ matrix ที่ใช้ในขณะที่วาด handle ใดก็ตามที่ถูก parse ภายใต้ matrix เก่าจะตอบคำถามเรื่องเรขาคณิตภายใต้ matrix เก่า และตอบโดยไม่มีการบ่นเลย
FPDFPage_TransFormWithClip เขียนอะไรใหม่จริง ๆ
มันเขียนหน้าใหม่ ไม่ใช่ snapshot ของคุณ FPDFPage_TransFormWithClip รับ FS_MATRIX และสี่เหลี่ยม clip FS_RECTF แล้วนำทั้งสองไปใช้กับ content ของหน้าทั้งหมด มันคือการเรียกที่ถูกต้องสำหรับขอบกระดาษ scale การจัดหน้าซ้อน (imposition) และการปรับหน้ากระดาษขนาดแปลกให้เข้ากับกรอบเป้าหมาย มันเป็นการเรียกที่ผิดถ้าคุณคาดหวังให้ handle ที่มีอยู่ตามไปด้วย และยังควรจำไว้ด้วยว่ามันแตะแค่ content ของหน้าเท่านั้น annotation เป็นชั้นแยกต่างหากและต้องการ TransformPageAnnotations ซึ่งส่งค่าสัมประสิทธิ์ matrix หกตัวเดียวกันไปยัง FPDFPage_TransformAnnots
var
Info: TPdfPageObjectInfo;
Scale: FS_MATRIX;
Clip: TPdfRectangle;
begin
Pdf.PageNumber:= 1;
Info:= Pdf.PageObjectInfo(0); // snapshot taken before the transform
Scale.a:= 0.9; Scale.b:= 0.0;
Scale.c:= 0.0; Scale.d:= 0.9;
Scale.e:= 29.7; Scale.f:= 42.0; // 5% margin, A4 in points
Clip:= Pdf.GetPageBox(pbMedia);
Pdf.TransformPageContent(Scale, Clip);
// Info.Bounds still holds pre-transform geometry, and Info.Handle now
// points into a page that TransformPageContent has already replaced
end;
ลำดับการรีเฟรชที่ TransformPageContent ใช้
สี่ขั้นตอน ตามลำดับนี้ ยกเลิกการโหลด text page, transform, สร้าง content, โหลดหน้าใหม่ TPdf.TransformPageContent รันตามลำดับนั้นเป๊ะ มันเรียก CheckPageActive คัดลอก matrix และ clip เข้าไปยังรูปแบบ record แบบเนทีฟ เรียก UnloadTextPage แล้ว FPDFPage_TransFormWithClip แล้ว UpdatePage ซึ่งเป็น wrapper รอบ FPDFPage_GenerateContent และสุดท้าย ReloadPage
แต่ละขั้นตอนมีเหตุผลของมัน UnloadTextPage มาก่อนเพราะ FPDF_TEXTPAGE ที่ cache ไว้ถือ character box ที่คำนวณภายใต้ matrix เก่า และมันยังทิ้งรายการ web-link ที่สืบทอดมาและ find session ใด ๆ ที่กำลังดำเนินอยู่ซึ่งถูกสร้างจากมันด้วย FPDFPage_GenerateContent ต้องรันก่อนการโหลดใหม่ เพราะ transform อยู่ในหน้าที่อยู่ในหน่วยความจำจนกว่ามันจะถูก serialize กลับเข้าไปใน content stream และการโหลดใหม่มิเช่นนั้นจะ parse stream ที่ไม่ได้แก้ไขซ้ำ ReloadPage ปิดท้ายด้วย FPDF_LoadPage เทียบกับ index ของหน้าปัจจุบัน ซึ่งเป็นสิ่งเดียวที่ให้ object graph ใหม่จริง ๆ กับคุณ
// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
I: Integer;
Info: TPdfPageObjectInfo;
begin
Pdf.TransformPageContent(Scale, Clip); // unload text page, transform,
// generate content, reload page
for I:= 0 to Pdf.ObjectCount- 1 do
begin
Info:= Pdf.PageObjectInfo(I); // handle and bounds from the new parse
if Info.Bounds.Right> PageWidth then
Log('object '+ IntToStr(I)+ ' still overflows after scaling');
end;
end;
มีรายละเอียดหนึ่งใน ReloadPage ที่ควรลอกไปใช้ถ้าคุณเคยเขียนลำดับนี้เอง มันโหลดหน้าใหม่ก่อนแล้วค่อย commit เข้าฟิลด์ทีหลังเท่านั้น ดังนั้นการโหลดหน้าที่ล้มเหลวจะปล่อยให้หน้าเนทีฟปัจจุบันและ cache ที่สืบทอดมาทั้งหมดของมันยังคงอยู่ แทนที่จะทิ้งคุณไว้ในสถานะที่ถูกรื้อไปครึ่งหนึ่ง การโหลดใหม่ไม่ฟรี คุณกำลังจ่ายค่าสำหรับการ re-parse หน้าใหม่ทั้งหมด แต่มันจ่ายครั้งเดียวต่อ transform ไม่ใช่ครั้งเดียวต่อ query และไม่มีทางเลือกอื่นที่ถูกต้องและถูกกว่านี้
อย่าพก handle ข้ามการโหลดใหม่
หลังจากโหลดใหม่ handle เก่าไม่ใช่แค่ค้าง มันคือ pointer ที่ห้อยลอยเลย FPDF_PAGE ก่อนหน้าถูกปิดไปแล้ว และค่า FPDF_PAGEOBJECT ที่เป็นของมันคือ pointer ไปยังหน่วยความจำที่ถูกปล่อยทิ้งแล้ว TPdfPageObjectInfo เปิดเผย native handle ในฟิลด์ Handle ของมัน ซึ่งมีประโยชน์จริงสำหรับการส่ง object ตรงเข้าไปยังการเรียกระดับต่ำ และก็อันตรายจริงพอกันที่จะเก็บไว้ใน form field หรือ list ข้ามการดำเนินการที่โหลดหน้าใหม่ ควรถือว่า snapshot record ถูกต้องแค่จนถึงการเรียกครั้งถัดไปที่สร้าง content ใหม่ ในจิตวิญญาณเดียวกับกฎความเป็นเจ้าของที่กล่าวไว้ใน บันทึกเรื่อง ABI และความปลอดภัยหน่วยความจำที่ขอบเขต PDFium
getter ล้มเหลวแต่ยังดูเหมือนข้อมูลถูกต้องได้ไหม
ได้ และนี่คือครึ่งที่สองของปัญหาเดียวกัน FPDFPageObj_GetRotatedBounds และ FPDFPageObj_GetIsActive เป็น getter แบบ out-parameter คือคืนค่า flag ความสำเร็จเป็น int และเขียนคำตอบจริงลงใน reference argument ทั้งคู่สามารถคืน FALSE สำหรับ object ที่ถูกสร้างแล้วแต่หน้าของมันยังไม่ถูก re-parse ได้ เมื่อเกิดแบบนั้น out parameter จะไม่ถูกแตะต้อง และ Pascal record ที่ถูก initialize ด้วย Default(TPdfPageObjectInfo) จะเป็นศูนย์ทั้งหมด ดังนั้น caller จึงเห็น quadrilateral ที่มีสี่จุดอยู่ที่จุดกำเนิดและ flag Active เป็น False การเรียกที่ล้มเหลวถูกเลื่อนขั้นเป็นข้อมูลที่ดูสมเหตุสมผลอย่างเงียบ ๆ
TPdfPageObjectInfo ตอบเรื่องนี้ด้วย sentinel ที่ชัดเจน HasRotatedBounds พกผลลัพธ์ของการเรียก FPDFPageObj_GetRotatedBounds, HasActiveState พกผลลัพธ์ของการเรียก FPDFPageObj_GetIsActive และฟิลด์เรขาคณิตกับสถานะจะถูกเขียนก็ต่อเมื่อ sentinel ที่เกี่ยวข้องเป็น True เท่านั้น รูปแบบเดียวกันนี้ซ้ำทั่ว record สำหรับ out-parameter getter อื่น ๆ ดังนั้น HasMatrix, HasFillColor, HasStrokeColor และ HasStrokeWidth ล้วนหมายถึงสิ่งเดียวกัน คือการเรียกเนทีฟสำเร็จและฟิลด์ข้าง ๆ มีความหมาย
Info:= Pdf.PageObjectInfo(I);
if Info.HasRotatedBounds then
// RotatedBounds is array [1..4] of TPdfPoint, in draw order
UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
Info.RotatedBounds[3], Info.RotatedBounds[4])
else
// the native call failed; fall back to the axis-aligned rectangle
UseRect(Info.Bounds);
if Info.HasActiveState and (not Info.Active) then
SkipObject(I); // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive
รูปแบบนี้ขยายไปยัง getter ทุกตัวของ PDFium ที่ทำตาม convention แบบ return-code-plus-out-parameter และมันมีอยู่เยอะมาก ถ้า wrapper ยุบ convention นั้นเป็นแค่ผลลัพธ์ฟังก์ชันธรรมดา มันทิ้งสัญญาณเดียวที่แยก "คำตอบคือศูนย์" ออกจาก "ไม่มีคำตอบเลย" ไปแล้ว การพก boolean เพิ่มหนึ่งตัวต่อฟิลด์มีต้นทุนแค่หนึ่งไบต์และกำจัดบั๊กประเภททั้งหมดที่ record ที่มีค่าเริ่มต้นถูกเข้าใจผิดว่าเป็นค่าที่วัดได้
จุดที่เรื่องนี้ยังกัดคุณอยู่
มีขอบเขตตรงไปตรงมาสามข้อ ข้อแรก การรีเฟรชเป็นแบบต่อหน้า transform หน้าสองแล้ว handle ที่คุณถืออยู่สำหรับหน้าหนึ่งไม่ได้รับผลกระทบ แต่ตอนนี้คุณมีสองหน้าที่ parse ในเวลาต่างกัน และเป็นหน้าที่ของคุณที่ต้องจำว่า snapshot ไหนมาจากหน้าไหน ข้อสอง ความเสถียรของ index ไม่ได้รับประกันข้ามการสร้าง content ใหม่ หลังจากโหลดใหม่ index 3 คือ index 3 อะไรก็ตามใน parse ใหม่ ดังนั้นควรระบุ object ใหม่ด้วย type และเรขาคณิตของมัน แทนที่จะสันนิษฐานว่าตำแหน่งยังคงอยู่ ข้อสาม สี่เหลี่ยม clip ใน FPDFPage_TransFormWithClip ถูกใช้กับ content ของหน้าและไม่ปรับขนาด page box ใด ๆ เลย ถ้าคุณ scale content ลงเพื่อสร้างขอบกระดาษ MediaBox ยังคงเป็นขนาดเดิมที่มันเคยเป็นเสมอ และ viewer จะแสดงแผ่นกระดาษต้นฉบับพร้อมภาพวาดที่หดตัวอยู่ข้างใน ไม่มีอะไรในนี้แปลกประหลาดเลย มันเป็นผลปกติของ C API ที่แจก pointer เข้าไปในสถานะที่ parse แล้วและปล่อยให้ caller จัดการเรื่องอายุการใช้งานเอง วิธีแก้คือแบบที่ใช้ได้ทุกที่อื่น กำหนดให้ชัดว่า snapshot หมดอายุตอนไหน รีเฟรชที่ขอบเขตนั้น และอย่าปล่อยให้การเรียกที่ล้มเหลวแอบอ้างเป็นค่าที่ถูกต้อง
ถ้าคุณกำลังศึกษาพฤติกรรม matrix ในภาพรวมมากขึ้น ลำดับการคูณที่ตัดสินว่า transform จะไปตกที่ไหนครอบคลุมใน บทความเรื่อง prepend, append และ pivot กับ matrix API การ transform และ page object ที่กล่าวถึงในบทความนี้มาพร้อมกับ PDFium Component สำหรับ Delphi และ C++Builder หน้าผลิตภัณฑ์มีเอกสารอ้างอิงฉบับเต็มสำหรับ page object snapshot record และฟิลด์ sentinel ของมัน