PDFium Component sprawdza równoważność metadanych Info–XMP w PDF/A przez TPdf.InspectPdfAMetadata, a naprawia ją przez TPdf.NormalizePdfAMetadata. ISO 19005-1 (po korekcie Cor.1) wymaga, by każdy z ośmiu mapowanych wpisów Info, od Title po ModDate, niósł tę samą wartość co jego właściwość XMP — nie tylko, by istniał; sprawdzenie czyta poprawny kształt RDF, dopasowuje przestrzenie nazw po URI i porównuje daty jako momenty
Zgłoszenie błędu, które zwykle rozpoczyna tę rozmowę, wygląda niewinnie. System zarządzania dokumentami wpiasuje nowe /ModDate do słownika Info przy każdym zapisie przyrostowym, zostawia pakiet XMP w spokoju, a pół roku później audyt archiwum flaguje tysiące plików jako niezgodne. Obie daty są. Po prostu przestały się zgadzać przy pierwszej edycji, a sprawdzenie obecności nigdy tego nie zauważyło. Edycje tytułu przez API tylko-Info oraz string Autora jak Finance; Controlling, który jakieś narzędzie rozbiło na dwa wpisy dc:creator, zawodzą tak samo
Dlaczego PDF/A odrzuca metadane obecne w obu miejscach?
PDF/A odrzuca je, bo ISO 19005-1 §6.7.3 to reguła wartości, nie reguła obecności: tabela 1 mapuje osiem kluczy Info na właściwości XMP, a skoro klucz Info jest obecny, zmapowana właściwość XMP musi trzymać równoważną wartość. Skaner bajtowy opisany w walidacji preflight PDF/A z PDFium Component potwierdza tylko, że xmp:CreateDate i xmp:ModifyDate istnieją (pvaiMissingXmpDates). Od v3.72.0 TPdf.ValidatePdfA dodatkowo uruchamia pełne porównanie wartości i dokłada pvaiInfoXmpValueMismatch do zbioru problemów, gdy pakiet XMP istnieje, ale rozmija się z Info (pakiet, którego nie da się sparsować, liczy się jako rozmijający się). Brakujący pakiet dalej jest raportowany jako pvaiMissingXmpMetadata, więc oba problemy nigdy nie liczą dwa razy tej samej wady
Jakiego kształtu RDF potrzebuje każda mapowana właściwość XMP?
Każde z ośmiu mapowań ma stały typ XMP, i poprawna wartość w złym pojemniku nadal zawodzi. ComparePdfAInfoAndXmp w FPdfPdfa.pas wyszukuje właściwości po URI przestrzeni nazw, więc pakiet wiążący http://purl.org/dc/elements/1.1/ z nietypowym prefiksem jest czytany dokładnie tak samo jak ten używający dc. Wymagane kształty to:
- Title →
dc:titleoraz Subject →dc:description: alternatywa językowardf:Alt, porównywana wyłącznie z jej wpisemx-default(tag języka jest dopasowywany bez rozróżniania wielkości liter); Alt bezx-defaultliczy się jako brak - Author →
dc:creator:rdf:Seqz dokładnie jednym wpisem tekstowym niosącym cały string Info, więc lista autorów rozdzielana średnikami pozostaje jednym wpisem - Keywords →
pdf:Keywordsoraz Producer →pdf:Producer(przestrzeń nazwhttp://ns.adobe.com/pdf/1.3/): proste właściwości tekstowe - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(przestrzeń nazwhttp://ns.adobe.com/xap/1.0/): proste właściwości tekstowe
<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>
Wartości tekstowe są porównywane jako dokładne sekwencje punktów kodowych Unicode, bez ucinania białych znaków, składania wielkości liter ani normalizacji. Spacja na końcu albo złożone é z jednej strony i rozłożone e plus łączący akcent z drugiej to prawdziwa niezgodność. Strona Info zawsze pochodzi z własnego dekodowania PDFium tekstu PDFDocEncoding i UTF-16 przez FPDF_GetMetaText, co chroni bibliotekę przed reimplementacją dekodowania stringów i subtelną pomyłką; strona XMP jest tak czysta, jak bajty, które ją urodziły — dlatego pułapki stron kodowych psujące metadane XMP pod Free Pascalem mają tu też znaczenie
Kiedy data PDF i data XMP są równe?
Data PDF i data XMP są równe, gdy opisują ten sam moment z dokładnością do sekundy i tą samą wiedzą o strefie czasowej po obu stronach. Oba parsery akceptują legalną obniżoną precyzję, więc D:2026 i 2026 znaczą oba 1 stycznia 2026, 00:00:00. Gdy obie wartości niosą strefę, są konwertowane na UTC przed porównaniem: D:20260827093659+08'00' równa się 2026-08-27T01:36:59Z. Gdy żadna nie niesie strefy, lokalne składniki są porównywane tak, jak zapisano. Gdy tylko jedna strona ma strefę, wynikiem jest pamsValueMismatch, bo wymyślenie offsetu byłoby zgadywaniem. Niezerowa ułamkowa sekunda jak .250 w XMP też wymusza niezgodność, bo data PDF nie ma jak jej wyrazić, a ciche jej zaokrąglenie ukryłoby realną niezgodę; .000 jest akceptowane. Wartości nieparsowalne są raportowane osobno jako pamsInvalidInfoDate albo pamsInvalidXmpDate
Obecność ma własną regułę. TPdfAMetadataValues.Present to zbiór wypełniany przez przejście po słowniku /Info aktywnego traileru, i rozdziela „klucza brak” od „klucz obecny z pustym stringiem”. Nieobecny klucz daje pamsNotRequired i nie żąda niczego od XMP; /Title () jest obecny, więc pakiet XMP musi nieść także pusty tytuł x-default
Jak obejrzeć metadane Info i XMP przed zapisem?
TPdf.InspectPdfAMetadata zwraca TPdfAMetadataReport z jednym TPdfAMetadataComparison na pole, każdy niesie wartość Info, wartość XMP i TPdfAMetadataState, więc porażkę da się wyjaśnić bez reverse-engineeringu pojedynczej flagi walidacji. MismatchFields streszcza zbiór niezgodnych pól, HasXmpPacket mówi, czy pakiet został znaleziony, a XmpParseError niesie komunikat parsera, gdy pakiet istnieje, ale nie da się go przeczytać
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;
Co zmienia NormalizePdfAMetadata i czego odmawia?
TPdf.NormalizePdfAMetadata traktuje słownik Info jako źródło prawdy i przepisuje wyłącznie właściwości XMP, których pole wylądowało w MismatchFields; wszystko inne w pakiecie przeżywa. Title i Subject są wpisywane do wpisu x-default, podczas gdy inne alternatywy językowe zostają nietknięte, Author staje się jednoelementowym rdf:Seq, nieznane przestrzenie nazw i niepowiązane właściwości są zachowywane, a właściwości XMP dla nieobecnych kluczy Info zostają nietknięte. Data Info ze strefą jest zapisywana jako kanoniczna data XMP w UTC z sufiksem Z; data bez strefy zachowuje swoje lokalne składniki. Overload plikowy zapisuje przez plik tymczasowy i atomową podmianę, a sama aktualizacja XMP jest doklejana jako aktualizacja przyrostowa
Odmowy są celowe. Bez pakietu XMP metoda rzuca EPdfError, bo budowa kompletnej identyfikacji i zestawu metadanych PDF/A to robota SaveAsPdfA, opisana w tekście o tworzeniu plików archiwalnych PDF/A z PDFium Component. Zniekształcona data Info rzuca EPdfXmpError, zamiast zapisać wiarygodnie wyglądającą złą wartość, i nic nie jest zapisywane. Dokumenty podpisane są odrzucane, dopóki wywołujący nie przekaże AllowSignedDocument = True. Równoważność to też tylko jedna z reguł ISO 19005-1, więc znormalizowany plik nie jest automatycznie zgodny
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; // już spójne, zostaw plik w spokoju
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 // zniekształcona data Info albo nieczytelny pakiet
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
Uruchamianie porównania na własnym pakiecie XMP
ComparePdfAInfoAndXmp i SynchronizePdfAInfoToXmp to zwykłe funkcje w FPdfPdfa działające na TPdfXmpPacket bez wczytanego dokumentu, co pasuje testom jednostkowym i potokom składającym XMP z szablonu. Jedna pułapka to Present: rekord zainicjalizowany przez Default(TPdfAMetadataValues) ma pusty zbiór, każde pole raportuje wtedy pamsNotRequired, a porównanie przechodzi próżniowo, niezależnie od tego, jakie wartości wpisałeś
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 rozstrzyga, które pola są obowiązkowe; same wartości są ignorowane
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// xmp:ModifyDate to teraz 2026-08-27T01:36:59Z, dc:creator to jednoelementowy rdf:Seq
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
Jeśli twój potok archiwizuje dokumenty, które inne systemy ciągle edytują, sparuj nocny przegląd InspectPdfAMetadata z NormalizePdfAMetadata dla plików, które odjechały, i trzymaj ValidatePdfA jako bramkę, zanim cokolwiek odjedzie do długoterminowego magazynu. Typowany raport, ścieżka naprawy i reszta narzędzi PDF/A jest w PDFium Component dla Delphi i C++Buildera