Dokument PDF, který dorazí na hranici produkce — do tiskové fronty, archivu nebo portálu pro nahrávání zákazníky — by měl být auditován předtím, než jej cokoli vykreslí. Soubor může obsahovat akci Launch nastavenou tak, aby spustila externí program, obrázky příliš hrubé na to, aby přežily tisk, šifrovací slovník, který zakazuje přesně ten tiskový úkol, pro který byl předložen, nebo štítek PDF/A, který nesplňuje. Kontrola dokumentu proti takovýmto pravidlům předtím, než vstoupí do pracovního postupu, se nazývá preflight, a C API PDFia poskytuje Delphi vše potřebné k implementaci těchto kontrol přímo, aniž by se musela vykreslit jediná stránka
Tento článek vytváří samotné kontroly: čtyři třídy auditu, každá je malou rutinou, která přidává zjištění do sdíleného seznamu výsledků. Interaktivní prvky, metriky zdrojů, stav zabezpečení a markery standardů — to vše dostává funkční kód, včetně výpočtů. Pokud potřebujete mašinérii kolem kontrol — dávkové zpracování složek, soubory s protokoly JSON a HTML, izolaci jednotlivých souborů — PDFium Component obsahuje hotový preflight engine, a článek o CLI pro dávkový preflight pokrývá toto potrubí. Obě řešení záměrně sdílejí jeden slovník návratových kódů (exit codes), takže auditor zde napsaný lze přímo vložit pod tento dávkový ovladač
Záznam o zjištění a smlouva o návratovém kódu
Každá kontrola zapisuje do jednoho plochého typu záznamu, protože alternativa, kde každá kontrola vypisuje svůj vlastní text, nelze později počítat, filtrovat nebo omezovat prahy. Čtyři pole jsou dostatečná
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // stabilní strojový klíč, např. 'ACT-LAUNCH'
Page: Integer; // indexováno od 1; 0 znamená úroveň dokumentu
Message: string; // pro lidi; text lze mezi vydáními volně měnit
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;
Nástroje na nižší úrovni klíčují podle Code, nikdy podle textu Message, který lze volně měnit. Návratový kód (exit code) procesu se řídí stejnou tříhodnotovou smlouvou jako dávkový článek: 0 znamená, že soubor neprodukuje žádná zjištění, 1 znamená, že zjištění existují, a 2 znamená, že samotný audit nemohl být spuštěn, protože soubor se nepodařilo analyzovat nebo vyžaduje heslo. Je důležité oddělit kód 2. Složka plná poškozených skenů je problém se skenerem výše v řetězci, nikoli náhlý kolaps dodržování předpisů, a spojení těchto dvou dohromady posílá někoho hledat špatný problém
Interaktivní prvky: skripty, cíle spuštění (launch), externí odkazy
PDFium klasifikuje každou nalezenou akci celočíselným typem a konstanty z fpdf_doc.h stojí za to si přesně určit, protože špatně opsané hodnoty udělají skener tiše slepým. Skutečný výčet je PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 a PDFACTION_EMBEDDEDGOTO = 5. Všimněte si, co chybí: není zde žádný člen JavaScriptu. Skripty na úrovni dokumentu nejsou akce odkazů a nikdy se neobjeví přes FPDFAction_GetType; jsou vyjmenovány samostatnou rodinou volání. Auditor, který testuje typy akcí oproti smyšlené konstantě JavaScriptu, se zkompiluje, poběží a nenajde nic, navždy
const
PDFACTION_GOTO = 1; // skok v rámci dokumentu: neškodné
PDFACTION_REMOTEGOTO = 2; // skok do jiného místního souboru
PDFACTION_URI = 3; // otevře externí URL
PDFACTION_LAUNCH = 4; // spustí externí program
PDFACTION_EMBEDDEDGOTO = 5; // skok do vloženého souboru
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; // odkaz pouze s cílem, nic k nahlášení
AType := FPDFAction_GetType(Action);
case AType of
PDFACTION_LAUNCH:
Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
'Akce Launch cílí na "' + ActionTarget(Doc, Action, AType) + '"');
PDFACTION_URI:
Add(Findings, fsWarning, 'ACT-URI', PageNo,
'odkaz otevře ' + ActionTarget(Doc, Action, AType));
PDFACTION_REMOTEGOTO, PDFACTION_EMBEDDEDGOTO:
Add(Findings, fsWarning, 'ACT-XFILE', PageNo,
'cíl mimo soubor "' + ActionTarget(Doc, Action, AType) + '"');
end; // PDFACTION_GOTO zůstává záměrně tiché
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 akce JavaScriptu na úrovni dokumentu spouštěné při otevření', [N]));
N := FPDFDoc_GetAttachmentCount(Doc);
if N > 0 then
Add(Findings, fsWarning, 'ATT-EMB', 0,
Format('%d vložené přílohy', [N]));
end;
Rozdělení závažnosti kóduje politiku (policy). Akce Launch je chybou, protože spuštění libovolného programu je tou nejnebezpečnější věcí, kterou může kliknutí v PDF udělat, a žádná faktura to nepotřebuje. Externí URI jsou varování: v legitimních dokumentech běžné, ale recenzent by měl vidět cíl předtím, než na něj klikne, protože viditelný text odkazu a skutečný cíl se nemusí shodovat. Skoky GoTo v rámci dokumentu tvoří strukturu, nikoli chování, a z protokolu zcela vypadávají — preflight, který křičí poplach na každou položku obsahu, učí lidi ho ignorovat. Pro čtení těl skriptů za počtem JavaScriptu a pro zjišťování úrovní podpisů MDP a detekci XFA vás provede stejným povrchem skrz objektový obal komponenty článek o auditu bezpečnostních rizik
Metriky zdrojů: efektivní DPI obrázků
Obrázek uvnitř PDF nemá žádné vlastní DPI. Má pixely a stránka tyto pixely umístí do obdélníku měřeného v bodech (points), kde 72 bodů tvoří jeden palec (inch). Rozlišení existuje pouze jako poměr těchto dvou hodnot, což je důvod, proč je stejná fotografie o velikosti 600 na 400 pixelů jako miniatura ostrá jako břitva a jako celostránkový obrázek rozmazaná machule. Audit proto potřebuje obě čísla pro každý obrázek: rozměry ve zdrojových pixelech z metadat obrázku a umístěný obdélník z hranic objektu
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; // umístěná velikost na stránce v bodech
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 bodů = 1 palec, takže umístěné palce = body / 72, a
// efektivní DPI = zdrojové pixely / umístěné palce.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // o kvalitě tisku rozhoduje horší osa
if EffDpi < 150.0 then
Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
Format('obrázek %dx%d px umístěný na %.1fx%.1f pt = efektivní DPI %.0f',
[Meta.Width, Meta.Height, WidthPt, HeightPt, EffDpi]))
else if EffDpi > 600.0 then
Add(Findings, fsInfo, 'IMG-BLOAT', PageNo,
Format('obrázek má při umístěné velikosti DPI %.0f; převzorkování ' +
'by zmenšilo soubor bez viditelné ztráty kvality', [EffDpi]));
end;
end;
Prahy jsou záležitostí politiky, ne fyziky: 150 DPI je dolní hranice, pod kterou kancelářský tisk viditelně pixeluje, 300 je obvyklý cíl pro komerční tisk a cokoli nad 600 nepřináší žádnou viditelnou kvalitu, zatímco nafukuje velikost souboru, a proto se to hlásí jako informativní upozornění na bloat spíše než jako vada. Jedno upřímné varování: FPDFPageObj_GetBounds vrací box zarovnaný k osám, takže pro obrázek umístěný s rotací vypočtená hodnota podhodnocuje skutečnou hustotu. Struktura FPDF_IMAGEOBJ_METADATA také nese pole horizontal_dpi a vertical_dpi, která PDFium odvozuje z celé transformační matice, a porovnání těchto dvou výsledků je levný způsob, jak odhalit otočená umístění. Stejná matematika s body a pixely řídí vykreslování v opačném směru, což pokrývá článek o exportu do JPEG
Stav zabezpečení: šifrování a bity oprávnění
Šifrování PDF definuje dvě hesla s různými funkcemi. Uživatelské heslo omezuje dešifrování: bez něj se soubor vůbec neotevře a FPDF_LoadDocument vrátí nil a FPDF_GetLastError ohlásí FPDF_ERR_PASSWORD. Heslo vlastníka omezuje oprávnění: soubor chráněný pouze heslem vlastníka se otevře bez jakýchkoli přihlašovacích údajů, ale nese omezující bity, které musí vyhovující prohlížeč respektovat. Pokus o načtení je tedy první bezpečnostní sondou a tento rozdíl rozhoduje o návratovém kódu (exit code) — soubor s uživatelským heslem nelze auditovat (kód 2), zatímco soubor s heslem vlastníka se audituje normálně a pouze shromažďuje zjištění
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,
'vyžadováno uživatelské heslo (heslo pro otevření); audit nemůže pokračovat')
else
Add(Findings, fsError, 'DOC-BROKEN', 0, 'soubor se nepodařilo analyzovat');
Exit;
end;
Revision := FPDF_GetSecurityHandlerRevision(Result);
if Revision >= 0 then // -1 znamená, že soubor není zašifrován
begin
// Otevřeno prázdným heslem, a přesto zašifrováno: pouze heslo vlastníka.
// Kdokoli to může číst, ale bity oprávnění omezují to,
// co jim vyhovující čtečka dovolí dělat. Nezašifrované soubory
// mají nastavené všechny bity, proto je revizní kontrola na prvním místě.
Perms := FPDF_GetDocPermissions(Result);
Add(Findings, fsInfo, 'SEC-ENC', 0,
Format('zašifrováno, revize manipulátoru zabezpečení %d', [Revision]));
if (Perms and 4) = 0 then // bit 3: tisk
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'tisk není povolen');
if (Perms and 16) = 0 then // bit 5: kopírování / extrakce obsahu
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'extrakce obsahu není povolena');
if (Perms and 2048) = 0 then // bit 12: tisk ve vysokém rozlišení
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'je povolen pouze tisk v nízkém rozlišení');
end;
end;
Masky pocházejí z tabulky 22 normy ISO 32000-1, která čísluje bity od 1: bit 3 z hodnoty /P je maska 4, bit 5 je 16, bit 12 je 2048. To, zda na daném zjištění záleží, je rozhodnutím při směrování. Tiskárna by měla odmítnout soubor SEC-NOPRINT na příjmu, kde odesílatel dostane jasnou zprávu, spíše než na RIPu tři hodiny před uzávěrkou. Archiv by měl považovat samotný SEC-ENC za blokační, protože šifrování a dlouhodobé uchovávání nejdou dohromady — to je bod, který formálně upozorní kontrola standardů
Markery standardů: čtení deklarace PDF/A
Soubor deklaruje shodu s PDF/A ve svém paketu metadat XMP prostřednictvím vlastnosti pdfaid:part (1 až 4) a pdfaid:conformance (písmeno úrovně, např. b pro vizuální věrnost nebo a pro úplné strukturální tagování). C API PDFia nenabízí žádný přístup k XMP; FPDF_GetMetaText čte pouze slovník Info, kde identifikace není uložena. Únikovou cestou je pravidlo v samotném standardu: ISO 19005 vyžaduje, aby proud metadat XMP byl uložen nekomprimovaný, a to právě proto, aby jej nástroje mohly najít bez plnohodnotného syntaktického analyzátoru PDF. Skenování surových bajtů je proto legitimním detektorem deklarace — a soubor, jehož deklarace se skrývá uvnitř komprimovaného proudu, již porušil standard, který deklaruje
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // prázdné = neexistuje deklarace PDF/A
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // identifikační schéma XMP
if P = 0 then
Exit;
// Zvládne jak <pdfaid:part>2</pdfaid:part>, tak pdfaid:part="2":
// vezme první číslici za názvem vlastnosti.
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;
Zjištění, které z toho vzejde, je záměrně informativní, protože deklarace je prohlášení, nikoli vlastnost souboru. Záznam v XMP je jeden řádek XML, který může napsat každý producent, včetně toho porouchaného; skutečná shoda je splnění stovek pravidel o vložených písmech, barvách nezávislých na zařízení a zakázaných funkcích. Zjištění deklarace vám řekne pouze to, které soubory směrovat na skutečné ověření (validation). Integrovaný preflight engine v komponentě provádí ověřování napříč profily PDF/A, PDF/UA a PDF/X a článek o dávkovém CLI ukazuje, jak jej začlenit do pipeline (roury) se zprávami, které může auditor později otevřít
Běh programu s problematickým souborem
Ovladač řadí kontroly za sebe: nejprve bezpečnostní, protože ta rozhoduje, zda se audit vůbec spustí, pak chování na úrovni dokumentu a deklarace standardů, a nakonec cyklus stránek pro akce a obrázky
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); // selhání auditu, nikoli verdikt
try
AuditDocumentBehaviors(Doc, Findings);
Claim := PdfAClaim(FileName);
if Claim <> '' then
Add(Findings, fsInfo, 'STD-PDFA', 0,
Claim + ' byla deklarována shoda (pouze deklarace, neověřeno)');
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, 'stránku se nepodařilo analyzovat');
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;
Na brožuře, která se vrátila z externí agentury, výstup vypadá následovně:
> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 finding(s)
[ERROR] ACT-LAUNCH page 3 Akce Launch cílí na "..\tools\setup.exe"
[ERROR] JS-DOC doc 2 akce JavaScriptu na úrovni dokumentu spouštěné při otevření
[WARNING] IMG-LOWRES page 7 obrázek 412x287 px umístěný na 396.0x275.8 pt = efektivní DPI 75
[WARNING] SEC-NOPRINT doc tisk není povolen
[INFO] STD-PDFA doc PDF/A-2 byla deklarována shoda (pouze deklarace, neověřeno)
exit code 1
Každý řádek je samostatně užitečný pro další akci, ale jejich kombinace tvoří skutečný verdikt. Tento soubor deklaruje shodu s PDF/A-2, přičemž nese šifrovací slovník a aktivní JavaScript, obojí PDF/A přímo zakazuje — deklarace je tedy prokazatelně nepravdivá ještě před spuštěním hlubokého validátoru. Právě tyto rozpory, které by jinak skrylo prosté binární rozhodnutí prošel/neprošel, odhaluje plochý seznam zjištění
Co vám tento audit neřekne
Upřímnost ohledně rozsahu je tím, co udržuje preflight nástroj důvěryhodným. Vše výše uvedené čte to, co soubor prohlašuje o sobě: PDFium analyzuje strukturu a tento audit ji inventarizuje. Neprovádí validaci PDF/A — žádné kontroly pokrytí glyfů vůči vloženým písmům, žádná analýza barevných prostorů (color spaces) oproti výstupním záměrům (output intents), žádná pravidla na úrovni ustanovení norem, která oddělují deklaraci od skutečné shody; k tomu potřebujete specializovaný validátor, jako je preflight engine komponenty nebo veraPDF. Bity oprávnění jsou deklarace, které dodržují vyhovující prohlížeče, nikoli kryptografické stěny, takže SEC-NOPRINT popisuje záměr spíše než jeho vynucení (enforcement). Sken akcí pokrývá anotace odkazů a skripty na úrovni dokumentu; skripty ukryté ve slovnících událostí polí formulářů vyžadují API formulářů. A kontrola podpisů, pokud byste jí audit rozšířili, ohlásí deklarovaný záměr, ne ověřenou kryptografii — validace řetězce certifikátů je samostatný úkol. Preflight audit je přijímací pohovor, nikoli samotný soud: jeho úkolem je učinit rozhodnutí o směrování informovaným, rychlým a opakovatelným
Poznámka: Rozhraní API pro dokumenty, stránky, anotace a objekty obrázků používané v tomto auditu se dodávají společně s vysokoúrovňovým Delphi obalem (wrapperem) a úplným modulem pro ověřování shody se standardy (preflight engine) v rámci produktu PDFium Component