مقاله فنی

سخت‌سازی رمزگشای TIFF در Delphi: BigTIFF و TIFF تایل‌بندی

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 بیتی آن در رمزگشای داخلی پیاده‌سازی نشده» را دریافت می‌کند، و این همان تفاوت میان تیکتی است که با یک پاسخ بسته می‌شود با تیکتی که به یک هفته حدس و گمان تبدیل می‌شود

لودر TIFF در PDFlibPas نشانگر ترتیب بایت و جادوی 16 بیتی را جداگانه می‌خواند، به‌طوری‌که هدر کوتاه، نشانگر نامعتبر، جادوی 43 در BigTIFF و هر مقدار جادوی دیگر هر کدام متن متمایزی در LastError تولید می‌کنند نه یک بولین بی‌صدا
TIFF کلاسیک و BigTIFF با نشانگر ترتیب بایت یکسانی باز می‌شوند، به همین دلیل PDFlibPas چهار حالت پس‌زدن را جدا می‌کند و نام هر یک را در 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;

تایل‌ها یک هندسه متفاوت‌اند، نه صرفاً آرایه آفست دیگری

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 را همان‌جا رها می‌کنند، تا این پس‌زدن واژه «تایل‌بندی‌شده» را با خود داشته باشد و نه اینکه هفته‌ها بعد به‌شکل شکایت رندری خودش را نشان دهد

PDFlibPas چیدمان strip را که باندهای تمام‌عرض در آن یک گام ردیف مشترک دارند با چیدمان تایل مقایسه می‌کند؛ شبکه‌ای دوبعدی از بلوک‌های لبه پرشده، و تگ‌های 324 و 325 را در زمان تجزیه تگ‌ها و پیش از دست زدن به هر داده پیکسلی رد می‌کند
آرایه‌های تایل از نظر ساختاری با آرایه‌های strip یکسان به نظر می‌رسند و همین است که دادن آن‌ها به رمزگشای strip به تصویری با ابعاد درست و پیکسل‌های غلط می‌انجامد

یک محدودیت ابعادی یک بودجه حافظه نیست

محدود کردن عرض و ارتفاع هر یک به 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 نامعتبر هم رعایت می‌کند، چون محدودیتی که فقط در یکی از سه در اعمال شود اصلاً محدودیت نیست

PDFlibPas هر بافر TIFF را با حساب Int64 اندازه‌گذاری می‌کند، ارتفاع تصویر را با تقسیم می‌آزماید تا حاصل‌ضرب سرریزشده هرگز شکل نگیرد، و همان روتین ValidatePageForDecode را هم در تجزیه تگ‌ها و هم در ورودی هر دو رمزگشا اجرا می‌کند
سه سقف، حساب Int64 برای بایت‌های ردیف و یک روتین اعتبارسنجی مشترک که از هر سه در به آن می‌رسند؛ چون محدودیتی که فقط در یکی از سه در اعمال شود محدودیت نیست

فراخوان پیش از خواندن 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 آمده است