Tehnički članak

PDF/A arhivska usklađenost u Delphiju uz PDFium VCL

Isporučite konverter koji svaku datoteku označi kao PDF/A-1b, korisnikov sustav evidencije ih godinu dana uredno prima, a onda revizija pusti cijelu seriju kroz veraPDF i trećina ih se vrati kao neusklađena. Ništa nije puklo, nijedna iznimka nije podignuta, datoteke se otvaraju normalno u svakom pregledniku na vašem stolu. Jednostavno nisu bile standard koji ste na njih utisnuli. To je uobičajeni obrazac neuspjeha za arhivski PDF, i zato tvrdnja "postavili smo zastavicu" nikad nije isto što i "valjano prolazi provjeru"

Prvo što treba razumjeti o PDFiumu i PDF/A jest da engine s time nema nikakve veze. PDFium renderira, parsira i zapisuje PDF, ali njegovo javno sučelje nema ConvertToPDFA, nema writer za OutputIntent i nema XMP API. Svaki dio arhivske usklađenosti - XMP paket, OutputIntent i njegov ICC profil, oznake u katalogu, provjera - živi u samom PDFiumPas-u, u otprilike 2,000 redaka čistog Pascal modula (FPdfPdfa.pas) koji parsira spremljene bajtove i ponovno ih zapisuje kroz inkrementalni update. Znati gdje se posao odvija govori vam gdje se skrivaju bugovi, a oni se ne skrivaju u PDFiumu

Što PDF/A doista zahtijeva i gdje grize

PDF/A nije jedan format. ISO 19005 definira tri dijela (PDF/A-1, -2, -3) i, unutar svakog, razine usklađenosti koje obećavaju različite stvari. Razina B (basic) jamči samo da je vizualni izgled reproducibilan. Razina A (accessible) nadograđuje B označenim stablom strukture i Unicode mapiranjem. Razina U, koja postoji samo za dijelove 2 i 3, nalazi se između njih: pouzdan Unicode tekst bez punog stabla strukture. ISO 19005-1 nema razinu U, a biblioteka tu ograničenost izravno kodira

Nekoliko pravila tog formata ona su koja u praksi doista grizu. Enkripcija je potpuno zabranjena (ISO 19005-1 §6.1.3 i kasnije verzije): PDF/A datoteka ne smije nositi /Encrypt rječnik. Dokument mora deklarirati uvjet izlaznog renderiranja kroz OutputIntent čije odredište je valjani ICC profil (§6.2.3.2). Sama tvrdnja o usklađenosti mora se pojaviti kao XMP metapodaci pod PDF/A identifikacijskom shemom. Razina A dodatno traži §6.8 logičku strukturu, stablo oznaka koje dokument čini strojno čitljivim. Propustite bilo što od toga i validator usklađenosti odbacuje datoteku iako se savršeno prikazuje

Jedan poziv koji proizvodi arhivu

PDFiumPas izlaže cijeli cjevovod iza TPdf.SaveAsPdfA. Jednostavni overload uzima ciljanu usklađenost i po zadanom koristi PDF/A-1b, što je prava početna vrijednost za čest slučaj "učini ovo trajno prikazivim"

Dijagram dvostupanjskog PDFium Component PDF/A tijeka spremanja u Delphiju gdje FPDF_SaveAsCopy serializira dokument, a InjectPdfAMarkers dodaje XMP metapodatke, sRGB OutputIntent i prepisani katalog kao jedno inkrementalno ažuriranje
SaveAsPdfA u dvije faze radi — PDFium dokument serijalizira, a zatim InjectPdfAMarkers arhivske markere nadodaje kao jedan inkrementalni update
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    // Zadana usklađenost je pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // datoteka sada nosi XMP, sRGB OutputIntent i oznake u katalogu
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Ispod haube ovo je dvofazni potez. SaveAsPdfA prvo traži od PDFiuma da serializira dokument pomoću FPDF_SaveAsCopy, zatim taj tok bajtova prosljeđuje InjectPdfAMarkers, koji dodaje XMP metapodatke, sRGB OutputIntent s ugrađenim ICC profilom i ponovno zapisan katalog kao inkrementalni update. Izvor se čita od pozicije nula, a odredište se zapisuje od pozicije nula; izvorno stablo objekata ostaje netaknuto, a oznake dolaze nakon postojećeg %%EOF. Ako trebate bajtove umjesto datoteke, SaveAsPdfAToStream uzima TStream i iste opcije

Odabir usklađenosti kroz records opcija

Da biste ciljali određeni dio i razinu, proslijedite TPdfASaveOptions record. Njegovo Conformance polje prima TPdfAConformance vrijednost. Enumeracija pokriva svaku valjanu kombinaciju i ništa drugo: pac1b, pac1a za dio 1; pac2b, pac2u, pac2a za dio 2; pac3b, pac3u, pac3a za dio 3, plus pacUnknown i pacNone za stranu provjere. Ne postoji pac1u, jer ta razina ne postoji u standardu

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: pouzdan Unicode tekst
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Ostavite IccProfileData prazno za korištenje ugrađenog sRGB IEC61966-2.1 profila
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

Većina recorda može ostati prazna. Ostavite Title, Author, Subject, Keywords, Creator i Producer prazne i SaveAsPdfA ih automatski popunjava iz dokumentova Info rječnika preko FPDF_GetMetaText. Ostavite CreationDate i ModDate prazne i koristi trenutačno UTC vrijeme za oba XMP datuma. Ostavite DocumentId i InstanceId prazne i biblioteka ih popunjava iz FPDF_GetFileIdentifier, uz rezervni pad na deterministički ID izveden iz izvornih bajtova. Jedino polje koje biste možda željeli namjerno prebrisati jest IccProfileData: prazno znači ugrađeni sRGB IEC61966-2.1 profil, ali CMYK ili grayscale radni tok trebali bi dati vlastiti

Zašto se razina A degradira i zašto je to pošten izbor

Evo suptilnosti koja sapliće ljude koji očekuju da je zastavica jamstvo. Možete zatražiti pac1a na dokumentu koji nema stablo oznaka, ali PDF/A-1a zahtijeva §6.8 logičku strukturu, a biblioteka ne može stvoriti stablo strukture iz neoznačenog PDF-a. Umjesto da izda datoteku koja tvrdi da je razina A, a pada na provjeri, SaveAsPdfA provjerava postoji li stvarna označena struktura (/StructTreeRoot plus /MarkInfo s /Marked true) i, ako je nema, spušta tvrdnju: pac1a postaje pac1b, pac2a postaje pac2b, i tako dalje kroz sva tri dijela. Interni pomoćnici su PdfAIsLevelA i PdfADowngradeToLevelB

Obrazloženje vrijedi reći jasno: datoteka koja pošteno deklarira razinu koju doista zadovoljava korisnija je od one koja laže o razini koju ne zadovoljava. Razina U tretira se drukčije. Prepoznavanje stvarne pokrivenosti Unicodea značilo bi naivan test "ima li /ToUnicode" koji bi pretjerano degradirao legitimne dokumente (WinAnsi i slična kodiranja su izuzeta), pa save strana emitira U tvrdnju onako kako ju je pozivatelj deklarirao i ostavlja da se neslaganje označi na strani provjere. Ako trebate zajamčenu razinu A arhivu, označite dokument prije pretvorbe; konverter neće izmišljati strukturu koja ondje ne postoji

Dijagram odluke koji prikazuje SaveAsPdfA u Delphiju kako degradira PDF/A tvrdnju Level A na Level B kada dokument ne nosi označeno stablo strukture, dok se Level U tvrdnja ispisuje kako je deklarirano
Stablo oznaka koje nedostaje tvrdnju pošteno razgrađuje — pac1a postaje pac1b, dok se Level U točno kako je deklarirano ispisuje i na validacijskoj strani sudi

ICC zamka koju otkrije samo pravi validator

Ovo je neuspjeh koji je naučio najtežu lekciju, jer je vlastiti checker biblioteke prošao, a veraPDF, referentni validator za ISO 19005, nije. PDF/A zahtijeva da odredišni profil OutputIntenta bude valjan ICCBased tok, a §6.2.3.2 nalaže validatoru da taj tok provjeri kao color space. ICCBased tok mora deklarirati /N, broj kanala boje. Rana verzija injektora zapisala je ICC stream rječnik samo s /Length i bez /N, a veraPDF je odbacio rezultat s porukom "The N entry (value null)... is missing"

Ono što je činilo grešku podmuklom jest to što se odbijanje pojavljivalo samo za PDF/A-1b i -1a. Modeli usklađenosti za dio 2 i dio 3 nisu pokretali tu konkretnu provjeru nad odredišnim profilom, pa je identična injektirana struktura prolazila pod pac2b, pac3b i pac2u ali padala pod pac1b samo zbog vrijednosti pdfaid:part. Jedan unit test to nikada ne bi vidio, jer je vlastiti ValidatePdfACompliance provjeravao samo postoji li ključ /DestOutputProfile, a ne što se nalazi unutar rječnika toka. Interni testovi ostali su zeleni; stvarna arhivska provjera nije uspjela

Popravak je IccComponentCount, koji čita potpis prostora boje podataka na offsetu 16 ICC zaglavlja i mapira ga na broj kanala: GRAY je 1, RGB , Lab i XYZ su 3, CMYK je 4, s nepoznatim profilom koji se zadano postavlja na 3. Taj broj ide u rječnik toka kao /N. Izračunava se, a nije čvrsto kodiran na 3, tako da pozivatelj koji dostavi CMYK ili grayscale profil kroz IccProfileData i dalje dobiva ispravnu vrijednost. Šira lekcija je metodološka: checker unutar biblioteke i autoritativni validator svaki imaju svoje slijepe točke, a PDF/A izlaz mora se testirati od kraja do kraja prema referentnoj implementaciji poput veraPDF-a, a ne oslanjati se samo na vlastite provjere. Ista disciplina inkrementalnog ažuriranja iza čistih arhiva obrađena je u provjeri komprimiranih objektnih i xref tokova, što je važno jer se moderni PDF-ovi koje injektor obrađuje često temelje na cross-reference tokovima

Enkripcija, xref streamovi i druge rubne situacije

Budući da ISO 19005 zabranjuje enkripciju, put spremanja uklanja je prije zapisivanja. SaveAsPdfA primjenjuje FPDF_REMOVE_SECURITY prilikom serializacije, pa se šifrirani izvor (učitan sa svojom lozinkom) dešifrira na putu u arhivu. Kod nešifriranog dokumenta ovo je no-op i ništa se ne mijenja. Posljedica je isto ograničenje koje HotPDF nameće s druge strane: jedna datoteka ne može istovremeno biti šifrirana i PDF/A. Kad radni tok treba oboje, odgovor su dva artefakta: šifrirana kopija za distribuciju i zasebna čista kopija za arhivu

Još jedna rubna situacija nevidljiva je dok vas ne ugrize: dokumenti PDF 1.5+ koji koriste čisti cross-reference tok i ne nose ključnu riječ trailer. Injektor čita trailer kako bi pronašao izvorni /Info i dodao svoj inkrementalni update, i mora prihvatiti oblik xref-toka, inače bi se takav dokument kopirao dalje s oznakama koje bi tiho ispale. ISO 32000-1 §7.5.6 izričito dopušta da klasični inkrementalni update s trailerom slijedi dokument s xref-tokom, s /Prev koji pokazuje na offset xref-toka, što je točno struktura koju injektor emitira. PDFiumov vlastiti FPDF_SaveAsCopy uvijek zapisuje klasični trailer, pa u normalnom cjevovodu injektor nikad ne susreće čisti izvor s xref-tokom, ali put čitanja to obrađuje za dokumente koji stižu odnekud drugdje

Provjera prije nego što povjerujete tvrdnji

Biblioteka dolazi s checkerom na razini bajtova, TPdf.ValidatePdfA, koji vraća TPdfAValidationResult. Njegovo polje Conformance javlja otkrivenu razinu, a Issues je skup vrijednosti TPdfAValidationIssue; pomoćna metoda IsCompliant istinita je samo kad je otkrivena stvarna razina i skup problema je prazan. Pokrenite ga kao brzu prvu prepreku u seriji

Dijagram IccComponentCount koji čita potpis ICC prostora boja na pomaku zaglavlja 16 radi pisanja /N unosa, nedostajućeg rječničkog unosa zbog kojeg je veraPDF odbijao PDF/A-1b datoteke koje je interni Delphi provjeritelj prolazio
IccComponentCount /N iz potpisa zaglavlja ICC-a izvodi, zatvarajući jaz koji je na datotekama dijela 1 hvatao samo veraPDF
var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
  Issue: TPdfAValidationIssue;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice_archive.pdf';
    Pdf.Active := True;
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      for Issue in Res.Issues do
        Writeln('Issue: ', Ord(Issue));
  finally
    Pdf.Free;
  end;
end;

Budite iskreni prema tome što vam ovo zapravo daje. Checker na razini bajtova hvata strukturne probleme (nedostajući OutputIntent, zabranjenu akciju, prisutan /Encrypt, transparentnost tamo gdje je dio 1 zabranjuje) s visokom pouzdanošću, a detekcija ugrađenih fontova koristi heuristiku brojanja koja namjerno javlja samo signal visoke pouzdanosti umjesto da lovi pokrivenost po glifu. Ono što ne radi jest analiza operatora content streama, za što bi bio potreban puni parser sadržaja i to je namjerno izvan opsega. Za release gate, uparite checker unutar biblioteke s veraPDF-om: checker je trenutačan i radi posvuda bez DLL-a, veraPDF je autoritativan. Ugradnja tog para u seriju je tema batch preflight report CLI-ja, gdje ta provjera i pripada u stvarnom arhivskom radnom toku

API-ji SaveAsPdfA, InjectPdfAMarkers i ValidatePdfA prikazani ovdje isporučuju se uz PDFium Component za Delphi, C++Builder i Lazarus/FPC. Stranica proizvoda vodi do potpune API reference, uključujući cjelovitu enumeraciju usklađenosti i zapis s opcijama iza ovih primjera