Artykuł techniczny

Geometria obrazów HotXLS w Delphi: EMU, cm i skalowanie

Wrzucasz logo 600×400 pikseli do nagłówka generowanej faktury, na Twoim monitorze deweloperskim 96 DPI wygląda dobrze, a tydzień później klient na laptopie high-DPI zgłasza, że na wydruku ma ono rozmiar znaczka pocztowego. Liczba pikseli się nie zmieniła. Zmieniło się tylko założenie, że liczba pikseli oznacza rozmiar fizyczny, a w OOXML tak nie jest. Obraz w arkuszu przechowuje swoje wymiary w EMU i dopóki nie zaczniesz myśleć w EMU, albo w rzeczywistych jednostkach, które dają się na nie dokładnie odwzorować, Twój układ zależy od tego, jakie DPI akurat założy maszyna renderująca

HotXLS to natywny komponent VCL arkuszy kalkulacyjnych dla Delphi i C++Builder, który odczytuje i zapisuje XLS oraz XLSX bez Excela i bez zależności od COM. Od wersji v2.91.0 obiekt obrazu XLSX przestaje zmuszać Cię do ręcznego liczenia jednostek: obok surowego EMU udostępnia szerokość i wysokość w centymetrach, calach i punktach oraz metodę Scale, która zmienia rozmiar procentowo z opcjonalną blokadą proporcji. Ten artykuł wyjaśnia, czym naprawdę jest EMU, dlaczego DrawingML wybrał właśnie tę jednostkę i jak używać nowej powierzchni geometrii do ustawiania obrazów według rozmiaru fizycznego zamiast według liczby pikseli, której nie da się ufać

Czym jest EMU i dlaczego DrawingML go używa

EMU oznacza English Metric Unit i jest podstawową jednostką długości w DrawingML, warstwie rysunkowej współdzielonej przez całą rodzinę Office Open XML (ECMA-376, Part 1, §20). Jedno EMU jest zdefiniowane tak, aby przypadało dokładnie 914400 EMU na cal oraz 360000 EMU na centymetr. Te dwie stałe są właściwie całym powodem istnienia tej jednostki. 914400 dzieli się przez 2, 3, 4, 5, 6, 8, 9, 10, 12 i wiele innych liczb; rozkłada się na 26 × 32 × 52 × 127. Ponieważ 1 cal = 2,54 cm dokładnie, wybranie jednostki podzielnej zarówno przez 360000, jak i przez sensowne ułamki 914400 pozwala formatowi zapisywać cale, centymetry i punkty jako liczby całkowite bez zaokrąglania na granicy jednostki. Tam, gdzie zmiennoprzecinkowe "1.27 cm" mogłoby dryfować, EMU zapisuje 457200 i pozostaje dokładne

Drugą istotną tutaj jednostką jest punkt. Punkt typograficzny to 1/72 cala, więc przypada 12700 EMU na punkt (914400 / 72). W punktach sam Excel myśli wewnętrznie o wysokościach wierszy, rozmiarach czcionek i marginesach, dlatego udostępnianie geometrii obrazu w punktach jest użyteczne, gdy chcesz wyrównać obraz do metryk tekstu, a nie do linijki na wydruku. HotXLS koduje wszystkie cztery zależności jako stałe jednostek w bibliotece:

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)

Ten ostatni wiersz jest sednem błędu ze znaczkiem pocztowym. Piksel ma rozmiar fizyczny dopiero wtedy, gdy ustalisz DPI, a 9525 EMU to rozmiar piksela konkretnie przy 96 DPI. Domyślne DPI renderowania w Excelu wynosi 96, więc obraz 100-pikselowy trafia przy ustawieniach domyślnych na 100 × 9525 = 952500 EMU ≈ 2,54 cm, ale nic w pliku nie gwarantuje, że odbiorca też użyje 96. Twórz w rzeczywistych jednostkach, a ta niejednoznaczność znika: 4 cm to 4 cm niezależnie od tego, czy ekran ma 96, czy 220 DPI

Powierzchnia geometrii TXLSXImage

Osadzony obraz w HotXLS to TXLSXImage. Jego kanoniczny zapis to dwa pola całkowite, WidthEMU oraz HeightEMU, zakotwiczone przy 1-based Row oraz Col , czyli górnej lewej komórce, od której wisi obraz. Właściwości w rzeczywistych jednostkach są tylko widokami obliczanymi na podstawie tych pól EMU, a nie osobnym stanem. Odczyt WidthCM dzieli EMU przez 360000, a zapis mnoży i zaokrągla z powrotem. Każdy ustawiany wymiar jest więc tylko innym zapisem tej samej bazowej wartości EMU:

  • WidthInch / HeightInch — EMU ÷ 914400
  • WidthCM / HeightCM — EMU ÷ 360000
  • WidthPt / HeightPt — EMU ÷ 12700
  • WidthEMU / HeightEMU — całkowitoliczbowe źródło prawdy

Obraz dodajesz przez AddImage(ARow, ACol, AData, AFormat), przekazując surowe zakodowane bajty oraz TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif albo xlsxImageBmp); metoda zwraca zero-based indeks w kolekcji Images arkusza. Istnieje też AddImageFromFile(ARow, ACol, AFileName), które wnioskuje format z rozszerzenia pliku. Zwróć uwagę na bazę indeksu: AddImage zwraca zero-based, a Images[] jest zero-based, co celowo kontrastuje z 1-based siatką Cells[Row, Col], więc nie zakładaj, że oba indeksowania są zgodne

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;

Świeżo utworzony obraz domyślnie ma 100×100 pikseli, czyli kwadrat 952500 EMU, mniej więcej 2,54 cm przy 96 DPI. Taki domyślny rozmiar istnieje po to, aby obraz był widoczny nawet wtedy, gdy zapomnisz go przeskalować, ale w każdym realnym układzie powinieneś ustawić jawny rozmiar fizyczny zamiast polegać na domyślnym rozmiarze wywiedzionym z pikseli

Skalowanie i flaga proporcji

Gdy chcesz zmienić rozmiar względem bieżących wymiarów zamiast ustawiać bezwzględny cel, na przykład zmniejszyć obraz wykresu do 60% rozmiaru importowanego pliku, użyj Scale:

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

APercent to procent, gdzie 100 oznacza brak zmiany, 150 zwiększa o połowę, a 50 zmniejsza o połowę. Gdy AKeepAspect ma domyślną wartość True, zarówno szerokość, jak i wysokość są mnożone przez ten sam współczynnik, więc proporcje się utrzymują, a obraz 4×3 cm staje się po Scale(150) rozmiarem 6×4,5 cm. Przekaż False, a przeskaluje się tylko szerokość, wysokość zostanie dokładnie taka jak wcześniej. Ta asymetria jest zamierzona: gdy chcesz niezależnie rozciągać jedną oś, właściwym narzędziem są jawne settery WidthCM/HeightCM, a gałąź bez zachowania proporcji w Scale istnieje dla węższego przypadku korekty samej szerokości. Łatwo odczytać Scale(150, False) jako "rozciągnij oba wymiary swobodnie" i się zdziwić, więc sięgaj po settery wtedy, gdy naprawdę chcesz dwóch niezależnych wymiarów

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

Jedno drobne zachowanie, o którym warto wiedzieć: Scale(100) wykonuje short-circuit i wraca bez dotykania któregokolwiek pola, więc można je bezpiecznie wywoływać bez warunków w pętli, w której procent może wynosić 100. A ponieważ geometria jest przechowywana jako liczba całkowita EMU, każdy setter zaokrągla. Round-trip przez ułamkowe centymetry może więc dryfować o ułamek EMU, znacznie poniżej progu widoczności, ale warto o tym wiedzieć, jeśli kiedyś asercją sprawdzasz dokładną równość w teście. Dla pixel-perfect kontroli ustawiaj WidthEMU oraz HeightEMU bezpośrednio i całkowicie pomiń konwersję jednostek

Odczytywanie geometrii z powrotem

Kolekcję obrazów można odpytywać, co ma znaczenie wtedy, gdy wczytujesz istniejący workbook i musisz sprawdzić lub skorygować to, co już w nim jest, a nie tylko to, co właśnie dodałeś. Images.Count wylicza każdy obraz na arkuszu, a Images[i] indeksuje je zero-based, a FindAt(ARow, ACol) zwraca obraz zakotwiczony przy określonej komórce, albo nil, jeśli żadnego nie ma. Istnieje też IndexOfCell zwracające indeks zamiast obiektu oraz DeleteAt / DeleteInRange do usuwania

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;

Ponieważ właściwości w rzeczywistych jednostkach są żywymi widokami, obraz zaimportowany z innego narzędzia przy jakimś rozmiarze EMU od razu raportuje swoją geometrię w centymetrach, bez żadnego dodatkowego kroku konwersji po Twojej stronie. To naturalnie łączy się z szerszym modelem drawing; jeśli umieszczasz wykresy i kształty obok obrazów rastrowych, to uzupełniający przewodnik po wykresach, obrazach i rysunkach Excela w HotXLS dla Delphi omawia model kotwiczenia współdzielony przez te obiekty

Metryczne marginesy page setup

To samo napięcie między EMU a jednostkami rzeczywistymi pojawia się poziom wyżej, na poziomie strony. OOXML i Excel przechowują marginesy wydruku w calach, co bywa niewygodne, jeśli Twoje szablony raportów są określane w milimetrach, jak w większości świata poza USA. Wersja v2.91.0 dodaje centymetrowe nakładki na marginesy w calach: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM oraz MarginFooterCM. Każda z nich jest cienką warstwą wygody nad odpowiadającą jej właściwością w calach, konwertując dokładnie według relacji 1 cal = 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;

Właściwości calowe (MarginLeft i podobne) pozostają kanonicznym miejscem przechowywania, więc możesz mieszać oba sposoby: ustawić górny margines w centymetrach i odczytać go z powrotem w calach albo odwrotnie, a plik zapisany na dysku będzie identyczny w obu przypadkach. Konwersja to zwykłe mnożenie przez 2,54, bez zaokrąglania do grubej siatki, więc 2 cm pozostaje 2 cm z pełną precyzją double. To ta sama filozofia wygody metrycznej co przy geometrii obrazów: format pod spodem mówi językiem imperialnym, a biblioteka pozwala tworzyć w tej jednostce, w której napisano Twoją specyfikację. Jeśli układasz otaczający raport, tytuły, bloki metadanych czy sumy, zobacz scalone komórki i układ szablonu raportu w HotXLS, gdzie te marginesy są używane razem ze scalonymi zakresami i obszarem wydruku

Uwaga o tym, co geometria gwarantuje, a czego nie

Właściwości geometrii kontrolują zadeklarowany rozmiar obrazu w pliku, czyli rozmiar, przy którym zgodny odbiorca go wyrenderuje. Nie wykonują resamplingu bajtów obrazu; PNG 50×50 pikseli ustawiony na 8 cm zostanie powiększony i będzie wyglądał kanciasto, dokładnie tak samo jak w Excelu. Skalowanie jest operacją układu, a nie przetwarzania obrazu, więc dostarczaj obrazowi wystarczającą rozdzielczość źródłową dla planowanego rozmiaru fizycznego. Biblioteka nie dokonuje też ponownego kodowania formatów: bajty przekazane do AddImage są przechowywane i zapisywane bez zmian, z zadeklarowanym przez Ciebie TXLSXImageFormat. Jeśli podasz bajty JPEG, ale oznaczysz je jako xlsxImagePng, otrzymasz plik, którego Excel nie otworzy, więc pozwól AddImageFromFile wywnioskować format z rozszerzenia, kiedy tylko możesz

Nic z tego nie jest egzotyczne, gdy przyswoisz jedną stojącą za tym ideę: w OOXML rozmiar fizyczny jest wartością rzeczywistą, a piksele są tylko pochodnym cieniem zależnym od DPI. Twórz obrazy i marginesy w centymetrach, calach albo punktach, pozwól HotXLS odwzorować je na dokładne EMU, a Twoje faktury i raporty będą drukować się w tym samym rozmiarze na każdej maszynie, która je otworzy

Opisane tu API geometrii obrazów, skalowania i metrycznych marginesów jest dostarczane razem z komponentem arkuszy kalkulacyjnych HotXLS dla Delphi, który odczytuje i zapisuje XLS oraz XLSX z Delphi i C++Builder bez wymaganej instalacji Excela