Techninis straipsnis

PDF/A Info ir XMP metaduomenų atitikimas Delphi

PDFium Component tikrina PDF/A Info ir XMP metaduomenų atitikimą su TPdf.InspectPdfAMetadata ir sutvarko jį su TPdf.NormalizePdfAMetadata. ISO 19005-1 (su Cor.1 pataisomis) reikalauja, kad kiekvienas iš aštuonių susietų Info įrašų — nuo Title iki ModDate — neštų tokią pačią reikšmę kaip jo XMP savybė, o ne tiesiog egzistuotų; patikra skaito teisingą RDF formą, vardų erdves gretina pagal URI, o datas lygina kaip momentus

Defektų pranešimas, paprastai atveriantis šį pokalbį, atrodo nekenksmingas. Dokumentų valdymo sistema kiekvieno inkrementinio įrašymo metu įspaudžia naują /ModDate į Info žodyną, XMP paketo neliečia, ir po šešių mėnesių archyvo auditas pažymi tūkstančius failų kaip neatitinkančius. Abi datos yra. Jos tiesiog nustojo sutarti pirmajame redagavime, o buvimo patikra to niekada nepastebėjo. Title redagavimai per tik Info palaikančią API ir Author eilutė, tokia kaip Finance; Controlling, kurią kažkoks įrankis perskyrė į du dc:creator elementus, klysta taip pat

Kodėl PDF/A atmeta metaduomenis, esančius abiejose vietose?

PDF/A to neatmeta, nes ISO 19005-1 §6.7.3 yra reikšmės taisyklė, o ne buvimo taisyklė: 1 lentelė susieja aštuonis Info raktus su XMP savybėmis, ir kai Info raktas jau yra, susietoji XMP savybė privalo turėti atitikmenį. Baitų lygio skeneris, aprašytas PDF/A preflight patikroje su PDFium Component, tik patvirtina, kad xmp:CreateDate ir xmp:ModifyDate egzistuoja (pvaiMissingXmpDates). Nuo v3.72.0 TPdf.ValidatePdfA papildomai atlieka pilną reikšmių palyginimą ir prideda pvaiInfoXmpValueMismatch prie problemų aibės, kai XMP paketas yra, bet nesutampa su Info (neparsėjamas paketas skaitomas kaip nesutampantis). Trūkstamas paketas lieka pranešamas kaip pvaiMissingXmpMetadata, tad dvi problemos niekada nedubliuoja to paties defekto

Kokios RDF formos reikia kiekvienai susietai XMP savybei?

Kiekvienas iš aštuonių susiejimų turi fiksuotą XMP tipą, ir teisinga reikšmė netinkamame konteineryje vis tiek žlunga. ComparePdfAInfoAndXmp faile FPdfPdfa.pas savybes suranda pagal vardų erdvės URI, tad paketas, pririšantis http://purl.org/dc/elements/1.1/ prie neįprasto priešdėlio, skaitomas lygiai taip pat kaip naudojantis dc. Reikalingos formos yra:

  • Title → dc:title ir Subject → dc:description: rdf:Alt kalbinė alternatyva, lyginama tik su jos x-default elementu (kalbos žyma gretinama neatsižvelgiant į raidžių dydį); Alt be x-default skaitomas kaip trūkstamas
  • Author → dc:creator: rdf:Seq su lygiai vienu teksto elementu, laikančiu visą Info eilutę, tad kabliataškiu skiriama autorių sąrašas lieka vienu įrašu
  • Keywords → pdf:Keywords ir Producer → pdf:Producer (vardų erdvė http://ns.adobe.com/pdf/1.3/): paprastos teksto savybės
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (vardų erdvė http://ns.adobe.com/xap/1.0/): paprastos teksto savybės
Aštuonios susietos poros, kurias ComparePdfAInfoAndXmp tikrina dėl PDF/A metaduomenų atitikimo Delphi: Title ir Subject reikalauja rdf:Alt su x-default elementu, Author — vieno elemento rdf:Seq, Keywords, Producer, Creator ir abi datos yra paprastas tekstas, kiekviena surandama pagal XMP vardų erdvės URI, o ne priešdėlį
Kai Info raktas jau yra, ISO 19005-1 reikalauja, kad susietoji XMP savybė turėtų atitinkamą reikšmę reikalaujamoje RDF formoje, tad reikšmė netinkamame konteineryje vis tiek žlunga
<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>

Tekstinės reikšmės lyginamos kaip tikslios Unicode kodo taškų sekos, be jokio apkarpytmo, raidžių dydžio suvienodinimo ar normalizacijos. Tarpas gale arba sukomponuota é vienoje pusėje ir išardyta e su jungiamuoju kirčiu kitoje — tikras nesutapimas. Info pusė visada ateina iš PDFium pačios PDFDocEncoding ir UTF-16 teksto dekodavimo per FPDF_GetMetaText, kas neleidžia bibliotekai iš naujo įgyvendinti eilučių dekodavimo ir padaryti subtilių klaidų; XMP pusė tokia švari, kokie buvo baitai, iš kurių ji atsirado, todėl koduoklių spąstai, gadinantys XMP metaduomenis Free Pascal, čia irgi svarbūs

Kada PDF data ir XMP data yra lygios?

PDF data ir XMP data lygios, kai abi aprašo tą patį momentą iki sekundės, su vienoda laiko juostos žinote abiejose pusėse. Abu parseriai priima teisėtą sutrumpintą tikslumą, tad D:2026 ir 2026 abu reiškia 2026 metų sausio 1, 00:00:00. Kai abi reikšmės neša laiko juostą, jos prieš palyginimą konvertuojamos į UTC: D:20260827093659+08'00' lygi 2026-08-27T01:36:59Z. Kai nei viena neša laiko juostos, vietiniai komponentai lyginami tokie, kokie parašyti. Kai tik viena pusė turi laiko juostą, rezultatas pamsValueMismatch, nes išgalvotas poslinkis būtų spėjimas. Ne nulinė trupmeninė sekundė, tokia kaip .250 XMP, irgi priverčia nesutapimą, nes PDF data neturi kaip jos išreikšti, o tyliai suapvalinus būtų paslėptas tikras nesutarimas; .000 priimama. Neparsėjamos reikšmės pranešamos atskirai kaip pamsInvalidInfoDate arba pamsInvalidXmpDate

Kaip PDFium Component sprendžia, kad PDF Info data ir XMP data Delphi lygios: dvi juostos konvertuojamos į UTC ir lyginami momentai, dvi be juostos reikšmės lyginamos tokios, kokios parašytos, viena juosta — pamsValueMismatch, ne nulinė trupmeninė sekundė neišreiškiama, o neparsėjamos reikšmės pranešamos atskirai
Lygybė reiškia tą patį momentą iki sekundės su vienoda laiko juostos žinote abiejose pusėse, tad išgalvotas poslinkis ar apvalinta trupmeninė sekundė paslėptų tikrą nesutarimą

Buvimas turi savo taisyklę. TPdfAMetadataValues.Present yra aibė, užpildoma apeinant aktyvaus trailer /Info žodyną, ir ji „rakto nėra“ laiko atskirai nuo „raktas yra su tuščia eilute“. Trūkstamas raktas duoda pamsNotRequired ir iš XMP nereikalauja nieko; /Title () yra, tad XMP paketas privalo nešti ir tuščią x-default pavadinimą

Kaip apžiūrėti Info ir XMP metaduomenis prieš išsaugant?

TPdf.InspectPdfAMetadata grąžina TPdfAMetadataReport su vienu TPdfAMetadataComparison laukui, kiekvienas laikantis Info reikšmę, XMP reikšmę ir TPdfAMetadataState, tad nesėkmę galima paaiškinti neįmintinant nė vienos patikros žymės. MismatchFields apibendrina žlungančią aibę, HasXmpPacket pasako, ar paketas rastas, o XmpParseError neša parserio pranešimą, kai paketas yra, bet neperskaitomas

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;

Ką keičia NormalizePdfAMetadata ir ką atsisako?

TPdf.NormalizePdfAMetadata Info žodyną laiko tiesos šaltiniu ir perrašo tik tas XMP savybes, kurių laukas pakliuvo į MismatchFields; visa kita pakete išgyvena. Title ir Subject rašomi į x-default elementą, kol kitos kalbinės alternatyvos lieka nepaliestos, Author tampa vieno elemento rdf:Seq, nežinomos vardų erdvės ir nesusijusios savybės išsaugomos, o XMP savybės trūkstamiems Info raktams paliekamos nelietos. Info data su laiko juosta rašoma kaip kanoninė UTC XMP data su Z priesaga; be juostos — išlaiko savo vietinius komponentus. Failo perkrova išsaugo per laikinąjį failą ir atomišką pakeitimą, o pats XMP atnaujinimas prijungiamas kaip inkrementinis atnaujinimas

Ką NormalizePdfAMetadata perrašo taisydamas PDF/A metaduomenis Delphi su PDFium Component: Info yra tiesos šaltinis, tik MismatchFields įrašai grąžinami kaip x-default Alt tekstas, vieno elemento Seq arba kanoninė UTC data, o nežinomos vardų erdvės, nesusijusios savybės ir trūkstamų raktų savybės išgyvena nepaliestos
Taisymas atsisako trūkstamo XMP paketo, sugadintos Info datos ir pasirašytų dokumentų, nes pilno PDF/A metaduomenų rinkinio sudarymas yra SaveAsPdfA darbas, o ne taikytinio atitikimo pataisymas

Atsisakymai sąmoningi. Be XMP paketo metodas kelia EPdfError, nes pilno PDF/A identifikacijos ir metaduomenų rinkinio sudarymas yra SaveAsPdfA darbas, aptartas PDF/A archyvinių failų kūrime su PDFium Component. Sugadinta Info data kelia EPdfXmpError, užuot įrašiusi tikėtinai atrodančią klaidingą reikšmę, ir niekas neišsaugoma. Pasirašyti dokumentai atmetami, nebent kvietėjas perduoda AllowSignedDocument = True. Atitikimas yra viena ISO 19005-1 taisyklė, tad suvienodintas failas dar nėra automatiškai atitinkantis

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;                      // jau suderinta, failo neliečiame
    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      // sugadinta Info data ar neperskaitomas paketas
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Palyginimo paleidimas savo XMP pakete

ComparePdfAInfoAndXmp ir SynchronizePdfAInfoToXmp yra paprastos funkcijos FPdfPdfa, dirbančios su TPdfXmpPacket be įkėto dokumento, kas tinka vienetų testams ir srautams, surenkantiems XMP iš šablono. Vienas spąstų yra Present: įrašas, inicializuotas su Default(TPdfAMetadataValues), turi tuščią aibę, kiekvienas laukas tada praneša pamsNotRequired, ir palyginimas praeina tuščiai, kokių besiveršių būtumėte užpildęs

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 nusprendžia, kurie laukai privalomi; vienos reikšmės ignoruojamos
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

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

Jei jūsų srautas archyvuoja dokumentus, kuriuos kitos sistemos nuolat redaguoja, derinkite naktinį InspectPdfAMetadata apžiūrėjimą su NormalizePdfAMetadata tiems failams, kurie nuslysta, ir laikykite ValidatePdfA vartais, dar ką nors išleisdami į ilgalaikį saugojimą. Tipizuota ataskaita, taisymo kelias ir likusi PDF/A įrankių dalis atkeliauja su PDFium Component for Delphi and C++Builder