Teknik Makale

Delphi TIFF Çözücü Sıkılaştırma: BigTIFF ve Tile TIFF

PDFlibPas, TIFF dosyalarını libtiff bağlantısı yerine elle yazılmış bir Object Pascal ayrıştırıcıyla çözer ve 3.534.1 sürümü, bu ayrıştırıcının girdiyi reddettiği noktaları tam olarak sıkılaştırdı. BigTIFF magic 43 artık adıyla reddediliyor, TileOffsets ve TileByteCounts etiket ayrıştırması sırasında geri çevriliyor ve her arabellek 256 MiB çözüm tavanı altında Int64 aritmetiğiyle boyutlandırılıyor

Kapatılan bu kusur laboratuvarda hiç ortaya çıkmaz. Ortaya çıktığı yer, üç yıldır sessizce çalışan bir tarama ağ geçididir; ta ki bir müşteri coğrafi bir arşiv dosyasını ya da whole-slide bir tıbbi görüntüyü ona yönlendirene kadar. Dosyanın meşru bir TIFF başlığı vardır. Ayrıştırma da başarıyla geçer. Dışarı çıkan şey ise çizgili bir gürültü sayfası ya da servisi düşüren çok gigabaytlı bir bellek ayrımıdır ve süreç boyunca girdinin geçersiz olduğu kimse tarafından bildirilmez. Mühendisliğe değer kusur şekli budur: çökme değil, kendinden emin biçimde teslim edilen yanlış bir cevap

II ya da MM, elinizde klasik bir TIFF olduğunu kanıtlar mı?

Çünkü bayt sırası işaretçisi her iki lehçe tarafından da paylaşılır. Klasik TIFF de BigTIFF de II ya da MM ile açılır ve onları gerçekten ayıran alan, hemen ardından gelen 16 bitlik magic değerdir: TIFF 6.0 belirtiminde tanımlanan klasik TIFF için 42, 64 bitlik ofsetleriyle BigTIFF için 43. FValidTIFF := PopWord = 42 olarak yazılmış bir yükleyici klasik TIFF konusunda yanlış değildir, ama iki çok farklı reddi tek bir sessiz boole değere indirger; sonuçta BigTIFF, adı değiştirilmiş kesik bir JPEG'den ayırt edilemez hâle gelir. PDFlibPas artık durumları ayırır ve her birini TPDFTIFF.LastError içine kaydeder: dört bayttan kısa bir başlık, geçersiz bir bayt sırası işaretçisi, magic 43 ve diğer tüm magic değerleri birbirinden farklı metinler üretir. Kütüphane hâlâ BigTIFF çözümlemiyor ve bunu açıkça söylemek asıl mesele. Çağıran kod, "bu bir TIFF değil" ile "bu, yerleşik çözücünün uygulamadığı 64 bitlik ofset düzenine sahip bir TIFF" arasındaki farkı alır; bu da tek cevapta kapatılan bir destek kaydı ile bir haftalık tahmin oyununa dönüşen kayıt arasındaki farktır

PDFlibPas TIFF yükleyicisi bayt sırası işaretçisini ve 16 bitlik magic değerini ayrı ayrı okur; böylece kısa başlık, geçersiz işaretçi, BigTIFF magic 43 ve diğer magic değerleri tek sessiz boole yerine birbirinden farklı LastError metinleri üretir
Klasik TIFF ve BigTIFF aynı bayt sırası işaretçisiyle açılır; bu yüzden PDFlibPas dört reddi durumunu ayırır ve her birini LastError içinde adlandırır
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'lar farklı bir geometridir, bir ofset dizisi daha değildir

PDFlibPas tile'lı TIFF'i etiket ayrıştırması sırasında, hiç piksel verisine dokunulmadan önce reddeder. Hatayı davet eden kestirme yol kolayca görünür: 324 numaralı etiket (TileOffsets) ile 325 numaralı etiket (TileByteCounts), dosya ofsetlerinin ve bayt sayılarının dizileridir ve yapısal olarak strip dizileriyle aynıdır; bu yüzden mevcut strip alanlarını onlara yönlendirmek iki satır sürer ve temiz derlenir. Ama yanlıştır. Tile'lar, dolgulu kenar bloklarıyla iki boyutlu bir ızgara oluşturur, her tile içinde kendine ait satır adımı vardır ve TIFF 6.0 tiled-image bölümünün açıkça anlattığı gibi hiçbir RowsPerStrip semantiği taşımaz. Tile verilerini bir strip çözücüsüne vermek bu nedenle gürültüyle başarısız olmaz. SimpleExtract ve CompDecode veriyi yanlış adımla gezinir ve doğru boyutlu, yanlış piksellik bir görüntü üretir. Eski kod bunu daha da büyüttü: TTIFFPage içinde StripsAreTiles, ColumnsPerTile ve RowsPerTile tutuyordu; arkasında tile birleştiricisi olmayan bir çözücünün kaydettiği tile geometrisi. 3.534.1 sürümünde 324 ve 325 etiketlerinin işleyicileri tile hatasını yükseltir ve IFD'yi hemen terk eder; böylece red, haftalar sonra bir render şikâyeti olarak ortaya çıkmak yerine "tiled" kelimesini taşır

PDFlibPas, tam genişlikte bantların tek satır adımı paylaştığı strip düzenini, dolgulu kenar bloklarından oluşan iki boyutlu bir ızgara olan tile düzeniyle karşılaştırır ve hiç piksel verisine dokunulmadan, etiket ayrıştırma sırasında 324 ve 325 etiketlerini reddeder
Tile dizileri yapısal olarak strip dizileriyle aynı görünür; strip çözücüsüne verildiklerinde doğru boyutlu, yanlış piksellik görüntü üretmelerinin sebebi budur

Tek boyut sınırı bir bellek bütçesi değildir

Genişlik ve yüksekliği ayrı ayrı 65.535 ile sınırlamak gereklidir ama hiçbir yeterde değildir, çünkü ayrımı belirleyen büyüklük bir çarpımdır. RowsPerStrip * Width * SamplesPerPixel çarpımı, taraflardan herhangi biri kendi sınırına ulaşmadan çok önce 32 bitlik aritmetiği aşabilir ve taşmasa bile hiçbir hizmetin denememesi gereken bir ayırım isteyebilir. PDFlibPas satır baytlarını Int64 ile hesaplar ve üç tavani birlikte uygular: boyut başına 65.535, 32 renk bileşeni ve çözülmüş 256 MiB bayt

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

// TPDFTIFF.ValidatePageForDecode içinde
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);

Oradaki üç ayrıntı, sabitlerin kendisinden daha önemlidir. Yükseklik testi çarpım yerine bölme yazılarak kurulmuştur; böylece abartılı çarpım hiçbir zaman oluşmaz. 1'in altındaki ya da görüntü yüksekliğinin üstündeki bir RowsPerStrip önce yüksekliğe normalize edilir; bu, TIFF 6.0 single-strip okumasının zaten ima ettiği şeydir ve düşmanca bir etiketin strip arabelleğini şişirmesini engeller. Yordam ayrıca paylaşılır: ValidatePageForDecode etiket ayrıştırmasının sonunda ve yine SimpleExtract ile CompDecode girişlerinde çalışır; böylece çözücüye doğrudan ulaşan kod bütçenin etrafından dolaşamaz. Bu, PDFlibPas'ın güvenilmeyen PDF nesne grafiklerini ayrıştırırken izlediği kuraldır; çünkü üç kapıdan sadece birinde uygulanan bir sınır, sınır değildir

PDFlibPas her TIFF arabelleğini Int64 aritmetiğiyle boyutlandırır, görüntü yüksekliğini bölmeyle sınar ki abartılı çarpım hiç oluşmasın ve aynı ValidatePageForDecode yordamını etiket ayrıştırmada ve iki çözücü girişinde çalıştırır
Üç tavan, Int64 satır bayt aritmetiği ve üç kapıdan da ulaşılabilen tek paylaşılan doğrulama yordamı; çünkü üç kapıdan birinde uygulanan sınır, sınır değildir

PageInfo'yu okumadan önce çağıran kod neyi sınamalı?

Önce ValidTIFF'i sınamalı, sonra PageCount'e bakmalı ve ancak ondan sonra PageInfo'ya indeksle erişmelisiniz. Reddedilen bir dosya PageCount'i sıfırda bırakabilir ve GetPageInfo, aralık dışı indekse başlatılmamış bir TTIFFPage kaydıyla cevap verir; böylece hatayı raporlarken yol üstünde çözünürlük ya da örnek sayısı okuyan bir hata yolu gürültü okuyarak biter. 3.534.1 sürümü kütüphane içindeki iki çağırıcıyı da düzeltti: görüntü içe aktarma yolu XRes ve YRes'i yalnızca geçerli dal içinde okuyor ve TPDFlib.GetImagePageCount, sıfırdan farklı sayfa sayısına kendi başına güvenmek yerine ValidTIFF istiyor. Aşağı akışta AddImageFromFile'un Options argümanı, çok sayfalı TIFF için 1 tabanlı sayfa numarasıdır; dolayısıyla GetImagePageCount döngü bittikten sonra değil, başlamadan önce güvenilir olmak zorunda. Sıfır sayfa artık bir erken dönüş kazası değil, "burada çözümlenebilir bir şey yok" anlamına gelen gerçek bir cevaptır; bu da dubleks tarama gruplarını harmanlayıp araya yerleştirirken en çok işe yarar, çünkü sessizce yanlış çözülmüş tek bir sayfa yanlış konuma düşer

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // hatalı başlık, BigTIFF, tile düzeni ya da bütçe aşımı
    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;

Çözücüyü mü yazmalı, libtiff'i mi bağlamalı?

PDFlibPas yerleşik çözücüyü tutuyor ve belirleyici etken yazarlık değil platform erişimidir. Yaklaşık 1.873 satırlık Object Pascal, derleyicinin gittiği her yerde derlenir: Win32, Win64, macOS, iOS, Android ve Linux üzerinde FPC. libtiff 4.7.1 ise 34 tif_*.c çeviri birimine yayılmış yaklaşık 30.000 satırlık C kodudur ve bugün var olan önceden derlenmiş nesne dosyaları yalnızca Windows'u kapsar. Onu benimsemek, eksiksiz TIFF kapsamını; C araç zincirini çalıştırabilen makinelerle daralan bir desteklenen platform listesine ve kimsenin henüz yürüyüp görmediği bir bağlayıcı adımına takas etmek olur

Bunun maliyetini süslemeden yazmak gerekir. Yerleşik çözücü, taranmış belge işinin gerçekte ürettiğini karşılar: tek boyutlu ve iki boyutlu CCITT Group 3, Group 4, LZW, Deflate, PackBits ve TIFF içinde JPEG; WhiteIsZero, BlackIsZero, RGB, palet ve CMYK fotometrikleriyle Predictor 1 ve 2. Bu veri biçimleri ISO 32000-1 §7.4.4 ve §7.4.6'daki PDF filtreleriyle birebir örtüşür; TIFF ön ucu bir tarama hattında bu yüzden bu kadar ağır taşınır. Karşılamadığı şey ise BigTIFF, tile'lar, kayan noktalı Predictor 3, PixarLog ve SGILog, eski tarz JPEG sıkıştırma 6 ve alt-IFD piramitleridir. 3.534.1 sürümünden bu yana bunların her biri yanlış bir görüntü yerine adı konmuş bir redtir ve kütüphane, libtiff kararını yeniden açmak için yazılı bir tetikleyici listesi tutar:

  • bir müşteri BigTIFF dosya bildirir ve dönüştürme adımı yerine yerel destek ister
  • bir müşteri tıbbi, GIS ya da endüstriyel kaynaklardan tile'lı TIFF bildirir ve dosyanın yerinde çözümlenmesini ister
  • bir müşteri kayan noktalı Predictor 3 TIFF bildirir
  • yayımlanmış bir güvenlik açığı yerleşik CCITT ya da LZW çözüm yollarına isabet eder
  • platformlar arası gerekçe geçersiz hâle gelir; ya macOS, iOS ve Android desteği kaldırıldığı için ya da yeniden kullanılabilir bir libtiff bütünleştirmesi macOS ve Linux'u zaten kapsadığı için

Göçün kendisi kurmaca değil, kapsamlıdır: bir USE_LIBTIFF koşullu derlemesi TPDFTIFF'in genel yüzeyini olduğu gibi tutar, LoadFromStream'i akış geri çağırmalarıyla TIFFClientOpen'a yönlendirir ve Pascal ayrıştırıcısını Windows dışı yedek olarak bırakır. Bu tetikleyicilerden biri gerçekten ateşlenene kadar iki çözücü ve ikiye katlanmış test matrisi sürdürmek, müşterinin hissedebileceği hiçbir şey satın almaz. Kaçış yolu yazılı hâlde beklerken bir maliyeti ertelemek, onu görmezden gelmekten farklı bir şeydir

Bunlar taranmış belge hattını nereye bırakıyor?

TPDFTIFF'e dönüştürücü değil kapı gibi davranın. Dosyayı yükleyin, ValidTIFF'i okuyun ve yanlış olduğunda LastError'i olduğu gibi günlüğe yazın; çünkü bu dize artık saha raporundan teşhise giden en kısa yoldur. Kapıdan geçemeyen dosyalar yukarı akışta dönüştürülerek kurtarılabilir; bugün BigTIFF ve tile'lı kaynaklar için pratik cevap budur. TIFF'in tamamen dışındaki girdiler için PDFlibPas, AVIF, HEIF ve JPEG XL görüntü girişi yolundan ayrı bir rota izler; hangi çözücünün hangi biçimi sahiplendiği sorusu böylece kendiliğinden değil, açıkça cevaplanır

Bütün bunlar sıradan görüntü API'sinin arkasındadır; böylece bir belge hattı, zaten sınaması gereken sayfa sayısını sınamanın ötesinde tek satır çağırma kodu değiştirmeden daha sıkı sınırı kazanır. Delphi ya da C++Builder için yerel bir TIFF'ten PDF'e yol tartışıyorsanız, bileşenin tamamı ve görüntü işleme kısmı PDF Library for Delphi sayfasında belgelenmiştir