PDFium Component проверява PDF/A еквивалентността на Info към XMP метаданни с TPdf.InspectPdfAMetadata и я поправя с TPdf.NormalizePdfAMetadata. ISO 19005-1 (както е коригиран от Cor.1) изисква всеки от осемте картнати Info записа, от Title до ModDate, да носи същата стойност като съответното му XMP свойство, а не просто да съществува; проверката чете правилната RDF форма, мачка namespaces по URI и сравнява датите като моменти
Бъг репортът, който обикновено стартира този разговор, изглежда безобиден. Система за управление на документи щампа нов /ModDate в Info речника при всяко incremental записване, оставя XMP packet-а на мира, а шест месеца по-късно архивен одит маркира хиляди файлове като неконформни. И двете дати са там. Просто са спрели да се съгласуват още при първата редакция, а проверка за наличие никога не го е забелязала. Title редакции, направени през API, който пипа само Info, и Author string като Finance; Controlling, който някой инструмент е разделил на два dc:creator записа, се провалят по същия начин
Защо PDF/A отхвърля метаданни, съществуващи и на двете места?
PDF/A го отхвърля, защото ISO 19005-1 §6.7.3 е правило за стойност, не правило за наличие: Таблица 1 картира осем Info ключа към XMP свойства, и щом Info ключ е налице, картнатото XMP свойство трябва да държи еквивалентна стойност. Byte-level сканерът, описан в PDF/A preflight валидацията с PDFium Component, само потвърждава, че xmp:CreateDate и xmp:ModifyDate съществуват (pvaiMissingXmpDates). От v3.72.0 TPdf.ValidatePdfA допълнително пуска пълното сравнение на стойности и добавя pvaiInfoXmpValueMismatch към набора от проблеми, когато XMP packet съществува, но не съгласува с Info (packet, който не може да бъде парснат, брои като несъгласуване). Липсващ packet продължава да се докладва като pvaiMissingXmpMetadata, така че двата проблема никога не броят двукратно един дефект
Каква RDF форма трябва да има всяко картнато XMP свойство?
Всяко от осемте картирания има фиксиран XMP тип, а коректна стойност в грешния контейнер пак се проваля. ComparePdfAInfoAndXmp в FPdfPdfa.pas търси свойствата по namespace URI, така че packet, свързал http://purl.org/dc/elements/1.1/ с необичаен префикс, се чете точно като такъв, ползващ dc. Изискваните форми са:
- Title →
dc:titleи Subject →dc:description:rdf:Altезикова алтернатива, сравнявана само срещу свояx-defaultзапис (езиковият таг се мачка без значение на регистра); Alt безx-defaultброи като липсващ - Author →
dc:creator:rdf:Seqс точно един текстов запис, носещ целия Info string, така че списък от автори, разделен с точки и запетаи, остава един-единствен запис - Keywords →
pdf:Keywordsи Producer →pdf:Producer(namespacehttp://ns.adobe.com/pdf/1.3/): прости текстови свойства - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://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>
Текстовите стойности се сравняват като точни последователности от Unicode code point-и, без подрязване, сгъване на регистра или нормализация. Интервал в края, или precomposed é от едната страна и decomposed e плюс комбиниращо ударение от другата, е истинско разминаване. Info страната винаги идва от собствения декодинг на PDFium на PDFDocEncoding и UTF-16 текст чрез FPDF_GetMetaText, което спира библиотеката да пре-имплементира string декодиране и да го обърка фино; XMP страната е толкова чиста, колкото байтовете, които са я родили, затова и codepage капаните, които развалят 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. Когато нито една не носи зона, локалните компоненти се сравняват такива, каквито са записани. Когато само едната страна има зона, резултатът е pamsValueMismatch, защото измислянето на отместване би било позьор. Ненулева дробна секунда като .250 в XMP също налага разминаване, тъй като PDF дата няма как да я изрази, а тихото ѝ закръгляване би скрило истинско несъответствие; .000 се приема. Непарсващи се стойности се докладват поотделно като pamsInvalidInfoDate или pamsInvalidXmpDate
Наличието има свое правило. TPdfAMetadataValues.Present е множество, пълнено от обхождането на /Info речника на активния trailer, и държи „ключ отсъства“ отделно от „ключ присъства с празен string“. Липсващ ключ дава pamsNotRequired и не иска нищо от XMP; /Title () е налице, така че XMP packet-ът трябва да носи и празна x-default title
Как инспектирате Info и XMP метаданни преди запис?
TPdf.InspectPdfAMetadata връща TPdfAMetadataReport с по един TPdfAMetadataComparison на поле, всеки носещ Info стойността, XMP стойността и TPdfAMetadataState, така че провал може да бъде обяснен без reverse-engineering на един-единствен validation флаг. MismatchFields обобщава провалилото се множество, 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 речника като източник на истина и пренаписва само XMP свойствата, чието поле е кацнало в MismatchFields; всичко останало в packet-а оцелява. Title и Subject се записват в x-default записа, докато другите езикови алтернативи остават непокътнати, Author става rdf:Seq с един запис, непознати namespaces и несвързани свойства се запазват, а XMP свойства за липсващи Info ключове не се пипат. Info дата със зона се записва като канонична UTC XMP дата със суфикс Z; такава без зона пази своите локални компоненти. File overload-ът записва през временен файл и атомарна подмяна, а самият XMP update се долепя като incremental update
Отказите са нарочни. Без XMP packet методът вдига EPdfError, защото изграждането на пълен PDF/A identification и метаданен набор е работата на 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 дата или нечитаем 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 от шаблон. Единственият капан е Present: record, инициализиран с 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;
Ако вашият pipeline архивира документи, които други системи продължават да редактират, съчетайте nightly InspectPdfAMetadata обхождане с NormalizePdfAMetadata за файловете, които са се отдръпнали, и дръжте ValidatePdfA като гейт, преди каквото и да е тръгне към дългосрочно съхранение. Типизираният report, пътят на поправка и останалият PDF/A инструментариум се доставят в PDFium Component за Delphi и C++Builder