PDFium Component ตรวจความเท่ากันของ metadata จาก Info ไป XMP ของ PDF/A ด้วย TPdf.InspectPdfAMetadata และซ่อมด้วย TPdf.NormalizePdfAMetadata ISO 19005-1 (ตามที่แก้ไขโดย Cor.1) บังคับว่า Info entry ที่ map กันไว้ทั้งแปดตัว ตั้งแต่ Title ถึง ModDate ต้องพกค่าเดียวกับ property XMP ของตัวเอง ไม่ใช่มีอยู่แค่นั้น การตรวจอ่านรูปร่าง RDF ที่ถูกต้อง, จับคู่ namespace ด้วย URI และเทียบวันที่ในฐานะช่วงเวลา
รายงาน bug ที่มักเปิดบทสนทนานี้ดูไร้พิษภัย ระบบจัดการเอกสารประทับ /ModDate ใหม่ลง Info dictionary ทุกครั้งที่ save แบบขยาย ปล่อย XMP packet ให้เฉย ๆ และหกเดือนต่อมาการ audit คลังเอกสารก็ flag ไฟล์เป็นพันฉบับว่าไม่เข้าเกณฑ์ วันที่ทั้งสองตัวก็อยู่ครบ แค่หยุดเห็นพ้องกันตั้งแต่การแก้ครั้งแรก และการเช็กแค่ว่ามีอยู่ก็ไม่เคยสังเกต การแก้ Title ผ่าน API ที่แตะแค่ Info และสตริง Author แบบ Finance; Controlling ที่เครื่องมือบางตัวแตกเป็นสอง item ใน dc:creator ก็พังด้วยวิธีเดียวกัน
ทำไม PDF/A ถึงปฏิเสธ metadata ที่มีอยู่ทั้งสองที่
PDF/A ปฏิเสธเพราะ ISO 19005-1 §6.7.3 เป็นกฎเรื่องค่า ไม่ใช่กฎเรื่องการมีอยู่: ตาราง 1 map Info key แปดตัวไปยัง property XMP และเมื่อ Info key หนึ่งมีอยู่ property XMP ที่ map กันต้องพกค่าที่เทียบเท่ากัน ตัวสแกนระดับ byte ที่อธิบายไว้ในการ preflight validation ของ PDF/A ด้วย PDFium Component ยืนยันแค่ว่า xmp:CreateDate กับ xmp:ModifyDate มีอยู่ (pvaiMissingXmpDates) ตั้งแต่ v3.72.0 TPdf.ValidatePdfA รันการเทียบค่าเต็มรูปแบบเพิ่มด้วย และเติม pvaiInfoXmpValueMismatch ลงชุดปัญหาเมื่อ XMP packet มีอยู่แต่ไม่เห็นพ้องกับ Info (packet ที่ parse ไม่ได้นับเป็นการไม่เห็นพ้อง) packet ที่หายไปยังถูกรายงานเป็น pvaiMissingXmpMetadata อยู่ ปัญหาสองตัวจึงไม่มีวันนับซ้ำ defect เดียวกัน
property XMP ที่ map แต่ละตัวต้องมีรูปร่าง RDF แบบไหน
การ map ทั้งแปดตัวมี type XMP ตายตัว และค่าที่ถูกต้องใน container ที่ผิดก็ยัง fail ComparePdfAInfoAndXmp ใน FPdfPdfa.pas ค้น property ด้วย namespace URI packet ที่ผูก http://purl.org/dc/elements/1.1/ กับ prefix แปลก ๆ จึงถูกอ่านเหมือนกับที่ใช้ dc เป๊ะ ๆ รูปร่างที่ต้องการคือ:
- Title →
dc:titleและ Subject →dc:description: เป็นทางเลือกภาษาแบบrdf:Altเทียบกับ itemx-defaultของมันเท่านั้น (language tag จับคู่แบบไม่สนตัวพิมพ์) Alt ที่ไม่มีx-defaultนับเป็นขาดหาย - Author →
dc:creator: เป็นrdf:Seqที่มี text item พอดีหนึ่งตัวถือสตริง Info ทั้งก้อน รายชื่อผู้เขียนที่คั่นด้วยเซมิโคลอนจึงยังเป็น entry เดียว - Keywords →
pdf:Keywordsและ Producer →pdf:Producer(namespacehttp://ns.adobe.com/pdf/1.3/): เป็น property ข้อความธรรมดา - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://ns.adobe.com/xap/1.0/): เป็น property ข้อความธรรมดา
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report 2026</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>Finance; Controlling</rdf:li></rdf:Seq></dc:creator>
<pdf:Producer>PDFium Component</pdf:Producer>
ค่าข้อความถูกเทียบเป็นลำดับ Unicode code point แบบเป๊ะ ๆ ไม่มีการ trim, ไม่ fold ตัวพิมพ์ ไม่ normalize เว้นวรรคท้าย หรือ é แบบ precompose ฝั่งหนึ่งเจอกับ e แยกเครื่องหมายผสมฝั่งโน้น ถือเป็น mismatch ของจริง ฝั่ง Info มาจากการ decode ของ PDFium เองเสมอ ทั้ง PDFDocEncoding และข้อความ UTF-16 ผ่าน FPDF_GetMetaText ไลบรารีจึงไม่ต้องเขียนการ decode สตริงซ้ำแล้วพลาดแบบละเอียดอ่อน ส่วนฝั่ง XMP สะอาดแค่ไหนขึ้นกับ byte ที่ผลิตมัน นั่นเหตุผลที่กับดัก codepage ที่ทำลาย metadata XMP ใต้ Free Pascal สำคัญในที่นี้ด้วย
เมื่อไรที่วันที่ PDF กับวันที่ XMP ถือว่าเท่ากัน
วันที่ PDF กับวันที่ XMP เท่ากันเมื่อพวกมันบรรยายช่วงเวลาเดียวกันถึงระดับวินาที พร้อมความรู้เขตเวลาเท่ากันทั้งสองฝั่ง parser ทั้งคู่รับความละเอียดที่ลดลงอย่างถูกกฎ D:2026 กับ 2026 จึงหมายถึง 1 มกราคม 2026, 00:00:00 ทั้งคู่ เมื่อทั้งสองค่าพกเขตเวลา พวกมันถูกแปลงเป็น UTC ก่อนเทียบ: D:20260827093659+08'00' เท่ากับ 2026-08-27T01:36:59Z เมื่อไม่มีใครพกเขตเวลา ส่วนประกอบ local ถูกเทียบตามที่เขียนไว้ เมื่อมีแค่ฝั่งเดียวพกเขตเวลา ผลลัพธ์คือ pamsValueMismatch เพราะการสมมุติ offset ขึ้นมาคือการเดา วินาทีเศษส่วนไม่เป็นศูนย์อย่าง .250 ใน XMP ก็บังคับ mismatch เช่นกัน เพราะวันที่แบบ PDF ไม่มีทางแสดงมันได้ และการปัดทิ้งเงียบ ๆ จะซ่อนความไม่ตรงกันที่แท้จริง ส่วน .000 ถูกรับได้ ค่าที่ parse ไม่ได้ถูกรายงานแยกเป็น pamsInvalidInfoDate หรือ pamsInvalidXmpDate
การมีอยู่มีกฎของตัวเอง TPdfAMetadataValues.Present เป็น set ที่ถูกเติมโดยเดินไล่ /Info dictionary ของ trailer ที่ใช้งานอยู่ และมันแยก "key ไม่มี" ออกจาก "key มีอยู่แต่เป็นสตริงว่าง" key ที่ไม่มีให้ pamsNotRequired และไม่เรียกร้องอะไรจาก XMP ส่วน /Title () นับว่ามีอยู่ XMP packet จึงต้องพก title x-default ว่าง ๆ มาด้วยเช่นกัน
จะตรวจ metadata Info กับ XMP ก่อน save อย่างไร
TPdf.InspectPdfAMetadata คืน TPdfAMetadataReport ที่มี TPdfAMetadataComparison หนึ่งตัวต่อ field แต่ละตัวพกค่า Info, ค่า XMP และ TPdfAMetadataState ความล้มเหลวจึงอธิบายได้โดยไม่ต้อง reverse engineer flag validation ตัวเดียว MismatchFields สรุปชุดที่ fail, HasXmpPacket บอกว่าเจอ packet หรือไม่ และ XmpParseError พกข้อความจาก parser เมื่อ packet มีอยู่แต่อ่านไม่ได้
uses
System.SysUtils, PDFium, FPdfPdfa;
const
FieldNames: array[TPdfAMetadataField] of string = (
'Title', 'Author', 'Subject', 'Keywords',
'Creator', 'Producer', 'CreationDate', 'ModDate');
StateNames: array[TPdfAMetadataState] of string = (
'not required', 'equivalent', 'XMP missing', 'XMP type mismatch',
'value mismatch', 'invalid Info date', 'invalid XMP date');
procedure ReportMetadata(Pdf: TPdf);
var
Report: TPdfAMetadataReport;
Item: TPdfAMetadataComparison;
begin
Report := Pdf.InspectPdfAMetadata;
if Report.XmpParseError <> '' then
Writeln('XMP packet unreadable: ', Report.XmpParseError)
else if not Report.HasXmpPacket then
Writeln('No XMP packet at all');
for Item in Report.Comparisons do
if not Item.IsEquivalent then
Writeln(Format('%-12s %-18s Info="%s" XMP="%s"',
[FieldNames[Item.Field], StateNames[Item.State],
Item.InfoValue, Item.XmpValue]));
end;
NormalizePdfAMetadata แก้อะไร และปฏิเสธอะไร
TPdf.NormalizePdfAMetadata ถือ Info dictionary เป็นแหล่งความจริง และเขียนทับเฉพาะ property XMP ที่ field ของมันตกไปอยู่ใน MismatchFields สิ่งอื่นใน packet รอดทั้งหมด Title กับ Subject ถูกเขียนลง item x-default โดยทางเลือกภาษาอื่นยังคงเดิม Author กลายเป็น rdf:Seq หนึ่ง item namespace ที่ไม่รู้จักกับ property ที่ไม่เกี่ยวข้องถูกเก็บไว้ และ property XMP ของ Info key ที่ไม่มีก็ไม่ถูกแตะ วันที่ Info ที่มีเขตเวลาถูกเขียนเป็นวันที่ XMP แบบ UTC มาตรฐานพร้อม suffix Z ส่วนที่ไม่มีเขตเวลาคงส่วนประกอบ local ของมัน overload แบบไฟล์ save ผ่านไฟล์ชั่วคราวกับการ replace แบบ atomic และการอัปเดต XMP เองถูก append เป็น incremental update
การปฏิเสธพวกนี้ตั้งใจให้เป็นแบบนั้น เมื่อไม่มี XMP packet เมธอด raise EPdfError เพราะการสร้างชุดระบุตัวตนกับ metadata ของ PDF/A ครบชุดเป็นหน้าที่ของ SaveAsPdfA ตามที่อธิบายในการสร้างไฟล์คลัง PDF/A ด้วย PDFium Component วันที่ Info ที่เพี้ยน raise EPdfXmpError แทนที่จะเขียนค่าผิดที่ดูน่าเชื่อลงไป และไม่มีอะไรถูก save เอกสารที่ลงลายเซ็นถูกปฏิเสธ เว้นแต่ผู้เรียกจะส่ง AllowSignedDocument = True ความเท่ากันเองก็เป็นกฎหนึ่งของ ISO 19005-1 ไฟล์ที่ normalize แล้วจึงไม่ได้แปลว่าเข้าเกณฑ์โดยอัตโนมัติ
uses
System.SysUtils, PDFium, FPdfPdfa, FPdfXmp;
procedure NormalizeArchive(const Source, Target: string);
var
Pdf: TPdf;
Report: TPdfAMetadataReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := Source;
Pdf.Active := True;
Report := Pdf.InspectPdfAMetadata;
if Report.IsEquivalent then
Exit; // สอดคล้องกันอยู่แล้ว ปล่อยไฟล์ไว้เฉย ๆ
if not Report.HasXmpPacket then
raise Exception.Create('No XMP packet: convert with SaveAsPdfA instead');
try
if not Pdf.NormalizePdfAMetadata(Target) then
raise Exception.Create('Normalized save failed');
except
on E: EPdfXmpError do // วันที่ Info เพี้ยน หรือ packet อ่านไม่ได้
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
รันการเทียบบน XMP packet ของคุณเอง
ComparePdfAInfoAndXmp กับ SynchronizePdfAInfoToXmp เป็นฟังก์ชันธรรมดาใน FPdfPdfa ทำงานบน TPdfXmpPacket โดยไม่ต้องโหลดเอกสาร เหมาะกับเทสต์แบบ unit และ pipeline ที่ประกอบ XMP จาก template กับดักตัวเดียวคือ Present: record ที่ initialize ด้วย Default(TPdfAMetadataValues) มี set ว่าง ทุก field จะรายงาน pamsNotRequired และการเทียบจะผ่านแบบว่างเปล่า ไม่ว่าคุณจะกรอกค่าอะไรลงไป
uses
System.SysUtils, System.IOUtils, FPdfPdfa, FPdfXmp;
procedure AlignTemplate(const TemplateFile: string);
var
Info: TPdfAMetadataValues;
Packet: TPdfXmpPacket;
Changed: TPdfAMetadataFields;
begin
Info := Default(TPdfAMetadataValues);
Info.Title := 'Quarterly Report 2026';
Info.Author := 'Finance; Controlling';
Info.ModDate := 'D:20260827093659+08''00''';
// Present ตัดสินว่า field ใดบังคับ ค่าอย่างเดียวถูกเมิน
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// ตอนนี้ xmp:ModifyDate เป็น 2026-08-27T01:36:59Z, dc:creator เป็น rdf:Seq หนึ่ง item
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
ถ้า pipeline ของคุณเก็บถาวรเอกสารที่ระบบอื่นเอาไปแก้ต่อเรื่อย ๆ จงจับกวาด InspectPdfAMetadata รายคืนคู่กับ NormalizePdfAMetadata สำหรับไฟล์ที่หลุดจากกัน และตั้ง ValidatePdfA เป็น gate ก่อนอะไรจะถูกส่งไปคลังระยะยาว รายงานแบบมี type, เส้นทางซ่อม และเครื่องมือ PDF/A ที่เหลือมาพร้อมกันใน PDFium Component for Delphi และ C++Builder