يفكك PDFlibPas ترميز TIFF باستخدام محلل Object Pascal مكتوب يدويا بدلا من ربط libtiff، وقد شدّد الإصدار 3.534.1 على النقطة نفسها التي يرفض عندها المحلل المدخلات۔ فقيمة BigTIFF السحرية 43 تُرفض الآن بالاسم، ووسما TileOffsets وTileByteCounts يُرفضان أثناء تحليل الوسوم، وكل مخزن مؤقت يُحجَم بحساب Int64 تحت سقف فك ترميز قدره 256 MiB
العيب الذي تسدّه هذه المعالجة لا يظهر أبدا في المختبر۔ بل يظهر كبوابة مسح ضوئي تعمل بهدوء منذ ثلاث سنوات، إلى أن يمرر عميل عبرها أرشيفا جغرافيا مكانيا أو صورة طبية لشريحة كاملة۔ الملف يحمل ترويسة 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) هما مصفوفتا إزاحات ملف وأعداد بايت، متطابقتان بنيويا مع مصفوفات الأشرطة، لذا فإن توجيه حقول الأشرطة القائمة إليهما يكلف سطرين ويُترجَم بنجاح۔ لكنه أيضا خاطئ۔ فالبلاطات تشكل شبكة ثنائية الأبعاد بكتل حواف مبطنة، وتباعد أسطر خاص داخل كل بلاطة، وبلا أي دلالة لـ 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; // ترويسة تالفة أو 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 داخل 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 والأهرامات الفرعية لـ IFD۔ منذ 3.534.1 أصبح كل واحد منها رفضا مسمى لا صورة خاطئة، وتحتفظ المكتبة بقائمة محفزات مكتوبة لإعادة فتح قرار libtiff:
- يبلغ عميل عن ملف BigTIFF ويحتاج إلى دعم أصلي بدلا من خطوة تحويل
- يبلغ عميل عن TIFF بلاطي من مصادر طبية أو جغرافية مكانية أو صناعية ويحتاج إلى فك ترميزه في موضعه
- يبلغ عميل عن TIFF بـ 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، بحيث يبقى سؤال أي مفكك يملك أي صيغة صريحا لا عفويا
كل هذا يقبع خلف واجهة الصور العادية، لذا يكتسب خط أنابيب المستندات الحد الأشد إحكاما دون تغيير سطر واحد من كود الاستدعاء سوى التحقق من عدد الصفحات الذي كان ينبغي التحقق منه أصلا۔ وإذا كنت تزن مسارا أصليا من TIFF إلى PDF لـ Delphi أو C++Builder، فإن المكون الكامل ومعالجته للصور موثقان في صفحة مكتبة PDF لـ Delphi