Sebuah PDF yang tiba di batas produksi — antrean cetak, arsip, portal unggahan pelanggan — sebaiknya diaudit sebelum apa pun me-render-nya. File itu bisa saja membawa Launch action yang dirangkai untuk menjalankan program eksternal, gambar yang terlalu kasar untuk bertahan saat dicetak, sebuah dictionary enkripsi yang justru melarang pekerjaan cetak yang menjadi alasan pengirimannya, atau label PDF/A yang tidak dipenuhinya. Memeriksa sebuah dokumen terhadap aturan semacam itu sebelum ia masuk ke dalam workflow disebut preflighting, dan PDFium C API memberi Delphi semua yang diperlukan untuk mengimplementasikan pemeriksaannya secara langsung, tanpa me-render satu halaman pun
Artikel ini membangun pemeriksaannya sendiri: empat kelas audit, masing-masing berupa rutin kecil yang menambahkan temuan ke sebuah daftar hasil bersama. Elemen interaktif, metrik sumber daya, keadaan keamanan, dan penanda standar semuanya memperoleh kode yang bisa dijalankan, termasuk aritmetikanya. Jika yang Anda butuhkan adalah mesin di sekeliling pemeriksaan itu — loop folder batch, file laporan JSON dan HTML, isolasi per file — PDFium Component menyertakan engine preflight siap pakai, dan artikel tentang CLI preflight batch membahas perpipaan tersebut. Keduanya sengaja berbagi satu kosakata exit code, sehingga auditor yang ditulis di sini langsung terpasang di bawah driver batch itu
Record temuan dan kontrak exit code-nya
Setiap pemeriksaan menulis ke dalam satu tipe record datar, karena alternatifnya, yaitu setiap pemeriksaan mencetak prosanya sendiri, tidak bisa dihitung, disaring, atau diambangkan sesudahnya. Empat field sudah cukup
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // kunci mesin yang stabil, mis. 'ACT-LAUNCH'
Page: Integer; // berbasis 1; 0 berarti tingkat dokumen
Message: string; // untuk manusia; bebas diubah antar rilis
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;
Perkakas di hilir berpatokan pada Code, tidak pernah pada teks Message, yang bebas berubah. Exit code prosesnya mengikuti kontrak tiga nilai yang sama dengan artikel batch: 0 berarti file itu tidak menghasilkan temuan, 1 berarti ada temuan, dan 2 berarti auditnya sendiri tidak bisa berjalan karena file-nya gagal di-parsing atau menuntut password. Menjaga kode 2 tetap terpisah itu penting. Satu folder berisi hasil pindaian rusak berarti pemindai yang bermasalah di hulu, bukan keruntuhan kepatuhan yang mendadak, dan menyatukan keduanya akan mengirim orang mengejar masalah yang keliru
Elemen interaktif: skrip, target launch, tautan eksternal
PDFium mengklasifikasikan setiap action yang ditemukannya dengan sebuah tipe bilangan bulat, dan konstanta dari fpdf_doc.h layak dipaku dengan tepat, karena nilai yang salah salin membuat pemindainya buta diam-diam. Enumerasi yang sebenarnya adalah PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4, dan PDFACTION_EMBEDDEDGOTO = 5. Perhatikan apa yang tidak ada: tidak ada anggota JavaScript. Skrip tingkat dokumen bukanlah link action dan tidak pernah muncul lewat FPDFAction_GetType; keduanya dienumerasi oleh keluarga panggilan yang terpisah. Sebuah auditor yang menguji tipe action terhadap konstanta JavaScript khayalan akan tetap ter-compile, tetap berjalan, dan tidak menemukan apa pun, selamanya
const
PDFACTION_GOTO = 1; // lompatan dalam dokumen: tidak berbahaya
PDFACTION_REMOTEGOTO = 2; // lompatan ke file lokal lain
PDFACTION_URI = 3; // membuka URL eksternal
PDFACTION_LAUNCH = 4; // menjalankan program eksternal
PDFACTION_EMBEDDEDGOTO = 5; // lompatan ke file tertanam
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; // tautan hanya tujuan, tak ada yang ditandai
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 sengaja dibiarkan diam
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;
Pembagian tingkat keparahannya mengodekan kebijakan. Sebuah Launch action adalah error karena menjalankan program sembarang adalah hal paling berbahaya yang bisa dilakukan sebuah klik di dalam PDF, dan tidak ada faktur yang membutuhkannya. URI eksternal berstatus peringatan: lazim ada di dokumen yang sah, namun seorang pemeriksa sebaiknya melihat targetnya tanpa mengklik, karena teks tautan yang terlihat dan tujuan sebenarnya tidak harus sama. Lompatan GoTo di dalam dokumen adalah struktur, bukan perilaku, dan sepenuhnya tidak masuk laporan — sebuah preflight yang berteriak serigala pada setiap entri daftar isi hanya melatih orang untuk mengabaikannya. Untuk membaca isi skrip di balik hitungan JavaScript itu, serta untuk level MDP signature dan deteksi XFA, artikel tentang audit risiko keamanan menyusuri permukaan yang sama lewat pembungkus objek milik component-nya
Metrik sumber daya: DPI efektif gambar
Sebuah gambar di dalam PDF tidak punya DPI-nya sendiri. Ia punya piksel, dan halaman menempatkan piksel itu ke dalam sebuah persegi panjang yang diukur dalam poin, di mana 72 poin sama dengan satu inci. Resolusi hanya ada sebagai rasio keduanya, itulah sebabnya foto 600 kali 400 yang sama bisa setajam silet sebagai thumbnail dan menjadi kekacauan buram sebagai hero satu halaman penuh. Karena itu auditnya membutuhkan kedua angka untuk setiap gambar: dimensi piksel sumber dari metadata gambarnya, dan persegi panjang penempatannya dari batas objeknya
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; // ukuran penempatan di halaman, dalam poin
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 poin = 1 inci, jadi inci penempatan = poin / 72, dan
// DPI efektif = piksel sumber / inci penempatan.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // sumbu terburuk menentukan kualitas cetak
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;
Ambangnya adalah kebijakan, bukan fisika: 150 DPI adalah lantai yang di bawahnya cetakan kantoran terlihat berpiksel, 300 adalah sasaran komersial yang biasa, dan apa pun di atas 600 tidak membeli kualitas yang kasatmata sambil menggelembungkan ukuran file, itulah sebabnya ia dilaporkan sebagai kegemukan yang bersifat informatif alih-alih sebagai cacat. Satu peringatan yang jujur: FPDFPageObj_GetBounds mengembalikan kotak sejajar sumbu, sehingga untuk gambar yang ditempatkan dengan rotasi angka hitungannya meremehkan kerapatan yang sebenarnya. Struct FPDF_IMAGEOBJ_METADATA juga membawa field horizontal_dpi dan vertical_dpi yang diturunkan PDFium dari matriks transformasi lengkapnya, dan membandingkan kedua hasil itu adalah cara murah untuk mengendus penempatan yang terotasi. Aritmetika poin-ke-piksel yang sama menggerakkan rendering ke arah sebaliknya, yang dibahas di artikel tentang ekspor JPEG
Keadaan keamanan: enkripsi dan bit izin
Enkripsi PDF mendefinisikan dua password dengan tugas berbeda. User password menjaga gerbang dekripsi: tanpanya file itu sama sekali tidak akan terbuka, dan FPDF_LoadDocument mengembalikan nil dengan FPDF_GetLastError melaporkan FPDF_ERR_PASSWORD. Owner password menjaga gerbang izin: sebuah file yang hanya dilindungi owner password akan terbuka tanpa kredensial namun membawa bit pembatasan yang wajib dihormati pembaca yang patuh. Karena itu upaya pemuatannya sendiri adalah probe keamanan yang pertama, dan pembedaan tersebut menentukan exit code-nya — file dengan user password tidak dapat diaudit (kode 2), sementara file dengan owner password diaudit secara normal dan sekadar mengumpulkan temuan
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 berarti file-nya tidak terenkripsi
begin
// Terbuka dengan password kosong namun terenkripsi: hanya owner password.
// Siapa pun boleh membacanya, tetapi bit izinnya membatasi apa yang
// diperbolehkan pembaca yang patuh. File tak terenkripsi melaporkan
// semua bit menyala, itulah sebabnya gerbang revisi datang lebih dulu.
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: cetak
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'printing is not permitted');
if (Perms and 16) = 0 then // bit 5: salin / ekstrak konten
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bit 12: cetak resolusi tinggi
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
Mask-nya berasal dari Tabel 22 ISO 32000-1, yang menomori bit mulai dari 1: bit 3 pada nilai /P adalah mask 4, bit 5 adalah 16, bit 12 adalah 2048. Apakah sebuah temuan tertentu penting atau tidak adalah keputusan perutean. Sebuah biro cetak sebaiknya menolak file SEC-NOPRINT pada saat penerimaan, ketika pengirimnya masih memperoleh pesan yang jelas, alih-alih di RIP tiga jam sebelum tenggat. Sebuah arsip sebaiknya memperlakukan SEC-ENC itu sendiri sebagai penghalang, karena enkripsi dan pelestarian jangka panjang tidak berbaur — sebuah poin yang sebentar lagi dinyatakan resmi oleh pemeriksaan standarnya
Penanda standar: membaca sebuah klaim PDF/A
Sebuah file menyatakan kesesuaian PDF/A di dalam paket metadata XMP-nya, melalui properti pdfaid:part (1 sampai 4) dan pdfaid:conformance (huruf levelnya, seperti b untuk kesetiaan visual atau a untuk penandaan struktural penuh). C API milik PDFium tidak menawarkan akses ke XMP; FPDF_GetMetaText hanya membaca dictionary Info, yang bukan tempat identifikasi itu berdiam. Jalan keluarnya adalah sebuah aturan di dalam standarnya sendiri: ISO 19005 mewajibkan stream metadata XMP disimpan tanpa kompresi, justru supaya perkakas bisa menemukannya tanpa parser PDF yang lengkap. Karena itu pemindaian byte mentah adalah pendeteksi klaim yang sah — dan sebuah file yang klaimnya bersembunyi di dalam stream terkompresi sudah melanggar standar yang diklaimnya
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // kosong = tidak ada klaim PDF/A
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // skema identifikasi XMP
if P = 0 then
Exit;
// Menangani <pdfaid:part>2</pdfaid:part> maupun pdfaid:part="2":
// ambil digit pertama setelah nama propertinya.
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;
Temuan yang dihasilkannya sengaja bersifat informatif, karena sebuah klaim adalah pernyataan, bukan sifat file-nya. Entry XMP itu hanyalah satu baris XML yang bisa ditulis produser mana pun, termasuk yang rusak; kesesuaian berarti file itu benar-benar memenuhi ratusan aturan tentang font tertanam, warna yang bebas perangkat, dan fitur yang terlarang. Mendeteksi klaimnya memberi tahu Anda file mana yang perlu dirutekan ke validasi yang sesungguhnya, dan tidak lebih dari itu. Engine preflight bawaan component-nya melakukan validasi itu untuk profil PDF/A, PDF/UA, dan PDF/X, dan artikel tentang CLI batch menunjukkan cara merangkainya ke dalam sebuah pipeline dengan laporan yang bisa dibuka auditor belakangan
Satu kali jalan terhadap file bermasalah
Driver-nya merangkai pemeriksaan itu: keamanan lebih dulu, karena ia menentukan apakah auditnya berjalan sama sekali, lalu perilaku tingkat dokumen dan klaim standarnya, kemudian sebuah loop halaman untuk action dan gambar
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); // kegagalan audit, bukan sebuah putusan
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;
Terhadap sebuah brosur yang kembali dari agensi luar, keluarannya tampak seperti ini
> 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
Setiap barisnya bisa ditindaklanjuti sendiri-sendiri, namun kombinasinyalah putusan yang sesungguhnya. File ini mengklaim PDF/A-2 sambil membawa dictionary enkripsi dan JavaScript yang hidup, sementara PDF/A melarang keduanya secara mutlak — sehingga klaimnya terbukti palsu sebelum validator mendalam mana pun dijalankan. Itulah jenis kontradiksi yang dimunculkan sebuah daftar temuan yang datar dan disembunyikan oleh lulus/gagal yang bersifat boolean
Apa yang tidak bisa diberitahukan audit ini kepada Anda
Kejujuran soal cakupan itulah yang menjaga sebuah tool preflight tetap dipercaya. Semua yang di atas membaca apa yang dinyatakan file itu tentang dirinya sendiri: PDFium mem-parsing strukturnya, dan audit ini menginventarisasinya. Ia tidak melakukan validasi PDF/A — tidak ada pemeriksaan cakupan glyph terhadap font tertanam, tidak ada analisis color space terhadap output intent, dan tidak ada satu pun aturan setingkat klausul yang memisahkan klaim dari kesesuaian; untuk itu Anda butuh validator khusus seperti engine preflight milik component-nya atau veraPDF. Bit izin adalah pernyataan yang dihormati pembaca yang patuh, bukan tembok kriptografis, sehingga SEC-NOPRINT menggambarkan niat alih-alih penegakan. Pemindaian action-nya mencakup anotasi tautan dan skrip tingkat dokumen; skrip yang terkubur di dalam dictionary event pada form field membutuhkan API form di atasnya. Dan sebuah pemeriksaan signature, jika Anda memperluas auditnya dengan itu, melaporkan niat yang dinyatakan, bukan kriptografi yang terverifikasi — validasi rantai sertifikat adalah pekerjaan tersendiri. Audit preflight adalah wawancara penerimaan, bukan persidangan: tugasnya adalah membuat keputusan perutean menjadi terinformasi, cepat, dan bisa diulang
Catatan: API dokumen, halaman, anotasi, dan objek gambar yang dipakai di sepanjang audit ini, bersama sebuah pembungkus Delphi tingkat tinggi dan engine preflight validasi standar yang lengkap, disertakan bersama PDFium Component