Egy PDF-et, amely egy termelési határhoz érkezik — nyomtatási sorba, archívumba, ügyfélfeltöltési portálra —, ellenőrizni kell, mielőtt bármi is megjelenítené. A fájl hordozhat egy olyan "Launch" (Indítás) műveletet, amely egy külső program indítására van bekötve, túl durva képeket a nyomtatás túléléséhez, egy titkosítási szótárat, amely megtiltja magát a nyomtatási feladatot, amelyre beküldték, vagy egy olyan PDF/A címkét, amelynek nem felel meg. Egy dokumentum ellenőrzését az ilyen szabályok alapján, mielőtt egy munkafolyamatba kerülne, preflightingnak (nyomdai előkészítő ellenőrzésnek) nevezzük, és a PDFium C API megadja a Delphinek mindazt, amire szükség van ezen ellenőrzések közvetlen implementálásához, egyetlen oldal megjelenítése nélkül
Ez a cikk magukat az ellenőrzéseket építi fel: négy ellenőrző osztályt, amelyek mindegyike egy kis rutin, ami megállapításokat fűz egy megosztott eredménylistához. Az interaktív elemek, az erőforrás-metrikák, a biztonsági állapot és a szabványjelölők mind működő kódot kapnak, beleértve a matematikát is. Ha arra a gépezetre van szüksége, amely az ellenőrzések körül van — kötegelt (batch) mappa ciklusok, JSON és HTML jelentésfájlok, fájlonkénti izoláció —, a PDFium komponens szállít egy kész preflight motort, és a kötegelt preflight CLI cikk foglalkozik ezzel az infrastruktúrával. A kettő szándékosan oszt meg egy kilépési kód (exit-code) szókincset, így egy itt írt ellenőrző egyenesen illeszkedik a kötegelt vezérlő alá
A megállapítás rekordja és a kilépési kód szerződés
Minden ellenőrzés egy lapos rekordtípusba ír, mert az alternatíva, miszerint minden ellenőrzés a saját prózáját nyomtatja, később nem számolható, szűrhető vagy küszöbözhető. Négy mező elegendő
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;
A feldolgozási láncban lejjebb lévő eszközök a Code-ra (kódra) támaszkodnak, sosem a Message (üzenet) szövegére, ami szabadon változhat. A folyamat kilépési kódja ugyanazt a háromértékű szerződést követi, mint a kötegelt cikkben: a 0 azt jelenti, hogy a fájl nem hozott megállapításokat, az 1 azt jelenti, hogy vannak megállapítások, a 2 pedig azt, hogy maga az ellenőrzés nem tudott lefutni, mert a fájl elemzése (parse) sikertelen volt, vagy jelszót követel. A 2-es kód külön tartása fontos. Egy mappa korrupt beolvasás (scan) egy hibás szkennert jelent a lánc elején, nem pedig hirtelen megfelelőségi összeomlást, és e kettő összemosása valakit egy rossz probléma üldözésére küld
Interaktív elemek: szkriptek, indítási célok, külső linkek
A PDFium minden általa talált műveletet egy egész típus alapján osztályoz, és az fpdf_doc.h-ból származó konstansokat érdemes pontosan rögzíteni, mert a rosszul másolt értékek némán vakká teszik a scannert. A valódi felsorolás: PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 és PDFACTION_EMBEDDEDGOTO = 5. Figyelje meg, mi hiányzik: nincs JavaScript tag. A dokumentum szintű szkriptek nem link műveletek, és sosem jelennek meg az FPDFAction_GetType híváson keresztül; ezeket a hívások egy külön családja sorolja fel. Egy olyan ellenőrző, amely a művelettípusokat egy elképzelt JavaScript konstans ellen teszteli, lefordul, lefut, és sosem talál semmit
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;
A súlyossági felosztás a házirendet kódolja. Egy Launch (Indítás) művelet hiba, mert egy tetszőleges program elindítása a legveszélyesebb dolog, amit egy PDF-ben történő kattintás tehet, és semmilyen számlának nincs szüksége rá. A külső URI-k figyelmeztetések: gyakoriak a legitim dokumentumokban, de egy felülvizsgálónak kattintás nélkül kellene látnia a célt, mivel a látható linkszöveg és a tényleges célpont nem feltétlenül egyezik meg. A dokumentumon belüli GoTo ugrások a struktúrát alkotják, nem a viselkedést, és teljesen kimaradnak a jelentésből — egy preflight, ami minden tartalomjegyzék bejegyzésnél farkast kiált, arra tanítja az embereket, hogy figyelmen kívül hagyják. A JavaScript darabszám mögötti szkripttestek olvasásához, valamint az aláírás MDP szintjeihez és az XFA észleléshez a biztonsági kockázatok ellenőrzéséről szóló cikk ugyanezt a felületet járja végig a komponens objektumcsomagolóján (object wrapper) keresztül
Erőforrás-metrikák: tényleges kép DPI
A PDF-ben lévő képnek nincs saját DPI-je. Képpontjai (pixelek) vannak, és az oldal ezeket a képpontokat egy pontokban mért téglalapba helyezi el, ahol 72 pont tesz ki egy hüvelyket. A felbontás csak e kettő arányaként létezik, ezért van az, hogy ugyanaz a 600 x 400-as fotó borotvaéles bélyegképként, és homályos massza egy teljes oldalas "hero" képként. Az ellenőrzésnek ezért mindkét számra szüksége van minden képnél: a forrás pixelméretekre a kép metaadataiból, és az elhelyezett téglalapra az objektum határaiból
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;
A küszöbértékek házirendi kérdések, nem fizikaiak: a 150 DPI az a határ, amely alatt az irodai nyomtatás láthatóan pixelessé válik, a 300 a szokásos kereskedelmi cél, a 600 feletti értékek pedig nem hoznak látható minőséget, miközben felduzzasztják a fájlméretet, ezért is jelenik meg tájékoztató jellegű pazarolásként (bloat), nem pedig hibaként. Egy őszinte figyelmeztetés: az FPDFPageObj_GetBounds a tengelyhez igazított (axis-aligned) dobozt adja vissza, így egy elforgatva elhelyezett kép esetén a számított érték alulbecsüli a valódi sűrűséget. Az FPDF_IMAGEOBJ_METADATA struktúra hordoz horizontal_dpi és vertical_dpi mezőket is, amelyeket a PDFium a teljes transzformációs mátrixból vezet le, és a két eredmény összehasonlítása egy olcsó módja az elforgatott elhelyezések észlelésének. Ugyanez a pontokból képpontokba történő matematika hajtja a megjelenítést az ellenkező irányba is, amivel a JPEG exportálás cikk foglalkozik
Biztonsági állapot: titkosítási és engedélybitek
A PDF titkosítás két jelszót definiál különböző feladatokkal. A felhasználói (user) jelszó védi a dekódolást: enélkül a fájl egyáltalán nem nyílik meg, és az FPDF_LoadDocument nil-t ad vissza, miközben az FPDF_GetLastError FPDF_ERR_PASSWORD hibát jelez. A tulajdonosi (owner) jelszó védi az engedélyeket: egy csak tulajdonosi jelszóval védett fájl hitelesítő adatok nélkül is megnyílik, de olyan korlátozási biteket hordoz, amelyeket egy szabványkövető olvasónak tiszteletben kell tartania. Maga a betöltési kísérlet tehát az első biztonsági szonda, és a különbségtétel dönti el a kilépési kódot — egy felhasználói jelszóval védett fájl nem ellenőrizhető (2-es kód), míg egy tulajdonosi jelszóval védett fájl normálisan ellenőrizhető, és csupán megállapításokat halmoz fel
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;
A maszkok az ISO 32000-1 22. táblázatából származnak, amely 1-től számozza a biteket: a /P érték 3. bitje a 4-es maszk, az 5. bit a 16-os, a 12. bit pedig a 2048-as. Hogy egy adott megállapítás számít-e, az egy útválasztási (routing) döntés. Egy nyomdaipari irodának már az átvételnél vissza kell utasítania egy SEC-NOPRINT fájlt, ahol a beküldő egyértelmű üzenetet kap, nem pedig a RIP fázisban, három órával a határidő előtt. Egy archívumnak magát a SEC-ENC-et kellene blokkoló tényezőként kezelnie, mivel a titkosítás és a hosszú távú megőrzés nem fér meg egymással — ez az a pont, amit a szabványellenőrzés hamarosan formálisan is megtesz
Szabványjelölők: a PDF/A állítás olvasása
Egy fájl a PDF/A megfelelőséget az XMP metaadat-csomagjában deklarálja, a pdfaid:part tulajdonságon (1-től 4-ig) és a pdfaid:conformance tulajdonságon (a szint betűje, mint például a b a vizuális hűségért vagy az a a teljes strukturális címkézésért) keresztül. A PDFium C API-ja nem kínál XMP hozzáférést; az FPDF_GetMetaText csak az Info szótárat olvassa, ami nem az a hely, ahol az azonosítás található. A kiskapu egy szabály magában a szabványban: az ISO 19005 megköveteli, hogy az XMP metaadat-folyamot tömörítetlenül tárolják, pontosan azért, hogy az eszközök egy teljes PDF elemző (parser) nélkül is megtalálják. Egy nyers bájtellenőrzés (byte scan) tehát egy legitim állítás-érzékelő — és az a fájl, amelynek állítása egy tömörített folyam (stream) belsejében rejtőzik, már megsértette azt a szabványt, amit állít
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;
A megállapítás, amit ez eredményez, szándékosan tájékoztató jellegű, mert az állítás csak egy deklaráció, nem pedig a fájl tulajdonsága. Az XMP bejegyzés egyetlen sornyi XML, amelyet bármelyik előállító megírhat, beleértve egy hibásat is; a megfelelőség az, hogy a fájl ténylegesen kielégíti-e a beágyazott betűtípusokra, az eszközfüggetlen színre és a tiltott funkciókra vonatkozó több száz szabályt. Az állítás észlelése megmondja, mely fájlokat kell a valódi validáláshoz irányítani, és semmi többet. A komponens beépített preflight motorja elvégzi ezt a validálást a PDF/A, PDF/UA és PDF/X profilokon, és a kötegelt CLI cikk bemutatja, hogyan lehet ezt bekötni egy olyan csővezetékbe (pipeline), amelynek jelentéseit egy ellenőr később megnyithatja
Futtatás egy problémás fájllal
A vezérlő (driver) összefűzi az ellenőrzéseket: először a biztonság, mert ez dönti el, hogy az ellenőrzés egyáltalán lefut-e, majd a dokumentum szintű viselkedések és a szabványállítások, ezután pedig egy oldal ciklus a műveletekhez és a képekhez
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;
Egy külső ügynökségtől visszatért prospektus esetén a kimenet így néz ki
> 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
Minden sor önmagában is cselekvésre ösztönöz, de a kombinációjuk a valódi ítélet. Ez a fájl PDF/A-2-t állít magáról, miközben titkosítási szótárat és élő JavaScriptet hordoz, és a PDF/A mindkettőt egyenesen tiltja — tehát az állítás bizonyíthatóan hamis, még mielőtt bármilyen mély validáló lefutna. Ez az a fajta ellentmondás, amit egy lapos megállapításlista felszínre hoz, egy logikai (boolean) siker/kudarc (pass/fail) pedig elrejt
Amit ez az ellenőrzés nem tud elmondani
Az őszinteség a hatókörről az, ami megőrzi a bizalmat egy preflight eszköz iránt. Minden fentebbi azt olvassa el, amit a fájl önmagáról deklarál: a PDFium elemzi a struktúrát, ez az ellenőrzés pedig leltározza. Nem végez PDF/A validálást — nincsenek glifa-lefedettségi ellenőrzések a beágyazott betűtípusokon, nincs színtérelemzés a kimeneti szándékokkal (output intents) szemben, a záradékszintű szabályok egyike sem, amelyek elválasztják az állítást a megfelelőségtől; ehhez egy dedikált validálóra van szükség, mint például a komponens preflight motorja vagy a veraPDF. Az engedélybitek (permission bits) olyan deklarációk, amelyeket a szabványkövető olvasók tiszteletben tartanak, nem pedig kriptográfiai falak, tehát a SEC-NOPRINT inkább a szándékot írja le, mint a kikényszerítést. A műveletek vizsgálata kiterjed a link annotációkra és a dokumentum szintű szkriptekre; az űrlapmezők eseményszótáraiba temetett szkriptekhez szükség van az űrlap (form) API-kra. És egy aláírás-ellenőrzés, ha azzal egészíti ki a vizsgálatot, a deklarált szándékot jelenti, nem az ellenőrzött kriptográfiát — a tanúsítványlánc validálása külön feladat. Egy preflight ellenőrzés a felvételi elbeszélgetés, nem a tárgyalás: a feladata, hogy az útválasztási döntést megalapozottá, gyorssá és megismételhetővé tegye
Megjegyzés: Az ebben az ellenőrzésben végig használt dokumentum, oldal, annotáció és kép objektum API-k, egy magas szintű Delphi burkolóval (wrapper) és egy teljes szabvány-validáló preflight motorral együtt, a PDFium komponenssel érkeznek