Een PDF die aankomt bij een productiegrens (production boundary) — een afdrukwachtrij (print queue), een archief, een klanten-uploadportaal — zou geaudit moeten worden voordat iets het rendert. Het bestand zou een Launch-actie kunnen dragen die is geprogrammeerd om een extern programma te starten, afbeeldingen die te grof (coarse) zijn om te overleven bij het afdrukken, een coderingswoordenboek (encryption dictionary) dat precies de afdruktaak verbiedt waarvoor het is ingediend, of een PDF/A-label waaraan het niet voldoet. Het inspecteren van een document tegen dergelijke regels voordat het een workflow binnengaat, wordt preflighting genoemd, en de PDFium C API geeft Delphi alles wat nodig is om de controles (checks) rechtstreeks te implementeren, zonder ook maar één pagina te renderen
Dit artikel bouwt de controles zelf: vier audit-klassen (audit classes), elk een kleine routine die bevindingen (findings) toevoegt aan een gedeelde resultatenlijst. Interactieve elementen, resourcemetrics, beveiligingsstatus en standaardenmarkeringen krijgen allemaal werkende code, inclusief de wiskunde. Als wat u nodig heeft de machinerie rond de controles is — batch maploops (folder loops), JSON- en HTML-rapportbestanden, isolatie per bestand — dan levert de PDFium-component een kant-en-klare preflight-engine (preflight engine), en het artikel over de batch preflight CLI behandelt die techniek. De twee delen opzettelijk één exit-code-vocabulaire, zodat een auditor die hier wordt geschreven rechtstreeks onder die batch driver past
Het bevindingsrecord (finding record) en het exit-code contract
Elke controle schrijft naar één plat record-type, omdat het alternatief, waarbij elke controle zijn eigen proza afdrukt, achteraf niet geteld, gefilterd of van een drempelwaarde (thresholded) kan worden voorzien. Vier velden zijn voldoende
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // stable machine key, e.g. 'ACT-LAUNCH'
Page: Integer; // 1-based; 0 means document level
Message: string; // for humans; free to reword between releases
end;
TFindings = TList<TPreflightFinding>;
procedure Add(Findings: TFindings; Severity: TFindingSeverity;
const Code: string; Page: Integer; const Msg: string);
var
F: TPreflightFinding;
begin
F.Severity := Severity;
F.Code := Code;
F.Page := Page;
F.Message := Msg;
Findings.Add(F);
end;
Downstream tooling gebruikt Code als sleutel (keys on), nooit de tekst in Message, die vrij kan veranderen. De proces-exitcode volgt hetzelfde dries-waarden contract (three-value contract) als het batch-artikel: 0 betekent dat het bestand geen bevindingen heeft opgeleverd, 1 betekent dat er bevindingen zijn, en 2 betekent dat de audit zelf niet kon draaien omdat het bestand niet kon worden geparseerd (failed to parse) of om een wachtwoord vraagt. Code 2 gescheiden houden is belangrijk. Een map met corrupte scans wijst op een kapotte scanner verderop in de keten (upstream), niet op een plotselinge ineenstorting van naleving (compliance collapse), en door de twee samen te vouwen stuur je iemand achter het verkeerde probleem aan
Interactieve elementen: scripts, startdoelen (launch targets), externe links
PDFium classificeert elke actie die het vindt met een geheel getal type (integer type), en de constanten van fpdf_doc.h zijn het waard om precies vast te leggen, omdat verkeerd gekopieerde waarden een scanner stilzwijgend blind maken. De echte opsomming (enumeration) is PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 en PDFACTION_EMBEDDEDGOTO = 5. Let op wat ontbreekt: er is geen JavaScript-lid (member). Document-niveau scripts zijn geen linkacties en verschijnen nooit via FPDFAction_GetType; ze worden opgesomd door een afzonderlijke familie van aanroepen (calls). Een auditor die actietypen test tegen een ingebeelde JavaScript constante compileert, draait en vindt niets, voor altijd
const
PDFACTION_GOTO = 1; // in-document jump: harmless
PDFACTION_REMOTEGOTO = 2; // jump into another local file
PDFACTION_URI = 3; // opens an external URL
PDFACTION_LAUNCH = 4; // starts an external program
PDFACTION_EMBEDDEDGOTO = 5; // jump into an embedded file
function ActionTarget(Doc: FPDF_DOCUMENT; Action: FPDF_ACTION;
AType: ULONG): string;
var
Buf: array[0..2047] of AnsiChar;
begin
FillChar(Buf, SizeOf(Buf), 0);
if AType = PDFACTION_URI then
FPDFAction_GetURIPath(Doc, Action, @Buf, SizeOf(Buf))
else
FPDFAction_GetFilePath(Action, @Buf, SizeOf(Buf));
Result := string(UTF8String(PAnsiChar(@Buf)));
end;
procedure AuditPageActions(Doc: FPDF_DOCUMENT; Page: FPDF_PAGE;
PageNo: Integer; Findings: TFindings);
var
StartPos: Integer;
Link: FPDF_LINK;
Action: FPDF_ACTION;
AType: ULONG;
begin
StartPos := 0;
while FPDFLink_Enumerate(Page, @StartPos, @Link) <> 0 do
begin
Action := FPDFLink_GetAction(Link);
if Action = nil then
Continue; // destination-only link, nothing to flag
AType := FPDFAction_GetType(Action);
case AType of
PDFACTION_LAUNCH:
Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
'Launch action targets "' + ActionTarget(Doc, Action, AType) + '"');
PDFACTION_URI:
Add(Findings, fsWarning, 'ACT-URI', PageNo,
'link opens ' + ActionTarget(Doc, Action, AType));
PDFACTION_REMOTEGOTO, PDFACTION_EMBEDDEDGOTO:
Add(Findings, fsWarning, 'ACT-XFILE', PageNo,
'cross-file destination "' + ActionTarget(Doc, Action, AType) + '"');
end; // PDFACTION_GOTO stays silent by design
end;
end;
procedure AuditDocumentBehaviors(Doc: FPDF_DOCUMENT; Findings: TFindings);
var
N: Integer;
begin
N := FPDFDoc_GetJavaScriptActionCount(Doc);
if N > 0 then
Add(Findings, fsError, 'JS-DOC', 0,
Format('%d document-level JavaScript action(s) run on open', [N]));
N := FPDFDoc_GetAttachmentCount(Doc);
if N > 0 then
Add(Findings, fsWarning, 'ATT-EMB', 0,
Format('%d embedded file attachment(s)', [N]));
end;
De splitsing op basis van ernst (severity split) codeert beleid (policy). Een Launch-actie is een fout, omdat het starten van een willekeurig programma het meest gevaarlijke is wat een klik in een PDF kan doen, en geen enkele factuur heeft dit nodig. Externe URL's zijn waarschuwingen (warnings): ze komen vaak voor in legitieme documenten, maar een beoordelaar moet het doel (target) kunnen zien zonder erop te klikken, aangezien de zichtbare linktekst en de daadwerkelijke bestemming niet hoeven overeen te komen. Interne GoTo sprongen zijn structuur, geen gedrag, en blijven helemaal uit het rapport — een preflight die bij elke inhoudsopgave-invoer alarm slaat, traint mensen om dit te negeren. Voor het lezen van de script-body's achter de JavaScript telling (count), en voor handtekening MDP niveaus en XFA-detectie, doorloopt het artikel over auditing van beveiligingsrisico's hetzelfde oppervlak via de object-wrapper van de component
Resourcemetrics: effectieve afbeeldings-DPI
Een afbeelding binnen een PDF heeft geen eigen DPI. Het heeft pixels, en de pagina plaatst die pixels in een rechthoek gemeten in punten (points), waarbij 72 punten een inch (2,54 cm) vormen. Resolutie bestaat alleen als de verhouding (ratio) van de twee, en dat is waarom dezelfde 600 bij 400 foto haarscherp is als miniatuur (thumbnail) en een wazige boel als hero-afbeelding over de hele pagina (full-page hero). De audit heeft daarom beide getallen nodig voor elke afbeelding: bron-pixelafmetingen uit de metadata van de afbeelding, en de geplaatste rechthoek uit de objectgrenzen (object bounds)
procedure AuditPageImages(Page: FPDF_PAGE; PageNo: Integer;
Findings: TFindings);
var
I, ObjCount: Integer;
Obj: FPDF_PAGEOBJECT;
Meta: FPDF_IMAGEOBJ_METADATA;
L, B, R, T: Single;
WidthPt, HeightPt, DpiX, DpiY, EffDpi: Double;
begin
ObjCount := FPDFPage_CountObjects(Page);
for I := 0 to ObjCount - 1 do
begin
Obj := FPDFPage_GetObject(Page, I);
if FPDFPageObj_GetType(Obj) <> FPDF_PAGEOBJ_IMAGE then
Continue;
if FPDFImageObj_GetImageMetadata(Obj, Page, @Meta) = 0 then
Continue;
if FPDFPageObj_GetBounds(Obj, @L, @B, @R, @T) = 0 then
Continue;
WidthPt := R - L; // placed size on the page, in points
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 points = 1 inch, so placed inches = points / 72, and
// effective DPI = source pixels / placed inches.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // the worse axis decides print quality
if EffDpi < 150.0 then
Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
Format('image %dx%d px placed at %.1fx%.1f pt = %.0f DPI effective',
[Meta.Width, Meta.Height, WidthPt, HeightPt, EffDpi]))
else if EffDpi > 600.0 then
Add(Findings, fsInfo, 'IMG-BLOAT', PageNo,
Format('image is %.0f DPI at placed size; resampling would ' +
'shrink the file with no visible loss', [EffDpi]));
end;
end;
De drempels (thresholds) zijn beleid (policy), geen natuurkunde: 150 DPI is een ondergrens (floor) waaronder kantoorafdrukken zichtbaar korrelig worden (pixelates), 300 is het gebruikelijke commerciële doel, en alles daarboven levert geen zichtbare kwaliteit op terwijl de bestandsgrootte opblaast, en dat is waarom het wordt gerapporteerd als informatieve ballast (bloat) in plaats van een defect. Een eerlijk voorbehoud (caveat): FPDFPageObj_GetBounds retourneert de as-uitgelijnde doos (axis-aligned box), dus voor een afbeelding die met rotatie is geplaatst, onderschat het berekende cijfer de werkelijke dichtheid (density). De FPDF_IMAGEOBJ_METADATA structuur bevat ook horizontal_dpi en vertical_dpi velden die PDFium afleidt van de volledige transformatie-matrix (transform matrix), en het vergelijken van de twee resultaten is een goedkope manier om geroteerde plaatsingen (rotated placements) op te sporen. Dezelfde punten-naar-pixels rekenkunde stuurt renderen in de tegenovergestelde richting, wat wordt behandeld in het artikel over JPEG-export
Beveiligingsstatus (Security state): codering en machtigingsbits (permission bits)
PDF-codering definieert twee wachtwoorden met verschillende taken. Het gebruikerswachtwoord (user password) fungeert als poortwachter (gates) voor decodering: zonder dit zal het bestand helemaal niet openen, en FPDF_LoadDocument retourneert nil waarbij FPDF_GetLastError FPDF_ERR_PASSWORD rapporteert. Het eigenaarswachtwoord (owner password) fungeert als poortwachter voor machtigingen (permissions): een bestand dat alleen door een eigenaarswachtwoord wordt beschermd, opent zonder inloggegevens (credentials) maar draagt beperkingsbits (restriction bits) die een conforme lezer (conforming reader) in acht moet nemen (honor). De poging tot laden (load attempt) zelf is daarom de eerste beveiligingssonde (security probe), en het onderscheid bepaalt de exit-code — een bestand met een gebruikerswachtwoord kan niet worden ge-audit (code 2), terwijl een bestand met een eigenaarswachtwoord normaal wordt ge-audit en alleen maar bevindingen (findings) verzamelt
const
FPDF_ERR_PASSWORD = 4;
function AuditSecurity(const FileName: string;
Findings: TFindings): FPDF_DOCUMENT;
var
Perms: ULONG;
Revision: Integer;
begin
Result := FPDF_LoadDocument(PAnsiChar(AnsiString(FileName)), nil);
if Result = nil then
begin
if FPDF_GetLastError() = FPDF_ERR_PASSWORD then
Add(Findings, fsError, 'SEC-USERPW', 0,
'user (open) password required; audit cannot proceed')
else
Add(Findings, fsError, 'DOC-BROKEN', 0, 'file failed to parse');
Exit;
end;
Revision := FPDF_GetSecurityHandlerRevision(Result);
if Revision >= 0 then // -1 means the file is not encrypted
begin
// Opened with an empty password yet encrypted: owner-password-only.
// Anyone may read it, but the permission bits restrict what a
// conforming reader lets them do. Unencrypted files report all
// bits set, which is why the revision gate comes first.
Perms := FPDF_GetDocPermissions(Result);
Add(Findings, fsInfo, 'SEC-ENC', 0,
Format('encrypted, security handler revision %d', [Revision]));
if (Perms and 4) = 0 then // bit 3: print
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'printing is not permitted');
if (Perms and 16) = 0 then // bit 5: copy / extract content
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bit 12: high-resolution print
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
De maskers (masks) komen uit Tabel 22 van ISO 32000-1, die bits nummert vanaf 1: bit 3 van de /P waarde is masker 4, bit 5 is 16, bit 12 is 2048. Of een bepaalde bevinding er toe doet, is een routeringsbeslissing (routing decision). Een afdrukbureau zou een SEC-NOPRINT bestand direct bij inname (intake) moeten weigeren (bounce), waarbij de indiener een duidelijke boodschap krijgt, in plaats van op de RIP (Raster Image Processor) drie uur voor een deadline. Een archief zou SEC-ENC zelf als een blokkade (blocker) moeten behandelen, aangezien encryptie en langetermijnbewaring niet samengaan — een punt dat de standaardencontrole zometeen formeel zal maken
Standaardenmarkeringen (Standards markers): een PDF/A-claim lezen
Een bestand verklaart PDF/A-conformiteit (conformance) in het XMP metadata-pakket, via de pdfaid:part eigenschap (1 tot en met 4) en pdfaid:conformance (de niveau-letter (level letter), zoals b voor visuele getrouwheid of a voor volledige structurele tagging). De PDFium C API biedt geen XMP-toegang (accessor); FPDF_GetMetaText leest alleen het Info woordenboek, dat niet de plaats is waar de identificatie leeft. De uitweg (escape hatch) is een regel in de standaard zelf: ISO 19005 vereist dat de XMP metadata stroom ongecomprimeerd wordt opgeslagen, precies zodat tools het kunnen vinden zonder een volledige PDF-parser. Een scan van ruwe bytes (raw byte scan) is daarom een legitieme claim detector — en een bestand waarvan de claim zich verbergt in een gecomprimeerde stroom (compressed stream), heeft de standaard waarop het aanspraak maakt (claims) al geschonden (violated)
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // empty = no PDF/A claim present
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // XMP identification schema
if P = 0 then
Exit;
// Handles both <pdfaid:part>2</pdfaid:part> and pdfaid:part="2":
// take the first digit after the property name.
Limit := Min(P + 32, Length(S));
Inc(P, Length('pdfaid:part'));
while (P <= Limit) and not (S[P] in ['1'..'4']) do
Inc(P);
if P <= Limit then
Result := 'PDF/A-' + Char(S[P]);
end;
De bevinding (finding) die dit oplevert is bewust informatief, want een claim is een verklaring (declaration), geen eigenschap van het bestand. De XMP invoer is één regel XML die elke producent (producer) kan schrijven, inclusief een kapotte; conformiteit is het daadwerkelijk voldoen aan honderden regels door het bestand over ingebedde lettertypen, apparaatonafhankelijke kleur (device-independent color), en verboden functies. Het detecteren van de claim vertelt u alleen welke bestanden u moet doorsturen (route) naar echte validatie, en niets meer. De ingebouwde preflight engine van de component voert die validatie uit over PDF/A-, PDF/UA-, en PDF/X-profielen, en het artikel over batch CLI laat zien hoe dit in een pijplijn (pipeline) moet worden aangesloten met rapporten die een auditor later kan openen
Een run op een probleembestand
De driver rijgt de controles (checks) aaneen: veiligheid (security) eerst, omdat het beslist of de audit überhaupt draait, vervolgens de document-niveau gedragingen (behaviors) en de standaardenclaim (standards claim), daarna een pagina-loop voor acties en afbeeldingen
function AuditFile(const FileName: string; Findings: TFindings): Integer;
var
Doc: FPDF_DOCUMENT;
Page: FPDF_PAGE;
I: Integer;
Claim: string;
begin
Doc := AuditSecurity(FileName, Findings);
if Doc = nil then
Exit(2); // audit failure, not a verdict
try
AuditDocumentBehaviors(Doc, Findings);
Claim := PdfAClaim(FileName);
if Claim <> '' then
Add(Findings, fsInfo, 'STD-PDFA', 0,
Claim + ' conformance claimed (declaration only, not validated)');
for I := 0 to FPDF_GetPageCount(Doc) - 1 do
begin
Page := FPDF_LoadPage(Doc, I);
if Page = nil then
begin
Add(Findings, fsError, 'PAGE-BROKEN', I + 1, 'page failed to parse');
Continue;
end;
try
AuditPageActions(Doc, Page, I + 1, Findings);
AuditPageImages(Page, I + 1, Findings);
finally
FPDF_ClosePage(Page);
end;
end;
finally
FPDF_CloseDocument(Doc);
end;
if Findings.Count > 0 then
Result := 1
else
Result := 0;
end;
Tegen een brochure die terugkwam van een extern bureau ziet de uitvoer er zo uit
> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 finding(s)
[ERROR] ACT-LAUNCH page 3 Launch action targets "..\tools\setup.exe"
[ERROR] JS-DOC doc 2 document-level JavaScript action(s) run on open
[WARNING] IMG-LOWRES page 7 image 412x287 px placed at 396.0x275.8 pt = 75 DPI effective
[WARNING] SEC-NOPRINT doc printing is not permitted
[INFO] STD-PDFA doc PDF/A-2 conformance claimed (declaration only, not validated)
exit code 1
Elke regel is op zichzelf actie-waardig (actionable), maar de combinatie is het echte oordeel (verdict). Dit bestand claimt PDF/A-2 terwijl het een encryptiewoordenboek (encryption dictionary) en actieve (live) JavaScript bevat, en PDF/A verbiedt beide botweg (outright) — dus de claim is aantoonbaar onwaar voordat enige diepgaande validator draait. Dat is het soort tegenstrijdigheid dat een platte lijst van bevindingen (flat findings list) aan het licht brengt (surfaces) en een boolean geslaagd/gezakt (pass/fail) verbergt
Wat deze audit u niet kan vertellen
Eerlijkheid over de reikwijdte (scope) is wat een preflight tool betrouwbaar (trusted) houdt. Alles hierboven leest wat het bestand over zichzelf verklaart (declares): PDFium ontleedt (parses) structuur, en deze audit inventariseert het. Het voert geen PDF/A-validatie uit — geen glyph-dekking controles tegen ingebedde lettertypen, geen kleurruimte analyse (color space analysis) tegen uitvoerintenties (output intents), geen van de clausule-niveau regels die een claim scheiden van conformiteit (conformance); daarvoor heeft u een toegewijde (dedicated) validator nodig, zoals de preflight engine van de component of veraPDF. Machtigingsbits (Permission bits) zijn verklaringen die conforme lezers (conforming readers) respecteren (honor), geen cryptografische muren, dus SEC-NOPRINT beschrijft de bedoeling (intent) in plaats van handhaving (enforcement). De actiescan (action scan) dekt linkannotaties en document-niveau scripts; scripts begraven in gebeurtenissenwoordenboeken (event dictionaries) van formuliervelden hebben de form-API's daarbovenop (on top) nodig. En een handtekeningcontrole (signature check), als u de audit daarmee uitbreidt, rapporteert een verklaarde bedoeling, geen geverifieerde cryptografie — de validatie van de certificaatketen is een aparte taak. Een preflight audit is het intakegesprek, niet het proces (trial): de taak ervan is de routeringsbeslissing geïnformeerd, snel, en herhaalbaar (repeatable) te maken
Let op: De API's voor document, pagina, annotatie, en afbeeldingsobjecten die in deze audit worden gebruikt, worden samen met een high-level Delphi wrapper en een volledige standaarden-validatie (standards-validation) preflight engine meegeleverd met de PDFium Component