Ett arkivimportsteg avvisade ett parti "PDF/A-2b"-filer som öppnades utan problem i alla visare på skrivbordet. Leverantören svor att de var standardkonforma. Det var de inte: varje fil bar på en JavaScript-åtgärd gömd i katalogen, sådant som ett hastigt ögonkast aldrig fångar och som en fullständig PDF/A-validerare som veraPDF markerar direkt. Problemet var att ingen ville koppla in en Java-miljö i en Delphi-batchtjänst bara för att svara på en enda ja-eller-nej-fråga per fil. Det är det gap som ValidatePdfACompliance i PDFium Component fyller, och det är värt att förstå hur den når ett beslut utan att någonsin fullt ut tolka en innehållsström
Varför PDFium själv inte kan svara på detta
Det första att vara ärlig med: den medföljande pdfium.dll har ingen PDF/A-funktionalitet alls. Det finns ingen ConvertToPDFA, ingen OutputIntent-skrivare, inget XMP-API i den publika ytan. Alla delar av PDF/A i det här biblioteket, både skriv- och kontrollsidan, ligger i ren Pascal i FPdfPdfa.pas och arbetar med byte-nivåparsing plus inkrementella uppdateringar. Så när du anropar valideraren frågar du inte Chromiums renderare om någonting. Du kör en Pascal-token-scanner över filens strukturella byte
Det publika API:et är medvetet litet. En funktion läser en ström från position 0 och returnerar en post:
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 uttrycker regeln som spelar roll i ett kontrollsteg: en fil blir godkänd bara när en verklig överensstämmelsenivå har identifierats och problemuppsättningen är tom. En analys som lyckas men inte hittar någon pdfaid-markör landar på pacNone, vilket uttryckligen inte är ett godkänt resultat. Det här är samma poäng som batchrapport-CLI:t för förhandskontroll gör utifrån: en tom träfflista för en okänd fil är inte ett rent hälsotecken
Rensa strömmarnas innehåll före all token-skanning
Här är den enskilt viktigaste implementeringsdetaljen, och den som är lättast att göra fel om du skriver din egen scanner. Detektorn hittar överträdelser genom att söka efter avgränsade namntoken, sådant som /JavaScript, /LZWDecode, /BM. Om du skannar de råa filbyten kommer de inbäddade binära strömmarna, komprimerade bilder, ICC-profiler och fontprogram slumpmässigt att innehålla byteföljder som liknar de tokenen. Du kommer att rapportera /AA eller /3D som ”hittad” bara för att tre byte inuti en JPEG råkade stava det. Det är en fabrik för falska positiva
Lösningen är PdfStructureBytes: den går igenom filen och fyller byte mellan varje stream och endstream nyckelord med blanktecken, samtidigt som ordboksstrukturen lämnas intakt. Först därefter körs skanningen. Varje kontroll av namntoken i valideraren arbetar på denna rensade kopia. Om du bara tar med dig en sak från den här artikeln, ta med dig den. Samma disciplin speglas i PDF/UA-valideraren, som behåller sin egen kopia av rutinen eftersom de två standarderna utvecklas oberoende
De 29 problemen och vad vart och ett betyder
TPdfAValidationIssue är ett dokumenterat kontrakt. Ordinallägena är låsta eftersom DUnitX-tester, demos och rapportlagret alla är beroende av dem, så nya fynd läggs bara till sist. Från och med v1.63.0 finns det 29 medlemmar. De faller i några grupper:
- Metadata och identitet:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Färg och utdata:
pvaiMissingOutputIntent,pvaiMissingIccProfile, ochpvaiMixedDeviceColorSpacesnär både DeviceRGB och DeviceCMYK förekommer (6.2.3.3) - Hårda förbud för varje del:
pvaiEncryptionPresent(en/Encryptordbok är helt förbjuden),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Teckensnitt:
pvaiFontNotEmbeddedoch det strängarepvaiUnembeddedFont, pluspvaiUnicodeMappingMissingför ett Level U-påstående utan/ToUnicode - Taggning:
pvaiLevelAStructureMissingnär ett påstående om överensstämmelse=A saknar taggad struktur
De sex nyaste medlemmarna, tillagda på ordinalerna 24 till 29, täcker de knepiga fall som granskare faktiskt snubblar på: pvaiTrappedTrue (en /Trapped /True i Info-ordboken, en ”falsk vän” eftersom värdet måste vara False eller Unknown), pvaiForbiddenActionSubtype (Sound eller Movie används som åtgärd, inte bara som annotering), pvaiTransparentColorSpace (ett blandningsläge som inte är Normal eller en /CA//ca som inte är lika med 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, och pvaiMixedDeviceColorSpaces
Partsmedveten styrning: A-1 är strikt, A-2 och A-3 släpper på kraven
PDF/A är inte en enda regelbok. Tre saker som PDF/A-1 förbjuder är uttryckligen tillåtna från och med PDF/A-2: transparens (en /Transparency grupp eller en aktiv /SMask, 6.4), valfritt innehåll (/OCProperties, 6.1.13) och inbäddade filer (/EmbeddedFiles eller /EF, 6.1.11). En naiv validerare som markerar alla tre för varje fil kommer att förkasta fullt giltiga PDF/A-2-dokument i mängd
Så valideraren läser partsnumret från pdfaid-markören via PdfAPartOf och låser dessa kontroller bakom PartNo = 1. Blend-läges- och annoteringsalfa-kontrollerna för de nya transparensproblemen är också bara för 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;
Ett konservativt standardval förtjänar att nämnas: när det inte finns någon pdfaid-markör alls behandlas delen som 1, den strängaste. Resonemanget är att en oidentifierad fil ska hållas till de stramaste reglerna i stället för att släppas igenom. JavaScript, förbjudna åtgärder, LZW, XFA, NeedAppearances, förbjudna annoteringar och icke-inbäddade teckensnitt förblir förbjudna för varje del, så de kontrollerna ligger aldrig bakom grinden
Expandera objektströmmar så att inget döljs
PDF 1.5 införde korsreferensströmmen och objektströmmen (/Type /ObjStm), och de skapar en blind fläck för en naiv byte-scanner. En katalog, en OutputIntent, en åtgärdsordbok, vad som helst som inte själv är en ström, kan Flate-komprimeras inne i en ObjStm. Skannar du den råa strukturen ser du inget av det och kan sedan rapportera en ren fil som allt annat än ren
PdfExpandObjectStreams stänger det gapet. Innan någon kontroll körs gör valideraren Data := PdfExpandObjectStreams(Data). Rutinen hittar varje ObjStm, läser dess /N och /First sidhuvud för att hämta de inneslutna objektnumren och offsetarna, inflaterar kroppen med PdfInflate (RTL:s zlib, System.ZLib i Delphi och zstream i FPC), och appenderar varje inneslutet objekt som en vanlig N 0 obj ... endobj till slutet av en kopia av bytena. De befintliga tokenkontrollerna hittar sedan de objekten utan någon ändring i logiken
Två begränsningar gör detta rent snarare än skört. Streamobjekt, Metadata, ICC-profil och fontprogram kan inte ligga i en objektström, bara icke-strömmade ordböcker kan det, så expansionen hanterar bara ordböcker och de tillagda objekten bär ingen stream nyckel att störa passet för rensning av innehållet. Och eftersom det tillagda innehållet hamnar efter %%EOF, hittar den bakåtgående sökningen från startxref fortfarande den ursprungliga trailer. Själva trailer för korsreferensströmmen hanterades redan tidigare, i v1.49.3, genom att läsa Root, Size och ID direkt ur den textuella xref-strömordboken, ett ämne som utforskas i den kompletterande artikeln om validering av objekt- och korsreferensströmmar; arbetet med objektströmmar behövde bara lägga till inflationssteget, utan behov av att avkoda xref-poster av typ 2 eller riva upp en PNG-prediktor
De ärliga gränserna för en byte-nivå-kontroll
Det här är ett förhandskontrollverktyg, inte en certifierad validerare, och gränserna är verkliga. Inbäddning av teckensnitt är en räkneheuristik, och att få den rätt krävde en korrigering som är värd att känna till. Den ursprungliga kontrollen använde PdfCountName('/FontDescriptor'), men varje teckensnitt bidrar med två /FontDescriptor token, en referens från teckensnittsordboken och en /Type i själva descriptor-objektet, så räknaren blev 2N mot N inbäddade program och testet blev alltid sant. Lösningen är PdfCountDescriptorRefs, som bara räknar den /FontDescriptor N G R referensformen, en per teckensnitt, och lägger till pvaiUnembeddedFont bara när de inbäddade programmen faktiskt är färre:
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);
Även efter korrigeringen är den grov: ett blandat dokument där varje descriptor råkar ha någon FontFile kan fortfarande släppa igenom ett enskilt icke-konformt teckensnitt. Att expandera objektströmmar har också en känd sidoeffekt, det exponerar de standard-14-standardresurser som ett AcroForm /DR bär, som /Helv, och heuristiken rapporterar plikttroget dem som inte inbäddade även om veraPDF låter dem passera eftersom de aldrig faktiskt används för rendering. Kontroller på innehållsströmnivå, operator för operator (6.2.10), ligger helt utanför scope eftersom de skulle kräva fullständig innehållsparsering snarare än en byte-skanning. Se valideraren som en snabb, beroende-fri första grind som fångar överträdelser som markörinjektion inte kan fixa, och spara en full validerare för slutlig certifiering
Det här är kontrollhalvan av berättelsen. Den kompletterande skrivsidan, där SaveAsPdfA injicerar XMP, OutputIntent och sRGB ICC-profilen och ärligt nedgraderar en Level A-begäran som saknar taggad struktur, bygger på samma byte-nivåmaskineri. Båda halvorna levereras i PDFium Component for Delphi, ett enda VCL-paket ovanpå en ren Pascal-implementering av PDF/A utan någon extern runtime att installera