Tehnički članak

Ojačanje Delphi TIFF dekodera: BigTIFF i tiled TIFF

PDFlibPas dekodira TIFF ručno pisanim Object Pascal parserom umesto libtiff povezivanja, a verzija 3.534.1 zaoštrava upravo tamo gde taj parser odbija ulaz. BigTIFF magic 43 se sada odbija po imenu, TileOffsets i TileByteCounts se odbijaju tokom parsiranja tagova, a svaki bafer se dimenzioniše Int64 aritmetikom ispod plafona dekodiranja od 256 MiB

Greška koju ovo zatvara nikada se ne pokazuje u laboratoriji. Pojavi se kao skenirajući gateway koji je tri godine mirno radio, sve dok klijent kroz njega ne usmeri geoprostorni arhivski fajl ili whole-slide medicinsku sliku. Fajl ima legitimno TIFF zaglavlje. Parsira se. Ono što izađe je stranica prugastog šuma ili alokacija od više gigabajta koja obara servis, i ništa duž puta nije proglasilo ulaz neispravnim. To je oblik greške vredan inženjerske odbrane: ne pad sistema, već pogrešan odgovor dat sa sigurnošću

Zašto II ili MM ne dokazuje da imate klasični TIFF?

Jer marker redosleda bajtova dele oba dijalekta. Klasični TIFF i BigTIFF oba počinju sa II ili MM, a polje koje ih stvarno razlikuje je 16-bitni magic neposredno posle njega: 42 za klasični TIFF definisan u specifikaciji TIFF 6.0, 43 za BigTIFF sa svojim 64-bitnim ofsetima. Loader napisan kao FValidTIFF := PopWord = 42 nije u krivu za klasični TIFF, ali spaja dva veoma različita odbijanja u jedan tih boolean, pa BigTIFF postaje nerazlikoviv od skraćenog JPEG-a kome je neko samo promenio ekstenziju. PDFlibPas sada razdvaja slučajeve i svaki beleži u TPDFTIFF.LastError: zaglavlje kraće od četiri bajta, neispravan marker redosleda bajtova, magic 43 i svaka druga vrednost magije daju različite tekstove. Biblioteka i dalje ne dekodira BigTIFF, i direktno rečeno to je poenta. Pozivaoc dobija razliku između „ovo nije TIFF" i „ovo je TIFF čiji 64-bitni raspored ofseta ugrađeni dekoder ne implementira", a to je razlika između support tiketa na koji odgovorite u jednom mejlu i onog koji se pretvori u nedelju nagađanja

PDFlibPas TIFF loader čita marker redosleda bajtova i 16-bitni magic odvojeno, pa kratko zaglavlje, neispravan marker, BigTIFF magic 43 i svaka druga vrednost magije daju različite LastError tekstove umesto jednog tihog booleana
Klasični TIFF i BigTIFF počinju istim markerom redosleda bajtova, pa PDFlibPas razdvaja četiri slučaja odbijanja i svaki imenuje u 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-ovi su druga geometrija, ne još jedan niz ofseta

PDFlibPas odbija tiled TIFF tokom parsiranja tagova, pre nego što bilo koji pikselni podatak bude dotaknut. Kratki put koji poziva grešku lako je videti: tag 324 (TileOffsets) i tag 325 (TileByteCounts) su nizovi ofseta fajla i brojača bajtova, strukturno identični strip nizovima, pa usmeravanje postojećih strip polja na njih košta dve linije i čisto se kompajlira. Ali je pogrešno. Tile-ovi formiraju dvodimenzionalnu mrežu sa dopunjenim ivičnim blokovima, sopstvenim korakom reda unutar svakog tile-a i bez ijedne RowsPerStrip semantike, kako to razjašnjava sekcija o tiled slikama u TIFF 6.0. Davanje tile sadržaja strip dekoderu zato ne propada glasno. SimpleExtract i CompDecode prelaze podatke pogrešnim korakom i izbacuju sliku sa pravim dimenzijama i pogrešnim pikselima. Stariji kod je to pojačao time što je u TTIFFPage držao StripsAreTiles, ColumnsPerTile i RowsPerTile: geometriju tile-a zabeleženu u dekoderu iza koga ne stoji nijedan tile assembler. U 3.534.1 handleri za tagove 324 i 325 dižu tile grešku i odmah napuštaju IFD, pa odbijanje nosi reč „tiled" umesto da iskoči nedeljama kasnije kao žalba na renderovanje

PDFlibPas upoređuje strip raspored, gde trake pune širine dele jedan korak reda, sa tile rasporedom, dvodimenzionalnom mrežom dopunjenih ivičnih blokova, i odbija tagove 324 i 325 tokom parsiranja tagova pre nego što bilo koji pikselni podatak bude dotaknut
Tile nizovi izgledaju strukturno identično strip nizovima, zbog čega njihovo davanje strip dekoderu daje prave dimenzije i pogrešne piksele

Jedno ograničenje dimenzije nije memorijski budžet

Ograničavanje širine i visine na 65.535 svaka je neophodno i nimalo dovoljno, jer veličina koja pokreće alokaciju je proizvod. RowsPerStrip * Width * SamplesPerPixel može preliti 32-bitnu aritmetiku davno pre nego što bilo koja strana dostigne sopstvenu granicu, a i bez prekoračenja može tražiti alokaciju koju nijedan servis ne treba da pokuša. PDFlibPas računa bajtove reda u Int64 i primenjuje tri plafona zajedno: 65.535 po dimenziji, 32 komponente boje i 256 MiB dekodiranih bajtova

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

// unutar 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);

Tri detalja tu su važnija od samih konstanti. Test visine je napisan kao deljenje umesto množenja, pa preveliki proizvod nikada ni ne nastaje. RowsPerStrip ispod 1 ili iznad visine slike se prvo normalizuje na visinu, što je čitanje jednog stripa koje TIFF 6.0 već podrazumeva i što sprečava neprijateljski tag da napuše strip bafer. A rutina je deljena: ValidatePageForDecode se izvršava na kraju parsiranja tagova i ponovo na ulazu i u SimpleExtract i u CompDecode, pa kod koji dolazi direktno do dekodera ne može zaobići budžet. To je isto pravilo koje PDFlibPas primenjuje kada parsira nepoverljive grafove PDF objekata, jer granica primenjena na jedna od tri vrata nije granica

PDFlibPas dimenzioniše svaki TIFF bafer Int64 aritmetikom, testira visinu slike deljenjem tako da preveliki proizvod nikada ne nastane, i izvršava istu rutinu ValidatePageForDecode pri parsiranju tagova i na oba ulaza u dekoder
Tri plafona, Int64 aritmetika bajtova reda i jedna deljena validaciona rutina do koje se dolazi sa sva tri vrata, jer granica primenjena na jedna od tri vrata nije granica

Šta pozivaoc mora proveriti pre čitanja PageInfo?

Prvo proverite ValidTIFF, zatim PageCount, i tek onda indeksirajte PageInfo. Odbijen fajl može ostaviti PageCount na nuli, a GetPageInfo odgovara na indeks van opsega neinicijalizovanim TTIFFPage zapisom, pa error putanja koja usput čita rezoluciju ili brojeve uzoraka na kraju čita šum. Verzija 3.534.1 je popravila oba pozivaoca unutar biblioteke: putanja uvoza slike čita XRes i YRes samo unutar validne grane, a TPDFlib.GetImagePageCount zahteva ValidTIFF umesto da sama veruje nenultom broju stranica. Nizvodno, Options argument AddImageFromFile je broj stranice od 1 za višestranični TIFF, pa GetImagePageCount mora biti pouzdan pre nego što petlja krene, a ne posle nje. Nula stranica je sada pravi odgovor koji znači „ovde ništa nije dekodabilno", a ne nesreća ranog izlaska, što je najvažnije kada sređujete i preplićete duplex skenirane serije i jedna tiho pogrešno dekodirana stranica bi završila na pogrešnom mestu

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // loše zaglavlje, BigTIFF, tiled raspored ili preko budžeta
    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;

Napisati dekoder ili povezati libtiff?

PDFlibPas zadržava ugrađeni dekoder, a odlučujući faktor je domet po platformama, a ne autorstvo. Oko 1.873 linije Object Pascal-a se kompajliraju gde god kompajler ide: Win32, Win64, macOS, iOS, Android i FPC na Linuxu. libtiff 4.7.1 je oko 30.000 linija C-a rasutih preko 34 tif_*.c prevodne jedinice, a prekompajlirani objektni fajlovi koji danas postoje pokrivaju samo Windows. Njegovo usvajanje bi zamenilo kompletnu TIFF pokrivenost listom podržanih platformi koja se skuplja na mašine koje mogu da pokrenu C toolchain, plus linker prolaz koji niko još nije prošao

Šta to košta vredi je reći bez uljepšavanja. Ugrađeni dekoder podržava ono što rad sa skeniranim dokumentima stvarno proizvodi: CCITT Group 3 jednodimenzionalni i dvodimenzionalni, Group 4, LZW, Deflate, PackBits i JPEG-in-TIFF, preko WhiteIsZero, BlackIsZero, RGB, paletnih i CMYK photometric režima sa Predictor 1 i 2. Ti podaci se poklapaju sa PDF filterima u ISO 32000-1 §7.4.4 i §7.4.6, i zato TIFF front-end nosi toliki značaj u skenirajućem pipeline-u. Ono što ne podržava je BigTIFF, tile-ovi, floating-point Predictor 3, PixarLog i SGILog, stara JPEG kompresija 6 i sub-IFD piramide. Od 3.534.1 svako od toga je imenovano odbijanje umesto pogrešne slike, a biblioteka vodi pisani spisak okidača za ponovno otvaranje libtiff odluke:

  • klijent prijavi BigTIFF fajl i traži nativnu podršku umesto koraka konverzije
  • klijent prijavi tiled TIFF iz medicinskih, GIS ili industrijskih izvora i traži dekodiranje na mestu
  • klijent prijavi floating-point Predictor 3 TIFF
  • objavljena ranjivost pogodi ugrađene CCITT ili LZW dekod putanje
  • cross-platform argument prestane da važi, bilo zato što je podrška za macOS, iOS i Android ukinuta, bilo zato što reusable libtiff integracija već pokriva macOS i Linux

Sama migracija ima definisan opseg, a nije hipotetička: USE_LIBTIFF uslovni blok bi zadržao TPDFTIFF javnu površinu netaknutom, usmerio LoadFromStream kroz TIFFClientOpen sa stream callbacks i ostavio Pascal parser kao ne-Windows fallback. Dok jedan od tih okidača stvarno ne opali, održavanje dva dekodera i udvostručene test matrice ne kupuje ništa što klijent može osetiti. Odlaganje troška čija je izlazna putanja već zapisana je druga stvar od ignorisanja

Gde ovo ostavlja pipeline skeniranih dokumenata

Tretirajte TPDFTIFF kao kapiju, a ne konverter. Učitajte fajl, pročitajte ValidTIFF, i logujte LastError doslovno kad god je false, jer je taj string sada najkraći put od terenske prijave do dijagnoze. Fajlovi koji padnu na kapiji ostaju spasiljivi konverzijom uzvodno, što je danas praktičan odgovor za BigTIFF i tiled izvore. Za ulaz potpuno van TIFF-a, PDFlibPas ide posebnim putem kroz svoju AVIF, HEIF i JPEG XL putanju unosa slika, pa pitanje koji dekoder poseduje koji format ostaje eksplicitno, a ne naslućeno

Sve ovo stoji iza običnog image API-ja, pa pipeline dokumenata dobija strožu granicu bez menjanja ijedne linije pozivajućeg koda osim provere broja stranica koji je već trebalo da proverava. Ako razmatrate nativni TIFF-to-PDF put za Delphi ili C++Builder, kompletna komponenta i njena obrada slika su dokumentovani na stranici PDF Library for Delphi