บทความเทคนิค

เอกสารงานวิศวกรรม PDF/E-1 ใน Delphi ด้วย PDFlibPas

PDF/E-1 คือโปรไฟล์เก็บถาวรสำหรับเอกสารงานวิศวกรรม และ PDFlibPas ติดตั้งมันเป็น author mode ที่คุณเปิดด้วย SetPDFEMode บวกกับ bounded preflight ที่อ่าน content stream ไล่ทีละ operator โปรไฟล์นี้ไม่ใช่ PDF/A ที่เปลี่ยนฉลากมาเท่านั้น มันมี identification namespace เป็นของตัวเอง มีข้อกำหนด lifecycle metadata ของตัวเอง และมีกฎหนึ่งข้อที่ทำให้การตรวจเนื้อหาเข้มกว่าโปรไฟล์เก็บถาวรที่คุณเคยเจอมาทุกตัว

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

identification เป็นของตัวเอง ไม่ใช่ PDF/A แบบแก้เล็กน้อย

สิ่งแรกที่ต้องจับให้ถูกคือ identification ของ PDF/E-1 ผลิตด้วยการดัดแปลง pattern ของ PDF/A หรือ PDF/X ไม่ได้ มันใช้ XMP namespace แยกต่างหากคือ http://www.aim.org/pdfe/ns/id/ และค่าเวอร์ชันต้องปรากฏในสองที่ ทั้งเป็น document information entry และเป็น XMP property ที่ขึ้นต้นด้วย namespace ปล่อยแค่ฝั่ง XMP อย่างเดียว หรือแค่ information entry อย่างเดียว จะได้ไฟล์ที่พกเจตนาไว้ครบแต่ตรวจไม่ผ่าน

output intent ก็มีรูปร่างเจาะจงไม่แพ้กัน PDF/E-1 ต้องการ ICC profile แบบฝังในไฟล์ที่มี subtype identifier เป็น ISO_PDFE1 และโปรไฟล์ต้องมีจำนวน component ตรงกับตระกูลสีอุปกรณ์ที่เอกสารใช้จริง อนุประโยคท้ายนี่แหละคือจุดที่ implementation พลาดกันเงียบ ๆ เพราะมันแปลว่า intent เลือกไว้ล่วงหน้าแล้วทิ้งขว้างไม่ได้

ทำไมสีอุปกรณ์จึงต้องกวาดตรวจทั้งเอกสาร

เพราะ color space ชอบซ่อนอยู่ใน resource dictionary ที่การสแกนระดับหน้าไม่มีทางไปถึง PDF/E-1 ถือว่า DeviceRGB กับ DeviceCMYK เป็นสองตระกูลที่ใช้ร่วมกันในเอกสารเดียวไม่ได้ การตรวจโปรไฟล์จึงแปลว่าต้องรู้ว่าทุก device color space ที่อะไรก็แล้วแต่ในไฟล์ใช้ form XObject มี resources ของตัวเอง pattern ก็มี รูปภาพก็มี tiling pattern ที่อยู่ใน form XObject ที่อยู่ในหน้าคือลึกสามชั้น และ validator ที่เช็กแค่ resources ระดับบนสุดของหน้าจะปล่อยผ่านเอกสารที่ใช้สองตระกูล

การกวาดตรวจจึงลงทะเบียน color space ระหว่างเดินผ่าน pages, forms, images และ patterns ในการทราเวิร์สเดียว แล้วจึงตัดสินว่าเอกสาร coherent หรือไม่ และ output intent ตรงกับที่พบหรือเปล่า เหตุผลแบบเดียวกันนี้กำกับสถาปัตยกรรม preflight ทั้งหมด การทราเวิร์สบางส่วนผลิตการผ่านลวง ๆ และการผ่านลวง ๆ บนเช็กคอนฟอร์แมนซ์แย่กว่าไม่มีเช็กเลย เพราะมันถูกบันทึกไว้เป็นหลักฐานเสียอีก

var
  Lib: TPDFlib;
  Diag: WideString;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('assembly-drawings.pdf');

    if Lib.SetPDFEMode(1) = 0 then
      raise Exception.Create('PDF/E author mode was refused');

    // Author mode คุม lifecycle metadata ให้ตรงกันทุกครั้งที่เซฟ
    // ถามก่อนเซฟว่าเอกสารจะผ่านด่านของตัวเองไหม
    if not Lib.PDFEReadyForSave then
    begin
      Diag := Lib.GetPDFEDiagnostics;
      Writeln('PDF/E blockers: ', Diag);
      Exit;
    end;

    Lib.SaveToFile('assembly-drawings-pdfe.pdf');
  finally
    Lib.Free;
  end;
end;

lifecycle metadata เป็นภาระรายการเซฟ

PDF/E-1 เรียกมากกว่า document identifier ธรรมดา ชุดขั้นต่ำรวม media management document identifier, version identifier, rendition class, เวลาสร้าง, เวลาแก้ไข, เวลาของ metadata และชื่อเรื่อง นั่นคือคำศัพท์ของการติดตาม revision และมันมีอยู่เพราะงานส่งมอบด้านวิศวกรรมถูกคาดหวังให้ reissue ซ้ำได้ ไม่ใช่เขียนครั้งเดียวจบ

ผลต่อ implementation คือฟิลด์พวกนี้ตั้งค่าตอนสร้างเอกสารไม่ได้ ถ้าเวลาแก้ไขถูกเขียนตอนที่คุณเปิดโหมด แล้วเอกสารถูกแก้ต่อ ภาพ XMP snapshot กับสถานะเอกสารจริงก็จะหลุดกัน และ validator ที่เทียบสองฝั่งจะรายงานความไม่สอดคล้องที่ไม่มีใครตั้งใจ author mode จึง sync ฟิลด์พวกนี้ให้ตรงกันทันทีก่อนทุกครั้งที่เซฟ เพื่อให้ metadata บรรยายไบต์ที่กำลังจะถูกเขียน ไม่ใช่ไบต์ที่เคยมีอยู่ตอนเปิดโหมด

นี่เป็นหลักการทั่วไปของ conformance metadata ที่ควรแยกพูดออกจาก PDF/E ให้ชัด: metadata ที่ derive มาเป็นของเส้นทางเซฟ ไม่ใช่เส้นทางแก้ไข ฟิลด์ใดก็ตามที่คำนวณจากสถานะเอกสารต้องถูกคำนวณใหม่ในวินาทีที่สถานะถูกตรึง ไม่เช่นนั้นมันคือ cache ที่ไม่มี invalidation

แผนภาพ PDF/E-1 ของ PDFlibPas แสดงการกวาดตรวจสีอุปกรณ์ทั้งเอกสารที่เดินผ่าน resource dictionary ของหน้า form XObject tiling pattern และรูปภาพเพื่อเก็บตระกูล DeviceRGB และ DeviceCMYK ก่อนตัดสินความ coherent ควบคู่กับฟิลด์ lifecycle metadata ที่ author mode sync ใหม่ทันทีก่อนทุกครั้งที่เซฟ เพื่อให้ XMP snapshot ตรงกับไบต์ที่กำลังจะถูกเขียน
ความ coherent ของสีตัดสินได้หลังการทราเวิร์สหนึ่งรอบไปถึงทุก resource dictionary เท่านั้น และ lifecycle metadata ที่ derive มาถูกคำนวณใหม่ในวินาทีที่สถานะเอกสารถูกตรึง ไม่ใช่ตอนเปิดโหมด

กฎที่ทำให้การตรวจเนื้อหาเข้มขึ้น

PDF/E-1 ไม่อนุญาตให้ operator ส่วน compatibility กลืนเนื้อหาที่ไม่รู้จัก ใน PDF ธรรมดา BX กับ EX ล้อมเขตพื้นที่ที่ผู้บริโภคเอกสารต้องมองข้าม operator ที่ตัวเองไม่รู้จัก ซึ่งเป็นบันไดหนีที่ให้ผู้ผลิตปล่อย construct ใหม่กว่าออกมาได้โดยไม่ทำ reader เก่าพัง ใต้ PDF/E-1 บันไดหนีนี้ถูกปิด operator ใดก็ตามที่ preflight ไม่รู้จักจะถูกรายงานแบบไม่มีเงื่อนไข ไม่ว่ามันจะอยู่ใน compatibility section หรือไม่

ผลต่อ validator หนักมาก มันข้ามพื้นที่ที่ไม่เข้าใจไม่ได้ แปลว่า operand parser ต้องแยกวิเคราะห์ operator ทุกตัวในทุก content stream จริง ๆ เพดานที่ว่ามานี่แหละคือที่มาของ bounds การทราเวิร์สถูกจำกัดที่ 128 ระดับของ nesting, หนึ่งล้านออบเจกต์ และเนื้อหา 64 MiB และค่าจำกัดเหล่านี้ไม่ใช่การจูนประสิทธิภาพ ไฟล์ที่มุ่งร้ายหรือแค่พังก็สามารถยื่น object graph ที่มีวงวนหรือความลึกของ nesting ที่เปลี่ยน recursive validator เป็น stack overflow ได้ และขีดจำกัดเหล่านี้คือสิ่งที่กันไม่ให้รอบการตรวจกลายเป็นช่องทาง denial-of-service ท่าทีป้องกันตัวแบบเดียวกันมีอธิบายไว้ใน การแยกวิเคราะห์ PDF ที่ไม่น่าเชื่อถืออย่างปลอดภัย

// ตรวจสอบแบบ standalone สำหรับไฟล์ที่ไม่ใช่คุณผลิต โดยไม่ต้องโหลด
// เข้าไปใน document instance
var
  Issues: TStringList;
  Stream: TFileStream;
  I: Integer;
begin
  Issues := TStringList.Create;
  Stream := TFileStream.Create('incoming.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if CheckCompliancePDFE(Stream, '', 0, Issues) = 0 then
      for I := 0 to Issues.Count - 1 do
        Writeln('PDF/E: ', Issues[I]);
  finally
    Stream.Free;
    Issues.Free;
  end;
end;

ด่านเซฟซ่อมอะไรได้ และปฏิเสธอะไร

ด่านนี้แบ่งงานเป็นสองช่วง และการแบ่งเป็นแบบนี้ก็เป็นไอเดียการออกแบบที่เอาไปใช้ต่อได้ ช่วงแรกมัน normalize สิ่งที่ซ่อมได้อย่างปลอดภัย: print flag ของ annotation, flag ห้ามซูมและห้ามหมุนบน text annotation และ appearance-generation flag บน form dictionary พวกนี้คือการตั้งค่าที่มีค่าที่ถูกเพียงค่าเดียวใต้โปรไฟล์และไม่พกสาระข้อมูลใด ๆ แก้ให้เงียบ ๆ จึงถูกต้อง ถ้าไปปฏิเสธเรื่องพวกนี้ก็เป็นความหวงหลักการเกินเหตุ

ช่วงถัดมาเช็กข้อจำกัดที่ซ่อมไม่ได้โดยไม่เปลี่ยนความหมายของเอกสาร: เวอร์ชัน, identification, encryption, output intent, ความ coherent ของสีอุปกรณ์ และการมีอยู่ของ form content แบบไดนามิก เอกสารที่พลาดข้อใดข้อหนึ่งจะถูกปฏิเสธ เพราะการเดา output intent ขึ้นมาเองหรือเลือกตระกูลสีแทนฝั่งผู้เขียนจะได้ไฟล์ที่ผ่านการตรวจแต่ให้ข้อมูลเนื้อหาผิดเพี้ยน

แผนภาพด่านเซฟ PDF/E-1 ของ PDFlibPas สำหรับ Delphi แสดง bounded preflight ที่สแกน operator ทุกตัวในทุก content stream ภายใต้เพดาน nesting 128 ระดับ ออบเจกต์หนึ่งล้านชิ้น และเนื้อหา 64 MiB ซ่อม print flag และ flag ซูมหมุนของ annotation แบบเงียบ ๆ ปฏิเสธเวอร์ชัน identification encryption output intent สีอุปกรณ์หรือ dynamic form content ที่ผิด แล้วรายงาน blocker ผ่าน GetPDFEDiagnostics
ด่านนี้ซ่อมแบบเงียบ ๆ เฉพาะสิ่งที่ไม่พกข้อมูล ปฏิเสธทุกข้อจำกัดที่การซ่อมจะบิดเบือนความหมาย แล้วเปลี่ยนการปฏิเสธเป็นรายการ blocker ผ่าน GetPDFEDiagnostics ก่อนไบต์ใดจะถึงดิสก์

การอ่านค่าวินิจฉัยกลับผ่าน GetPDFEDiagnostics ก่อนเซฟเปลี่ยนการปฏิเสธนั้นเป็นรายการที่ลงมือทำต่อได้ แทนที่จะเป็นแค่ operation ที่ล้มเหลว ใน batch pipeline ให้เรียกมันกับทุกเอกสาร log blocker รายไฟล์ แล้วส่งฝั่งล้มเหลวเข้าคิวที่ให้คนมาดู มันมีประโยชน์กว่าการเซฟที่ raise ตรง ๆ อยู่มาก เพราะ blocker มักจับกลุ่มกัน สี่สิบเอกสารที่ล้มเหลวเพราะ output intent หายไปแบบเดียวกันคือแก้ครั้งเดียว ไม่ใช่สี่สิบครั้ง

เลือกระหว่างโปรไฟล์เก็บถาวรอย่างไร

PDF/E-1 คือเป้าหมายที่ถูกเมื่องานส่งมอบเป็นเอกสารวิศวกรรมที่มีวงจร revision โดยเฉพาะเมื่อความ coherent ของสีอุปกรณ์สำคัญ เพราะผลงานต้องออกเครื่องพล็อตเตอร์และเครื่องพิมพ์ฟอร์แมตใหญ่ PDF/A คือเป้าหมายที่ถูกเมื่อเป้าหมายคือการอ่านได้ระยะยาวของเอกสารทั่วไป และเป็นโปรไฟล์ที่เครื่องมือ validator รองรับกว้างที่สุด สองตัวนี้แลกกันไม่ได้ และเอกสารหนึ่งฉบับอาจผ่านตัวหนึ่งแต่ตกตัวหนึ่ง

แผนภาพการตัดสินใจของ PDFlibPas เทียบโปรไฟล์เก็บถาวร PDF/E-1 และ PDF/A สำหรับ Delphi: PDF/E-1 สำหรับงานส่งมอบวิศวกรรมที่มีวงจร revision สีเครื่องพล็อตเตอร์และการตรวจตามสัญญาภายใต้ XMP namespace ของตัวเองพร้อม output intent แบบ ISO_PDFE1, PDF/A สำหรับการอ่านได้ระยะยาวโดยทั่วไปที่เครื่องมือ validator รองรับกว้างที่สุด
เริ่มจากคำถามว่าใครเป็นผู้ตรวจไฟล์อีกฝั่งปลายทาง โปรไฟล์ทั้งสองเรียก identification metadata และการรับประกันด้านสีที่ต่างกัน และเอกสารหนึ่งฉบับอาจผ่านตัวหนึ่งแต่ตกอีกตัว

ถ้าคุณคือคนเลือก ให้เริ่มจากว่าใครเป็นผู้ตรวจไฟล์ที่ปลายทาง เครื่องมือตรวจ PDF/A อยู่ทุกที่ และ preflight ฝั่ง PDF/A ของ PDFlibPas มีอธิบายไว้ใน preflight ของ PDF/A และ PDF/UA ส่วนการตรวจ PDF/E เชี่ยวชาญเฉพาะกว่าและมักเป็นข้อกำหนดตามสัญญา ไม่ใช่ค่าเริ่มต้น เมื่อ archive เดิมต้องถูกดันขึ้นมาให้ถึงโปรไฟล์ที่มันไม่เคยถูกเขียนให้ เส้นทางซ่อม metadata ใน การแปลงเป็น PDF/A พร้อมซ่อม metadata คือ pattern ที่ควรเดินตาม และรูปร่างแบบเดียวกันใช้ที่นี่ได้เช่นกัน ระบุให้เจอ ซ่อมเฉพาะที่ปลอดภัย ที่เหลือปฏิเสธพร้อมรายการ

author mode, bounded content preflight และ compliance check แบบ standalone ทั้งหมดมาพร้อมกับ PDFlibPas Delphi PDF library เอกสารจึงถูกผลิตใต้โปรไฟล์และตรวจสอบยืนยันภายหลังแยกกันผ่าน code path คนละเส้นทาง ซึ่งเป็นเพียงรูปแบบเดียวที่ควรเชื่อสำหรับการอ้างคอนฟอร์แมนซ์