ใน PDFium Component for Delphi กับ Lazarus การกำหนด TPdf.Active := True ไม่มีวันโยน exception เมื่อ PDF โหลดไม่สำเร็จ: TPdf.SetActive จับ exception ทุกตัวแล้วปล่อย component ให้อยู่ในสถานะ inactive อยากเห็น error จริง ให้เรียก TPdf.LoadDocument(Options, Report) แทน overload ตัวนี้โยน exception เดิมกลับมาใหม่และเติม TPdfLoadReport ด้วยสถานะการโหลด, error code แท้ของ PDFium และว่าตาราง cross-reference ถูกบังคับสร้างใหม่หรือไม่
ปัญหามักโผล่ในโค้ดแบบ batch งานดึงตารางเดินไล่โฟลเดอร์ที่มี PDF โลกจริง 13 ไฟล์ด้วย TPdf ตัวเดียวที่ใช้ร่วมกัน และมี 7 ไฟล์กลับมาเป็นความล้มเหลว ทั้งที่ไฟล์ 7 ตัวนั้นไม่มีตัวไหนเสียจริง บล็อก except รอบการโหลดไม่เคยทำงาน log ประจานชื่อไฟล์ผิด ๆ และ error ตัวแรกที่มองเห็นคือ EPdfError เปล่า ๆ เรื่อง component ที่ inactive ซึ่งโผล่จากการอ่าน property หลายบรรทัดหลังการโหลดที่ล้มจริง พฤติกรรมแยกกันสองอย่างกองซ้อนกันจนเกิดภาพนี้ และทั้งคู่ทำงานตามที่ออกแบบไว้
ทำไม TPdf.Active := True ถึงไม่โยน exception เมื่อ PDF โหลดไม่สำเร็จ
TPdf.SetActive ห่อ LoadDocument ด้วย try..except ที่กลืน exception ทุกคลาสแล้วปล่อย component ให้ inactive เฉย ๆ การกลืนนี้ตั้งใจ setter ตัวเดิมรันตอน form designer สลับ Active ใน IDE และ path ผิดต้องไม่ทำ IDE พัง ตอนรันจริง TPdf.Active แค่รายงานว่ามี native document handle อยู่ไหม โหลดพังแล้วมันจึงอ่านได้ False และไม่มีอะไรตามมา สิ่งที่ถูกโยนหายไปหมด ไม่ว่าจะเป็น EPdfError จาก parser, stream error หรือ EAccessViolation จาก pdfium.dll ที่ผูกครึ่ง ๆ กลาง ๆ ข้อความ DLL แบบละเอียดที่เล่าไว้ในการวินิจฉัย pdfium.dll โหลดล้มเหลวใน Delphi จะถึงมือ handler ของคุณได้ผ่านการเรียกที่ไม่กลืนมันทิ้งเท่านั้น
Pdf.FileName := FileName;
try
Pdf.Active := True; // SetActive กลืน exception การโหลดทุกตัว
except
on E: Exception do
Log.Add(FileName + ': ' + E.Message); // ไม่มีวันรัน
end;
// ความล้มเหลวโผล่ที่นี่แทน ในรูป EPdfError ทั่วไป:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));
// แก้น้อยที่สุดสำหรับโค้ดเดิม: ทดสอบ Active ทันทีหลังกำหนดค่า;
// ตั้งแต่ v3.122.1 LastLoadReport เก็บข้อความ error ที่ถูกกลืนไว้
Pdf.Active := True;
if not Pdf.Active then
Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);
ความล้มเหลวจบลงที่การเรียกที่มี guard ตัวแรกสุดท้าย TPdf.PageCount เหมือน property ของเอกสารส่วนใหญ่ เริ่มด้วย CheckActive ซึ่งโยน EPdfError ที่ระบุชื่อ component แต่ไม่ระบุไฟล์และไม่ระบุสาเหตุ การทดสอบ Pdf.Active ทันทีหลังกำหนดค่าเปลี่ยน crash ที่ประจานคนผิดให้กลายเป็นรายการ "ล้มเหลว" ที่ซื่อสัตย์ ก่อน PDFiumPas v3.122.1 สาเหตุหายไปตรงจุดนั้น ตั้งแต่ v3.122.1 การกำหนดค่าที่ล้มแทน LastLoadReport ด้วยรายงาน plsFailed ที่พกข้อความ error มาด้วย สาเหตุจึงรอด object exception ตัวมันเองกับการตรวจระดับไบต์ยังต้องใช้จุดเข้าต่างกันอยู่
ทำไมการใช้ TPdf ตัวเดียวซ้ำถึงพังตั้งแต่ไฟล์ที่สองเป็นต้นไป
TPdf.FileName กำหนดค่าได้ต่อเมื่อ component อยู่ในสถานะ inactive เท่านั้น instance ที่ใช้ร่วมกันจึงปฏิเสธไฟล์ที่สองก่อนจะพยายามโหลดมันด้วยซ้ำ TPdf.SetFileName เริ่มด้วย CheckInactive และ guard เดียวกันคุ้มครอง Password กับ FormFill หลังโหลดสำเร็จครั้งแรก instance ค้างอยู่ในสถานะ active การกำหนดค่าครั้งถัดไปจึงโยน exception และถ้า loop ของ batch จับ exception นั้นแล้วเดินต่อ error จะไปลงใต้ชื่อไฟล์ใหม่ ทั้งที่เอกสารเก่ายังเปิดค้างอยู่ ปนกับความล้มเหลวในการโหลดที่ถูกกลืน log จึงเลิกตรงกับความจริง ในการทำซ้ำกับ 13 ไฟล์ instance ที่ใช้ร่วมกันรายงานความล้มเหลว 7 ราย ขณะที่ TPdf.Create(nil) สด ๆ ต่อหนึ่งเอกสารเปิดได้ครบ 13 การตั้ง Active := False ระหว่างไฟล์ก็ใช้ได้เหมือนกัน แต่ instance เดียวต่อหนึ่งเอกสารทำให้ทุกไฟล์ถูกแยกขาดกันตั้งแต่โครงสร้าง
TPdf.LoadDocument กับ TPdfLoadReport ให้อะไรกับคุณ
TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) โยน exception จริงและยังบอกสิ่งที่เกิดขึ้นในรูปแบบมีโครงสร้าง overload ไฟล์โหลด FileName overload พี่น้องรับ TBytes หรือ pointer กับขนาด และ LoadCustomDocument(AStream, AOwnsStream, Options, Report) รับงาน stream ทุกตัวตรวจความถูกต้องของ options, เช็กว่า instance อยู่ในสถานะ inactive, รันการตรวจระดับไบต์ของ header, startxref, ส่วน xref และ marker %%EOF แล้วจึงทำการโหลดแท้ การตรวจถูกผูกด้วยขีดจำกัดแบบเดียวกับที่คุยกันในงบทรัพยากรของ parser สำหรับ PDF ที่ไม่น่าเชื่อถือ: TPdfLoadOptions.Default ตั้ง AuditByteLimit เป็น 256 MiB, MaxIssues เป็น 256, MaxXrefSections เป็น 1024 และ MaxXrefEntries เป็น 4,000,000 เมื่อล้มเหลวเมธอดตั้ง Report.Status := plsFailed แล้วโยนซ้ำ เพราะ Report ถูกเขียนลงที่เดิม เนื้อหาของมันจึงรอดจาก exception และสำเนาหนึ่งถูกเก็บไว้ใน TPdf.LastLoadReport
ฟิลด์ของรายงานตอบคำถามที่ log ของ batch ต้องใช้จริง Status เป็นหนึ่งใน plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected หรือ plsFailed NativeErrorCode เก็บค่า FPDF_GetLastError FPDF_ERR_PASSWORD (4) จึงแยกรหัสผ่านที่หายหรือผิดออกจากไฟล์เสียหายที่รายงานเป็น FPDF_ERR_FORMAT (3) UsedRecovery, CrossReferenceTableValid กับ RecoveryRoute บอกว่า PDFium ถูกบังคับให้สร้างตาราง xref ใหม่หรือไม่ และ Issues เรียงผลตรวจแต่ละข้อพร้อม Code, Severity, Offset, ObjectNumber และ MessageText โดย IssuesTruncated จะถูกตั้งเมื่อ MaxIssues ตัดรายการให้สั้นลง
uses
SysUtils, Classes, TypInfo, FPdfView, PDFium;
procedure ProcessBatch(Files, Log: TStrings);
var
I: Integer;
Pdf: TPdf;
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default(plmCompatible);
for I := 0 to Files.Count - 1 do
begin
Pdf := TPdf.Create(nil); // instance เดียวต่อหนึ่งเอกสาร
try
Pdf.FileName := Files[I];
try
Pdf.LoadDocument(Options, Report);
except
on E: Exception do
begin
// Report ถูกเติมแล้วแม้ LoadDocument จะโยน exception
if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
Log.Add(Files[I] + ': password required')
else
Log.Add(Format('%s: %s (%s)', [Files[I],
GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
E.Message]));
Continue;
end;
end;
if Report.UsedRecovery then
Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
ExtractTables(Pdf, Log);
finally
Pdf.Free;
end;
end;
end;
ควรโหลดด้วย plmStrict เมื่อไร
ใช้ plmStrict ทุกครั้งที่ไฟล์ที่ถูกซ่อมแบบเงียบ ๆ แย่กว่าไฟล์ที่ถูกปฏิเสธ เช่นการรับเข้า archive, การจัดการหลักฐาน หรือ pipeline การเซ็น PDFium สร้างตาราง cross-reference ที่พังขึ้นใหม่เงียบ ๆ (ISO 32000-1 §7.5.4) ด้วยการกวาดหา object ในไฟล์ ดีสำหรับ viewer แต่เป็นปัญหากับสิ่งที่ต้องประมวลผลไบต์ที่ได้รับมาเป๊ะ ๆ หลังโหลดแท้ component ถาม FPDF_DocumentHasValidCrossReferenceTable ในโหมด plmCompatible การสร้างใหม่ให้ plsLoadedWithRecovery บวก warning plicNativeCrossReferenceRebuild ในโหมด plmStrict component จะเอาเอกสารออก ตั้ง plsRejected เพิ่ม plicStrictModeRejected แล้วโยน EPdfError พร้อมข้อความ "Strict PDF load rejected the document" โหมดเข้มยังปฏิเสธ error จากการตรวจทุกข้อ และ TPdfLoadOptions.Default(plmStrict) เปิด RequireFinalEndOfFileMarker ซึ่งเลื่อน %%EOF ที่หายไปหรือข้อมูลหลังตัวสุดท้าย (§7.5.5) จาก warning ขึ้นเป็น error การตรวจ xref เสริมการเช็กระดับ object ที่เล่าไว้ในการตรวจ object กับ xref stream ด้วย PDFium VCL
function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
Pdf: TPdf;
Report: TPdfLoadReport;
I: Integer;
begin
Result := False;
Reason := '';
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
try
Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
Result := True; // xref ถูกต้อง ไม่มี error จากการตรวจ
except
on E: EPdfError do
begin
Reason := E.Message;
for I := 0 to High(Report.Issues) do
if Report.Issues[I].Severity = plisError then
Reason := Reason + sLineBreak + Format(' at offset %d: %s',
[Report.Issues[I].Offset, Report.Issues[I].MessageText]);
end;
end;
finally
Pdf.Free;
end;
end;
TPdf.LastLoadReport หยุดพูดความจริงตรงไหน
TPdf.LastLoadReport จะครบถ้วนต่อเมื่อหลัง overload ของ LoadDocument ที่รับ options เท่านั้น เพราะมีแต่ overload พวกนั้นที่รันการตรวจไบต์ Active := True ที่สำเร็จเขียนรายงานโหมด compatible โดยไม่มีการตรวจไบต์ AuditAttempted จึงค้างที่ False ก่อน PDFiumPas v3.122.1 ตัวที่ล้มไม่เขียนอะไรเลย แปลว่าบน instance ที่ใช้ร่วมกัน LastLoadReport ยังเล่าเรื่องไฟล์ก่อนหน้าอยู่ มักพร้อม plsLoaded ที่ปลอบใจ ตั้งแต่ v3.122.1 การโหลดที่ล้มทุกกรณีแทนที่รายงาน: Active := True ที่ล้ม ซึ่งยังปล่อย component ให้ inactive โดยไม่โยน และการเรียก LoadDocument ธรรมดาหรือ LoadCustomDocument ที่ล้ม บันทึก plsFailed พร้อมข้อความ error แต่ก็ยังไม่มีการตรวจเช่นเคย อีกสองช่องว่างที่สำคัญในทางปฏิบัติ การตรวจ options กับ CheckInactive รันก่อนรายงานจะถูก initialize การกำหนด AuditByteLimit ติดลบหรือ instance ที่ active อยู่แล้วจึงโยนโดยไม่ผลิตรายงาน และ NativeErrorCode มีความหมายต่อเมื่อ PDFium ได้ลอง parse จริง ไฟล์ที่หายไป wrapper จะโยนก่อน PDFium รัน จึงควร log ErrorMessage กับข้อความ exception แทน
กฎที่ใช้งานได้จริงสั้น ๆ เก็บ Active := True ไว้กับ viewer ที่ผูกกับ designer ซึ่ง component ที่ inactive เป็นผลลัพธ์ที่ยอมรับได้ ที่เหลือทั้งหมด โดยเฉพาะโค้ด batch กับเซิร์ฟเวอร์ สร้าง TPdf หนึ่งตัวต่อหนึ่งเอกสาร เรียก LoadDocument(Options, Report) จับ exception ที่มันโยนแล้ว log Report.Status, NativeErrorCode และ Issues ระดับ error ควบคู่กับชื่อไฟล์ ต้นทุนคือไม่กี่บรรทัดต่อจุดเรียก และทุกความล้มเหลวจะถูกประจานไปที่ไฟล์ถูกต้องพร้อมสาเหตุจริง
API load report, โหมดเข้มและการตรวจระดับไบต์ ship มากับPDFium Component for Delphi, C++Builder and Lazarus พร้อมกับการเรนเดอร์ การดึงข้อความ การกรอกฟอร์มและการตรวจ PDF/A