مقاله فنی

هندسهٔ تصویر HotXLS در Delphi: EMU، cm و Scaling

شما یک لوگوی 600×400 پیکسلی را در header یک invoice تولیدشده می‌گذارید، روی مانیتور توسعهٔ 96-DPI خودتان درست به نظر می‌رسد، و یک هفته بعد مشتری‌ای روی یک laptop با DPI بالا گزارش می‌دهد که آن را به اندازهٔ یک تمبر پستی چاپ می‌کند. پیکسل‌ها عوض نشده‌اند. چیزی که عوض شده فرضی است که می‌گوید شمارش پیکسل یعنی اندازهٔ فیزیکی، و در OOXML این‌طور نیست. یک تصویر spreadsheet ابعادش را در EMU حمل می‌کند، و تا وقتی در EMU - یا در واحدهای دنیای واقعی که به‌طور دقیق به آن نگاشت می‌شوند - فکر نکنید، layout شما در اختیار هر DPIای است که ماشین رندر کردن اتفاقی فرض کند

HotXLS یک مؤلفهٔ بومی VCL spreadsheet برای Delphi و C++Builder است که XLS و XLSX را بدون Excel یا هر وابستگی COM می‌خواند و می‌نویسد. از v2.91.0 به بعد object تصویر XLSX دیگر شما را مجبور نمی‌کند arithmetic واحد را دستی انجام دهید: در کنار EMU خام، عرض و ارتفاع را به centimetre، inch، و point نشان می‌دهد، به‌علاوهٔ یک Scale می‌دهد که با درصد resize می‌کند و در صورت نیاز aspect ratio را قفل می‌کند. این مقاله دربارهٔ این است که EMU واقعاً چیست، چرا DrawingML آن را انتخاب کرد، و چگونه از surface هندسهٔ تازه برای قرار دادن تصویرها بر اساس اندازهٔ فیزیکی استفاده کنید، نه بر اساس شمارش پیکلی که نمی‌توانید به آن اعتماد کنید

EMU چیست و چرا DrawingML از آن استفاده می‌کند

EMU مخفف English Metric Unit است، و واحد طول پایهٔ DrawingML، لایهٔ ترسیم مشترک در سراسر خانوادهٔ Office Open XML (ECMA-376، Part 1، §20) است. یک EMU طوری تعریف شده که دقیقاً 914400 EMU per inch و 360000 EMU per centimetre وجود داشته باشد. همین دو constant تمام دلیل وجود این واحد هستند. 914400 بر 2، 3، 4، 5، 6، 8، 9، 10، 12 و خیلی‌های دیگر بخش‌پذیر است؛ تجزیه‌اش می‌شود 26 × 32 × 52 × 127. چون 1 inch = 2.54 cm دقیقاً است، انتخاب واحدی که هم بر 360000 و هم بر یک کسر ساده از 914400 بخش‌پذیر باشد به format اجازه می‌دهد inch، centimetre، و point را به‌شکل integer بدون rounding در مرز واحد بیان کند. جایی که یک floating-point مثل "1.27 cm" drift می‌کند، EMU مقدار 457200 را ذخیره می‌کند و دقیق می‌ماند

واحد دیگری که اینجا اهمیت دارد point است. یک point تایپوگرافی 1/72 inch است، پس 12700 EMU per point (914400 / 72) وجود دارد. pointها همان چیزی هستند که خود Excel در زیرساخت برای ارتفاع ردیف، اندازهٔ فونت، و marginها به آن فکر می‌کند، و به همین دلیل نشان دادن هندسهٔ تصویر به point وقتی مفید است که بخواهید یک تصویر را با metrics متن هم‌راستا کنید نه با خط‌کش چاپی. HotXLS همهٔ این چهار رابطه را به‌صورت constantهای unit در library encode می‌کند:

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)

آن خط آخر هستهٔ bug تمبر پستی است. یک pixel فقط وقتی اندازهٔ فیزیکی دارد که DPI را ثابت کنید، و 9525 EMU اندازهٔ یک pixel در 96 DPI به‌طور خاص است. DPI رندر پیش‌فرض Excel 96 است، پس یک تصویر 100 پیکسلی روی setup پیش‌فرض به 100 × 9525 = 952500 EMU ≈ 2.54 cm می‌رسد - اما هیچ چیز در فایل تضمین نمی‌کند مصرف‌کننده از 96 استفاده کند. در واحدهای واقعی author کنید و آن ابهام ناپدید می‌شود: 4 cm، 4 cm است چه screen 96 DPI باشد چه 220 DPI

surface هندسهٔ TXLSXImage

یک تصویر embedded در HotXLS یک TXLSXImage است. storage canonical آن دو فیلد integer است، WidthEMU و HeightEMU، لنگر زده به یک Row یک‌مبنایی و Col (سلول بالای-چپ که تصویر از آن آویزان است). propertyهای real-unit viewهای محاسبه‌شده روی آن فیلدهای EMU هستند، نه state جداگانه - خواندن WidthCM EMU را بر 360000 تقسیم می‌کند، و نوشتن آن را ضرب و دوباره round می‌کند. پس هر بعدی که set می‌کنید فقط یک spelling دیگر از همان مقدار EMU زیربنایی است:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - منبع حقیقت integer

شما یک تصویر را با AddImage(ARow, ACol, AData, AFormat) اضافه می‌کنید، با دادن bytes خام encoded و یک TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, یا xlsxImageBmp)؛ این متد index یک‌مبنایی را به collection Images worksheet برمی‌گرداند. همچنین AddImageFromFile(ARow, ACol, AFileName) هم وجود دارد که format را از file extension حدس می‌زند. به base index دقت کنید: AddImage zero-based برمی‌گرداند و Images[] نیز zero-based است، که contrastی عمدی با grid یک‌مبنای Cells[Row, Col] است، پس فرض نکنید این دو با هم موافق‌اند

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 pixel است، یعنی 952500 EMU مربعی، تقریباً یک box 2.54 cm در 96 DPI. این default وجود دارد تا اگر sizing را فراموش کنید تصویر دیده شود، اما برای هر layout واقعی باید به‌جای تکیه بر default پیکلی، یک اندازهٔ فیزیکی صریح تنظیم کنید

Scaling، و flag نسبت aspect

وقتی می‌خواهید به‌جای target مطلق، نسبت به ابعاد فعلی resize کنید - مثلاً یک تصویر chart را به 60٪ هر چیزی که با آن import شده کوچک کنید - از Scale استفاده کنید:

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

APercent یک percentage است که 100 یعنی بدون تغییر، 150 نصفی بزرگ‌تر می‌کند، 50 نصف می‌کند. با AKeepAspect در حالت پیش‌فرض True، هم عرض و هم ارتفاع با همان factor ضرب می‌شوند، پس proportions حفظ می‌شوند و یک تصویر 4×3 cm بعد از Scale(150) به 6×4.5 cm تبدیل می‌شود. False بدهید و فقط عرض scale می‌شود، ارتفاع دقیقاً همان‌طور که بود باقی می‌ماند. آن asymmetry عمدی است: وقتی می‌خواهید یک axis را مستقل بکشید، ابزار درست setterهای صریح WidthCM و HeightCM است، و branch غیر-aspect در Scale برای حالت محدودترِ تنظیم فقط عرض آنجا قرار داده شده است. خواندن Scale(150, False) به‌عنوان «هر دو را آزاد stretch کن» آسان است و می‌تواند غافلگیرتان کند، پس وقتی واقعاً منظورتان دو بعد مستقل است به سراغ setterها بروید

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 باشد short-circuit می‌کند و بدون دست زدن به هیچ فیلدی برمی‌گردد، پس در loopهایی که درصد ممکن است 100 باشد می‌توان آن را بی‌قید و شرط صدا زد. و چون هندسه به‌صورت EMU integer ذخیره می‌شود، هر setter round می‌کند. round-trip از centimetreهای کسری بنابراین می‌تواند به‌اندازهٔ کسری از یک EMU drift کند - بسیار پایین‌تر از هر چیزی که دیده شود، اما اگر جایی exact equality را در یک test assert می‌کنید دانستنش مفید است. برای کنترل pixel-perfect، WidthEMU و WidthEMUHeightEMUHeightEMU را مستقیم set کنید و تبدیل واحد را کاملاً کنار بگذارید

خواندن هندسه به عقب

مجموعهٔ تصویر queryable است، و این مهم است وقتی workbook موجودی را load می‌کنید و باید چیزی را که از قبل آنجا هست inspect یا adjust کنید، نه چیزی را که تازه اضافه کرده‌اید. Images.Count هر تصویر را روی sheet enumerate می‌کند، Images[i] zero-based آنها را index می‌کند، و FindAt(ARow, ACol) تصویر را که در یک cell مشخص anchor شده برمی‌گرداند - یا nil اگر هیچ‌کدام نباشد. همچنین IndexOfCell برای index به‌جای object وجود دارد، و DeleteAt / DeleteInRange برای removal

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;

چون propertyهای real-unit viewهای زنده‌اند، تصویری که از ابزار دیگری با یک اندازهٔ EMU مشخص import شده، هندسهٔ خود را فوراً به centimetre گزارش می‌کند - بدون هیچ conversion مرحلهٔ اضافی از سمت شما. این به‌طور طبیعی با model ترسیم بزرگ‌تر جفت می‌شود؛ اگر در کنار raster imageها chart و shape هم place می‌کنید، راهنمای همراه دربارهٔ HotXLS charts, images, and Excel drawings in Delphi model anchorای را که این objectها مشترک دارند پوشش می‌دهد

marginهای صفحهٔ metric

همان tension بین EMU و real-unit یک لایه بیرون‌تر، در page، هم دیده می‌شود. OOXML و Excel marginهای print را در inches ذخیره می‌کنند، که اگر templateهای report شما مثل بیشتر نقاط بیرون از US بر حسب millimetre مشخص شده باشند awkward است. v2.91.0 wrapperهای centimetre را روی marginهای inch اضافه می‌کند: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, و MarginFooterCM. هر کدام یک convenience نازک روی property inch متناظر است که با نسبت دقیق 1 inch = 2.54 cm تبدیل می‌کند

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;

propertyهای inch (MarginLeft و بقیه) storage canonical می‌مانند، پس می‌توانید این دو را mix کنید - یک top margin را به centimetre set کنید و بعد آن را به inch بخوانید، یا برعکس - و فایلی که روی disk نوشته می‌شود در هر دو حالت یکسان است. conversion یک ضرب ساده در 2.54 است، بدون rounding به یک grid خشن، پس 2 cm تا دقت کامل double precision همان 2 cm می‌ماند. این همان فلسفهٔ metric-convenience هندسهٔ تصویر است: format زیرِ درپوش imperial حرف می‌زند، و library به شما اجازه می‌دهد در هر واحدی که specification شما نوشته شده author کنید. برای lay کردن گزارش اطراف آن - titleها، metadata blockها، totalها - به cells ادغام‌شده و layout template گزارش در HotXLS نگاه کنید، که از این marginها همراه با rangeهای merged و یک print area استفاده می‌کند

یک یادداشت دربارهٔ اینکه هندسه چه چیزی را تضمین می‌کند و چه چیزی را نه

propertyهای هندسه اعلام‌شدهٔ اندازهٔ image را در فایل کنترل می‌کنند - همان اندازه‌ای که یک consumer سازگار آن را render خواهد کرد. آن‌ها bytes تصویر را resample نمی‌کنند؛ یک PNG پنجاه‌درپنجاه پیکسلی که به 8 cm size داده شود، بزرگ می‌شود و blocky به نظر می‌رسد، دقیقاً همان‌طور که در Excel هم همین‌طور بود. sizing یک operation layout است، نه یک operation image-processing، پس به تصویر به‌اندازهٔ کافی resolution منبع برای اندازهٔ فیزیکی‌ای که قصد دارید بدهید. library همچنین formatها را دوباره encode نمی‌کند: bytesی که به AddImage می‌دهید همان‌طور stored و written through می‌شوند، همراه با TXLSXImageFormatی که اعلام می‌کنید. bytes JPEG بدهید اما آن‌ها را xlsxImagePng برچسب بزنید و یک فایل تولید می‌کنید که Excel نمی‌تواند باز کند، پس وقتی می‌توانید بگذارید AddImageFromFile format را از extension infer کند

همهٔ این‌ها وقتی آن ایدهٔ زیرش را درونی کنید exotic نیست: در OOXML اندازهٔ فیزیکی quantity واقعی است و pixelها سایه‌ای مشتق‌شده و وابسته به DPI از آن هستند. تصویرها و marginها را در centimetre، inch، یا point author کنید، بگذارید HotXLS آن‌ها را روی EMU دقیق map کند، و invoiceها و reportهای شما در هر ماشینی که بازشان می‌کند همان اندازه چاپ شوند

image geometry، scaling، و APIهای metric-margin که اینجا توصیف شدند همراه با HotXLS Delphi spreadsheet component ship می‌شوند، که XLS و XLSX را از Delphi و C++Builder می‌خواند و می‌نویسد بدون اینکه نصب Excel لازم باشد