Un PDF care ajunge la o limită de producție — o coadă de imprimare, o arhivă, un portal de încărcare a clienților — ar trebui auditat înainte ca ceva să îl randeze. Fișierul ar putea conține o acțiune de lansare setată să pornească un program extern, imagini prea neclare pentru a supraviețui imprimării, un dicționar de criptare care interzice chiar sarcina de imprimare pentru care a fost trimis, sau o etichetă PDF/A la standardele căreia nu se ridică. Inspectarea unui document conform unor reguli ca acestea înainte ca acesta să intre într-un flux de lucru se numește verificare preliminară (preflighting), iar API-ul C PDFium oferă limbajului Delphi tot ce este necesar pentru a implementa verificările direct, fără a randa o singură pagină
Acest articol construiește verificările în sine: patru clase de audit, fiecare o mică rutină care adaugă constatările la o listă de rezultate partajată. Elementele interactive, metricile resurselor, starea de securitate și markerii standardelor primesc toate cod de lucru, inclusiv aritmetica. Dacă ceea ce aveți nevoie este mecanismul din jurul verificărilor — bucle de dosare în lot, fișiere de raport JSON și HTML, izolare pe fișier — Componenta PDFium livrează un motor de verificare preliminară gata făcut, iar articolul despre CLI de verificare preliminară în lot acoperă acele detalii. Cele două împart în mod deliberat același vocabular pentru codurile de ieșire, astfel încât un auditor scris aici se integrează direct sub acel driver de lot
Înregistrarea constatărilor și contractul codurilor de ieșire
Fiecare verificare scrie într-un singur tip de înregistrare plată, deoarece alternativa, în care fiecare verificare își imprimă propria proză, nu poate fi numărată, filtrată sau supusă unor limite ulterior. Patru câmpuri sunt suficiente
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;
Uneltele din aval se bazează pe Code, niciodată pe textul Message, care se poate modifica liber. Codul de ieșire al procesului urmează același contract cu trei valori ca în articolul despre verificarea în lot: 0 înseamnă că fișierul nu a produs nicio constatare, 1 înseamnă că există constatări, iar 2 înseamnă că auditul în sine nu a putut rula deoarece fișierul nu a putut fi analizat sau necesită o parolă. Păstrarea codului 2 separat este importantă. Un dosar cu scanări corupte reprezintă un scaner stricat în amonte, nu un colaps brusc de conformitate, iar combinarea celor două trimite pe cineva să urmărească problema greșită
Elemente interactive: scripturi, ținte de lansare, linkuri externe
PDFium clasifică fiecare acțiune pe care o găsește printr-un tip de număr întreg, iar constantele din fpdf_doc.h merită fixate cu precizie, deoarece valorile copiate greșit fac un scaner orb în tăcere. Enumerația reală este PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 și PDFACTION_EMBEDDEDGOTO = 5. Observați ce lipsește: nu există niciun membru JavaScript. Scripturile la nivel de document nu sunt acțiuni de link și nu apar niciodată prin FPDFAction_GetType; ele sunt enumerate printr-o familie separată de apeluri. Un auditor care testează tipurile de acțiuni față de o constantă JavaScript imaginată compilează, rulează și nu găsește nimic, pentru totdeauna
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;
Împărțirea severității codifică politica. O acțiune de lansare este o eroare deoarece pornirea unui program arbitrar este cel mai periculos lucru pe care îl poate face un clic într-un PDF, și nicio factură nu are nevoie de asta. URI-urile externe sunt avertismente: comune în documente legitime, dar un recenzent ar trebui să vadă ținta fără a da clic, deoarece textul vizibil al linkului și destinația reală nu trebuie să se potrivească. Salturile GoTo în interiorul documentului sunt structură, nu comportament, și rămân în afara raportului în întregime — o verificare preliminară care declanșează alarme false la fiecare intrare din cuprins antrenează oamenii să o ignore. Pentru citirea corpurilor scripturilor din spatele numărătorii JavaScript, și pentru nivelurile MDP de semnătură și detectarea XFA, articolul despre auditarea riscurilor de securitate parcurge aceeași suprafață prin wrapper-ul de obiecte al componentei
Metrici de resurse: DPI efectiv al imaginilor
O imagine din interiorul unui PDF nu are un DPI propriu. Are pixeli, iar pagina plasează acești pixeli într-un dreptunghi măsurat în puncte, unde 72 de puncte reprezintă un inch. Rezoluția există doar ca raportul celor două, motiv pentru care aceeași fotografie de 600 pe 400 este extrem de clară ca miniatură și o mizerie neclară ca imagine principală pe o pagină întreagă. Prin urmare, auditul necesită ambele numere pentru fiecare imagine: dimensiunile pixelilor sursă din metadatele imaginii și dreptunghiul plasat din limitele obiectului
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;
Pragurile sunt politici, nu fizică: 150 DPI este limita de jos sub care imprimarea de birou pixelează vizibil, 300 este ținta comercială obișnuită, și orice este peste 600 nu aduce nicio calitate vizibilă, umflând în același timp dimensiunea fișierului, motiv pentru care se raportează ca balast informațional, nu ca defect. Un avertisment sincer: FPDFPageObj_GetBounds returnează caseta aliniată la axe, astfel încât pentru o imagine plasată cu rotație, cifra calculată subestimează densitatea adevăratată. Structura FPDF_IMAGEOBJ_METADATA poartă, de asemenea, câmpurile horizontal_dpi și vertical_dpi pe care PDFium le derivă din matricea de transformare completă, iar compararea celor două rezultate este o modalitate ieftină de a detecta plasările rotite. Aceeași aritmetică puncte-în-pixeli conduce randarea în direcția opusă, abordată în articolul despre exportul JPEG
Starea de securitate: criptare și biți de permisiune
Criptarea PDF definește două parole cu roluri diferite. Parola utilizatorului condiționează decriptarea: fără ea fișierul nu se va deschide deloc, iar FPDF_LoadDocument returnează nil cu FPDF_GetLastError raportând FPDF_ERR_PASSWORD. Parola proprietarului condiționează permisiunile: un fișier protejat doar de o parolă a proprietarului se deschide fără credențiale, dar poartă biți de restricție pe care un cititor conform trebuie să îi onoreze. Încercarea de încărcare în sine este, prin urmare, prima sondă de securitate, iar distincția decide codul de ieșire — un fișier cu parolă de utilizator este ne-auditabil (cod 2), în timp ce un fișier cu parolă de proprietar este auditat normal și doar acumulează constatări
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;
Măștile provin din Tabelul 22 al ISO 32000-1, care numerotează biții de la 1: bitul 3 din valoarea /P este masca 4, bitul 5 este 16, bitul 12 este 2048. Dacă o anumită constatare contează este o decizie de rutare. Un birou de imprimare ar trebui să respingă un fișier SEC-NOPRINT la primire, unde cel care l-a trimis primește un mesaj clar, în loc să o facă la RIP cu trei ore înainte de termenul limită. O arhivă ar trebui să trateze SEC-ENC în sine ca pe un blocaj, deoarece criptarea și conservarea pe termen lung nu se amestecă — un aspect pe care verificarea standardelor urmează să îl sublinieze oficial
Markeri ai standardelor: citirea unei revendicări PDF/A
Un fișier declară conformitatea PDF/A în pachetul său de metadate XMP, prin proprietatea pdfaid:part (1 până la 4) și pdfaid:conformance (litera nivelului, cum ar fi b pentru fidelitate vizuală sau a pentru etichetare structurală completă). API-ul C al PDFium nu oferă un accesor XMP; FPDF_GetMetaText citește doar dicționarul Info, care nu este locul unde se află identificarea. Soluția de ieșire este o regulă chiar în standard: ISO 19005 cere ca fluxul de metadate XMP să fie stocat necomprimat, tocmai pentru ca uneltele să îl poată găsi fără un analizor PDF complet. O scanare a octeților bruți este, prin urmare, un detector legitim al revendicării — și un fișier a cărui revendicare se ascunde într-un flux comprimat a încălcat deja standardul pe care îl revendică
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;
Constatarea produsă este deliberat informațională, deoarece o revendicare este o declarație, nu o proprietate a fișierului. Intrarea XMP este o singură linie de XML pe care orice producător o poate scrie, inclusiv unul stricat; conformitatea înseamnă ca fișierul să satisfacă efectiv sute de reguli despre fonturi încorporate, culori independente de dispozitiv și funcționalități interzise. Detectarea revendicării vă spune care fișiere trebuie direcționate spre validarea reală, și nimic mai mult. Motorul de verificare preliminară încorporat al componentei efectuează acea validare pe profilurile PDF/A, PDF/UA și PDF/X, iar articolul despre CLI în lot arată cum să-l conectați într-o conductă cu rapoarte pe care un auditor le poate deschide mai târziu
O rulare pe un fișier problemă
Driver-ul leagă verificările: mai întâi securitatea, deoarece decide dacă auditul rulează sau nu deloc, apoi comportamentele la nivel de document și revendicarea standardelor, urmate de o buclă de pagină pentru acțiuni și imagini
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;
Pe o broșură venită de la o agenție externă, rezultatul arată astfel
> 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
Fiecare linie este acționabilă în sine, dar combinația reprezintă adevăratul verdict. Acest fișier revendică PDF/A-2 în timp ce poartă un dicționar de criptare și JavaScript activ, iar PDF/A le interzice pe ambele direct — deci revendicarea este demonstrabil falsă înainte să ruleze orice validator profund. Acesta este genul de contradicție pe care o listă plată de constatări o aduce la suprafață, în timp ce un rezultat boolean trece/pică o ascunde
Ce nu vă poate spune acest audit
Sinceritatea cu privire la scop este ceea ce menține încrederea într-o unealtă de verificare preliminară. Tot ce este mai sus citește ceea ce declară fișierul despre sine: PDFium analizează structura, iar acest audit o inventariază. Nu efectuează validare PDF/A — nu există verificări ale acoperirii glifelor pentru fonturile încorporate, nicio analiză a spațiului de culori pentru intențiile de ieșire, niciuna dintre regulile la nivel de clauză care separă o revendicare de conformitate; pentru asta aveți nevoie de un validator dedicat, cum ar fi motorul de verificare preliminară al componentei sau veraPDF. Biții de permisiune sunt declarații pe care cititorii conformi le onorează, nu ziduri criptografice, deci SEC-NOPRINT descrie o intenție mai degrabă decât o aplicare. Scanarea acțiunilor acoperă adnotările de tip link și scripturile la nivel de document; scripturile îngropate în dicționarele de evenimente ale câmpurilor din formulare necesită API-urile de formular în plus. Și o verificare a semnăturii, dacă extindeți auditul cu una, raportează intenția declarată, nu criptografia verificată — validarea lanțului de certificate este o sarcină separată. Un audit de verificare preliminară este interviul de la preluare, nu procesul în sine: treaba lui este să facă decizia de rutare informată, rapidă și repetabilă
Notă: API-urile obiectelor de document, pagină, adnotare și imagine utilizate pe tot parcursul acestui audit, împreună cu un wrapper Delphi de nivel înalt și un motor complet de verificare preliminară pentru validarea standardelor, sunt livrate cu Componenta PDFium