مقاله فنی

عرض ستون Excel و Max Digit Width (MDW) در Delphi

PDF خروجی مرز هر ستون را نیم‌کاراکتر سمت چپِ جایی که Excel آن را می‌کشد می‌گذارد، و حالا هر سلول شکسته‌شده در جای متفاوتی می‌شکند. عرض ستون Excel نه به کاراکتر سنجیده می‌شود و نه به پوینت. به واحد Max Digit Width (MDW) فونت Normal کتاب‌کار سنجیده می‌شود، و HotXLS آن فونت را با GDI پیش از هر build صفحه‌بندی اندازه می‌گیرد. حالت خرابی بی‌سروصداست: هیچ چیزی پرتاب نمی‌شود، عرض‌های ذخیره‌شده بایت به بایت سالم برمی‌گردند، و هندسه همچنان به ازای هر ستون چند درصد خطا دارد تا وقتی drift انباشته یک جدول یک‌صفحه‌ای را به دو صفحه هل بدهد

واحد عرض ستون Excel چیست؟

عرض ستون در یک worksheet شمارش کاراکترهای رقم فونت Normal کتاب‌کار است، نه یک اندازه‌گیری مطلق. ECMA-376 §18.3.1.13 اتریبیوت width عنصر <col> را بر مبنای Maximum Digit Width همان فونت در 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 پیکسل اندازه گرفته می‌شود که دقیقاً عرض‌های پیکسلی
  // را بازتولید می‌کند که Excel ذخیره می‌کند (8.43 واحد -> 64 پیکسل -> 48 پوینت).
  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 به آن دلیل هست که یک ستون به‌شدت باریک هم همچنان نواری باقی بگذارد که renderer بتواند داخلش خط بکشد. مدخل عمومی ColumnWidthToPoints امضای تک‌آرگومانی قدیمی‌اش را نگه می‌دارد و یک MDW اندازه‌گیری‌شده به این تابع می‌فرستد، و همین چیزی بود که اجازه داد تغییر رفتار بدون دست زدن به حتی یک call site فرود بیاید

زنجیره تبدیل عرض ستون HotXLS در Delphi، که Max Digit Width اندازه‌گیری‌شده فونت Normal کتاب‌کار را به فرمول مشخصات می‌دهد تا عرض ذخیره‌شده 8.43 واحدی به 64 پیکسل و سپس 48 پوینت تبدیل شود
عرض ذخیره‌شده یک شمارش رقم است، پس MDW اندازه‌گیری‌شده فونت Normal ورودی فرمول است نه یک جزئیات استایل، و رفت‌وبرگشت 8.43 به 64 به 48 حساب را چک می‌کند

چرا فونت Normal غیر از Calibri همه مرزها را جابه‌جا می‌کند

drift ضربی است، و به همین دلیل است که خوانده می‌شود به‌شکل باگ رندرینگ نه باگ واحدها. MDW یک ضریب روی عرض است، نه یک آفست. MDW را از 7 به 8 ببرید و ستون پیش‌فرض 8.43 واحدی از 64 پیکسل به 72 می‌رود، یک جهش 8 پیکسلی یا 6 پوینتی روی یک ستون. ده ستون از این و لبه راست جدول تقریباً یک اینچ جابه‌جا شده است. کتاب‌کارهایی که این را فعال می‌کنند کاملاً معمولی‌اند: هر چیزی که یک ابزار گزارش‌ساز با درج Arial یا Segoe UI در استایل Normal تولید کرده باشد، هر چیزی که از یک قالب خروجی ERP ذخیره شده باشد، هر چیزی که یک مشتری یک بار restyle کرده و فراموشش کرده باشد

دو سیستم چیدمان مرتبط خطا را به ارث می‌برند نه اینکه آن را بسازند. ناحیه‌های ادغام‌شده عرض‌های پوینتی ستون‌های عضوشان را جمع می‌زنند، پس ادغامی که در Excel در یک صفحه جا می‌شد می‌تواند بعد از drift با MDW سرریز کند، و این ارزش به یاد داشتن را دارد وقتی قالب‌های گزارش سلول ادغام‌شده می‌سازید. Shrink-to-fit عرض متن اندازه‌گیری‌شده را با همان عرض ستون مقایسه می‌کند، پس MDW غلط هم عوض می‌کند کدام سلول‌ها کوچک شوند و چقدر. همان خانواده سردرگمی واحدی در drawing anchorها هم سر باز می‌کند، جایی که هندسه تصویر و مقیاس‌بندی EMU زنجیره تبدیل خودش را برای غلط کردن دارد

مقایسه دو خط‌کش ستون HotXLS، یکی با MDW برابر 7 پیکسل و یکی با 8 اندازه‌گیری شده، که نشان می‌دهد جهش به ازای هر ستون از 64 به 72 پیکسل چطور روی ده ستون انباشته می‌شود در حالی که ناحیه‌های ادغام‌شده و shrink-to-fit خطا را به ارث می‌برند
چون MDW ضرب می‌کند نه آفست، یک اندازه‌گیری غلط همه مرزهای ستون را جابه‌جا می‌کند، و ناحیه‌های ادغام‌شده و shrink-to-fit بدون هیچ پرتاب استثنایی drift را به ارث می‌برند

HotXLS چطور در زمان اجرا MDW را اندازه می‌گیرد

HotXLS مقدار MDW را از خود کتاب‌کار حل می‌کند نه اینکه یک ثابت را فرض کند، و دو رویه این کار را انجام می‌دهند. PaginationApplyNormalFont فونت استایل Normal را از کتاب‌کار می‌خواند و در بالای build صفحه‌بندی اجرا می‌شود، پیش از محاسبه هر هندسه ستونی؛ اول به Calibri 11 ریست می‌کند، پس کتاب‌کاری بدون جدول فونت نمی‌تواند state کهنه از یک build قبلی به ارث ببرد. فونت استایل Normal همان fonts[0] در styles.xml است که کامپوننت آن را به‌شکل Workbook.Fonts[0] در معرض می‌گذارد

// fonts[0] یعنی فونت استایل Normal را از کتاب‌کار worksheet می‌خواند.
// worksheetهای کلاسیک بدون جدول فونت پیش‌فرض 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 روی یک bitmap canvas آفسکرین مشترک می‌پرسد، وقتی فراخوانی گستره شکست خورد به tmAveCharWidth از GetTextMetricsW برمی‌گردد، و وقتی هیچ‌کدام در دسترس نیست به DefaultMDW. کش آن یک اسلات تکی با کلید (name, size) است، که خام به نظر می‌رسد تا وقتی به الگوی دسترسی نگاه کنید: یک build صفحه‌بندی همان فونت Normal را روی هر ستون هر صفحه می‌پرسد، پس یک اسلات نرخ hit تقریباً کامل دارد و سه مقایسه به ازای هر فراخوانی هزینه دارد

با نبود جدول فونت، نبود GUI یا نبود فونت چه می‌شود؟

HotXLS در هر حالتی که فونت Normal واقعی قابل تعیین نباشد به ثابت Calibri 11 تنزل می‌یابد، و این کار را طبق طراحی بی‌سروصدا انجام می‌دهد. worksheetهای کلاسیک BIFF مورد رایج‌اند: قالب‌های قدیمی هیچ استخر فونت XLSX برای ارجاع fonts[0] حمل نمی‌کنند، پس گارد نوع زود خارج می‌شود و MDW پیش‌فرض 7 برقرار می‌ماند. این یک فیکس نیست، رفتار قبلی است که عمداً حفظ شده، تا اضافه کردن اندازه‌گیری به مسیر XLSX نتواند خروجی قالب کلاسیک را regress کند

وابستگی به GDI همان هشدار صادقانه است. اندازه‌گیری روی یک device context ویندوزی اجرا می‌شود، پس این مسیر فرض می‌کند میزبان ویندوزی با فونت نصب‌شده است. در یک سرویس یا build agent بی‌سر، متریک‌های متنی GDI معمولاً همچنان حل می‌شوند، اما فونتی که روی آن ماشین نصب نیست توسط font mapper جایگزین می‌شود و شما به‌جای آن جایگزین را اندازه می‌گیرید. هرگز به‌شکل بلند شکست نمی‌خورد؛ برای typeface غلط یک عدد محتمل برمی‌گرداند. اگر خروجی‌های سمت سرور باید با یک مرجع دسکتاپ بخوانند، فونت‌هایی که قالب‌هایتان نام می‌برند را روی میزبان خروجی نصب کنید، یا فونت Normal را پیش از فراخوانی مسیر خروجی PDF worksheet پین کنید

var
  Book: TXLSXWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSXWorkbook.Create;
  Exporter := TXLSPDFExport.Create;
  try
    Book.Open('quarterly-report.xlsx');

    // فونت Normal را پین کنید تا MDW اندازه‌گیری‌شده روی این میزبان همان
    // چیزی باشد که چیدمان برایش طراحی شده، نه جایگزین font-mapper.
    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 می‌شود نه یک ضرب، باید کش شود، و کش کردن داخل یک render pass همان‌جایی است که این کار خون داد. حلقه shrink-to-fit اندازه فونت را با گام‌های 0.5 پوینتی پایین می‌برد و بعد از هر گام دوباره اندازه می‌گیرد، پس یک سلول می‌تواند PaginationMeasureTextWidth را یک دوجین بار با همان رشته صدا بزند، و word wrap به ازای هر خط کاندید دوباره صدایش می‌زند. یک memo با کلید نام فونت و اندازه و متن آن را به یک فراخوانی GDI به ازای هر رشته متمایز فرو می‌کاهد، که در یک TStringList به‌شکل جفت‌های name/value ذخیره می‌شود

کش دیگری که در کنارش اضافه شد آن‌قدرها مرتب نبود. Render pass 5 استخر فونت را به ازای هر سلول با FontIndex حل می‌کند، و memo آن از آرایه‌های پویای موازی با FontMemoCount دستی استفاده می‌کرد. نسخه اول فراموش کرده بود ResetFontMemo را در ابتدای هر صفحه صدا بزند، پس شمارنده روی صفحه‌ها بالا می‌رفت در حالی که آرایه‌ها نمی‌رفتند، و کد از انتهای همه آنها فراتر می‌نوشت. روی Win32 این بی‌سروصدا داخل heap مجاور خط خطی می‌کرد و تمام؛ روی Win64 بلافاصله یک access violation روی نوشتن به 0x538 پرتاب می‌کرد. درس تعمیم‌پذیر: یک کش پشتیبانی‌شده با آرایه که در متغیری در سطح واحد نگه داشته می‌شود باید در ورود هر passی که از آن استفاده می‌کند ریست شود، چون یک string list یا dictionary ریست جاافتاده را با رشد می‌بخشد و آرایه‌های موازی نمی‌بخشند

HotXLS چطور فونت Normal کتاب‌کار را حل می‌کند، Max Digit Width آن را از طریق GDI با دو فالبک اندازه می‌گیرد و نتیجه را کش می‌کند، در کنار دو memo مسیر رندر و قاعده ریستی که یک کش آرایه‌موازی به آن نیاز دارد
MDW از کتاب‌کار حل می‌شود و به ازای هر فونت یک بار با GDI اندازه گرفته و بعد با کلید کش می‌شود، در حالی که memoهای render pass نشان می‌دهند چرا یک کش آرایه‌موازی باید در ورود هر pass ریست شود

چک کردن تبدیل خودتان

برای راستی‌آزمایی هیچ‌کدام از این‌ها به کامپوننت نیاز ندارید. کتاب‌کاری بردارید که فونت Normal آن Calibri 11 نیست، یک عرض از <col width="..."/> بخوانید، و دو بار از فرمول مشخصات عبورش دهید، یک بار با MDW 7 و یک بار با MDWای که renderer شما واقعاً برای آن فونت اندازه می‌گیرد؛ اگر جواب‌ها فرق کردند و خروجی شما با مورد اول خواند، drift را پیدا کرده‌اید. هندسه ستون یکی از آن بخش‌های موتور صفحه‌گسترده است که یا نامرئی است یا تنها چیزی که همه متوجه می‌شوند، و درست درآوردنش یعنی فونت Normal را ورودی چیدمان ببینید نه یک جزئیات استایل. اگر برنامه‌های Delphi یا C++Builder می‌سازید که بدون نصب Office کتاب‌کارهای Excel را می‌خوانند، می‌نویسند، رندر و چاپ می‌کنند، HotXLS Delphi Excel component اندازه‌گیری MDW و مدل صفحه‌بندی و پایپ‌لاین PDF را پشت یک مجموعه کلاس VCL انجام می‌دهد