PDFium Component همارزی متادیتای Info تا XMP مربوط به PDF/A را با TPdf.InspectPdfAMetadata میسنجد و با TPdf.NormalizePdfAMetadata ترمیم میکند. ISO 19005-1 (با اصلاحیهٔ Cor.1) از هر یک از هشت مدخل نگاشتشدهٔ Info، از Title تا ModDate، میخواهد همان مقدار پراپرتی XMP متناظرش را حمل کند، نه صرفاً وجود داشتن؛ چک شکل درست RDF را میخواند، namespaceها را با URI مچ میکند و تاریخها را بهعنوان لحظه مقایسه میکند
گزارش باگی که معمولاً این بحث را شروع میکند بیآزار به نظر میرسد. یک سیستم مدیریت اسناد روی هر ذخیرهٔ افزایشی یک /ModDate جدید داخل دیکشنری Info میکوبد، بستهٔ XMP را به حال خودش میگذارد، و شش ماه بعد یک ممیزی آرشیو هزاران فایل را ناهمخوان علامت میزند. هر دو تاریخ سر جایشاناند. فقط از اولین ویرایش دیگر با هم موافق نیستند، و یک چک حضور هرگز متوجهش نشد. ویرایشهای Title که از یک API فقط-Info انجام شده، و رشتهٔ Author مثل Finance; Controlling که بعضی ابزارها به دو مدخل dc:creator میشکنند، به همان شکل رد میشوند
چرا PDF/A متادیتایی را که در هر دو جا هست رد میکند؟
PDF/A ردش میکند چون ISO 19005-1 §6.7.3 یک قاعدهٔ مقداری است نه قاعدهٔ حضوری: Table 1 هشت کلید Info را به پراپرتیهای XMP نگاشت میکند، و بهمحض حضور یک کلید Info، پراپرتی XMP متناظر باید مقدار معادلی را حمل کند. اسکنر بایتی که در preflight اعتبارسنجی PDF/A با PDFium Component توصیف شده فقط تأیید میکند که xmp:CreateDate و xmp:ModifyDate وجود دارند (pvaiMissingXmpDates). از نسخهٔ v3.72.0، TPdf.ValidatePdfA علاوه بر آن مقایسهٔ کامل مقدار را اجرا میکند و وقتی بستهٔ XMP وجود دارد ولی با Info نمیخواند pvaiInfoXmpValueMismatch را به مجموعهٔ موارد اضافه میکند (بستهای که پارس نشود ناهمخوان حساب میشود). بستهٔ غایب همچنان بهعنوان pvaiMissingXmpMetadata گزارش میشود، تا این دو مورد هرگز همان نقص را دوبار نشمارند
هر پراپرتی XMP نگاشتشده به چه شکلی از RDF نیاز دارد؟
هر یک از هشت نگاشت یک نوع XMP ثابت دارد، و یک مقدار درست در ظرف غلط باز هم رد میشود. ComparePdfAInfoAndXmp در FPdfPdfa.pas پراپرتیها را با URI namespace پیدا میکند، پس بستهای که http://purl.org/dc/elements/1.1/ را به یک پیشوند عجیب بسته باشد دقیقاً مثل بستهای با dc خوانده میشود. شکلهای مورد نیاز اینهاست:
- Title →
dc:titleو Subject →dc:description: یک جایگزین زبانی از نوعrdf:Altکه فقط با مدخلx-defaultآن مقایسه میشود (تگ زبان case-insensitive مچ میشود)؛ Alt بدونx-defaultغایب حساب میشود - Author →
dc:creator: یکrdf:Seqبا دقیقاً یک مدخل متنی که کل رشتهٔ Info را حمل میکند، پس یک فهرست نویسنده که با سمیکالن جدا شده یک مدخل تکی میماند - Keywords →
pdf:Keywordsو Producer →pdf:Producer(namespace باhttp://ns.adobe.com/pdf/1.3/): پراپرتیهای متنی ساده - Creator →
xmp:CreatorToolو CreationDate →xmp:CreateDateو ModDate →xmp:ModifyDate(namespace باhttp://ns.adobe.com/xap/1.0/): پراپرتیهای متنی ساده
<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>
مقادیر متنی بهعنوان دنبالههای دقیق code point یونیکد مقایسه میشوند، بدون trim و بدون case folding و بدون نرمالسازی. یک فاصلهٔ انتهایی، یا یک é پیشساخته در یک طرف در برابر یک e تجزیهشده بهعلاوهٔ اکسنت جدا در طرف دیگر، یک ناهمخوانی واقعی است. طرف Info همیشه از دیکدینگ خود PDFium برای متنهای PDFDocEncoding و UTF-16 از طریق FPDF_GetMetaText میآید، که نمیگذارد کتابخانه دیکدینگ رشته را دوباره پیاده کند و ظریف اشتباه بزند؛ طرف XMP فقط به تمیزی بایتهایی است که ساختهاش کردهاند، و دلیلش همین است که تلههای codepage که متادیتای XMP را زیر Free Pascal خراب میکنند اینجا هم مهماند
کی یک تاریخ PDF و یک تاریخ XMP برابرند؟
یک تاریخ PDF و یک تاریخ XMP وقتی برابرند که همان لحظه تا دقت ثانیه را توصیف کنند، با همان میزان آگاهی از منطقهٔ زمانی در هر دو طرف. هر دو پارسر دقت کاهشیافتهٔ مجاز را قبول میکنند، پس D:2026 و 2026 هر دو یعنی 1 ژانویه 2026، 00:00:00. وقتی هر دو مقدار منطقه دارند، قبل از مقایسه به UTC تبدیل میشوند: D:20260827093659+08'00' برابر 2026-08-27T01:36:59Z است. وقتی هیچکدام منطقه ندارند، مؤلفههای محلی همانطور که نوشته شدهاند مقایسه میشوند. وقتی فقط یک طرف منطقه دارد، نتیجه pamsValueMismatch است، چون ساختن یک آفست از صفر حدس زدن است. کسر ثانیهٔ ناصفری مثل .250 در XMP هم ناهمخوانی تحمیل میکند، چون تاریخ PDF راهی برای بیانش ندارد و گرد کردن بیسروصدایش یک مخالفت واقعی را میپوشاند؛ .000 قبول میشود. مقادیر غیرقابل پارس جداگانه بهعنوان pamsInvalidInfoDate یا pamsInvalidXmpDate گزارش میشوند
حضور قاعدهٔ خودش را دارد. TPdfAMetadataValues.Present مجموعهای است که با گشتن در دیکشنری /Info تریلر فعال پر میشود، و «کلید غایب» را از «کلید حاضر با رشتهٔ خالی» جدا نگه میدارد. کلید غایب pamsNotRequired میدهد و از XMP هیچ نمیخواهد؛ /Title () حاضر است، پس بستهٔ XMP باید یک عنوان x-default خالی هم حمل کند
قبل از ذخیره چطور Info و متادیتای XMP را بازرسی کنید؟
TPdf.InspectPdfAMetadata یک TPdfAMetadataReport برمیگرداند با یک TPdfAMetadataComparison بهازای هر فیلد، که هر کدام مقدار Info و مقدار XMP و یک TPdfAMetadataState را حمل میکند، پس یک شکست را میشود بدون مهندسی معکوس فلگ اعتبارسنجی تکی توضیح داد. MismatchFields مجموعهٔ نافرمان را خلاصه میکند، HasXmpPacket میگوید بستهای پیدا شده یا نه، و XmpParseError پیام پارسر را حمل میکند وقتی بسته هست ولی خوانده نمیشود
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 را حقیقت مبدأ میگیرد و فقط پراپرتیهای XMPی را بازنویسی میکند که فیلدشان در MismatchFields افتاده؛ بقیهٔ بسته سالم میماند. Title و Subject داخل مدخل x-default نوشته میشوند در حالی که جایگزینهای زبانی دیگر دستنخورده میمانند، Author به یک rdf:Seq تکمدخله تبدیل میشود، namespaceهای ناشناخته و پراپرتیهای بیربط حفظ میشوند، و پراپرتیهای XMP مربوط به کلیدهای غایب Info دست نمیخورند. یک تاریخ Info منطقهدار بهعنوان یک تاریخ XMP کانونی UTC با پسوند Z نوشته میشود؛ تاریخ بیمنطقه مؤلفههای محلیش را نگه میدارد. overload فایلی از طریق فایل موقت و جایگزینی اتمیک ذخیره میکند، و خود آپدیت XMP بهعنوان یک incremental update append میشود
رد کردنها عامدانهاند. بدون بستهٔ XMP متد EPdfError میدهد، چون ساختن یک مجموعهٔ کامل شناسایی و متادیتای PDF/A کار SaveAsPdfA است که در ساختن فایلهای آرشیوی PDF/A با PDFium Component پوشش داده شده. یک تاریخ Info بدشکل بهجای نوشتن یک مقدار غلط محتمل-به-نظر EPdfXmpError میدهد و هیچ چیزی ذخیره نمیشود. سندهای امضاشده رد میشوند مگر اینکه فراخوان AllowSignedDocument = True بدهد. همارزی خودش هم یک قاعدهٔ ISO 19005-1 است، پس یک فایل نرمالشده خودبهخود همخوان نیست
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 بدشکل یا بستهٔ غیرقابل خواندن
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
اجرای مقایسه روی بستهٔ XMP خودتان
ComparePdfAInfoAndXmp و SynchronizePdfAInfoToXmp توابع سادهای در FPdfPdfa هستند که روی یک TPdfXmpPacket بدون سند لودشده کار میکنند، که برای unit testها و پایپلاینهایی که XMP را از یک قالب مونتاژ میکنند مناسب است. یک تله هست: Present. رکوردی که با Default(TPdfAMetadataValues) مقداردهی شود مجموعهٔ خالی دارد، بعد هر فیلدی 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 تصمیم میگیرد کدام فیلدها الزامیاند؛ فقط مقادیر نادیده گرفته میشوند
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 تکمدخله
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
اگر پایپلاین شما سندهایی را آرشیو میکند که سیستمهای دیگر مدام ویرایششان میکنند، یک sweep شبانه از نوع InspectPdfAMetadata را با NormalizePdfAMetadata برای فایلهایی که drift کردهاند جفت کنید، و ValidatePdfA را بهعنوان گیت نگه دارید قبل از اینکه چیزی به انبار بلندمدت برود. گزارش تایپدار و مسیر ترمیم و بقیهٔ ابزارهای PDF/A همراه PDFium Component برای Delphi و C++Builder ارسال میشود