Sebuah PDF yang tiba di batas produksi — sebuah antrean cetak, arsip, portal unggahan pelanggan — harus diaudit sebelum apa pun merendernya. File tersebut mungkin membawa aksi Luncurkan (Launch) yang ditautkan untuk memulai sebuah program eksternal, gambar-gambar yang terlalu kasar untuk dapat bertahan di pencetakan, sebuah kamus enkripsi yang melarang pekerjaan cetak yang ditujukannya, atau sebuah label PDF/A yang ternyata tidak ditepatinya. Memeriksa sebuah dokumen terhadap aturan-aturan seperti ini sebelum ia memasuki sebuah alur kerja disebut prapenerbangan (preflighting), dan C API dari PDFium memberi Delphi segala hal yang dibutuhkan untuk mengimplementasikan pemeriksaan-pemeriksaan tersebut secara langsung, tanpa merender satu halaman pun
Artikel ini membangun pemeriksaan itu sendiri: empat kelas audit, yang mana masing-masing berupa rutinitas kecil yang menambahkan temuan ke daftar hasil bersama. Elemen interaktif, metrik sumber daya, keadaan keamanan, dan penanda standar semuanya mendapatkan kode yang bekerja, termasuk aritmatikanya. Jika apa yang Anda perlukan adalah mesin-mesin di sekitar pemeriksaannya — perulangan folder bergelombang, file laporan JSON dan HTML, isolasi per-file — Komponen PDFium dilengkapi dengan mesin prapenerbangan yang sudah jadi, dan artikel CLI prapenerbangan tumpak (batch) meliput sistem perpipaan tersebut. Keduanya sengaja berbagi satu kosakata kode keluar (exit-code), sehingga seorang auditor yang ditulis di sini pas terhubung tepat di bawah pendorong tumpakan tersebut
Catatan temuan dan kontrak kode keluar
Setiap pemeriksaan menulis ke dalam satu tipe rekaman datar (flat record), karena alternatifnya, yaitu setiap pemeriksaan mencetak prosanya masing-masing, tidak dapat dihitung, disaring, atau diberi ambang batas sesudahnya. Empat bidang (fields) 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 level dokumen
Message: string; // untuk manusia; bebas diubah kata-katanya di antara 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 aliran-hilir menggunakan Code sebagai kuncinya, dan sama sekali tidak pernah pada teks Message, yang mana bebas untuk diubah. Kode keluar proses mengikuti kontrak tiga-nilai yang sama dengan artikel batch: 0 berarti file tidak menghasilkan temuan, 1 berarti ada temuan, dan 2 berarti audit itu sendiri tidak dapat berjalan karena file tersebut gagal diurai atau menuntut adanya sandi. Memisahkan kode 2 itu penting. Sebuah folder berisi hasil pindaian yang korup adalah pemindai yang rusak di aliran-hulu, bukan kehancuran kepatuhan yang mendadak, dan melipatgandakan keduanya bersama-sama mengirimkan seseorang untuk mengejar masalah yang salah
Elemen interaktif: skrip, target peluncuran, tautan eksternal
PDFium mengklasifikasikan setiap aksi yang ia temukan ke dalam tipe integer, dan konstanta-konstanta dari fpdf_doc.h layak ditetapkan secara akurat, karena nilai yang disalin-salah membuat pemindai buta secara diam-diam. Enumerasi aslinya adalah PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4, dan PDFACTION_EMBEDDEDGOTO = 5. Catat apa yang tak ada: tidak ada anggota JavaScript. Skrip tingkat-dokumen bukanlah aksi tautan dan tidak pernah muncul melalui FPDFAction_GetType; mereka dienumerasi oleh sebuah keluarga panggilan tersendiri. Seorang auditor yang menguji tipe-tipe aksi terhadap suatu konstanta JavaScript imajiner akan mengompilasi, berjalan, dan tak menemukan apa pun, selamanya
const
PDFACTION_GOTO = 1; // lompatan dalam-dokumen: tidak berbahaya
PDFACTION_REMOTEGOTO = 2; // melompat ke file lokal lain
PDFACTION_URI = 3; // membuka URL eksternal
PDFACTION_LAUNCH = 4; // memulai program eksternal
PDFACTION_EMBEDDEDGOTO = 5; // melompat 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 perlu ditandai
AType := FPDFAction_GetType(Action);
case AType of
PDFACTION_LAUNCH:
Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
'Aksi peluncuran menargetkan "' + ActionTarget(Doc, Action, AType) + '"');
PDFACTION_URI:
Add(Findings, fsWarning, 'ACT-URI', PageNo,
'tautan membuka ' + ActionTarget(Doc, Action, AType));
PDFACTION_REMOTEGOTO, PDFACTION_EMBEDDEDGOTO:
Add(Findings, fsWarning, 'ACT-XFILE', PageNo,
'tujuan lintas-file "' + ActionTarget(Doc, Action, AType) + '"');
end; // PDFACTION_GOTO tetap diam berdasarkan desainnya
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 aksi JavaScript tingkat-dokumen berjalan saat dibuka', [N]));
N := FPDFDoc_GetAttachmentCount(Doc);
if N > 0 then
Add(Findings, fsWarning, 'ATT-EMB', 0,
Format('%d lampiran file tertanam', [N]));
end;
Pemisahan tingkat keparahan mengodekan sebuah kebijakan. Sebuah aksi Peluncuran adalah galat (error) karena memulai program sembarang adalah hal paling berbahaya yang dapat dilakukan oleh sebuah klik di PDF, dan tak ada faktur yang membutuhkannya. URI eksternal adalah peringatan: lazim pada dokumen yang sah, tetapi seorang peninjau harus melihat sasarannya tanpa mengkliknya, karena teks tautan yang terlihat dan sasaran yang sesungguhnya tak harus setuju. Lompatan GoTo dalam-dokumen adalah tentang struktur, bukan perilaku, dan tetap tak dimunculkan pada laporannya seutuhnya — sebuah prapenerbangan yang meneriakkan peringatan palsu pada tiap entri daftar isi akan melatih orang-orang untuk mengabaikannya. Untuk membaca raga-raga skrip di balik hitungan JavaScript, dan untuk tingkat MDP tanda tangan dan deteksi XFA, artikel audit risiko-keamanan menyusuri permukaan yang sama melalui pembungkus (wrapper) objek milik komponen
Metrik sumber daya: DPI gambar efektif
Sebuah gambar di dalam PDF tidak memiliki DPI-nya sendiri. Ia memiliki piksel, dan halamannya menempatkan piksel-piksel tersebut ke dalam sebuah persegi panjang yang diukur dalam poin, di mana 72 poin merupakan satu inci. Resolusi hanya ada sebagai rasio dari keduanya, yang mana itu merupakan alasan gambar foto 600 kali 400 yang sama akan terlihat tajam-silet sebagai sebuah keluku (thumbnail) dan akan menjadi kekacauan buram sebagai jagoan selebar halaman-penuh. Dengan begitu auditor membutuhkan kedua angka ini untuk setiap gambar: dimensi piksel sumber dari metadata gambarnya, dan persegi panjang yang ditempatkan dari batas-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 ditempatkan 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, maka inci ditempatkan = poin / 72, dan
// DPI efektif = piksel sumber / inci ditempatkan.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // sumbu yang lebih buruk menentukan kualitas cetak
if EffDpi < 150.0 then
Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
Format('gambar %dx%d px ditempatkan pada %.1fx%.1f pt = %.0f DPI efektif',
[Meta.Width, Meta.Height, WidthPt, HeightPt, EffDpi]))
else if EffDpi > 600.0 then
Add(Findings, fsInfo, 'IMG-BLOAT', PageNo,
Format('gambar %.0f DPI pada ukuran penempatan; pengambilan sampel ulang akan ' +
'mengecilkan file tanpa kehilangan yang terlihat', [EffDpi]));
end;
end;
Batas-batas ini adalah kebijakan, bukan fisika: 150 DPI merupakan lantai pijak di bawah apa pencetakan perkantoran akan terlihat termozaik (pixelate) nampak jelas, 300 ialah target standar perniagaannya, lalu apapun di atas 600 niscaya tiada lagi menebus ganjaran kualitas yang terinderakan mata sementara ianya meroketkan membengkakkan bobot berkas, itulah pasal mengapa hal itu dilaporkan sebatas sebagai informasi gembung kelebihan bobot dan bukannya kecacatan. Sebuah tangkisan ujaran jujur: FPDFPageObj_GetBounds memulangkan kotak penyesuaian-poros (axis-aligned box), jadi untuk sebuah gambar yang ditempatkan dengan perputaran maka angka yang dihitung tersebut memperkirakan lebih rendah ketimbang kepadatan yang sesungguhnya. Struktur dari FPDF_IMAGEOBJ_METADATA turut pula menjinjing bidang horizontal_dpi bersama vertical_dpi yang diturunkan PDFium dari matriks perubahan (transform matrix) seutuhnya, serta membandingkan kedua hasil darinya merupakan cara murah untuk menyingkap penempatan yang terputar. Aritmetika dari poin-ke-piksel yang sama menggerakkan perenderan ke arah sebaliknya, tercakup dalam artikel mengenai ekspor JPEG
Keadaan keamanan: enkripsi dan bit izin
Enkripsi PDF mendefinisikan sepasang sandi dengan pekerjaan yang berlainan. Sandi pengguna (user password) memagari dekripsi: tanpanya file akan menolak untuk terbuka sama sekali, dan FPDF_LoadDocument mengembalikan nil dengan FPDF_GetLastError melaporkan FPDF_ERR_PASSWORD. Sandi pemilik (owner password) mengunci izin: sebuah file yang dilindungi hanya oleh sandi pemilik terbuka tanpa kredensial tetapi membawa bit larangan (restriction) yang mana pembaca patuh (conforming reader) harus menghormatinya. Percobaan pemuatan itu sendiri dengan demikian menjadi penduga (probe) keamanan yang pertama, dan perbedaan ini memutuskan kode keluarnya — sebuah file sandi-pengguna tidak dapat diaudit (kode 2), sementara file sandi-pemilik diaudit secara normal dan hanya 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,
'sandi pengguna (buka) diperlukan; audit tidak dapat dilanjutkan')
else
Add(Findings, fsError, 'DOC-BROKEN', 0, 'file gagal diurai');
Exit;
end;
Revision := FPDF_GetSecurityHandlerRevision(Result);
if Revision >= 0 then // -1 berarti file tidak terenkripsi
begin
// Dibuka dengan sandi kosong namun terenkripsi: hanya-sandi-pemilik.
// Siapa pun dapat membacanya, namun bit izin membatasi apa yang
// pembaca patuh izinkan untuk mereka lakukan. File tak terenkripsi
// melaporkan semua bit diatur, oleh karena itu gerbang revisi muncul lebih dulu.
Perms := FPDF_GetDocPermissions(Result);
Add(Findings, fsInfo, 'SEC-ENC', 0,
Format('terenkripsi, revisi penangan keamanan %d', [Revision]));
if (Perms and 4) = 0 then // bit 3: cetak
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'pencetakan tidak diizinkan');
if (Perms and 16) = 0 then // bit 5: salin / ekstrak konten
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'ekstraksi konten tidak diizinkan');
if (Perms and 2048) = 0 then // bit 12: cetak resolusi-tinggi
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'hanya pencetakan resolusi rendah yang diizinkan');
end;
end;
Topeng-topengnya (masks) berasal dari Tabel 22 dari ISO 32000-1, yang menomori bit dari 1: bit 3 dari nilai /P adalah topeng 4, bit 5 adalah 16, bit 12 adalah 2048. Apakah temuan tertentu penting merupakan keputusan perutean (routing). Biro pencetakan harus memantulkan sebuah SEC-NOPRINT saat pendaftaran awal (intake), tempat sang pengirim mendapat pesan yang jelas, bukannya pada saat RIP tiga jam sebelum tenggat waktu. Sebuah arsip harus memperlakukan SEC-ENC itu sendiri sebagai pemblokir, karena enkripsi dan pelestarian jangka panjang tidak dapat bersatu — poin yang bakal dibuat secara resmi oleh pemeriksaan standar ini
Penanda standar: membaca sebuah klaim PDF/A
Sebuah file mendeklarasikan kesesuaian PDF/A pada paket metadata XMP-nya, melalui properti pdfaid:part (1 hingga 4) dan pdfaid:conformance (huruf tingkatnya, seperti b untuk ketepatan visual atau a untuk penandaan struktural yang penuh). C API PDFium tak menawarkan pengakses XMP; FPDF_GetMetaText hanya membaca kamus Info, yang bukan merupakan tempat keberadaan identifikasi tersebut. Celah pelariannya adalah sebuah aturan di dalam standarnya itu sendiri: ISO 19005 mewajibkan aliran metadata XMP untuk disimpan tanpa dikompres, tepat agar peralatan-peralatan dapat menemukannya tanpa alat pengurai PDF yang penuh. Oleh karenanya sebuah pindaian byte mentah adalah detektor klaim yang sah — dan sebuah file yang klaimnya bersembunyi di dalam aliran yang dikompres (compressed stream) telah melanggar standar yang ia klaim
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // kosong = tidak ada klaim PDF/A hadir
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 baik <pdfaid:part>2</pdfaid:part> dan pdfaid:part="2":
// ambil digit pertama setelah nama properti.
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 berupa informasional belaka, atas alasan bahwa sebuah klaim adalah deklarasi, bukan properti dari file itu sendiri. Entri XMP merupakan satu baris XML yang produsen mana pun bisa menulisnya, tak terkecuali yang rusak sekalipun; kepatuhannya bermuara pada file secara nyata memuaskan ratusan aturan perihal font tertanam, warna yang independen terhadap-perangkat, dan larangan atas fitur-fitur yang terlarang. Mendeteksi klaim tersebut semata bertutur kepada Anda perihal file-file mana yang harus dirutekan ke validasi sungguhan, dan tidak lebih dari itu. Mesin prapenerbangan (preflight) bawaan dari Komponen tersebut melakukan validasi itu melintasi profil-profil PDF/A, PDF/UA, dan PDF/X, dan artikel CLI kelompok tumpak (batch CLI) memaparkan bagaimana cara menyematkannya ke dalam sebuah perpipaan dengan memuat laporan-laporan yang dapat dibuka auditor nantinya
Menjalankannya terhadap file yang bermasalah
Sang pendorong (driver) menguntai pemeriksaan-pemeriksaan tersebut bersama-sama: keamanan lebih dulu, karena ini yang akan memutuskan apakah audit tersebut akan berjalan secara penuh, disusul oleh perilaku tingkat-dokumen dan klaim standar, barulah kemudian perulangan halaman untuk aksi 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 vonis
try
AuditDocumentBehaviors(Doc, Findings);
Claim := PdfAClaim(FileName);
if Claim <> '' then
Add(Findings, fsInfo, 'STD-PDFA', 0,
Claim + ' kesesuaian diklaim (hanya deklarasi, tidak divalidasi)');
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, 'halaman gagal diurai');
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;
Apabila dijalankan terhadap brosur yang baru kembali dari agensi luar, keluarannya terlihat seperti ini
> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 temuan
[ERROR] ACT-LAUNCH halaman 3 Aksi peluncuran menargetkan "..\tools\setup.exe"
[ERROR] JS-DOC dok 2 aksi JavaScript tingkat-dokumen berjalan saat dibuka
[WARNING] IMG-LOWRES halaman 7 gambar 412x287 px ditempatkan pada 396.0x275.8 pt = 75 DPI efektif
[WARNING] SEC-NOPRINT dok pencetakan tidak diizinkan
[INFO] STD-PDFA dok Kesesuaian PDF/A-2 diklaim (hanya deklarasi, tidak divalidasi)
kode keluar 1
Setiap barisnya dapat ditindaklanjuti secara mandiri, tetapi kombinasinya merupakan vonis yang sesungguhnya. File ini mengklaim PDF/A-2 sembari membawa kamus enkripsi dan JavaScript yang hidup, sedangkan PDF/A melarang keduanya secara mutlak — sehingga klaim tersebut dapat dibuktikan salah jauh sebelum validator dalam (deep validator) apa pun sempat berjalan. Ini merupakan macam pertentangan (kontradiksi) yang mana sebuah senarai datar akan memunculkannya ke permukaan dan ditutupi oleh lulus/gagal biner
Yang tidak dapat diberitahukan oleh audit ini kepada Anda
Kejujuran mengenai cakupan adalah hal yang menjaga alat prapenerbangan tetap dipercaya. Segala hal di atas membaca apa yang dideklarasikan oleh file tersebut mengenai dirinya sendiri: PDFium mengurai strukturnya, dan audit ini menginventarisasinya. Ia tidak menjalankan validasi PDF/A — tak ada pemeriksaan jangkauan gilf (glyph-coverage) terhadap font tertanam, tiada analisis ruang warna terhadap intensi keluaran (output intents), tidak satupun aturan tingkat-klausul yang membedakan sebuah klaim dari kepatuhan sejatinya; untuk melakukan hal tersebut Anda membutuhkan sebuah validator tersendiri khusus seperti mesin prapenerbangan komponen atau veraPDF. Bit izin adalah deklarasi yang dipatuhi oleh pembaca-pembaca patuh, bukannya tembok kriptografi, maka SEC-NOPRINT mendedahkan intensi (niat) alih-alih penegakan paksa (enforcement). Pindaian aksi tersebut mencakup anotasi tautan dan skrip tingkat-dokumen; namun skrip yang terkubur di dalam kamus peristiwa bidang-formulir membutuhkan lapis API formulir di atasnya. Dan lagi, pemeriksaan tanda tangan, jika Anda memperluas audit ini dengannya, hanya melaporkan intensi yang dideklarasikan, bukannya kriptografi yang terverifikasi — validasi rantai sertifikat adalah pekerjaan tersendiri. Sebuah audit prapenerbangan laksana wawancara penerimaan asupan dokumen (intake interview), bukannya sebuah persidangan: tugas utamanya adalah membuat keputusan peruteannya menjadi terinformasi, gesit, dan dapat diulang
Catatan: Dokumen, halaman, anotasi, dan API objek gambar yang digunakan di sepanjang audit ini, dipadukan bersama pembungkus Delphi tingkat-tinggi dan sebuah mesin prapenerbangan validasi standar yang lengkap, dikirimkan bersama Komponen PDFium