Tekninen artikkeli

PDF/A:n Info- ja XMP-metadatan vastaavuus Delphissä

PDFium Component tarkistaa PDF/A:n Info-XMP-metadatan vastaavuuden TPdf.InspectPdfAMetadatailla ja korjaa sen TPdf.NormalizePdfAMetadatailla. ISO 19005-1 (Cor.1:n oikaisemana) vaatii jokaisen kahdeksan yhdistetyn Info-merkinnän, Titlesta ModDateen, kantavan samaa arvoa kuin sen XMP-ominaisuus, ei ainoastaan olemassaoloa; tarkistus lukee oikean RDF-muodon, vertaa nimiavaruuksia URI:lla ja vertaa päivämääriä hetkinä

Vikaraportti joka yleensä käynnistää tämän keskustelun näyttää harmitomalta. Asiakirjanhallintajärjestelmä leimaa uuden /ModDatein Info-sanakirjaan jokaisella inkrementaalisella tallennuksella, jättää XMP-paketin rauhaan, ja puoli vuotta myöhemmin arkistotarkastus liputtaa tuhansia tiedostoja vaatimustenvastaisiksi. Molemmat päivämäärät ovat siellä. Ne vain lakkasivat sopimasta ensimmäisessä muokkauksessa, eikä läsnäolotarkistus koskaan huomannut. Title-muokkaukset pelkän Info-API:n kautta sekä Author-merkkijono kuten Finance; Controlling jonka jokin työkalu jakoi kahdeksi dc:creator-merkinnäksi epäonnistuvat samalla tavalla

Miksi PDF/A hylkää metadatan joka on olemassa molemmissa paikoissa?

PDF/A hylkää sen koska ISO 19005-1 §6.7.3 on arvosääntö, ei läsnäolosääntö: Taulukko 1 yhdistää kahdeksan Info-avainta XMP-ominaisuuksiin, ja kun Info-avain on läsnä, yhdistetyn XMP-ominaisuuden on pidettävä vastaavaa arvoa. Artikkelissa PDF/A-preflight-validointi PDFium Componentilla kuvattu tavutason skanneri vain varmistaa että xmp:CreateDate ja xmp:ModifyDate ovat olemassa (pvaiMissingXmpDates). Versiosta v3.72.0 lähtien TPdf.ValidatePdfA ajaa lisäksi koko arvovertailun ja lisää pvaiInfoXmpValueMismatchin ongelmajoukkoon kun XMP-paketti on olemassa mutta on eri mieltä Infosta (jäsentymätön paketti lasketaan eri mieltä olevaksi). Puuttuva paketti raportoidaan yhä pvaiMissingXmpMetadataina, joten kaksi ongelmaa eivät koskaan laske samaa vikaa kahdesti

Mitä RDF-muotoa jokainen yhdistetty XMP-ominaisuus tarvitsee?

Jokaisella kahdeksasta yhdistämisestä on kiinteä XMP-tyyppi, ja oikea arvo väärässä säiliössä epäonnistuu silti. ComparePdfAInfoAndXmp tiedostossa FPdfPdfa.pas etsii ominaisuudet nimiavaruuden URI:lla, joten paketti joka sitoo http://purl.org/dc/elements/1.1/in epätavalliseen etuliitteeseen luetaan täsmälleen kuten dcää käyttävä. Vaaditut muodot ovat:

  • Title → dc:title ja Subject → dc:description: rdf:Alt-kielivaihtoehto, vertaillaan vain sen x-default-merkintää vasten (kielitag täsmäytetään kirjainkokoa riippumattomasti); Alt ilman x-defaultia lasketaan puuttuvaksi
  • Author → dc:creator: rdf:Seq jossa on täsmälleen yksi tekstimerkintä joka pitää koko Info-merkkijonon, joten puolipisteillä erotettu author-luettelo pysyy yhtenä merkintänä
  • Keywords → pdf:Keywords ja Producer → pdf:Producer (nimiavaruus http://ns.adobe.com/pdf/1.3/): yksinkertaisia tekstiominaisuuksia
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (nimiavaruus http://ns.adobe.com/xap/1.0/): yksinkertaisia tekstiominaisuuksia
Kahdeksan yhdistettyä paria jotka ComparePdfAInfoAndXmp tarkistaa PDF/A-metadatan vastaavuudesta Delphissä: Title ja Subject tarvitsevat rdf:Alt:n x-default-merkinnällä, Author yhden merkinnän rdf:Seq:n, Keywords, Producer, Creator ja kaksi päivämäärää ovat yksinkertaista tekstiä, kukin etsitty XMP-nimiavaruuden URI:lla eikä etuliitteellä
Kun Info-avain on läsnä, ISO 19005-1 vaatii yhdistetyn XMP-ominaisuuden pitävän vastaavaa arvoa vaaditussa RDF-muodossa, joten arvo väärässä säiliössä epäonnistuu silti
<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>

Tekstiarvot vertaillaan tarkkoina Unicode-koodipisteseensseinä ilman trimmausta, kirjainkokojen muunnosta tai normalisointia. Loppu välilyönti, tai prekompositoitu é toisella puolella ja dekompositoitu e plus yhdistävä aksentti toisella, on aito ristiriita. Infopuoli tulee aina PDFiumin omasta PDFDocEncoding- ja UTF-16-tekstin dekoodauksesta FPDF_GetMetaTextin kautta, mikä estää kirjastoa toteuttamasta merkkijonodekoodausta uudelleen ja saamasta sitä hienovaraisesti väärin; XMP-puoli on yhtä puhdas kuin sitä tuottaneet tavut, minkä vuoksi koodisivuansat jotka korruptoivat XMP-metadataa Free Pascalissa merkitsevät täälläkin

Milloin PDF-päivämäärä ja XMP-päivämäärä ovat yhtä suuret?

PDF-päivämäärä ja XMP-päivämäärä ovat yhtä suuret kun ne kuvaavat saman hetken sekunnin tarkkuudella samalla aikavyöhyketiedolla molemmilla puolilla. Molemmat jäsennykset hyväksyvät laillisen kutistetun tarkkuuden, joten D:2026 ja 2026 tarkoittavat molemmat 1. tammikuuta 2026 kello 00:00:00. Kun molemmat arvot kantavat vyöhykettä, ne konvertoitu UTC:hin ennen vertailua: D:20260827093659+08'00' on yhtä suuri kuin 2026-08-27T01:36:59Z. Kun kumpikaan ei kanna vyöhykettä, paikalliset komponentit vertaillaan kirjoitettuina. Kun vain toisella puolella on vyöhyke, tulos on pamsValueMismatch, koska siirtymän keksiminen olisi arvaus. Nollasta poikkeava murtosekunti kuten .250 XMP:ssä pakottaa myös ristiriidan, koska PDF-päivämäärällä ei ole tapaa ilmaista sitä ja hiljainen pyöristys piilottaisi todellisen erimielisyyden; .000 hyväksytään. Jäsennyskelvottomat arvot raportoidaan erikseen pamsInvalidInfoDateina tai pamsInvalidXmpDateina

Miten PDFium Component päättää että PDF:n Info-päivämäärä ja XMP-päivämäärä ovat yhtä suuret Delphissä: kaksi vyöhykettä konvertoituu UTC:hin ja vertaa hetkiä, kaksi vyöhykkeetöntä arvoa vertaillaan kirjoitettuina, yksin vyöhyke on pamsValueMismatch, nollasta poikkeavaa murtosekuntia ei voi ilmaista, ja jäsennyskelvottomat arvot raportoidaan erikseen
Yhtäsuuruus tarkoittaa samaa hetkeä sekunnin tarkkuudella samalla aikavyöhyketiedolla molemmilla puolilla, joten siirtymän keksiminen tai murtosekunnin pyöristäminen pois piilottaisi todellisen erimielisyyden

Läsnäololla on oma sääntönsä. TPdfAMetadataValues.Present on joukko joka täyttyy käymällä aktiivisen trailerin /Info-sanakirjan läpi, ja se pitää ”avain puuttuu” erillään ”avain läsnä tyhjällä merkkijonolla”. Puuttuva avain tuottaa pamsNotRequiredin eikä vaadi mitään XMP:ltä; /Title () on läsnä, joten XMP-paketin on kannettava myös tyhjä x-default-title

Miten tarkastelet Info- ja XMP-metadataa ennen tallennusta?

TPdf.InspectPdfAMetadata palauttaa TPdfAMetadataReportin yhdellä TPdfAMetadataComparisonilla kenttää kohti, kukin kantaa Info-arvon, XMP-arvon ja TPdfAMetadataStaten, joten epäonnistuminen voidaan selittää kääntämättä yhtään validointilippua takaperin. MismatchFields tiivistää epäonnistuvan joukon, HasXmpPacket kertoo löytyikö pakettia, ja XmpParseError kantaa jäsentimen viestin kun paketti on olemassa mutta sitä ei voi lukea

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;

Mitä NormalizePdfAMetadata muuttaa ja mistä se kieltäytyy?

TPdf.NormalizePdfAMetadata käsittelee Info-sanakirjan totuuden lähteenä ja kirjoittaa uudelleen vain ne XMP-ominaisuudet joiden kenttä päätyi MismatchFieldsiin; kaikki muu paketissa selviää. Title ja Subject kirjoitetaan x-default-merkintään kun taas muut kielivaihtoehdot pysyvät ehjinä, Author muuttuu yhden merkinnän rdf:Seqiksi, tuntemattomat nimiavaruudet ja asiaan liittymättömät ominaisuudet säilytetään, ja puuttuvien Info-avainten XMP-ominaisuuksia ei kosketeta. Vyöhykkeellinen Info-päivämäärä kirjoitetaan kanoniseksi UTC-XMP-päivämääräksi Z-päätteellä; vyöhykkeetön säilyttää paikalliset komponenttinsa. Tiedosto-overload tallentaa väliaikaisen tiedoston ja atomin korvauksen kautta, ja itse XMP-päivitys liitetään inkrementaalisena päivityksenä

Mitä NormalizePdfAMetadata kirjoittaa uudelleen korjatessaan PDF/A-metadataa Delphissä PDFium Componentilla: Info on totuuden lähde, vain MismatchFields-merkinnät kirjoitetaan takaisin x-default-Alt-tekstinä, yhden merkinnän Seq:nä tai kanonisena UTC-päivämääränä, kun taas tuntemattomat nimiavaruudet, asiaan liittymättömät ominaisuudet ja puuttuvien avainten ominaisuudet selviävät koskemattomina
Korjaus kieltäytyy puuttuvasta XMP-paketista, muotoillusta Info-päivämäärästä ja allekirjoitetuista asiakirjoista, koska täydellisen PDF/A-metadatasetin rakentaminen on SaveAsPdfA:n työtä, ei kohdennetun vastaavuuskorjauksen

Kieltäytymiset ovat tahallisia. Ilman XMP-pakettia metodi nostaa EPdfErrorin, koska täydellisen PDF/A-identifikaation ja metadatasetin rakentaminen on SaveAsPdfAn työtä, josta kertoo PDF/A-arkistotiedostojen luominen PDFium Componentilla. Muotoillut Info-päivämäärä nostaa EPdfXmpErrorin sen sijaan että kirjoittaisi uskottavan näköisen väärän arvon, eikä mitään tallenneta. Allekirjoitetut asiakirjat hylätään ellet kutsuja välitä AllowSignedDocument = True:ta. Vastaavuus on myös yksi ISO 19005-1:n säännöistä, joten normalisoitu tiedosto ei ole automaattisesti vaatimustenmukainen

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;                      // jo yhdenmukainen, jätä tiedosto rauhaan
    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      // muotoillut Info-päivämäärä tai lukukelvoton paketti
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Vertailun ajaminen omalle XMP-paketillesi

ComparePdfAInfoAndXmp ja SynchronizePdfAInfoToXmp ovat tavallisia funktioita FPdfPdfassa jotka toimivat TPdfXmpPacketilla ilman ladattua asiakirjaa, mikä sopii yksikkötesteihin ja putkiin jotka kokoavat XMP:n mallipohjasta. Yksi ansa on Present: Default(TPdfAMetadataValues)illa alustetulla recordilla on tyhjä joukko, jolloin jokainen kenttä raportoi pamsNotRequiredin ja vertailu läpäisee tyhjänpäiväisesti riippumatta siitä mitä arvoja täytit

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 päättää mitkä kentät ovat pakollisia; pelkät arvot ohitetaan
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

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

Jos putkesi arkistoi asiakirjoja joita muut järjestelmät jatkuvasti muokkaavat, yhdistä yöllinen InspectPdfAMetadata-kierros NormalizePdfAMetadatain kanssa ajautuneille tiedostoille ja pidä ValidatePdfA porttina ennen kuin mikään lähtee pitkäaikaissäilytykseen. Tyypitetty raportti, korjauspolku ja loput PDF/A-työkaluista toimitetaan PDFium Component for Delphi and C++Builder -komponentissa