Tehnički članak

PDF/A Info i XMP ekvivalenca metapodataka u Delphi-ju

PDFium Component proverava PDF/A Info-XMP ekvivalencu metapodataka sa TPdf.InspectPdfAMetadata i popravlja je sa TPdf.NormalizePdfAMetadata. ISO 19005-1 (kako ga ispravlja Cor.1) traži da svaki od osam mapiranih Info unosa, od Title do ModDate, nosi istu vrednost kao njegovo XMP svojstvo, ne samo da postoji; provera čita ispravan RDF oblik, poklapa namespace-ove po URI i poredi datume kao trenutke

Izveštaj o bugu koji obično pokrene ovaj razgovor deluje bezazleno. Sistem za upravljanje dokumentima utiskuje novi /ModDate u Info rečnik pri svakom inkrementalnom čuvanju, ostavlja XMP paket na miru, i šest meseci kasnije arhivski audit označi hiljade fajlova kao neusaglašene. Oba datuma su tu. Samo su prestali da se slažu od prve izmene, i provera prisutnosti nikada nije primetila. Izmene naslova učinjene kroz Info-only API, i Author string poput Finance; Controlling koji je neki alat razbio na dva dc:creator unosa, padaju na isti način

Zašto PDF/A odbija metapodatke koji postoje na oba mesta?

PDF/A ga odbija jer je ISO 19005-1 §6.7.3 pravilo o vrednosti, ne o prisutnosti: Tabela 1 mapira osam Info ključeva na XMP svojstva, i jednom kad je Info ključ prisutan, mapirano XMP svojstvo mora da nosi ekvivalentnu vrednost. Skenator na nivou bajtova opisan u PDF/A preflight validaciji sa PDFium Component-om samo potvrđuje da xmp:CreateDate i xmp:ModifyDate postoje (pvaiMissingXmpDates). Od v3.72.0, TPdf.ValidatePdfA dodatno pokreće puno poređenje vrednosti i dodaje pvaiInfoXmpValueMismatch u skup problema kad XMP paket postoji ali se ne slaže sa Info (paket koji ne može da se parsira računa se kao neslaganje). Nedostajući paket ostaje prijavljen kao pvaiMissingXmpMetadata, pa se ta dva problema nikada ne računaju dva puta za isti defekt

Koji RDF oblik treba svako mapirano XMP svojstvo?

Svako od osam mapiranja ima fiksni XMP tip, i ispravna vrednost u pogrešnom kontejneru i dalje pada. ComparePdfAInfoAndXmp u FPdfPdfa.pas traži svojstva po namespace URI, pa se paket koji veže http://purl.org/dc/elements/1.1/ za neobičan prefiks čita tačno kao onaj koji koristi dc. Traženi oblici su:

  • Title → dc:title i Subject → dc:description: rdf:Alt jezička alternativa, poređena samo sa svojim x-default unosom (jezička oznaka poklapa se neosećljivo na velika i mala slova); Alt bez x-default računa se kao nedostajući
  • Author → dc:creator: rdf:Seq sa tačno jednim tekstualnim unosom koji drži ceo Info string, pa lista autora razdvojena tačka-zapetom ostaje jedan unos
  • Keywords → pdf:Keywords i Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): prosta tekstualna svojstva
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): prosta tekstualna svojstva
Osam mapiranih parova koje ComparePdfAInfoAndXmp proverava za PDF/A ekvivalencu metapodataka u Delphi-ju: Title i Subject traže rdf:Alt sa x-default unosom, Author rdf:Seq od jednog unosa, Keywords, Producer, Creator i oba datuma su prost tekst, svako traženo po XMP namespace URI a ne po prefiksu
Jednom kad je Info ključ prisutan, ISO 19005-1 traži da mapirano XMP svojstvo nosi ekvivalentnu vrednost u traženom RDF obliku, pa vrednost u pogrešnom kontejneru i dalje pada
<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>

Tekstualne vrednosti porede se kao tačne sekvence Unicode kodnih tačaka, bez odsecanja, savijanja velikih i malih slova ili normalizacije. Prazan razmak na kraju, ili prekompozitno é na jednoj strani i dekompozitno e plus kombinacioni akcenat na drugoj, prava su nepoklapanja. Info strana uvek dolazi iz sopstvenog dekodovanja PDFium-a PDFDocEncoding i UTF-16 teksta kroz FPDF_GetMetaText, što sprečava biblioteku da ponovo implementira dekodovanje string-ova i pogreši suptilno; XMP strana je čista onoliko koliko su čisti bajtovi koji su je proizveli, pa codepage zamke koje kvare XMP metapodatke pod Free Pascal-om i ovde igraju ulogu

Kada su PDF datum i XMP datum jednaki?

PDF datum i XMP datum jednaki su kad opisuju isti trenutak do sekunde, sa istim znanjem o vremenskoj zoni na obe strane. Oba parsera prihvataju legalnu umanjenu preciznost, pa D:2026 i 2026 oba znače 1. januar 2026, 00:00:00. Kad obe vrednosti nose zonu, konvertuju se u UTC pre poređenja: D:20260827093659+08'00' jednako je 2026-08-27T01:36:59Z. Kad ni jedna ne nosi zonu, lokalne komponente porede se kako su zapisane. Kad samo jedna strana ima zonu, rezultat je pamsValueMismatch, jer bi izmišljanje pomeraja bilo nagađanje. Nenulti razlomljeni deo sekunde poput .250 u XMP takođe nameće nepoklapanje, jer PDF datum nema načina da ga izrazi i tiho zaokruživanje bi sakrilo pravo neslaganje; .000 se prihvata. Nerasparsirive vrednosti prijavljuju se odvojeno kao pamsInvalidInfoDate ili pamsInvalidXmpDate

Kako PDFium Component odlučuje da su PDF Info datum i XMP datum jednaki u Delphi-ju: dve zone konvertuju se u UTC i porede trenutci, dve vrednosti bez zone porede se kako su zapisane, jedna zona sama je pamsValueMismatch, nenulti razlomljeni deo sekunde ne može se izraziti, a nerasparsirive vrednosti prijavljuju se odvojeno
Jednakost znači isti trenutak do sekunde sa istim znanjem o vremenskoj zoni na obe strane, pa bi izmišljanje pomeraja ili zaokruživanje razlomljene sekunde sakrilo pravo neslaganje

Prisutnost ima sopstveno pravilo. TPdfAMetadataValues.Present je skup koji se puni šetnjom kroz /Info rečnik aktivnog trailer-a, i on drži „ključ odsutan“ odvojeno od „ključ prisutan sa praznim string-om“. Odsutan ključ daje pamsNotRequired i ništa ne traži od XMP-a; /Title () je prisutan, pa XMP paket mora da nosi i prazan naslov x-default

Kako pregledati Info i XMP metapodatke pre čuvanja?

TPdf.InspectPdfAMetadata vraća TPdfAMetadataReport sa jednim TPdfAMetadataComparison po polju, od kojih svaki nosi Info vrednost, XMP vrednost i TPdfAMetadataState, pa se neuspeh može objasniti bez inženjeringa unatraške jedne validacione zastavice. MismatchFields sumira skup koji pada, HasXmpPacket govori da li je paket nađen, i XmpParseError nosi poruku parsera kad paket postoji ali ne može da se pročita

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;

Šta NormalizePdfAMetadata menja, a šta odbija?

TPdf.NormalizePdfAMetadata tretira Info rečnik kao izvor istine i preslovljava samo XMP svojstva čije je polje sletelo u MismatchFields; sve ostalo u paketu preživi. Title i Subject pišu se u x-default unos dok druge jezičke alternative ostaju netaknute, Author postaje rdf:Seq od jednog unosa, nepoznati namespace-ovi i nesrodna svojstva čuvaju se, i XMP svojstva za odsute Info ključeve ostavljaju se na miru. Info datum sa zonom piše se kao kanonski UTC XMP datum sa sufiksom Z; onaj bez zone čuva svoje lokalne komponente. File overload čuva kroz privremeni fajl i atomsku zamenu, i sam XMP update dodaje se kao inkrementalni update

Šta NormalizePdfAMetadata preslovljava pri popravci PDF/A metapodataka u Delphi-ju sa PDFium Component-om: Info je izvor istine, samo unosi iz MismatchFields pišu se nazad kao x-default Alt tekst, Seq od jednog unosa ili kanonski UTC datum, dok nepoznati namespace-ovi, nesrodna svojstva i svojstva odsutnih ključeva prežive netaknuta
Popravka odbija nedostajući XMP paket, neispravan Info datum i potpisane dokumente, jer je gradnja kompletnog seta PDF/A metapodataka posao SaveAsPdfA-e, ne ciljane popravke ekvivalencije

Odbijanja su namerna. Bez XMP paketa metoda podiže EPdfError, jer je gradnja kompletnog seta PDF/A identifikacije i metapodataka posao SaveAsPdfA-e, pokriven u pravljenju PDF/A arhivskih fajlova sa PDFium Component-om. Neispravan Info datum podiže EPdfXmpError umesto da upiše uverljivo izgledajuću pogrešnu vrednost, i ništa se ne čuva. Potpisani dokumenti se odbijaju osim ako pozivalac prosledi AllowSignedDocument = True. Ekvivalenca je i sama jedna od pravila ISO 19005-1, pa normalizovan fajl nije automatski usaglašen

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;                      // već usklađeno, ostavi fajl na miru
    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      // neispravan Info datum ili nečitljiv paket
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Pokretanje poređenja na sopstvenom XMP paketu

ComparePdfAInfoAndXmp i SynchronizePdfAInfoToXmp su obične funkcije u FPdfPdfa-i koje rade na TPdfXmpPacket-u bez učitanog dokumenta, što odgovara unit testovima i pipeline-ovima koji sklapaju XMP iz šablona. Jedna zamka je Present: record inicijalizovan sa Default(TPdfAMetadataValues) ima prazan skup, svako polje tada prijavljuje pamsNotRequired, i poređenje prolazi prazno ma šta ste popunili

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 odlučuje koja su polja obavezna; same vrednosti se ignorišu
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

  Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
  try
    Changed := SynchronizePdfAInfoToXmp(Info, Packet);
    // xmp:ModifyDate je sada 2026-08-27T01:36:59Z, dc:creator rdf:Seq od jednog unosa
    if Changed <> [] then
      TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
  finally
    Packet.Free;
  end;
end;

Ako Vaš pipeline arhivira dokumente koje drugi sistemi nastavljaju da uređuju, uparite noćni presek InspectPdfAMetadata sa NormalizePdfAMetadata za fajlove koji odstupe, i držite ValidatePdfA kao kapiju pre nego što bilo šta ode u dugoročno skladištenje. Tipizirani izveštaj, putanja popravke i ostatak PDF/A alata isporučuju se u PDFium Component-u za Delphi i C++Builder