Exportované PDF kladie každú hranicu stĺpca o pol znaku ľavejšie, než ju kreslí Excel, a každá zalomená bunka sa teraz láme na inom mieste. Šírka stĺpca Excelu sa nemeria v znakoch ani bodoch. Meria sa v jednotkách Max Digit Width (MDW) Normal fontu zošita a HotXLS meria ten font cez GDI pred každým zostavením stránkovania. Režim zlyhania je tichý: nič nespadne, uložené šírky prežijú round-trip bajt po bajte a geometria je stále vedľa o pár percent na stĺpec, kým akumulovaný drift nepotlačí jednostranovú tabuľku na dve
V akej jednotke je šírka stĺpca Excelu?
Šírka stĺpca v pracovnom liste je počet znakov číslice Normal fontu zošita, nie absolútne meranie. ECMA-376 §18.3.1.13 definuje atribút width elementu <col> v termínoch Maximum Digit Width tohto fontu pri 96 dpi a dáva konverziu z uloženej šírky späť na pixely ako skracujúci výraz nad MDW. Pre Calibri 11, ktorý Excel dodáva ako Normal štýl, MDW meria 7 pixelov. Prehnite predvolenú šírku 8.43 jednotiek cez špecifikačný vzorec s MDW 7 a dostanete presne 64 pixelov, čo je 48 bodov pri 96 dpi. To sú čísla, ktoré Excel sám hlási, takže tvoria užitočnú kontrolu: ak vaša konverzia reprodukuje 8.43 jednotiek na 64 pixelov, aritmetika je správna a len vstup MDW môže byť ešte zlý
const
// Maximálna šírka číslice (MDW) predvoleného textového fontu v pixeloch pri 96 dpi.
// Calibri 11 meria 7 px, čo reprodukuje presné pixelové šírky,
// ktoré Excel ukladá (8.43 jednotiek -> 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 96 dpi -> body
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS drží tú aritmetiku v presne jednej funkcii, v jednotke lxPagination, takže existuje jediné miesto, kde môže byť pravítko zlé. + 5 je padding, ktorý Excel pridáva pre mriežku a okraje buniek, * 0.75 prevádza pixely 96 dpi na PostScript body a podlaha pri MinimumColumnWidth existuje preto, aby patologicky úzky stĺpec stále nechal prúžok, do ktorého renderer dokáže nakresliť orámovanie. Verejný vstupný bod ColumnWidthToPoints drží svoj starý jednoargumentový podpis a preposiela merané MDW tejto funkcii, čo umožnilo správanie zmeniť bez dotknutia jediného miesta volania
Prečo ne-Calibri Normal font posúva každú hranicu
Drift je multiplikatívny, čo je dôvod, prečo sa číta ako renderovacia chyba, nie chyba jednotiek. MDW je faktor na šírke, nie ofset. Potlačte MDW z 7 na 8 a predvolený stĺpec 8.43 jednotiek pôjde z 64 pixelov na 72, skok o 8 pixelov alebo 6 bodov na jednom stĺpci. Desať stĺpcov toho a pravý okraj tabuľky sa pohnul o takmer palec. Zošity, ktoré to spustia, sú úplne obyčajné: čokoľvek vygenerované reportovacím nástrojom, ktorý pečiatkuje Arial alebo Segoe UI do Normal štýlu, čokoľvek uložené z ERP exportnej šablóny, čokoľvek, čo zákazník raz prestylizoval a zabudol
Dva súvisiace systémy rozloženia zdede chybu, namiesto aby ju spôsobovali. Zlúčené regióny sčítavajú bodové šírky svojich členských stĺpcov, takže merge, ktorý sa zmestil na jednu stranu v Exceli, môže po MDW drifte pretekať, čo stojí za zapamätanie, keď staviate šablóny reportov so zlúčenými bunkami. Shrink-to-fit porovnáva meranú šírku textu s tou istou šírkou stĺpca, takže zlé MDW tiež mení, ktoré bunky sa zmenšia a o koľko. Rovnaká rodina zmatenia jednotiek sa objavuje v kresbových ukotveniach, kde geometria obrázkov a škálovanie EMU má vlastný konverzný reťazec, ktorý sa dá pokaziť
Ako HotXLS meria MDW za behu
HotXLS rozlíši MDW zo samotného zošita namiesto predpokladu konštanty a dve procedúry robia prácu. PaginationApplyNormalFont prečíta Normal štýl fontu zo zošita a beží na vrchole zostavenia stránkovania, pred vypočítaním akejkoľvek geometrie stĺpcov; najprv sa resetuje na Calibri 11, takže zošit bez fontovej tabuľky nemôže zdediť zastaraný stav z predchádzajúceho zostavenia. Font Normal štýlu je fonts[0] v styles.xml, vystavený komponentom ako Workbook.Fonts[0]
// Číta fonts[0] (font Normal štýlu) z pracovného listu zošita.
// Klasické listy bez fontovej tabuľky držia predvolené 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á procedúra, PaginationMeasureMdW, pýta GDI rozsah jediného znaku '0' cez GetTextExtentPoint32W na zdieľanom off-screen bitmapovom canvas, pada späť na tmAveCharWidth z GetTextMetricsW, keď volanie rozsahu zlyhá, a pada späť na DefaultMDW, keď ani jedno nie je dostupné. Jeho cache je jediný slot kľúčovaný (name, size), čo znie hrubo, kým sa nepozriete na prístupový vzor: zostavenie stránkovania pýta ten istý Normal font na každom stĺpci každej strany, takže jeden slot má takmer dokonalú mieru zásahov a stojí tri porovnania na volanie
Čo sa stane bez fontovej tabuľky, bez GUI alebo s chýbajúcim fontom?
HotXLS degraduje na konštantu Calibri 11 v každom prípade, keď sa skutočný Normal font nedá určiť, a robí to mlčky zámerne. Klasické pracovné listy BIFF sú ten bežný: legacy formáty neniosu XLSX fontový fond, na ktorý by fonts[0] odkazovalo, takže typová stráž skončí skoro a predvolené MDW 7 platí. To nie je oprava, je to predchádzajúce správanie zachované zámerne, takže pridanie merania na XLSX cestu nemohlo regresovať klasicko-formátový výstup
Závislosť GDI je úprimná výhrada. Meranie beží proti Windows device contextu, takže cesta predpokladá Windows hostiteľa s nainštalovaným fontom. V službe alebo headless build agentovi GDI textové metriky sa spravidla stále rozlíšia, ale font, ktorý nie je nainštalovaný na tom stroji, sa substituuje font mapperom a meriate substitút. Nikdy nespadne nahlas; vráti presvedčivé číslo pre zlý rez písma. Ak serverové exporty musia zodpovedať desktop referencii, nainštalujte fonty, ktoré vaše šablóny menujú, na exportný hostiteľ, alebo pripnite Normal font pred vyvolaním cesty PDF exportu pracovného listu
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Pripnite Normal font, aby MDW merané na tomto hostiteľovi bolo to,
// proti ktorému bolo rozloženie navrhnuté, nie substitút font mappera.
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 meraní a tá jedna, ktorá spadla na Win64
Keď je textové meranie GDI okruh namiesto násobenia, musí sa cachovať a cachovanie vo vnútri renderovacej passy je miesto, kde táto práca pustila krv. Shrink-to-fit slučka krokuje veľkosť fontu nadol v prírastkoch 0.5 pt a znova meria po každom kroku, takže jedna bunka môže zavolať PaginationMeasureTextWidth tuctokrát s tým istým reťazcom a word wrap ho volá znova na každý kandidátsky riadok. Memo kľúčované názvom fontu, veľkosťou a textom zrúti to na jedno GDI volanie na odlišný reťazec, uložené v TStringList ako páry názov/hodnota
Druhá cache pridaná popri nej nebola taká uprataná. Render pass 5 rozlíši fontový fond na bunku cez FontIndex a jeho memo používalo paralelné dynamické polia s ručne udržiavaným FontMemoCount. Prvá verzia zabudla zavolať ResetFontMemo na začiatku každej strany, takže počet stále stúpal naprieč stranami, zatiaľ čo polia nie, a kód zapisoval za koniec všetkých. Na Win32 to mlčky škriabalo do susednej heapu a skončilo; na Win64 to okamžite vyvolalo access violation pri zápise na 0x538. Zovšeobecniteľná lekcia: cache podložená poľom držaná v variable na úrovni jednotky musí byť resetovaná na vstupe každej passy, ktorá ju používa, pretože string list alebo slovník odpúšťa chýbajúci reset rastom a paralelné polia nie
Kontrola vlastnej konverzie
Nepotrebujete komponent na overenie čohokoľvek z toho. Vezmite zošit, ktorého Normal font nie je Calibri 11, prečítajte šírku z <col width="..."/> a prehnite ju špecifikačným vzorcom dvakrát, raz s MDW 7 a raz s MDW, ktoré váš renderer skutočne meria pre ten font; ak sa odpovede líšia a váš výstup zhoduje s prvou, našli ste drift. Geometria stĺpcov je jedna z tých častí spreadsheet enginu, ktorá je buď neviditeľná, alebo jediná vec, ktorú ktokoľvek zaznamená, a dostať ju správne znamená považovať Normal font za vstup rozloženia, nie štylistický detail. Ak staviate Delphi alebo C++Builder aplikácie, ktoré čítajú, zapisujú, renderujú a tlačia Excel zošity bez nainštalovaného Office, HotXLS Delphi Excel component zvláda MDW meranie, stránkovací model a PDF pipeline za jednou sadou VCL tried