Вы вставляете логотип 600×400 пикселей в заголовок генерируемого счета, на вашем 96-DPI мониторе разработки он выглядит правильно, а через неделю клиент с ноутбуком высокого DPI сообщает, что при печати картинка размером с почтовую марку. Сами пиксели не изменились. Изменилась лишь предпосылка, будто количество пикселей определяет физический размер, а в OOXML это не так. Изображение в spreadsheet хранит размеры в EMU, и пока вы не начнете мыслить в EMU или в реальных единицах, которые чисто на них отображаются, ваш макет будет зависеть от того DPI, который решит принять машина рендеринга
HotXLS - нативный VCL spreadsheet component для Delphi и C++Builder, который читает и пишет XLS и XLSX без Excel и без какой-либо зависимости от COM. Начиная с v2.91.0 объект изображения XLSX перестает заставлять вас вручную считать единицы: рядом с сырыми EMU он открывает width и height в сантиметрах, дюймах и points, а также метод Scale , который меняет размер на процент с необязательной блокировкой aspect ratio. Эта статья о том, что такое EMU на самом деле, почему DrawingML выбрал именно эту единицу и как пользоваться новой поверхностью geometry, чтобы размещать картинки по физическому размеру, а не по числу пикселей, которому нельзя доверять
Что такое EMU и зачем он нужен DrawingML
EMU означает English Metric Unit и является базовой единицей длины в DrawingML, общем drawing layer для всего семейства 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 как целые числа без округления на границе единиц. Там, где число с плавающей точкой "1.27 cm" дрейфовало бы, EMU сохраняет 457200 и остается точным
Еще одна важная единица здесь - point. Типографский point равен 1/72 inch, поэтому в одном point содержится 12700 EMU на point , то есть 914400 / 72. Именно в points Excel сам мыслит о высоте строк, размере шрифтов и полях под капотом, поэтому открывать geometry изображения в points особенно удобно, когда вы хотите выровнять картинку не по печатной линейке, а по метрике текста. 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 . Excel по умолчанию рендерит при 96 DPI, поэтому картинка шириной 100 пикселей попадает в 100 × 9525 = 952500 EMU, то есть примерно 2.54 cm при стандартной конфигурации, но в самом файле ничто не гарантирует, что потребитель тоже будет использовать 96. Пишите в реальных единицах, и эта неоднозначность исчезнет: 4 cm остаются 4 cm и на 96, и на 220 DPI
Поверхность geometry у TXLSXImage
Встроенная картинка в HotXLS представлена как TXLSXImage . Каноническое хранение основано на двух целочисленных полях, WidthEMU и HeightEMU , закрепленных за 1-based Row и Col , то есть верхней левой ячейкой, к которой подвешено изображение. Свойства в реальных единицах являются вычисляемыми представлениями поверх этих полей 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 . Метод возвращает zero-based индекс в коллекции worksheet Images . Существует и AddImageFromFile(ARow, ACol, AFileName) , который выводит формат по расширению файла. Обратите внимание на базу индексов: AddImage возвращает zero-based значение, и Images[] тоже zero-based. Это осознанный контраст с 1-based 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 пикселей, то есть квадрат 952500 EMU, примерно 2.54 cm при 96 DPI. Это значение по умолчанию нужно лишь затем, чтобы картинка была видима, даже если вы забыли назначить размер, но для любого реального макета задавайте явный физический размер, а не полагайтесь на производный размер из пикселей
Масштабирование и флаг сохранения пропорций
Когда вам нужно изменить размер относительно текущих размеров, а не до абсолютной цели, например уменьшить изображение chart до 60% от того, в каком виде оно импортировалось, используйте Scale :
procedure Scale(APercent: Double; AKeepAspect: Boolean = True);
APercent Значение AKeepAspect - это процент, где 100 означает без изменений, 150 увеличивает в полтора раза, а 50 делит размер пополам. Если True остается в значении по умолчанию Scale(150) , и ширина, и высота умножаются на один и тот же коэффициент, поэтому пропорции сохраняются, а изображение 4×3 cm превращается в 6×4.5 cm после False . Передайте WidthCM , и масштабируется только ширина, а высота остается строго прежней. Эта асимметрия сделана намеренно: когда нужно независимо тянуть каждую ось, правильный инструмент - явные setters HeightCM / Scale , а ветка Scale(150, False) без сохранения aspect ratio оставлена для более узкого случая, когда вы хотите подстроить только ширину. Легко прочитать
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
как "свободно растянуть обе оси" и получить сюрприз, поэтому при действительно независимых размерах сразу берите setters.Scale(100)Есть одна небольшая особенность поведения: сразу завершает работу, ничего не меняя, поэтому его безопасно вызывать без условий в цикле, где процент иногда оказывается равным 100. А поскольку geometry хранится как целочисленныеWidthEMU EMU, каждый setter округляет. Поэтому round-trip через дробные сантиметры может дать дрейф на доли одного EMU - настолько малый, что его нельзя увидеть, но об этом стоит помнить, если вы когда-нибудь проверяете точное равенство в тесте. Для pixel-perfect контроля задавайте HeightEMU и
напрямую и полностью обходите преобразование единиц
Чтение geometry обратноImages.CountКоллекцию изображений можно запрашивать, и это важно, когда вы загружаете уже существующую workbook и хотите исследовать или скорректировать то, что там уже лежит, а не только то, что вы только что добавили. 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, сразу сообщает свою geometry в сантиметрах, без какого-либо дополнительного шага преобразования с вашей стороны. Это естественно сочетается с более общей drawing model. Если вы размещаете не только raster image, но и charts и shapes, то сопутствующее руководство по HotXLS charts, images, and Excel drawings in Delphi
разбирает общую anchor model этих объектов
Поля page setup в метрических единицахТо же напряжение между EMU и реальными единицами проявляется уровнем выше, уже на странице. OOXML и Excel хранят поля печати в дюймахMarginLeftCM , что неудобно, если шаблоны отчетов заданы в миллиметрах, как почти везде за пределами США. В v2.91.0 добавлены centimeter wrappers поверх полей в inches: MarginRightCM , MarginTopCM , MarginBottomCM , MarginHeaderCM , MarginFooterCM и
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;
. Каждое из них является тонкой оберткой над соответствующим свойством в дюймах и преобразует значения по точному соотношению 1 inch = 2.54 cm.MarginLeftСвойства в inches, и его соседи, остаются каноническим хранилищем, поэтому вы можете свободно смешивать оба способа: задать верхнее поле в сантиметрах и прочитать его обратно в дюймах или наоборот, а файл на диске получится идентичным в обоих случаях. Преобразование - это обычное умножение на 2.54, без округления к грубой сетке, поэтому 2 cm остаются 2 cm с полной double precision. Это та же философия метрического удобства, что и у geometry изображения: под капотом формат говорит на империале, а библиотека позволяет вам писать в той единице, в которой задана спецификация. Для раскладки окружающего отчета, заголовков, блоков метаданных и итогов смотрите merged cells and report template layout in HotXLS
, где эти поля используются вместе с merged range и print area
Замечание о том, что geometry гарантирует и чего не гарантируетСвойства geometry управляют заявленнымAddImage размером изображения в файле, то есть тем размером, в котором его отрисует совместимый consumer. Они не пересэмплируют сами байты изображения. PNG 50×50 пикселей, растянутый до 8 cm, увеличится и будет выглядеть зернисто ровно так же, как в Excel. Назначение размера - это операция layout, а не image processing, поэтому подавайте картинку с достаточным исходным разрешением для того физического размера, который собираетесь использовать. Библиотека также не перекодирует форматы: байты, которые вы передаете в TXLSXImageFormat , сохраняются и выводятся как есть, с тем xlsxImagePng , который вы объявили. Передайте байты JPEG, но отметьте их как AddImageFromFile , и вы получите файл, который Excel не сможет открыть, поэтому, когда это возможно, лучше позволять
выводить формат по расширению
Ничего экзотического здесь нет, как только вы усвоите одну базовую идею: в OOXML реальной величиной является физический размер, а пиксели - лишь его производная, зависящая от DPI тень. Задавайте изображения и поля в сантиметрах, дюймах или points, позвольте HotXLS отобразить их на точные EMU, и ваши счета и отчеты будут печататься одним и тем же размером на любой машине, которая их открывает.Описанные здесь API для geometry изображения, масштабирования и метрических полей поставляются вместе с HotXLS Delphi spreadsheet component