Техническа статия

Геометрия на изображенията в HotXLS в Delphi: EMU, cm и мащабиране

Пускате лого 600×400 пиксела в header-а на генерирана фактура, изглежда правилно на 96-DPI монитора ви, а седмица по-късно клиент на high-DPI лаптоп съобщава, че се отпечатва с размера на пощенска марка. Пикселите не са се променили. Променило се е предположението, че броят на пикселите означава физически размер, а в OOXML това не е така. Spreadsheet image-ът носи размерите си в EMU и докато не мислите в EMU или в реалните единици, които се напасват към него, оформлението ви зависи от DPI-то, което рендър машината случайно приема

HotXLS е нативен VCL spreadsheet компонент за Delphi и C++Builder, който чете и записва XLS и XLSX без Excel или COM dependency. От v2.91.0 нататък XLSX image object-ът вече не ви кара да правите сметките за единиците на ръка: наред със суровите EMU той излага ширина и височина в сантиметри, инчове и points, плюс метод за мащабиране по процент с опционално заключване на aspect ratio-то. Тази статия е за това какво всъщност е EMU, защо DrawingML го е избрал и как да използвате новата геометрична повърхност, за да поставяте картинки по физически размер, вместо по броя пиксели, на който не можете да разчитате.Scale Тази статия е за това какво всъщност е EMU, защо DrawingML го е избрал и как да използвате новата геометрична повърхност, за да поставяте картинки по физически размер, вместо по броя пиксели, на който не можете да разчитате

Какво е EMU и защо DrawingML използва такъв

EMU означава English Metric Unit и е основната единица за дължина на DrawingML, слоя за рисуване, споделен от цялото семейство Office Open XML (ECMA-376, Part 1, §20). Един EMU е дефиниран така, че да има точно 914400 EMU на инч и 360000 EMU на сантиметър. Тези две константи са цялата причина тази единица да съществува. 914400 се дели на 2, 3, 4, 5, 6, 8, 9, 10, 12 и още много; той се разлага като 26 × 32 × 52 × 127. Понеже 1 inch = 2.54 cm точно, избирайки единица, делима и на 360000, и на чиста част от 914400, форматът може да изразява inches, centimetres и points като цели числа без закръгляне на границата на единицата. Там, където floating-point "1.27 cm" би дрейфнал, EMU пази 457200 и остава точен

Другата единица, която е важна тук, е point-ът. Typographic point е 1/72 inch, така че има 12700 EMU на point (914400 / 72). Points са начинът, по който Excel мисли за row heights, font sizes и margins под капака, което е причината да е полезно да излагате image geometry в points, когато искате картинка да се подравни с text metrics, а не с печатна линия. HotXLS кодира всички четири връзки като unit constants в библиотеката:

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 конкретно. Excel по подразбиране рендерира на 96 DPI, така че изображение от 100 пиксела каца на 100 × 9525 = 952500 EMU или около 2.54 cm при default setup - но нищо във файла не гарантира, че потребителят използва 96. Ако авторирате в реални единици, тази неяснота изчезва: 4 cm са 4 cm, независимо дали екранът е 96 или 220 DPI

Геометричната повърхност на TXLSXImage

Вградена картинка в HotXLS е TXLSXImage. Каноничното ѝ хранилище са две integer полета, WidthEMU и HeightEMU, закрепени към едно-базирана Row и Col (горната лява клетка, от която виси картинката). Свойствата в реални единици са изчисляеми изгледи върху тези EMU полета, не отделно състояние - четенето на WidthCM дели EMU на 360000, а записът умножава и закръгля обратно. Така всеки размер, който задавате, е просто друг начин да се напише същата подлежаща EMU стойност:

  • WidthInch / HeightInch — EMU ÷ 914400
  • WidthCM / HeightCM — EMU ÷ 360000
  • WidthPt / HeightPt — EMU ÷ 12700
  • WidthEMU / HeightEMU — integer източникът на истината

Добавяте изображение с AddImage(ARow, ACol, AData, AFormat), като подавате суровите encoded bytes и TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, или xlsxImageBmp); той връща zero-based index в worksheet Images collection-а. Има и AddImageFromFile(ARow, ACol, AFileName), който извежда формата от file extension-а. Обърнете внимание на базата на индекса: AddImage връща zero-based и Images[] е zero-based, което е умишлен контраст с one-based Cells[Row, Col] grid-а, така че не приемайте, че двете съвпадат

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 cm при 96 DPI. Този default съществува, за да е изображението видимо дори ако забравите да го оразмерите, но за всяко реално оформление трябва да зададете изричен физически размер, вместо да разчитате на pixel-derived default-а

Мащабиране и флагът за aspect ratio

Когато искате да преоразмерите спрямо текущите размери вместо към абсолютна цел - да речем, да свиете chart image до 60% от това, с което е импортнато - използвайте Scale:

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

APercent е процент, при който 100 означава без промяна, 150 увеличава с половина, 50 го дели на две. С AKeepAspect в неговия default True, ширината и височината се умножават по един и същ фактор, така че proportions-ите се запазват и 4×3 cm изображение става 6×4.5 cm след Scale(150). Подайте False и само ширината се мащабира - височината остава точно каквато е била. Тази асиметрия е умишлена: когато искате да разтегнете една ос независимо, правилният инструмент са изричните WidthCM/ HeightCM setters, а не-aspect branch-ът на Scale е за по-тесния случай да коригирате само ширината. Лесно е да прочетете Scale(150, False) като "stretch both freely" и да се изненадате, така че посягайте към setters, когато наистина искате две независими измерения

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

Една малка behaviour подробност, която трябва да знаете: Scale(100) short-circuit-ва и се връща без да пипа което и да е поле, така че е безопасно да го викате безусловно в цикъл, в който процентът може да е 100. И понеже геометрията се съхранява като integer EMU, всеки setter закръгля. Round-tripping през fractional сантиметри може следователно да дрейфне с част от EMU - далеч под това, което е видимо, но си струва да го знаете, ако някога проверявате exact equality в test. За pixel-perfect control задайте WidthEMU и HeightEMU директно и прескочете unit conversion-а изцяло

Четене на геометрията обратно

Image collection-ът е queryable, което има значение, когато зареждате съществуващ workbook и трябва да инспектирате или коригирате това, което вече е там, вместо това, което току-що сте добавили. Images.Count изброява всяка картинка на sheet-а, Images[i] я индексира zero-based, и 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;

Понеже свойствата в реални единици са live изгледи, картинка, импортната на някакъв EMU размер от друг инструмент, веднага отчита геометрията си в сантиметри - без conversion стъпка от ваша страна. Това се съчетава естествено с по-широкия drawing model; ако поставяте chart-и и shapes наред с raster images, придружаващото ръководство за HotXLS charts, images, and Excel drawings in Delphi покрива anchor модела, който тези обекти споделят

Метрики за margins на page setup

Същото напрежение между EMU и реалните единици се появява едно ниво навън, на страницата. OOXML и Excel съхраняват print margins в инчове, което е неудобно, ако report template-ите ви са зададени в милиметри, както е при повечето от света извън US. v2.91.0 добавя сантиметрови wrappers върху inch margin-ите: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, и MarginFooterCM. Всеки е тънко удобство върху съответното inch property, което конвертира при точния ratio 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;

Inch properties-те (MarginLeft и останалите) остават каноничното хранилище, така че можете да ги смесвате - да зададете горен margin в сантиметри и да го прочетете обратно в инчове, или обратно - и файлът, записан на диск, е идентичен и в двата случая. Конверсията е обикновено умножение по 2.54, без закръгляне към груба мрежа, така че 2 cm остава 2 cm с пълна double precision. Това е същата metric-convenience философия като image geometry-то: форматът говори imperial под капака, а библиотеката ви позволява да author-вате в която и да е единица е написана спецификацията ви. За подреждането на обкръжаващия report - titles, metadata blocks, totals - вижте merged cells and report template layout in HotXLS, който използва тези margins заедно с merged ranges и print area

Бележка за това какво геометрията гарантира и какво не

Геометричните properties управляват declared size на изображението във файла - размера, с който съвместим consumer ще го рендерира. Те не ресемплират image bytes-овете; 50×50 pixel PNG, оразмерен до 8 cm, ще се увеличи и ще изглежда blocky, точно както би било в Excel. Оразмеряването е layout операция, не image-processing операция, така че подавайте на картинката достатъчна source resolution за физическия размер, който искате. Библиотеката също не прекодира format-и: байтовете, които подавате на AddImage, се съхраняват и се записват такива, каквито са, с TXLSXImageFormat който декларирате. Подадете JPEG bytes, но ги отбележите xlsxImagePng и ще произведете файл, който Excel не може да отвори, така че оставяйте AddImageFromFile да извежда формата от extension-а, когато можете

Нищо от това не е екзотично, щом усвоите една идея под всичко: в OOXML физическият размер е реалната величина, а пикселите са производна, зависима от DPI сянка на нея. Авторирайте изображения и margins в сантиметри, инчове или points, оставете HotXLS да ги картографира към точен EMU и фактурите и отчетите ви ще се отпечатват в еднакъв размер на всяка машина, която ги отваря

Image geometry, scaling и metric-margin API-тата, описани тук, идват с HotXLS Delphi spreadsheet component, който чете и записва XLS и XLSX от Delphi и C++Builder без да се изисква Excel инсталация