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
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
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
Š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