يقرأ HotXLS هندسة البكسل الحقيقية للصورة من ترويسة ملف الصورة نفسه قبل كتابة أي هندسة رسم. توزع GetImageSize في lxImage.pas على امتداد الملف لتصل إلى GetJpegSize أو GetGifSize أو GetBmpSize أو GetPngSize أو GetWmfEmfSize، وتحوّل البكسلات إلى نقاط بعامل 0.75، وتعيد -1 حين لا تستطيع الجزم فعلاً
تلك الجملة الأخيرة هي الجزء الذي استغرق إتقانه أطول وقت. قارئ أحجام لا يستطيع الفشل قارئ يكذب، وكذبة عن هندسة الصورة لا تطفو على السطح وقت الإدراج. إنها تطفو بعد أسابيع، حين يفتح أحدهم المصنف ويكون شعار الشركة مستطيلاً مبعوجاً
لماذا تحتاج الصورة هندستها الحقيقية قبل دخولها إلى الرسم؟
لأن صيغة الجداول تخزن المدى، لا الصورة. حين تضع AddPicture أو Images.AddFromFile صورة، يسجل جزء الرسم عرضاً وارتفاعاً صريحين للشكل — في DrawingML هما سمتا cx وcy في عنصر a:ext، بوحدات EMU الإنجليزية، وفق ECMA-376 الجزء 1 §20.4. لا شيء في ذلك السجل يقول «استخدم الحجم الطبيعي للصورة»، لذا يجب أن يأتي الحجم الطبيعي من مكان ما، والمكان الوحيد الذي يوجد فيه هو بايتات الصورة. أخطئ بعامل ثلاثة وستكون الصورة ممطوطة بعامل ثلاثة، في كل تطبيق يفتح المصنف، إلى الأبد. تحويل الوحدات من أبعاد البكسل تلك إلى EMU شأن منفصل، مشروح في مقال وحدات EMU والتحجيم؛ هذا النص عن الخطوة التي تسبقه، حيث تُحصل أرقام البكسل أصلاً. القيمة الاحتياطية حين يفشل قارئ هي 100 في 100، وذلك الرقم خاطئ عمداً وبشكل واضح. إنه ليس تخميناً لحجم مقنع، بل علامة تعني «لا أحد يستطيع قراءة هذا الملف»، وهو أفضل بكثير من رقم خاطئ ذي مظهر مقنع لأن المستخدم يبلغ عنه فوراً
GIF: الواصف الذي لم يُحلل قط
GetGifSize جديد، وقبل وجوده كان كل GIF يُدرج في ورقة عمل يخرج عند 100 في 100 بالضبط — لم يكن هناك فرع GIF أصلاً، فتساقط الامتداد إلى ذراع الافتراضي في GetImageSize وأعاد -1. هذه أرخص ترويسة في المجموعة تحليلاً، مما يجعل الإغفال محرجاً بعض الشيء وسهل الإصلاح جداً. تضع مواصفة GIF89a واصف الشاشة المنطقية مباشرةً بعد توقيع الستة بايتات، ويعرّف §18 أول حقليه على أنهما عرض الشاشة المنطقية وارتفاعها، كلٌّ قيمة 16 بت بلا إشارة مخزنة بترتيب little-endian. بلغة الملف هذان هما البايتان 6 و7 للعرض، والبايتان 8 و9 للارتفاع، لذا قراءة واحدة من عشرة بايتات تغطي فحص التوقيع والبعدين معاً
function GetGifSize(const AFileName: WideString; var AWidth, AHeight: Double): Integer;
var
f: TWFileStream;
Header: array [0.. 9] of Byte;
wWidth, wHeight: Word;
begin
Result:= - 1;
AWidth:= 100;
AHeight:= 100;
f:= TWFileStream.Create(AFileName, fmOpenRead);
try
// توقيع "GIF8"، ثم واصف الشاشة المنطقية، بالعرض
// والارتفاع ككلمتين little-endian عند الإزاحتين 6 و8
if (f.Read(Header[0], SizeOf(Header))= SizeOf(Header))and
(Header[0]= Ord('G'))and (Header[1]= Ord('I'))and
(Header[2]= Ord('F'))and (Header[3]= Ord('8')) then
begin
wWidth:= Header[6]or (Header[7] shl 8);
wHeight:= Header[8]or (Header[9] shl 8);
if (wWidth> 0)and (wHeight> 0) then
begin
AWidth:= wWidth* 0.75;
AHeight:= wHeight* 0.75;
Result:= 1;
end;
end;
finally
f.Free;
end;
end;
لاحظ شكلها: قيم الفشل تُسند أولاً، ومسار النجاح هو الوحيد الذي يستبدلها. كل قارئ في lxImage.pas يتبع هذا الترتيب، لأنه يجعل «لم أستطع قراءة هذا» هو الافتراضي لا شيئاً يجب تذكره عند كل خروج مبكر. الفحص على GIF8 فقط بدل السلسلة الكاملة GIF87a أو GIF89a مقصود — تخطيط الواصف متطابق في النسختين، ولا فائدة من رفض ملف هندسته قابلة للقراءة
ماذا يغيّر حجم ترويسة BMP البالغ 12 فعلاً؟
يقرأ GetBmpSize البايتات الأربعة عند الإزاحة 14 كحجم ترويسة DIB ويتفرع على القيمة، لأن BMP بحجم ترويسة 12 يخزن أبعاده بعرض حقل مختلف تماماً عن كل BMP آخر. يوضح [MS-WMF] §2.2.2 العائلة: BITMAPCOREHEADER طوله 12 بايتاً ويحمل عرضاً وارتفاعاً بعرض 16 بت، بينما تبدأ BITMAPINFOHEADER وBITMAPV4HEADER وBITMAPV5HEADER كلها بـ biWidth وbiHeight موقعين بعرض 32 بت. قراءة أربعة بايتات حيث يحتوي الملف على اثنين لا تفشل بصخب؛ إنها تسحب عدد المستويات وعمق البت كالنصف الأعلى لرقم، فتبلغ عن صورة نقطية بعرض عدة مئات آلاف من البكسلات. والفخ الثاني في الترويسة نفسها هو إشارة biHeight: القيمة السالبة لا تعني ارتفاعاً سالباً، بل تعني أن صفوف DIB مخزنة من الأعلى إلى الأسفل بدلاً من الأسفل إلى الأعلى، وهي عبارة عن ترتيب البكسلات لا علاقة لها بالهندسة
f.Seek(14, soFromBeginning);
if f.Read(HeaderSize, 4)= 4 then
begin
if HeaderSize= 12 then
begin
// BITMAPCOREHEADER يخزن أبعاداً بعرض 16 بت
if (f.Read(wWidth, 2)= 2)and (f.Read(wHeight, 2)= 2) then
begin
lWidth:= wWidth;
lHeight:= wHeight;
Result:= 1;
end;
end
else if (f.Read(lWidth, 4)= 4)and (f.Read(lHeight, 4)= 4) then
Result:= 1;
end;
// ...
// biHeight السالبة تعني فقط DIB من الأعلى إلى الأسفل؛ المقدار هو الحجم
if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
begin
AWidth:= lWidth* 0.75;
AHeight:= Abs(lHeight)* 0.75;
end
else
Result:= - 1;
الحارس الأخير يستحق القراءة بعناية. يُفحص الارتفاع مقابل <> 0 لا > 0، لأن السالب قانوني والصفر ليس كذلك، ويُطبَّق Abs فقط عند نقطة التحويل. لا حالة مكافئة للعرض — biWidth السالب غير معرّف في الصيغة، لذا يُعامل كملف تالف ويُخفض إلى -1. تمرير قيمة سالبة خام إلى الأعلى بدلاً من ذلك سينتج مدى سالباً في سجل الرسم، وهو ما يُقص أو يُتجاهل أو يُعرض كشكل مقلوب بحسب أي تطبيق يفتح المصنف
JPEG: أي العلامات هي ترويسات إطارات فعلاً؟
يقبل GetJpegSize مجموعة العلامات [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF]، والفواصل الثلاثة في ذلك النطاق هي النقطة كلها. يعرّف ITU-T T.81 §B.2.2 ترويسة الإطار باسم SOFn، وترويسة الإطار هي حيث يعيشان عدد الأسطر وعدد العينات في كل سطر، في كل نوع إطار لا في الأنواع الشائعة فقط. العلامات التي تقع داخل نفس المدى الرقمي دون أن تكون ترويسات إطارات هي $C4 (DHT، تعريف جداول Huffman) و$C8 (JPG، محجوزة) و$CC (DAC، تعريف شروط الترميز الحسابي). قارئ يميز فقط baseline وextended sequential وprogressive — $C0 و$C1 و$C2 — يعمل على كل JPEG تقريباً تنتجه كاميرا أو أداة لقطة شاشة، ثم يفشل بهدوء مع تلك التي تهم سير عمل المستندات: JPEG بلا فقدان ($C3)، والأنواع المرمزة حسابياً ($C9 حتى $CB)، وأنواع الإطارات الهرمية ($CD حتى $CF) — كلها تمسح متجاوزة ترويسة الإطار، وتهرب من نهاية الملف، وتهبط في القيمة الاحتياطية. خطوط معالجة التصوير الطبي والمستندات الممسوحة تنتج تلك الملفات بالضبط
// أي علامة SOFn تحمل أبعاد الإطار؛ $C4 (DHT)، $C8 (JPG)
// و$CC (DAC) تتشارك المدى لكنها ليست ترويسات إطارات
if Seg in [$C0.. $C3, $C5.. $C7, $C9.. $CB, $CD.. $CF] then
begin
ReadLen:= f.Read(Dummy[0], 3);
ReadLen:= f.Read(wHeight, 2);
wHeight:= swapByte(wHeight);
ReadLen:= f.Read(wWidth, 2);
wWidth:= swapByte(wWidth);
found:= True;
Break;
end
تفصيلان في تلك الأسطر السبعة. البايتات الثلاثة المتخطاة هي كلمة طول المقطع مع بايت دقة العينات، لذا تهبط القراءة على عدد الأسطر؛ يخزن JPEG الارتفاع قبل العرض، وهو عكس معظم الصيغ وتبديل سهل الوقوع فيه. والقيم تمر عبر swapByte لأن JPEG بترتيب big-endian في كامل أجزائه، وهو نفس السبب الذي يجعل GetPngSize تمرر قيم IHDR ذات الـ 32 بت عبر lswapByte. وللمشي عبر المقاطع حارسه الخاص: حين لا تكون العلامة بلا معاملات، يأخذ القارئ طول البايتين ويبدّل بايتاته، ويرفض الإزاحة إن كانت القيمة أدنى من 2 — طول أقصر من حقله نفسه سيتحول إلى إزاحة للخلف، والإزاحة للخلف في حلقة مسح حلقة لا نهائية على ملف يملكه شخص آخر
إعادة سالب واحد أفضل من إعادة رقم مقنع
الخيط المشترك عبر القرّاء الخمسة كلهم هو أن GetImageSize تميز ثلاث نتائج لا اثنتين. 1 تعني أن الهندسة قُرئت من الملف. -1 تعني أنها لم تُقرأ، وعلى المستدعي استخدام افتراضيه. و7 يعيدها GetWmfEmfSize وتعني شيئاً مختلفاً كلياً: تخبر AddPicture أن تأخذ الحجم من سجل BSE في ملف التعريف بدلاً من ذلك، لأن WMF أو EMF يحمل مداه في سجل الصورة الخاص بالمصنف لا في إزاحة ثابتة داخل ترويسة الملف. معالجة ملفات metafile بشكل عام مشروحة في مقال مفكك EMF وWMF المقيّد. يوضح GetJpegSize لماذا تحتاج ذلك التمييز إلى علم صريح لا استنتاجاً: حلقة المسح قد تنتهي بثلاث طرق — وجدت ترويسة إطار، أو بلغت نهاية الملف، أو انسحبت على طول مقطع تالف — وواحدة فقط منها تسند wWidth وwHeight. من دون المنطقي found، كان المساران الآخران سيحوّلان أي شيء تحمله تلك المتغيرات المحلية مصادفةً ويسلمان النتيجة للأعلى كنجاح. المتغيرات المحلية غير المهيأة كثيراً ما تكون صفراً، والصفر أسوأ إجابة ممكنة هنا: ينجو من كل فحص صلاحية لاحق وينتج صورة غير مرئية لا يستطيع أحد تحديدها أو حذفها
var
W, H: Double;
begin
case GetImageSize(FileName, W, H) of
1: Sheet.AddPicture(FileName, Row, Col, W, H);
7: Sheet.AddPicture(FileName, Row, Col); // metafile: الحجم من سجل BSE
else
// W وH هما 100 x 100؛ حدد السياسة بدل قبول قيمة مؤقتة
LogWarning('unreadable image geometry: ' + FileName);
end;
end;
أين الحدود الصادقة
هذه القرّاء تحلل ترويسة عمداً ثم تتوقف. إنها لا تفك ترميز البكسلات، لذا تبلغ عن الأبعاد المخزنة ولا شيء أكثر، وهناك حالات لا تكون فيها الأبعاد المخزنة القصة كاملة. GIF متحرك إطاره أصغر من الشاشة المنطقية ما زال يبلغ عن الشاشة المنطقية، وهو صحيح للتخطيط وقد لا يطابق ما يعرضه العارض. JPEG يحمل وسم اتجاه EXIF يحدد دوراناً بـ 90 درجة يبلغ عن أبعاد الإطار غير المدوَّر، لذا قد تعود صورة عمودية التقطها هاتف كصورة أفقية. DIB بنسبة بكسل غير مربعة يبلغ عن أعداد بكسلات خام، وعامل 0.75 يفترض 96 DPI في كل مكان. لا شيء من هذا عيب في قراءة الترويسة ولا شيء منها قابل للإصلاح دون فك ترميز الصورة، وهو ليس ما صُنعت له هذه الطبقة — إنها حدود موثقة، لذا إن كان خط معالجتك يتعامل مع صور الهواتف فاقرأ وسم الاتجاه بنفسك وبدّل القيمتين قبل استدعاء AddPicture. وكل ما تبقى عن وضع الصور وترسيها ونموذج كائنات الرسم مشروح في مقال المخططات والصور والرسومات
يقرأ HotXLS ويكتب XLS وXLSX وODS وCSV أصلياً في Delphi وC++Builder دون الحاجة إلى تثبيت Excel، وإدراج الصور يمر عبر مسار الترويسة هذا في كل واحدة منها. تفاصيل دعم الصيغ والترخيص على صفحة منتج مكوّن جداول HotXLS لـ Delphi