PDFlibPas dekoduje TIFF ręcznie napisanym parserem w Object Pascal, a nie wiązaniem z libtiff, i wersja 3.534.1 zaostrzyła dokładnie te miejsca, w których parser odmawia przyjęcia danych wejściowych. BigTIFF o magicznej liczbie 43 jest teraz odrzucany z podaniem nazwy, TileOffsets i TileByteCounts są odrzucane przy parsowaniu tagów, a każdy bufor jest wymiarowany arytmetyką Int64 pod sufitem dekodowania 256 MiB
Defekt, który to zamyka, nigdy nie ujawnia się w laboratorium. Ujawnia się jako brama skanowania, która przez trzy lata pracowała po cichu, aż klient puści przez nią archiwum geoprzestrzenne albo medyczny obraz całego szkiełka. Plik ma legalny nagłówek TIFF. Parsuje się. Na wyjściu jest strona prążkowanego szumu albo wielogigabajtowa alokacja, która kładzie usługę, a nic po drodze nigdy nie zadeklarowało danych wejściowych jako nieprawidłowych. To jest kształt awarii, przeciw któremu warto inżynierować: nie crash, lecz pewnie dostarczona błędna odpowiedź
Dlaczego II albo MM nie dowodzą, że masz klasyczny TIFF?
Bo znacznik kolejności bajtów jest wspólny dla obu dialektów. Klasyczny TIFF i BigTIFF otwierają się od II albo MM, a pole, które faktycznie je rozróżnia, to 16-bitowa magiczna liczba bezpośrednio za nim: 42 dla klasycznego TIFF ze specyfikacji TIFF 6.0, 43 dla BigTIFF z jego 64-bitowymi offsetami. Wczytywarka napisana jako FValidTIFF := PopWord = 42 nie myli się co do klasycznego TIFF, ale zlepia dwa zupełnie różne odrzucenia w jeden cichy boolean, więc BigTIFF staje się nieodróżnialny od obciętego JPEG, który ktoś przemianował. PDFlibPas rozdziela teraz te przypadki i zapisuje każdy z nich w TPDFTIFF.LastError: nagłówek krótszy niż cztery bajty, nieprawidłowy znacznik kolejności bajtów, magiczna liczba 43 i każda inna magiczna wartość dają odrębny tekst. Biblioteka nadal nie dekoduje BigTIFF i powiedzenie tego wprost jest sednem sprawy. Wywołujący dostaje różnicę między „to nie jest TIFF" a „to jest TIFF, którego 64-bitowego układu offsetów wbudowany dekoder nie implementuje", czyli różnicę między zgłoszeniem wsparcia, na które odpowiesz w jednej wiadomości, a takim, które zamienia się w tydzień zgadywania
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;
Kafelki to inna geometria, nie kolejna tablica offsetów
PDFlibPas odrzuca kafelkowy TIFF już przy parsowaniu tagów, zanim cokolwiek dotknie danych pikselowych. Upraszczający skrót, który zaprasza ten błąd, łatwo dostrzec: tag 324 (TileOffsets) i tag 325 (TileByteCounts) to tablice offsetów plikowych i liczników bajtów, strukturalnie identyczne z tablicami pasów, więc skierowanie istniejących pól pasów na nie kosztuje dwie linie i kompiluje się bez zgrzytu. Jest to też błędne. Kafelki tworzą dwuwymiarową siatkę z wypełnionymi blokami brzegowymi, mają własny skok wiersza wewnątrz każdego kafelka i nie mają żadnej semantyki RowsPerStrip, co jasno opisuje sekcja obrazów kafelkowych TIFF 6.0. Podanie danych kafelków dekoderowi pasów nie kończy się zatem głośną porażką. SimpleExtract i CompDecode przechodzą dane ze złym skokiem i emitują obraz o właściwych wymiarach i złych pikselach. Starszy kod pogłębiał to, trzymając w TTIFFPage pola StripsAreTiles, ColumnsPerTile i RowsPerTile: geometrię kafelków zapisywaną przez dekoder, za którym nie stoi żaden składacz kafelków. W 3.534.1 procedury obsługi tagów 324 i 325 podnoszą błąd kafelków i natychmiast porzucają IFD, więc odmowa niesie słowo „kafelki", zamiast wynurzać się tygodniami później jako reklamacja renderingu
Jedno ograniczenie wymiaru to nie budżet pamięci
Przycięcie szerokości i wysokości do po 65 535 jest konieczne i ani z bliska wystarczające, bo wielkością, która napędza alokację, jest iloczyn. RowsPerStrip * Width * SamplesPerPixel potrafi przepełnić arytmetykę 32-bit na długo zanim któraś strona osiągnie własny limit, a nawet bez przepełnienia potrafi wskazać alokację, której żadna usługa nie powinna podejmować. PDFlibPas liczy bajty wiersza na Int64 i egzekwuje trzy sufity naraz: 65 535 na wymiar, 32 komponenty barwne i 256 MiB zdekodowanych bajtów
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// wewnątrz 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);
Trzy detale mają tam większe znaczenie niż same stałe. Test wysokości jest zapisany jako dzielenie, a nie mnożenie, więc przewymiarowany iloczyn nigdy w ogóle nie powstaje. RowsPerStrip mniejszy od 1 albo większy od wysokości obrazu jest najpierw normalizowany do wysokości, co odpowiada odczytowi jednopasmowemu, który TIFF 6.0 już implikuje, i blokuje wrogi tag przed napompowaniem bufora pasów. A procedura jest wspólna: ValidatePageForDecode uruchamia się na końcu parsowania tagów i ponownie na wejściu do SimpleExtract oraz CompDecode, więc kod docierający bezpośrednio do dekodera nie może ominąć budżetu. To ta sama zasada, której PDFlibPas pilnuje przy parsowaniu niezaufanych grafów obiektów PDF, bo limit wyegzekwowany przy jednych drzwiach z trzech nie jest limitem
Co wywołujący musi sprawdzić przed sięgnięciem po PageInfo?
Najpierw sprawdź ValidTIFF, potem PageCount, a dopiero potem indeksuj PageInfo. Odrzucony plik może zostawić PageCount na zerze, a GetPageInfo odpowiada na indeks poza zakresem niezainicjalizowanym rekordem TTIFFPage, więc ścieżka błędu, która czyta rozdzielczość albo liczniki próbek w drodze do zgłoszenia awarii, kończy z odczytem szumu. Wersja 3.534.1 naprawiła oba wywołania wewnątrz biblioteki: ścieżka importu obrazu czyta XRes i YRes wyłącznie w gałęzi poprawności, a TPDFlib.GetImagePageCount wymaga ValidTIFF zamiast ufać samemu niezerowemu licznikowi stron. Niżej Options w AddImageFromFile to numer strony liczony od 1 dla wielostronicowego TIFF, więc GetImagePageCount musi być wiarygodny, zanim pętla się zacznie, a nie po fakcie. Zero stron to teraz realna odpowiedź znacząca „tu nic nie nadaje się do dekodowania", nie wypadek wcześniejszego powrotu, co najbardziej liczy się, gdy składasz i przeplatasz paczki skanów dwustronnych, a jeden cicho źle zdekodowany arkusz wylądowałby na złej pozycji
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // zły nagłówek, BigTIFF, układ kafelkowy lub przekroczony budżet
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;
Zbudować dekoder czy linkować libtiff?
PDFlibPas zostaje przy wbudowanym dekoderze, a czynnikiem rozstrzygającym jest zasięg platform, nie autorstwo. Jakieś 1 873 linie Object Pascal kompilują się wszędzie, gdzie dociera kompilator: Win32, Win64, macOS, iOS, Android i FPC na Linuksie. libtiff 4.7.1 to około 30 000 linii C rozłożone na 34 jednostki translacji tif_*.c, a prekompilowane pliki obiektowe, jakie istnieją dziś, pokrywają wyłącznie Windows. Jego przyjęcie wymieniłoby pełne pokrycie TIFF na listę wspieranych platform kurczącą się do maszyn, na których rusza toolchain C, plus przejście linkera, przez które nikt jeszcze nie przeszedł
Co to kosztuje, warto powiedzieć bez owijania. Wbudowany dekoder obsługuje to, co praca ze skanowanymi dokumentami faktycznie produkuje: CCITT Group 3 jedno- i dwuwymiarowy, Group 4, LZW, Deflate, PackBits i JPEG-in-TIFF, w fotometrykach WhiteIsZero, BlackIsZero, RGB, paleta i CMYK z Predictor 1 i 2. Te ładunki pokrywają się z filtrami PDF z ISO 32000-1 §7.4.4 i §7.4.6, dlatego front-end TIFF niesie taki ciężar w potoku skanowania. Nie obsługuje za to BigTIFF, kafelków, zmiennoprzecinkowego Predictor 3, PixarLog i SGILog, starego kompresowania JPEG 6 oraz piramid sub-IFD. Od 3.534.1 każdy z tych przypadków to nazwana odmowa, a nie zły obraz, a biblioteka trzyma spisaną listę wyzwalaczy do ponownego otwarcia decyzji o libtiff:
- klient zgłasza plik BigTIFF i potrzebuje natywnego wsparcia, a nie kroku konwersji
- klient zgłasza kafelkowy TIFF ze źródeł medycznych, GIS albo przemysłowych i potrzebuje go zdekodować na miejscu
- klient zgłasza TIFF ze zmiennoprzecinkowym Predictor 3
- opublikowana podatność trafia w wbudowane ścieżki dekodowania CCITT albo LZW
- argument wieloplatformowy przestaje obowiązywać, bo albo wsparcie macOS, iOS i Android zostaje porzucone, albo gotowa integracja z libtiff już pokrywa macOS i Linuksa
Sama migracja jest wyszkicowana, a nie hipotetyczna: warunek USE_LIBTIFF zachowałby publiczną powierzchnię TPDFTIFF w całości, poprowadził LoadFromStream przez TIFFClientOpen ze zwrotnymi callbackami strumienia i zostawił parser Pascal jako fallback poza Windowsem. Dopóki żaden z tych wyzwalaczy faktycznie nie zapali się, utrzymywanie dwóch dekoderów i podwojonej macierzy testów nie kupuje niczego, co klient by poczuł. Odkładanie kosztu z już zapisaną drogą ucieczki to co innego niż ignorowanie go
Co to zostawia potokowi skanowanych dokumentów
Traktuj TPDFTIFF jako bramę, a nie konwerter. Wczytaj plik, odczytaj ValidTIFF i loguj LastError dosłownie, ilekroć jest fałszywy, bo ten łańcuch jest teraz najkrótszą drogą od zgłoszenia z terenu do diagnozy. Pliki, które nie przejdą bramy, pozostają do odzyskania przez konwersję wyżej w strumieniu, co jest dziś praktyczną odpowiedzią dla źródeł BigTIFF i kafelkowych. Dla danych wejściowych całkiem poza TIFF PDFlibPas biegnie osobną trasą przez swoją ścieżkę wejścia obrazów AVIF, HEIF i JPEG XL, więc pytanie, który dekoder włada którym formatem, zostaje jawne zamiast wyłaniać się przy okazji
To wszystko siedzi za zwyczajnym API obrazów, więc potok dokumentów dostaje ciaśniejszą granicę bez zmiany ani jednej linii kodu wywołującego poza sprawdzeniem liczby stron, które i tak powinien już sprawdzać. Jeśli rozważasz natywną ścieżkę TIFF do PDF dla Delphi albo C++Builder, pełny komponent i jego obsługa obrazów są udokumentowane na stronie PDF Library for Delphi