คุณเรียก AddText เพื่อประทับบรรทัดหนึ่งลงบนหน้า PDF ด้วย PDFiumPas แล้วเรียก FindFirst ทันทีเพื่อยืนยันว่าตราประทับนั้นลงเอยถูกที่ แต่การค้นหากลับว่างเปล่า ข้อความอยู่บนหน้าจริงๆ Acrobat แสดงมันด้วยซ้ำ แต่คอมโพเนนต์ TPdf ของ PDFiumPas เก็บโครงสร้าง FPDF_TEXTPAGE ที่ cache ไว้แยกต่างหาก parse ครั้งเดียวจาก content stream ของหน้า และการแก้ไขไม่ได้อัปเดตโครงสร้างนั้นย้อนหลังเองโดยอัตโนมัติ query มันก่อนที่มันจะถูกรีเฟรช แล้วคุณจะอ่านหน้าตามที่มันเป็นก่อนการเปลี่ยนแปลงของคุณเป๊ะ ไม่ใช่หลังจากนั้น
ทำไม PDFium ถึงคืนข้อความค้างทันทีหลังการแก้ไข
PDFiumPas ห่อเอนจิ้น render PDFium ของ Google สำหรับ Delphi และ C++Builder และการเรียกข้อความและการแก้ไขของมันเข้าถึงระบบย่อยที่ต่างกันสองอย่างภายในเอนจิ้นนั้น FPDF_TEXTPAGE เป็นของฝั่งการอ่าน FPDFText_LoadPage เดินผ่าน content stream ของหน้าครั้งเดียวและสร้าง text page ขึ้นมา รหัสตัวอักษร, ตำแหน่ง, font metrics, ขอบเขตคำ และ PDFiumPas เก็บโครงสร้างนั้นไว้ตราบเท่าที่หน้ายังคงโหลดอยู่ การเรียกแก้ไขอย่าง FPDFPage_InsertObject หรือ FPDFPage_GenerateContent ทำงานบนการแสดงผลที่ต่างกันโดยสิ้นเชิง คือ graph ของ object และ content-stream ของหน้า และ PDFium ไม่ผลักการเปลี่ยนแปลงเหล่านั้นเข้าไปยัง text page ที่เปิดอยู่แล้วเองเลย การสร้างมันใหม่ในทุกการแก้ไขจะทำให้การแก้ไขแบบ batch ช้าเกินยอมรับได้ ดังนั้นการออกแบบจึงแลกต้นทุนนั้นด้วยกฎแทน ใครก็ตามที่ถือ handle จะปิดมันหลังการแก้ไขที่เปลี่ยนเนื้อหา และการอ่านครั้งถัดไปจะสร้างอันใหม่
ภายใน text cache ของ TPdf: FTextPage, LoadTextPage และ UnloadTextPage
TPdf ติดตาม handle ที่ cache ไว้ในฟิลด์ส่วนตัวเดียวชื่อ FTextPage และห่อวงจรชีวิตของมันไว้ในสอง method LoadTextPage ตรวจสอบว่า FTextPage เป็น nil หรือไม่ และในกรณีนั้นเท่านั้นที่จะเรียก FPDFText_LoadPage กับหน้าปัจจุบัน ถ้า handle มีอยู่แล้ว LoadTextPage จะใช้มันซ้ำโดยไม่ถามว่าหน้าเปลี่ยนไปตั้งแต่มันถูกสร้างหรือไม่ UnloadTextPage เป็นอีกครึ่ง มันปิด handle เนทีฟด้วย FPDFText_ClosePage ตั้งค่า FTextPage กลับเป็น nil และยังทิ้งรายการ web-link ที่ cache ไว้และ session การค้นหาที่กำลังดำเนินอยู่ด้วย เพราะทั้งคู่ได้มาจาก text page เดียวกันและค้างด้วยเหตุผลเดียวกัน
พฤติกรรมใช้ซ้ำ-โดยไม่ตรวจสอบของ LoadTextPage คือเหตุผลตรงๆ ว่าทำไมลำดับถึงสำคัญ ทุก query ข้อความบน TPdf ไม่ว่าจะเป็น Text, FindFirst, GetWebLinks ล้วนวิ่งผ่าน LoadTextPage ก่อน ดังนั้นตราบใดที่ FTextPage ยังถือ handle ก่อนการแก้ไขอยู่ ไม่มีการเรียกใดในนั้นที่มีทางรู้ว่ามีการเปลี่ยนแปลงเกิดขึ้น การนำทางหน้าไม่เคยเป็นความเสี่ยงตรงนี้เลย UnloadPage ซึ่งรันตอนเปลี่ยนหน้า, reload และปิดเอกสาร ปิด text page พร้อมกับตัวหน้าเองมาโดยตลอด คำถามที่ยังค้างอยู่เสมอคือเรื่องการแก้ไขที่ใช้กับหน้าที่คุณยังคงอยู่ตรงนั้น
method ไหนของ PDFiumPas ที่รีเฟรช cache โดยอัตโนมัติ
method การแก้ไขหน้าของ TPdf เอง ได้แก่ AddText, SetText, SetTextPositions, AddPath, RemoveObject และ InsertFormObjectFromXObject แต่ละตัวเรียก UnloadTextPage ก่อนที่จะเรียก UpdatePage (FPDFPage_GenerateContent ของ PDFium) เพื่อ serialize การเปลี่ยนแปลงเข้า content stream เรียกตัวใดตัวหนึ่งเหล่านี้ แล้วการเรียก Text, FindFirst หรือ GetWebLinks ครั้งถัดไปจะสร้าง text page ขึ้นใหม่จากเนื้อหาตามที่มันเป็นตอนนี้ โดยไม่ต้องเรียกเพิ่มเติมจากฝั่งคุณเลย
var
Pdf: TPdf;
Index: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
// AddText already closed the cached text page, so this FindFirst
// call rebuilds it fresh before it searches
Index := Pdf.FindFirst('Reviewed by J. Alvarez');
if Index >= 0 then
ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
finally
Pdf.Free;
end;
end;
รูปแบบที่ยังคงพัง: การ cache handle TextPage ดิบ
TPdf เปิด handle ที่ยังทำงานอยู่ผ่าน property TextPage แบบอ่านอย่างเดียว สำหรับกรณีหายากที่คุณต้องเรียกฟังก์ชัน FPDFText_* ที่ PDFiumPas ยังไม่ได้ห่อไว้ ทางออกฉุกเฉินนั้นก็เป็นจุดเดียวที่การทำให้ไม่ถูกต้องอัตโนมัติช่วยไม่ได้เช่นกัน ทันทีที่คุณ copy ค่า FPDF_TEXTPAGE ออกจาก property เข้าตัวแปรท้องถิ่น PDFiumPas ไม่มีทางรู้เลยว่าคุณยังคงถือมันอยู่ และไม่มีทางอัปเดตสำเนาของคุณเมื่อ UnloadTextPage รันที่อื่นในโค้ดของคุณ
var
Pdf: TPdf;
RawHandle: FPDF_TEXTPAGE;
StaleCount: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
RawHandle := Pdf.TextPage; // FPDFText_LoadPage handle, cached in FTextPage
Pdf.SetText(0, 'Amended Clause 4.2');
// SetText already closed RawHandle and set Pdf.TextPage back to nil.
// Calling any FPDFText_* function against the old value now touches a
// handle PDFium has already freed — undefined behavior, not a bug you
// can catch with a nil check
StaleCount := FPDFText_CountChars(RawHandle);
finally
Pdf.Free;
end;
end;
การใช้ handle หลังจากที่ FPDFText_ClosePage รันกับมันแล้วเป็นพฤติกรรมที่ไม่นิยามไว้ใน PDFium เอง ไม่ใช่ข้อตกลงของ PDFiumPas ที่คุณเลือกเพิกเฉยได้ มันอาจคืนข้อมูลที่รู้ล่าสุด, คืนอะไรก็ไม่ได้เลย หรือ crash โปรเซส และสิ่งไหนจะเกิดขึ้นบน build หนึ่งๆ ไม่ใช่สิ่งที่โค้ดแอปพลิเคชันควรพึ่งพา กฎที่ปลอดภัยแคบ อ่าน Pdf.TextPage ใหม่ ทันทีก่อนการเรียก FPDFText_* ที่ต้องการมัน และอย่าถือสำเนาข้ามคำสั่งใดๆ ที่อาจแก้ไขหน้า
batch การแก้ไขของคุณ แล้ว query ครั้งเดียว
ไม่มีอะไรในนี้ที่หมายความว่าทุกการเรียก AddText หรือ RemoveObject ต้องการ query ข้อความแบบป้องกันทันทีหลังจากมันเพื่อตรวจสอบผลลัพธ์ แต่ละ method การแก้ไขจ่ายต้นทุนของการปิด text page ไปแล้วครั้งหนึ่ง การ query หลังทุกการแก้ไขเดี่ยวๆ ภายใน loop จ่ายต้นทุนนั้นซ้ำอีกโดยไม่ได้ประโยชน์อะไรเลย เพราะ FPDFText_LoadPage เดินผ่าน content stream ทั้งหมดใหม่ทุกครั้งที่มันรัน
var
Pdf: TPdf;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'watermarked.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
// Strip every text object that looks like a draft watermark. Each
// RemoveObject call already invalidates the cache on its own, so
// nothing needs refreshing by hand between iterations
for I := Pdf.ObjectCount - 1 downto 0 do
if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
Pdf.RemoveObject(I, True);
// Query once, after the whole batch is done, not once per removal
if Pdf.FindFirst('DRAFT') < 0 then
ShowMessage('Watermark cleared');
finally
Pdf.Free;
end;
end;
logic การ batch เดียวกันนี้ใช้กับ state การค้นหาโดยเฉพาะ FindNext และ FindPrevious ทำ session ที่เริ่มโดย FindFirst ต่อ และ session นั้นถูกรื้อโดย UnloadTextPage พร้อมกับทุกอย่างอื่น ดังนั้นการเรียก FindNext อีกครั้งหลังการแก้ไข แทนที่จะเรียก FindFirst อีกครั้ง จะยก exception แทนที่จะกลับมาทำงานต่ออย่างเงียบๆ กับเนื้อหาที่ไม่มีอยู่แล้ว ให้ปฏิบัติต่อการแก้ไขใดๆ เป็นขอบเขตที่แข็งแรงสำหรับทั้งเนื้อหาข้อความและตำแหน่งการค้นหา และปล่อยให้ FindFirst ใหม่หนึ่งครั้งที่ฝั่งตรงข้ามของการแก้ไขของคุณเริ่มการค้นหาใหม่
สิ่งนี้เข้ากับงาน extraction และ annotation อย่างไร
การดึงข้อความธรรมดา การอ่านข้อความของหน้าโดยไม่เปลี่ยนแปลงอะไร ไม่เคยเจอปัญหานี้เลย เพราะไม่มีอะไรทำให้ handle ที่ไม่มีการแก้ไขใดแตะไม่ถูกต้อง สำหรับวิธีที่ Text, สี่เหลี่ยมตัวอักษร และขอบเขตคำทำงานบนหน้าที่ไม่ได้แก้ไข บทความคู่กันเรื่องการดึงข้อความด้วย PDFiumPas ครอบคลุมพื้นที่นั้นโดยไม่มีวงจรชีวิต text page cache ที่บทความนี้เพิ่มเข้ามาด้วย
วงจรชีวิตของ cache สำคัญที่สุดใน workflow ที่แก้ไขแล้วดำเนินการกับผลลัพธ์ทันที ประทับการแก้ไขแล้วค้นหามัน, ปิดบังย่อหน้าแล้วยืนยันว่ามันหายไปแล้ว หรือค้นหาวลีเพื่อยึด annotation แบบ markup ทันทีหลังจากแทรกข้อความใกล้มัน กรณีสุดท้ายนั้นควรค่าแก่การชี้ให้เห็นเป็นพิเศษ annotation markup แบบ quad-point ถูกวางตำแหน่งจากสี่เหลี่ยมตัวอักษรที่อ่านจาก text page ดังนั้น annotation ที่สร้างจากพิกัดที่จับไว้ก่อนการแก้ไขจะลงเอยไฮไลต์จุดที่ผิดทันทีที่การแก้ไขนั้นลงเอย
API การแก้ไขและข้อความของ TPdf เป็นส่วนหนึ่งของPDFium Componentสำหรับ Delphi และ C++Builder และหน้าผลิตภัณฑ์มีเอกสารอ้างอิง method แบบเต็มสำหรับพื้นผิวการแก้ไข, การดึงข้อมูล และการค้นหาที่ครอบคลุมในบทความนี้