Technischer Artikel

HotXLS-Bildgeometrie in Delphi: EMU, cm und Skalierung

Sie fügen ein Logo mit 600×400 Pixeln in die Kopfzeile einer generierten Rechnung ein, es sieht auf Ihrem Entwicklungsmonitor mit 96 DPI korrekt aus, und eine Woche später meldet ein Kunde auf einem Laptop mit hoher DPI-Zahl, dass es in der Größe einer Briefmarke gedruckt wird. Die Pixel haben sich nie geändert. Was sich geändert hat, ist die Annahme, dass eine Pixelanzahl eine physische Größe bedeutet, und in OOXML ist das nicht der Fall. Ein Tabellenkalkulationsbild trägt seine Abmessungen in EMU, und solange Sie nicht in EMU denken – oder in den realen Einheiten, die sich sauber darauf abbilden lassen –, ist Ihr Layout der DPI-Zahl ausgeliefert, die der rendernde Rechner gerade annimmt

HotXLS ist eine native VCL-Tabellenkalkulationskomponente für Delphi und C++Builder, die XLS- und XLSX-Dateien ohne Excel oder COM-Abhängigkeiten liest und schreibt. Seit der Version v2.91.0 müssen Sie die Einheitenrechnung beim XLSX-Bildobjekt nicht mehr manuell durchführen: Neben den rohen EMU-Werten stellt es Breite und Höhe in Zentimetern, Zoll und Punkten bereit, plus eine Scale-Methode, die die Größe prozentual mit optionaler Seitenverhältnissperre ändert. In diesem Artikel geht es darum, was EMU eigentlich ist, warum DrawingML diese Einheit gewählt hat und wie Sie die neue Geometrieoberfläche nutzen, um Bilder nach ihrer physischen Größe zu platzieren, anstatt sich auf eine Pixelanzahl zu verlassen, der Sie nicht trauen können

Was eine EMU ist und warum DrawingML sie verwendet

EMU steht für English Metric Unit und ist die Basislängeneinheit von DrawingML, der Zeichnungsebene, die in der gesamten Office Open XML-Familie verwendet wird (ECMA-376, Teil 1, §20). Eine EMU ist so definiert, dass es genau 914400 EMU pro Zoll und 360000 EMU pro Zentimeter gibt. Diese beiden Konstanten sind der einzige Grund für die Existenz dieser Einheit. 914400 ist teilbar durch 2, 3, 4, 5, 6, 8, 9, 10, 12 und viele weitere; seine Primfaktorzerlegung lautet 26 × 32 × 52 × 127. Da 1 Zoll exakt gleich 2,54 cm ist, ermöglicht die Wahl einer Einheit, die sowohl durch 360000 als auch durch einen sauberen Bruchteil von 914400 teilbar ist, dem Format, Zoll, Zentimeter und Punkte als Ganzzahlen ohne Rundungsfehler an den Einheitsgrenzen auszudrücken. Wo ein Floating-Point-Wert von „1,27 cm“ driften würde, speichert EMU exakt 457200 und bleibt präzise

Die andere Einheit, die hier von Bedeutung ist, ist der Punkt (Point). Ein typografischer Punkt entspricht 1/72 Zoll, sodass genau 12700 EMU pro Punkt anfallen (914400 / 72). In Punkten rechnet Excel unter der Haube selbst Zeilenhöhen, Schriftgrößen und Ränder um. Daher ist die Angabe der Bildgeometrie in Punkten nützlich, wenn das Bild an Textmetriken statt an einem gedruckten Lineal ausgerichtet werden soll. HotXLS codiert alle vier Beziehungen als Einheitskonstanten in der Bibliothek:

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)

Diese letzte Zeile ist der Kern des Briefmarken-Bugs. Ein Pixel hat erst dann eine physische Größe, wenn Sie eine DPI-Zahl festlegen, und 9525 EMU ist die Größe eines Pixels speziell bei 96 DPI. Die Standard-Rendering-DPI von Excel ist 96, sodass ein Bild mit 100 Pixeln bei einer Standardkonfiguration bei 100 × 9525 = 952500 EMU ≈ 2,54 cm landet – aber nichts in der Datei garantiert, dass der Empfänger 96 verwendet. Wenn Sie in realen Einheiten schreiben, verschwindet diese Mehrdeutigkeit: 4 cm sind 4 cm, unabhängig davon, ob der Bildschirm 96 oder 220 DPI hat

Die TXLSXImage-Geometrieoberfläche

Ein eingebettetes Bild in HotXLS ist ein TXLSXImage. Seine kanonische Speicherung besteht aus zwei Ganzzahlfeldern, WidthEMU und HeightEMU, verankert an einer einsbasierten Row und Col (der Zelle oben links, an der das Bild hängt). Die Real-Unit-Eigenschaften sind berechnete Ansichten über diese EMU-Felder, kein separater Zustand – das Lesen von WidthCM teilt die EMU durch 360000, und das Schreiben multipliziert und rundet zurück. Jede von Ihnen festgelegte Dimension ist also nur eine andere Schreibweise desselben zugrunde liegenden EMU-Werts:

  • WidthInch / HeightInch — EMU ÷ 914400
  • WidthCM / HeightCM — EMU ÷ 360000
  • WidthPt / HeightPt — EMU ÷ 12700
  • WidthEMU / HeightEMU — die Ganzzahl als Quelle der Wahrheit

Sie fügen ein Bild mit AddImage(ARow, ACol, AData, AFormat) hinzu, wobei Sie die rohen kodierten Bytes und ein TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif oder xlsxImageBmp) übergeben. Die Funktion gibt den nullbasierten Index in der Images-Sammlung des Arbeitsblatts zurück. Es gibt auch die Funktion AddImageFromFile(ARow, ACol, AFileName), die das Format aus der Dateiendung ableitet. Beachten Sie die Indexbasis: AddImage gibt nullbasiert zurück und Images[] is nullbasiert, was ein bewusster Kontrast zum einsbasierten Raster Cells[Row, Col] ist – gehen Sie also nicht davon aus, dass beide übereinstimmen

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;

Ein neu erstelltes Bild ist standardmäßig 100×100 Pixel groß, d. h. 952500 EMU im Quadrat, was in etwa einer Box von 2,54 cm bei 96 DPI entspricht. Dieser Standardwert existiert, damit ein Bild auch dann sichtbar ist, wenn Sie vergessen, seine Größe anzugeben. Bei jedem echten Layout sollten Sie jedoch eine explizite physische Größe festlegen, anstatt sich auf den von Pixeln abgeleiteten Standardwert zu verlassen

Skalierung und die Flagge für das Seitenverhältnis

Wenn Sie die Größe relativ zu den aktuellen Abmessungen anstelle eines absoluten Ziels ändern möchten – beispielsweise ein Diagrammbild auf 60 % der Größe verkleinern, mit der es importiert wurde –, verwenden Sie Scale:

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

APercent ist ein Prozentsatz, bei dem 100 unverändert bedeutet, 150 um die Hälfte vergrößert, 50 halbiert. Wenn AKeepAspect auf dem Standardwert True steht, werden sowohl Breite als auch Höhe mit demselben Faktor multipliziert, sodass die Proportionen erhalten bleiben und ein 4×3 cm großes Bild nach Scale(150) zu 6×4,5 cm wird. Übergeben Sie False, wird nur die Breite skaliert – die Höhe bleibt genau so, wie sie war. Diese Asymmetrie ist beabsichtigt: Wenn Sie eine Achse unabhängig dehnen möchten, sind die expliziten Setter WidthCM/HeightCM das richtige Werkzeug. Der Zweig von Scale ohne Seitenverhältnis ist für den engeren Fall gedacht, nur die Breite anzupassen. Man kann Scale(150, False) leicht als „beide frei dehnen“ missverstehen und eine Überraschung erleben. Nutzen Sie daher die Setter, wenn Sie tatsächlich zwei unabhängige Dimensionen meinen

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

Ein kleines Detail zum Verhalten: Scale(100) bricht sofort ab und kehrt zurück, ohne eines der Felder zu berühren. Es ist also sicher, den Aufruf bedingungslos in einer Schleife auszuführen, in der der Prozentsatz 100 sein kann. Und da die Geometrie als Ganzzahl-EMU gespeichert wird, rundet jeder Setter. Das Hin- und Herrechnen über gebrochene Zentimeter kann daher um einen Bruchteil einer EMU abweichen – weit unter allem Sichtbaren, aber gut zu wissen, falls Sie jemals exakte Gleichheit in einem Test prüfen. Für pixelgenaue Kontrolle setzen Sie WidthEMU und HeightEMU direkt und überspringen die Einheitenumrechnung ganz

Geometrie wieder auslesen

Die Bildersammlung ist abfragbar, was wichtig ist, wenn Sie eine vorhandene Arbeitsmappe laden und prüfen oder anpassen müssen, was bereits vorhanden ist, anstatt was Sie gerade hinzugefüg haben. Images.Count listet jedes Bild auf dem Blatt auf, Images[i] indiziert sie nullbasiert, und FindAt(ARow, ACol) gibt das Bild zurück, das an einer bestimmten Zelle verankert ist – oder nil, wenn keines vorhanden ist. Es gibt auch IndexOfCell für den Index anstelle des Objekts sowie DeleteAt / DeleteInRange zum Entfernen

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;

Da die Real-Unit-Eigenschaften Live-Ansichten sind, meldet ein Bild, das mit einer bestimmten EMU-Größe aus einem anderen Werkzeug importiert wurde, seine Geometrie sofort in Zentimetern – ohne Umrechnungsschritt Ihrerseits. Dies passt hervorragend zum breiteren Zeichnungsmodell; wenn Sie Diagramme und Formen sowie Rasterbilder platzieren, behandelt der Begleitfaden über HotXLS-Diagramme, Bilder und Excel-Zeichnungen in Delphi das Anker-Modell, das diese Objekte gemeinsam nutzen

Metrische Seiteneinstellungen-Ränder

Das gleiche Spannungsfeld zwischen EMU und realen Einheiten zeigt sich eine Ebene höher, bei der Seite. OOXML und Excel speichern Druckränder in Zoll (Inches), was unpraktisch ist, wenn Ihre Berichtsvorlagen in Millimetern spezifiziert sind, wie fast überall außerhalb der USA. v2.91.0 fügt Zentimeter-Wrapper über die Zoll-Ränder hinzu: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM und MarginFooterCM. Jeder ist eine bequeme Hilfsfunktion über der entsprechenden Zoll-Eigenschaft, die im genauen Verhältnis 1 Zoll = 2,54 cm umrechnet

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;

Die Zoll-Eigenschaften (MarginLeft und Verwandte) bleiben der kanonische Speicher, sodass Sie beide mischen können – legen Sie einen oberen Rand in Zentimetern fest und lesen Sie ihn in Zoll aus, oder umgekehrt. Die auf die Festplatte geschriebene Datei ist in beiden Fällen identisch. Die Umrechnung ist eine einfache Multiplikation mit 2,54 ohne Rundung auf ein grobes Raster, sodass 2 cm mit voller Double-Präzision 2 cm bleiben. Dies ist dieselbe Philosophie des metrischen Komforts wie bei der Bildgeometrie: Das Format spricht unter der Haube imperial, und die Bibliothek lässt Sie in der Einheit schreiben, in der Ihre Spezifikation verfasst ist. Informations zum Layouten des umgebenden Berichts – Titel, Metadatenblöcke, Summen – finden Sie unter Verbundene Zellen und Berichtsvorlagen-Layout in HotXLS, das diese Ränder zusammen mit verbundenen Bereichen und einem Druckbereich verwendet

Ein Hinweis darauf, was die Geometrie garantiert und was nicht

Die Geometrieeigenschaften steuern die deklarierte Größe des Bildes in der Datei – die Größe, in der ein konformer Empfänger es rendern wird. Sie führen kein Resampling der Bildbytes durch; ein PNG mit 50×50 Pixeln, das auf 8 cm vergrößert wird, wird hochskaliert und pixelig aussehen, genau wie in Excel. Die Größenänderung ist eine Layout-Operation, keine Bildbearbeitung. Stellen Sie dem Bild also genügend Ausgangsauflösung für die beabsichtigte physische Größe zur Verfügung. Die Bibliothek kodiert auch keine Formate neu: Die Bytes, die Sie an AddImage übergeben, werden so gespeichert und weitergegeben, wie sie sind, mit dem von Ihnen deklarierten TXLSXImageFormat. Wenn Sie JPEG-Bytes übergeben, diese aber als xlsxImagePng kennzeichnen, erzeugen Sie eine Datei, die Excel nicht öffnen kann. Lassen Sie also AddImageFromFile das Format nach Möglichkeit aus der Dateiendung ableiten

Nichts davon ist ungewöhnlich, wenn man die eine zugrunde liegende Idee verinnerlicht hat: In OOXML ist die physische Größe die reale Größe, und Pixel sind nur ein abgeleiteter, DPI-abhängiger Schatten davon. Schreiben Sie Bilder und Ränder in Zentimetern, Zoll oder Punkten, lassen Sie HotXLS diese auf exakte EMU abbilden, und Ihre Rechnungen und Berichte werden auf jedem Rechner, der sie öffnet, in derselben Größe gedruckt

Die hier beschriebenen APIs für Bildgeometrie, Skalierung und metrische Ränder werden mit der HotXLS Delphi spreadsheet component ausgeliefert, die XLS- und XLSX-Dateien in Delphi und C++Builder liest und schreibt, ohne dass eine Excel-Installation erforderlich ist