Um PDF que chega a um limite de produção — uma fila de impressão, um arquivo ou um portal de upload de cliente — deve ser auditado antes que qualquer coisa o renderize. O arquivo pode carregar uma ação de Launch (lançamento) programada para iniciar um programa externo, imagens demasiadamente precárias para tolerar a impressão, um dicionário de criptografia que proíbe o próprio trabalho de impressão para o qual foi enviado ou um rótulo PDF/A que ele não cumpre. Inspecionar um documento contra regras como essas antes que ele entre em um fluxo de trabalho é chamado de preflight (ou preflighting), e a API em C do PDFium fornece ao Delphi tudo o que é necessário para implementar as verificações diretamente, sem renderizar uma única página
Este artigo constrói as verificações em si: quatro classes de auditoria, cada uma sendo uma pequena rotina que anexa as descobertas (findings) a uma lista de resultados compartilhada. Elementos interativos, métricas de recursos, estado de segurança e marcadores de normas (standards) recebem um código de trabalho prático, incluindo a aritmética. Se o que você precisa é das engrenagens (machinery) em torno das verificações — loops de pasta em lote, arquivos de relatório JSON e HTML, isolamento por arquivo — o Componente PDFium já traz um motor de preflight pronto para uso, e o artigo sobre a CLI de preflight em lote cobre todo esse encanamento (plumbing). Os dois compartilham propositalmente um vocabulário de código de saída, logo, um auditor escrito aqui se encaixa direto debaixo desse driver em lote
O registro de achados (findings) e o contrato do código de saída
Toda verificação grava em um único tipo de registro plano (flat record), porque a alternativa, em que cada verificação imprime sua própria prosa, não pode ser contada, filtrada ou submetida a limites depois. Quatro campos são suficientes
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // chave de máquina estável, ex: 'ACT-LAUNCH'
Page: Integer; // baseado em 1; 0 significa nível de documento
Message: string; // para humanos; pode ser reescrito livremente entre lançamentos
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;
As ferramentas subsequentes utilizam o campo Code como chave, e nunca o texto Message, que pode ser alterado livremente. O código de saída (exit code) do processo segue o mesmo contrato de três valores exposto no artigo do lote (batch): 0 significa que o arquivo não produziu descobertas; 1 significa que descobertas existem, e 2 significa que a própria auditoria não pôde ser executada porque o arquivo falhou na análise (parse) ou demanda uma senha. Manter o código 2 separado é importante. Uma pasta de varreduras (scans) corrompidas denota um scanner quebrado lá atrás, não um colapso repentino de conformidade, e juntar os dois manda alguém perseguir o problema errado
Elementos interativos: scripts, alvos de lançamento, links externos
O PDFium classifica toda ação que encontra por um tipo inteiro, e vale a pena definir as constantes de fpdf_doc.h com precisão, porque valores copiados de forma incorreta tornam um scanner silenciosamente cego. A verdadeira enumeração é PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 e PDFACTION_EMBEDDEDGOTO = 5. Observe o que está ausente: não há membro do JavaScript. Os scripts no nível do documento não são ações de links e nunca são exibidos em FPDFAction_GetType; eles são enumerados por uma família de chamadas separada. Um auditor que testa os tipos de ações contrastados a uma imaginada constante em JavaScript compila, corre a sua execução e não descortina achado algum; estendendo o vazio para sempre
const
PDFACTION_GOTO = 1; // salto no documento: inofensivo
PDFACTION_REMOTEGOTO = 2; // salto para outro arquivo local
PDFACTION_URI = 3; // abre um URL externo
PDFACTION_LAUNCH = 4; // inicia um programa externo
PDFACTION_EMBEDDEDGOTO = 5; // salto para um arquivo embutido
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; // link somente de destino, nada para sinalizar
AType := FPDFAction_GetType(Action);
case AType of
PDFACTION_LAUNCH:
Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
'Ação Launch direciona-se a "' + 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 fica em silêncio por 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 ação(ões) JavaScript em nível de documento é(são) executada(s) ao abrir', [N]));
N := FPDFDoc_GetAttachmentCount(Doc);
if N > 0 then
Add(Findings, fsWarning, 'ATT-EMB', 0,
Format('%d embedded file attachment(s)', [N]));
end;
A divisão de gravidade (severity) codifica a política. Uma ação Launch é um erro porque iniciar um programa arbitrário é a coisa mais perigosa que um clique num PDF pode fazer, e nenhuma fatura precisa disso. URIs externos são avisos: são comuns em documentos legítimos, mas um revisor deve ver o alvo sem clicar, visto que o texto do link visível e o destino real não precisam coincidir. Saltos GoTo dentro do documento são parte da estrutura, não comportamento, e ficam totalmente de fora do relatório — um preflight que avisa a todo momento "lobo! lobo!" num alarme falso sobre cada entrada de sumário acabará treinando as pessoas a meramente ignorá-lo. Para ler os corpos de script por trás da contagem do JavaScript, bem como detectar níveis MDP de assinaturas e XFA, o artigo de auditoria de risco de segurança aborda a mesma superfície através do wrapper (invólucro) de objeto do componente
Métricas de recursos: DPI efetivo da imagem
Uma imagem dentro de um PDF não tem DPI próprio. Ela possui pixels, e a página coloca esses pixels em um retângulo medido em pontos, no qual 72 pontos representam uma polegada. A resolução só existe como a proporção dos dois; eis o motivo que explica a razão de uma mesma fotografia de 600 por 400 sobressaia-se pautada na extrema nitidez numa visualização da envergadura envolta num ícone na base miniatura (thumbnail) enquanto num patamar alçado à face inteira concernente a totalidade nas lides envolta perante dita página em imagem principal (hero) sucumba no crivo dum confuso borrão. A auditoria, portanto, precisa de ambos os números para cada imagem: a dimensão do pixel original do metadados da imagem, assim como, o retângulo alocado desde os limites concernentes a envergadura voltado ao objeto
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; // tamanho colocado na página, em pontos
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 pontos = 1 polegada, logo, polegadas colocadas = pontos / 72, e
// DPI efetivo = pixels de origem / polegadas colocadas.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // o pior eixo decide a qualidade de impressão
if EffDpi < 150.0 then
Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
Format('imagem %dx%d px posta em %.1fx%.1f pt = %.0f DPI efetivo',
[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;
Os limites são de política, não de física: 150 DPI é um piso abaixo do qual a impressão de escritório pixela visivelmente, 300 é o alvo comercial comum e qualquer valor acima de 600 não traz qualidade visível, apenas inflacionando o tamanho do arquivo, o que explica por que é relatado como um inchaço (bloat) informativo e não como um defeito. Uma ressalva honesta: FPDFPageObj_GetBounds retorna a caixa alinhada ao eixo, então, para uma imagem posicionada com rotação, o valor calculado subestima a verdadeira densidade. A estrutura FPDF_IMAGEOBJ_METADATA também traz os campos horizontal_dpi e vertical_dpi que o PDFium deriva de toda a matriz de transformação, e comparar os dois resultados é uma forma barata de detectar posicionamentos rotacionados. A mesma aritmética de pontos para pixels impulsiona a renderização na direção oposta, o que é abordado no artigo de exportação de JPEG
Estado de segurança: criptografia e bits de permissão
A criptografia do PDF define duas senhas com diferentes trabalhos. A senha de usuário (user password) restringe a descriptografia: sem ela, o arquivo não abrirá de jeito nenhum e FPDF_LoadDocument retornará nil com FPDF_GetLastError reportando FPDF_ERR_PASSWORD. A senha de proprietário (owner password) controla as permissões: um arquivo protegido apenas por uma senha de proprietário abre sem credenciais, mas carrega bits de restrição que um leitor em conformidade deve honrar. A própria tentativa de carregamento é, portanto, a primeira sonda de segurança, e a distinção decide o código de saída — um arquivo com senha de usuário não pode ser auditado (código 2), enquanto que um arquivo com senha de proprietário é auditado normalmente, apenas acumulando as descobertas
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 significa que o arquivo não está criptografado
begin
// Aberto com uma senha vazia e ainda assim criptografado: apenas-senha-de-proprietário.
// Qualquer um pode lê-lo, mas os bits de permissão restringem o que um
// leitor em conformidade permite a eles fazer. Arquivos não criptografados reportam todos
// os bits configurados, que é o motivo pelo qual o limitador (gate) de revisão vem em primeiro.
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: imprimir
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'a impressão não é permitida');
if (Perms and 16) = 0 then // bit 5: copiar / extrair conteúdo
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bit 12: impressão de alta resolução
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
As máscaras vêm da Tabela 22 da ISO 32000-1, a qual enumera os bits começando de 1: o bit 3 do valor de /P possui a máscara 4, bit 5 é a 16 e o bit 12 é a 2048. O fato de uma determinada descoberta importar ou não consiste em uma decisão de roteamento. Uma agência de impressão (print bureau) deve barrar e rebater (bounce) um arquivo SEC-NOPRINT no ato da admissão, quando assim quem enviou recebe uma mensagem franca e nítida, ao invés de deixá-lo barrado já dentro no RIP (Raster Image Processor), transcorridas as vias de embate de restrições umas três horas precedentes a um prazo final. Um arquivo (archive) deve tratar o próprio SEC-ENC como um bloqueador em sua integralidade, pois a criptografia e a preservação de longo prazo não se misturam — um ponto que a verificação dos padrões está prestes a deixar formalizado a seguir
Marcadores de padrões: lendo uma reivindicação PDF/A
Um arquivo declara conformidade PDF/A no seu pacote de metadados XMP, por meio da propriedade pdfaid:part (1 a 4) e de pdfaid:conformance (a letra do nível, como b para fidelidade visual ou a para rotulagem estrutural total). A API em C do PDFium não oferece nenhum acesso a XMP; o FPDF_GetMetaText lê apenas o dicionário de Info, que não é o local em que reside a identificação. A rota de escape está em uma regra no próprio padrão: a norma ISO 19005 requer que o fluxo de metadados XMP seja armazenado sem compressão, precisamente para que as ferramentas consigam encontrá-lo sem necessidade de um parser PDF total. Uma verificação de byte cru (raw byte scan) é, portanto, um legítimo detector de reivindicações (claims) — e um arquivo cuja reivindicação encontra-se oculta num fluxo comprimido já cometeu a quebra e violação de regras do próprio padrão que está pleiteando
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // vazio = nenhuma reivindicação PDF/A presente
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // Esquema de identificação XMP
if P = 0 then
Exit;
// Lida tanto com <pdfaid:part>2</pdfaid:part> quanto com pdfaid:part="2":
// pegue o primeiro dígito após o nome da propriedade.
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 descoberta gerada é intencionalmente apenas informativa, porque uma reivindicação é uma declaração, e não uma propriedade do arquivo. A entrada XMP é uma linha de XML que qualquer produtor pode escrever, até os defeituosos; a conformidade refere-se ao arquivo que efetivamente satisfaz a centenas de regras sobre as fontes embutidas (embedded fonts), cores independentes do dispositivo bem como atributos e feições proibidos. A detecção da reivindicação indica quais arquivos encaminhar para a validação real, e nada mais. O motor embutido (built-in preflight engine) do componente realiza essa validação sobre os perfis PDF/A, PDF/UA e PDF/X, e o artigo sobre a CLI em lote mostra a forma de vinculá-lo (wire it) em um pipeline acompanhado de relatórios que um auditor pode abrir posteriormente
Uma execução com um arquivo problemático
O driver une (strings together) as verificações: primeiro a segurança, porque isso decide se a auditoria em geral roda; depois, os comportamentos no nível do documento e as reivindicações sobre normas (standards); e em seguida um loop de página focado em ações bem como nas imagens
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); // falha da auditoria, não um veredicto
try
AuditDocumentBehaviors(Doc, Findings);
Claim := PdfAClaim(FileName);
if Claim <> '' then
Add(Findings, fsInfo, 'STD-PDFA', 0,
Claim + ' conformidade reivindicada (apenas declaração, não validada)');
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, 'página falhou ao analisar');
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;
Contra um folheto (brochure) que voltou de uma agência terceirizada e externa, a saída se parece e fica com esta feição
> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 achado(s)
[ERROR] ACT-LAUNCH page 3 Ação Launch direciona-se a "..\tools\setup.exe"
[ERROR] JS-DOC doc 2 ação(ões) JavaScript em nível de documento é(são) executada(s) ao abrir
[WARNING] IMG-LOWRES page 7 imagem 412x287 px posta em 396.0x275.8 pt = DPI efetivo em 75
[WARNING] SEC-NOPRINT doc a impressão não é permitida
[INFO] STD-PDFA doc a conformidade PDF/A-2 é reivindicada (declaração unicamente, sem validação)
código de saída 1
Cada linha permite atitude e ação por si só (is actionable on its own), mas a combinação é o verdadeiro veredicto. Este arquivo reivindica ser PDF/A-2, mas ao mesmo tempo traz consigo um dicionário de criptografia e um JavaScript ativo (live), e o PDF/A proíbe ambos taxativamente — desse modo, a reivindicação é comprovadamente falsa muito antes de qualquer validador profundo sequer correr sua execução. Esse é o tipo de contradição que uma lista plana de achados (flat findings) traz à superfície, e que um resultado booleano (aprovado/reprovado) esconde
O que esta auditoria não pode lhe informar
A honestidade sobre o escopo é o que mantém a confiança em uma ferramenta de preflight. Tudo o que foi exposto acima lê o que o arquivo declara sobre si próprio: o PDFium faz o parse da estrutura e esta auditoria a inventaria. Ele não executa a validação de PDF/A — não há verificações de cobertura de glifos nas fontes embutidas, nenhuma análise de espaço de cor (color space) perante as intenções de saída, nenhuma das regras no nível de cláusula que separam uma reivindicação da efetiva conformidade; para isso você necessita de um validador dedicado, a exemplo do motor de preflight do componente ou o veraPDF. Os bits de permissão formam declarações que os leitores em conformidade tratam de honrar e não formam de fato barreiras e muralhas criptográficas blindadas; logo o SEC-NOPRINT descreve mais uma intenção do que propriamente uma imposição coercitiva (enforcement). O rastreio da ação abrange as anotações do link (elo do vínculo) bem como roteiros (scripts) de nível de documento; os roteiros (scripts) enterrados nos dicionários perante lides no trâmite pertinente aos eventos de campo do formulário necessitam das APIs do formulário aplicadas por cima (on top). E uma verificação de assinatura, caso estendas a tua auditoria com uma delas, reportará unicamente e na via estritamente pautada na formatação declarada com foco nos domínios do intento intencional a pautar na intencionalidade, perante ao contraponto e em contraste a constatação frente à vertente nas esferas a referendar num verificado modelo no trato na criptografia comprovada verificada — a lide da modalidade atinente à validação no foco às trilhas de cadeia de certificado trata-se de um labor (job) coligado na pauta separada ao foco. A auditoria concernente a formatação no crivo no âmbito da diretriz do formato de preflight representa a dotação de formatação pautada no perfil condizente e análogo num escrutínio a entrevista à moda das frentes voltadas aos parâmetros de admissão em entrada primária (intake interview) perante o escrutínio e não atinente perante as engrenagens perante a ramificação pautada num julgamento frente à fase final de processos numa dotação ao tribunal a título a baliza atinente perante as vias e roteiros duma base à referir no perfil em trial: o roteiro ditado a título no labor face a seus encargos foca num crivo a tomar formatações baseadas perante a formatação e as amarrações numa pauta e envergadura de decisões atreladas com foco num roteamento para torná-las dotada ao arranjo onde perante a lide o resultado resulte no perfil da via informada com embasamentos com contornos céleres no limite à velocidade (fast) a par no decurso nas vertentes em perfis de ser propício a repetidas execuções com a possibilidade para iterar (repeatable)
Nota: As APIs de objetos de documento, página, anotação e imagem usadas em toda esta auditoria, juntamente com um invólucro (wrapper) de Delphi de alto nível e um motor (engine) completo de verificação em preflight atrelado perante a padronagem em validação ligada às vias pautadas na formatação ligada em (standards-validation), acompanham com seus embarques dispostos no Componente PDFium