Techninis straipsnis

PDF/A archyvinė atitiktis Delphi aplinkoje su PDFium Component

Jūs išleidžiate keitiklį, kuris kiekvieną failą pažymi kaip PDF/A-1b, kliento įrašų sistema juos vienerius metus ramiai priima, o tada auditas paleidžia visą partiją per veraPDF ir trečdalis failų grįžta kaip neatitinkantys reikalavimų. Niekas nesugriuvo, nebuvo jokios išimties, failai puikiai atsidaro kiekvienoje peržiūros programoje ant jūsų stalo. Tiesiog jie niekada nebuvo tuo standartu, kuriuo jūs juos pažymėjote. Tai įprastas archyvinių PDF gedimo režimas, todėl teiginys „mes nustatėme vėliavą“ niekada nėra tas pats, kas teiginys „jis validuojasi“

Pirmas dalykas, kurį reikia suprasti apie PDFium ir PDF/A, yra tas, kad pats variklis su tuo neturi nieko bendra. PDFium atvaizduoja, analizuoja ir rašo PDF, tačiau jo viešajame paviršiuje nėra nei ConvertToPDFA, nei OutputIntent rašyklės, nei XMP API. Visa archyvinio atitikimo logika - XMP paketas, OutputIntent su ICC profiliu, katalogo žymekliai ir validacija - gyvena pačiame PDFiumPas, maždaug 2000 eilučių grynojo Pascal modulyje (FPdfPdfa.pas), kuris analizuoja išsaugotus baitus ir perrašo juos per incremental update. Kai žinote, kur vyksta darbas, žinote ir kur slepiasi klaidos, o jos čia nesislepia PDFium viduje

Ko PDF/A iš tikrųjų reikalauja ir kur jis kanda

PDF/A nėra vienas formatas. ISO 19005 apibrėžia tris dalis (PDF/A-1, -2 ir -3) ir kiekvienoje jų atitikties lygius, kurie žada skirtingus dalykus. Lygis B (basic) garantuoja tik tai, kad vizualinė išvaizda bus atkartojama. Lygis A (accessible) ant B viršaus prideda žymėtą struktūros medį ir Unicode susiejimą. Lygis U, kuris egzistuoja tik 2 ir 3 dalyse, yra per vidurį: patikimas Unicode tekstas be pilno struktūros medžio. ISO 19005-1 lygio U neturi, ir biblioteka šį apribojimą koduoja tiesiogiai

Keli formato reikalavimai praktikoje kerta dažniausiai. Šifravimas draudžiamas tiesiogiai (ISO 19005-1 §6.1.3 ir vėlesni skyriai): PDF/A failas negali turėti /Encrypt žodyno. Dokumentas turi deklaruoti išvesties atvaizdavimo sąlygą per OutputIntent, kurio paskirties profilis yra galiojantis ICC profilis (§6.2.3.2). Pats atitikties teiginys turi būti XMP metaduomenyse pagal PDF/A identifikavimo schemą. Lygis A papildomai reikalauja §6.8 loginės struktūros, t. y. žymų medžio, kuris daro dokumentą mašininio skaitymo objektu. Praleiskite bent vieną iš šių dalių ir atitikties tikrintuvas atmes failą, nors vizualiai jis atrodys nepriekaištingai

Vienas kvietimas, kuris pagamina archyvą

PDFiumPas visą grandinę pateikia per TPdf.SaveAsPdfA. Paprastoji perkrova priima tikslinę atitiktį ir pagal nutylėjimą naudoja PDF/A-1b, o tai yra teisinga numatytoji reikšmė dažniausiam scenarijui „padaryk taip, kad šis failas būtų atvaizduojamas amžinai“

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;

Po gaubtu tai yra dviejų žingsnių veiksmas. SaveAsPdfA pirmiausia paprašo PDFium serializuoti dokumentą per FPDF_SaveAsCopy, o tada šį baitų srautą perduoda InjectPdfAMarkers, kuris kaip incremental update prideda XMP metaduomenis, sRGB OutputIntent su įterptu ICC profiliu ir perrašytą katalogą. Šaltinis skaitomas nuo nulinės pozicijos, paskirties srautas rašomas nuo nulinės pozicijos, pradinė objektų medis lieka nepaliestas, o žymekliai prisikabina po esamu %%EOF. Jei jums reikia ne failo, o baitų, SaveAsPdfAToStream priima TStream ir tas pačias parinktis

Atitikties pasirinkimas per parinkčių įrašą

Jei norite nusitaikyti į konkrečią dalį ir lygį, perduokite TPdfASaveOptions įrašą. Jo laukas Conformance priima TPdfAConformance reikšmę. Enum apima visas galiojančias kombinacijas ir nieko daugiau: pac1b, pac1a 1 daliai; pac2b, pac2u, pac2a 2 daliai; pac3b, pac3u, pac3a 3 daliai, taip pat pacUnknown ir pacNone validavimo pusei. pac1u neegzistuoja, nes toks lygis standarte neegzistuoja

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;

Dauguma įrašo laukų gali likti tušti. Jei paliksite Title, Author, Subject, Keywords, Creator ir Producer tuščius, SaveAsPdfA automatiškai juos užpildys iš dokumento Info žodyno per FPDF_GetMetaText. Jei paliksite CreationDate ir ModDate tuščius, jis abiem XMP datoms naudos dabartinį UTC laiką. Jei tušti lieka DocumentId ir InstanceId, biblioteka juos iš anksto užpildo iš FPDF_GetFileIdentifier, o jei to nėra - iš deterministinio ID, išvesto iš šaltinio baitų. Vienintelis laukas, kurį galite norėti sąmoningai perrašyti, yra IccProfileData: tuščias reiškia komplekte esantį sRGB IEC61966-2.1 profilį, tačiau CMYK arba pilkų tonų darbas turėtų pateikti savąjį

Kodėl lygis A nuleidžiamas ir kodėl tai yra sąžininga

Štai subtili detalė, kuri kerta žmonėms, laukiantiems, kad viena vėliava būtų garantija. Galite paprašyti pac1a dokumentui, kuris neturi žymų medžio, tačiau PDF/A-1a reikalauja §6.8 loginės struktūros, o biblioteka negali iš nežymėto PDF išburti struktūros medžio. Užuot išleidusi failą, kuris tvirtina turįs lygį A, bet iš tiesų jo neatitinka, SaveAsPdfA patikrina, ar egzistuoja tikra žymėta struktūra (/StructTreeRoot ir /MarkInfo su /Marked true), ir jei jos nėra, pažadą pažemina: pac1a virsta pac1b, pac2a virsta pac2b ir taip per visas tris dalis. Vidiniai pagalbininkai vadinasi PdfAIsLevelA ir PdfADowngradeToLevelB

Vertinimas čia vertas aiškaus įvardijimo: failas, kuris sąžiningai deklaruoja lygį, kurį tikrai atitinka, yra naudingesnis už failą, kuris meluoja apie aukštesnį lygį. Su lygiu U elgiamasi kitaip. Tikros Unicode aprėpties nustatymas reikštų grubų testą „ar yra /ToUnicode“, kuris per daug pažemintų teisėtus dokumentus (WinAnsi ir panašios koduotės yra atleidžiamos), todėl išsaugojimo pusė tiesiog išrašo U teiginį taip, kaip jį deklaravo kvietėjas, o neatitikimą palieka pagauti validavimo pusei. Jei jums reikia garantuoto lygio A archyvo, dokumentą pažymėkite prieš konvertuodami; keitiklis neišgalvos struktūros, kurios ten nėra

ICC spąstai, kuriuos pagauna tik tikras validatorius

Tai buvo klaida, kuri išmokė skaudžiausią pamoką, nes pačios bibliotekos tikrintuvas ją praleido, o veraPDF, ISO 19005 etaloninis validatorius, - ne. PDF/A reikalauja, kad OutputIntent paskirties profilis būtų galiojantis ICCBased srautas, o §6.2.3.2 reikalauja, kad tikrintuvas vertintų šį srautą kaip spalvų erdvę. ICCBased srautas privalo deklaruoti /N, t. y. spalvinių komponentų skaičių. Ankstyva injektoriaus versija į ICC srauto žodyną įrašydavo tik /Length, be /N, ir veraPDF atmesdavo rezultatą su klaida apie trūkstamą N įrašą

Klastingiausia buvo tai, kad atmetimas pasireikšdavo tik PDF/A-1b ir -1a atvejais. 2 ir 3 dalių atitikties modeliai to konkretaus paskirties profilio tikrinimo netaikė, todėl identiška įterpta struktūra validuodavosi kaip pac2b, pac3b ir pac2u, bet žlugdavo kaip pac1b vien dėl pdfaid:part reikšmės. Vieneto testas to negalėjo pamatyti, nes pačios bibliotekos ValidatePdfACompliance tikrino tik tai, ar yra /DestOutputProfile raktas, o ne tai, kas gyvena pačiame srauto žodyne. Vidiniai testai buvo žali; tikroji archyvinė validacija - ne

Pataisa yra IccComponentCount, kuris nuskaito duomenų spalvų erdvės parašą iš ICC antraštės 16 baito poslinkio ir paverčia jį komponentų skaičiumi: GRAY yra 1, RGB , Lab ir XYZ yra 3, CMYK yra 4, o nežinomas profilis pagal nutylėjimą laikomas 3. Šis skaičius įrašomas į srauto žodyną kaip /N. Jis apskaičiuojamas, o ne kietai prikalamas prie 3, kad kvietėjas, kuris per IccProfileData pateikia CMYK ar pilkų tonų profilį, vis tiek gautų teisingą reikšmę. Platesnė pamoka čia metodologinė: vidinis tikrintuvas ir autoritetingas validatorius turi skirtingas akląsias zonas, todėl PDF/A išvestis turi būti testuojama nuo pradžios iki galo prieš etaloninę implementaciją, tokią kaip veraPDF, o ne pasitikima vien savitikromis. Ta pati incremental update disciplina, kuri duoda švarius archyvus, aptariama straipsnyje apie suspaustų objektų ir xref srautų validavimą, nes šiuolaikiniai PDF, kuriuos šis injektorius valgo, dažnai remiasi kryžminių nuorodų srautais

Šifravimas, xref srautai ir kiti kampai

Kadangi ISO 19005 draudžia šifravimą, išsaugojimo kelias jį pašalina prieš rašydamas. SaveAsPdfA serializuodamas pritaiko FPDF_REMOVE_SECURITY, todėl užšifruotas šaltinis (įkeltas su slaptažodžiu) pakeliui į archyvą iššifruojamas. Neužšifruotame dokumente tai yra no-op ir nieko nekeičia. Iš to seka tas pats apribojimas, kurį HotPDF taiko iš kitos pusės: vienas failas negali būti ir užšifruotas, ir PDF/A. Kai darbo eigai reikia abiejų, atsakymas yra du artefaktai: užšifruota kopija platinimui ir atskira švari kopija archyvui

Dar vienas kampas nematomas, kol neįkanda: PDF 1.5+ dokumentai, kurie naudoja tik kryžminių nuorodų srautą ir apskritai neturi trailer raktažodžio. Injektorius skaito trailer tam, kad surastų šaltinio /Info ir prikabintų savo incremental update, todėl jis turi suprasti ir xref-stream formą, kitaip toks dokumentas būtų perkopijuotas su tyliai praleistais žymekliais. ISO 32000-1 §7.5.6 aiškiai leidžia klasikiniam trailer incremental update sekti po xref-stream dokumento, kai /Prev rodo į xref-stream poslinkį, ir būtent tokią struktūrą injektorius ir generuoja. Paties PDFium FPDF_SaveAsCopy visada rašo klasikinį trailer, todėl įprastame vamzdyne injektorius su grynu xref-stream šaltiniu nesusiduria, tačiau skaitymo kelias tai tvarko dokumentams, kurie atkeliauja iš išorės

Tikrinkite prieš patikėdami deklaracija

Biblioteka turi baitų lygio tikrintuvą TPdf.ValidatePdfA, kuris grąžina TPdfAValidationResult. Jo laukas Conformance praneša aptiktą lygį, o Issues yra TPdfAValidationIssue reikšmių aibė; patogumo metodas IsCompliant tampa true tik tada, kai aptiktas tikras lygis ir problemų aibė tuščia. Paketiniuose darbuose verta jį naudoti kaip greitą pirmą vartą

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;

Būkite sąžiningi dėl to, ką tai nuperka. Baitų lygio tikrintuvas labai patikimai pagauna struktūrines problemas - trūkstamą OutputIntent, draudžiamą veiksmą, esamą /Encrypt, skaidrumą ten, kur 1 dalis jį draudžia, - o šriftų įterpimo aptikimas remiasi skaičiavimo euristika, kuri sąmoningai praneša tik aukšto pasitikėjimo signalą, užuot vaikęsi kiekvieno glifo aprėptį. Ko jis nedaro, tai turinio srauto operatorių analizės, kuri reikalautų pilno turinio parserio ir pagal sumanymą nepatenka į šio sluoksnio ribas. Leidimo vartuose poruokite bibliotekos tikrintuvą su veraPDF: pirmasis yra momentinis ir veikia visur be DLL, antrasis - autoritetingas. Kaip šią porą įpinti į paketinius paleidimus, aptariama batch preflight report CLI straipsnyje; būtent ten tokia validacija ir priklauso tikroje archyvinėje darbo eigoje

Čia parodyti SaveAsPdfA, InjectPdfAMarkers ir ValidatePdfA API tiekiami kartu su PDFium Component, skirtu Delphi, C++Builder ir Lazarus/FPC. Produkto puslapis pateikia nuorodas į visą API nuorodyną, įskaitant pilną atitikties enum ir parinkčių įrašą, naudojamą šiuose pavyzdžiuose