Műszaki cikk

PDF/A archiválási megfelelőség Delphiben PDFium VCL segítségével

Ön szállít egy konvertert, amely minden fájlt PDF/A-1b formátumúnak jelöl meg, az ügyfél nyilvántartási rendszere egy évig fogadja őket, majd egy audit során a teljes tételt átfuttatják a veraPDF-en, és a harmaduk nem megfelelő minősítést kap. Semmi sem omlott össze, nem keletkezett kivétel, a fájlok jól nyílnak meg az asztalán lévő összes nézőkében. Egyszerűen csak nem feleltek meg annak a szabványnak, amit rájuk bélyegzett. Ez az archiválásra szánt PDF-ek tipikus hibaállapota, és ez a magyarázata annak, hogy a „beállítottuk a jelzőt” állítás soha nem egyenértékű azzal, hogy „érvényes és hitelesített”

Az első dolog, amit meg kell érteni a PDFiummal és a PDF/A-val kapcsolatban, hogy a motornak semmi köze hozzá. A PDFium rendereli, elemzi és írja a PDF-et, de a nyilvános felülete nem tartalmaz ConvertToPDFA metódust, sem OutputIntent írót vagy XMP API-t. Az archiválási megfelelőség minden része — az XMP csomag, az OutputIntent és annak ICC profilja, a katalógus jelzői (catalog markers) és az érvényesítés — magában a PDFiumPas-ban található, egy nagyjából 2000 soros tiszta Pascal egységben (FPdfPdfa.pas), amely elemzi a mentett bájtokat, és inkrementális frissítéssel újraírja azokat. Ha tudja, hol folyik a munka, azt is tudni fogja, hol bújkálnak a hibák — és ezek nem a PDFiumban vannak

Mit követel meg valójában a PDF/A, és hol adódhatnak problémák

A PDF/A nem egyetlen formátum. Az ISO 19005 három részt határoz meg (PDF/A-1, -2, -3), és mindegyiken belül különböző megfelelőségi szinteket, amelyek eltérő dolgokat ígérnek. A 'B' (basic, alapvető) szint csak azt garantálja, hogy a vizuális megjelenés reprodukálható. Az 'A' (accessible, akadálymentes) szint ezen felül egy címkézett struktúrafát (tagged structure tree) és Unicode leképezést ad a 'B'-hez. Az 'U' szint, amely csak a 2. és 3. résznél létezik, a kettő között helyezkedik el: megbízható Unicode szöveg a teljes struktúrafa nélkül. Az ISO 19005-1 nem tartalmaz 'U' szintet, ezt a korlátozást a könyvtár közvetlenül kódolja

A formátum néhány szabálya az, amely a gyakorlatban problémát okozhat. A titkosítás teljesen tilos (ISO 19005-1 §6.1.3 és utódai): a PDF/A fájl nem tartalmazhat /Encrypt szótárat. A dokumentumnak deklarálnia kell a kimeneti renderelési feltételt egy OutputIntent-en keresztül, amelynek célja egy érvényes ICC profil (§6.2.3.2). Magának a megfelelőségi állításnak XMP metaadatként kell megjelennie a PDF/A azonosítási séma alatt. Az 'A' szint ezen felül megköveteli a §6.8 szerinti logikai struktúrát, azaz a dokumentumot géppel olvashatóvá tevő címkefát (tag tree). Ha ezek közül bármelyik hiányzik, a megfelelőség-ellenőrző elutasítja a fájlt, még akkor is, ha az tökéletesen renderelődik

Az egyetlen hívás, amely archívumot hoz létre

A PDFiumPas a teljes folyamatot a TPdf.SaveAsPdfA metódus mögé rejti. Az egyszerűbb túlterhelés egy célmegfelelőséget fogad, és alapértelmezés szerint a PDF/A-1b szintre áll be, ami a helyes választás az „ez a dokumentum örökre renderelhető legyen” általános esetre

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;

A színfalak mögött ez egy kétlépcsős folyamat. A SaveAsPdfA először megkéri a PDFiumot, hogy szerializálja a dokumentumot az FPDF_SaveAsCopy segítségével, majd átadja ezt a bájtfolyamot az InjectPdfAMarkers-nek, amely hozzáfűzi az XMP metaadatokat, az sRGB OutputIntent-et a beágyazott ICC profiljával, és egy újraírt katalógust inkrementális frissítésként. A forrás a nullás pozíciótól kerül beolvasásra, és a cél a nullás pozíciótól kerül kiírásra; az eredeti objektumfa érintetlen marad, a jelölők pedig a meglévő %%EOF után kerülnek beillesztésre. Ha fájl helyett bájtokra van szüksége, a SaveAsPdfAToStream egy TStream-et és ugyanezeket az opciókat fogadja

Megfelelőség kiválasztása az opciórekord segítségével

Egy konkrét rész és szint célzásához adjon át egy TPdfASaveOptions rekordot. Annak Conformance mezője egy TPdfAConformance értéket fogad. A felsorolás minden érvényes kombinációt lefed és semmi mást: pac1b, pac1a az 1. részhez; pac2b, pac2u, pac2a a 2. részhez; pac3b, pac3u, pac3a a 3. részhez, valamint a pacUnknown és pacNone az ellenőrzési oldalon. Nincs pac1u, mert ez a szint nem létezik a szabványban

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;

A rekord nagy része üresen maradhat. Ha a Title, Author, Subject, Keywords, Creator és Producer mezőket üresen hagyja, a SaveAsPdfA automatikusan kitölti azokat a dokumentum Info szótárából az FPDF_GetMetaText segítségével. Ha a CreationDate és ModDate mezőket üresen hagyja, a rendszer az aktuális UTC időt használja mindkét XMP dátumhoz. Ha a DocumentId and InstanceId mezőket üresen hagyja, a könyvtár előre feltölti azokat az FPDF_GetFileIdentifier alapján, visszalépve a forrásbájtokból származtatott determinisztikus azonosítóra. Az egyetlen mező, amelyet érdemes lehet tudatosan felülírni, az az IccProfileData: ha üres, az a beépített sRGB IEC61966-2.1 profilt jelenti, de egy CMYK or szürkeárnyalatos munkafolyamatnak saját profilt kell biztosítania

Miért fokozódik le az A-szint, és miért ez a becsületes választás

Íme egy apróság, amely megzavarhatja azokat, akik a jelzőt garanciának tekintik. Kérhet pac1a szintet egy olyan dokumentumnál, amelynek nincs címkefája, de a PDF/A-1a megköveteli a §6.8 szerinti logikai struktúrát, a könyvtár pedig nem tud felépíteni egy struktúrafát egy címkézetlen PDF-ből. Ahelyett, hogy olyan fájlt bocsátana ki, amely 'A' szintet állít, miközben nem felel meg annak, a SaveAsPdfA ellenőrzi a valódi címkézett struktúrát (/StructTreeRoot valamint /MarkInfo, ahol a /Marked true), és ha hiányzik, leminősíti az állítást: a pac1a-ból pac1b lesz, a pac2a-ból pac2b, és így tovább mindhárom részben. A belső segédfunkciók a PdfAIsLevelA és a PdfADowngradeToLevelB

Az indoklást érdemes egyértelműen megfogalmazni: egy olyan fájl, amely őszintén deklarálja a teljesített szintet, hasznosabb, mint az, amelyik hazudik egy olyan szintről, amelyet nem ér el. Az 'U' szintet másképp kezeljük. A valódi Unicode lefedettség észlelése egy naiv „tartalmaz-e /ToUnicode-ot” tesztet jelentene, amely túlkorrigálná (leminősítené) a legitim dokumentumokat (a WinAnsi és hasonló kódolások kivételt képeznek), így a mentési oldal úgy bocsátja ki az 'U' szintű állítást, ahogy a hívó deklarálta, és az eltérést inkább az ellenőrzési oldalon hagyja megjelölni. Ha garantált 'A' szintű archívumra van szüksége, címkézze meg a dokumentumot az átalakítás előtt; a konverter nem fog olyan struktúrát kitalálni, ami nincs ott

Az ICC buktatója, amelyet csak egy valódi ellenőrző észlel

Ez az a hiba, amely a legkeményebb leckét tanította meg, mert a könyvtár saját ellenőrzője átengedte, míg a veraPDF, az ISO 19005 referencia-ellenőrzője nem. A PDF/A megköveteli, hogy az OutputIntent célprofilja érvényes ICCBased adatfolyam legyen, a §6.2.3.2 pedig arra kötelezi az ellenőrzőt, hogy ezt az adatfolyamot színterekként ellenőrizze. Egy ICCBased adatfolyamnek deklarálnia kell a /N kulcsot, a színkomponensek számát. Az injektor egy korai verziója az ICC adatfolyam szótárát csak a /Length kulccsal írta meg, a /N nélkül, és a veraPDF elutasította az eredményt a következő üzenettel: „The N entry (value null)... is missing”

Ami ezt alattomossá tette, az az, hogy az elutasítás csak a PDF/A-1b és -1a esetében lépett életbe. A 2. és 3. rész megfelelőségi modelljei nem futtatták le ezt az ellenőrzést a célprofilon, így a megegyező injektált struktúra sikeresen megfelelt a pac2b, pac3b és pac2u vizsgálatokon, de elbukott a pac1b alatt, csupán a pdfaid:part értéke miatt. Egy egységteszt ezt soha nem láthatta, mert a könyvtár saját ValidatePdfACompliance metódusa csak azt ellenőrizte, hogy a /DestOutputProfile kulcs létezik-e, de azt nem, hogy mi található a folyam szótárában. A belső tesztek zöldek maradtak; a valódi archiválási ellenőrzés elbukott

A javítás az IccComponentCount, amely leolvassa a színadat-tér aláírását az ICC fejléc 16-os eltolásánál, és leképezi azt egy komponensszámra: a GRAY értéke 1, az RGB , Lab és XYZ értéke 3, a CMYK értéke 4, az ismeretlen profil pedig alapértelmezetten 3. Ez a szám bekerül a folyam szótárába /N kulcsként. Ez kiszámításra kerül, nem pedig 3-ra van égetve, így az a hívó, aki CMYK vagy szürkeárnyalatos profilt ad meg az IccProfileData-n keresztül, továbbra is a helyes értéket kapja. A tágabb tanulság módszertani: a könyvtáron belüli ellenőrzőnek és egy hiteles ellenőrzőnek egyaránt megvannak a maguk vakfoltjai, és a PDF/A kimenetet végponttól végpontig tesztelni kell egy referencia-implementációval, mint például a veraPDF, ahelyett, hogy bízna az önellenőrzésben. A tiszta archívumok mögötti megegyező inkrementális frissítési fegyelemről szól a tömörített objektum- és xref-folyamok érvényesítéséről szóló cikk, ami azért fontos, mert az injektor által feldolgozott modern PDF-ek gyakran kereszthivatkozási folyamokra (cross-reference streams) épülnek

Titkosítás, xref folyamok és egyéb határterületek

Mivel az ISO 19005 tiltja a titkosítást, a mentési útvonal a kiírás előtt megfosztja ettől a fájlt. A SaveAsPdfA az FPDF_REMOVE_SECURITY beállítást alkalmazza a szerializálás során, így a titkosított forrás (a jelszóval betöltve) a dokumentum archívumba kerülésekor visszafejtésre kerül. Egy titkosítatlan dokumentum esetében ez egy semleges művelet (no-op), és semmin nem változtat. Ennek következménye ugyanaz a korlátozás, amelyet a HotPDF a másik irányból kényszerít ki: egyetlen fájl nem lehet egyszerre titkosított és PDF/A formátumú. Ha egy munkafolyamatnak mindkettőre szüksége van, a megoldás két különálló dokumentum: egy titkosított másolat a terjesztéshez és egy különálló tiszta másolat az archívumhoz

Még egy részlet láthatatlan, amíg problémát nem okoz: az olyan PDF 1.5+ dokumentumok, amelyek tiszta kereszthivatkozási folyamot (cross-reference stream) használnak, és nem tartalmaznak trailer kulcsszót. Az injektor beolvassa a trailert, hogy megtalálja a forrás /Info szekciót, és hozzáfűzze az inkrementális frissítést, és el kell fogadnia az xref-stream formátumot, különben egy ilyen dokumentumot a jelölők csendes elvetésével másolna át. Az ISO 32000-1 §7.5.6 kifejezetten engedélyezi egy klasszikus trailer inkrementális frissítésének követését egy xref-stream dokumentum után, ahol a /Prev az xref-stream eltolására mutat — ez pontosan az a struktúra, amelyet az injektor kibocsát. A PDFium saját FPDF_SaveAsCopy funkciója mindig klasszikus trailert ír ki, így a normál folyamatban az injektor soha nem találkozik tiszta xref-stream forrással, de az olvasási útvonal kezeli ezt a más honnan érkező dokumentumok esetében

Ellenőrzés, mielőtt bízna az állításban

A könyvtár tartalmaz egy bájtszintű ellenőrzőt, a TPdf.ValidatePdfA-t, amely egy TPdfAValidationResult-ot ad vissza. Annak Conformance mezője jelzi az észlelt szintet, az Issues pedig a TPdfAValidationIssue értékek készlete; a kényelmi IsCompliant csak akkor igaz, ha valódi szintet észlelt, és a hibák halmaza üres. Futtassa ezt gyors első kapuként egy kötegelt folyamatban

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;

Legyen őszinte azzal kapcsolatban, hogy ez mit jelent Önnek. A bájtszintű ellenőrző nagy biztonsággal észleli a strukturális problémákat (hiányzó OutputIntent, tiltott művelet, meglévő /Encrypt, transzparencia ott, ahol az 1. rész tiltja), a betűtípus-beágyazás észlelése pedig egy olyan számlálási heurisztikát használ, amely szándékosan csak nagy biztonságú jelet jelent, nem pedig a glifenkénti lefedettséget követi. Amit nem csinál, az a tartalomfolyam-operátor (content-stream operator) elemzése, amelyhez teljes tartalomelemzőre lenne szükség, és ez tervezésénél fogva kívül esik a hatókörén. A kiadási kapuhoz párosítsa a könyvtáron belüli ellenőrzőt a veraPDF-fel: az ellenőrző azonnali és mindenhol fut DLL nélkül, a veraPDF pedig hiteles. Ennek a párosításnak a kötegelt futtatásba történő beillesztése a tárgya a kötegelt preflight jelentés CLI-ről szóló cikknek, ahová ez az ellenőrzés tartozik egy valódi archiválási folyamatban

Az itt bemutatott SaveAsPdfA, InjectPdfAMarkers és ValidatePdfA API-k a Delphi, C++Builder és Lazarus/FPC rendszerekhez készült PDFium komponenssel együtt kerülnek szállításra. A termékoldal linkeli a teljes API-referenciát, beleértve a teljes megfelelőségi felsorolást és az ezen példák mögött álló opciórekordot