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"

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;

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.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;

Većina recorda može ostati prazna. Ostavite TitleTitle, AuthorAuthor, SubjectSubject, KeywordsKeywords, CreatorCreator, ProducerProducerSaveAsPdfA auto-popunjava ih 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

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 only checked that the /DestOutputProfile key existed, not what lived inside the stream dictionary. Internal tests stayed green; real archival validation failed

The fix is IccComponentCount, which reads the data colour space signature at offset 16 of the ICC header and maps it to a component count: GRAY is 1, RGB , Lab , and XYZ are 3, CMYK is 4, with an unknown profile defaulting to 3. That count goes into the stream dictionary as /N. It is computed, not hard-coded to 3, so that a caller who supplies a CMYK or grayscale profile through IccProfileData still gets the correct value. The broader lesson is methodological: the in-library checker and an authoritative validator each have blind spots, and PDF/A output has to be tested end to end against a reference implementation like veraPDF rather than trusted to self-checks. The same incremental-update discipline behind clean archives is covered in validating compressed object and xref streams, which matters because modern PDFs the injector consumes are often built on cross-reference streams

Enkripcija, xref streamovi i druge rubne situacije

Because ISO 19005 forbids encryption, the save path strips it before writing. SaveAsPdfA applies FPDF_REMOVE_SECURITY when serializing, so an encrypted source (loaded with its password) is decrypted on the way into the archive. On an unencrypted document this is a no-op and changes nothing. The corollary is the same constraint HotPDF enforces from the other direction: a single file cannot be both encrypted and PDF/A. When a workflow needs both, the answer is two artifacts, an encrypted copy for distribution and a separate clean copy for the archive

One more edge is invisible until it bites: PDF 1.5+ documents that use a pure cross-reference stream and carry no trailer keyword. The injector reads the trailer to find the source /Info and append its incremental update, and it has to accept the xref-stream form, otherwise such a document would be copied through with the markers silently dropped. ISO 32000-1 §7.5.6 explicitly permits a classic trailer incremental update to follow an xref-stream document, with /Prev pointing at the xref-stream offset, which is exactly the structure the injector emits. PDFium's own FPDF_SaveAsCopy always writes a classic trailer, so in the normal pipeline the injector never meets a pure xref-stream source, but the read path handles it for documents that arrive from elsewhere

Provjera prije nego što povjerujete tvrdnji

The library ships a byte-level checker, TPdf.ValidatePdfA, which returns a TPdfAValidationResult. Its Conformance field reports the detected level and Issues is a set of TPdfAValidationIssue values; the convenience method IsCompliant is true only when a real level was detected and the issue set is empty. Run it as a fast first gate in a batch

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;

Be honest about what this buys you. The byte-level checker catches structural problems (a missing OutputIntent, a forbidden action, a present /Encrypt, transparency where part 1 prohibits it) with high confidence, and font-embedding detection uses a count heuristic that deliberately reports only a high-confidence signal rather than chasing per-glyph coverage. What it does not do is content-stream operator analysis, which would require a full content parser and is out of scope by design. For a release gate, pair the in-library checker with veraPDF: the checker is instant and runs everywhere with no DLL, veraPDF is authoritative. Wiring that pairing into a batch run is the subject of the batch preflight report CLI, which is where this validation belongs in a real archival workflow

The SaveAsPdfA, InjectPdfAMarkers, and ValidatePdfA APIs shown here ship with 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