Articolo tecnico

Geometria delle immagini HotXLS in Delphi: EMU, cm e ridimensionamento

Inserisci un logo da 600×400 pixel nell'intestazione di una fattura generata, lo vedi giusto sul monitor di sviluppo a 96 DPI e, una settimana dopo, un cliente con un portatile ad alta DPI segnala che viene stampato grande quanto un francobollo. I pixel non sono mai cambiati. È cambiata l'ipotesi che un numero di pixel corrisponda a una dimensione fisica, e in OOXML non è così. Un'immagine di foglio di calcolo porta le proprie dimensioni in EMU e, finché non ragioni in EMU, o nelle unità reali che vi si mappano in modo pulito, il layout dipende da quale DPI il motore di rendering decide di assumere

HotXLS è un componente VCL nativo per fogli di calcolo in Delphi e C++Builder che legge e scrive XLS e XLSX senza Excel né alcuna dipendenza COM. Dalla v2.91.0 l'oggetto immagine XLSX non ti costringe più a fare a mano l'aritmetica delle unità: oltre agli EMU grezzi espone larghezza e altezza in centimetri, pollici e punti, più un Scale metodo che ridimensiona in percentuale con il blocco opzionale del rapporto d'aspetto. Questo articolo spiega che cosa sia davvero un EMU, perché DrawingML lo abbia scelto e come usare la nuova superficie di geometria per posizionare le immagini in base alla dimensione fisica invece che a un conteggio di pixel di cui non ti puoi fidare

Che cos'è un EMU e perché DrawingML ne usa uno

EMU sta per English Metric Unit, ed è l'unità di lunghezza base di DrawingML, il livello di disegno condiviso da tutta la famiglia Office Open XML (ECMA-376, Parte 1, §20). Un EMU è definito in modo che ci siano esattamente 914400 EMU per pollice e 360000 EMU per centimetro. Queste due costanti sono l'unico motivo per cui l'unità esiste. 914400 è divisibile per 2, 3, 4, 5, 6, 8, 9, 10, 12 e molti altri; si fattorizza in 26 × 32 × 52 × 127. Poiché 1 inch = 2.54 cm esattamente, scegliere un'unità divisibile sia per 360000 sia per una frazione pulita di 914400 permette al formato di esprimere pollici, centimetri e punti come interi senza arrotondamenti al confine dell'unità. Dove un valore in virgola mobile "1.27 cm" deriverebbe, EMU memorizza 457200 e resta esatto

L'altra unità che conta qui è il punto. Un punto tipografico è 1/72 di pollice, quindi ci sono 12700 EMU per punto (914400 / 72). Excel ragiona internamente in punti per altezze delle righe, dimensioni dei font e margini, ed è per questo che esporre la geometria delle immagini in punti è utile quando vuoi che un'immagine si allinei alle metriche del testo invece che a un righello stampato. HotXLS codifica tutte e quattro le relazioni come costanti di unità nella libreria:

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)

Quell'ultima riga è il cuore del bug del francobollo. Un pixel ha una dimensione fisica solo quando fissi un DPI, e 9525 EMU è la dimensione di un pixel a 96 DPI nello specifico. Il DPI di rendering predefinito di Excel è 96, quindi un'immagine da 100 pixel arriva a 100 × 9525 = 952500 EMU ≈ 2.54 cm in una configurazione predefinita, ma nel file non c'è nulla che garantisca che l'applicazione che lo apre usi 96. Scrivi nelle unità reali e quell'ambiguità scompare: 4 cm è 4 cm sia che lo schermo sia a 96 sia a 220 DPI

La superficie di geometria di TXLSXImage

Un'immagine incorporata in HotXLS è una TXLSXImage. La sua memorizzazione canonica consiste in due campi interi, WidthEMU e HeightEMU, ancorati a una coppia di coordinate a base uno Row e Col (la cella in alto a sinistra da cui pende l'immagine). Le proprietà in unità reali sono viste calcolate sopra quei campi EMU, non uno stato separato: leggere WidthCM divide l'EMU per 360000 e scriverlo lo moltiplica e lo arrotonda indietro. Quindi ogni dimensione che imposti è solo un altro modo di scrivere lo stesso valore EMU sottostante:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - la fonte di verità intera

Aggiungi un'immagine con AddImage(ARow, ACol, AData, AFormat), passando i byte codificati grezzi e un TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, oppure xlsxImageBmp); restituisce l'indice a base zero nella raccolta `Images` del foglio. Esiste anche Images, che deduce il formato dall'estensione del file. Nota la base dell'indice: AddImageFromFile(ARow, ACol, AFileName) restituisce un indice a base zero e AddImage è a base zero, in contrasto deliberato con la griglia a base uno Images[] griglia, quindi non dare per scontato che coincidano.Cells[Row, Col]Un'immagine appena creata usa per impostazione predefinita 100×100 pixel, cioè un quadrato da 952500 EMU, circa un riquadro da 2.54 cm a 96 DPI. Quel valore predefinito serve a rendere visibile l'immagine anche se ti dimentichi di dimensionarla, ma per qualsiasi layout reale dovresti impostare una dimensione fisica esplicita invece di affidarti al valore predefinito derivato dai pixel

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;

Ridimensionamento e flag del rapporto d'aspetto

Quando vuoi ridimensionare in relazione alle dimensioni correnti invece che a un obiettivo assoluto, per esempio ridurre un'immagine di un grafico al 60% di ciò con cui è stata importata, usa

:Scale `APercent` è una percentuale in cui 100 significa invariato, 150 aumenta della metà, 50 dimezza. Con

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

APercent `AKeepAspect` al suo valore predefinito AKeepAspect `True`, larghezza e altezza si moltiplicano per lo stesso fattore, quindi le proporzioni restano e un'immagine da 4×3 cm diventa 6×4.5 cm dopo True. Passa Scale(150) `False` e solo la larghezza si ridimensiona - l'altezza resta esattamente com'era. Quell'asimmetria è intenzionale: quando vuoi stirare un asse in modo indipendente, lo strumento giusto sono i setter espliciti False/WidthCM e HeightCM , e il ramo non aspect di Scale serve al caso più ristretto di regolare solo la larghezza. È facile leggere Scale(150, False) come "stira entrambi liberamente" e restare sorpresi, quindi usa i setter quando intendi davvero due dimensioni indipendenti

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

Un piccolo comportamento da conoscere: Scale(100) esce subito e restituisce senza toccare nessuno dei due campi, quindi è sicuro chiamarlo senza condizioni in un ciclo in cui la percentuale potrebbe essere 100. E poiché la geometria è memorizzata come intero EMU, ogni setter arrotonda. Il round-trip attraverso centimetri frazionari può quindi deviare di una frazione di EMU, molto al di sotto di qualunque cosa sia visibile, ma utile da sapere se mai verifichi un'uguaglianza esatta in un test. Per un controllo pixel-perfect, imposta WidthEMU e HeightEMU direttamente e salta del tutto la conversione di unità

Leggere la geometria di ritorno

La raccolta di immagini è interrogabile, e questo conta quando carichi una cartella di lavoro esistente e devi ispezionare o modificare ciò che c'è già invece di ciò che hai appena aggiunto. Images.Count enumera ogni immagine del foglio, Images[i] le indicizza a base zero, e FindAt(ARow, ACol) restituisce l'immagine ancorata a una cella specifica, oppure nil se non ce n'è alcuna. Esiste anche IndexOfCell per l'indice anziché per l'oggetto, e DeleteAt / DeleteInRange per la rimozione

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;

Poiché le proprietà in unità reali sono viste live, un'immagine importata da un altro strumento a una certa dimensione EMU riporta immediatamente la propria geometria in centimetri, senza alcun passaggio di conversione da parte tua. Questo si abbina bene al modello di disegno più ampio; se stai posizionando anche grafici e forme oltre alle immagini raster, la guida complementare su Grafici, immagini e disegni Excel in Delphi con HotXLS copre il modello di ancoraggio che questi oggetti condividono

Margini metrici di impostazione pagina

La stessa tensione tra EMU e unità reali riemerge un livello più in là, a livello di pagina. OOXML e Excel memorizzano i margini di stampa in pollici, il che è scomodo se i modelli di report sono specificati in millimetri, come per gran parte del mondo fuori dagli Stati Uniti. La v2.91.0 aggiunge wrapper in centimetri sopra i margini in pollici: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, e MarginFooterCM. Ogni wrapper è una sottile comodità sopra la corrispondente proprietà in pollici, con conversione esatta nel rapporto 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;

Le proprietà in pollici (MarginLeft e le altre) restano l'archiviazione canonica, quindi puoi mischiare le due modalità, impostare un margine superiore in centimetri e leggerlo in pollici, o viceversa, e il file scritto su disco è identico in entrambi i casi. La conversione è una semplice moltiplicazione per 2.54, senza arrotondare a una griglia grossolana, quindi 2 cm resta 2 cm a piena precisione double. È la stessa filosofia di comodità metrica della geometria delle immagini: il formato parla imperiale sotto il cofano e la libreria ti permette di scrivere nella unità in cui è redatta la tua specifica. Per impaginare il report attorno, titoli, blocchi di metadati e totali, vedi celle unite e layout dei modelli di report in HotXLS, che usa questi margini insieme agli intervalli uniti e a un'area di stampa

Una nota su ciò che la geometria garantisce e ciò che non garantisce

Le proprietà di geometria controllano la dichiarata dimensione dell'immagine nel file, la dimensione alla quale un consumer conforme la renderà. Non ricampionano i byte dell'immagine; un PNG da 50×50 pixel dimensionato a 8 cm verrà ingrandito e apparirà a blocchi, esattamente come in Excel. Il dimensionamento è un'operazione di layout, non di elaborazione delle immagini, quindi fornisci all'immagine una risoluzione sorgente sufficiente per la dimensione fisica che intendi ottenere. La libreria inoltre non ricodifica i formati: i byte che passi a AddImage sono memorizzati e scritti invariati, con il TXLSXImageFormat che dichiari. Se passi byte JPEG ma li marchi come xlsxImagePng e produrrai un file che Excel non può aprire, quindi lascia che AddImageFromFile deduca il formato dall'estensione quando puoi

Niente di tutto questo è esotico una volta che interiorizzi l'idea alla base: in OOXML, la dimensione fisica è la quantità reale e i pixel sono la sua ombra derivata, dipendente dal DPI. Scrivi immagini e margini in centimetri, pollici o punti, lascia che HotXLS li mappi su EMU esatti e le tue fatture e i tuoi report si stamperanno nella stessa dimensione su ogni macchina che li apre

Le API di geometria delle immagini, di ridimensionamento e dei margini metrici descritte qui sono incluse nel componente Delphi per fogli di calcolo HotXLS, che legge e scrive XLS e XLSX da Delphi e C++Builder senza richiedere l'installazione di Excel