Artikel Teknis

Prapenerbangan PDF Otomatis dan Audit Risiko dengan PDFium

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