Гейт за входящ архив отхвърли пакет файлове "PDF/A-2b", които се отваряха добре във всеки viewer на бюрото. Доставчикът твърдеше, че са conformant. Не бяха: всеки от тях носеше JavaScript action, скрито в catalog-а, от онзи тип, който обикновено око не хваща, а пълен PDF/A validator като veraPDF маркира веднага. Проблемът е, че никой не искаше да закачи Java toolchain към Delphi batch service само за да получи по един yes-or-no отговор на файл. Тази празнина ValidatePdfACompliance запълва в PDFium Component и си струва да се разбере как стига до verdict, без изобщо да парсва изцяло content stream
Защо PDFium самият не може да отговори на това
Първото честно нещо: вграденият pdfium.dll няма PDF/A capability изобщо. Няма ConvertToPDFA, няма OutputIntent writer, няма XMP API в публичната повърхност. Всяка част от PDF/A в тази библиотека, и на write страната, и на check страната, живее в pure Pascal в FPdfPdfa.pas и работи чрез byte-level parsing плюс incremental update. Така че когато извикате validator-а, не питате renderer-а на Chromium за нищо. Пускате Pascal token scanner върху structural bytes-овете на файла
Публичният API е нарочно малък. Една функция чете stream от позиция 0 и връща record:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
IsCompliant кодира правилото, което има значение в един gate: файлът минава само когато е засечено реално conformance ниво и issue set-ът е празен. Parse, който успява, но не намира pdfaid marker, се свежда до pacNone, което изрично не е pass. Това е същата точка, която batch preflight report CLI прави отвън: празен findings list върху неразпознат файл не е clean bill of health
Премахване на stream bodies преди какъвто и да е token scan
Ето най-важният implementation detail и най-лесният за грешка, ако си пишете собствен scanner. Detector-ът намира violations, като търси delimited name tokens, неща като /JavaScript, /LZWDecode, /BM. Ако сканирате raw байтовете на файла, вградените binary stream bodies, compressed images, ICC profiles, font programs, случайно ще съдържат byte sequences, които приличат на тези tokens. Ще отчетете /AA или /3D "found", защото три байта вътре в JPEG по случайност са ги изписали. Това е фабрика за false positive-и
Fix-ът е PdfStructureBytes: той обхожда файла и заменя байтовете между всеки stream и endstream keyword със spaces, като оставя dictionary структурата непокътната. Едва след това започва scan-ът. Всяка name-token проверка във validator-а работи върху това stripped копие. Ако вземете само една идея от тази статия, нека е тази. Същата дисциплина е огледално повторена и в PDF/UA validator-а, който пази собствено копие на routine-а, защото двата стандарта се развиват независимо
29-те issue-а и какво означава всеки
TPdfAValidationIssue е documented contract. Ordinal-ите са frozen, защото DUnitX tests, demo-тата и report layer-ът разчитат на тях, така че нови findings се добавят само в края. Към v1.63.0 има 29 members. Те попадат в няколко семейства:
- Metadata и identity:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Color и output:
pvaiMissingOutputIntent,pvaiMissingIccProfile, иpvaiMixedDeviceColorSpaceswhen both DeviceRGB and DeviceCMYK appear (6.2.3.3) - Забрани за всички части:
pvaiEncryptionPresent(an/Encryptdictionary е категорично забранен),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Fonts:
pvaiFontNotEmbeddedи по-строгияpvaiUnembeddedFont, плюсpvaiUnicodeMappingMissingза Level U claim без/ToUnicode - Tagging:
pvaiLevelAStructureMissingкогато conformance=A claim няма tagged structure
Шестте най-нови members, добавени на ordinal-и 24 до 29, покриват фините случаи, в които reviewer-ите реално се спъват: pvaiTrappedTrue (a /Trapped /True в Info dictionary-а, "false friend", защото value-то трябва да е False или Unknown), pvaiForbiddenActionSubtype (Sound или Movie, използвано като action, не само като annotation), pvaiTransparentColorSpace (non-Normal blend mode или /CA/ /ca неравно на 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, и pvaiMixedDeviceColorSpaces
Part-aware gating: A-1 е strict, A-2 и A-3 relax
PDF/A не е една книга с правила. Три неща, които PDF/A-1 забранява, са изрично позволени от PDF/A-2 нататък: transparency (group /Transparency или active /SMask, 6.4), optional content (/OCProperties, 6.1.13), и embedded files (/EmbeddedFiles или /EF, 6.1.11). Наивен validator, който маркира и трите за всеки файл, ще отхвърли масово напълно валидни PDF/A-2 документи
Така че validator-ът чете part номера от pdfaid marker-а чрез PdfAPartOf и поставя тези проверки зад PartNo = 1. Blend-mode и annotation-alpha проверките за новите transparency issues са също само за part 1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Една conservative default стойност заслужава да се спомене: когато изобщо няма pdfaid marker, part-ът се третира като 1, най-строгият. Разсъждението е, че неидентифициран файл трябва да бъде подложен на най-тесните правила, а не да се пусне напред. JavaScript, forbidden actions, LZW, XFA, NeedAppearances, forbidden annotations и unembedded fonts остават забранени за всяка part, така че тези проверки никога не стоят зад gate-а
Разширяване на object streams, така че нищо да не се скрие
PDF 1.5 въведе cross-reference stream-а и object stream-а (/Type /ObjStm), и те създават blind spot за наивен byte scanner. Catalog, OutputIntent, action dictionary, всичко, което само по себе си не е stream, може да бъде Flate-компресирано вътре в ObjStm. Сканирайте raw структурата и няма да видите нищо от това, а после ще отчетете clean файл, който изобщо не е такъв
PdfExpandObjectStreams затваря тази празнина. Преди да започне която и да е проверка, validator-ът прави Data := PdfExpandObjectStreams(Data). Routine-ът намира всеки ObjStm, чете неговия /N и /First header, за да вземе съдържащите се object номера и offsets, inflates body-то с PdfInflate (RTL zlib, System.ZLib на Delphi и zstream на FPC), и добавя всеки съдържащ се object като обикновен N 0 obj ... endobj в края на копие на байтовете. Съществуващите token проверки после намират тези objects без промяна в логиката си
Две ограничения правят това чисто, а не крехко. Stream objects, Metadata, ICC profile и font programs не могат да живеят в object stream, само non-stream dictionaries могат, така че expansion-ът работи само с dictionaries, а добавените objects не носят stream keyword, който да наруши body-stripping pass-а. И понеже добавеното съдържание каца след %%EOF, reverse search-ът от startxref още намира оригиналния trailer. Самият cross-reference stream trailer вече беше обработен по-рано, във v1.49.3, чрез четене на Root, Size и ID направо от plaintext xref-stream dictionary-а, тема, разгледана в companion статията за validating object and cross-reference streams; работата с object stream-овете трябваше само да добави inflate стъпката, без нужда да декодира type-2 xref entries или да разопакова PNG predictor
Честните граници на byte-level checker-а
Това е preflight инструмент, не certified validator, и границите са реални. Font embedding е counting heuristic и да се направи правилно изискваше корекция, която си заслужава да се знае. Първоначалната проверка използваше PdfCountName('/FontDescriptor'), но всеки font допринася с два /FontDescriptor tokens, една reference от font dictionary-а и една /Type в descriptor object-а самия, така че броят беше 2N срещу N embedded programs и тестът винаги излизаше true. Fix-ът е PdfCountDescriptorRefs, който брои само /FontDescriptor N G R reference form-а, по един на font, и вдига pvaiUnembeddedFont само когато embedded programs наистина са по-малко:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Дори след корекцията това е грубо: смесен документ, в който всеки descriptor случайно има някакъв FontFile, пак може да остави отделен non-conformant font да се промъкне. Разширяването на object streams има и известен страничен ефект: то изкарва на светло стандарт-14 default resources, които един AcroForm /DR носи, като /Helv, а heuristic-ът прилежно ги отчита като not embedded, въпреки че veraPDF ги допуска, защото никога не се използват реално за rendering. Content-stream operator-level проверки (6.2.10) изобщо са извън обхвата, защото биха изисквали пълно content parsing вместо byte scan. Третирайте validator-а като бърз first gate без зависимости, който хваща violations, които marker injection не може да поправи, и запазете пълен validator за final certification
Това е проверяващата половина на историята. Допълващата write страна, където SaveAsPdfA injects XMP, OutputIntent и sRGB ICC profile и честно понижава Level A request без tagged structure, стъпва върху същата byte-level machinery. И двете половини идват в PDFium Component for Delphi, един VCL package върху pure-Pascal PDF/A implementation без външна runtime инсталация