En PDF, der ankommer til en produktionsgrænse — en printkø, et arkiv, en kunde-uploadportal — bør revideres (audited), før noget som helst render den. Filen bærer muligvis en Launch-handling (handling til start), der er kodet til at starte et eksternt program, billeder, der er for grove til at overleve print, en krypteringsordbog (encryption dictionary), der forbyder selve det printjob, den blev indsendt til, eller et PDF/A-label, den ikke lever op til. At inspicere et dokument mod regler som disse, før det indgår i en arbejdsgang (workflow), kaldes preflighting, og PDFium C API'et giver Delphi alt, hvad der behøves for at implementere tjekkene direkte, uden at rendere en eneste side
Denne artikel bygger selve tjekkene: fire revisionsklasser, hver en lille rutine, der tilføjer fund til en delt resultatliste. Interaktive elementer, ressource-metrikker, sikkerhedstilstand og standardmarkører får alle fungerende kode, inklusive aritmetikken. Hvis det, du har brug for, er maskineriet omkring tjekkene — batch mappe-loops, JSON- og HTML-rapportfiler, isolation pr. fil — leveres PDFium-komponenten med en færdiglavet preflight-motor, og artiklen om batch preflight CLI dækker den infrastruktur (plumbing). De to deler bevidst ét exit-kode-vokabular, så en revisor skrevet her passer direkte ind under den batch-driver
Fund-recorden (finding record) og exit-kode-kontrakten
Hvert tjek skriver i én flad record-type, fordi alternativet, hvor hvert tjek udskriver sin egen prosa, ikke kan tælles, filtreres eller sættes tærskler for bagefter. Fire felter er nok
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-værktøjer afhænger (keys on) af Code, aldrig af Message-teksten, som frit kan ændres. Processens exit-kode følger den samme tre-værdi kontrakt som batch-artiklen: 0 betyder, at filen ikke producerede nogen fund, 1 betyder, at der findes fund, og 2 betyder, at selve revisionen ikke kunne køre, fordi filen mislykkedes med at parse eller kræver en adgangskode. Det er vigtigt at holde kode 2 separat. En mappe med korrupte scanninger er en ødelagt scanner upstream, ikke et pludseligt compliance-kollaps, og at folde de to sammen sender nogen på jagt efter det forkerte problem
Interaktive elementer: scripts, launch-mål, eksterne links
PDFium klassificerer hver handling, den finder, efter en heltalstype, og konstanterne fra fpdf_doc.h er værd at slå fast præcist, fordi forkert kopierede værdier gør en scanner lydløst blind. Den rigtige opregning (enumeration) er PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 og PDFACTION_EMBEDDEDGOTO = 5. Bemærk hvad der mangler: der er intet JavaScript-medlem. Scripts på dokumentniveau er ikke link-handlinger og dukker aldrig op via FPDFAction_GetType; de opregnes af en separat familie af kald. En revisor, der tester handlingstyper mod en indbildt JavaScript-konstant kompilerer, kører og finder ingenting, for evigt
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;
Alvorlighedsopdelingen koder politikken. En Launch-handling er en fejl, fordi at starte et vilkårligt program er det farligste, et klik i en PDF kan gøre, og ingen faktura har brug for det. Eksterne URI'er er advarsler: almindelige i legitime dokumenter, men en anmelder (reviewer) bør se målet uden at klikke, da den synlige linktekst og den faktiske destination ikke behøver at stemme overens. GoTo-spring i dokumentet er struktur, ikke adfærd, og holdes helt ude af rapporten — et preflight, der råber ulv på hver indholdsfortegnelsespost, træner folk til at ignorere det. For at læse script-kroppene (script bodies) bag JavaScript-tællingen og for signatur-MDP-niveauer og XFA-detektion, gennemgår artiklen om revision af sikkerhedsrisici den samme overflade gennem komponentens objekt-wrapper
Ressource-metrikker: effektiv billed-DPI
Et billede inde i en PDF har ingen egen DPI. Det har pixels, og siden placerer de pixels i et rektangel målt i punkter (points), hvor 72 punkter udgør en tomme (inch). Opløsning eksisterer kun som forholdet mellem de to, hvilket er grunden til, at det samme 600 x 400 foto er knivskarpt som miniaturebillede og et sløret rod som et helsides heltebillede (hero image). Revisionen har derfor brug for begge tal for hvert billede: kildepixeldimensioner fra billedmetadataene og det placerede rektangel fra objektgrænserne
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;
Tærsklerne er politik, ikke fysik: 150 DPI er en bund, under hvilken kontorprint synligt pixelerer, 300 er det sædvanlige kommercielle mål, og alt over 600 køber ingen synlig kvalitet, mens det oppuster filstørrelsen, hvilket er grunden til, at det rapporteres som informativt oppustethed (bloat) snarere end en defekt. Ét ærligt forbehold: FPDFPageObj_GetBounds returnerer den akse-justerede (axis-aligned) boks, så for et billede placeret med rotation undervurderer det beregnede tal den sande tæthed. FPDF_IMAGEOBJ_METADATA-structen bærer også horizontal_dpi og vertical_dpi felter, som PDFium udleder fra den fulde transformationsmatrix, og at sammenligne de to resultater er en billig måde at spotte roterede placeringer. Den samme punkter-til-pixels aritmetik driver rendering i den modsatte retning, dækket i JPEG-eksportartiklen
Sikkerhedstilstand: kryptering og tilladelsesbits
PDF-kryptering definerer to adgangskoder med forskellige job. Brugeradgangskoden styrer dekryptering: uden den åbner filen slet ikke, og FPDF_LoadDocument returnerer nil med FPDF_GetLastError, der rapporterer FPDF_ERR_PASSWORD. Ejeradgangskoden styrer tilladelser: en fil, der kun er beskyttet af en ejeradgangskode, åbner uden legitimationsoplysninger, men bærer restriktionsbits, som en overensstemmende læser skal overholde. Selve indlæsningsforsøget er derfor den første sikkerheds-sondering, og sondringen bestemmer exit-koden — en brugeradgangskodefil kan ikke revideres (kode 2), mens en ejeradgangskodefil reviderer normalt og blot akkumulerer fund
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;
Maskerne kommer fra tabel 22 i ISO 32000-1, som nummererer bits fra 1: bit 3 af /P-værdien er maske 4, bit 5 er 16, bit 12 er 2048. Om et givet fund betyder noget, er en routing-beslutning. Et printbureau bør afvise (bounce) en SEC-NOPRINT-fil ved indtagelsen, hvor indsenderen får en klar besked, snarere end ved RIP'en tre timer før en deadline. Et arkiv bør behandle SEC-ENC i sig selv som en blokering, da kryptering og langsigtet bevarelse ikke passer sammen — en pointe, standardtjekket er ved at fremføre formelt
Standardmarkører: læsning af et PDF/A-krav
En fil erklærer PDF/A-overensstemmelse i sin XMP-metadatapakke gennem pdfaid:part-egenskaben (1 til 4) og pdfaid:conformance (niveau-bogstavet, såsom b for visuel troskab eller a for fuld strukturel tagging). PDFiums C API tilbyder ingen XMP-accessor; FPDF_GetMetaText læser kun Info-ordbogen, hvilket ikke er der, identifikationen lever. Nødudgangen er en regel i selve standarden: ISO 19005 kræver, at XMP-metadatastrømmen gemmes ukomprimeret, netop så værktøjer kan finde den uden en fuld PDF-parser. En rå byte-scanning er derfor en legitim detektor af krav (claim detector) — og en fil, hvis krav (claim) gemmer sig i en komprimeret strøm, har allerede overtrådt den standard, den hævder (claims)
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;
Fundet, dette producerer, er bevidst informativt, fordi et krav (claim) er en erklæring, ikke en egenskab ved filen. XMP-posten er én linje XML, som enhver producent kan skrive, inklusive en defekt én; overensstemmelse er, at filen faktisk opfylder hundreder af regler om indlejrede skrifttyper, uafhængig farve fra enheden og forbudte funktioner. At opdage kravet fortæller dig, hvilke filer der skal dirigeres til ægte validering, og intet mere. Komponentens indbyggede preflight-motor udfører den validering på tværs af PDF/A-, PDF/UA- og PDF/X-profiler, og batch CLI-artiklen viser, hvordan man forbinder den til en pipeline med rapporter, som en revisor kan åbne senere
En kørsel mod en problemfil
Driveren binder (strings) tjekkene sammen: sikkerhed først, fordi det afgør, om revisionen overhovedet kører, derefter adfærd på dokumentniveau og standardkravet, og derefter en sideløkke (page loop) for handlinger og billeder
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;
Mod en brochure, der kom tilbage fra et eksternt bureau, ser outputtet således ud
> 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
Hver linje kan handles på alene (actionable), men kombinationen er den rigtige dom. Denne fil hævder PDF/A-2, mens den bærer en krypteringsordbog og live JavaScript, og PDF/A forbyder begge dele fuldstændigt — så påstanden er bevisligt falsk, før nogen dyb validering kører. Det er den slags modsigelse, en flad fundliste (findings list) bringer op til overfladen, og som et boolsk bestået/ikke-bestået skjuler
Hvad denne revision ikke kan fortælle dig
Ærlighed omkring omfang (scope) er det, der holder et preflight-værktøj betroet. Alt ovenstående læser, hvad filen erklærer om sig selv: PDFium parser struktur, og denne revision opgør (inventories) den. Den udfører ikke PDF/A-validering — ingen tjek af glyf-dækning mod indlejrede skrifttyper, ingen farverumsanalyse mod output-intents (output intents), ingen af reglerne på klausul-niveau, der adskiller et krav (claim) fra overensstemmelse; til det har du brug for en dedikeret validator såsom komponentens preflight-motor eller veraPDF. Tilladelsesbits (permission bits) er erklæringer, som overensstemmende læsere respekterer, ikke kryptografiske mure, så SEC-NOPRINT beskriver hensigt snarere end håndhævelse. Handlings-scanningen (action scan) dækker link-annotationer og scripts på dokumentniveau; scripts begravet i formularfelt-hændelsesordbøger har brug for form-API'erne ovenpå. Og et signatur-tjek, hvis du udvider revisionen med et, rapporterer erklæret hensigt, ikke verificeret kryptografi — validering af certifikatkæden er et særskilt job. En preflight-revision er indtagelsessamtalen (intake interview), ikke retssagen: dens opgave er at gøre routingbeslutningen informeret, hurtig og repeterbar
Bemærk: De dokument-, side-, annotations- og billedobjekt-API'er, der bruges gennem hele denne revision, sammen med en højniveau Delphi-wrapper og en fuld standard-validerings preflight-motor, leveres med PDFium-komponenten