PDFium Component kiểm tra tính tương đương metadata PDF/A Info với XMP bằng TPdf.InspectPdfAMetadata và sửa nó bằng TPdf.NormalizePdfAMetadata. ISO 19005-1 (đã được Cor.1 đính chính) đòi mỗi entry Info trong tám cặp ánh xạ, từ Title tới ModDate, phải mang cùng giá trị với thuộc tính XMP tương ứng, chứ không chỉ cần tồn tại; phép kiểm đọc đúng hình dạng RDF, khớp namespace theo URI và so ngày như các khoảnh khắc thời gian
Báo lỗi thường mở đầu câu chuyện này thì nhìn vô hại lắm. Một hệ thống quản lý tài liệu đóng dấu /ModDate mới vào Info dictionary ở mỗi lần save tăng dần, để yên packet XMP, và sáu tháng sau một cuộc audit lưu trữ gắn cờ hàng nghìn file là không tuân thủ. Cả hai ngày đều có mặt. Chúng chỉ ngừng thống nhất với nhau từ lần chỉnh sửa đầu tiên, và một phép kiểm sự hiện diện chẳng bao giờ để ý. Các lần sửa Title qua một API chỉ-Info, và một chuỗi Author kiểu Finance; Controlling bị vài công cụ tách thành hai item dc:creator, thất bại theo cùng một kiểu
Vì sao PDF/A chối metadata có mặt ở cả hai nơi?
PDF/A chối vì ISO 19005-1 §6.7.3 là một luật về giá trị, không phải luật về sự hiện diện: Bảng 1 ánh xạ tám khóa Info sang các thuộc tính XMP, và một khi khóa Info có mặt, thuộc tính XMP được ánh xạ phải giữ một giá trị tương đương. Máy quét mức byte mô tả trong PDF/A preflight validation với PDFium Component chỉ xác nhận xmp:CreateDate và xmp:ModifyDate tồn tại (pvaiMissingXmpDates). Từ v3.72.0, TPdf.ValidatePdfA còn chạy thêm phép so giá trị đầy đủ và thêm pvaiInfoXmpValueMismatch vào tập vấn đề khi một packet XMP tồn tại nhưng bất đồng với Info (một packet không parse được được tính là bất đồng). Một packet vắng mặt vẫn được báo là pvaiMissingXmpMetadata, nên hai vấn đề không bao giờ đếm trùng một khiếm khuyết
Mỗi thuộc tính XMP được ánh xạ cần hình dạng RDF nào?
Mỗi trong tám ánh xạ có một kiểu XMP cố định, và một giá trị đúng nằm sai container vẫn thất bại. ComparePdfAInfoAndXmp trong FPdfPdfa.pas tra thuộc tính theo URI namespace, nên một packet gán http://purl.org/dc/elements/1.1/ cho một prefix lạ được đọc y hệt một packet dùng dc. Các hình dạng bắt buộc là:
- Title →
dc:titlevà Subject →dc:description: một lựa chọn ngôn ngữrdf:Alt, chỉ đối chiếu với itemx-defaultcủa nó (language tag khớp không phân biệt hoa thường); một Alt thiếux-defaultđược tính là thiếu - Author →
dc:creator: mộtrdf:Seqvới đúng một text item giữ nguyên chuỗi Info, nên một danh sách tác giả phân tách bằng dấu chấm phẩy vẫn là một entry duy nhất - Keywords →
pdf:Keywordsvà Producer →pdf:Producer(namespacehttp://ns.adobe.com/pdf/1.3/): thuộc tính text trơn - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://ns.adobe.com/xap/1.0/): thuộc tính text trơn
<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>
Các giá trị text được so như những chuỗi điểm mã Unicode chính xác, không cắt khoảng trắng, không thu về một kiểu chữ hay chuẩn hóa. Một dấu cách đuôi, hay một é dựng sẵn ở bên này và một e cộng dấu tổ hợp ở bên kia, là một lệch thật sự. Phía Info luôn đến từ việc PDFium tự decode text PDFDocEncoding và UTF-16 qua FPDF_GetMetaText, điều giữ cho thư viện khỏi phải cài lại phần decode chuỗi và sai một cách tinh vi; phía XMP chỉ sạch bằng đúng những byte đã sinh ra nó, vì thế các bẫy codepage làm hỏng metadata XMP dưới Free Pascal cũng có ý nghĩa ở đây
Khi nào một ngày PDF và một ngày XMP bằng nhau?
Một ngày PDF và một ngày XMP bằng nhau khi chúng mô tả cùng một khoảnh khắc tới từng giây, với cùng mức hiểu biết múi giờ ở cả hai phía. Cả hai parser đều nhận độ chính xác rút gọn hợp lệ, nên D:2026 và 2026 cùng nghĩa 1 tháng 1 năm 2026, 00:00:00. Khi cả hai giá trị đều mang múi giờ, chúng được chuyển sang UTC trước khi so: D:20260827093659+08'00' bằng 2026-08-27T01:36:59Z. Khi chẳng bên nào mang múi giờ, các thành phần địa phương được so nguyên văn. Khi chỉ một phía có múi giờ, kết quả là pamsValueMismatch, vì bịa ra một offset sẽ là một phỏng đoán. Một phần giây khác 0 kiểu .250 trong XMP cũng ép ra mismatch, vì một ngày PDF không có cách nào diễn đạt nó và việc âm thầm làm tròn bỏ đi sẽ che giấu một lệch thật; .000 thì được chấp nhận. Các giá trị không parse được được báo riêng là pamsInvalidInfoDate hay pamsInvalidXmpDate
Sự hiện diện có luật riêng. TPdfAMetadataValues.Present là một tập được đổ khi đi qua dictionary /Info của trailer đang kích hoạt, và nó tách “khóa vắng mặt” khỏi “khóa hiện diện với chuỗi rỗng”. Một khóa vắng mặt cho pamsNotRequired và chẳng đòi hỏi gì ở XMP; /Title () là có mặt, nên packet XMP phải mang một tiêu đề x-default rỗng tương ứng
Soi metadata Info và XMP trước khi lưu bằng cách nào?
TPdf.InspectPdfAMetadata trả về một TPdfAMetadataReport với một TPdfAMetadataComparison mỗi trường, mỗi cái giữ giá trị Info, giá trị XMP và một TPdfAMetadataState, nên một lần thất bại có thể được giải thích mà không cần dịch ngược một cờ validation duy nhất. MismatchFields tóm tắt tập thất bại, HasXmpPacket cho biết có tìm thấy packet hay không, và XmpParseError mang thông điệp parser khi packet tồn tại nhưng không đọc được
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 đổi gì, và từ chối gì?
TPdf.NormalizePdfAMetadata coi Info dictionary là nguồn sự thật và chỉ viết lại các thuộc tính XMP mà trường của nó rơi vào MismatchFields; mọi thứ khác trong packet sống sót. Title và Subject được ghi vào item x-default trong khi các lựa chọn ngôn ngữ khác nguyên vẹn, Author thành một rdf:Seq một phần tử, các namespace lạ và thuộc tính không liên quan được bảo toàn, và các thuộc tính XMP cho các khóa Info vắng mặt được để yên. Một ngày Info có múi giờ được ghi thành một ngày XMP UTC chuẩn hóa với hậu tố Z; một ngày không múi giờ giữ nguyên các thành phần địa phương. Overload file lưu qua một file tạm và một phép thay thế nguyên tử, và bản cập nhật XMP tự thân được nối thêm dưới dạng một incremental update
Các lần từ chối là có chủ ý. Với không packet XMP, method raise EPdfError, vì dựng một bộ nhận diện và metadata PDF/A trọn vẹn là việc của SaveAsPdfA, được trình bày trong tạo file lưu trữ PDF/A với PDFium Component. Một ngày Info dị dạng raise EPdfXmpError thay vì ghi xuống một giá trị sai nhưng nhìn hợp lý, và chẳng gì được lưu. Tài liệu có chữ ký bị từ chối trừ khi bên gọi truyền AllowSignedDocument = True. Tính tương đương cũng chỉ là một luật của ISO 19005-1, nên một file đã chuẩn hóa không tự động là một file tuân thủ
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; // đã nhất quán, đừng đụng vào file
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 date dị dạng hay packet không đọc được
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
Chạy phép so sánh trên packet XMP của riêng bạn
ComparePdfAInfoAndXmp và SynchronizePdfAInfoToXmp là các hàm trơn trong FPdfPdfa làm việc trên một TPdfXmpPacket mà chẳng cần nạp tài liệu nào, hợp với unit test và các pipeline lắp XMP từ một template. Cái bẫy duy nhất là Present: một record khởi tạo bằng Default(TPdfAMetadataValues) có một tập rỗng, mọi trường khi đó báo pamsNotRequired, và phép so sánh qua một cách rỗng tuệch bất kể bạn đã đổ giá trị gì vào
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 quyết định trường nào bắt buộc; riêng các giá trị thì bị bỏ qua
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// xmp:ModifyDate giờ là 2026-08-27T01:36:59Z, dc:creator thành một rdf:Seq một phần tử
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
Nếu pipeline của bạn lưu trữ những tài liệu mà các hệ thống khác tiếp tục chỉnh sửa, hãy ghép một lượt quét InspectPdfAMetadata đêm với NormalizePdfAMetadata cho những file trôi dạt, và giữ ValidatePdfA làm cổng trước khi bất cứ thứ gì rời đi vào lưu trữ dài hạn. Báo cáo có kiểu, đường sửa chữa và phần còn lại của bộ công cụ PDF/A nằm trong PDFium Component for Delphi and C++Builder