PDF dokumentas, kuris pasiekia gamybos ribą — spausdinimo eilę, archyvą, klientų įkėlimo portalą — turėtų būti patikrintas prieš jį atvaizduojant. Faile gali būti „Launch“ veiksmas, skirtas paleisti išorinę programą, vaizdai, per daug neryškūs, kad juos būtų galima atspausdinti, šifravimo žodynas, draudžiantis būtent tą spausdinimo užduotį, kuriai jis buvo pateiktas, arba PDF/A žyma, kurios jis neatitinka. Dokumento tikrinimas pagal tokias taisykles prieš jam patenkant į darbo eigą vadinamas pirminiu patikrinimu (preflighting), o PDFium C API suteikia Delphi viską, ko reikia šiems patikrinimams tiesiogiai įgyvendinti, neatvaizduojant nė vieno puslapio
Šiame straipsnyje kuriami patys patikrinimai: keturios audito klasės, iš kurių kiekviena yra nedidelė rutina, pridedanti radinius į bendrą rezultatų sąrašą. Interaktyvūs elementai, išteklių metrikos, saugumo būsena ir standartų žymekliai gauna veikiantį kodą, įskaitant aritmetiką. Jei jums reikia paties patikrinimų mechanizmo — paketinių aplankų ciklų, JSON ir HTML ataskaitų failų, atskirų failų izoliavimo — „PDFium Component“ pateikia jau paruoštą pirminio patikrinimo variklį, o straipsnis apie paketinį pirminio patikrinimo CLI apima šią infrastruktūrą. Abu sąmoningai dalijasi vienu išėjimo kodų (exit-code) žodynu, todėl čia parašytas auditorius tinka tiesiai po tuo paketiniu tvarkyklių (batch driver) varikliu
Radinių įrašas ir išėjimo kodo (exit-code) sutartis
Kiekvienas patikrinimas rašo į vieną plokščią (flat) įrašo tipą, nes alternatyvos — kai kiekvienas patikrinimas spausdina savo tekstą — vėliau negalima suskaičiuoti, filtruoti ar taikyti ribinių verčių. Pakanka keturių laukų
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;
Tolesni įrankiai remiasi Code, o ne Message tekstu, kurį galima laisvai keisti. Proceso išėjimo kodas vadovaujasi ta pačia trijų reikšmių sutartimi kaip ir paketinis (batch) straipsnis: 0 reiškia, kad faile nerasta jokių problemų, 1 reiškia, kad rasta problemų, o 2 reiškia, kad paties audito nepavyko atlikti, nes failo nepavyko apdoroti (parse) arba jam reikia slaptažodžio. Svarbu išlaikyti 2 kodą atskirą. Sugadintų nuskaitytų dokumentų aplankas reiškia sugedusį skaitytuvą (scanner), o ne staigų atitikties (compliance) žlugimą, todėl šių dviejų dalykų suplakimas į vieną priverstų ką nors ieškoti neteisingos problemos
Interaktyvūs elementai: scenarijai, paleidimo tikslai, išorinės nuorodos
PDFium klasifikuoja kiekvieną rastą veiksmą (action) pagal sveikojo skaičiaus (integer) tipą, o konstantas iš fpdf_doc.h verta tiksliai nustatyti, nes neteisingai nukopijuotos reikšmės daro skaitytuvą tyliai aklu. Tikrasis išvardijimas yra PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 ir PDFACTION_EMBEDDEDGOTO = 5. Atkreipkite dėmesį, ko trūksta: nėra jokio JavaScript nario. Dokumento lygio scenarijai nėra nuorodų veiksmai ir niekada nepasirodo per FPDFAction_GetType; jie išvardijami atskira iškvietimų šeima. Auditorius, testuojantis veiksmų tipus pagal įsivaizduojamą JavaScript konstantą, susikompiliuoja, veikia ir nieko neranda, visam laikui
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;
Sunkumo laipsnio atskyrimas koduoja politiką (policy). „Launch“ veiksmas yra klaida, nes savavališkos programos paleidimas yra pavojingiausias dalykas, kurį gali padaryti paspaudimas PDF faile, ir jokiai sąskaitai faktūrai to nereikia. Išoriniai URI yra įspėjimai: dažni teisėtuose dokumentuose, tačiau peržiūrintis asmuo turėtų matyti tikslą nepaspaudęs, nes matomas nuorodos tekstas ir tikrasis paskirties taškas neturi sutapti. Dokumente esantys „GoTo“ perėjimai yra struktūra, o ne elgsena, ir jie visiškai neįtraukiami į ataskaitą — pirminis patikrinimas (preflight), kuris kelia aliarmą dėl kiekvieno turinio įrašo, išmoko žmones jį ignoruoti. Norint perskaityti scenarijų (script) kūnus už JavaScript skaičiaus, ir parašo MDP lygiams bei XFA aptikimui, saugumo rizikos audito straipsnis nagrinėja tą patį paviršių per komponento objektų apvalkalą (wrapper)
Išteklių metrikos: efektyvusis vaizdo DPI
PDF faile esantis vaizdas neturi savo DPI. Jis turi pikselius, o puslapis tuos pikselius patalpina į stačiakampį, matuojamą taškais (points), kur 72 taškai sudaro vieną colį. Skiriamoji geba egzistuoja tik kaip šių dviejų dydžių santykis, todėl ta pati 600 x 400 nuotrauka yra itin ryški kaip miniatiūra (thumbnail) ir tampa neryškia dėme per visą puslapį (hero vaizdu). Todėl auditui reikalingi abu skaičiai kiekvienam vaizdui: šaltinio pikselių matmenys iš vaizdo metaduomenų ir patalpinto stačiakampio matmenys iš objekto ribų (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;
Ribinės vertės (thresholds) yra politika, o ne fizika: 150 DPI yra riba, žemiau kurios biuro spausdinimas akivaizdžiai tampa pikseliuotas, 300 yra įprastas komercinis tikslas, o bet kas, kas viršija 600, nesuteikia jokios matomos kokybės, tik išpučia failo dydį, todėl apie tai pranešama kaip apie informacinį išsipūtimą (bloat), o ne kaip apie defektą. Vienas sąžiningas įspėjimas: FPDFPageObj_GetBounds grąžina ašimis sulygiuotą dėžutę, todėl, jei vaizdas patalpintas su pasukimu (rotation), apskaičiuotas skaičius nepakankamai įvertina tikrąjį tankį. FPDF_IMAGEOBJ_METADATA struktūra taip pat turi horizontal_dpi ir vertical_dpi laukus, kuriuos PDFium gauna iš visos transformacijos matricos, ir šių dviejų rezultatų palyginimas yra pigus būdas pastebėti pasuktas vietas. Ta pati taškų į pikselius aritmetika valdo atvaizdavimą (rendering) priešinga kryptimi, aptartą JPEG eksporto straipsnyje
Saugumo būsena: šifravimas ir leidimų bitai
PDF šifravimas apibrėžia du slaptažodžius, turinčius skirtingas užduotis. Vartotojo slaptažodis valdo iššifravimą: be jo failas apskritai neatsidarys, o FPDF_LoadDocument grąžins nil, kartu su FPDF_GetLastError, pranešančiu apie FPDF_ERR_PASSWORD. Savininko slaptažodis valdo leidimus: failas, apsaugotas tik savininko slaptažodžiu, atidaromas be jokių prisijungimo duomenų, tačiau turi apribojimų bitus, kurių atitinkantis (conforming) skaitytuvas turi laikytis. Todėl pats bandymas įkelti failą yra pirmasis saugumo zondas, o šis skirtumas nulemia išėjimo kodą — vartotojo slaptažodžio failas negali būti audituojamas (kodas 2), o savininko slaptažodžio failas audituojamas įprastai ir tiesiog kaupia radinius
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;
Kaukės (masks) pateikiamos iš ISO 32000-1 standarto 22 lentelės, kurioje bitai numeruojami nuo 1: 3-asis /P reikšmės bitas yra kaukė 4, 5-asis bitas yra 16, 12-asis bitas yra 2048. Tai, ar tam tikras radinys yra svarbus, priklauso nuo maršrutizavimo sprendimo. Spausdinimo biuras turėtų atmesti SEC-NOPRINT failą dar priėmimo etape, kai pateikėjas gauna aiškų pranešimą, o ne RIP procese likus trims valandoms iki termino. Archyvas turėtų traktuoti patį SEC-ENC kaip blokatorių, nes šifravimas ir ilgalaikis išsaugojimas nesiderina — tai faktas, kurį netrukus oficialiai patvirtins standartų patikrinimas
Standartų žymekliai: PDF/A teiginio skaitymas
Failas deklaruoja PDF/A atitiktį savo XMP metaduomenų pakete, naudodamas pdfaid:part savybę (nuo 1 iki 4) ir pdfaid:conformance (lygio raidę, pavyzdžiui, b vizualiniam tikslumui arba a visam struktūriniam žymėjimui). PDFium C API nesiūlo jokio XMP prieigos metodo (accessor); FPDF_GetMetaText nuskaito tik Info žodyną, kuriame identifikacija nelaikoma. Išeitis (escape hatch) yra pati standarto taisyklė: ISO 19005 reikalauja, kad XMP metaduomenų srautas būtų saugomas nesuspaustas (uncompressed), būtent tam, kad įrankiai galėtų jį rasti be pilno PDF analizatoriaus (parser). Todėl neapdorotų baitų nuskaitymas (raw byte scan) yra teisėtas teiginio detektorius — o failas, kurio teiginys slepiasi suspaustame sraute, jau pažeidė standartą, kurį jis teigia atitinkąs
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;
Tai sukuriantis radinys yra sąmoningai informacinis, nes teiginys yra deklaracija, o ne failo savybė. XMP įrašas yra viena XML eilutė, kurią gali parašyti bet koks kūrėjas (producer), įskaitant ir sugedusį; atitiktis (conformance) yra tai, kai failas iš tikrųjų atitinka šimtus taisyklių dėl įterptųjų šriftų (embedded fonts), nuo įrenginio nepriklausomų spalvų (device-independent color) ir draudžiamų funkcijų. Teiginio aptikimas tik nurodo, kuriuos failus nukreipti tikram patvirtinimui (validation), ir nieko daugiau. Komponento įtaisytasis pirminio patikrinimo (preflight) variklis atlieka tą patvirtinimą PDF/A, PDF/UA ir PDF/X profiliuose, o paketinis CLI straipsnis parodo, kaip jį sujungti į konvejerį (pipeline) su ataskaitomis, kurias auditorius galės atidaryti vėliau
Probleminio failo paleidimas
Tvarkyklė (driver) sujungia patikrinimus kartu: pirmiausia saugumas, nes jis nusprendžia, ar auditas apskritai vykdomas, tada dokumento lygio elgsenos ir standartų teiginys, o tada puslapių ciklas (page loop) veiksmams ir vaizdams
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;
Patikrinus brošiūrą, grįžusią iš išorinės agentūros, išvestis atrodo taip
> 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
Kiekviena eilutė savaime leidžia imtis veiksmų, tačiau jų kombinacija yra tikrasis nuosprendis. Šis failas teigia esąs PDF/A-2, bet turi šifravimo žodyną ir veikiantį JavaScript, o PDF/A abu šiuos dalykus griežtai draudžia — todėl teiginys yra įrodomai klaidingas dar prieš paleidžiant bet kokį giluminį tvirtinimo įrankį (validator). Tai yra tas prieštaravimas, kurį atskleidžia plokščias (flat) radinių sąrašas ir kurį paslepia loginių verčių (boolean) išlaikymo/neišlaikymo (pass/fail) rezultatas
Ko šis auditas negali jums pasakyti
Sąžiningumas dėl apimties (scope) yra tai, kas išlaiko pasitikėjimą pirminio patikrinimo (preflight) įrankiu. Viskas, kas paminėta aukščiau, perskaito tai, ką failas deklaruoja pats apie save: PDFium analizuoja (parses) struktūrą, o šis auditas ją inventorizuoja. Jis neatlieka PDF/A patvirtinimo — jokių glifų aprėpties (glyph-coverage) patikrinimų įterptiems šriftams, jokios spalvų erdvės (color space) analizės išvesties ketinimams (output intents), jokių taisyklių punktų lygmeniu (clause-level), kurios atskiria teiginį nuo atitikties; tam jums reikalingas specialus tvirtinimo įrankis (validator), pvz., komponento preflight variklis arba veraPDF. Leidimų bitai yra deklaracijos, kurių laikosi (honor) atitinkantys (conforming) skaitytuvai, o ne kriptografinės sienos, todėl SEC-NOPRINT apibūdina ketinimą, o ne vykdymo užtikrinimą. Veiksmų nuskaitymas (action scan) apima nuorodų anotacijas (link annotations) ir dokumento lygio scenarijus; scenarijams, paslėptiems formos laukų (form-field) įvykių žodynuose, papildomai reikia formų API. O parašo patikrinimas (signature check), jei praplėsite auditą, praneš apie deklaruotą ketinimą, bet ne patikrintą kriptografiją — sertifikatų grandinės (certificate chain) patvirtinimas yra atskiras darbas. Pirminis (preflight) auditas yra priėmimo interviu, o ne teismo procesas: jo darbas — padaryti maršrutizavimo (routing) sprendimą pagrįstą, greitą ir pakartojamą
Pastaba: Dokumentų, puslapių, anotacijų ir vaizdų objektų API, naudojamos viso šio audito metu, kartu su aukšto lygio Delphi apvalkalu (wrapper) ir pilnu standartų patvirtinimo (standards-validation) pirminio patikrinimo (preflight) varikliu, pateikiamos kartu su PDFium komponentu