Ein PDF, das an einer Produktionsgrenze ankommt — einer Druckerwarteschlange, einem Archiv, einem Kunden-Upload-Portal — sollte geprüft werden, bevor irgendetwas es rendert. Die Datei könnte eine Launch-Aktion enthalten, die so programmiert ist, dass sie ein externes Programm startet, Bilder, die zu grob sind, um den Druck zu überstehen, ein Verschlüsselungs-Wörterbuch, das genau den Druckauftrag verbietet, für den sie eingereicht wurde, oder ein PDF/A-Label, das sie nicht erfüllt. Die Prüfung eines Dokuments gegen solche Regeln, bevor es in einen Workflow gelangt, wird als Preflighting bezeichnet, und die PDFium C API bietet Delphi alles, was nötig ist, um die Prüfungen direkt zu implementieren, ohne auch nur eine einzige Seite zu rendern
Dieser Artikel erstellt die Prüfungen selbst: vier Audit-Klassen, jede eine kleine Routine, die Ergebnisse an eine gemeinsame Ergebnisliste anfügt. Interaktive Elemente, Ressourcen-Metriken, Sicherheitsstatus und Standard-Markierungen erhalten alle funktionierenden Code, einschließlich der Arithmetik. Wenn Sie die Maschinerie um die Prüfungen herum benötigen — Batch-Ordnerschleifen, JSON- und HTML-Berichtsdateien, Isolation pro Datei —, so liefert die PDFium-Komponente eine vorgefertigte Preflight-Engine mit, und der Artikel über die Batch-Preflight-CLI behandelt diese Infrastruktur. Beide teilen sich absichtlich ein Vokabular für Beendigungscodes, sodass sich ein hier geschriebener Auditor direkt in diesen Batch-Treiber einfügt
Der Ergebnisdatensatz und der Beendigungscode-Vertrag
Jede Prüfung schreibt in einen flachen Datensatztyp, denn die Alternative, dass jede Prüfung ihre eigene Prosa ausgibt, kann im Nachhinein nicht gezählt, gefiltert oder mit Schwellenwerten versehen werden. Vier Felder reichen aus:
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;
Nachgelagerte Werkzeuge orientieren sich an Code, niemals am Text der Message, die sich jederzeit ändern kann. Der Exit-Code des Prozesses folgt dem gleichen dreiwertigen Vertrag wie im Batch-Artikel: 0 bedeutet, dass die Datei keine Auffälligkeiten aufweist, 1 bedeutet, dass Ergebnisse vorliegen, und 2 bedeutet, dass die Prüfung selbst nicht durchgeführt werden konnte, da die Datei nicht geparst werden konnte oder ein Passwort verlangt. Es ist wichtig, Code 2 separat zu halten. Ein Ordner mit beschädigten Scans ist ein defekter Scanner im Vorfeld, kein plötzlicher Compliance-Kollaps, und wenn man beides zusammenfasst, schickt man jemanden auf die Jagd nach dem falschen Problem
Interaktive Elemente: Skripte, Launch-Ziele, externe Links
PDFium klassifiziert jede gefundene Aktion nach einem Integer-Typ, und die Konstanten aus fpdf_doc.h sollten genau festgelegt werden, da falsch kopierte Werte einen Scanner im Stillen blind machen. Die eigentliche Aufzählung lautet PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 und PDFACTION_EMBEDDEDGOTO = 5. Beachten Sie, was fehlt: Es gibt kein JavaScript-Element. Skripte auf Dokumentenebene sind keine Link-Aktionen und tauchen niemals über FPDFAction_GetType auf; sie werden durch eine separate Familie von Aufrufen aufgezählt. Ein Auditor, der Aktionstypen gegen eine imaginäre JavaScript-Konstante testet, kompiliert, läuft und findet nichts, und zwar für immer
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;
Die Aufteilung des Schweregrads kodiert eine Richtlinie. Eine Launch-Aktion ist ein Fehler, weil das Starten eines beliebigen Programms das Gefährlichste ist, was ein Klick in einem PDF anrichten kann, und keine Rechnung benötigt dies. Externe URIs sind Warnungen: Sie kommen in legitimen Dokumenten häufig vor, aber ein Gutachter sollte das Ziel sehen, ohne klicken zu müssen, da der sichtbare Linktext und das tatsächliche Ziel nicht übereinstimmen müssen. GoTo-Sprünge innerhalb des Dokuments sind Struktur, kein Verhalten, und bleiben komplett aus dem Bericht heraus — ein Preflight, der bei jedem Eintrag im Inhaltsverzeichnis falschen Alarm schlägt, gewöhnt die Benutzer daran, ihn zu ignorieren. Das Lesen der Skript-Rümpfe hinter der JavaScript-Anzahl, sowie MDP-Stufen für Signaturen und XFA-Erkennung werden im Artikel zum Security-Risk Auditing über den Objekt-Wrapper der Komponente behandelt
Ressourcen-Metriken: Effektive Bild-DPI
Ein Bild innerhalb eines PDFs hat keine eigenen DPI. Es hat Pixel, und die Seite platziert diese Pixel in einem Rechteck, das in Punkten gemessen wird, wobei 72 Punkte einen Zoll ergeben. Eine Auflösung existiert nur als das Verhältnis der beiden, weshalb dasselbe 600 mal 400 große Foto als Miniaturansicht gestochen scharf und als ganzseitiges Titelbild ein verschwommener Matsch ist. Der Audit benötigt daher beide Zahlen für jedes Bild: die Quell-Pixel-Dimensionen aus den Bild-Metadaten und das platzierte Rechteck aus den Objektgrenzen
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;
Die Schwellenwerte sind eine Richtlinie, keine Physik: 150 DPI ist eine Untergrenze, unter der der Bürodruck sichtbar verpixelt, 300 ist das übliche kommerzielle Ziel, und alles über 600 bringt keine sichtbare Qualität, bläht aber die Dateigröße auf, weshalb es eher als informativer Ballast denn als Defekt gemeldet wird. Eine ehrliche Einschränkung: FPDFPageObj_GetBounds liefert das achsenausgerichtete Rechteck. Bei einem mit Rotation platzierten Bild unterschätzt der berechnete Wert also die tatsächliche Dichte. Die Struktur FPDF_IMAGEOBJ_METADATA enthält zudem die Felder horizontal_dpi und vertical_dpi, die PDFium aus der vollständigen Transformationsmatrix ableitet. Der Vergleich der beiden Ergebnisse ist ein einfacher Weg, um rotierte Platzierungen zu erkennen. Dieselbe Punkt-zu-Pixel-Arithmetik treibt das Rendern in die entgegengesetzte Richtung an, was im Artikel über JPEG-Export behandelt wird
Sicherheitsstatus: Verschlüsselung und Berechtigungsbits
Die PDF-Verschlüsselung definiert zwei Passwörter mit unterschiedlichen Aufgaben. Das Benutzerpasswort kontrolliert die Entschlüsselung: Ohne dieses lässt sich die Datei überhaupt nicht öffnen, und FPDF_LoadDocument liefert nil zurück, wobei FPDF_GetLastError den Wert FPDF_ERR_PASSWORD meldet. Das Besitzerpasswort kontrolliert die Berechtigungen: Eine Datei, die nur durch ein Besitzerpasswort geschützt ist, lässt sich ohne Zugangsdaten öffnen, enthält aber Einschränkungsbits, die ein konformes Leseprogramm beachten muss. Der Ladeversuch selbst ist daher die erste Sicherheitsprüfung, und die Unterscheidung entscheidet über den Exit-Code — eine Datei mit Benutzerpasswort ist nicht prüfbar (Code 2), während eine Datei mit Besitzerpasswort normal geprüft wird und lediglich Ergebnisse sammelt
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;
Die Masken stammen aus Tabelle 22 der ISO 32000-1, die die Bits beginnend bei 1 nummeriert: Bit 3 des /P-Werts ist Maske 4, Bit 5 ist 16, Bit 12 ist 2048. Ob ein bestimmter Fund eine Rolle spielt, ist eine Routing-Entscheidung. Eine Druckerei sollte eine SEC-NOPRINT-Datei bei der Annahme ablehnen, damit der Einreicher eine klare Nachricht erhält, und nicht erst beim RIP drei Stunden vor Ablauf einer Frist. Ein Archiv sollte SEC-ENC an sich als Blocker behandeln, da Verschlüsselung und Langzeitarchivierung nicht zusammenpassen — ein Punkt, den die Standardprüfung gleich formal machen wird
Standard-Markierungen: Ein PDF/A-Zertifikat lesen
Eine Datei deklariert die PDF/A-Konformität in ihrem XMP-Metadatenpaket, und zwar über die Eigenschaft pdfaid:part (1 bis 4) und pdfaid:conformance (der Level-Buchstabe, wie b für visuelle Wiedergabetreue oder a für vollständiges strukturelles Tagging). Die C-API von PDFium bietet keinen XMP-Zugriff; FPDF_GetMetaText liest nur das Info-Wörterbuch, wo sich diese Identifikation jedoch nicht befindet. Der Ausweg ist eine Regel im Standard selbst: ISO 19005 fordert, dass der XMP-Metadaten-Stream unkomprimiert gespeichert wird, genau damit Werkzeuge ihn ohne einen vollständigen PDF-Parser finden können. Ein Scan der rohen Bytes ist daher ein legitimer Zertifikatsdetektor — und eine Datei, deren Zertifikat sich in einem komprimierten Stream versteckt, hat bereits gegen den Standard verstoßen, den sie beansprucht
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;
Das Ergebnis, das hier produziert wird, ist absichtlich informativ, da ein Zertifikat eine Deklaration ist, keine Eigenschaft der Datei. Der XMP-Eintrag ist eine Zeile XML, die jeder Erzeuger schreiben kann, einschließlich fehlerhafter; Konformität bedeutet, dass die Datei tatsächlich hunderte von Regeln bezüglich eingebetteter Schriftarten, geräteunabhängiger Farben und verbotener Funktionen erfüllt. Die Erkennung des Zertifikats sagt Ihnen lediglich, welche Dateien an eine echte Validierung weitergeleitet werden sollen, und nicht mehr. Die eingebaute Preflight-Engine der Komponente führt diese Validierung für PDF/A-, PDF/UA- und PDF/X-Profile durch, und der Artikel zur Batch-CLI zeigt, wie man sie in eine Pipeline mit Berichten einbindet, die ein Auditor später öffnen kann
Ein Lauf gegen eine problematische Datei
Der Treiber reiht die Prüfungen aneinander: zuerst die Sicherheit, da sie entscheidet, ob das Audit überhaupt läuft, dann Verhaltensweisen auf Dokumentenebene und das Standard-Zertifikat, gefolgt von einer Seitenschleife für Aktionen und Bilder
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;
Bei einer Broschüre, die von einer externen Agentur zurückkam, sieht die Ausgabe wie folgt aus:
> 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
Jede Zeile ist für sich genommen umsetzbar, aber die Kombination ist das eigentliche Urteil. Diese Datei beansprucht PDF/A-2, während sie ein Verschlüsselungswörterbuch und aktives JavaScript enthält, und PDF/A verbietet beides rundheraus — das Zertifikat ist also nachweislich falsch, bevor überhaupt ein tieferer Validator läuft. Das ist die Art von Widerspruch, die eine flache Ergebnisliste ans Tageslicht bringt und die ein bloßes Bestanden/Durchgefallen verbirgt
Was dieser Audit Ihnen nicht sagen kann
Ehrlichkeit bezüglich des Umfangs ist das, was das Vertrauen in ein Preflight-Tool erhält. Alles oben genannte liest das, was die Datei über sich selbst deklariert: PDFium parst die Struktur, und dieser Audit inventarisiert sie. Er führt keine PDF/A-Validierung durch — keine Prüfungen der Glyphen-Abdeckung gegen eingebettete Schriften, keine Farbraum-Analyse gegen Ausgabe-Intents, keine der Regeln auf Klausel-Ebene, die ein Zertifikat von der Konformität trennen; dafür benötigen Sie einen speziellen Validator wie die Preflight-Engine der Komponente oder veraPDF. Berechtigungsbits sind Deklarationen, die von konformen Leseprogrammen beachtet werden, keine kryptografischen Mauern, sodass SEC-NOPRINT eher eine Absicht als eine Durchsetzung beschreibt. Der Aktions-Scan deckt Link-Anmerkungen und Skripte auf Dokumentenebene ab; Skripte, die in Event-Wörterbüchern von Formularfeldern vergraben sind, benötigen zusätzlich die Formular-APIs. Und eine Signaturprüfung, falls Sie den Audit um eine solche erweitern, meldet die deklarierte Absicht, keine verifizierte Kryptografie — die Validierung der Zertifikatskette ist eine separate Aufgabe. Ein Preflight-Audit ist das Aufnahmegespräch, nicht das Gerichtsverfahren: Seine Aufgabe ist es, die Routing-Entscheidung fundiert, schnell und wiederholbar zu machen
Hinweis: Die in diesem Audit verwendeten APIs für Dokument-, Seiten-, Anmerkungs- und Bildobjekte werden zusammen mit einem High-Level-Delphi-Wrapper und einer vollständigen Standard-Validierungs-Preflight-Engine mit der PDFium-Komponente ausgeliefert