Artikel Teknis

Hardening Dekoder TIFF Delphi: BigTIFF dan TIFF Tiled

PDFlibPas mendekode TIFF dengan parser Object Pascal yang ditulis sendiri, bukan binding libtiff, dan versi 3.534.1 memperketat tepat di titik-titik parser tersebut menolak input. Magic 43 BigTIFF kini ditolak dengan disebut namanya, TileOffsets dan TileByteCounts ditolak saat parsing tag, dan setiap buffer diukur lewat aritmetika Int64 di bawah plafon dekode 256 MiB

Cacat yang ditutup ini tidak pernah muncul di laboratorium. Ia muncul sebagai gateway pemindai yang berjalan sunyi selama tiga tahun, sampai seorang pelanggan mengarahkan arsip geospasial atau citra medis whole-slide ke dalamnya. Berkasnya memiliki header TIFF yang sah. Berkas itu terurai. Yang keluar adalah satu halaman noise bergaris, atau alokasi multigigabyte yang menjatuhkan layanan, dan tidak ada satu pun langkah di sepanjang jalan yang menyatakan input itu tidak valid. Itulah bentuk kegagalan yang patut diantisipasi secara rekayasa: bukan crash, melainkan jawaban yang salah yang disampaikan dengan percaya diri

Mengapa II atau MM tidak membuktikan Anda punya TIFF klasik?

Karena penanda byte-order dipakai bersama oleh kedua dialek. TIFF klasik dan BigTIFF sama-sama dibuka dengan II atau MM, dan field yang benar-benar membedakan keduanya adalah magic 16-bit tepat setelahnya: 42 untuk TIFF klasik sebagaimana didefinisikan dalam spesifikasi TIFF 6.0, 43 untuk BigTIFF dengan offset 64-bit-nya. Loader yang ditulis sebagai FValidTIFF := PopWord = 42 tidak salah soal TIFF klasik, tetapi ia menciutkan dua penolakan yang sangat berbeda menjadi satu boolean sunyi, sehingga BigTIFF tak bisa dibedakan dari JPEG terpotong yang di-rename seseorang. PDFlibPas kini memisahkan kasus-kasus itu dan mencatat masing-masing di TPDFTIFF.LastError: header lebih pendek dari empat byte, penanda byte-order tidak valid, magic 43, dan nilai magic lain semuanya menghasilkan teks yang berbeda. Pustaka ini tetap tidak mendekode BigTIFF, dan mengatakannya dengan gamblang justru intinya. Pemanggil mendapatkan perbedaan antara "ini bukan TIFF" dan "ini TIFF yang tata letak offset 64-bit-nya tidak diimplementasikan dekoder bawaan", yang merupakan perbedaan antara tiket dukungan yang bisa dijawab dalam satu balasan dan tiket yang berubah menjadi seminggu penuh tebakan

Loader TIFF PDFlibPas membaca penanda byte-order dan magic 16-bit secara terpisah, sehingga header pendek, penanda tidak valid, magic 43 BigTIFF, dan nilai magic lain masing-masing menghasilkan teks LastError yang berbeda, bukan satu boolean sunyi
TIFF klasik dan BigTIFF dibuka dengan penanda byte-order yang sama, sehingga PDFlibPas memisahkan empat kasus penolakan dan menyebut masing-masing di LastError
var
  Tiff: TPDFTIFF;
  Page: Integer;
begin
  Tiff := TPDFTIFF.Create;
  try
    Tiff.LoadFromFile('inbox\scan-0417.tif');
    if not Tiff.ValidTIFF then
      raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
    if Tiff.PageCount < 1 then
      raise Exception.Create('TIFF carries no decodable page');
    for Page := 1 to Tiff.PageCount do
      Writeln(Format('page %d: %dx%d, %d spp',
        [Page,
         Tiff.PageInfo[Page].Width,
         Tiff.PageInfo[Page].Height,
         Tiff.PageInfo[Page].SamplesPerPixel]));
  finally
    Tiff.Free;
  end;
end;

Tile adalah geometri yang berbeda, bukan array offset lain

PDFlibPas menolak TIFF tiled saat mem-parsing tag, sebelum data piksel mana pun disentuh. Jalan pintas yang mengundang bug ini mudah terlihat: tag 324 (TileOffsets) dan tag 325 (TileByteCounts) adalah array offset berkas dan jumlah byte, secara struktural identik dengan array strip, sehingga mengarahkan field strip yang sudah ada ke keduanya hanya butuh dua baris dan terkompilasi bersih. Itu juga salah. Tile membentuk grid dua dimensi dengan blok tepi yang di-pad, stride barisnya sendiri di dalam setiap tile, dan tanpa semantik RowsPerStrip sama sekali, sebagaimana diuraikan bagian tiled-image pada TIFF 6.0. Memberikan payload tile ke dekoder strip karenanya tidak gagal dengan keras. SimpleExtract dan CompDecode menyusuri data dengan stride yang salah dan menghasilkan gambar dengan dimensi yang benar dan piksel yang salah. Kode lama memperparahnya dengan menyimpan StripsAreTiles, ColumnsPerTile dan RowsPerTile di TTIFFPage: geometri tile yang dicatat oleh dekoder tanpa perakit tile sama sekali. Di 3.534.1, handler untuk tag 324 dan 325 memunculkan error tile dan langsung meninggalkan IFD, sehingga penolakan itu membawa kata "tiled" alih-alih muncul berminggu-minggu kemudian sebagai keluhan rendering

PDFlibPas membandingkan tata letak strip, tempat pita selebar penuh berbagi satu stride baris, dengan tata letak tile, grid dua dimensi dari blok tepi yang di-pad, dan menolak tag 324 serta 325 saat parsing tag sebelum data piksel mana pun disentuh
Array tile tampak secara struktural identik dengan array strip, itulah sebabnya memberikannya ke dekoder strip menghasilkan dimensi yang benar dan piksel yang salah

Satu clamp dimensi bukanlah anggaran memori

Membatasi lebar dan tinggi masing-masing ke 65.535 itu perlu namun jauh dari cukup, karena besaran yang menggerakkan alokasi adalah hasil perkalian. RowsPerStrip * Width * SamplesPerPixel bisa overflow dalam aritmetika 32-bit jauh sebelum salah satu sisi mencapai batasnya sendiri, dan bahkan tanpa overflow ia bisa menyebut alokasi yang tak seharusnya dicoba layanan mana pun. PDFlibPas menghitung byte baris dalam Int64 dan menegakkan tiga plafon sekaligus: 65.535 per dimensi, 32 komponen warna, dan 256 MiB byte terdekode

const
  PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
  PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
  PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;

// di dalam TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
   (RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
   (Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
  Exit(False);

DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
   (DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
   (DecodedBytes > MaxInt) then
  Exit(False);

Tiga detail di sana lebih penting daripada konstantanya sendiri. Uji tinggi ditulis sebagai pembagian, bukan perkalian, sehingga produk yang terlalu besar tidak pernah terbentuk sama sekali. RowsPerStrip di bawah 1 atau di atas tinggi gambar dinormalisasi ke tinggi terlebih dahulu, yang sudah tersirat dalam pembacaan single-strip pada TIFF 6.0 dan menghentikan tag jahat dari menggelembungkan buffer strip. Dan rutinitasnya bersifat bersama: ValidatePageForDecode berjalan di akhir parsing tag dan lagi di pintu masuk SimpleExtract maupun CompDecode, sehingga kode yang mencapai dekoder secara langsung tidak bisa memutar menghindari anggaran. Itu aturan yang sama yang diikuti PDFlibPas saat mem-parsing graf objek PDF yang tidak tepercaya, karena batasan yang ditegakkan di satu dari tiga pintu bukanlah batasan

PDFlibPas mengukur setiap buffer TIFF lewat aritmetika Int64, menguji tinggi gambar dengan pembagian sehingga produk yang terlalu besar tidak pernah terbentuk, dan menjalankan rutinitas ValidatePageForDecode yang sama di parsing tag dan di kedua pintu masuk dekoder
Tiga plafon, aritmetika byte baris Int64, dan satu rutinitas validasi bersama yang dicapai dari ketiga pintu, karena batasan yang ditegakkan di satu dari tiga pintu bukanlah batasan

Apa yang harus diperiksa pemanggil sebelum membaca PageInfo?

Periksa ValidTIFF lebih dulu, lalu PageCount, dan baru kemudian indeks PageInfo. Berkas yang ditolak bisa meninggalkan PageCount di nol, dan GetPageInfo menjawab indeks di luar jangkauan dengan record TTIFFPage yang belum diinisialisasi, sehingga jalur error yang membaca resolusi atau jumlah sampel di tengah jalan menuju pelaporannya malah membaca noise. Versi 3.534.1 memperbaiki kedua pemanggil di dalam pustaka: jalur impor gambar membaca XRes dan YRes hanya di dalam cabang valid, dan TPDFlib.GetImagePageCount mensyaratkan ValidTIFF alih-alih memercayai jumlah halaman non-nol dengan sendirinya. Di hilir, argumen Options milik AddImageFromFile adalah nomor halaman berbasis 1 untuk TIFF multi-halaman, sehingga GetImagePageCount harus bisa dipercaya sebelum loop dimulai, bukan setelahnya. Nol halaman kini adalah jawaban nyata yang berarti "tidak ada yang bisa didekode di sini", bukan kecelakaan dari return dini, yang paling berarti saat Anda menyusun dan menyisipkan batch pemindaian dupleks dan satu lembar yang terdekode salah secara sunyi akan mendarat di posisi yang keliru

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // header buruk, BigTIFF, tata letak tiled, atau melebihi anggaran
    Pdf.NewDocument;
    for I := 1 to Pages do
    begin
      Pdf.NewPage;
      ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
      if ImageID > 0 then
      begin
        Pdf.SelectImage(ImageID);
        Pdf.DrawImage(0, 0, 595, 842);
      end;
    end;
    Pdf.SaveToFile('scan-0417.pdf');
  finally
    Pdf.Free;
  end;
end;

Membangun dekoder sendiri atau menautkan libtiff?

PDFlibPas mempertahankan dekoder bawaan, dan faktor penentunya adalah jangkauan platform, bukan siapa penulisnya. Kira-kira 1.873 baris Object Pascal terkompilasi di mana pun kompilator pergi: Win32, Win64, macOS, iOS, Android, dan FPC di Linux. libtiff 4.7.1 berjumlah sekitar 30.000 baris C yang tersebar di 34 unit translasi tif_*.c, dan file objek jadi yang ada hari ini hanya mencakup Windows. Mengadopsinya berarti menukar cakupan TIFF yang lengkap dengan daftar platform yang didukung yang menyusut ke mesin apa pun yang bisa menjalankan toolchain C, plus satu lintasan linker yang belum pernah dilalui siapa pun

Apa yang hilang karena itu layak dinyatakan tanpa dibungkus-bungkus. Dekoder bawaan menangani apa yang benar-benar dihasilkan pekerjaan dokumen hasil pindai: CCITT Group 3 satu dimensi dan dua dimensi, Group 4, LZW, Deflate, PackBits, dan JPEG-in-TIFF, lintas fotometrik WhiteIsZero, BlackIsZero, RGB, palet, dan CMYK dengan Predictor 1 dan 2. Payload-payload itu sejalan dengan filter PDF dalam ISO 32000-1 §7.4.4 dan §7.4.6, itulah sebabnya front-end TIFF membawa bobot besar dalam pipeline pemindaian. Yang tidak ditangani adalah BigTIFF, tile, Predictor 3 floating-point, PixarLog dan SGILog, kompresi JPEG gaya lama 6, serta piramida sub-IFD. Sejak 3.534.1, setiap hal itu adalah penolakan bernama alih-alih gambar yang salah, dan pustaka menyimpan daftar pemicu tertulis untuk membuka kembali keputusan libtiff:

  • seorang pelanggan melaporkan berkas BigTIFF dan butuh dukungan native alih-alih langkah konversi
  • seorang pelanggan melaporkan TIFF tiled dari sumber medis, GIS, atau industri dan butuh didekode langsung di tempat
  • seorang pelanggan melaporkan TIFF Predictor 3 floating-point
  • sebuah kerentanan yang dipublikasikan menghantam jalur dekode CCITT atau LZW bawaan
  • argumen lintas platform berhenti berlaku, entah karena dukungan macOS, iOS, dan Android dihentikan, atau karena integrasi libtiff yang dapat dipakai ulang sudah mencakup macOS dan Linux

Migrasinya sendiri terukur, bukan hipotesis: conditional USE_LIBTIFF akan menjaga permukaan publik TPDFTIFF tetap utuh, mengarahkan LoadFromStream melalui TIFFClientOpen dengan callback stream, dan membiarkan parser Pascal sebagai fallback non-Windows. Sampai salah satu pemicu itu benar-benar terjadi, memelihara dua dekoder dan matriks uji yang berlipat tidak membeli apa pun yang bisa dirasakan pelanggan. Menunda biaya dengan rute pelarian yang sudah tertulis adalah hal yang berbeda dari mengabaikannya

Di mana posisi ini dalam pipeline dokumen hasil pindai

Perlakukan TPDFTIFF sebagai gerbang, bukan konverter. Muat berkasnya, baca ValidTIFF, dan catat LastError apa adanya setiap kali bernilai false, karena string itu kini adalah rute terpendek dari laporan lapangan ke diagnosis. Berkas yang gagal di gerbang tetap dapat diselamatkan dengan mengonversinya di hulu, yang merupakan jawaban praktis untuk sumber BigTIFF dan tiled hari ini. Untuk input di luar TIFF sepenuhnya, PDFlibPas mengambil rute terpisah melalui jalur input gambar AVIF, HEIF dan JPEG XL-nya, sehingga pertanyaan dekoder mana yang memiliki format mana tetap eksplisit, bukan muncul sendiri

Semua ini berada di balik API gambar biasa, sehingga pipeline dokumen mendapatkan batas yang lebih ketat tanpa mengubah satu baris kode pemanggil pun, di luar memeriksa jumlah halaman yang memang seharusnya sudah diperiksa. Jika Anda sedang menimbang jalur TIFF-ke-PDF native untuk Delphi atau C++Builder, komponen lengkap beserta penanganan gambarnya terdokumentasi di halaman PDF Library for Delphi