تضع شعارًا 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 كل العلاقات الأربع على هيئة ثوابت وحدات في المكتبة:
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 الأساسية نفسها:
WidthInch/HeightInch= EMU ÷ 914400WidthCM/HeightCM= EMU ÷ 360000WidthPt/HeightPt- EMU ÷ 12700WidthEMU/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');
// Anchor a PNG at row 3, column 2; AddImage returns a 0-based index.
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
// Same geometry, read back in other units.
// Img.WidthPt is now 113.39 pt, Img.WidthInch is 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) على أنها "تمديد كلا البعدين بحرية" ثم تتفاجأ، لذا استخدم الضابطات عندما تقصد حقًا بُعدين مستقلين
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); // width only: 3.0 cm wide, height unchanged at 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-check before use
if Img <> nil then
Img.Scale(80);
end;
وبما أن خصائص الوحدات الحقيقية هي عروض حيّة، فإن الصورة المستوردة بحجم EMU معين من أداة أخرى تعرض هندستها بالسنتيمتر فورًا، من دون أي خطوة تحويل من جانبك. ويتناغم هذا طبيعيًا مع نموذج الرسم الأوسع؛ فإذا كنت تضع المخططات والأشكال إلى جانب الصور النقطية، فالدليل المرافق حول مخططات HotXLS والصور ورسومات Excel في Delphi يشرح نموذج التثبيت الذي تتشاركه هذه الكائنات
هوامش إعداد الصفحة بالمترية
يظهر التوتر نفسه بين EMU والوحدات الحقيقية على مستوى الصفحة أيضًا. يخزن OOXML وExcel هوامش الطباعة بالبوصات، وهو أمر غير مريح إذا كانت قوالب التقارير لديك محددة بالمليمتر مثل معظم العالم خارج الولايات المتحدة. يضيف v2.91.0 أغلفة بالسنتيمتر فوق هوامش البوصة: ، ، MarginLeftCM، MarginRightCM، MarginTopCM، MarginBottomCM، وMarginHeaderCM. وكل واحد منها مجرد طبقة راحة رقيقة فوق خاصية البوصة المقابلة، مع تحويل دقيق وفق نسبة 1 بوصة = 2.54 سم.MarginFooterCMخصائص البوصة (
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;
ورفاقها) تظل التخزين المرجعي، لذلك يمكنك المزج بين الاثنين، فتضبط هامشًا علويًا بالسنتيمتر وتقرأه لاحقًا بالبوصة، أو بالعكس، وتبقى القيمة المكتوبة إلى القرص مطابقة في الحالتين. التحويل مجرد ضرب في 2.54، من دون تقريب إلى شبكة خشنة، لذا يبقى 2 سم هو 2 سم حتى كامل دقة double. هذه هي الفلسفة نفسها الخاصة بتيسير القياس كما في هندسة الصورة: الصيغة تتحدث بالنظام الإمبراطوري تحت الغطاء، والمكتبة تتيح لك التأليف بالوحدة التي كتبت بها المواصفة. ولتنسيق التقرير المحيط، من عناوين وكتل بيانات وخلاصات، راجع MarginLeftتخطيط الخلايا المدمجة وقوالب التقارير في HotXLS، الذي يستخدم هذه الهوامش مع النطاقات المدمجة ومنطقة الطباعة.ملاحظة حول ما تضمنه الهندسة وما لا تضمنه
خصائص الهندسة تضبط الحجم
المُعلَن للصورة في الملف، أي الحجم الذي سيرسمه المستهلك المتوافق. لكنها لا تعيد تشكيل وحدات البكسل؛ فصورة PNG بحجم 50×50 بكسل إذا ضُبطت على 8 سم ستكبر وتبدو متكسرة، تمامًا كما يحدث في Excel. التحجيم عملية تخطيط، لا معالجة صور، لذا امنح الصورة دقة مصدر كافية للحجم الفيزيائي الذي تريده. كما أن المكتبة لا تعيد ترميز الصيغ: البايتات التي تمررها إلى AddImage تُخزَّن وتُكتب كما هي، مع TXLSXImageFormat الذي تُعلنه. مرّر بايتات JPEG لكن وسمها xlsxImagePng وستنتج ملفًا لا يستطيع Excel فتحه، لذلك دع AddImageFromFile يستنتج الصيغة من الامتداد عندما تستطيع
لا يعود في هذا كله أي غرابة بعد أن تستوعب الفكرة الواحدة الكامنة تحته: في OOXML، الحجم الفيزيائي هو الكمية الحقيقية، والبكسلات مجرد ظل مشتق يعتمد على DPI. اكتب الصور والهوامش بالسنتيمتر أو البوصة أو النقطة، واترك HotXLS يطابقها مع EMU دقيق، وستطبع فواتيرك وتقاريرك بالحجم نفسه على كل جهاز يفتحها
تُشحن واجهات برمجة تطبيقات هندسة الصور والتحجيم وهوامش القياس المتري الموصوفة هنا مع مكوّن جداول بيانات HotXLS لـ Delphi، الذي يقرأ ويكتب XLS وXLSX من Delphi وC++Builder من دون الحاجة إلى تثبيت Excel