Den eksporterede PDF lægger hver kolonnegrænse et halvt tegn til venstre for, hvor Excel tegner den, og hver ombrydt celle bryder nu et andet sted. Excel-kolonnebredde måles ikke i tegn eller punkter. Den måles i Max Digit Width (MDW)-enheder af arbejdsbogens Normal-font, og HotXLS måler den font med GDI før hver pagineringsbuild. Fejlmåden er stille: intet kaster en fejl, de gemte bredder round-tripper byte for byte, og geometrien afviger stadig med et par procent pr. kolonne, indtil den akkumulerede drift skubber en tabel på én side ud på to
Hvilken enhed måles Excel-kolonnebredde i?
En kolonnebredde i et regneark er et antal ciffertegn i arbejdsbogens Normal-font, ikke en absolut måling. ECMA-376 §18.3.1.13 definerer width-attributten på <col> ud fra den fonts Maximum Digit Width ved 96 dpi og giver konverteringen fra en gemt bredde tilbage til pixels som et trunkerende udtryk over MDW. For Calibri 11, som Excel afskiber som Normal-typografien, måler MDW 7 pixels. Før standardbredden på 8.43 enheder gennem specifikationsformlen med MDW 7, og du får præcis 64 pixels, hvilket er 48 punkter ved 96 dpi. Det er tallene, Excel selv rapporterer, så de er en nyttig kontrol: hvis din konvertering gengiver 8.43 enheder til 64 pixels, er aritmetikken rigtig, og kun MDW-inputtet kan stadig være forkert
const
// Maximum digit width (MDW) for standardbrødtekstfonten i pixels ved 96 dpi.
// Calibri 11 måler 7 px, hvilket gengiver de præcise pixelbredder,
// Excel gemmer (8.43 enheder -> 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; // 96 dpi-pixels -> punkter
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS holder den aritmetik i præcis én funktion, i lxPagination-enheden, så der er ét enkelt sted, hvor linealen kan tage fejl. + 5 er den padding, Excel tilføjer til gitterlinjer og cellemarginer, * 0.75 konverterer 96 dpi-pixels til PostScript-punkter, og gulvet ved MinimumColumnWidth findes, så en patologisk smal kolonne stadig efterlader en stribe, rendereren kan tegne en kant ind i. Det offentlige indgangspunkt ColumnWidthToPoints beholder sin gamle signatur med ét argument og videresender en målt MDW til denne funktion, hvilket var det, der lod adfærdsændringen lande uden at røre et eneste kaldsted
Hvorfor en Normal-font, der ikke er Calibri, flytter hver grænse
Driften er multiplikativ, hvilket er grunden til, at den ligner en renderingsfejl frem for en enhedsfejl. MDW er en faktor på bredden, ikke et offset. Skub MDW fra 7 til 8, og standardkolonnen på 8.43 enheder går fra 64 pixels til 72, et hop på 8 pixels eller 6 punkter på én kolonne. Ti kolonner af dét, og tabellens højre kant er flyttet det meste af en tomme. Arbejdsbøger, der snubler i dette, er helt almindelige: alt, hvad der genereres af et rapportværktøj, der stampler Arial eller Segoe UI ind i Normal-typografien, alt gemt ud af en ERP-eksportskabelon, alt, hvad en kunde omstylede én gang og glemte
To relaterede layoutsystemer arver fejlen frem for at forårsage den. Flettede områder summerer deres medlemskolonners punktbredder, så en fletning, der passede på én side i Excel, kan flyde over efter MDW-drift, hvilket er værd at huske, når du bygger rapportskabeloner med flettede celler. Shrink-to-fit sammenligner målt tekstbredde med samme kolonnebredde, så den forkerte MDW ændrer også, hvilke celler der skrumper, og hvor meget. Samme familie af enhedsforvirring dukker op i tegneankre, hvor billedgeometri og EMU-skalering har sin egen konverteringskæde at tage fejl på
Hvordan HotXLS måler MDW ved kørselstid
HotXLS løser MDW ud fra arbejdsbogen selv frem for at antage en konstant, og to procedurer udfører arbejdet. PaginationApplyNormalFont læser Normal-typografiens font af arbejdsbogen og kører i toppen af pagineringsbuilden, før nogen kolonnegeometri beregnes; den nulstiller først til Calibri 11, så en arbejdsbog uden en fonttabel ikke kan arve forældet tilstand fra en tidligere build. Normal-typografiens font er fonts[0] i styles.xml, eksponeret af komponenten som Workbook.Fonts[0]
// Læser fonts[0] (Normal-typografiens font) af regnearkets arbejdsbog.
// Klassiske regneark uden en fonttabel beholder Calibri 11-defaulten.
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;
Den anden procedure, PaginationMeasureMdW, spørger GDI om omfanget af det enkelte tegn '0' gennem GetTextExtentPoint32W på et delt skærmfrit bitmap-canvas, falder tilbage til tmAveCharWidth fra GetTextMetricsW, når extent-kaldet fejler, og falder tilbage til DefaultMDW, når ingen af delene er tilgængelige. Dens cache er én enkelt slot nøglet efter (name, size), hvilket lyder råt, indtil du ser adgangsmønsteret: en pagineringsbuild beder om den samme Normal-font for hver kolonne på hver side, så én slot har en næsten perfekt hit-rate og koster tre sammenligninger pr. kald
Hvad sker der uden fonttabel, uden GUI eller med en manglende font?
HotXLS degraderer til Calibri 11-konstanten i hvert tilfælde, hvor den rigtige Normal-font ikke kan bestemmes, og det gør den stille af design. Klassiske BIFF-regneark er det almindelige tilfælde: de ældre formater bærer ingen XLSX-fontpool for fonts[0] at referere til, så typevogteren exit tidligt, og standard-MDW på 7 står. Det er ikke en fix, det er den tidligere adfærd bevaret bevidst, så at tilføje måling til XLSX-stien ikke kunne lave en regression på output i klassisk format
GDI-afhængigheden er det ærlige forbehold. Måling kører mod en Windows device context, så stien antager en Windows-vært med fonten installeret. I en service eller en headless build-agent løser GDI-tekstmetrikker sig generelt stadig, men en font, der ikke er installeret på den maskine, erstattes af font-mapperen, og du måler erstatningen i stedet. Det fejler aldrig højt; det returnerer et plausibelt tal for den forkerte skrifttype. Hvis server-side-eksporter skal matche en desktopreference, skal du installere de fonte, dine skabeloner navngiver, på eksporthandten eller fastlåse Normal-fonten, inden du kalder regnearkets PDF-eksportsti
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Fastlås Normal-fonten, så den MDW, der måles på denne vært, er den,
// layoutet blev designet mod, ikke en font-mapper-erstatning.
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;
Målingscacher, og den der crashede på Win64
Når tekstmåling er en GDI-round-trip frem for en multiplikation, skal den cacheres, og caching inde i et render-pass er der, hvor dette arbejde trak blod. Shrink-to-fit-løkken trapper skriftstørrelsen ned i trin på 0.5 pt og måler igen efter hvert trin, så én celle kan kalde PaginationMeasureTextWidth et dusin gange med den samme streng, og word wrap kalder den igen pr. kandidatlinje. En memo nøglet efter fontnavn, størrelse og tekst skrumper det til ét GDI-kald pr. distinkt streng, gemt i en TStringList som navn/værdi-par
Den anden cache, der blev tilføjet ved siden af, var ikke så pæn. Render-pass 5 løser fontpoolen pr. celle via FontIndex, og dens memo brugte parallelle dynamiske arrays med en håndholdt FontMemoCount. Den første version glemte at kalde ResetFontMemo i starten af hver side, så antallet blev ved med at klatre på tværs af sider, mens arraysene ikke gjorde, og koden skrev forbi enden af dem alle. På Win32 kradser det stille i nabohoben og blev færdig; på Win64 udløste det en access violation ved en skrivning til 0x538 med det samme. Den generaliserbare lære: en array-baseret cache holdt i en variabel på enhedsniveau skal nulstilles ved indgangen til hvert pass, der bruger den, for en strengliste eller ordbog tilgiver en manglende nulstilling ved at vokse, og parallelle arrays gør ikke
Kontrollér din egen konvertering
Du behøver ikke komponenten for at verificere noget af dette. Tag en arbejdsbog, hvis Normal-font ikke er Calibri 11, læs en bredde fra <col width="..."/> og kør den gennem specifikationsformlen to gange, én gang med MDW 7 og én gang med den MDW, din renderer faktisk måler for den font; hvis svarene er forskellige, og dit output matcher det første, har du fundet driften. Kolonnegeometri er en af de dele af en regnearksmotor, der enten er usynlig eller det eneste, nogen bemærker, og at få den rigtig betyder at behandle Normal-fonten som et input til layout frem for en styledetalje. Hvis du bygger Delphi- eller C++Builder-applikationer, der læser, skriver, renderer og udskriver Excel-arbejdsbøger uden Office installeret, håndterer HotXLS Delphi Excel component MDW-målingen, pagineringsmodellen og PDF-pipelinen bag ét sæt VCL-klasser