مقال تقني

هندسة صور HotXLS في Delphi: وحدات EMU والسنتيمتر والتحجيم

تضع شعارًا 600×400 بكسل في ترويسة فاتورة مُولَّدة، فيبدو صحيحًا على شاشة التطوير لديك ذات 96 DPI، ثم بعد أسبوع يشتكي عميل على حاسوب محمول عالي الكثافة من أن طباعة الصورة صارت بحجم طابع بريدي. البكسلات لم تتغير. الذي تغير هو الافتراض بأن عدد البكسلات يعني حجمًا فيزيائيًا، وفي OOXML هذا الافتراض غير صحيح. تحتفظ صورة الجدول بأبعادها بوحدة EMU، وحتى تفكر بوعي داخل EMU—أو داخل الوحدات الواقعية التي تطابقه مباشرة—سيظل التخطيط لديك رهينة لقيمة DPI التي يفترضها جهاز العرض مصادفة

HotXLS هو مكوّن جداول بيانات VCL أصلي لـ Delphi وC++Builder يقرأ ويكتب XLS وXLSX من دون Excel أو أي اعتماد على COM. ابتداءً من v2.91.0 يتوقف كائن صور XLSX عن إلزامك بإجراء حسابات الوحدات يدويًا: فإلى جانب EMU الخام يوفّر العرض والارتفاع بالسنتيمتر والبوصة والنقاط، بالإضافة إلى Scale دالة تعيد التحجيم بنسبة مئوية مع إمكانية قفل نسبة الأبعاد. هذه المقالة تشرح ما هو EMU فعلًا، ولماذا اختارت DrawingML هذه الوحدة، وكيف تستخدم سطح الهندسة الجديد لوضع الصور بحسب الحجم الفيزيائي بدلًا من الاعتماد على عدد بكسلات لا يمكنك الوثوق به

ما هو EMU ولماذا يستخدمه DrawingML

EMU تعني English Metric Unit، وهي وحدة الطول الأساسية في DrawingML، طبقة الرسم المشتركة في عائلة Office Open XML كلها (ECMA-376، الجزء 1، §20). وقد صُممت EMU بحيث يكون هناك بالضبط 914400 EMU لكل بوصة و360000 EMU لكل سنتيمتر. هاتان الثابتتان هما السبب الكامل لوجود هذه الوحدة. فـ 914400 يقبل القسمة على 2 و3 و4 و5 و6 و8 و9 و10 و12 وغيرها كثيرًا، لأنه يتحلل إلى 26 × 32 × 52 × 127. وبما أن 1 بوصة = 2.54 سم تمامًا، فإن اختيار وحدة تقبل القسمة على كل من 360000 وعلى كسر نظيف من 914400 يتيح للصيغة التعبير عن البوصات والسنتيمترات والنقاط كـ أعداد صحيحة من دون أي تقريب عند حد الوحدة. ولو خُزنت "1.27 cm" كقيمة عائمة لتسربت الأخطاء، أما EMU فيخزن 457200 ويظل دقيقًا

والوحدة الأخرى المهمة هنا هي النقطة. فالنقطة الطباعية تساوي 1/72 بوصة، لذا يوجد 12700 EMU لكل نقطة (914400 / 72). والنقاط هي الطريقة التي يفكر بها Excel نفسه في ارتفاع الصفوف وأحجام الخطوط والهوامش تحت الغطاء، ولهذا فإن إظهار هندسة الصورة بالنقاط مفيد عندما تريد أن تصطف الصورة مع مقاييس النص بدلًا من مسطرة مطبوعة. ويعرض HotXLS كل العلاقات الأربع على هيئة ثوابت وحدات في المكتبة:

خريطة وحدة الطول EMU لـ HotXLS في Delphi توضح وحدة English Metric Unit واحدة تتحول بدقة إلى 914400 للبوصة و360000 للسنتيمتر و12700 للنقطة و9525 لبكسل 96 DPI معتمدة على DPI
يقابل EMU واحد وحدات البوصة والسنتيمتر والنقطة تماماً، بينما بكسل الـ 9525 EMU هو التحويل الوحيد المعتمد على DPI ومصدر علة الصورة بحجم الطابع البريدي
const
  XlsxEmuPerInch  = 914400;  // 1 inch
  XlsxEmuPerCm    = 360000;  // 1 centimetre
  XlsxEmuPerPoint = 12700;   // 1 point (1/72 inch)
  XlsxEmuPerPixel = 9525;    // 1 pixel at 96 DPI (914400 / 96)

وهنا يكمن جوهر مشكلة الصورة بحجم الطابع البريدي. فالبكسل لا يملك حجمًا فيزيائيًا إلا بعد تثبيت قيمة DPI، و9525 EMU هو حجم البكسل عند 96 DPI تحديدًا. يستخدم Excel افتراضيًا DPI يساوي 96، لذلك تستقر صورة بعرض 100 بكسل عند 100 × 9525 = 952500 EMU ≈ 2.54 سم في الإعداد الافتراضي—لكن لا شيء في الملف يضمن أن المستهلك سيستخدم 96. وإذا ألفتَ بالوحدات الحقيقية تختفي هذه الغموض: 4 سم تظل 4 سم سواء كانت الشاشة 96 DPI أو 220 DPI

سطح الهندسة الخاص بـ TXLSXImage

الصورة المضمنة في HotXLS هي TXLSXImage. وتخزينها الأساسي هو حقلان صحيحان، WidthEMU وHeightEMU، متمركزان عند Row وCol ذات فهرسة تبدأ من 1، وهي الخلية العلوية اليسرى التي تتدلى منها الصورة. أما خصائص الوحدات الحقيقية فهي عروض محسوبة فوق حقول EMU هذه، وليست حالة منفصلة—فقراءة WidthCM تقسم EMU على 360000، وكتابتها تضرب ثم تعيد التقريب. لذلك فكل بُعد تضبطه ليس إلا طريقة أخرى لكتابة قيمة EMU الأساسية نفسها:

سطح هندسة TXLSXImage في HotXLS لـ Delphi حيث يكون WidthEMU و HeightEMU مصدر الحقيقة الصحيح وخصائص البوصة والسنتيمتر والنقطة عروضًا محسوبة تقسم عند القراءة وتضرب وتقرّب عند الكتابة
WidthEMU وHeightEMU هما الحالة المخزنة الوحيدة — وكل خاصية بوحدة حقيقية تقسم EMU عند القراءة وتضربها راجعةً مع التقريب عند الكتابة
  • WidthInch / HeightInch — EMU ÷ 914400
  • WidthCM / HeightCM — EMU ÷ 360000
  • WidthPt / HeightPt — EMU ÷ 12700
  • WidthEMU / HeightEMU — المصدر العددي المرجعي

أضف صورة باستخدام AddImage(ARow, ACol, AData, AFormat), مع تمرير البايتات المشفّرة الخام وTXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, أو xlsxImageBmp)؛ فتُعيد الفهرس الصفري داخل مجموعة Images. وهناك أيضًا AddImageFromFile(ARow, ACol, AFileName)، الذي يستنتج الصيغة من امتداد الملف. انتبه إلى أساس الفهرسة: AddImage يعيد فهرسًا يبدأ من الصفر وImages[] يبدأ من الصفر أيضًا، وهذا يقابله عمدًا Cells[Row, Col] الشبكي ذي الفهرسة التي تبدأ من 1، لذلك لا تفترض أن الاثنتين متطابقتان

var
  Sheet: TXLSXWorksheet;
  Img: TXLSXImage;
  Idx: Integer;
begin
  Sheet := Workbook.Sheets.Add('Images');

  // ثبّت PNG في الصف 3، العمود 2؛ يعيد AddImage فهرسًا يبدأ من الصفر
  Idx := Sheet.AddImage(3, 2, LogoBytes, xlsxImagePng);

  Img := Sheet.Images[Idx];
  Img.WidthCM := 4.0;    // 4 cm wide  -> 1440000 EMU
  Img.HeightCM := 3.0;   // 3 cm tall  -> 1080000 EMU

  // الهندسة نفسها، مقروءة بوحدات أخرى
  // أصبحت Img.WidthPt  تساوي 113.39 pt، وImg.WidthInch تساوي 1.5748 in
end;

الصورة المنشأة حديثًا تُضبط افتراضيًا على 100×100 بكسل، أي مربع 952500 EMU، وهو تقريبًا مربع 2.54 سم عند 96 DPI. يوجد هذا الافتراضي لكي تبقى الصورة مرئية حتى لو نسيت تحديد حجمها، لكن لأي تخطيط حقيقي يجب أن تضبط حجمًا فيزيائيًا صريحًا بدلًا من الاعتماد على الافتراضي المشتق من البكسلات

التحجيم، وخيار الحفاظ على النسبة

عندما تريد تغيير الحجم بالنسبة إلى الأبعاد الحالية بدلًا من هدف مطلق—مثلًا لتصغير صورة مخطط إلى 60% من الحجم الذي استوردت به—استخدم Scale:

procedure Scale(APercent: Double; AKeepAspect: Boolean = True);

APercent هي نسبة مئوية، حيث 100 تعني بلا تغيير، و150 تزيد الحجم بمقدار النصف، و50 تنصفه. ومع AKeepAspect عند قيمته الافتراضية True، يضرب العرض والارتفاع في العامل نفسه، فتظل النسب محفوظة وتتحول صورة 4×3 سم إلى 6×4.5 سم بعد Scale(150). مرّر False فيُحجَّم العرض فقط—بينما يبقى الارتفاع كما هو تمامًا. هذا عدم التماثل مقصود: عندما تريد تمديد محور واحد بصورة مستقلة، فالأداة المناسبة هي الضابِطات الصريحة لـ WidthCM/HeightCM، أما الفرع غير المقيد بالنسبة في Scale فموجود للحالة الأضيق الخاصة بضبط العرض وحده. يسهل أن تُقرأ Scale(150, False) على أنها "تمديد كلا البعدين بحرية" ثم تتفاجأ، لذا استخدم الضابطات عندما تقصد حقًا بُعدين مستقلين

مقارنة تابع Scale في HotXLS داخل Delphi على صورة 4×3 سم: يكبّر Scale(150) مع قفل نسبة الأبعاد المحورين إلى 6×4.5 سم، بينما يصغّر Scale(50, False) العرض فقط إلى 3.0 سم ويترك الارتفاع دون تغيير
مع علم نسبة الأبعاد على قيمته الافتراضية True يتضاعف البعدان بالعامل نفسه — وتمرير False يُقيس العرض وحده ويرفع الارتفاع بلا مساس
Img.WidthCM := 4.0;
Img.HeightCM := 3.0;

Img.Scale(150);          // aspect locked: now 6.0 x 4.5 cm
Img.Scale(100);          // no-op, returns immediately

Img.Scale(50, False);    // العرض فقط: 3.0 cm، والارتفاع ثابت عند 4.5 cm

ومن الأمور الصغيرة التي ينبغي معرفتها: Scale(100) يقفز مباشرة ويعود من دون لمس أي من الحقلين، لذلك من الآمن استدعاؤه دون شرط داخل حلقة قد تكون النسبة فيها 100. وبما أن الهندسة مخزنة على هيئة عدد صحيح من EMU، فإن كل setter يجري التقريب. لذلك قد ينجرف التمرير ذهابًا وإيابًا عبر السنتيمترات الكسرية بمقدار جزء من EMU واحد—وهو أقل بكثير من أن يُرى، لكنه يستحق الذكر إذا كنت تختبر المساواة الدقيقة. وللحصول على تحكم على مستوى البكسل، اضبط WidthEMU وHeightEMU مباشرة وتجاوز تحويل الوحدات بالكامل

قراءة الهندسة مرة أخرى

مجموعة الصور قابلة للاستعلام، وهذا مهم عندما تفتح مصنفًا موجودًا وتحتاج إلى فحص ما هو موجود بالفعل أو تعديله بدلًا مما أضفته أنت للتو. Images.Count تعدّد كل صورة في الورقة، Images[i] يضعها بترقيم يبدأ من الصفر، وFindAt(ARow, ACol) يعيد الصورة المثبتة عند خلية محددة—أو nil إذا لم توجد. وهناك أيضًا IndexOfCell للفهرس بدلًا من الكائن، وDeleteAt / DeleteInRange للإزالة

var
  i: Integer;
  Img: TXLSXImage;
begin
  for i := 0 to Sheet.Images.Count - 1 do
  begin
    Img := Sheet.Images[i];
    Writeln(Format('[%d] R%dC%d  %.2f x %.2f cm  (%d x %d EMU)',
      [i, Img.Row, Img.Col, Img.WidthCM, Img.HeightCM,
       Img.WidthEMU, Img.HeightEMU]));
  end;

  Img := Sheet.Images.FindAt(3, 2);   // تحقق من nil قبل الاستخدام
  if Img <> nil then
    Img.Scale(80);
end;

وبما أن خصائص الوحدات الحقيقية هي عروض حيّة، فإن الصورة المستوردة بحجم EMU معين من أداة أخرى تعرض هندستها بالسنتيمتر فورًا—من دون أي خطوة تحويل من جانبك. ويتناغم هذا طبيعيًا مع نموذج الرسم الأوسع؛ فإذا كنت تضع المخططات والأشكال إلى جانب الصور النقطية، فالدليل المرافق حول مخططات HotXLS والصور ورسومات Excel في Delphi يشرح نموذج التثبيت الذي تتشاركه هذه الكائنات

هوامش إعداد الصفحة بالمترية

يظهر التوتر نفسه بين EMU والوحدات الحقيقية على مستوى الصفحة أيضًا. يخزن OOXML وExcel هوامش الطباعة بـالبوصات، وهو أمر غير مريح إذا كانت قوالب التقارير لديك محددة بالمليمتر مثل معظم العالم خارج الولايات المتحدة. يضيف v2.91.0 أغلفة بالسنتيمتر فوق هوامش البوصة: MarginLeftCM، MarginRightCM، MarginTopCM، MarginBottomCM، MarginHeaderCM، وMarginFooterCM. وكل واحد منها مجرد طبقة راحة رقيقة فوق خاصية البوصة المقابلة، مع تحويل دقيق وفق نسبة 1 بوصة = 2.54 سم

Sheet.MarginLeftCM := 2.0;     // 2 cm  == 0.7874 inch
Sheet.MarginRightCM := 2.0;
Sheet.MarginTopCM := 2.5;
Sheet.MarginBottomCM := 2.5;
Sheet.MarginHeaderCM := 1.0;
Sheet.MarginFooterCM := 1.0;

خصائص البوصة (MarginLeft ورفاقها) تظل التخزين المرجعي، لذلك يمكنك المزج بين الاثنين—فتضبط هامشًا علويًا بالسنتيمتر وتقرأه لاحقًا بالبوصة، أو بالعكس—وتبقى القيمة المكتوبة إلى القرص مطابقة في الحالتين. التحويل مجرد ضرب في 2.54، من دون تقريب إلى شبكة خشنة، لذا يبقى 2 سم هو 2 سم حتى كامل دقة double. هذه هي الفلسفة نفسها الخاصة بتيسير القياس كما في هندسة الصورة: الصيغة تتحدث بالنظام الإمبراطوري تحت الغطاء، والمكتبة تتيح لك التأليف بالوحدة التي كتبت بها المواصفة. ولتنسيق التقرير المحيط—من عناوين وكتل بيانات وخلاصات—راجع تخطيط الخلايا المدمجة وقوالب التقارير في HotXLS، الذي يستخدم هذه الهوامش مع النطاقات المدمجة ومنطقة الطباعة

ملاحظة حول ما تضمنه الهندسة وما لا تضمنه

خصائص الهندسة تضبط الحجم المُعلَن للصورة في الملف—أي الحجم الذي سيرسمه المستهلك المتوافق. لكنها لا تعيد تشكيل وحدات البكسل؛ فصورة PNG بحجم 50×50 بكسل إذا ضُبطت على 8 سم ستكبر وتبدو متكسرة، تمامًا كما يحدث في Excel. التحجيم عملية تخطيط، لا معالجة صور، لذا امنح الصورة دقة مصدر كافية للحجم الفيزيائي الذي تريده. كما أن المكتبة لا تعيد ترميز الصيغ: البايتات التي تمررها إلى AddImage تُخزَّن وتُكتب كما هي، مع TXLSXImageFormat الذي تُعلنه. مرّر بايتات JPEG لكن وسمها xlsxImagePng وستنتج ملفًا لا يستطيع Excel فتحه، لذلك دع AddImageFromFile يستنتج الصيغة من الامتداد عندما تستطيع

لا يعود في هذا كله أي غرابة بعد أن تستوعب الفكرة الواحدة الكامنة تحته: في OOXML، الحجم الفيزيائي هو الكمية الحقيقية، والبكسلات مجرد ظل مشتق يعتمد على DPI. اكتب الصور والهوامش بالسنتيمتر أو البوصة أو النقطة، واترك HotXLS يطابقها مع EMU دقيق، وستطبع فواتيرك وتقاريرك بالحجم نفسه على كل جهاز يفتحها

تُشحن واجهات برمجة تطبيقات هندسة الصور والتحجيم وهوامش القياس المتري الموصوفة هنا مع مكوّن جداول بيانات HotXLS لـ Delphi، الذي يقرأ ويكتب XLS وXLSX من Delphi وC++Builder من دون الحاجة إلى تثبيت Excel