En PDF som anländer till en produktionsgräns – en utskriftskö, ett arkiv, en kunduppladdningsportal – bör revideras innan något renderar den. Filen kan innehålla en Launch-åtgärd som är kopplad till att starta ett externt program, bilder som är för grova för att överleva utskrift, en krypteringsordbok som förbjuder just det utskriftsjobb den skickades in för, eller en PDF/A-etikett den inte lever upp till. Att inspektera ett dokument mot regler som dessa innan det går in i ett arbetsflöde kallas preflighting, och PDFium C API:et ger Delphi allt som behövs för att implementera kontrollerna direkt, utan att rendera en enda sida
Den här artikeln bygger själva kontrollerna: fyra revisionsklasser, var och en en liten rutin som lägger till fynd i en gemensam resultatlista. Interaktiva element, resursmått, säkerhetsstatus och standardmarkörer får alla fungerande kod, inklusive aritmetiken. Om det du behöver är maskineriet kring kontrollerna – batchmappsloopar, JSON- och HTML-rapportfiler, isolering per fil – levereras PDFium Component med en färdig preflight-motor, och artikeln om batch-preflight CLI täcker den infrastrukturen. De två delar avsiktligt ett ordförråd för avslutningskoder, så en granskare som skrivs här passar rakt in under den batchdrivrutinen
Fyndposten och kontraktet för avslutningskoden
Varje kontroll skriver till en platt posttyp (record), eftersom alternativet, där varje kontroll skriver ut sin egen text, inte kan räknas, filtreras eller tröskelsättas i efterhand. Fyra fält räcker
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;
Verktyg i senare led förlitar sig på Code, aldrig på Message-texten, som kan ändras fritt. Processens avslutningskod följer samma trevärdeskontrakt som batchartikeln: 0 betyder att filen inte producerade några fynd, 1 betyder att fynd existerar och 2 betyder att själva granskningen inte kunde köras eftersom filen inte kunde tolkas eller kräver ett lösenord. Att hålla kod 2 separat spelar roll. En mapp med korrupta skanningar är en trasig skanner i tidigare led, inte en plötslig kollaps i efterlevnaden, och att slå ihop de två får någon att jaga fel problem
Interaktiva element: skript, startmål, externa länkar
PDFium klassificerar varje åtgärd den hittar med en heltalstyp, och konstanterna från fpdf_doc.h är värda att fastställa exakt, eftersom felkopierade värden gör en skanner tyst blind. Den riktiga uppräkningen är PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 och PDFACTION_EMBEDDEDGOTO = 5. Notera vad som saknas: det finns ingen JavaScript-medlem. Skript på dokumentnivå är inte länkåtgärder och dyker aldrig upp via FPDFAction_GetType; de räknas upp av en separat familj av anrop. En granskare som testar åtgärdstyper mot en påhittad JavaScript-konstant kompilerar, körs och hittar ingenting, någonsin
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;
Allvarlighetsgraden kodar policy. En Launch-åtgärd är ett fel eftersom att starta ett godtyckligt program är det farligaste ett klick i en PDF kan göra, och ingen faktura behöver det. Externa URI:er är varningar: vanliga i legitima dokument, men en granskare bör se målet utan att klicka, eftersom den synliga länktexten och den faktiska destinationen inte behöver stämma överens. GoTo-hopp i dokumentet är struktur, inte beteende, och hålls helt utanför rapporten – en preflight som ropar varg på varje innehållsförteckningspost tränar människor att ignorera den. För att läsa skriptkropparna bakom JavaScript-antalet, och för signatur-MDP-nivåer och XFA-detektering, går artikeln om säkerhetsriskgranskning igenom samma yta genom komponentens objektomslag
Resursmått: effektiv bild-DPI
En bild inuti en PDF har ingen egen DPI. Den har pixlar, och sidan placerar de pixlarna i en rektangel mätt i punkter, där 72 punkter utgör en tum. Upplösning existerar bara som förhållandet mellan de två, vilket är anledningen till att samma foto på 600 x 400 är knivskarpt som en miniatyrbild och en suddig röra som en helsideshjälte. Granskningen behöver därför båda siffrorna för varje bild: källpixeldimensioner från bildens metadata och den placerade rektangeln från objektets gränser
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;
Tröskelvärdena är policy, inte fysik: 150 DPI är ett golv under vilket kontorsutskrifter synligt pixlas, 300 är det vanliga kommersiella målet, och allt över 600 köper ingen synlig kvalitet samtidigt som det blåser upp filstorleken, vilket är anledningen till att det rapporteras som informativ uppblåsthet snarare än en defekt. Ett ärligt förbehåll: FPDFPageObj_GetBounds returnerar den axeljusterade rutan, så för en bild som placerats med rotation underskattar den beräknade siffran den verkliga densiteten. FPDF_IMAGEOBJ_METADATA-strukturen bär också horizontal_dpi och vertical_dpi-fält som PDFium härleder från den fullständiga transformeringsmatrisen, och att jämföra de två resultaten är ett billigt sätt att upptäcka roterade placeringar. Samma punkt-till-pixel-aritmetik driver rendering i motsatt riktning, vilket behandlas i artikeln om JPEG-export
Säkerhetsstatus: kryptering och behörighetsbitar
PDF-kryptering definierar två lösenord med olika jobb. Användarlösenordet vaktar dekrypteringen: utan det öppnas inte filen alls, och FPDF_LoadDocument returnerar nil med FPDF_GetLastError som rapporterar FPDF_ERR_PASSWORD. Ägarlösenordet vaktar behörigheter: en fil som skyddas enbart av ett ägarlösenord öppnas utan inloggningsuppgifter men bär restriktionsbitar som en överensstämmande läsare måste respektera. Själva inläsningsförsöket är därför den första säkerhetskontrollen, och skillnaden avgör avslutningskoden – en användarlösenordsfil kan inte granskas (kod 2), medan en ägarlösenordsfil granskas normalt och enbart ackumulerar fynd
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;
Maskerna kommer från tabell 22 i ISO 32000-1, som numrerar bitar från 1: bit 3 i /P-värdet är mask 4, bit 5 är 16, bit 12 är 2048. Huruvida ett givet fynd spelar någon roll är ett dirigeringsbeslut. En utskriftsbyrå bör avvisa en SEC-NOPRINT-fil vid intaget, där inlämnaren får ett tydligt meddelande, snarare än vid RIP:en tre timmar före en deadline. Ett arkiv bör behandla själva SEC-ENC som ett hinder, eftersom kryptering och långtidsbevarande inte hör ihop – en poäng som standardkontrollen är på väg att göra formellt
Standardmarkörer: läsa ett PDF/A-anspråk
En fil deklarerar PDF/A-överensstämmelse i sitt XMP-metadatapaket, genom egenskapen pdfaid:part (1 till 4) och pdfaid:conformance (nivåbokstaven, såsom b för visuell trohet eller a för fullständig strukturell taggning). PDFiums C API erbjuder ingen XMP-åtkomst; FPDF_GetMetaText läser bara Info-ordboken, vilket inte är där identifikationen bor. Nödutgången är en regel i själva standarden: ISO 19005 kräver att XMP-metadataströmmen lagras okomprimerad, just för att verktyg ska kunna hitta den utan en fullständig PDF-tolkare. En rå byte-skanning är därför en legitim anspråksdetektor – och en fil vars anspråk gömmer sig inuti en komprimerad ström har redan brutit mot den standard den gör anspråk på
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;
Fyndet detta producerar är avsiktligt informativt, eftersom ett anspråk är en deklaration, inte en egenskap hos filen. XMP-posten är en rad XML som vilken producent som helst kan skriva, inklusive en trasig sådan; överensstämmelse är att filen faktiskt uppfyller hundratals regler om inbäddade teckensnitt, enhetsoberoende färg och förbjudna funktioner. Att upptäcka anspråket talar om för dig vilka filer du ska dirigera till verklig validering, och inget mer. Komponentens inbyggda preflight-motor utför den valideringen över PDF/A-, PDF/UA- och PDF/X-profiler, och artikeln om batch CLI visar hur man kopplar in den i en pipeline med rapporter som en granskare kan öppna senare
En körning mot en problemfil
Drivrutinen binder samman kontrollerna: säkerhet först, eftersom det avgör om granskningen körs alls, sedan beteenden på dokumentnivå och standardanspråket, sedan en sidloop för åtgärder och 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); // 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;
Mot en broschyr som kom tillbaka från en extern byrå ser utdata ut så här
> 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
Varje rad är handlingsbar i sig, men kombinationen är den verkliga domen. Den här filen gör anspråk på PDF/A-2 samtidigt som den bär en krypteringsordbok och live JavaScript, och PDF/A förbjuder båda rakt av – så anspråket är bevisligen falskt innan någon djup validerare ens körs. Det är den typen av motsägelse en platt fyndlista för fram i ljuset och ett booleskt godkänd/underkänd döljer
Vad den här granskningen inte kan berätta för dig
Ärlighet om omfattning är det som håller ett preflight-verktyg pålitligt. Allt ovan läser vad filen deklarerar om sig själv: PDFium tolkar struktur, och den här granskningen inventerar den. Den utför ingen PDF/A-validering – inga glyftäckningskontroller mot inbäddade teckensnitt, ingen färgrymdsanalys mot utgångsavsikter (output intents), inga av reglerna på klausulnivå som skiljer ett anspråk från överensstämmelse; för det behöver du en dedikerad validerare som komponentens preflight-motor eller veraPDF. Behörighetsbitar är deklarationer som överensstämmande läsare respekterar, inte kryptografiska murar, så SEC-NOPRINT beskriver avsikt snarare än tillämpning. Åtgärdsskanningen täcker länkkommentarer och skript på dokumentnivå; skript begravda i formulärfälts händelseordböcker (event dictionaries) behöver formulär-API:erna ovanpå. Och en signaturkontroll, om du utökar granskningen med en sådan, rapporterar deklarerad avsikt, inte verifierad kryptografi – validering av certifikatkedjor är ett separat jobb. En preflight-granskning är intagningsintervjun, inte rättegången: dess jobb är att göra dirigeringsbeslutet välinformerat, snabbt och repeterbart
Obs: API:erna för dokument-, sid-, kommentars- och bildobjekt som används genomgående i den här granskningen, tillsammans med ett Delphi-omslag på hög nivå och en fullständig preflight-motor för standardvalidering, levereras med PDFium Component