Technický článek

Šířka sloupce Excelu a Max Digit Width (MDW) v Delphi

Exportované PDF dává každou hranici sloupce o půl znaku vlevo od místa, kde ji Excel kreslí, a každá zalomená buňka teď láme jinde. Šířka sloupce Excelu se neměří ve znacích ani v bodech. Měří se v jednotkách Max Digit Width (MDW) Normal písma sešitu a HotXLS měří to písmo přes GDI před každým sestavením stránkování. Režim selhání je tichý: nic nehodí výjimku, uložené šířky projdou tam a zpět bajt za bajt a geometrie je pořád mimo o pár procent na sloupec, dokud nashromážděný drift nepostrčí jednostránkovou tabulku na dvě

V jakých jednotkách je šířka sloupce Excelu?

Šířka sloupce v listu je počet znaků číslic Normal písma sešitu, ne absolutní měření. ECMA-376 §18.3.1.13 definuje atribut width prvku <col> přes Maximum Digit Width toho písma při 96 dpi a dává konverzi z uložené šířky zpět na pixely jako zkracující výraz přes MDW. Pro Calibri 11, který Excel dodává jako Normal styl, MDW měří 7 pixelů. Pusťte výchozí šířku 8,43 jednotek specifikačním vzorcem s MDW 7 a dostanete přesně 64 pixelů, což je 48 bodů při 96 dpi. To jsou čísla, která hlásí sám Excel, takže tvoří užitečnou kontrolu: pokud vaše konverze reprodukuje 8,43 jednotek na 64 pixelů, aritmetika sedí a jediné, co může být pořád špatně, je vstup MDW

const
  // Maximum digit width (MDW) výchozího tělového písma v pixelech při 96 dpi.
  // Calibri 11 měří 7 px, což reprodukuje přesné pixelové šířky,
  // které Excel ukládá (8,43 jednotek -> 64 px -> 48 pt).
  DefaultMDW = 7;
  MinimumColumnWidth = 24.0;

function ColumnWidthToPointsMdW(Value: Double; MdW: Integer): Double;
var
  Pixels: Integer;
begin
  if Value <= 0 then
    Value := 8.43;
  if MdW <= 0 then
    MdW := DefaultMDW;
  Pixels := Trunc(((256 * Value + Trunc(128 / MdW)) / 256) * MdW) + 5;
  Result := Pixels * 0.75; // pixely při 96 dpi -> body
  if Result < MinimumColumnWidth then
    Result := MinimumColumnWidth;
end;

HotXLS drží tu aritmetiku v přesně jedné funkci, v jednotce lxPagination, takže existuje jediné místo, kde může pravítko být špatně. To + 5 je výplň, kterou Excel přidává pro mřížku a okraje buněk, to * 0.75 převádí pixely 96 dpi na PostScript body a spodek na MinimumColumnWidth existuje proto, aby patologicky úzký sloupec nechal pruh, do kterého renderer nakreslí okraj. Veřejný vstupní bod ColumnWidthToPoints si drží svůj starý jednoargumentový podpis a přeposílá změřené MDW této funkci, což dovolilo změně chování přistát bez doteku jediného místa volání

Konverzní řetězec šířky sloupce HotXLS v Delphi, krmící specifikační vzorec změřeným Max Digit Width Normal písma sešitu, takže uložená šířka 8,43 jednotek se stane 64 pixely a pak 48 body
Uložená šířka je počet číslic, takže změřené MDW Normal písma je vstup do vzorce, ne stylový detail, a okruh 8,43 na 64 na 48 kontroluje aritmetiku

Proč Normal písmo jiné než Calibri posune každou hranici

Drift je multiplikativní, proto se čte jako chyba renderování, ne jako chyba jednotek. MDW je činitel šířky, ne posun. Postrčte MDW ze 7 na 8 a výchozí sloupec 8,43 jednotek jde z 64 pixelů na 72, skok 8 pixelů neboli 6 bodů na jednom sloupci. Deset takových sloupců a pravá hrana tabulky se pohnula o většinu palce. Sešity, do kterých to zkousne, jsou naprosto obvyklé: cokoliv vygenerované nástrojem sestav, který otiskne Arial nebo Segoe UI do Normal stylu, cokoliv uloženého z ERP exportní šablony, cokoliv, co zákazník jednou přestyloval a zapomněl na to

Dva příbuzné rozvrhové systomy chybu zdědí, místo aby ji způsobovaly. Sloučené oblasti sčítají bodové šířky svých členských sloupců, takže sloučení, které se vešlo na jednu stránku v Excelu, může po MDW driftu přetéct, na co je dobré pamatovat, když stavíte šablony sestav se sloučenými buňkami. Zmenšení na míru porovnává změřenou šířku textu s touže šířkou sloupce, takže špatné MDW mění i to, které buňky se smrští a o kolik. Tatáž rodina zmatku jednotek se objevuje v kotvách kresby, kde geometrie obrázků a škálování EMU má vlastní konverzní řetězec k pokazení

Srovnání dvou pravítek sloupců HotXLS, jedno měřené s MDW 7 pixelů a druhé s 8, ukazující, jak skok z 64 na 72 pixelů na sloupec hromadí přes deset sloupců, zatímco sloučené oblasti a zmenšení na míru chybu zdědí
Protože MDW násobí místo posouvání, jedno špatné měření pohně každou hranici sloupce a sloučené oblasti se zmenšováním na míru zdědí drift, aniž by cokoliv hodilo výjimku

Jak HotXLS měří MDW za běhu

HotXLS rozřešuje MDW ze samotného sešitu místo domněnky o konstantě a dvě procedury dělají práci. PaginationApplyNormalFont přečte písmo Normal stylu ze sešitu a běží na vrcholu sestavení stránkování, dřív než se spočítá jakákoli geometrie sloupců; nejdřív se resetuje na Calibri 11, takže sešit bez tabulky písem nemůže zdědit zvětralý stav z předchozího sestavení. Písmo Normal stylu je fonts[0] v styles.xml, vystavené komponentou jako Workbook.Fonts[0]

// Přečte fonts[0] (písmo Normal stylu) z workbooku listu.
// Klasické listy bez tabulky písem drží výchozí Calibri 11.
procedure PaginationApplyNormalFont(Worksheet: TObject);
var
  Sh: TXLSXWorksheet;
  Fnt: TXLSXFont;
begin
  PaginationNormalFontName := 'Calibri';
  PaginationNormalFontSize := 11;
  if not (Worksheet is TXLSXWorksheet) then
    Exit;
  Sh := TXLSXWorksheet(Worksheet);
  if (Sh.Workbook = nil) or (Sh.Workbook.Fonts.Count < 1) then
    Exit;
  Fnt := Sh.Workbook.Fonts[0];
  if Fnt.Name <> '' then
    PaginationNormalFontName := Fnt.Name;
  if Fnt.Size > 0 then
    PaginationNormalFontSize := Fnt.Size;
end;

Druhá procedura, PaginationMeasureMdW, ptá se GDI na rozsah jediného znaku '0' přes GetTextExtentPoint32W na sdíleném bitmapovém plátně mimo obrazovku, sestupuje k tmAveCharWidth z GetTextMetricsW, když volání rozsahu selže, a k DefaultMDW, když není k dispozici ani jedno. Její cache je jediný slot klíčovaný (jméno, velikost), což zní hrubě, dokud se nepodíváte na přístupový vzor: sestavení stránkování si žádá stejné Normal písmo na každém sloupci každé stránky, takže jeden slot má téměř dokonalou úspěšnost zásahů a stojí tři srovnání na volání

Co se stane bez tabulky písem, bez GUI nebo s chybějícím písmem?

HotXLS sestupuje ke konstantě Calibri 11 v každém případě, kdy skutečné Normal písmo nelze zjistit, a dělá to tiše záměrně. Klasické listy BIFF jsou ten obvyklý: zastaralé formáty nenosí fond písem XLSX, na který by fonts[0] mohl odkazovat, takže typová hlídka odejde brzy a výchozí MDW 7 platí dál. To není oprava, je to předchozí chování zachované záměrně, aby přidání měření do cesty XLSX nemohlo zregresovat výstup klasického formátu

Závislost na GDI je ta poctivá výhrada. Měření běží proti Windows kontextu zařízení, takže cesta předpokládá Windows hostitele s nainstalovaným písmem. Ve službě nebo v bezhlavém build agentovi se metriky textu GDI obvykle pořád rozřešou, ale písmo, které na tom stroji není nainstalované, vymění font mapper a vy měříte náhradníka. Nikdy to neselhává hlasitě; vrátí věrohodné číslo pro špatnou rez. Pokud exporty na serveru musí sedět s desktopovou referencí, nainstalujte na exportní hostitele písma, která vaše šablony jmenují, nebo přichyťte Normal písmo dřív, než zavoláte cestu exportu listu do PDF

var
  Book: TXLSXWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSXWorkbook.Create;
  Exporter := TXLSPDFExport.Create;
  try
    Book.Open('quarterly-report.xlsx');

    // Přichyťte Normal písmo, aby MDW změřené na tomto hostiteli bylo to,
    // proti kterému bylo rozvržení navržené, ne náhrada od font mapperu.
    if Book.Fonts.Count > 0 then
    begin
      Book.Fonts[0].Name := 'Calibri';
      Book.Fonts[0].Size := 11;
    end;

    Exporter.UseWorksheetPageSetup := True;
    Exporter.SaveAsPDF(Book, 'quarterly-report.pdf');
  finally
    Exporter.Free;
    Book.Free;
  end;
end;

Cache měření a ta, která spadla na Win64

Jakmile je měření textu GDI okruhem tam a zpět místo násobením, musí se cachovat a cachování uvnitř renderovacího průchodu je místo, kde tahle práce tekla krev. Smyčka zmenšení na míru schází velikost písma dolů po 0,5 pt krocích a přeměří po každém kroku, takže jedna buňka může volat PaginationMeasureTextWidth tucetkrát se stejným řetězcem a zalamování slov ho volá znovu pro každý kandidátní řádek. Memo klíčované názvem písma, velikostí a textem sbalí to na jedno GDI volání na odlišný řetězec, uložené v TStringList jako páry název/hodnota

Druhá cache přidaná vedle ní nebyla tak uklizená. Renderovací průchod 5 rozřešuje fond písem po buňce přes FontIndex a jeho memo používalo paralelní dynamická pole s ručně vedeným FontMemoCount. První verze zapomněla volat ResetFontMemo na začátku každé stránky, takže počet stále stoupal přes stránky, zatímco pole ne, a kód psal za konec všech. Na Win32 to tiše potřelo sousední haldu a došlo to; na Win64 to hned hodilo access violation při zápisu na 0x538. Zobecnitelná lekce: cache podepřená polem, držená v proměnné na úrovni jednotky, se musí resetovat na vstupu každého průchodu, který ji používá, protože seznam řetězců nebo slovník odpustí chybějící reset tím, že roste, a paralelní pole ne

Jak HotXLS rozřeší Normal písmo sešitu, měří jeho Max Digit Width přes GDI se dvěma zálohami a cachuje výsledek, vedle dvou mem renderovacích průchodů a resetovacího pravidla, které cache z paralelních polí potřebuje
MDW se rozřeší ze sešitu a změří přes GDI jednou na písmo, pak se cachuje podle klíče, zatímco mem renderovacích průchodů ukazují, proč cache z paralelních polí se musí resetovat na vstupu každého průchodu

Kontrola vlastní konverze

Nepotřebujete komponentu, abyste si cokoliv z toho ověřili. Vezměte sešit, jehož Normal písmo není Calibri 11, přečtěte šířku z <col width="..."/> a pusťte ji specifikačním vzorcem dvakrát, jednou s MDW 7 a jednou s MDW, které váš renderer skutečně měří pro to písmo; pokud se odpovědi liší a váš výstup sedí s tou první, našli jste drift. Geometrie sloupců je jedna z těch částí tabulkového enginu, která je buď neviditelná, nebo jediná, které si někdo všimne, a trefit ji znamená brát Normal písmo jako vstup do rozvržení, ne jako stylový detail. Pokud stavíte aplikace Delphi nebo C++Builder, které čtou, píšou, renderují a tisknou sešity Excelu bez nainstalovaného Office, HotXLS Delphi Excel komponenta zvládne měření MDW, model stránkování a PDF pipeline za jedinou sadou tříd VCL