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í
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í
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
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