مقاله فنی

باز کردن و ذخیره اسپردشیت‌های ODS در Delphi با HotXLS

یک بک‌اند گزارش‌دهی در Delphi که سال‌ها خروجی .xlsx تولید کرده، حالا با یک نیازمندی جدید روبه‌رو می‌شود: قوانین تدارکات یک مشتری بخش دولتی، خروجی OpenDocument Spreadsheet را الزامی می‌کند، و تحلیل‌گران آن حساب ویرایش‌های خود را به‌صورت فایل‌های .ods ذخیره‌شده از LibreOffice برمی‌گردانند. پس حالا همان کد باید هم ODS بنویسد و هم آن را بخواند. HotXLS، کتابخانهٔ بومی صفحه‌گستردهٔ Object Pascal از losLab برای Delphi و C++Builder، هر دو جهت را بدون نصب اکسل یا LibreOffice در هیچ‌جا انجام می‌دهد. کاری که HotXLS نمی‌کند این است که این دو جهت را متقارن کند. خروجی (export) بسیار بیشتر از چیزی است که ورودی (import) بازیابی می‌کند، و تیمی که خلاف این را فرض کند، خواهد دید فرمول‌ها و قالب‌بندی جایی میان بازبینی مشتری و گزارش بعدی ناپدید می‌شوند، بدون این‌که هیچ خطایی برای اشاره وجود داشته باشد

پشتیبانی ODS روی facade مربوط به XLSX است، نه XLS

HotXLS دو سلسله‌مراتب کلاس مستقل را در یک بستهٔ واحد ارائه می‌دهد: TXLSWorkbook در unit به‌نام lxHandle برای فایل‌های باینری BIFF8 با پسوند .xls، و TXLSXWorkbook در unit به‌نام lxHandleX برای بسته‌های OOXML با پسوند .xlsx. هر نقطهٔ ورود OpenDocument - یعنی OpenODS، SaveAsODS، GetODSSheetNames - به TXLSXWorkbook وابسته است. این جایگذاری تصادفی نیست. یک بستهٔ ODS، طبق مشخصات OASIS ODF 1.3، یک آرشیو zip است که یک عضو mimetype، یک manifest و یک بدنهٔ content.xml را حمل می‌کند، که این آن را از نظر ساختاری خویشاوند zip مربوط به OOXML می‌سازد؛ BIFF8 یک جریان رکورد باینری متعلق به دههٔ ۱۹۹۰ است که هیچ وجه اشتراکی با آن ندارد

این جایگذاری یک اثر عملی هم دارد: یک ورک‌بوک قدیمی .xls نمی‌تواند در یک فراخوانی به .ods تبدیل شود. ابتدا باید محتوای BIFF را با SaveXLSWorkbookAsXLSX از unit به‌نام lxXlsxExport به مدل XLSX پل بزنید، نتیجه را از طریق TXLSXWorkbook دوباره باز کنید، و سپس از آن‌جا خروجی بگیرید. این پل بدون افت اطلاعات نیست، و پیش از این‌که چیزی روی آن بسازید، ارزش دارد شکاف‌هایش را بشناسید. این پل مقادیر، فرمول‌ها، قالب‌های عددی، فونت‌ها، پرکردن‌ها (fills) و عرض ستون‌ها را کپی می‌کند. حاشیه‌ها (borders)، بازه‌های merge‌شده، کامنت‌ها، نمودارها و قالب‌بندی شرطی را حذف می‌کند. یک منبع .xls با قالب‌بندی سنگین، به ODS ساده‌تر از چیزی که بوده می‌رسد، و این ویژگی خود پل است، نه نویسندهٔ ODS

تشخیص در سمت ورودی خودکار است. متد سادهٔ Open یک بستهٔ ODS را از روی عضو mimetype آن تشخیص می‌دهد و در صورت نبود آن عضو، به بررسی content.xml در سطح بالا برمی‌گردد، پس یک مسیر کد عمومیِ «هرچه کاربر آپلود کرده را باز کن» نیازی به بو کشیدن (sniffing) پسوند فایل ندارد. پس از باز شدن، property به‌نام SourceFormat گزارش می‌دهد کدام شاخه فعال شده است

دیاگرام چیدمان کلاس HotXLS در Delphi؛ هر نقطهٔ ورود ODS روی TXLSXWorkbook نشسته و پل SaveXLSWorkbookAsXLSX محتوای ‎.xls با BIFF8 را عبور می‌دهد
هر نقطه ورود OpenDocument به TXLSXWorkbook آویزان است، و یک .xls قدیمی فقط از طریق پل BIFF به XLSXِ با‌اتلاف به ODS می‌رسد

خروجی گرفتن به ODS با TODSExportOptions

خود فراخوانی export فقط یک خط است؛ شیء options اطراف آن، تصمیم‌هایی را حمل می‌کند که یک reviewer بعداً دربارهٔ آن‌ها می‌پرسد:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // فراخواننده مالک آن است و آن را آزاد می‌کند
    try
      Opts.Generator := 'ReportService 4.2'; // بازنویسی meta:generator
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

مالکیت شیء options با فراخواننده است. HotXLS آن را آزاد نمی‌کند، و به همین دلیل try..finally داخلی آن‌جا اختیاری نیست، بلکه لازم است. دو property‌ای که واقعاً خروجی را تغییر می‌دهند، نه فقط برچسب‌گذاری‌اش می‌کنند، ارزش نگاه دقیق‌تری دارند. تنظیم IncludeCharts := False بیش از پنهان‌کردن نمودارها انجام می‌دهد: زیرسندهای نمودار و ورودی‌های manifest متناظرشان را از بسته حذف می‌کند، که دقیقاً همان چیزی است که وقتی مصرف‌کننده یک خط لولهٔ داده (data pipeline) است که با آن‌ها به مشکل برمی‌خورد، می‌خواهید. Generator رشتهٔ meta:generator در ODF را بازنویسی می‌کند که در غیر این‌صورت HotXLS/<version> نمایش می‌دهد؛ آن را زمانی بازنویسی کنید که ابزارهای پایین‌دستی از روی تولیدکنندهٔ فایل انگشت‌نگاری می‌کنند تا پشتیبانی را مسیریابی کنند. اگر هیچ‌کدام از این‌ها صدق نمی‌کند، شیء options را کاملاً کنار بگذارید. فراخوانی SaveAs(FileName, xlsxOpenDocumentSpreadsheet) همان SaveAsODS با مقادیر پیش‌فرض است، و overloadهای stream روی هر دو به شما اجازه می‌دهند بسته را مستقیماً داخل یک پاسخ HTTP بدون فایل موقت بنویسید

مسیر ورودی چه چیزی را می‌خواند - و چه چیزی را عمداً نادیده می‌گیرد

این بخش را با دقت بخوانید پیش از این‌که به کسی وفاداری round-trip قول بدهید. ورودی ODS در HotXLS عمداً یک مسیر سبک‌وزن است. مقادیر عددی/متنی سلول‌ها و نتیجهٔ کش‌شده‌ای که هر فرمول در زمان ذخیره حمل می‌کرد را حفظ می‌کند، و ردیف‌ها و ستون‌های تکرارشونده را در شبکه باز می‌کند. سبک‌ها، عبارت‌های فرمول ODS یا drawingها را منتقل نمی‌کند

انتخابی که دربارهٔ فرمول‌ها انجام شده، همان چیزی است که احتمال بیشتری دارد گازتان بگیرد، و این عمدی انتخاب شده است. یک سلول ODF دو چیز را کنار هم ذخیره می‌کند: عبارت فرمول، نوشته‌شده به گویش OpenFormula که در ODF 1.3 Part 4 تعریف شده، و آخرین مقداری که اپلیکیشن تولیدکننده برایش محاسبه کرده است. ترجمهٔ OpenFormula به نحو فرمول اکسل، خود یک مسئلهٔ تبدیل گویش جداگانه است، با موارد خاص واقعی دربارهٔ واژگان توابع، نحو ارجاع و مدل‌های خطا. به‌جای آن، خواندن مقدار کش‌شده کل آن دستهٔ ترجمهٔ نادرست خاموش را دور می‌زند، پس عددهایی که ایمپورت می‌کنید دقیقاً همان عددهایی‌اند که فرستنده آخرین بار دیده است. هزینه‌اش این است که آن‌ها به‌صورت عدد می‌رسند، نه به‌صورت فرمول‌های زنده‌ای که آن‌ها را تولید کرده‌اند

حالت شکستی که باید طراحی خود را حول آن بسازید مستقیماً از این‌جا می‌آید: یک صفحه‌گسترده که جمع‌های آن وقتی LibreOffice آخرین بار آن را ذخیره کرد درست بودند، با عددهای درست ایمپورت می‌شود، اما آن عددها اکنون ثابت‌اند. یک سلول ورودی را ویرایش کنید، دوباره‌محاسبه کنید، و هیچ‌چیز تکان نمی‌خورد - فرمول رفته، فقط نتیجهٔ نهایی آن باقی مانده است. اگر گردش‌کار پس از ایمپورت به فرمول‌های زنده نیاز دارد، آن‌ها را به‌صورت برنامه‌نویسی‌شده و از قواعد کسب‌وکاری خودتان از طریق Cell.Formula دوباره برقرار کنید، که در facade مربوط به XLSX عبارت را بدون علامت مساوی پیشرو می‌گیرد

طراحی حول round-trip نامتقارن

خروجی از روی مدل کامل ورک‌بوک درون‌حافظه رندر می‌شود: مقادیر، سبک‌ها، و در صورت درخواست، نمودارها و تصاویر. ورودی فقط مقادیر را برمی‌گرداند. پس مسیر .xlsx به .ods وفاداری بالایی دارد، و مسیر .ods به .xlsx مقادیر و نتایج کش‌شده را برمی‌گرداند اما نه قالب‌بندی و نه فرمول‌های زنده. این دو را زنجیره کنید و نامتقارنی تشدید می‌شود. یک چرخهٔ کامل .xlsx به .ods و دوباره به .xlsx، هنگام رفتن همه‌چیز را وفادارانه می‌نویسد و هنگام برگشت سبک‌ها و فرمول‌ها را از دست می‌دهد، حتی با این‌که در هیچ مرحله‌ای مشکلی پیش نیامده است

دیاگرام رفت‌وبرگشت نامتقارن ODS در HotXLS از Delphi؛ برون‌بری تمام‌وفاداری از مدل کتاب‌کار درون‌حافظه‌ای و درون‌ریزی فقط-مقادیر که فرمول‌ها را به‌صورت ثابت باقی می‌گذارد
Export مدل کامل درون‌حافظه را رندر می‌کند، در حالی که import مقادیر و نتایج کش‌شده را برمی‌گرداند، پس یک چرخه کامل .xlsx به .ods به .xlsx بی‌سروصدا سبک‌ها و فرمول‌های زنده را می‌اندازد
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // قالب به‌طور خودکار تشخیص داده می‌شود
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // مقادیر و نتایج کش‌شده فرمول پس از یک ایمپورت ODS حاضرند؛
    // سبک‌ها و فرمول‌های زنده حاضر نیستند. هر چیزی که خط لوله
    // پایین‌دستی به آن وابسته است را پیش از ذخیره بازسازی کنید.
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

الگوی معماری‌ای که از همین‌جا نتیجه می‌شود این است: با فایل‌های .ods ورودی مانند خوراک داده (data feed) رفتار کنید، نه مانند سندهایی که باید در جا ویرایش شوند. ورک‌بوک canonical را در .xlsx نگه دارید، مقادیر را از بازبینی‌های مشتری بخوانید، و بر اساس تقاضا از روی نسخهٔ canonical یک ODS تازه تولید کنید. بازبینی باید در هر دو اردوگاه انجام شود - فایل‌های صادرشده را هم در LibreOffice Calc، مصرف‌کنندهٔ مرجع ODF، و هم در اکسل باز کنید، که سال‌ها است ODS را می‌خواند اما در لبه‌های پشتیبانی نمودار و سبک با LibreOffice اختلاف دارد. تعداد شیت‌ها، چند سلول کلیدی و حضور نمودار، یک بررسی دودی (smoke check) کافی برای هر پروفایل export می‌سازند

غربالگری یک فایل ODS پیش از تعهد به یک ایمپورت

وقتی یک endpoint آپلود می‌پذیرد، فهرست‌کردن نام شیت‌ها بسیار ارزان‌تر از یک parse کامل است و شگفتی‌های ساختاری را زودتر می‌گیرد:

دیاگرام دروازهٔ دسته‌بندی بارگذاری HotXLS در Delphi؛ GetODSSheetNames بسته‌های ODS ناخوانا و برگه‌های غایب را پیش از اجرای درون‌ریزی کامل رد می‌کند
یک پروب GetODSSheetNames بسیار ارزان‌تر از یک تجزیه کامل است و شکست کاربرگ-تغییرنام‌یافته را در لحظه‌ای می‌گیرد که خطا هنوز می‌تواند نام فایل را بگوید
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
    raise Exception.Create('not a readable ODS package');
  if Names.IndexOf('Data') < 0 then
    raise Exception.Create('revision is missing the Data sheet');
finally
  Book.Free;
  Names.Free;
end;

قرارداد مقدار بازگشتی افراد را غافلگیر می‌کند: فراخوانی‌های HotXLS معمولاً در صورت موفقیت یک شمارش مثبت یا ۱ برمی‌گردانند و در صورت شکست -۱، و در حالت شکست لیست را پاک می‌کنند، پس به‌جای مقایسه با یک مقدار مثبت مشخص، <= 0 را آزمون کنید. GetODSSheetNames نمونهٔ workbook را نه ریست می‌کند و نه پر می‌کند، پس یک شیء probe واحد می‌تواند کل یک پوشه از فایل‌های ورودی را بررسی کند. بررسی‌های ساختاری از این دست، رایج‌ترین خطای دنیای واقعی - یک تحلیل‌گر که پیش از فرستادن بازبینی، شیتی را تغییرنام داده یا حذف کرده - را دم دروازه می‌گیرند، جایی که پیام خطا هنوز می‌تواند نام فایل و شیت گمشده را ذکر کند، به‌جای این‌که سه لایه پایین‌تر به‌صورت یک ارجاع nil ظاهر شود

اگر دارید یک خط لولهٔ تبدیل گسترده‌تر حول این موضوع می‌سازید، الگوی میزکار ممیزی و تبدیل ورک‌بوک نشان می‌دهد چگونه پیش از انتخاب فرمت مقصد، ویژگی‌های یک فایل را فهرست‌برداری کنید، و راهنمای کارایی ورک‌بوک‌های بزرگ خروجی‌های دسته‌ای را در محدودهٔ حافظهٔ معقول نگه می‌دارد

HotXLS یک کتابخانهٔ بومی صفحه‌گستردهٔ Delphi و C++Builder با کد منبع کامل است؛ فهرست کامل ویژگی‌ها و جزئیات لایسنس در صفحهٔ محصول HotXLS Delphi Component موجود است