PDFiumPas ตัวห่อ Delphi และ C++Builder รอบเอนจิ้น PDFium ของ Google บันทึกเอกสารที่เวอร์ชัน PDF ที่แน่นอนตั้งแต่ 1.3 ถึง 1.7 ผ่านพารามิเตอร์ PdfVersion ของ method TPdf.SaveAs การเรียก FPDF_SaveWithVersion ของ PDFium เองแค่เขียน header %PDF-M.m ใหม่เท่านั้น โดยไม่ตรวจสอบว่าเนื้อหาจริงของเอกสารถูกกฎหมายที่เวอร์ชันนั้นหรือไม่ PDFiumPas ปิดช่องว่างนั้นด้วยรอบตรวจสอบความสอดคล้องหลังบันทึก ที่เดินผ่าน chain revision cross-reference ที่กำลังทำงานอยู่และตรวจสอบการประกาศ Adobe Extension Level ก่อนที่ไฟล์จะออกจาก method นั้น
ความแตกต่างนี้สำคัญที่สุดในงาน print production ที่โปรไฟล์ PDF/X ระบุเวอร์ชัน PDF ที่แน่นอน และเครื่องมือ preflight หรือ RIP ปฏิเสธอะไรก็ตามที่ไม่ตรงกับ header ของตัวเองอย่างเงียบๆ สถานการณ์ที่ครอบคลุมจากฝั่งเอาต์พุตในการตรวจสอบเอกสาร PDF/X พร้อมพิมพ์ด้วย PDFiumPas SaveAs เปิดเป้าหมายเป็น enum TPdfVersion คือ pv13 ถึง pv17 ควบคู่กับค่า pv10 ถึง pv12 รุ่นเก่า บวก TSaveOption อิสระสำหรับการเขียนแบบ incremental หรือแบบเต็ม ส่ง PdfVersion เข้าไปแล้ว PDFiumPas จะทำงานสองอย่างในการเรียกเดียว มันขอให้ PDFium ประทับ header ที่ขอไว้ แล้วจึงอ่านไบต์ที่เพิ่งเขียนใหม่กลับ และปฏิเสธที่จะส่งไฟล์กลับมาถ้าเนื้อหาที่กำลังทำงานอยู่ไม่สามารถมีอยู่ได้ตามกฎหมายที่เวอร์ชันนั้น
var
Pdf: TPdf;
begin
Pdf:= TPdf.Create(nil);
try
Pdf.FileName:= 'source.pdf';
Pdf.Active:= True;
try
Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
except
on E: Exception do
// E.Message names the offending feature and the version or
// extension level it actually needs, for example:
// "RichMedia annotations and RichMediaExecute actions require
// /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
// or newer."
raise;
end;
finally
Pdf.Free;
end;
end;
ทำไมนิยาม object ล่าสุดในไฟล์ถึงเป็นสิ่งที่เชื่อถือผิด
object ทางกายภาพล่าสุดที่มีหมายเลขหนึ่งในไฟล์ PDF ไม่จำเป็นต้องเป็น object ที่ reader ที่เป็นไปตามมาตรฐานจะ resolve สำหรับหมายเลขนั้นในวันนี้ PDF ที่ผ่านการอัปเดตแบบ incremental หลายครั้งไม่ได้มี object graph เดียว มันมีประวัติของมันซ้อนกันอยู่ภายในไฟล์เดียว และทุกรอบการต่อท้ายสามารถปล่อย object, กำหนดมันใหม่ภายใต้หมายเลข generation ใหม่ หรือทิ้งเนื้อหาทางกายภาพเก่าของมันไว้ระหว่าง marker endobj สองตัว โดยไม่มี cross-reference entry ชี้ไปที่มันอีกต่อไป
PDFiumPas เจอโหมดความล้มเหลวนั้นพอดีก่อนที่มันจะติดตาม xref revision อย่างชัดเจน annotation Redact ที่ถูกทิ้งไว้กำพร้าโดยการเขียน page-object ใหม่ในภายหลัง หรือ dictionary /MarkInfo ที่ทิ้งไว้อยู่จริงทางกายภาพโดยไม่มี xref entry ชี้ไปที่มันเลย ก็ยังคงปรากฏขึ้นในการสแกนไบต์และยังคงทำให้การตรวจสอบฟีเจอร์ตามเวอร์ชันที่ใช้ไม่ได้กับเอกสารที่ reader จะเปิดจริงๆ อีกต่อไป ทำงานผิดพลาด ทิศทางความล้มเหลวคือการปฏิเสธเท็จ ไม่ใช่การยอมรับเท็จ ไฟล์ที่ผ่านพ้นฟีเจอร์หนึ่งไปแล้วจริงๆ ใน revision ปัจจุบันของมัน ก็ยังคงถูกบล็อคจากการบันทึกที่เวอร์ชันต่ำกว่าได้ เพราะเนื้อหาที่ไม่มีใครเข้าถึงได้อีกต่อไปแล้ว
PDFiumPas พิจารณาว่านิยาม object ตัวไหนกำลังทำงานอยู่จริงได้อย่างไร
PDFiumPas resolve ชุด object ที่กำลังทำงานอยู่แบบเดียวกับที่ reader ที่เป็นไปตามมาตรฐานทำ ด้วยการเดินผ่าน cross-reference chain แทนที่จะสแกนไบต์หา object header ตัว resolver เริ่มที่ offset startxref ล่าสุดในไฟล์และตามลิงก์ /Prev แต่ละตัวย้อนกลับผ่าน revision เก่ากว่า parse cross-reference table แบบดั้งเดิม, สตรีมแบบ hybrid ที่ลิงก์ /XRefStm และ cross-reference stream ล้วนๆ ไปตลอดทาง การเดินรันจากใหม่ไปเก่า และยุติแต่ละหมายเลข object ตอนที่มันถูกเห็นครั้งแรก ดังนั้น free entry ใน revision ที่หลังกว่าจึงบัง object body ที่เขียนไว้ก่อนหน้าอย่างถูกต้อง และการกำหนดใหม่ภายใต้ offset หรือ generation ใหม่ก็จะชนะสิ่งที่มันแทนที่เสมอ
member ของ object stream ได้รับการตรวจสอบเพิ่มเติมที่การค้นหา offset ธรรมดาให้เองไม่ได้ กลไกที่ครอบคลุมลึกกว่าในการตรวจสอบ object และ cross-reference stream ด้วย PDFiumPas object ที่บีบอัดซึ่งกู้คืนจาก /ObjStm ต้องมีสตรีมแม่ของมันได้รับการยืนยันว่ากำลังทำงานอยู่ในการเดินเดียวกัน และดัชนีของมันต้องตรงกับตำแหน่งของ member เองภายใน header ของสตรีมนั้น ก่อนที่ PDFiumPas จะปฏิบัติต่อมันเป็นเนื้อหาที่มีชีวิต ISO 32000-1 มาตรา 7.5.8.4 ถึงกับอธิบายกรณี hybrid-reference ที่ table ความเข้ากันได้แบบดั้งเดิมทำเครื่องหมาย object ว่าว่าง ในขณะที่ entry /XRefStm ของ trailer นิยาม object เดียวกันนั้นเป็น member ที่บีบอัดในที่อื่นพร้อมกัน PDFiumPas รวม xref stream เสริมเข้ากับ revision เดียวกันก่อนที่ entry แบบดั้งเดิมจะถูกใช้ ดังนั้นนิยามที่บีบอัดจึงชนะตามที่สเปคตั้งใจ
Adobe Extension Level: ประตูที่อยู่เหนือหมายเลขเวอร์ชัน
header %PDF-1.7 รับประกันแค่ชุดฟีเจอร์ที่ ISO 32000-1 ทำให้เป็นมาตรฐานในปี 2008 เท่านั้น ในขณะที่ความสามารถหลายอย่างที่ผู้ผลิต PDF พึ่งพาในทุกวันนี้ถูกส่งออกมาทีหลังในฐานะส่วนเสริมเฉพาะของ Adobe ที่ซ้อนทับบนหมายเลขเวอร์ชันเดียวกันนั้น Adobe ลงทะเบียนแต่ละส่วนเสริมเป็นคู่ BaseVersion และ ExtensionLevel ที่บันทึกไว้ใน dictionary /Extensions ของ catalog เอกสาร ภายใต้ prefix ผู้พัฒนา ADBE สำหรับส่วนเสริมของ Adobe เอง ดังนั้น reader จึงแยกไฟล์ PDF 1.7 ธรรมดาออกจากไฟล์ที่ implement extension level ที่มีหมายเลขด้วยได้ การบันทึกที่ pv17 โดยไม่มีการประกาศนั้นไม่ใช่ข้อผิดพลาดในตัวมันเอง มันจะกลายเป็นข้อผิดพลาดก็ต่อเมื่อเนื้อหาที่กำลังทำงานอยู่พึ่งพาฟีเจอร์ที่การประกาศนั้นควรครอบคลุมจริงๆ เท่านั้น
ฟีเจอร์เวอร์ชันสูงตัวไหนบ้างที่สะดุดประตูเวอร์ชันแบบชัดเจน
PDFiumPas ตรวจสอบรายการที่เจาะจงและอิงตามสเปค แทนที่จะเดาจากหมายเลขเวอร์ชันเพียงอย่างเดียว dictionary ของภาพที่พก entry /SMaskInData อย่างชัดเจน หรือค่า /BitsPerComponent เป็น 16 ทั้งคู่ต้องการ PDF 1.5 โดยกรณีสิบหกบิตเดินตามกฎ image-component ของ PDF Reference 1.5 มาตรา 4.8 โดยตรง annotation RichMedia และ action RichMediaExecute ต้องการ /BaseVersion /1.7 พร้อม /ExtensionLevel 3 หรือสูงกว่า สตรีม 3D แบบ PRC ที่ระบุด้วย dictionary ที่พกทั้ง /Type /3D และ /Subtype /PRC ต้องการ base version เดียวกันแต่แค่ /ExtensionLevel 1 dictionary Geospatial Measure และ annotation Projection ต้องการ /BaseVersion /1.7 พร้อม /ExtensionLevel 3 ซึ่งเป็นส่วนเสริม Adobe เดียวกับที่ RichMedia พึ่งพา
การตรวจสอบ geospatial พกรายละเอียดการอ่านสเปคที่ควรรู้ไว้ถ้าคุณเคยสร้าง logic แบบ version-gated ของตัวเองเหนือ PDFiumPas ISO 32000-1 Table 254 ทำเครื่องหมาย entry /Type ของ dictionary Measure ว่าเป็นตัวเลือก โดยระบุแค่ว่า "ถ้ามีอยู่ ต้องเป็น Measure" ในขณะที่ Table 311 ทำให้ /Type บังคับสำหรับ dictionary สตรีม 3D ที่เนื้อหา PRC อาศัยอยู่ เอาต์พุต GeoPDF จริงจากเครื่องมือทำแผนที่มักละเว้น /Type บน dictionary Measure และเขียนแค่ /Subtype /GEO ดังนั้นตัวตรวจจับ geospatial ของ PDFiumPas จึง match แค่ /Subtype เท่านั้น แทนที่จะต้องการทั้งสอง key แบบที่ตัวตรวจจับ PRC 3D ของมันทำได้อย่างปลอดภัย การต้องการ /Type บนทั้งสอง dictionary จะปล่อยให้เนื้อหา GeoPDF ที่สอดคล้องกันหลุดผ่านประตูไปโดยไม่ถูกตรวจจับ ลงเอยในไฟล์ PDF 1.7 ธรรมดาโดยไม่มีการประกาศ extension level มาหนุนหลัง
PDFiumPas ลดระดับฟีเจอร์ที่ไม่รองรับโดยอัตโนมัติหรือไม่
ไม่ใช่ในฐานะความสามารถทั่วไป และการสมมติว่าเป็นอย่างอื่นคือข้อผิดพลาดที่ควรหลีกเลี่ยงตรงนี้ SaveAs ส่งเวอร์ชันเป้าหมายผ่านรูทีนภายในชื่อ ValidatePdfVersionCompliance และเมื่อรูทีนนั้นพบฟีเจอร์ที่เวอร์ชันเป้าหมายหรือการประกาศ extension level ของมันรองรับไม่ได้ SaveAs จะยก exception ที่พกข้อความข้อผิดพลาดของรูทีนนั้น แทนที่จะเขียนไฟล์ ผู้เรียกจะได้เหตุผลที่แม่นยำและระบุฟีเจอร์กลับมา ไม่ใช่เอกสารที่ถูกเขียนใหม่อย่างเงียบๆ เลย จุดเดียวที่ PDFiumPas เขียนเนื้อหาใหม่โดยอัตโนมัติคือเป้าหมาย PDF 1.3 ที่มันตัดค่าเริ่มต้น transparency แบบไม่มีนัยสำคัญทางความหมาย /BM /Normal, /CA 1 และ /ca 1 ที่ PDFium เขียนเข้า dictionary ExtGState เสมอไม่ว่าเวอร์ชันเป้าหมายจะเป็นอะไร เพราะค่าเฉพาะเหล่านั้นไม่มีความหมายทางภาพเลย และ PDF 1.3 มีมาก่อน key เหล่านั้นทั้งหมด
// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode
transparency ที่ไม่ใช่ค่าเริ่มต้นและ soft mask ของภาพที่แท้จริงยังคงล้มเหลวโดยสิ้นเชิงที่เป้าหมาย PDF 1.3 เพราะการลบมันออกจะเปลี่ยนหน้าตาของหน้ากระดาษจริงๆ และ PDFiumPas จะไม่ตัดสินใจแทนคุณ มีขีดจำกัดที่เกี่ยวข้องกันสองอย่างที่ควรวางแผนไว้ก่อนที่เวอร์ชันที่แน่นอนจะเข้าไปใน batch pipeline เอาต์พุตแบบระบุเวอร์ชันชัดเจนไม่เคยพก dictionary /Encrypt เลย การบันทึกล้มเหลวทันทีถ้าต้นทางถูกป้องกันไว้ ซึ่งบังเอิญตรงกับโปรไฟล์ PDF/X และ PDF/A ที่ห้ามการเข้ารหัสอยู่แล้ว แต่ก็หมายความว่าการถอดรหัสเป็นขั้นตอนแยกต่างหากใน workflow ของคุณ ไม่ใช่สิ่งที่ SaveAs ทำให้คุณ PDFiumPas ยังไม่มี method สาธารณะสำหรับเขียนการประกาศ /Extensions /ADBE ลงบน catalog ดังนั้นไฟล์ต้นทางที่มีเนื้อหา RichMedia, PRC 3D หรือ geospatial แต่ขาดการประกาศนั้น จะไม่ผ่านประตูไม่ว่าคุณจะขอ PdfVersion อะไรก็ตาม การประกาศต้องมีอยู่แล้วในต้นทาง โดยทั่วไปเพราะเครื่องมือสร้างเขียนมันไว้ หรือไม่ก็ต้องเอาฟีเจอร์นั้นออกก่อนการบันทึก property TPdf.PdfVersion แบบอ่านอย่างเดียวควรค่าแก่การตรวจสอบก่อนที่การบันทึกตามเวอร์ชันที่แน่นอนจะถูกลองด้วยซ้ำ เพราะมัน resolve เวอร์ชันที่มีผลจริงที่รู้จัก catalog เดียวกัน ไม่ว่าจะเป็น header หรือ override /Version อันไหนที่เป็นปัจจุบัน ซึ่งเป็นสิ่งเดียวกับที่ตัวตรวจสอบตอนบันทึกเองพึ่งพา
Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);
ให้ปฏิบัติต่อ exception ของ SaveAs บนเป้าหมายเวอร์ชันที่แน่นอนเป็นรายงาน preflight มากกว่าจะเป็นบั๊ก ข้อความระบุข้อกำหนดที่แน่นอนที่เอกสารต้นทางกำลังละเมิดอยู่ ซึ่งเป็นข้อมูลที่ print shop หรือ archive pipeline ต้องการพอดีก่อนที่ไฟล์จะไปต่อ เส้นทางบันทึกแบบระบุเวอร์ชันชัดเจน, ตัว resolve xref revision ที่กำลังทำงานอยู่ และการตรวจสอบ Adobe Extension Level ที่อธิบายไว้ในบทความนี้ มาพร้อมกับPDFiumPas Componentรุ่นมาตรฐานสำหรับ Delphi และ C++Builder หน้าผลิตภัณฑ์มีเอกสารอ้างอิง TPdf.SaveAs แบบเต็ม ควบคู่ไปกับ API ด้านความสอดคล้องและฟอร์มที่เหลือ