Articol tehnic

Geometria imaginilor HotXLS în Delphi: EMU, cm și scalare

Pui un logo de 600×400 pixeli în antetul unei facturi generate, arată bine pe monitorul tău de dezvoltare la 96-DPI, iar o săptămână mai târziu un client pe un laptop high-DPI raportează că se tipărește cât un timbru poștal. Pixelii nu s-au schimbat. S-a schimbat doar presupunerea că un număr de pixeli înseamnă o dimensiune fizică, iar în OOXML nu înseamnă. O imagine de foaie de calcul își poartă dimensiunile în EMU și, până nu gândești în EMU sau în unități reale care se mapează curat pe ele, layout-ul tău depinde de DPI-ul pe care îl presupune mașina de randare

HotXLS este un component VCL nativ pentru foi de calcul în Delphi și C++Builder care citește și scrie XLS și XLSX fără Excel sau vreo dependență COM. Începând cu v2.91.0, obiectul de imagine XLSX nu te mai obligă să faci manual aritmetica unităților: pe lângă EMU brut, expune lățimea și înălțimea în centimetri, inci și puncte, plus o Scale metodă care redimensionează cu un procent și, opțional, blochează raportul de aspect. Articolul acesta este despre ce este de fapt EMU, de ce a ales DrawingML această unitate și cum să folosești noua suprafață de geometrie pentru a poziționa imaginile după dimensiunea fizică, nu după un număr de pixeli în care nu poți avea încredere

Ce este un EMU și de ce folosește DrawingML unul

EMU înseamnă English Metric Unit și este unitatea de lungime de bază din DrawingML, stratul de desen partajat de întreaga familie Office Open XML (ECMA-376, Part 1, §20). Un EMU este definit astfel încât să existe exact 914400 EMU per inch și 360000 EMU pe centimetru. Aceste două constante sunt tot motivul pentru care există unitatea. 914400 se divide cu 2, 3, 4, 5, 6, 8, 9, 10, 12 și multe altele; se factorizează în 26 × 32 × 52 × 127. Pentru că 1 inch = 2.54 cm exact, alegerea unei unități divizibile atât cu 360000, cât și cu o fracție curată din 914400 face ca formatul să poată exprima inci, centimetri și puncte ca numere întregi fără rotunjire la limita unității. Când un "1.27 cm" în virgulă mobilă ar deriva, EMU stochează 457200 și rămâne exact

Cealaltă unitate care contează aici este punctul. Un punct tipografic este 1/72 inch, așa că există 12700 EMU per punct (914400 / 72). Punctele sunt modul în care Excel însuși gândește despre înălțimea rândurilor, dimensiunea fonturilor și marginile în culise, ceea ce face ca expunerea geometriei imaginilor în puncte să fie utilă atunci când vrei ca o imagine să se alinieze cu metricile textului, nu cu o riglă tipărită. HotXLS codifică toate cele patru relații ca constante de unitate în bibliotecă:

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)

Ultimul rând este miezul problemei cu dimensiunea de timbru poștal. Un pixel are o dimensiune fizică doar după ce fixezi un DPI, iar 9525 EMU este dimensiunea unui pixel la 96 DPI, în mod specific. DPI-ul implicit de randare al Excel este 96, așa că o imagine de 100 de pixeli ajunge la 100 × 9525 = 952500 EMU ≈ 2.54 cm într-o configurație implicită, însă nimic din fișier nu garantează că aplicația care îl consumă folosește 96. Scrie în unități reale și această ambiguitate dispare: 4 cm sunt 4 cm, fie că ecranul are 96 sau 220 DPI

Suprafața de geometrie TXLSXImage

O imagine încorporată în HotXLS este un TXLSXImage. Stocarea ei canonică are două câmpuri întregi, WidthEMU și HeightEMU, ancorate la un Row și Col (celula din stânga sus de care atârnă imaginea). Proprietățile în unități reale sunt vederi calculate peste acele câmpuri EMU, nu stare separată, iar citirea WidthCM împarte EMU la 360000, iar scrierea îl înmulțește și îl rotunjește înapoi. Așadar, fiecare dimensiune pe care o setezi este doar o altă formă de a scrie aceeași valoare EMU de bază:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - sursa de adevăr în format întreg

Adaugi o imagine cu AddImage(ARow, ACol, AData, AFormat), transmițând octeții brute codificați și o TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, sau xlsxImageBmp); aceasta returnează cu indexare de la zero în colecția foii de calcul. Mai există și Images, care deduce formatul din extensia fișierului. Reține baza indexului: AddImageFromFile(ARow, ACol, AFileName) returnează indexare de la zero, iar AddImage este cu indexare de la zero, ceea ce contrastează deliberat cu grila Images[] bazată pe unu, așa că nu presupune că cele două coincid.Cells[Row, Col] grila, așa că nu presupune că cele două coincid

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;

O imagine creată recent are implicit 100×100 pixeli, adică un pătrat de 952500 EMU, aproximativ o cutie de 2,54 cm la 96 DPI. Această valoare implicită există ca imaginea să fie vizibilă chiar dacă uiți să îi stabilești dimensiunea, dar pentru orice aspect real ar trebui să setezi o dimensiune fizică explicită, nu să te bazezi pe valoarea implicită derivată din pixeli

Scalarea și indicatorul aspect-ratio

Când vrei să redimensionezi raportat la dimensiunile curente, nu la o țintă absolută - să zicem, să micșorezi o imagine de grafic la 60% din dimensiunea cu care a fost importată - folosește Scale:

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

APercent reprezintă un procent în care 100 înseamnă neschimbat, 150 mărește cu jumătate, 50 înjumătățește. Cu AKeepAspect la valoarea implicită True, atât lățimea, cât și înălțimea se multiplică cu același factor, astfel încât proporțiile se păstrează, iar o imagine de 4×3 cm devine 6×4,5 cm după Scale(150). Transmite False și doar lățimea se scalează - înălțimea rămâne exact cum era. Această asimetrie este intenționată: când vrei să întinzi independent o axă, instrumentul potrivit este setarea explicită a WidthCM/HeightCM setterelor, iar ramura fără aspect din Scale există pentru cazul mai restrâns în care ajustezi doar lățimea. E ușor să citești Scale(150, False) ca „întinde ambele liber” și să fii surprins, așa că apelează la settere când chiar te referi la două dimensiuni independente

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 comportament mic de știut: Scale(100) scurtcircuitează și revine fără să atingă niciun câmp, astfel încât este sigur să o apelezi necondiționat într-o buclă în care procentul poate fi 100. Iar pentru că geometria este stocată ca întreg EMU, fiecare setter rotunjește. Parcurgerea dus-întors prin centimetri fracționari poate devia astfel cu o fracțiune de EMU - mult sub orice nivel vizibil, dar util de știut dacă verifici vreodată egalitatea exactă într-un test. Pentru control pixel-perfect, setează WidthEMU și HeightEMU direct și sari peste conversia de unități cu totul

Citirea geometriei înapoi

Colecția de imagini poate fi interogată, ceea ce contează când încarci un registru de lucru existent și trebuie să inspectezi sau să ajustezi ceea ce este deja acolo, nu doar ceea ce ai adăugat acum. Images.Count enumeră fiecare imagine de pe foaie, Images[i] le indexează de la zero, iar FindAt(ARow, ACol) returnează imaginea fixată la o anumită celulă sau nil dacă nu există niciuna. Există și IndexOfCell pentru index, nu pentru obiect, și DeleteAt / DeleteInRange pentru eliminare

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;

Deoarece proprietățile în unități reale sunt vizualizări live, o imagine importată la o anumită dimensiune EMU dintr-un alt instrument își raportează imediat geometria în centimetri, fără niciun pas de conversie din partea ta. Acest lucru se potrivește firesc cu modelul mai larg de desen; dacă plasezi grafice și forme, precum și imagini raster, ghidul însoțitor despre HotXLS: diagrame, imagini și desene Excel în Delphi acoperă modelul de ancorare pe care îl împart aceste obiecte

Margini metrice pentru configurarea paginii

Aceeași tensiune dintre EMU și unitățile reale apare și la un nivel mai sus, la pagină. OOXML și Excel stochează marginile de imprimare în inci, ceea ce este incomod dacă șabloanele tale de raport sunt specificate în milimetri, ca în mare parte a lumii din afara SUA. v2.91.0 adaugă proprietăți de comoditate în centimetri pentru marginile în inci: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, și MarginFooterCM. Fiecare este o comoditate subțire peste proprietatea corespunzătoare în inci, convertind exact la raportul 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;

Proprietățile în inci (MarginLeft și celelalte) rămân stocarea canonică, așa că le poți amesteca pe ambele, setezi o margine superioară în centimetri și o citești înapoi în inci, sau invers, iar fișierul scris pe disc este identic în ambele cazuri. Conversia este o simplă înmulțire cu 2.54, fără rotunjire la o grilă grosieră, așa că 2 cm rămân 2 cm la precizie dublă completă. Aceeași filosofie de comoditate metrică se aplică și geometriei imaginii: formatul vorbește, pe dedesubt, în sistem imperial, iar biblioteca îți permite să redactezi în unitatea în care este scrisă specificația ta. Pentru aranjarea raportului din jur, titluri, blocuri de metadate, totaluri, vezi celule îmbinate și aspectul șablonului de raport în HotXLS, care folosește aceste margini împreună cu intervale îmbinate și o zonă de imprimare

O notă despre ce garantează și ce nu garantează geometria

Proprietățile de geometrie controlează dimensiunea declarată a imaginii din fișier, dimensiunea la care o va reda un consumator conform. Nu redesenează datele imaginii; un PNG de 50×50 pixeli dimensionat la 8 cm se va mări și va arăta pătrățos, exact ca în Excel. Redimensionarea este o operație de aspect, nu una de procesare a imaginii, așa că oferă imaginii suficientă rezoluție sursă pentru dimensiunea fizică pe care o intenționezi. Biblioteca nici nu reîncodifică formatele: datele pe care le transmiți la AddImage sunt stocate și scrise mai departe ca atare, împreună cu TXLSXImageFormat formatul pe care îl declari. Dacă transmiți biți JPEG, dar îi etichetezi drept xlsxImagePng și vei produce un fișier pe care Excel nu îl poate deschide, așa că lasă AddImageFromFile să deducă formatul din extensie când poți

Nimic din toate acestea nu este exotic odată ce internalizezi ideea de bază din spate: în OOXML, dimensiunea fizică este mărimea reală, iar pixelii sunt o umbră derivată, dependentă de DPI. Creează imaginile și marginile în centimetri, inci sau puncte, lasă HotXLS să le mapeze la EMU exacte, iar facturile și rapoartele tale se imprimă la aceeași dimensiune pe orice mașină care le deschide

API-urile pentru geometria imaginilor, scalare și margini metrice descrise aici sunt livrate împreună cu componenta pentru foi de calcul HotXLS Delphi, care citește și scrie XLS și XLSX din Delphi și C++Builder fără a avea nevoie de o instalare Excel