PDF, joka saapuu tuotannon rajalle — tulostusjonoon, arkistoon, asiakkaan latausportaaliin — tulisi auditoida ennen kuin mikään piirtää sen. Tiedosto saattaa kantaa Launch-toimintoa, joka on kytketty käynnistämään ulkoinen ohjelma, kuvia jotka ovat liian karkeita selviämään tulostuksesta, salaussanakirjaa joka kieltää juuri sen tulostustyön johon se lähetettiin, tai PDF/A-merkinnän jota se ei lunasta. Dokumentin tarkastamista tällaisia sääntöjä vasten ennen sen pääsyä työnkulkuun kutsutaan preflight-tarkastukseksi, ja PDFiumin C-rajapinta antaa Delphille kaiken tarvittavan tarkistusten toteuttamiseen suoraan, ilman että yhtäkään sivua piirretään
Tämä artikkeli rakentaa itse tarkistukset: neljä auditointiluokkaa, joista kukin on pieni rutiini, joka liittää löydökset jaettuun tuloslistaan. Interaktiiviset elementit, resurssimitat, tietoturvatila ja standardimerkinnät saavat kaikki toimivan koodin, aritmetiikka mukaan lukien. Jos tarvitset tarkistusten ympärillä olevaa koneistoa — eräkansioiden silmukat, JSON- ja HTML-raporttitiedostot, tiedostokohtainen eristys — PDFium Component toimittaa valmiin preflight-moottorin, ja erä-preflightin CLI-artikkeli kattaa tuon putkiston. Nämä kaksi jakavat tarkoituksella yhden paluukoodisanaston, joten tässä kirjoitettu auditoija loksahtaa suoraan tuon eräajurin alle
Löydöstietue ja paluukoodisopimus
Jokainen tarkistus kirjoittaa yhteen litteään tietuetyyppiin, koska vaihtoehtoa, jossa kukin tarkistus tulostaa omaa proosaansa, ei voi laskea, suodattaa eikä kynnystää jälkikäteen. Neljä kenttää riittää
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // vakaa koneavain, esim. 'ACT-LAUNCH'
Page: Integer; // 1-pohjainen; 0 tarkoittaa dokumenttitasoa
Message: string; // ihmisille; saa muotoilla uudelleen julkaisujen välillä
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;
Myöhempi työkalusto avaintaa Code-kenttään, ei koskaan Message-tekstiin, joka saa vapaasti muuttua. Prosessin paluukoodi noudattaa samaa kolmen arvon sopimusta kuin erä-artikkeli: 0 tarkoittaa, ettei tiedosto tuottanut löydöksiä, 1 tarkoittaa, että löydöksiä on, ja 2 tarkoittaa, ettei auditointia itseään voitu ajaa, koska tiedoston jäsennys epäonnistui tai se vaatii salasanan. Koodin 2 pitäminen erillään merkitsee. Kansiollinen turmeltuneita skannauksia on rikkinäinen skanneri ylävirrassa, ei äkillinen vaatimustenmukaisuuden romahdus, ja näiden kahden niputtaminen lähettää jonkun jahtaamaan väärää ongelmaa
Interaktiiviset elementit: skriptit, käynnistyskohteet, ulkoiset linkit
PDFium luokittelee jokaisen löytämänsä toiminnon kokonaislukutyypin mukaan, ja tiedoston fpdf_doc.h vakiot kannattaa naulata täsmällisesti, koska väärin kopioidut arvot tekevät skannerista hiljaa sokean. Todellinen luettelo on PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 ja PDFACTION_EMBEDDEDGOTO = 5. Huomaa mikä puuttuu: JavaScript-jäsentä ei ole. Dokumenttitason skriptit eivät ole linkkitoimintoja eivätkä koskaan näy FPDFAction_GetType-kutsun kautta; ne luetellaan erillisellä kutsuperheellä. Auditoija, joka testaa toimintotyyppejä kuviteltua JavaScript-vakiota vasten, kääntyy, ajautuu ja löytää tyhjää, ikuisesti
const
PDFACTION_GOTO = 1; // dokumentin sisäinen hyppy: vaaraton
PDFACTION_REMOTEGOTO = 2; // hyppy toiseen paikalliseen tiedostoon
PDFACTION_URI = 3; // avaa ulkoisen URL-osoitteen
PDFACTION_LAUNCH = 4; // käynnistää ulkoisen ohjelman
PDFACTION_EMBEDDEDGOTO = 5; // hyppy upotettuun tiedostoon
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; // pelkkä kohdelinkki, ei liputettavaa
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 vaikenee tarkoituksella
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;
Vakavuusjako koodaa politiikan. Launch-toiminto on virhe, koska mielivaltaisen ohjelman käynnistäminen on vaarallisin asia, minkä klikkaus PDF:ssä voi tehdä, eikä yksikään lasku tarvitse sitä. Ulkoiset URI-osoitteet ovat varoituksia: yleisiä laillisissa dokumenteissa, mutta katselmoijan pitäisi nähdä kohde klikkaamatta, koska näkyvän linkkitekstin ja todellisen kohteen ei tarvitse olla samaa mieltä. Dokumentin sisäiset GoTo-hypyt ovat rakennetta, eivät käyttäytymistä, ja jäävät kokonaan raportin ulkopuolelle — preflight, joka huutaa sutta jokaisesta sisällysluettelon rivistä, opettaa ihmiset sivuuttamaan sen. Skriptirunkojen lukemiseen JavaScript-laskurin takaa sekä allekirjoitusten MDP-tasoihin ja XFA-tunnistukseen tietoturvariskien auditointiartikkeli kävelee saman pinnan läpi komponentin objektikääreen kautta
Resurssimitat: kuvan tehollinen DPI
PDF:n sisällä olevalla kuvalla ei ole omaa DPI-arvoa. Sillä on pikseleitä, ja sivu asettaa nuo pikselit suorakulmioon, joka mitataan pisteinä, joita menee 72 tuumaan. Resoluutio on olemassa vain näiden kahden suhteena, minkä vuoksi sama 600 kertaa 400 -valokuva on partaveitsenterävä pikkukuvana ja sumea sotku koko sivun kokoisena kuvituksena. Auditointi tarvitsee siis molemmat luvut jokaisesta kuvasta: lähteen pikselimitat kuvan metadatasta ja sijoitetun suorakulmion objektin rajoista
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; // sijoitettu koko sivulla, pisteinä
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 pistettä = 1 tuuma, joten sijoitetut tuumat = pisteet / 72, ja
// tehollinen DPI = lähdepikselit / sijoitetut tuumat.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // huonompi akseli ratkaisee tulostuslaadun
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;
Kynnykset ovat politiikkaa, eivät fysiikkaa: 150 DPI on lattia, jonka alapuolella toimistotulostus pikselöityy näkyvästi, 300 on tavanomainen kaupallinen tavoite, ja mikä tahansa yli 600 ei osta näkyvää laatua vaan paisuttaa tiedostokokoa, minkä vuoksi se raportoidaan informatiivisena turvotuksena eikä virheenä. Yksi rehellinen varauma: FPDFPageObj_GetBounds palauttaa akselin suuntaisen laatikon, joten kierrettynä sijoitetun kuvan kohdalla laskettu luku aliarvioi todellisen tiheyden. FPDF_IMAGEOBJ_METADATA-rakenne kantaa myös horizontal_dpi- ja vertical_dpi-kenttiä, jotka PDFium johtaa täydestä muunnosmatriisista, ja näiden kahden tuloksen vertaaminen on halpa tapa huomata kierretyt sijoitukset. Sama pisteistä pikseleiksi -aritmetiikka ajaa piirtoa vastakkaiseen suuntaan, mitä käsitellään JPEG-vientiartikkelissa
Tietoturvatila: salaus ja oikeusbitit
PDF-salaus määrittelee kaksi salasanaa eri tehtävillä. Käyttäjän salasana portittaa salauksen purun: ilman sitä tiedosto ei aukea lainkaan, ja FPDF_LoadDocument palauttaa nil-arvon ja FPDF_GetLastError raportoi arvon FPDF_ERR_PASSWORD. Omistajan salasana portittaa oikeudet: tiedosto, jota suojaa vain omistajan salasana, aukeaa ilman tunnuksia mutta kantaa rajoitusbittejä, joita standardinmukaisen lukijan on kunnioitettava. Latausyritys itse on siis ensimmäinen tietoturvaluotain, ja ero ratkaisee paluukoodin — käyttäjäsalasanalla suojattu tiedosto ei ole auditoitavissa (koodi 2), kun taas omistajasalasanalla suojattu tiedosto auditoituu normaalisti ja vain kerryttää löydöksiä
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 tarkoittaa ettei tiedosto ole salattu
begin
// Aukesi tyhjällä salasanalla mutta on salattu: vain omistajan salasana.
// Kuka tahansa saa lukea sen, mutta oikeusbitit rajoittavat sitä, mitä
// standardinmukainen lukija sallii. Salaamattomat tiedostot raportoivat
// kaikki bitit asetetuiksi, minkä vuoksi revisioportti tulee ensin.
Perms := FPDF_GetDocPermissions(Result);
Add(Findings, fsInfo, 'SEC-ENC', 0,
Format('encrypted, security handler revision %d', [Revision]));
if (Perms and 4) = 0 then // bitti 3: tulostus
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'printing is not permitted');
if (Perms and 16) = 0 then // bitti 5: kopiointi / sisällön poiminta
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bitti 12: korkean resoluution tulostus
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
Maskit tulevat ISO 32000-1:n taulukosta 22, joka numeroi bitit ykkösestä alkaen: /P-arvon bitti 3 on maski 4, bitti 5 on 16 ja bitti 12 on 2048. Se, merkitseekö tietty löydös, on reitityspäätös. Painotalon tulisi kimmottaa SEC-NOPRINT-tiedosto takaisin heti vastaanotossa, jolloin lähettäjä saa selkeän viestin, eikä vasta RIPissä kolme tuntia ennen määräaikaa. Arkiston tulisi kohdella SEC-ENC-löydöstä itsessään esteenä, koska salaus ja pitkäaikaissäilytys eivät sovi yhteen — seikka, jonka standarditarkistus on kohta esittämässä muodollisesti
Standardimerkinnät: PDF/A-väitteen lukeminen
Tiedosto julistaa PDF/A-yhdenmukaisuutensa XMP-metadatapaketissaan pdfaid:part-ominaisuudella (1-4) ja pdfaid:conformance-ominaisuudella (tasokirjain, kuten b visuaaliselle uskollisuudelle tai a täydelle rakenteelliselle tagitukselle). PDFiumin C-rajapinta ei tarjoa XMP-lukijaa; FPDF_GetMetaText lukee vain Info-sanakirjan, joka ei ole se paikka missä tunniste asuu. Pakoluukku on sääntö itse standardissa: ISO 19005 vaatii, että XMP-metadatavirta tallennetaan pakkaamattomana, juuri siksi että työkalut löytäisivät sen ilman täyttä PDF-jäsennintä. Raaka tavuselaus on siis pätevä väitteen tunnistin — ja tiedosto, jonka väite piileskelee pakatun virran sisällä, on jo rikkonut sen standardin jota se väittää noudattavansa
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // tyhjä = ei PDF/A-väitettä
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // XMP-tunnistusskeema
if P = 0 then
Exit;
// Käsittelee sekä muodon <pdfaid:part>2</pdfaid:part> että pdfaid:part="2":
// ota ensimmäinen numero ominaisuuden nimen jälkeen.
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;
Tästä syntyvä löydös on tarkoituksella informatiivinen, koska väite on julistus, ei tiedoston ominaisuus. XMP-merkintä on yksi rivi XML:ää, jonka mikä tahansa tuottaja voi kirjoittaa, rikkinäinen mukaan lukien; yhdenmukaisuus on sitä, että tiedosto oikeasti täyttää sadat säännöt upotetuista fonteista, laiteriippumattomasta väristä ja kielletyistä ominaisuuksista. Väitteen havaitseminen kertoo, mitkä tiedostot pitää reitittää oikeaan validointiin, eikä mitään muuta. Komponentin sisäänrakennettu preflight-moottori suorittaa tuon validoinnin PDF/A-, PDF/UA- ja PDF/X-profiileille, ja erä-CLI-artikkeli näyttää, miten sen kytkee liukuhihnaan raporteilla, jotka auditoija voi avata myöhemmin
Ajo ongelmatiedostoa vastaan
Ajuri ketjuttaa tarkistukset yhteen: ensin tietoturva, koska se ratkaisee ajetaanko auditointia lainkaan, sitten dokumenttitason käyttäytyminen ja standardiväite, sitten sivusilmukka toiminnoille ja kuville
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); // auditoinnin epäonnistuminen, ei tuomio
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;
Ulkopuoliselta toimistolta takaisin tulleen esitteen kohdalla tuloste näyttää tältä
> 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
Jokainen rivi on toimintakelpoinen yksinään, mutta yhdistelmä on todellinen tuomio. Tämä tiedosto väittää olevansa PDF/A-2 samalla kun se kantaa salaussanakirjaa ja elävää JavaScriptiä, ja PDF/A kieltää molemmat suoraan — joten väite on todistettavasti epätosi ennen kuin yksikään syvävalidaattori ajetaan. Se on juuri sellainen ristiriita, jonka litteä löydöslista tuo esiin ja jonka totuusarvoinen hyväksytty tai hylätty piilottaa
Mitä tämä auditointi ei voi kertoa sinulle
Rehellisyys laajuudesta on se, mikä pitää preflight-työkalun luotettuna. Kaikki yllä oleva lukee sitä, mitä tiedosto julistaa itsestään: PDFium jäsentää rakenteen, ja tämä auditointi inventoi sen. Se ei suorita PDF/A-validointia — ei glyfikattavuustarkistuksia upotettuja fontteja vasten, ei väriavaruusanalyysia tulostetarkoituksia vasten, ei mitään niistä lausetason säännöistä, jotka erottavat väitteen yhdenmukaisuudesta; siihen tarvitset omistetun validaattorin, kuten komponentin preflight-moottorin tai veraPDF:n. Oikeusbitit ovat julistuksia, joita standardinmukaiset lukijat kunnioittavat, eivät kryptografisia muureja, joten SEC-NOPRINT kuvaa aikomusta eikä pakotusta. Toimintoselaus kattaa linkkihuomautukset ja dokumenttitason skriptit; lomakekenttien tapahtumasanakirjoihin haudatut skriptit vaativat lomakerajapinnat päälle. Ja allekirjoitustarkistus, jos laajennat auditointia sellaisella, raportoi julistetun aikomuksen, ei todennettua kryptografiaa — varmenneketjun validointi on erillinen työ. Preflight-auditointi on vastaanottohaastattelu, ei oikeudenkäynti: sen tehtävä on tehdä reitityspäätöksestä perusteltu, nopea ja toistettava
Huomio: Tässä auditoinnissa käytetyt dokumentti-, sivu-, huomautus- ja kuvaobjektirajapinnat sekä korkean tason Delphi-kääre ja täysi standardivalidoinnin preflight-moottori toimitetaan PDFium Component -komponentin mukana