Пускате конвертор, който маркира всеки файл като 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.FileName := 'invoice.pdf';
Pdf.Active := True;
// Conformance по подразбиране е pac1b (PDF/A-1b)
if Pdf.SaveAsPdfA('invoice_archive.pdf') then
// файлът вече носи XMP, sRGB OutputIntent и 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.FileName := 'report.pdf';
Pdf.Active := True;
Opts := TPdfASaveOptions.Default;
Opts.Conformance := pac2u; // PDF/A-2u: надежден Unicode текст
Opts.Title := 'Quarterly Report 2026';
Opts.Author := 'Finance';
// Оставете IccProfileData празно, за да използвате вградения 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 и ModDate празни и библиотеката използва текущото UTC време и за двете XMP дати. Оставете DocumentId и InstanceId празни и библиотеката ги попълва предварително от FPDF_GetFileIdentifier, като пада обратно към deterministic ID, изведен от source bytes-овете. Единственото поле, което може да искате да override-нете умишлено, е IccProfileData: празно означава bundled sRGB IEC61966-2.1 profile, но CMYK или grayscale workflow трябва да подаде свой
Защо Level A деградира и защо това е честният избор
Ето една тънкост, която спъва хората, очакващи flag-ът да е гаранция. Можете да поискате pac1a за документ, който няма tag tree, но PDF/A-1a изисква §6.8 logical structure, а библиотеката не може да произведе structure tree от untagged PDF. Вместо да изведе файл, който твърди Level A, докато го проваля, SaveAsPdfA проверява за истински tagged structure (/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
Това е провалът, който научи най-тежкия урок, защото собственият checker на библиотеката го приемаше, а veraPDF, ISO 19005 reference validator-ът, не. PDF/A изисква destination profile-ът на OutputIntent да е валиден ICCBased stream, а §6.2.3.2 кара verifier-ът да валидира този stream като colour space. Един ICCBased stream трябва да декларира /N, броя цветови компоненти. Ранна версия на injector-а записваше ICC stream dictionary-то само с /Length и без /N, и veraPDF отхвърли резултата с „The N entry (value null)... is missing“
Коварното беше, че отхвърлянето се случваше само за PDF/A-1b и -1a. Conformance моделите за part 2 и part 3 не пускаха точно тази проверка върху destination profile-а, така че идентичната инжектирана структура минаваше валидация под pac2b, pac3b и pac2u, но се проваляше под pac1b само заради стойността на pdfaid:part. Unit тест никога не би го видял, защото собственият ValidatePdfACompliance на библиотеката проверяваше само дали ключът /DestOutputProfile съществува, не какво живее вътре в stream dictionary-то. Вътрешните тестове оставаха зелени; реалната архивна валидация се проваляше
Fix-ът е IccComponentCount, който чете data colour space signature-а на offset 16 от ICC header-а и го map-ва към component count: GRAY е 1, RGB , Lab и XYZ са 3, CMYK е 4, а unknown profile по подразбиране става 3. Този count влиза в stream dictionary-а като /N. Той е изчислен, не hard-coded към 3, така че caller, който подава CMYK или grayscale profile през IccProfileData, пак получава правилната стойност. По-широкият урок е методологичен: checker-ът в библиотеката и авторитетният validator имат по един сляп ъгъл и PDF/A output трябва да се тества end to end срещу reference implementation като veraPDF, вместо да се доверява на self-checks. Същата incremental-update дисциплина зад чистите архиви е описана в валидирането на compressed object и xref streams, което има значение, защото модерните PDF-и, които injector-ът консумира, често са построени върху cross-reference streams
Криптиране, xref streams и други граници
Понеже ISO 19005 забранява encryption, save пътят го премахва преди запис. SaveAsPdfA прилага FPDF_REMOVE_SECURITY при сериализацията, така че encrypted source (зареден с password-а си) се decrypt-ва по пътя към archive-а. При нешифрован документ това е no-op и не променя нищо. Следствието е същото ограничение, което HotPDF налага от другата посока: един файл не може да бъде едновременно encrypted и PDF/A. Когато workflow изисква и двете, отговорът са два artifacts-а, encrypted copy за разпространение и отделно чисто copy за archive-а
Още една граница е невидима, докато не ухапе: PDF 1.5+ документи, които използват чист cross-reference stream и нямат trailer keyword. Injector-ът чете trailer-а, за да намери source /Info и да добави своя incremental update, и трябва да приема xref-stream формата, иначе такъв документ би бил копиран с тихо изпуснати markers. ISO 32000-1 §7.5.6 изрично допуска класически trailer incremental update да следва xref-stream документ, с /Prev сочещ към xref-stream offset-а, което е точно структурата, която injector-ът изписва. Собственият FPDF_SaveAsCopy на PDFium винаги записва класически trailer, така че в нормалния pipeline injector-ът никога не среща чист xref-stream source, но read пътят го обработва за документи, идващи отвън
Проверка преди да повярвате на claim-а
Библиотеката доставя byte-level checker, TPdf.ValidatePdfA, който връща TPdfAValidationResult. Неговото Conformance поле отчита засеченото ниво, а Issues е set от TPdfAValidationIssue values; convenience методът IsCompliant е true само когато е засечено реално ниво и issue set-ът е празен. Пуснете го като бърза първа gate стъпка в batch
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;
Бъдете честни какво ви купува това. Byte-level checker-ът хваща structural проблеми (липсващ OutputIntent, забранено действие, наличен /Encrypt, 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
API-тата SaveAsPdfA, InjectPdfAMarkers и ValidatePdfA, показани тук, идват с PDFium Component за Delphi, C++Builder и Lazarus/FPC. Продуктовата страница линква пълния API reference, включително пълното conformance enumeration и options record-а зад тези примери