En PDF som ankommer en produksjonsgrense — en utskriftskø, et arkiv, en opplastingsportal for kunder — bør revideres før noe som helst gjengir den. Filen kan bære en Launch-handling koblet til å starte et eksternt program, bilder for grove til å overleve utskrift, en krypteringsdictionary som forbyr nettopp den utskriftsjobben den ble sendt inn for, eller en PDF/A-etikett den ikke lever opp til. Å inspisere et dokument mot slike regler før det slippes inn i en arbeidsflyt kalles preflighting, og PDFium C-API-et gir Delphi alt som trengs for å implementere kontrollene direkte, uten å gjengi en eneste side
Denne artikkelen bygger selve kontrollene: fire revisjonsklasser, hver en liten rutine som føyer funn til en delt resultatliste. Interaktive elementer, ressursmålinger, sikkerhetstilstand og standardmarkører får alle fungerende kode, aritmetikken inkludert. Trenger du maskineriet rundt kontrollene — mappeløkker for batch, JSON- og HTML-rapportfiler, isolasjon per fil — leveres PDFium Component med en ferdig preflightmotor, og artikkelen om batch-preflight-kommandolinjen dekker den rørleggingen. De to deler bevisst ett vokabular for avslutningskoder, så en revisor skrevet her passer rett inn under den batchdriveren
Funn-recorden og kontrakten for avslutningskoder
Hver kontroll skriver inn i én flat recordtype, fordi alternativet, at hver kontroll skriver sin egen prosa, ikke kan telles, filtreres eller terskles i etterkant. 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; // stabil maskinnøkkel, f.eks. 'ACT-LAUNCH'
Page: Integer; // 1-basert; 0 betyr dokumentnivå
Message: string; // for mennesker; fritt å omformulere mellom utgivelser
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;
Nedstrømsverktøy nøkler på Code, aldri på Message-teksten, som fritt kan endre seg. Prosessens avslutningskode følger den samme trekontrakten som batchartikkelen: 0 betyr at filen ikke ga noen funn, 1 betyr at det finnes funn, og 2 betyr at selve revisjonen ikke kunne kjøre fordi filen ikke lot seg parse eller krever et passord. Å holde kode 2 atskilt betyr noe. En mappe med ødelagte skanninger er en defekt skanner oppstrøms, ikke et plutselig samsvarssammenbrudd, og å slå de to sammen sender noen ut på jakt etter feil problem
Interaktive elementer: skript, Launch-mål, eksterne lenker
PDFium klassifiserer hver handling den finner, med en heltallstype, og konstantene fra fpdf_doc.h er verdt å fastslå presist, fordi feilkopierte verdier gjør en skanner stille blind. Den virkelige enumerasjonen er PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 og PDFACTION_EMBEDDEDGOTO = 5. Merk deg hva som mangler: det finnes ikke noe JavaScript-medlem. Skript på dokumentnivå er ikke lenkehandlinger og dukker aldri opp gjennom FPDFAction_GetType; de enumereres av en egen familie av kall. En revisor som tester handlingstyper mot en innbilt JavaScript-konstant, kompilerer, kjører og finner ingenting, for alltid
const
PDFACTION_GOTO = 1; // hopp inne i dokumentet: ufarlig
PDFACTION_REMOTEGOTO = 2; // hopp inn i en annen lokal fil
PDFACTION_URI = 3; // åpner en ekstern URL
PDFACTION_LAUNCH = 4; // starter et eksternt program
PDFACTION_EMBEDDEDGOTO = 5; // hopp inn i en innebygd fil
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; // lenke med bare destinasjon, ingenting å flagge
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 tier etter 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;
Delingen i alvorlighetsgrader koder policy. En Launch-handling er en feil fordi det å starte et vilkårlig program er det farligste et klikk i en PDF kan gjøre, og ingen faktura trenger det. Eksterne URI-er er advarsler: vanlige i legitime dokumenter, men en gjennomgåer bør se målet uten å klikke, siden den synlige lenketeksten og den faktiske destinasjonen ikke trenger å stemme overens. GoTo-hopp inne i dokumentet er struktur, ikke atferd, og holdes helt utenfor rapporten — en preflight som roper ulv ved hver innholdsfortegnelsesoppføring, lærer folk opp til å overse den. For å lese skriptkroppene bak JavaScript-antallet, og for MDP-nivåer for signaturer og XFA-oppdaging, går artikkelen om revisjon av sikkerhetsrisiko gjennom den samme flaten via komponentens objektinnpakning
Ressursmålinger: effektiv bilde-DPI
Et bilde inne i en PDF har ingen DPI av seg selv. Det har piksler, og siden plasserer de pikslene inn i et rektangel målt i punkter, der 72 punkter utgjør en tomme. Oppløsning finnes bare som forholdet mellom de to, som er grunnen til at det samme bildet på 600 ganger 400 er knivskarpt som miniatyrbilde og et uskarpt rot som helsidebilde. Revisjonen trenger derfor begge tallene for hvert bilde: kildens pikseldimensjoner fra bildemetadataene, og det plasserte rektangelet fra objektets avgrensning
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; // plassert størrelse på siden, i punkter
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 punkter = 1 tomme, så plasserte tommer = punkter / 72, og
// effektiv DPI = kildepiksler / plasserte tommer.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // den dårligste aksen avgjør utskriftskvaliteten
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;
Tersklene er policy, ikke fysikk: 150 DPI er et gulv der kontorutskrift begynner å pikselere synlig, 300 er det vanlige kommersielle målet, og alt over 600 kjøper ingen synlig kvalitet, men blåser opp filstørrelsen, som er grunnen til at det rapporteres som informativ oppblåsthet snarere enn en defekt. Ett ærlig forbehold: FPDFPageObj_GetBounds returnerer den akseparallelle boksen, så for et bilde plassert med rotasjon undervurderer det beregnede tallet den virkelige tettheten. FPDF_IMAGEOBJ_METADATA-strukturen bærer også feltene horizontal_dpi og vertical_dpi, som PDFium utleder fra den fulle transformasjonsmatrisen, og å sammenligne de to resultatene er en billig måte å oppdage roterte plasseringer på. Den samme aritmetikken fra punkter til piksler driver gjengivelsen i motsatt retning, dekket i artikkelen om JPEG-eksport
Sikkerhetstilstand: kryptering og tillatelsesbiter
PDF-kryptering definerer to passord med ulike oppgaver. Brukerpassordet porter dekrypteringen: uten det åpner filen seg ikke i det hele tatt, og FPDF_LoadDocument returnerer nil med FPDF_GetLastError som melder FPDF_ERR_PASSWORD. Eierpassordet porter tillatelsene: en fil beskyttet bare av et eierpassord åpner seg uten legitimasjon, men bærer restriksjonsbiter som en leser som følger spesifikasjonen må respektere. Selve innlastingsforsøket er derfor den første sikkerhetssonden, og skillet avgjør avslutningskoden — en fil med brukerpassord kan ikke revideres (kode 2), mens en fil med eierpassord revideres normalt og bare samler opp funn
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 betyr at filen ikke er kryptert
begin
// Åpnet med tomt passord, men likevel kryptert: kun eierpassord.
// Hvem som helst kan lese den, men tillatelsesbitene begrenser hva en
// leser som følger spesifikasjonen lar dem gjøre. Ukrypterte filer melder
// alle biter satt, som er grunnen til at revisjonsporten kommer først.
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: utskrift
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'printing is not permitted');
if (Perms and 16) = 0 then // bit 5: kopiering / uttrekk av innhold
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bit 12: høyoppløst utskrift
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
Maskene kommer fra tabell 22 i ISO 32000-1, som nummererer biter fra 1: bit 3 av /P-verdien er maske 4, bit 5 er 16, bit 12 er 2048. Hvorvidt et gitt funn betyr noe, er en rutingbeslutning. Et trykkeri bør avvise en SEC-NOPRINT-fil ved inntaket, der innsenderen får en tydelig melding, snarere enn ved RIP-en tre timer før en frist. Et arkiv bør behandle SEC-ENC i seg selv som en blokkering, siden kryptering og langtidsbevaring ikke går sammen — et poeng standardkontrollen er i ferd med å slå fast formelt
Standardmarkører: å lese en PDF/A-påstand
En fil erklærer PDF/A-samsvar i XMP-metadatapakken sin, gjennom egenskapen pdfaid:part (1 til 4) og pdfaid:conformance (nivåbokstaven, som b for visuell troskap eller a for full strukturell tagging). PDFiums C-API tilbyr ingen XMP-tilgang; FPDF_GetMetaText leser bare Info-dictionaryen, som ikke er der identifikasjonen bor. Nødutgangen er en regel i selve standarden: ISO 19005 krever at XMP-metadatastrømmen lagres ukomprimert, nettopp slik at verktøy kan finne den uten en full PDF-parser. En rå bytegjennomgang er derfor en legitim påstandsdetektor — og en fil hvis påstand gjemmer seg inne i en komprimert strøm, har allerede brutt standarden den påberoper seg
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // tom = ingen PDF/A-påstand til stede
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // XMP-identifikasjonsskjema
if P = 0 then
Exit;
// Håndterer både <pdfaid:part>2</pdfaid:part> og pdfaid:part="2":
// ta det første sifferet etter egenskapsnavnet.
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;
Funnet dette gir, er bevisst informativt, fordi en påstand er en erklæring, ikke en egenskap ved filen. XMP-oppføringen er én linje XML som enhver produsent kan skrive, også en defekt en; samsvar er at filen faktisk oppfyller hundrevis av regler om innebygde fonter, enhetsuavhengige farger og forbudte funksjoner. Å oppdage påstanden forteller deg hvilke filer du skal rute til ekte validering, og ikke noe mer. Komponentens innebygde preflightmotor utfører den valideringen på tvers av PDF/A-, PDF/UA- og PDF/X-profiler, og artikkelen om batch-kommandolinjen viser hvordan du kobler den inn i en rørledning med rapporter en revisor kan åpne senere
En kjøring mot en problemfil
Driveren tråder kontrollene sammen: sikkerhet først, fordi den avgjør om revisjonen kjører i det hele tatt, deretter atferd på dokumentnivå og standardpåstanden, så en sideløkke for handlinger og bilder
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); // revisjonsfeil, ikke en dom
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;
Mot en brosjyre som kom tilbake fra et eksternt byrå ser utdataene slik ut
> 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 er handlingsrettet i seg selv, men kombinasjonen er den virkelige dommen. Denne filen påberoper seg PDF/A-2 samtidig som den bærer en krypteringsdictionary og levende JavaScript, og PDF/A forbyr begge deler rett ut — så påstanden er beviselig usann før noen dyp validator i det hele tatt kjører. Det er den typen motsigelse en flat funnliste bringer frem i lyset og en boolsk bestått/ikke bestått skjuler
Hva denne revisjonen ikke kan fortelle deg
Ærlighet om virkeområdet er det som holder et preflightverktøy troverdig. Alt det ovenstående leser hva filen erklærer om seg selv: PDFium parser strukturen, og denne revisjonen inventerer den. Den utfører ikke PDF/A-validering — ingen kontroll av glyffdekning mot innebygde fonter, ingen fargeromsanalyse mot utdataintensjoner, ingen av reglene på klausulnivå som skiller en påstand fra samsvar; til det trenger du en dedikert validator som komponentens preflightmotor eller veraPDF. Tillatelsesbiter er erklæringer som lesere som følger spesifikasjonen respekterer, ikke kryptografiske murer, så SEC-NOPRINT beskriver intensjon snarere enn håndhevelse. Handlingsskanningen dekker lenkemerknader og skript på dokumentnivå; skript begravd i hendelsesdictionaryer for skjemafelt krever skjema-API-ene i tillegg. Og en signaturkontroll, om du utvider revisjonen med en, melder erklært intensjon, ikke verifisert kryptografi — validering av sertifikatkjeder er en egen jobb. En preflightrevisjon er inntaksintervjuet, ikke rettssaken: jobben dens er å gjøre rutingbeslutningen informert, rask og repeterbar
Merk: Dokument-, side-, merknads- og bildeobjekt-API-ene som brukes gjennom hele denne revisjonen, sammen med en høynivå Delphi-innpakning og en fullstendig preflightmotor for standardvalidering, følger med PDFium Component