PDFlibPas فایلهای TIFF را با یک تجزیهگر دستنویس به زبان Object Pascal رمزگشایی میکند، نه از طریق اتصال به libtiff، و نسخه 3.534.1 دقیقاً همان نقطهای را محکم کرد که این تجزیهگر ورودی را پس میزند. جادوی 43 در BigTIFF اکنون با ذکر نام رد میشود، TileOffsets و TileByteCounts در زمان تجزیه تگها پذیرفته نمیشوند و هر بافر با حساب Int64 و زیر سقف رمزگشایی 256 مگابایت اندازهگذاری میشود
این نقص هرگز در آزمایشگاه ظاهر نمیشود. ظاهر میشود در قالب یک دروازه اسکن که سه سال بیسروصدا کار کرده، تا زمانی که مشتری یک آرشیو ژئوفضایی یا تصویر پزشکی از نوع whole-slide را از آن عبور میدهد. فایل هدر TIFF معتبری دارد. تجزیه هم میشود. چیزی که بیرون میآید یا صفحهای از نویز راهراه است، یا تخصیص حافظه چند گیگابایتی که سرویس را زمین میزند، و در هیچ نقطهای از این مسیر کسی ورودی را نامعتبر اعلام نکرده است. این همان شکل خرابیای است که ارزش مهندسی در برابرش را دارد: نه یک کرش، بلکه یک پاسخ اشتباه که با اعتمادبهنفس تحویل داده میشود
چرا 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 ثبت میکند: هدر کوتاهتر از چهار بایت، نشانگر ترتیب بایت نامعتبر، جادوی 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 یکساناند، بنابراین اشاره دادن فیلدهای موجود strip به آنها فقط دو خط کد است و بدون خطا کامپایل میشود. اما همین کار اشتباه است. تایلها یک شبکه دوبعدی با بلوکهای لبه پُرشده میسازند، داخل هر تایل گام ردیف خودشان را دارند و هیچ معنای RowsPerStripای ندارند، همانطور که بخش تصاویر تایلبندیشده در TIFF 6.0 توضیح میدهد. پس دادن بار تایلها به یک رمزگشای strip بهصورت بلند و واضح شکست نمیخورد. SimpleExtract و CompDecode با گام اشتباه روی داده حرکت میکنند و تصویری با ابعاد درست و پیکسلهای غلط تولید میکنند. کد قدیمیتر با نگه داشتن StripsAreTiles، ColumnsPerTile و RowsPerTile در TTIFFPage مشکل را دوچندان میکرد: هندسه تایل ثبتشده توسط رمزگشایی که هیچ مونتاژکننده تایلی پشت آن نبود. در 3.534.1 هندلرهای تگهای 324 و 325 خطای تایل را صادر میکنند و IFD را همانجا رها میکنند، تا این پسزدن واژه «تایلبندیشده» را با خود داشته باشد و نه اینکه هفتهها بعد بهشکل شکایت رندری خودش را نشان دهد
یک محدودیت ابعادی یک بودجه حافظه نیست
محدود کردن عرض و ارتفاع هر یک به 65,535 لازم است و به هیچ وجه کافی نیست، چون کمیتی که تخصیص حافظه را تعیین میکند یک حاصلضرب است. عبارت RowsPerStrip * Width * SamplesPerPixel مدتها پیش از آنکه هر طرف به سقف خودش برسد میتواند از حساب 32 بیتی سرریز کند، و حتی بدون سرریز هم میتواند تخصیصی را مطالبه کند که هیچ سرویسی نباید امتحانش کند. PDFlibPas بایتهای هر ردیف را در Int64 محاسبه میکند و سه سقف را با هم اعمال میکند: 65,535 برای هر بُعد، 32 مؤلفه رنگ و 256 مگابایت بایت رمزگشاییشده
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 یا بزرگتر از ارتفاع تصویر ابتدا به ارتفاع نرمال میشود؛ همان قرائت تکstripای که TIFF 6.0 از قبل تلویح میکند و جلوی تورم بافر strip توسط یک تگ خرابکارانه را میگیرد. و این روتین مشترک است: 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 باید پیش از شروع حلقه قابل اعتماد باشد نه بعد از آن. صفر صفحه اکنون یک پاسخ واقعی به معنای «اینجا چیزی برای رمزگشایی نیست» است، نه نتیجه تصادفی یک return زودهنگام؛ این نکته بیش از همه زمانی اهمیت دارد که دستههای اسکن دورو را تطبیق و درهممیکنید و یک برگ اشتباه رمزگشاییشده بیصدا میتواند در جای اشتباهی بنشیند
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // هدر خراب، 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 روی لینوکس. libtiff 4.7.1 حدود 30,000 خط C است که در 34 واحد ترجمه tif_*.c پخش شده و فایلهای object آمادهای که امروز وجود دارند فقط ویندوز را پوشش میدهند. پذیرفتن آن یعنی معامله پوشش کامل 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، تایلها، Predictor 3 ممیز شناور، PixarLog و SGILog، فشردهسازی JPEG قدیمی نوع 6 و هرمهای sub-IFD است. از 3.534.1 هر یک از این موارد یک رد شدن با نام مشخص است نه یک تصویر غلط، و کتابخانه فهرست مکتوبی از محرکهای بازگشایی تصمیم libtiff نگه میدارد:
- مشتری فایل BigTIFF گزارش کند و به جای مرحله تبدیل، پشتیبانی بومی بخواهد
- مشتری TIFF تایلبندیشده از منابع پزشکی، GIS یا صنعتی گزارش کند و بخواهد درجا رمزگشایی شود
- مشتری TIFF با Predictor 3 ممیز شناور گزارش کند
- آسیبپذیری منتشرشدهای روی مسیرهای رمزگشایی CCITT یا LZW داخلی بیفتد
- استدلال چندپلتفرمی اعتبارش را از دست بدهد؛ چه به دلیل کنار گذاشتن پشتیبانی macOS، iOS و Android، چه به دلیل وجود یک یکپارچهسازی قابل استفاده مجدد libtiff که macOS و لینوکس را پوشش میدهد
خود مهاجرت هم محدودهدار است نه فرضی: یک شرطی USE_LIBTIFF سطح عمومی TPDFTIFF را دستنخورده نگه میدارد، LoadFromStream را با کالبکهای استریم از طریق TIFFClientOpen عبور میدهد و تجزیهگر Pascal را بهعنوان جایگزین غیر ویندوزی باقی میگذارد. تا وقتی یکی از این محرکها واقعاً فعال نشود، نگهداری دو رمزگشا و دو برابر شدن ماتریس تست هیچ چیز قابل لمسی برای مشتری نمیخرد. به تعویق انداختن یک هزینه همراه با نوشته شدن راه فرارش چیز دیگری است از نادیده گرفتنش
این وضعیت برای خط لوله اسناد اسکنشده چه معنایی دارد
با TPDFTIFF مثل یک گیت رفتار کنید نه یک مبدل. فایل را لود کنید، ValidTIFF را بخوانید و هر وقت false بود LastError را عیناً لاگ کنید، چون این رشته اکنون کوتاهترین مسیر از گزارش میدانی تا تشخیص است. فایلهایی که از گیت رد نمیشوند با تبدیل در بالادست همچنان قابل بازیابیاند که امروز پاسخ عملی برای منابع BigTIFF و تایلبندیشده همین است. برای ورودیهای خارج از TIFF، PDFlibPas مسیر جداگانهای از طریق ورودی تصویر AVIF، HEIF و JPEG XL میرود تا این پرسش که کدام رمزگشا مالک کدام فرمت است صریح بماند و اتفاقی شکل نگیرد
همه اینها پشت API معمولی تصویر نشستهاند، پس خط لوله سند بدون تغییر حتی یک خط کد فراخوانی — جز بررسی تعداد صفحاتی که از قبل باید بررسی میکرد — مرز محکمتری میگیرد. اگر برای Delphi یا C++Builder یک مسیر بومی TIFF به PDF را میسنجید، مستندات کامل کامپوننت و نحوه مدیریت تصویر آن در صفحه PDF Library for Delphi آمده است