Technical Article

การตรวจสอบความถูกต้องของ PDF แบบบีบอัด: กระแสวัตถุและการอ้างอิงไขว้ (XRef)

คุณเขียนโปรแกรมตรวจสอบความถูกต้องขนาดเล็กขึ้นมาตัวหนึ่ง มันจะเปิดไฟล์ PDF ค้นหาข้อมูลไปที่จุดสิ้นสุด ค้นหาคำสั่ง startxref อ่านค่าออฟเซ็ต และคาดหวังว่าจะพบคำหลัก xref พร้อมตารางการอ้างอิงไขว้ขนาดคงที่อยู่ด้านล่าง จากตารางนั้นมันจะรวบรวมค่าออฟเซ็ตวัตถุ จากนั้นสแกนย้อนกลับเพื่อหาคำหลัก trailer เพื่อทำความเข้าใจ /Root และ /Size มันทำงานได้อย่างสมบูรณ์แบบในทุกไฟล์ที่คุณสร้างขึ้นเพื่อทดสอบ แต่เมื่อไฟล์ที่ผลิตจาก Word รุ่นปัจจุบัน หรือจากไลบรารีที่มีเป้าหมายเป็น PDF 1.5 ส่งมาถึง โปรแกรมตรวจสอบจะระบุว่าไฟล์เสียหาย ไม่มีคำหลัก xref ในตำแหน่งที่ออฟเซ็ตชี้ไป ไม่มีพจนานุกรม trailer ในจุดใดเลย และตารางวัตถุที่โปรแกรมตรวจสอบสร้างขึ้นก็แทบจะว่างเปล่า ไฟล์ดังกล่าวใช้งานได้ปกติ แต่โปรแกรมตรวจสอบกำลังอ่านมันผ่านมุมมองของเทคโนโลยีเมื่อสิบห้าปีที่แล้ว

นี่คือเหตุผลที่พบบ่อยที่สุดประการหนึ่งที่ทำให้การตรวจสอบ PDF ระดับไบต์ที่เขียนขึ้นเพื่อรองรับโครงสร้างแบบคลาสสิกล้มเหลวในเอกสารสมัยใหม่ โครงสร้างที่มันพึ่งพา ได้แก่ ตารางการอ้างอิงไขว้แบบข้อความธรรมดาและคำหลัก trailer ถูกกำหนดให้เป็นทางเลือกเสริมใน PDF 1.5 และมักจะไม่มีการระบุไว้ มีคุณสมบัติสองประการที่เข้ามาแทนที่: กระแสข้อมูลการอ้างอิงไขว้ (cross-reference stream) และกระแสข้อมูลวัตถุแบบบีบอัด (compressed object stream) ทั้งสองมีคำอธิบายใน ISO 32000-1 และโปรแกรมตรวจสอบที่ไม่รู้จักโครงสร้างเหล่านี้จะมองเห็นไฟล์ปกติเป็นกลุ่มของวัตถุที่สูญหายไป

สิ่งที่ PDF 1.5 เปลี่ยนแปลงเกี่ยวกับส่วนท้ายของไฟล์

มาตรฐาน ISO 32000-1 §7.5.8 กำหนดกระแสการอ้างอิงไขว้ และ §7.5.7 กำหนดกระแสวัตถุประเภท /ObjStm ทั้งสองส่วนนี้ช่วยให้โปรแกรมเขียนละทิ้งโครงสร้างสองส่วนที่ตัววิเคราะห์แบบคลาสสิกใช้ตรวจสอบได้ ไฟล์ PDF 1.5 อาจลงท้ายด้วยการไม่มีตาราง xref เลย ในตำแหน่งของมัน วัตถุที่ startxref ชี้ไปจะเป็นวัตถุกระแสข้อมูลปกติซึ่งพจนานุกรมระบุ /Type /XRef และกระแสนั้นจะเก็บข้อมูลการอ้างอิงไขว้ในรูปแบบไบนารีที่กระชับ และไม่มีคำหลัก trailer เช่นกัน เนื่องจากส่วนเทรลเลอร์ (trailer) ปัจจุบันคือพจนานุกรมของตัวกระแสข้อมูลเอง คีย์ที่ตัววิเคราะห์แบบคลาสสิกค้นหา ได้แก่ /Root, /Size และ /ID จะอยู่ภายในพจนานุกรมนั้น

การเปลี่ยนแปลงประการที่สองคือการย้ายตำแหน่งของวัตถุเอง แทนที่จะเขียนวัตถุทางอ้อม (indirect object) ทุกตัวในออฟเซ็ตไบต์ของตัวเอง โปรแกรมเขียนสามารถรวมวัตถุขนาดเล็กจำนวนมาก เช่น พจนานุกรมหน้า พจนานุกรมคำอธิบายประกอบ แผนผังโครงสร้าง เข้ามารวมกันในกระแสวัตถุเดียวและบีบอัดคอนเทนเนอร์ทั้งหมดด้วยระบบ Flate วัตถุแต่ละตัวจะไม่เก็บในรูปออฟเซ็ตไบต์ในไฟล์อีกต่อไป พวกมันจะอยู่ในตำแหน่งภายในบล็อบ (blob) ที่บีบอัด ตัวตรวจสอบที่สแกนไบต์ดิบเพื่อค้นหา 1 0 obj จะไม่มีทางพบมัน เนื่องจากข้อความนั้นจะมีอยู่หลังการคลายบีบอัดเท่านั้น สำหรับตัววิเคราะห์แบบคลาสสิก เอกสารครึ่งหนึ่งได้หายสาบสูญไปแล้ว

คีย์ของเทรลเลอร์อยู่ในรูปข้อความปกติ แม้ในไฟล์ที่บีบอัด

ส่วนที่ช่วยเบาใจได้คือการอ่านค่าเทรลเลอร์ของกระแสการอ้างอิงไขว้ไม่จำเป็นต้องทำการคลายบีบอัดส่วนใดๆ วัตถุกระแสข้อมูลจะถูกเขียนในรูปของพจนานุกรมและตามด้วยคำหลัก stream แล้วตามด้วยไบต์ที่บีบอัด พจนานุกรมนั้นอยู่ในรูปข้อความปกติ ดังนั้นเมื่อ startxref ชี้ไปยังกระแสการอ้างอิงไขว้ ไบต์ข้อมูลหลังจากหมายเลขวัตถุจะดูเหมือนพจนานุกรมปกติ และคีย์ /Root, /Size และ /ID จะแสดงอยู่อย่างชัดเจน ก่อนที่คำหลัก stream และข้อมูล Flate จะเริ่มต้น

นั่นหมายความว่าตัวตรวจสอบสามารถเรียนรู้ข้อเท็จจริงสามประการที่สำคัญที่สุด ได้แก่ จุดเก็บแคตตาล็อก จำนวนวัตถุที่ไฟล์ระบุ และตัวระบุไฟล์ โดยการวิเคราะห์เฉพาะพจนานุกรมกระแสข้อมูลเท่านั้น มันไม่จำเป็นต้องคลายบีบอัดข้อมูลการอ้างอิงไขว้ และไม่ต้องตีความรายการไบนารีภายในตัว งานที่คอยขัดขวางตัววิเคราะห์ข้อความทั่วไปไม่ใช่การอ่านเทรลเลอร์ แต่เป็นการค้นหาวัตถุ ซึ่งเป็นสองปัญหาที่แยกจากกันได้ และการแก้ปัญหาแรกมีต้นทุนต่ำมาก

กระแสวัตถุ: ส่วนหัว และตามด้วยบล็อบ Flate

กระแสวัตถุ (object stream) คือคอนเทนเนอร์ พจนานุกรมของมันจะเก็บคีย์ /Type /ObjStm รายการ /N ระบุจำนวนวัตถุที่จัดเก็บอยู่ภายใน และรายการ /First ระบุค่าออฟเซ็ตไบต์ ภายในข้อมูลที่คลายบีบอัดแล้ว ซึ่งเป็นจุดที่ตัวเนื้อหาของวัตถุแรกเริ่มต้น ข้อมูลส่วนเนื้อหาที่บีบอัดไว้ เมื่อคลายบีบอัดแล้ว จะเริ่มต้นด้วยส่วนหัวขนาดเล็กของคู่จำนวนเต็ม /N คู่ แต่ละคู่คือหมายเลขวัตถุและค่าออฟเซ็ตของตัววัตถุนั้นเทียบกับคีย์ /First หลังจากส่วนหัวแล้ว จะเป็นตัวเนื้อหาของวัตถุแต่ละตัวต่อเรียงกันไป

การขยายโครงสร้างภายในจะเป็นขั้นตอนอัตโนมัติเมื่อไบต์ข้อมูลคลายบีบอัดแล้ว คุณอ่านพจนานุกรมเพื่อรับค่า /N และ /First คลายบีบอัดกระแสข้อมูลด้วยตัวถอดรหัส Flate ไล่ตามคู่ข้อมูล /N ด้านหน้าเพื่อเรียนรู้ว่าหมายเลขวัตถุใดบันทึกอยู่ที่ออฟเซ็ตใด และดึงเนื้อหาแต่ละส่วนออกมาเสมือนว่ามันเป็นวัตถุทางอ้อมปกติ สิ่งที่ต้องพึ่งพาเพียงอย่างเดียวคือตัวถอดรหัส Flate ซึ่งคุณมีอยู่แล้วในตัว: Delphi มาพร้อมกับ System.ZLib และ Free Pascal มาพร้อมกับหน่วยทำงาน zstream ซึ่งทั้งสองตัวห่อหุ้ม zlib และทำหน้าที่คลายบีบอัดกระแส Flate ดิบได้โดยไม่ต้องพึ่งพาโค้ดจากภายนอก รูทีนที่ทำหน้าที่ต่อเติมวัตถุที่ดึงมาแต่ละตัวเข้ากับตารางวัตถุของตัวตรวจสอบ จะช่วยให้การทำงานที่เหลือของตัวตรวจสอบ เช่น การเดินหา /Root และการตรวจสอบแผนผังหน้ากระดาษ ทำงานได้เหมือนกับในไฟล์สไตล์คลาสสิกทุกประการ

คุณไม่จำเป็นต้องจัดการกับฟังก์ชันตัวคาดการณ์ PNG และ TIFF ที่กระแสการอ้างอิงไขว้อาจนำมาใช้ผ่าน /DecodeParms เพียงเพื่อจะดึงคีย์เทรลเลอร์ออกมา ตัวคาดการณ์ทำหน้าที่กรองแถวข้อมูลอ้างอิงไขว้ไบนารีเพื่อให้บีบอัดข้อมูลได้ดีขึ้น มันไม่ได้เกี่ยวข้องกับพจนานุกรมที่อยู่ข้างหน้าตัวกระแสข้อมูลเลย การอัปเกรดขั้นต่ำสุดเพื่อให้ตัวตรวจสอบสไตล์คลาสสิกเข้าใจเอกสาร PDF สมัยใหม่จึงมีขนาดเล็กมาก: เมื่อ startxref ระบุพิกัดที่กระแสข้อมูลแทนคำหลัก xref ให้วิเคราะห์พจนานุกรมกระแสเพื่อค้นหาคีย์เทรลเลอร์ และขยายวัตถุ /ObjStm ใดๆ ที่คุณพบเพื่อนำเนื้อหาของมันเข้าสู่ตารางวัตถุ การถอดรหัสรายการประเภท 2 และตัวคาดการณ์เป็นงานแยกต่างหากขนาดใหญ่กว่าที่คุณสามารถเลื่อนออกไปทำเมื่อจำเป็นต้องดำเนินการสืบค้นหาวัตถุแบบสุ่มได้

เหตุผลที่การตรวจสอบการปฏิบัติตามข้อกำหนดต้องขยายกระแสข้อมูลก่อนก็จัดอยู่ในกลุ่มเดียวกัน ตัวตรวจสอบ PDF/A หรือ PDF/X จะเข้าตรวจสอบวัตถุเฉพาะเจาะจง: แคตตาล็อกเอกสารสำหรับอาร์เรย์ /OutputIntents กระแสข้อมูล /Metadata สำหรับแพ็กเกจ XMP ที่มีตัวระบุที่ถูกต้อง ข้อมูลรายละเอียดฟอนต์แต่ละตัวสำหรับไฟล์ฟอนต์ที่ฝังตัว และส่วนเทรลเลอร์สำหรับคีย์ /ID ในไฟล์บีบอัด วัตถุเหล่านี้ส่วนใหญ่จะอยู่ภายในกระแสวัตถุ ตัวตรวจสอบที่ไม่ได้ทำการขยายกระแสวัตถุจะไม่สามารถมองเห็นคีย์แคตตาล็อก ไม่พบข้อมูลเมตา และไม่สามารถแจกแจงฟอนต์ได้ มันจะรายงานเอกสารที่ถูกต้องตามเกณฑ์ปกติว่าขาดเป้าหมายเอาต์พุต ขาด XMP และขาดโครงสร้างครึ่งหนึ่ง เนื่องจากหลักฐานที่มันต้องการยังคงบันทึกอยู่ในบล็อบ Flate ที่ไม่เคยถูกคลายบีบอัดเลย

ลำดับการทำงานมีความสำคัญ การขยายโครงสร้างต้องเกิดขึ้นก่อนการตรวจสอบรันงาน ไม่ใช่ทำไปควบคู่กัน เนื่องจากทุกการตรวจสอบจะสมมติว่าสามารถเข้าถึงวัตถุได้ด้วยหมายเลข หากคุณเชื่อมโยงการตรวจสอบโปรไฟล์เข้ากับการสแกนไบต์ดิบโดยตรง มันจะรับความตาบอดของตัววิเคราะห์สไตล์คลาสสิกมาและรายงานข้อผิดพลาดปลอมกับไฟล์สมัยใหม่ที่มีโครงสร้างถูกต้องสมบูรณ์ เนื่องจากไฟล์เหล่านั้นผลิตมาจากเครื่องมือรุ่นใหม่ที่มีความสามารถเขียนกระแสการอ้างอิงไขว้ได้ตั้งแต่แรก

ส่วนประกอบ PDFium จะวิเคราะห์กระแสการอ้างอิงไขว้และกระแสวัตถุเป็นส่วนหนึ่งของการโหลดเอกสาร ซึ่งเป็นวิธีที่สะดวกเพื่อหลีกเลี่ยงการเขียนโค้ดคลายบีบอัดและขยายด้วยตัวเอง เมื่อคุณโหลดไฟล์ด้วยส่วนประกอบ TPdf วัตถุที่จัดเก็บไว้ในคอนเทนเนอร์ /ObjStm จะได้รับการแก้ไขเรียบร้อยแล้ว และจุดเข้าใช้งานการตรวจสอบความถูกต้องจะมองเห็นเอกสารที่ขยายสมบูรณ์ ฟังก์ชัน ValidatePdfA จะส่งคืนเรกคอร์ด TPdfAValidationResult ซึ่งฟิลด์ Conformance จะเป็นค่า TPdfAConformance เช่น pac1b หรือ pacNone ฟิลด์ Issues คือชุดปัญหาเฉพาะเจาะจงที่พบ และเมธอด IsCompliant จะเป็นจริงก็ต่อเมื่อตรวจพบระดับความสอดคล้องและไม่มีรายการปัญหาในชุดความผิดพลาด เนื่องจากวัตถุถูกขยายเรียบร้อยแล้วระหว่างการโหลด อาร์เรย์ /OutputIntents หรือฟอนต์ที่ฝังอยู่ภายในกระแสวัตถุจะถูกตรวจพบและไม่ถูกรายงานว่าขาดหายไป

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // parses xref/object streams on load
    Result := Pdf.ValidatePdfA;    // sees the expanded object table
  finally
    Pdf.Free;
  end;
end;

หากไบต์ข้อมูลอยู่ในหน่วยความจำแล้วแทนที่จะอยู่บนดิสก์ ลำดับการโหลดและตรวจสอบแบบเดียวกันนี้จะทำงานผ่านส่วนการโอเวอร์โหลด LoadDocument(const Data: TBytes) ซึ่งรับเนื้อหาไฟล์ดิบและวิเคราะห์กระแสอ้างอิงไขว้และกระแสวัตถุแบบเดียวกับที่การระบุเส้นทางไฟล์ทำ ข้อควรจำสำหรับตัวตรวจสอบที่เขียนด้วยตนเองคือสัญกรณ์เชิงโครงสร้าง ไม่ใช่ API: อ่านคีย์เทรลเลอร์จากพจนานุกรมกระแสเป็นข้อความธรรมดา ขยายทุก /ObjStm ด้วยตัวถอดรหัส Flate ก่อนที่จะตรวจสอบเอกสาร และจัดการการถอดรหัสรายการอ้างอิงไขว้ไบนารีเป็นงานที่ใหญ่ขึ้นซึ่งเลือกทำภายหลังได้

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', Ord(R.Conformance))
    else
      Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
  finally
    Pdf.Free;
  end;
end;

เมื่อโครงสร้างถูกขยายแล้ว ตัวตรวจสอบสามารถขับเคลื่อนส่วนอื่นๆ ของกระบวนการทำงานต่อจากนั้นได้ สำหรับโปรแกรมสั่งงานแบบบรรทัดคำสั่งที่รายงานความสอดคล้องระหว่างโฟลเดอร์ของอินพุต โปรดดูคู่มือการสร้างรายงานตรวจสอบล่วงหน้าแบบกลุ่มผ่าน CLI ของเรา เมื่อการตรวจสอบเป็นด่านกั้นก่อนการแยกเอกสารขนาดใหญ่ออกจากกัน เทคนิคในคู่มือการแยกเอกสาร PDF ออกเป็นหลายไฟล์ของเรา จะทำงานร่วมกับรูปแบบการโหลดและตรวจสอบที่แสดงในที่นี้ได้อย่างลงตัว ทั้งสองส่วนพัฒนาขึ้นบนสัญกรณ์การโหลดและการตรวจสอบของ PDFium Component สำหรับ Delphi และ C++Builder