Техническа статия

Съответствие с PDF/A за архивиране в Delphi с PDFium VCL

Пускате конвертор, който маркира всеки файл като PDF/A-1b, системата на клиента ги приема година наред, и после audit пуска целия пакет през veraPDF, а една трета се връщат като non-conformant. Нищо не се срива, не е хвърлено изключение, файловете се отварят нормално във всеки viewer на бюрото ви. Те просто не са стандартът, който сте им поставили. Това е нормалният failure mode за архивен PDF и е причината „сложихме flag-а“ никога да не е същото твърдение като „валидира се“

Първото нещо, което трябва да разберете за PDFium и PDF/A, е че engine-ът няма нищо общо с това. PDFium рендерира, парсва и записва PDF, но неговата публична повърхност няма ConvertToPDFA, няма OutputIntent writer, няма XMP API. Всяка част от архивното съответствие - XMP packet-ът, OutputIntent и неговият ICC profile, catalog markers-ите, validation-а - живее в PDFiumPas самия, в приблизително 2000-редов pure-Pascal unit (FPdfPdfa.pas) който парсва запазените байтове и ги пренаписва чрез incremental update. Да знаете къде се случва работата, означава да знаете къде се крият бъговете, а те не се крият в PDFium

Какво PDF/A всъщност изисква и къде хваща

PDF/A не е един формат. ISO 19005 дефинира три части (PDF/A-1, -2, -3) и във всяка от тях - нива на съответствие, които обещават различни неща. Level B (basic) гарантира само, че визуалният вид може да се възпроизведе. Level A (accessible) добавя tagged structure tree и Unicode mapping върху B. Level U, който съществува само за parts 2 и 3, стои между тях: надежден Unicode текст без пълното structure tree. ISO 19005-1 няма Level U, ограничение, което библиотеката кодира директно

Няколко правила на формата са тези, които хващат на практика. Encryption е забранен категорично (ISO 19005-1 §6.1.3 и наследниците му): PDF/A файл не може да носи /Encrypt dictionary. Документът трябва да декларира output rendering condition чрез OutputIntent, чиято destination е валиден ICC profile (§6.2.3.2). Самият conformance claim трябва да присъства като XMP metadata под PDF/A identification schema. Level A допълнително изисква §6.8 logical structure, tag tree-ят, който прави документа machine-readable. Ако пропуснете някое от тези неща, verifier за съответствие отхвърля файла, въпреки че той се рендерира съвършено

Едното извикване, което произвежда архив

PDFiumPas излага целия pipeline зад TPdf.SaveAsPdfA. Простият overload приема target conformance и по подразбиране е PDF/A-1b, което е правилният default за обичайния случай „направи това да може да се рендерира завинаги“

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;

Под капака това е движение в два етапа. SaveAsPdfA първо кара PDFium да сериализира документа с FPDF_SaveAsCopy, после подава този byte stream на InjectPdfAMarkers, който добавя XMP metadata, sRGB OutputIntent с вграден ICC profile и пренаписан catalog като incremental update. Source-ът се чете от позиция нула и destination-ът се записва от позиция нула; оригиналното object tree остава непокътнато, а marker-ите кацат след съществуващия %%EOF. Ако ви трябват байтовете вместо файл, SaveAsPdfAToStream приема TStream и същите options

Избор на conformance с options record-а

За да таргетирате конкретна part и level, подайте TPdfASaveOptions record. Неговото Conformance field приема TPdfAConformance value. Enumeration-ът покрива всяка валидна комбинация и нищо друго: pac1b, pac1a за part 1; pac2b, pac2u, pac2a за part 2; pac3b, pac3u, pac3a за part 3, плюс pacUnknown и pacNone за validation страната. Няма pac1u, защото такова ниво не съществува в стандарта

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;

Повечето от record-а може да остане празно. Оставете Title, Author, Subject, Keywords, Creator, и Producer празни и SaveAsPdfA ги попълва автоматично от Info dictionary-а на документа чрез FPDF_GetMetaText празни и CreationDate празни и библиотеката ги попълва от текущото UTC време и за двете XMP дати. Оставете ModDate и DocumentId празни и библиотеката ги попълва от InstanceId, като пада обратно към deterministic ID, изведен от source bytes-овете. Единственото поле, което може да искате да override-нете умишлено, е FPDF_GetFileIdentifier: празно означава bundled sRGB IEC61966-2.1 profile, но CMYK или grayscale workflow трябва да подаде свой.IccProfileDataЗащо Level A деградира и защо това е честният избор

Защо Level A деградира и защо това е честният избор

на документ, който няма tag tree, но PDF/A-1a изисква §6.8 logical structure и библиотеката не може да създаде structure tree от untagged PDF. Вместо да изведе файл, който твърди Level A, а реално не го покрива, pac1a проверява за истински tagged structure (SaveAsPdfA плюс /StructTreeRoot с /MarkInfo) и, ако липсва, понижава твърдението: /Marked true става pac1a, pac1b става pac2a, и така нататък през трите части. Вътрешните помощници са pac2b и PdfAIsLevelA. PdfADowngradeToLevelB

Аргументацията си струва да се каже ясно: файл, който честно декларира нивото, което покрива, е по-полезен от такъв, който лъже за ниво, което не покрива. Level U се обработва различно. Засичането на истинско Unicode покритие би означавало наивен тест „има ли /ToUnicode“, който сваля прекалено много легитимни документи (WinAnsi и подобни encodings са изключение), така че save страната изписва U claim-а така, както го е подал caller-ът, и оставя несъответствието да бъде хванато на validation страната. Ако ви трябва гарантиран Level A archive, tag-нете документа преди да го конвертирате; converter-ът няма да измисли структура, която не съществува

ICC подводният камък, който хваща само истински validator

This is the failure that taught the hardest lesson, because the library's own checker passed it while veraPDF, the ISO 19005 reference validator, did not. PDF/A requires the OutputIntent's destination profile to be a valid ICCBased stream, and §6.2.3.2 makes a verifier validate that stream as a colour space. An ICCBased stream must declare /N, the number of colour components. An early version of the injector wrote the ICC stream dictionary with only /Length and no /N, and veraPDF rejected the result with "The N entry (value null)... is missing"

What made it insidious is that the rejection only fired for PDF/A-1b and -1a. The part 2 and part 3 conformance models did not run that particular check on the destination profile, so the identical injected structure validated under pac2b, pac3b, and pac2u but failed under pac1b on nothing more than the pdfaid:part value. A unit test could never see it, because the library's own ValidatePdfACompliance only checked that the /DestOutputProfile проверяваше само дали

ключът съществува, не какво има вътре в stream dictionary-а. Вътрешните тестове оставаха зелени; реалната архивна валидация се проваляше.IccComponentCountFix-ът е GRAY, който чете data colour space signature-а на offset 16 от ICC header-а и го map-ва към component count: RGB е 1, Lab , XYZ и CMYK са 3, /N е 4, а unknown profile по подразбиране става 3. Този count влиза в stream dictionary-а като IccProfileData. Той е изчислен, не hard-coded към 3, така че caller, който подава CMYK или grayscale profile през , пак получава правилната стойност. По-широкият урок е методологичен: checker-ът в библиотеката и авторитетният validator имат сляп ъгъл и PDF/A output трябва да се тества end to end срещу reference implementation като veraPDF, а не да се доверява на self-checks. Същата incremental-update дисциплина зад чистите архиви е описана в валидиране на compressed object и xref streams

Криптиране, xref streams и други граници

Encryption, xref streams и други границиSaveAsPdfAПонеже ISO 19005 забранява encryption, save пътят го премахва преди запис. FPDF_REMOVE_SECURITY прилага

при сериализация, така че encrypted source (зареден с password-а си) се decrypt-ва по пътя към archive-а. При нешифрован документ това е no-op и не променя нищо. Следствието е същото ограничение, което HotXLS налага и от другата посока: един файл не може да бъде едновременно encrypted и PDF/A. Когато workflow изисква и двете, отговорът са два artifacts-а, encrypted copy за разпространение и отделно чисто copy за archive-а.trailerОще една граница е невидима, докато не ухапе: PDF 1.5+ документи, които използват чист cross-reference stream и нямат /Info keyword. Injector-ът чете trailer-а, за да намери source /Prev и да добави incremental update-а си, и трябва да приема xref-stream формата, иначе такъв документ би минал през pipeline-а с тихо изпуснати markers. ISO 32000-1 §7.5.6 изрично допуска класически trailer incremental update да следва xref-stream документ, с FPDF_SaveAsCopy сочещ към xref-stream offset-а, което е точно структурата, която injector-ът изписва. Собственият

Проверка преди да повярвате на claim-а

Проверка преди да повярвате на claim-аTPdf.ValidatePdfAБиблиотеката доставя byte-level checker, TPdfAValidationResult, който връща Conformance. Неговото Issues поле отчита засеченото ниво, а TPdfAValidationIssue е set от IsCompliant values; convenience методът

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;

е true само когато е засечено реално ниво и issue set-ът е празен. Пуснете го като бърза първа gate стъпка в batch./EncryptБъдете честни какво ви купува това. Byte-level checker-ът хваща structural проблеми (липсващ OutputIntent, забранено действие, наличен , transparency там, където part 1 я забранява) с висока увереност, а откриването на font embedding използва count heuristic, който нарочно отчита само high-confidence signal, вместо да гони per-glyph покритие. Това, което не прави, е content-stream operator analysis, която би изисквала пълен content parser и е извън обхвата по дизайн. За release gate, комбинирайте in-library checker-а с veraPDF: checker-ът е мигновен и работи навсякъде без DLL, veraPDF е authoritative. Да вградите тази двойка в batch run е темата на batch preflight report CLI

, където тази validation принадлежи в реален архивен workflow.SaveAsPdfAThe InjectPdfAMarkers, ValidatePdfA and APIs, показани тук, идват с PDFium Component