Technisch artikel

Afbeeldingsgeometrie in Delphi: EMU, cm en schalen in HotXLS

Je zet een logo van 600×400 pixels in de kop van een gegenereerde factuur, het ziet er goed uit op je ontwikkelscherm van 96 DPI, en een week later meldt een klant op een high-DPI laptop dat het als een postzegel wordt afgedrukt. De pixels zijn niet veranderd. Wat veranderde is de aanname dat een pixel-aantal een fysieke grootte betekent, en in OOXML betekent het dat niet. Een spreadsheetafbeelding draagt zijn afmetingen in EMU, en totdat je in EMU redeneert, of in de echte eenheden die daar netjes op aansluiten, staat je lay-out aan de genade van welke DPI de renderende machine toevallig aanneemt

HotXLS is een native VCL spreadsheetcomponent voor Delphi en C++Builder die XLS en XLSX leest en schrijft zonder Excel of enige COM-afhankelijkheid. Vanaf v2.91.0 laat het XLSX-imageobject je het unit-arithmetiek niet meer met de hand doen: naast de ruwe EMU stelt het breedte en hoogte beschikbaar in centimeters, inches en points, plus een Scale-methode die met een percentage verkleint of vergroot, optioneel met aspectratio-lock. Dit artikel gaat over wat EMU precies is, waarom DrawingML het gebruikte, en hoe je de nieuwe geometrie-oppervlakte gebruikt om afbeeldingen te plaatsen op fysieke maat in plaats van op een pixel-aantal dat je niet kunt vertrouwen

Wat een EMU is en waarom DrawingML er een gebruikt

EMU staat voor English Metric Unit, en het is de basislengte-eenheid van DrawingML, de tekenlaag die door de hele Office Open XML-familie wordt gedeeld (ECMA-376, Part 1, §20). Eén EMU is zo gedefinieerd dat er exact 914400 EMU per inch en 360000 EMU per centimeter zijn. Die twee constanten zijn de hele reden dat de eenheid bestaat. 914400 is deelbaar door 2, 3, 4, 5, 6, 8, 9, 10, 12 en nog veel meer; het factoriseert als 26 × 32 × 52 × 127. Omdat 1 inch exact 2,54 cm is, maakt een eenheid die zowel door 360000 als door een nette fractie van 914400 deelbaar is het mogelijk om inches, centimeters en points als integers uit te drukken zonder afronding aan de eenheidsgrens. Waar een floating-point "1,27 cm" zou weglopen, bewaart EMU 457200 en blijft exact

De andere eenheid die hier telt is de point. Een typografische point is 1/72 inch, dus er zijn 12700 EMU per point (914400 / 72). Points zijn hoe Excel zelf onder de motorkap denkt over rijhoogtes, lettergroottes en marges, en daarom is het nuttig om afbeeldingsgeometrie in points aan te bieden wanneer je een afbeelding wilt laten uitlijnen op tekstmetrics in plaats van op een liniaal. HotXLS codeert alle vier relaties als unit-constanten in de bibliotheek:

Die laatste regel is de kern van de postzegel-bug. Een pixel heeft pas een fysieke grootte zodra je een DPI vastzet, en 9525 EMU is de grootte van een pixel specifiek bij 96 DPI. Excel's standaard render-DPI is 96, dus een afbeelding van 100 pixels valt op 100 × 9525 = 952500 EMU, ongeveer 2,54 cm in een standaardconfiguratie, maar niets in het bestand garandeert dat de consumer 96 gebruikt. Auteur in echte units en die ambiguïteit verdwijnt: 4 cm is 4 cm, of het scherm nu 96 of 220 DPI is

De geometrie-oppervlakte van TXLSXImage

Een ingesloten afbeelding in HotXLS is een TXLSXImage. De canonieke opslag bestaat uit twee integer-velden, WidthEMU en HeightEMU, geankerd op een een-based Row en Col (de cel linksboven waar de afbeelding aan hangt). De echte-eenheden-eigenschappen zijn berekende weergaven bovenop die EMU-velden, geen aparte staat. Als je WidthCM leest, deel je de EMU door 360000, en als je hem schrijft, vermenigvuldig je en rond je terug. Dus elke dimensie die je zet is gewoon een andere spelling van dezelfde onderliggende EMU-waarde:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - de integer bron van waarheid

Je voegt een afbeelding toe met AddImage(ARow, ACol, AData, AFormat), waarbij je de ruwe gecodeerde bytes en een TXLSXImageFormat doorgeeft (xlsxImagePng, xlsxImageJpeg, xlsxImageGif of xlsxImageBmp); de methode geeft de zero-based index in de Images-collectie van het werkblad terug. Er is ook AddImageFromFile(ARow, ACol, AFileName), dat het formaat afleidt uit de bestandsextensie. Let op de indexbasis: AddImage geeft zero-based terug en Images[] is zero-based, in scherp contrast met de one-based Cells[Row, Col]-grid, dus ga er niet vanuit dat die twee samenvallen

Een pas aangemaakte afbeelding staat standaard op 100×100 pixels, dus 952500 EMU vierkant, ongeveer een box van 2,54 cm bij 96 DPI. Die default bestaat zodat een afbeelding zichtbaar is zelfs als je vergeet haar te schalen, maar voor elke echte lay-out moet je een expliciete fysieke maat zetten in plaats van op de pixel-afgeleide default te vertrouwen

Schalen en de aspectratio-vlag

Wanneer je relatief wilt schalen in plaats van naar een absoluut doel, bijvoorbeeld een grafiekafbeelding tot 60% van de geïmporteerde maat verkleinen, gebruik je Scale:

APercent is een percentage waarbij 100 onveranderd betekent, 150 de grootte met de helft vergroot en 50 halveert. Met AKeepAspect op de standaardwaarde True vermenigvuldigen breedte en hoogte met dezelfde factor, zodat de verhoudingen behouden blijven en een afbeelding van 4×3 cm na Scale(150) 6×4,5 cm wordt. Geef False door en alleen de breedte schaalt, de hoogte blijft exact gelijk. Dat asymmetrische gedrag is opzettelijk: als je één as onafhankelijk wilt uitrekken, zijn de expliciete WidthCM/HeightCM-setters het juiste gereedschap, en de niet-aspecttak van Scale is er voor het smallere geval waarin je alleen de breedte aanpast. Het is makkelijk om Scale(150, False) te lezen als "rek beide vrij uit" en verrast te worden, dus grijp naar de setters wanneer je echt twee onafhankelijke dimensies bedoelt

Eén klein gedrag om te kennen: Scale(100) keert direct terug zonder een van beide velden aan te raken, dus het is veilig om hem onvoorwaardelijk in een lus aan te roepen waarin het percentage misschien 100 is. En omdat de geometrie als integer EMU wordt opgeslagen, rondt elke setter af. Round-tripping via fractionele centimeters kan daardoor een fractie van een EMU afwijken, ver onder iets zichtbaars maar goed om te weten als je ooit exacte gelijkheid in een test assert. Voor pixelnauwkeurige controle zet je WidthEMU en HeightEMU rechtstreeks en sla je de unitconversie helemaal over

Geometrie weer teruglezen

De afbeeldingscollectie is opvraagbaar, wat telt wanneer je een bestaand workbook laadt en moet inspecteren of aanpassen wat al aanwezig is in plaats van wat je net hebt toegevoegd. Images.Count telt elke afbeelding op het blad, Images[i] indexeert ze zero-based, en FindAt(ARow, ACol) geeft de afbeelding terug die aan een specifieke cel is verankerd, of nil als er geen is. Er is ook IndexOfCell voor de index in plaats van het object, en DeleteAt / DeleteInRange voor verwijdering

Omdat de echte-eenheden-eigenschappen live weergaven zijn, rapporteert een afbeelding die uit een andere tool op een bepaalde EMU-maat is geïmporteerd zijn geometrie meteen in centimeters, zonder dat jij iets hoeft te converteren. Dat past goed bij het bredere tekenmodel; als je naast rasterafbeeldingen ook grafieken en shapes plaatst, behandelt de begeleidende gids over HotXLS-grafieken, afbeeldingen en Excel-tekeningen in Delphi het anchor-model dat die objecten delen

Metric page-setup-marges

Dezelfde spanning tussen EMU en echte units duikt één niveau hoger op, bij de pagina. OOXML en Excel bewaren afdrukmarges in inches, wat onhandig is als je rapportsjablonen in millimeters zijn gespecificeerd, zoals in het grootste deel van de wereld buiten de VS. v2.91.0 voegt centimeter-wrappers toe bovenop de inch-marges: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM en MarginFooterCM. Elk daarvan is een dunne convenience over de corresponderende inch-property, met de exacte 1 inch = 2,54 cm-conversie

De inch-properties (MarginLeft en de rest) blijven de canonieke opslag, dus je kunt de twee mixen, een bovenmarge in centimeters zetten en hem in inches teruglezen of omgekeerd, en het bestand dat naar schijf wordt geschreven is in beide gevallen identiek. De conversie is gewoon vermenigvuldigen met 2,54, zonder afronding naar een grove grid, dus 2 cm blijft 2 cm tot op volledige double-precisie. Dit is dezelfde metrische-conveniencefilosofie als de afbeeldingsgeometrie: het formaat spreekt onder de motorkap imperiaal, en de bibliotheek laat je schrijven in de eenheid die in jouw specificatie staat. Voor de omliggende rapportlay-out, titels, metadatablokken en totalen, zie samengevoegde cellen en rapporttemplate-lay-out in HotXLS, waar deze marges samen met samenvoegingen en een print area worden gebruikt

Een notitie over wat de geometrie wel en niet garandeert

De geometrie-properties sturen de gedeclareerde grootte van de afbeelding in het bestand, dus de maat waarop een conformerende consumer hem rendert. Ze resamplen de image-bytes niet; een PNG van 50×50 pixels die op 8 cm wordt gezet, wordt groter geschaald en ziet er blokkerig uit, precies zoals in Excel. Schalen is een lay-outoperatie, geen beeldbewerking, dus geef de afbeelding genoeg bronresolutie voor de fysieke maat die je wilt. De bibliotheek hercodeert formaten ook niet opnieuw: de bytes die je aan AddImage geeft worden 그대로 opgeslagen en weggeschreven, met het TXLSXImageFormat dat je opgeeft. Geef JPEG-bytes door maar tag ze als xlsxImagePng en je produceert een bestand dat Excel niet kan openen, dus laat AddImageFromFile het formaat uit de extensie afleiden wanneer dat kan

Dit alles is niet exotisch zodra je het ene idee eronder internaliseert: in OOXML is fysieke maat de echte grootheid en pixels zijn daar een afgeleide, DPI-afhankelijke schaduw van. Auteur afbeeldingen en marges in centimeters, inches of points, laat HotXLS ze naar exacte EMU mappen, en je facturen en rapporten drukken op elke machine hetzelfde formaat af

De afbeeldingsgeometrie-, schaal- en metrische-marge-API's die hier zijn beschreven, worden meegeleverd met de HotXLS Delphi spreadsheet component, die XLS en XLSX leest en schrijft vanuit Delphi en C++Builder zonder dat Excel geïnstalleerd hoeft te zijn

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)
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;
procedure Scale(APercent: Double; AKeepAspect: Boolean = True);
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
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;
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;