يضع PDF المصدَّر كل حد عمود على بعد نصف حرف إلى يسار حيث يرسمه Excel، وكل خلية ملفوفة تنكسر الآن في موضع مختلف. عرض عمود Excel لا يُقاس بالأحرف أو النقاط. بل يُقاس بوحدات عرض الرقم الأقصى (Max Digit Width أو MDW) لخط Normal في المصنف، ويقيس HotXLS ذلك الخط عبر GDI قبل كل بناء لترقيم الصفحات. نمط الفشل هادئ: لا شيء يثير استثناء، والعروض المخزنة تعود كما هي بايتا ببايت، والهندسة ما زالت منحرفة ببضعة في المئة لكل عمود حتى يدفع التراكم جدولا من صفحة واحدة إلى صفحتين
بأي وحدة يُقاس عرض عمود Excel؟
عرض العمود في ورقة العمل هو عدد أحرف الأرقام لخط Normal في المصنف، لا قياس مطلق. تعرّف ECMA-376 §18.3.1.13 سمة width للعنصر <col> بدلالة عرض الرقم الأقصى لذلك الخط عند 96 dpi، وتعطي التحويل من عرض مخزَّن إلى بكسل كتعبير اقتطاعي فوق MDW. بالنسبة لـCalibri 11، وهو ما يشحنه Excel بوصفه نمط Normal، يقيس MDW 7 بكسلات. مرّر العرض الافتراضي 8.43 وحدة عبر معادلة المواصفة مع MDW يساوي 7 وستحصل بالضبط على 64 بكسلا، وهي 48 نقطة عند 96 dpi. هذه هي الأرقام التي يبلغ عنها Excel نفسه، ما يجعلها فحصا مفيدا: إن أعاد تحويلك إنتاج 8.43 وحدة إلى 64 بكسلا، فالحساب صحيح ووحيد مدخل MDW يمكن أن يبقى خاطئا
const
// عرض الرقم الأقصى (MDW) لخط المتن الافتراضي بالبكسل عند 96 dpi.
// Calibri 11 يقيس 7 px، ما يعيد إنتاج عروض البكسل الدقيقة
// التي يخزنها Excel (8.43 وحدة -> 64 px -> 48 pt).
DefaultMDW = 7;
MinimumColumnWidth = 24.0;
function ColumnWidthToPointsMdW(Value: Double; MdW: Integer): Double;
var
Pixels: Integer;
begin
if Value <= 0 then
Value := 8.43;
if MdW <= 0 then
MdW := DefaultMDW;
Pixels := Trunc(((256 * Value + Trunc(128 / MdW)) / 256) * MdW) + 5;
Result := Pixels * 0.75; // بكسلات 96 dpi -> نقاط
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
يحتفظ HotXLS بذلك الحساب في دالة واحدة بالضبط، في الوحدة lxPagination، بحيث يكون هناك موضع واحد يمكن أن تخطئ فيه المسطرة. + 5 هي الحشوة التي يضيفها Excel لخطوط الشبكة وهوامش الخلايا، و* 0.75 تحوّل بكسلات 96 dpi إلى نقاط PostScript، والحد الأدنى عند MinimumColumnWidth موجود كي يترك حتى العمود الشديد الضيق شريطا يستطيع العارض رسم حد فيه. نقطة الدخول العامة ColumnWidthToPoints تحتفظ بتوقيعها القديم أحادي المعامل وتُمرّر MDW مقاسة إلى هذه الدالة، وهو ما سمح لتغيير السلوك بالوصول دون لمس أي موضع استدعاء
لماذا يزيح خط Normal غير Calibri كل الحدود
الانحراف ضربي، ولهذا يُقرأ كخلل عرض لا كخلل وحدات. MDW عامل على العرض لا إزاحة. ادفع MDW من 7 إلى 8 فينتقل العمود الافتراضي 8.43 وحدة من 64 بكسلا إلى 72، بقفزة 8 بكسلات أو 6 نقاط على عمود واحد. عشرة أعمدة من ذلك وتكون الحافة اليمنى للجدول قد تحركت معظم البوصة. المصنفات التي تستثير هذا عادية تماما: أي شيء أنتجته أداة تقارير تطبع Arial أو Segoe UI في نمط Normal، أو أي شيء حُفظ من قالب تصدير ERP، أو أي شيء أعاد عميل تنسيقه مرة ونسيه
نظاما تخطيط مرتبطان يرثان الخطأ بدلا من التسبب فيه. المناطق المدمجة تجمع عروض النقاط لأعمدتها الأعضاء، فيمكن لدمج كان يسع صفحة واحدة في Excel أن يفيض بعد انحراف MDW، وهو ما يجدر تذكره حين تبني قوالب تقارير بخلايا مدمجة. والاحتواء بالتصغير يقارن عرض النص المقاس بعرض العمود نفسه، لذا فإن MDW الخاطئة تغيّر أيضا أي الخلايا تتقلص وبكم. عائلة ارتباك الوحدات نفسها تظهر في مراسي الرسوم، حيث لدى هندسة الصور وتحجيم EMU سلسلة تحويل خاصة بها لتُخطأ
كيف يقيس HotXLS قيمة MDW وقت التشغيل
يستنتج HotXLS قيمة MDW من المصنف نفسه بدلا من افتراض ثابت، وإجراءان يقومان بالعمل. PaginationApplyNormalFont يقرأ خط نمط Normal من المصنف ويعمل في بداية بناء ترقيم الصفحات، قبل حساب أي هندسة أعمدة؛ وهو يعيد الضبط إلى Calibri 11 أولا، كي لا يرث مصنف بلا جدول خطوط حالة بالية من بناء سابق. خط نمط Normal هو fonts[0] في styles.xml، ويكشفه المكوّن بوصفه Workbook.Fonts[0]
// يقرأ fonts[0] (خط نمط Normal) من مصنف ورقة العمل.
// أوراق العمل الكلاسيكية بلا جدول خطوط تبقى على افتراضي Calibri 11.
procedure PaginationApplyNormalFont(Worksheet: TObject);
var
Sh: TXLSXWorksheet;
Fnt: TXLSXFont;
begin
PaginationNormalFontName := 'Calibri';
PaginationNormalFontSize := 11;
if not (Worksheet is TXLSXWorksheet) then
Exit;
Sh := TXLSXWorksheet(Worksheet);
if (Sh.Workbook = nil) or (Sh.Workbook.Fonts.Count < 1) then
Exit;
Fnt := Sh.Workbook.Fonts[0];
if Fnt.Name <> '' then
PaginationNormalFontName := Fnt.Name;
if Fnt.Size > 0 then
PaginationNormalFontSize := Fnt.Size;
end;
الإجراء الثاني، PaginationMeasureMdW، يسأل GDI عن امتداد الحرف الوحيد '0' عبر GetTextExtentPoint32W على لوحة رسم نقطية مشتركة خارج الشاشة، ويرجع إلى tmAveCharWidth من GetTextMetricsW حين يفشل استدعاء الامتداد، ويرجع إلى DefaultMDW حين لا يتوفر أي منهما. ذاكرته المؤقتة فتحة واحدة مفتاحها (name, size)، وهو ما يبدو فجا حتى تنظر إلى نمط الوصول: بناء ترقيم الصفحات يطلب خط Normal نفسه على كل عمود من كل صفحة، لذا تحقق فتحة واحدة معدل إصابة شبه مثالي وتكلّف ثلاث مقارنات لكل استدعاء
ماذا يحدث بلا جدول خطوط أو بلا واجهة رسومية أو مع خط مفقود؟
يتدهور HotXLS إلى ثابت Calibri 11 في كل حالة يتعذر فيها تحديد خط Normal الحقيقي، ويفعل ذلك بصمت حسب التصميم. أوراق عمل BIFF الكلاسيكية هي الحالة الشائعة: الصيغ القديمة لا تحمل مجمّع خطوط XLSX يشير إليه fonts[0]، لذا يخرج حارس النوع مبكرا وتبقى MDW الافتراضية 7. هذا ليس إصلاحا، بل السلوك السابق محفوظا عمدا، كي لا تتراجع مخرجات الصيغة الكلاسيكية بسبب إضافة القياس إلى مسار XLSX
الاعتماد على GDI هو التحفّظ الصادق. القياس يعمل مقابل سياق جهاز Windows، لذا يفترض المسار مضيف Windows عليه الخط مثبتا. في خدمة أو وكيل بناء بلا شاشة، تُحَل مقاييس نص GDI عموما، لكن الخط غير المثبت على ذلك الجهاز يُستبدل عبر مخطّط الخطوط وتقيس البديل بدلا منه. لا يفشل بصوت عال أبدا؛ بل يُرجع رقما معقولا لخط خاطئ. وإن كان يجب أن تطابق الصادرات الخادمية مرجعا مكتبيا، فثبّت الخطوط التي تسمّيها قوالبك على مضيف التصدير، أو ثبّت خط Normal قبل استدعاء مسار تصدير ورقة العمل إلى PDF
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// ثبّت خط Normal كي تكون MDW المقاسة على هذا المضيف هي تلك
// التي صُمم التخطيط مقابلها، لا بديل مخطّط الخطوط.
if Book.Fonts.Count > 0 then
begin
Book.Fonts[0].Name := 'Calibri';
Book.Fonts[0].Size := 11;
end;
Exporter.UseWorksheetPageSetup := True;
Exporter.SaveAsPDF(Book, 'quarterly-report.pdf');
finally
Exporter.Free;
Book.Free;
end;
end;
ذاكرات القياس المؤقتة، وتلك التي تعطّلت على Win64
حين يصبح قياس النص رحلة ذهابا وإيابا إلى GDI بدلا من ضربة ضرب، يجب أن يُخزَّن مؤقتا، والتخزين المؤقت داخل مرحلة عرض هو المكان الذي سالت فيه الدماء في هذا العمل. حلقة الاحتواء بالتصغير تُنزل حجم الخط بزيادات 0.5 نقطة وتعيد القياس بعد كل خطوة، فيمكن لخلية واحدة أن تستدعي PaginationMeasureTextWidth اثنتي عشرة مرة بالسلسلة نفسها، والتفاف الكلمات يستدعيها مرة أخرى لكل سطر مرشح. مذكرة مفتاحها اسم الخط وحجمه والنص تطوي ذلك إلى استدعاء GDI واحد لكل سلسلة مميزة، مخزنة في TStringList كأزواج اسم/قيمة
الذاكرة المؤقتة الأخرى التي أُضيفت إلى جوارها لم تكن بهذا الترتيب. مرحلة العرض 5 تحل مجمّع الخطوط لكل خلية عبر FontIndex، ومذكرتها استخدمت مصفوفات ديناميكية متوازية مع FontMemoCount يُصان يدويا. النسخة الأولى نسيت استدعاء ResetFontMemo عند بداية كل صفحة، فظل العدّاد يتصاعد عبر الصفحات بينما المصفوفات لا تفعل، وكتب الكود بعد نهايتها كلها. على Win32 كتب ذلك بهدوء في الكومة المجاورة وانتهى؛ وعلى Win64 أثار انتهاك وصول عند كتابة إلى 0x538 فورا. الدرس القابل للتعميم: ذاكرة مؤقتة مسندة إلى مصفوفة محفوظة في متغير على مستوى الوحدة يجب أن تُصفَّر عند مدخل كل مرحلة تستخدمها، لأن قائمة سلاسل أو قاموسا يغفر تصفيرا مفقودا بالنمو، بينما المصفوفات المتوازية لا تغفر
التحقق من تحويلك بنفسك
لا تحتاج إلى المكوّن للتحقق من أي من هذا. خذ مصنفا خط Normal فيه ليس Calibri 11، واقرأ عرضا من <col width="..."/>، ومرّره عبر معادلة المواصفة مرتين، مرة بـMDW تساوي 7 ومرة بـMDW التي يقيسها عارضك فعلا لذلك الخط؛ إن اختلفت الإجابتان وطابق مخرجك الأولى، فقد وجدت الانحراف. هندسة الأعمدة أحد تلك الأجزاء من محرك جداول البيانات التي تكون إما غير مرئية أو الشيء الوحيد الذي يلاحظه أحد، وإصابتها تعني معاملة خط Normal كمدخل للتخطيط لا كتفصيلة تنسيق. وإن كنت تبني تطبيقات Delphi أو C++Builder تقرأ وتكتب وتعرض وتطبع مصنفات Excel دون تثبيت Office، فإن مكوّن HotXLS لـExcel في Delphi يتولى قياس MDW ونموذج ترقيم الصفحات وخط أنابيب PDF خلف مجموعة واحدة من فئات VCL