PDFlibPas декодира TIFF с ръчно написан парсер на Object Pascal, а не с обвързване към libtiff, и версия 3.534.1 стегна точно местата, на които този парсер отказва вход. BigTIFF с магия 43 вече се отхвърля с изрично название, TileOffsets и TileByteCounts се отхвърлят още при анализа на таговете, а всеки буфер се оразмерява с Int64 аритметика под таван от 256 MiB за декодиране
Дефектът, който това затваря, никога не се появява в лаборатория. Той се появява като шлюз за сканиране, който тихо работи три години, докато клиент не прати през него геопространствен архив или медицинско изображение на цял слайд. Файлът има легитимна TIFF header част. Той се парсва. На изхода излиза страница на ивици шум или многогигабайтова заделка, която сваля услугата, и по пътя нищо не е обявило входа за невалиден. Това е формата на отказ, която си струва да проектираш срещу: не срив, а грешен отговор, поднесен с увереност
Защо II или MM не доказва, че имате класически TIFF?
Защото маркерът за ред на байтовете е общ и за двата диалекта. Класическият TIFF и BigTIFF започват с II или MM, а полето, което наистина ги разграничава, е 16-битовата магия непосредствено след него: 42 за класически TIFF по спецификацията TIFF 6.0 и 43 за BigTIFF с неговите 64-битови отмествания. Зареждач, написан като FValidTIFF := PopWord = 42, не греши за класическия TIFF, но слива две съвсем различни отхвърляния в едно мълчаливо булево значение, така че BigTIFF става неотличим от отрязан JPEG, който някой е преименувал. PDFlibPas вече разделя случаите и записва всеки поотделно в TPDFTIFF.LastError: header част по-къса от четири байта, невалиден маркер за ред на байтовете, магия 43 и всяка друга стойност на магията дават различен текст. Библиотеката и досега не декодира BigTIFF, и точно да се каже това направо е смисълът. Извикващият получава разликата между „това не е TIFF“ и „това е TIFF, чието 64-битово разположение на отмествания вграденият декодер не изпълнява“, а това е разликата между тикет, на който отговаряш с един отговор, и тикет, който се превръща в седмица гадателство
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;
Плочките са друга геометрия, не още един масив от отмествания
PDFlibPas отхвърля TIFF с плочки още при анализа на таговете, преди да е пипнат каквито и да е пикселни данни. Краткият път, който кани бъга, се вижда лесно: таг 324 (TileOffsets) и таг 325 (TileByteCounts) са масиви от файлови отмествания и брой байтове, структурно идентични с ивичните масиви, така че насочването на съществуващите strip полета към тях струва два реда и се компилира чисто. Това обаче е грешно. Плочките образуват двумерна мрежа с подплатени крайни блокове, собствена стъпка по ред вътре във всяка плочка и никаква семантика на RowsPerStrip, както изрично пише в раздела за плочкисти изображения на TIFF 6.0. Затова подаването на плочкови данни към ивичен декодер не гърми веднага. SimpleExtract и CompDecode минават данните с грешната стъпка и издават изображение с правилните размери и грешните пиксели. По-старият код утежнява това, като държи StripsAreTiles, ColumnsPerTile и RowsPerTile в TTIFFPage: плочкова геометрия, записана от декодер без плочков асемблер зад него. В 3.534.1 манипулаторите на тагове 324 и 325 вдигат плочковата грешка и веднага изоставят IFD, така че отказът носи думата „tiled“ вместо да изплува седмици по-късно като жалба за изобразяване
Едно ограничение по размерност не е бюджет за памет
Стягането на ширина и височина до по 65 535 е необходимо и съвсем недостатъчно, защото величината, която управлява заделянето, е произведение. RowsPerStrip * Width * SamplesPerPixel може да прелее 32-битовата аритметика много преди някой от множителите да стигне собствения си лимит, а и без преливане може да назове заделка, която никаква услуга не бива да опитва. PDFlibPas смята байтовете на реда в Int64 и налага три тавана заедно: 65 535 на размерност, 32 цветови компонента и 256 MiB декодирани байтове
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// в 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);
Три подробности тук са по-важни от самите константи. Проверката за височина е написана като деление, а не умножение, така че прекалено големият продукт изобщо не се образува. RowsPerStrip под 1 или над височината на изображението първо се нормализира към височината, което четенето на единична ивица в TIFF 6.0 вече предполага и което спира враждебен таг да надуе ивичния буфер. А рутината е споделена: ValidatePageForDecode се изпълнява в края на анализа на таговете и отново при входа и на SimpleExtract, и на CompDecode, така че код, който стига директно до декодер, не може да заобиколи бюджета. Това е същото правило, което PDFlibPas спазва при анализ на недоверени PDF обектни графики, защото лимит, наложен на една врата от три, не е лимит
Какво трябва да провери извикващият преди четене на PageInfo?
Първо проверете ValidTIFF, после PageCount и чак тогава индексирайте PageInfo. Отхвърлен файл може да остави PageCount на нула, а GetPageInfo отговаря на индекс извън диапазона с неинициализиран запис TTIFFPage, така че грешен път, който чете резолюция или брой проби по пътя си към докладването на отказа, свършва като чете шум. Версия 3.534.1 поправи и двете извикващи места вътре в библиотеката: пътят за импорт на изображение чете XRes и YRes само в валидния клон, а TPDFlib.GetImagePageCount изисква ValidTIFF вместо да вярва самостоятелно на ненулев брой страници. По-надолу аргументът Options на AddImageFromFile е номерът на страницата, броен от 1, за многостраничен TIFF, така че GetImagePageCount трябва да е достоверен преди началото на цикъла, а не след него. Нула страници вече е истински отговор със значение „тук нищо не е декодируемо“, а не случайност от ранно връщане, което е най-важно, когато подреждате и преплитате партиди от дуплексно сканиране и една тихо погрешно декодирана страница би попаднала на грешната позиция
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, BigTIFF, плочки или над бюджета
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;
Собствен декодер или свързване на libtiff?
PDFlibPas запазва вградения декодер, а решаващият фактор е обхватът по платформи, не авторството. Около 1 873 реда Object Pascal се компилират навсякъде, където стига компилаторът: Win32, Win64, macOS, iOS, Android и FPC на Linux. libtiff 4.7.1 е около 30 000 реда C, разпръснати в 34 преводни единици tif_*.c, а предварително сглобените обектни файлове, които съществуват днес, покриват само Windows. Осиновяването му би разменило пълното TIFF покритие за списък с поддържани платформи, свит до машините, на които върви C инструментариумът, плюс линкер пас, който никой още не е минал
Цената на това си заслужава да се каже без маркетинг. Вграденият декодер се справя с това, което работата по сканирани документи действително ражда: CCITT Group 3 едномерен и двумерен, Group 4, LZW, Deflate, PackBits и JPEG-in-TIFF, при WhiteIsZero, BlackIsZero, RGB, палитрена и CMYK фотометрия с Predictor 1 и 2. Тези данни съвпадат с PDF филтрите в ISO 32000-1 §7.4.4 и §7.4.6, затова TIFF входът носи такава тежест в конвейер за сканиране. Това, с което не се справя, е BigTIFF, плочките, floating-point Predictor 3, PixarLog и SGILog, старият JPEG compression 6 и пирамидите от sub-IFD. От 3.534.1 всяко от тях е назовано отхвърляне вместо грешно изображение, а библиотеката пази писмен списък с тригери за повторно отваряне на решението libtiff:
- клиент съобщава BigTIFF файл и има нужда от собствена поддръжка, а не от стъпка за конвертиране
- клиент съобщава TIFF с плочки от медицински, GIS или индустриални източници и го иска декодиран на място
- клиент съобщава TIFF с floating-point Predictor 3
- публикувана уязвимост засяга вградените пътища за декодиране на CCITT или LZW
- аргументът за междуплатформеност спира да важи, защото поддръжката на macOS, iOS и Android е свалена, или защото преизползваема libtiff интеграция вече покрива macOS и Linux
Самата миграция е очертана, а не хипотетична: условното USE_LIBTIFF би запазило публичната повърхност на TPDFTIFF недокосната, пренасочило LoadFromStream през TIFFClientOpen с потокови обратни повиквания и оставило Pascal парсера като резервен вариант за не-Windows. Докато един от тези тригери действително не се задейства, поддръжката на два декодера и удвоена тестова матрица не купува нищо, което клиент да усети. Отлагането на цена, при която изходната врата вече е записана, е различно нещо от игнорирането ѝ
Какво остава на конвейера за сканирани документи
Отнасяйте се към TPDFTIFF като към шлюз, а не конвертор. Заредете файла, прочетете ValidTIFF и логвайте LastError дословно, когато е false, защото този низ вече е най-краткият път от теренно съобщение до диагноза. Файловете, пропаднали на шлюза, остават възстановими чрез конвертиране нагоре по веригата, което е практическият отговор за BigTIFF и плочкови източници днес. За вход изцяло извън TIFF PDFlibPas върви по отделен път през своето приемане на изображения AVIF, HEIF и JPEG XL, така че въпросът кой декодер притежава кой формат остава изричен, а не емпиричен
Всичко това стои зад обикновения API за изображения, така че документен конвейер печели по-стегнатата граница, без да променя нито ред извикващ код, освен проверката на броя страници, която уж вече е трябвало да прави. Ако претегляте собствен TIFF-към-PDF път за Delphi или C++Builder, пълният компонент и обработката му на изображения са документирани на страницата на PDF Library for Delphi