Ви вставляєте логотип 600×400 пікселів у верхній колонтитул згенерованого рахунка, на своєму 96-DPI моніторі для розробки все виглядає правильно, а за тиждень клієнт на ноутбуці з високим DPI скаржиться, що на друці він має розмір поштової марки. Пікселі не змінилися. Змінилося припущення, ніби кількість пікселів означає фізичний розмір, а в OOXML це не так. Зображення в електронній таблиці зберігає свої розміри в EMU, і поки ви не мислите в EMU або в реальних одиницях, що точно на нього накладаються, макет залежить від того, який DPI припустить система рендерингу
HotXLS - це рідний VCL-компонент для електронних таблиць у Delphi та C++Builder, який читає і записує XLS та XLSX без Excel чи будь-якої залежності від COM. Починаючи з v2.91.0, об’єкт зображення XLSX перестає змушувати вас вручну рахувати одиниці: поряд із сирим EMU він відкриває ширину і висоту в сантиметрах, дюймах і пунктах, а також 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 дюйм = 2.54 см точно, вибір одиниці, що ділиться і на 360000, і на зручну частку 914400, дає змогу формату виражати дюйми, сантиметри і пункти як цілі числа без округлення на межі одиниць. Там, де число з плаваючою комою на кшталт "1.27 cm" почало б дрейфувати, EMU зберігає 457200 і лишається точним
Інша одиниця, що має тут значення, це пункт. Типографський пункт дорівнює 1/72 дюйма, тож існує 12700 EMU на пункт (914400 / 72). Саме в пунктах Excel насправді мислить висоту рядків, розміри шрифту і поля під капотом, тому відображення геометрії зображень у пунктах корисне, коли потрібно вирівняти картинку за текстовими метриками, а не за друкованою лінійкою. HotXLS кодує всі чотири співвідношення як константи одиниць у бібліотеці:
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)
Останній рядок і є суттю помилки з поштовою маркою. Піксель набуває фізичного розміру лише після фіксації DPI, а 9525 EMU - це розмір пікселя саме при 96 DPI. Стандартний DPI для відображення в Excel - 96, тому зображення на 100 пікселів потрапляє як 100 × 9525 = 952500 EMU ≈ 2.54 cm у типовому середовищі, але у файлі ніщо не гарантує, що споживач використає саме 96. Якщо задавати розміри в реальних одиницях, ця неоднозначність зникає: 4 cm це 4 cm, чи екран має 96, чи 220 DPI
Поверхня геометрії TXLSXImage
Вбудоване зображення в HotXLS - це TXLSXImage. Його канонічне сховище складається з двох цілих полів, WidthEMU і HeightEMU, прив’язаних до Row з нумерацією від 1Col (верхня ліва комірка, до якої кріпиться зображення). Властивості з реальними одиницями є обчислюваними поданнями цих полів EMU, а не окремим станом - читання WidthCM ділить EMU на 360000, а запис множить і округлює назад. Тож кожен заданий вами розмір - це просто інший запис того самого базового значення EMU:
WidthInch/HeightInch- EMU ÷ 914400WidthCM/HeightCM- EMU ÷ 360000WidthPt/HeightPt- EMU ÷ 12700WidthEMU/HeightEMU- цілісне джерело істини
Зображення додають за допомогою AddImage(ARow, ACol, AData, AFormat), передаючи необроблені закодовані байти та TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, або xlsxImageBmp); вона повертає нульовий індекс у колекції аркуша. Також є Images, яка визначає формат за розширенням файлу. Зверніть увагу на базу індексації: AddImageFromFile(ARow, ACol, AFileName) повертає індексацію з нуля, а AddImage теж використовує нульову індексацію, що навмисно контрастує з однобазовою Images[] сіткою, тож не припускайте, що ці дві схеми збігаються.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 пікселів, тобто квадрат 952500 EMU, приблизно коробка 2,54 см при 96 DPI. Таке значення за замовчуванням існує, щоб зображення було видно, навіть якщо ви забудете задати розмір, але для будь-якого реального компонування слід явно вказати фізичний розмір, а не покладатися на розмір, виведений із пікселів
Масштабування та прапорець aspect-ratio
Коли потрібно змінити розмір відносно поточних розмірів, а не до абсолютної цілі - наприклад, зменшити зображення діаграми до 60% від того, з чим воно імпортувалося, - використовуйте Scale:
procedure Scale(APercent: Double; AKeepAspect: Boolean = True);
APercent - це відсоток, де 100 означає без змін, 150 збільшує на половину, 50 ділить навпіл. З AKeepAspect у значенні за замовчуванням True, і ширина, і висота множаться на один і той самий коефіцієнт, тож пропорції зберігаються, а зображення 4×3 см після Scale(150). False і масштабується лише ширина, тоді як висота лишається точно такою, як була. Така асиметрія навмисна: коли потрібно незалежно розтягнути одну вісь, правильний інструмент - явні WidthCM/HeightCM сеттери, а неаспектна гілка Scale існує для вужчого випадку налаштування лише ширини. Легко прочитати Scale(150, False) як "розтягнути обидві вільно" і здивуватися, тож звертайтеся до сеттерів, коли справді маєте на увазі дві незалежні розмірності
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. А оскільки геометрія зберігається як цілі EMU, кожен сеттер округлює. Тому проходження туди й назад через дробові сантиметри може зміститися на частку EMU - це далеко нижче за помітний рівень, але про це варто знати, якщо ви колись перевіряєте точну рівність у тесті. Для піксельно точного контролю задавайте WidthEMU і HeightEMU безпосередньо та взагалі пропустіть перетворення одиниць
Читання геометрії назад
Колекцію зображень можна запитувати, і це важливо, коли ви завантажуєте наявну книгу та маєте перевірити або скоригувати те, що вже є, а не лише те, що щойно додали. Images.Count перелічує кожне зображення на аркуші, Images[i] індексує їх від нуля, а 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;
Оскільки властивості з реальними одиницями є живими представленнями, зображення, імпортоване з іншого інструмента в певному розмірі EMU, одразу показує свою геометрію в сантиметрах, без жодного кроку перетворення з вашого боку. Це природно поєднується з ширшою моделлю креслення; якщо ви розміщуєте також діаграми та фігури, а не лише растрові зображення, супровідний посібник Діаграми, зображення та креслення Excel у Delphi пояснює модель прив'язки, спільну для цих об'єктів
Метричні поля сторінки
Та сама напруга між EMU та реальними одиницями проявляється на рівень вище, на сторінці. OOXML та Excel зберігають поля друку в дюймах, що незручно, якщо ваші шаблони звітів задані в міліметрах, як у більшості світу поза межами США. У v2.91.0 додано сантиметрові обгортки для дюймових полів: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, і MarginFooterCM. Кожна з них є тонкою зручною обгорткою над відповідною дюймовою властивістю та перетворює значення за точним співвідношенням 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;
Дюймові властивості (MarginLeft та інші) залишаються канонічним сховищем значень, тож ви можете змішувати обидва варіанти: задати верхнє поле в сантиметрах і прочитати його в дюймах, або навпаки, а файл, записаний на диск, у будь-якому разі буде однаковим. Перетворення є простим множенням на 2.54 без округлення до грубої сітки, тож 2 cm залишаються 2 cm з повною точністю double. Це та сама філософія зручних метричних одиниць, що й у геометрії зображень: формат під капотом говорить імперськими одиницями, а бібліотека дає змогу авторувати в тій одиниці, у якій написана ваша специфікація. Для компонування супровідного звіту, заголовків, блоків метаданих, підсумків дивіться об'єднані комірки та компонування шаблонів звітів у HotXLS, де ці поля використовуються разом з об'єднаними діапазонами та областю друку
Примітка про те, що геометрія гарантує, а що ні
Властивості геометрії керують задекларованим розміром зображення у файлі, тобто розміром, у якому сумісний клієнт його відрендерить. Вони не виконують повторне семплювання байтів зображення; PNG 50×50 пікселів, який задано як 8 cm, масштабуватиметься вгору і виглядатиме пікселізовано, саме як в Excel. Визначення розміру є операцією компонування, а не обробки зображення, тож подавайте картинку з достатньою вихідною роздільною здатністю для фізичного розміру, який ви плануєте. Бібліотека також не перекодовує формати: байти, які ви передаєте в AddImage зберігаються і записуються без змін, разом із TXLSXImageFormat, який ви оголошуєте. Передайте байти JPEG, але позначте їх як xlsxImagePng і ви отримаєте файл, який Excel не зможе відкрити, тож, коли можете, дозвольте AddImageFromFile визначати формат за розширенням
Усе це вже не здається екзотикою, щойно ви засвоїте одну ідею, що лежить під цим: в OOXML фізичний розмір є справжньою величиною, а пікселі є похідною, залежною від DPI тінню від нього. Задавайте розміри зображень і полів у сантиметрах, дюймах або пунктах, дозвольте HotXLS точно відображати їх у EMU, і ваші рахунки та звіти друкуватимуться однакового розміру на кожному комп'ютері, де їх відкривають
API геометрії зображень, масштабування та метричних полів, описані тут, постачаються разом із компонентом електронних таблиць HotXLS Delphi, який читає та записує XLS і XLSX з Delphi та C++Builder без потреби в установленому Excel