Tekninen artikkeli

PDF/A-arkistointiyhteensopivuus Delphissä PDFium VCL -komponentilla

Toimitat muuntimen, joka merkitsee jokaisen tiedoston PDF/A-1b-yhteensopivaksi. Asiakkaan arkistointijärjestelmä ottaa niitä vastaan vuoden ajan, ja sitten tarkastus ajaa koko erän veraPDF-validaattorin läpi, jolloin kolmasosa tiedostoista paljastuu yhteensopimattomiksi. Mikään ei kaatunut, yhtään poikkeusta ei heitetty, ja tiedostot avautuvat hienosti kaikilla työpöytäsi katseluohjelmilla. Ne eivät vain olleet sen standardin mukaisia, jonka niihin leimasit. Tämä on PDF-arkistoinnin tyypillinen vikatila, ja se on syy siihen, miksi "asetimme lipun" ei koskaan vastaa väitettä "se läpäisee validoinnin"

Ensimmäinen asia, joka on ymmärrettävä PDFiumista ja PDF/A-standardista, on se, että moottorilla itsellään ei ole mitään tekemistä sen kanssa. PDFium renderöi, jäsentää ja kirjoittaa PDF-tiedostoja, mutta sen julkisessa rajapinnassa ei ole metodia ConvertToPDFA, OutputIntent-kirjoittajaa tai XMP-API:a. Jokainen arkistointiyhteensopivuuden osa – XMP-paketti, OutputIntent ja sen ICC-profiili, luettelomerkinnät (catalog markers), validointi – sijaitsee itse PDFiumPas-yksikössä, noin 2 000 rivin puhtaassa Pascal-yksikössä (FPdfPdfa.pas), joka jäsentää tallennetut tavut ja kirjoittaa ne uudelleen inkrementaalisen päivityksen kautta. Tieto siitä, missä työ tehdään, paljastaa missä virheet piileskelevät, eivätkä ne piileksi PDFiumissa

Mitä PDF/A todellisuudessa vaatii ja missä se vaikeutuu

PDF/A ei ole vain yksi formaatti. ISO 19005 määrittelee kolme osaa (PDF/A-1, -2, -3) ja jokaisen osan sisällä yhteensopivuustasot (conformance levels), jotka lupaavat eri asioita. B-taso (basic) takaa ainoastaan sen, että visuaalinen ulkoasu on toisinnettavissa. A-taso (accessible) lisää tagitetun rakennekuvauksen (structure tree) ja Unicode-kartoituksen B-tason päälle. U-taso, joka on olemassa vain osissa 2 ja 3, sijoittuu näiden väliin tarjoten luotettavan Unicode-tekstin ilman täyttä rakennekuvausta. Standardissa ISO 19005-1 ei ole U-tasoa, minkä rajoituksen kirjasto koodaa suoraan

Kourallinen formaatin sääntöjä on niitä, jotka aiheuttavat vaikeuksia käytännössä. Salaus on ehdottomasti kielletty (ISO 19005-1 §6.1.3 ja sen seuraajat): PDF/A-tiedosto ei saa sisältää /Encrypt-sanakirjaa. Asiakirjan on ilmoitettava tulostuksen renderöintiolosuhde OutputIntent-merkinnällä, jonka kohde on validi ICC-profiili (§6.2.3.2). Yhteensopivuusväitteen on itsessään näyttävä XMP-metatietona PDF/A-tunnistusskeeman alla. A-taso vaatii lisäksi pykälän §6.8 mukaisen loogisen rakenteen eli tagipuun, joka tekee asiakirjasta koneluettavan. Jos jokin näistä puuttuu, yhteensopivuuden tarkistaja hylkää tiedoston, vaikka se renderöityisi täydellisesti

Yksi kutsu, joka tuottaa arkiston

PDFiumPas tuo näkyville koko prosessin TPdf.SaveAsPdfA-metodin takana. Yksinkertainen ylikuormitus ottaa kohdeyhteensopivuuden ja on oletuksena PDF/A-1b, mikä on oikea oletus yleisimmälle käyttötapaukselle "tee tästä ikuisesti renderöitävä"

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Konepellin alla tämä on kaksivaiheinen toimenpide. SaveAsPdfA pyytää ensin PDFiumia sarjallistamaan asiakirjan funktiolla FPDF_SaveAsCopy, ja antaa sitten tuon tavuvirran metodille InjectPdfAMarkers, joka lisää XMP-metatiedot, sRGB OutputIntent -määrityksen upotetulla ICC-profiililla sekä uudelleenkirjoitetun katalogin inkrementaalisena päivityksenä. Lähde luetaan kohdasta nolla ja kohde kirjoitetaan kohdasta nolla; alkuperäinen objektipuu jätetään ennalleen ja merkinnät sijoittuvat olemassa olevan %%EOF-merkinnän jälkeen. Jos tarvitset tiedoston sijaan tavut, SaveAsPdfAToStream ottaa TStream-olion ja samat asetukset

Yhteensopivuustason valitseminen asetusrekisterillä

Kohdistaaksesi tiettyyn osaan ja tasoon, välitä TPdfASaveOptions-tietue. Sen Conformance-kenttä ottaa TPdfAConformance-arvon. Luettelo (enumeration) kattaa jokaisen validin yhdistelmän eikä mitään muuta: pac1b, pac1a osalle 1; pac2b, pac2u, pac2a osalle 2; pac3b, pac3u, pac3a osalle 3, sekä pacUnknown ja pacNone validointipuolelle. Luettelossa ei ole arvoa pac1u, koska kyseistä tasoa ei ole olemassa standardissa

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

Suurin osa tietueesta voi jäädä tyhjäksi. Jätä kentät Title, Author, Subject, Keywords, Creator ja Producer tyhjiksi, jolloin SaveAsPdfA täyttää ne automaattisesti asiakirjan Info-sanakirjasta FPDF_GetMetaText-funktiolla. Jätä CreationDate- ja ModDate-kentät tyhjiksi, jolloin se käyttää nykyistä UTC-aikaa molemmille XMP-päivämäärille. Jätä DocumentId ja InstanceId tyhjiksi, ja kirjasto esitäyttää ne FPDF_GetFileIdentifier-kutsun avulla, käyttäen varalta lähdetavuista laskettua determinististä ID-tunnusta. Ainoa kenttä, jonka saatat haluta tietoisesti korvata, on IccProfileData: tyhjä tarkoittaa mukana toimitettua sRGB IEC61966-2.1 -profiilia, mutta CMYK- tai harmaasävytyönkulun tulisi tarjota oma profiilinsa

Miksi A-taso alenee ja miksi se on rehellinen valinta

Tässä on hienovaraisuus, joka hämmentää ihmisiä, jotka odottavat lipun olevan tae yhteensopivuudesta. Voit pyytää tasoa pac1a asiakirjalle, jossa ei ole tagipuuta, mutta PDF/A-1a vaatii kohdan §6.8 mukaisen loogisen rakenteen, eikä kirjasto voi luoda rakennetta tyhjästä ilman tagitettua PDF-tiedostoa. Sen sijaan, että SaveAsPdfA tuottaisi tiedoston, joka väittää olevansa A-tasoa mutta epäonnistuu siinä, se tarkistaa todellisen tagitetun rakenteen (/StructTreeRoot ja /MarkInfo arvolla /Marked true). Jos tämä puuttuu, se alentaa yhteensopivuusvaatimusta: pac1a muuttuu muotoon pac1b, pac2a muotoon pac2b ja niin edelleen kaikissa kolmessa osassa. Sisäiset apuohjelmat ovat PdfAIsLevelA ja PdfADowngradeToLevelB

Perustelu on syytä sanoa suoraan: tiedosto, joka rehellisesti ilmoittaa tason, jonka se saavuttaa, on hyödyllisempi kuin tiedosto, joka valehtelee tasosta, jota se ei saavuta. U-tasoa käsitellään toisin. Aidon Unicode-kattavuuden tunnistaminen vaatisi naiivin "onko siinä /ToUnicode" -testin, joka alentaa turhaan legitiimejä asiakirjoja (WinAnsi ja vastaavat koodaukset ovat vapautettuja), joten tallennuspuoli tuottaa U-väitteen sellaisena kuin kutsuja sen määritti ja jättää ristiriidan havaitsemisen validointivaiheeseen. Jos tarvitset taatun A-tason arkiston, tagita asiakirja ennen muuntamista; muunnin ei keksii rakennetta, jota ei ole olemassa

ICC-sudenkuoppa, jonka vain todellinen validaattori havaitsee

Tämä oli se epäonnistuminen, joka opetti vaikeimman läksyn, koska kirjaston oma tarkistaja päästi sen läpi, mutta veraPDF (ISO 19005 -viitevalidaattori) ei. PDF/A vaatii, että OutputIntent-kohteen profiili on validi ICCBased-virta, ja pykälä §6.2.3.2 edellyttää, että tarkistaja validoi tuon virran väriavaruutena. ICCBased-virran on ilmoitettava komponenttien määrä /N. Injektorin varhainen versio kirjoitti ICC-virtasanakirjan vain /Length-tiedolla ilman /N-arvoa, jolloin veraPDF hylkäsi tuloksen virheellä "The N entry (value null)... is missing"

Tästä teki petollisen se, että hylkäys tapahtui ainoastaan tasoilla PDF/A-1b ja -1a. Osien 2 ja 3 yhteensopivuusmallit eivät ajaneet tätä nimenomaista tarkistusta kohdeprofiilille, joten täysin identtinen injektoitu rakenne validoitui tasoilla pac2b, pac3b ja pac2u, mutta epäonnistui tasolla pac1b pelkän pdfaid:part-arvon perusteella. Yksikkötesti ei voinut koskaan havaita tätä, koska kirjaston oma ValidatePdfACompliance tarkisti vain, että /DestOutputProfile-avain oli olemassa, muttei sitä, mitä virtasanakirjan sisällä oli. Sisäiset testit pysyivät vihreinä; todellinen arkistointivalidointi epäonnistui

Korjaus on IccComponentCount, joka lukee väriavaruuden allekirjoituksen ICC-otsikon siirtymästä 16 ja yhdistää sen komponenttien määrään: GRAY on 1, RGB , Lab ja XYZ ovat 3, CMYK on 4, ja tuntemattomalla profiililla oletuksena on 3. Tämä määrä asetetaan virtasanakirjaan arvolla /N. Se lasketaan – ei kovakoodata arvoon 3 – jotta kutsuja, joka tarjoaa CMYK- tai harmaasävyprofiilin IccProfileData-kentässä, saa edelleen oikean arvon. Laajempi opetus on metodologinen: kirjaston sisäisellä tarkistimella ja auktoritatiivisella validaattorilla on molemmilla omat sokeat pisteensä, ja PDF/A-tulosteet on testattava päästä päähän viitetoteutusta (kuten veraPDF) vasten sen sijaan, että luotettaisiin itsetarkistuksiin. Sama siistien arkistojen taustalla oleva inkrementaalisen päivityksen kurinalaisuus katetaan artikkelissa pakattujen objekti- ja xref-virtojen validoinnista, mikä on tärkeää, koska injektorin käyttämät nykyaikaiset PDF-tiedostot perustuvat usein ristiviitevirtoihin (cross-reference streams)

Salaus, xref-virrat ja muut erikoistapaukset

Koska ISO 19005 kieltää salauksen, tallennuspolku poistaa sen ennen kirjoittamista. SaveAsPdfA soveltaa FPDF_REMOVE_SECURITY-lippua sarjallistuksen yhteydessä, joten salattu lähde (joka on ladattu salasanallaan) puretaan matkalla arkistoon. Salaamattomalle asiakirjalle tämä ei tee mitään eikä muuta mitään. Johtopäätös on sama rajoitus, jonka HotPDF asettaa toisesta suunnasta: mikään yksittäinen tiedosto ei voi olla sekä salattu että PDF/A-yhteensopiva. Kun työnkulku vaatii molempia, ratkaisuna on kaksi erillistä tiedostoa: salattu kopio jakeluun ja erillinen puhdas kopio arkistoon

Vielä yksi erikoistapaus on näkymätön, kunnes se aiheuttaa ongelmia: PDF 1.5+ -asiakirjat, jotka käyttävät pelkkää ristiviitevirtaa (cross-reference stream) eivätkä sisällä trailer-avainsanaa. Injektori lukee trailer-osion löytääkseen lähteen /Info-sanakirjan ja lisätäkseen sen inkrementaalisen päivityksen, ja sen on hyväksyttävä xref-virran muoto, muutoin tällainen asiakirja kopioitaisiin eteenpäin siten, että merkinnät pudotettaisiin hiljaisesti pois. ISO 32000-1 §7.5.6 nimenomaisesti sallii klassisen trailer-inkrementaalipäivityksen seurata xref-virta-asiakirjaa, jolloin /Prev osoittaa xref-virran siirtymään, mikä on täsmälleen se rakenne, jonka injektori tuottaa. PDFiumin oma FPDF_SaveAsCopy kirjoittaa aina klassisen trailer-osion, joten normaalissa putkessa injektori ei koskaan kohtaa puhdasta xref-virtalähdettä, mutta lukupolku käsittelee sen muualta saapuvien asiakirjojen osalta

Verifiointi ennen kuin luotat väitteeseen

Kirjasto sisältää tavutason tarkistimen TPdf.ValidatePdfA, joka palauttaa TPdfAValidationResult-tietueen. Sen Conformance-kenttä ilmoittaa havaitun tason ja Issues on TPdfAValidationIssue-arvojen joukko; mukavuusmetodi IsCompliant on tosi vain silloin, kun todellinen taso on havaittu ja ongelmajoukko on tyhjä. Suorita se nopeana ensimmäisenä porttina eräajossa

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

Ole rehellinen siitä, mitä tämä tuo mukanaan. Tavutason tarkistaja havaitsee rakenteelliset ongelmat (puuttuva OutputIntent, kielletty toiminto, /Encrypt-avain, läpinäkyvyys siellä missä osa 1 sen kieltää) korkealla luottamuksella, ja fonttien upottamisen havaitseminen käyttää laskentaheuristiikkaa, joka raportoi vain vahvasti varmistetun signaalin sen sijaan, että analysoisi jokaisen yksittäisen glyyfin. Se mitä se ei tee, on sisältövirran operaattorien analysointi, mikä vaatisi täyden sisältöjäsentimen ja on jätetty suunnitellusti pois. Julkaisuvaiheen tarkistuksessa yhdistä kirjaston oma tarkistaja veraPDF-työkaluun: oma tarkistaja on välitön ja toimii kaikkialla ilman DLL-kirjastoja, kun taas veraPDF on auktoritatiivinen. Tämän parin kytkeminen eräajoon on aiheena artikkelissa eräajon esitarkastusraporttien CLI:stä, johon tämä validointi kuuluu todellisessa arkistointityönkulussa

Tässä näytetyt SaveAsPdfA-, InjectPdfAMarkers- ja ValidatePdfA-API:t toimitetaan PDFium-komponentin mukana Delphille, C++Builderille ja Lazarus/FPC-kehitysympäristöille. Tuotesivu sisältää linkin täyteen API-viitteeseen, mukaan lukien täydellinen yhteensopivuusluettelo ja asetusrekisteri näiden esimerkkien takana