Den exporterade PDF:en placerar varje kolumngräns ett halvt tecken till vänster om där Excel ritar den, och varje radbruten cell bryter nu på en annan plats. Excel-kolumnbredd mäts inte i tecken eller punkter. Den mäts i Max Digit Width-enheter (MDW) för arbetsbokens Normal-teckensnitt, och HotXLS mäter det teckensnittet med GDI före varje pagineringsbygge. Felläget är tyst: inget kastar, de lagrade bredderna round-trippar byte för byte, och geometrin är fortfarande fel med några procent per kolumn tills den ackumulerade driften trycker en en-sidastabell över på två
Vilken enhet mäts Excel-kolumnbredd i?
En kolumnbredd i ett kalkylark är ett antal siffertecken i arbetsbokens Normal-teckensnitt, inte ett absolut mått. ECMA-376 §18.3.1.13 definierar attributet width på <col> i termer av Maximum Digit Width för det teckensnittet vid 96 dpi, och ger konverteringen från en lagrad bredd tillbaka till pixlar som ett trunkerande uttryck över MDW. För Calibri 11, vilket är vad Excel skeppar som Normal-stilen, mäter MDW 7 pixlar. Mata standardbredden 8,43 enheter genom specifikationsformeln med MDW 7 och du får exakt 64 pixlar, vilket är 48 punkter vid 96 dpi. Det är de tal Excel självt rapporterar, så de utgör en användbar kontroll: om din konvertering reproducerar 8,43 enheter till 64 pixlar är aritmetiken rätt och bara MDW-indatan kan fortfarande vara fel
const
// Maximum digit width (MDW) för standardbrödtextteckensnittet i pixlar vid 96 dpi.
// Calibri 11 mäter 7 px, vilket reproducerar de exakta pixelbredder
// Excel lagrar (8,43 enheter -> 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-pixlar -> punkter
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS håller den aritmetiken i exakt en funktion, i enheten lxPagination, så det finns en enda plats där linjalen kan vara fel. + 5 är den padding Excel lägger till för rutnätslinjer och cellmarginaler, * 0.75 konverterar 96 dpi-pixlar till PostScript-punkter, och golvet vid MinimumColumnWidth finns så att en patologiskt smal kolumn fortfarande lämnar en remsa som renderaren kan rita en ram i. Den publika ingångspunkten ColumnWidthToPoints behåller sin gamla signatur med ett argument och vidarebefordrar en uppmätt MDW till denna funktion, vilket är vad som lät beteendeändringen landa utan att röra en enda anropsplats
Varför ett icke-Calibri Normal-teckensnitt flyttar varje gräns
Driften är multiplikativ, vilket är därför den läses som en renderingsbugg snarare än en enhetsbugg. MDW är en faktor på bredden, inte en offset. Tryck MDW från 7 till 8 och standardkolumnen på 8,43 enheter går från 64 pixlar till 72, ett hopp på 8 pixlar eller 6 punkter på en kolumn. Tio kolumner av det och tabellens högra kant har flyttat större delen av en tum. Arbetsböcker som snubblar på detta är helt ordinära: allt genererat av ett rapportverktyg som stämplat Arial eller Segoe UI in i Normal-stilen, allt sparat ur en ERP-exportmall, allt en kund stylade om en gång och glömde bort
Två besläktade layoutsystem ärver felet i stället för att orsaka det. Sammanfogade regioner summerar punktbredderna för sina medlemskolumner, så en sammanfogning som fick plats på en sida i Excel kan svämma över efter MDW-drift, vilket är värt att komma ihåg när du bygger rapportmallar med sammanfogade celler. Shrink-to-fit jämför uppmätt textbredd mot samma kolumnbredd, så fel MDW ändrar också vilka celler som krymper och hur mycket. Samma familj av enhetsförvirring dyker upp i ritankare, där bildgeometri och EMU-skalning har sin egen konverteringskedja att få fel
Hur HotXLS mäter MDW vid körning
HotXLS löser MDW från själva arbetsboken i stället för att anta en konstant, och två procedurer gör arbetet. PaginationApplyNormalFont läser Normal-stilens teckensnitt från arbetsboken och körs högst upp i pagineringsbygget, innan någon kolumngeometri beräknas; den återställer till Calibri 11 först, så en arbetsbok utan teckensnittstabell inte kan ärva inaktuellt tillstånd från ett tidigare bygge. Normal-stilens teckensnitt är fonts[0] i styles.xml, exponerat av komponenten som Workbook.Fonts[0]
// Läser fonts[0] (Normal-stilens teckensnitt) från kalkylarkets arbetsbok.
// Klassiska kalkylark utan teckensnittstabell behåller Calibri 11-default.
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 andra proceduren, PaginationMeasureMdW, frågar GDI om extenten för det enda tecknet '0' via GetTextExtentPoint32W på en delad bitmapskanvas utanför skärmen, faller tillbaka på tmAveCharWidth från GetTextMetricsW när extentanropet misslyckas, och faller tillbaka på DefaultMDW när inget av dem är tillgängligt. Dess cache är en enda slot nycklad på (name, size), vilket låter grovt tills man tittar på åtkomstmönstret: ett pagineringsbygge frågar efter samma Normal-teckensnitt på varje kolumn på varje sida, så en slot har en nästan perfekt träffrekvens och kostar tre jämförelser per anrop
Vad händer utan teckensnittstabell, utan GUI eller med ett saknat teckensnitt?
HotXLS degraderar till Calibri 11-konstanten i varje fall där det verkliga Normal-teckensnittet inte kan fastställas, och den gör det tyst by design. Klassiska BIFF-kalkylark är det vanliga fallet: de äldre formaten bär ingen XLSX-teckensnittspool som fonts[0] kan referera till, så typvakten avslutar tidigt och default-MDW:n på 7 står kvar. Det är inte en fix, det är det tidigare beteendet bevarat avsiktligt, så att att lägga till mätning på XLSX-vägen inte kunde regressera utdata för klassiskt format
GDI-beroendet är den ärliga brasklappen. Mätningen körs mot en Windows-enhetskontext, så vägen förutsätter en Windows-värd med teckensnittet installerat. I en tjänst eller en huvudlös byggagent löser GDI-textmått i allmänhet fortfarande, men ett teckensnitt som inte är installerat på den maskinen ersätts av fontmapparen och du mäter ersättningen i stället. Det misslyckas aldrig högljutt; det returnerar ett trovärdigt tal för fel typsnitt. Om export på serversidan måste matcha en skrivbordsreferens, installera de teckensnitt dina mallar namnger på exportvärden, eller fäst Normal-teckensnittet innan du anropar PDF-exportvägen för kalkylark
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Fäst Normal-teckensnittet så att MDW:n som mäts på den här värden är den
// layouten designades mot, inte en fontmapperersättning.
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ätcachar, och den som kraschade på Win64
När textmätning väl är en GDI-roundtrip snarare än en multiplikation måste den cachas, och cachning inuti ett renderingspass är där detta arbete drog blod. Shrink-to-fit-loopen stegar teckenstorleken ned i steg om 0,5 pt och mäter om efter varje steg, så en cell kan anropa PaginationMeasureTextWidth ett dussin gånger med samma sträng, och radbrytning anropar den igen per kandidatrad. En memo nycklad på teckensnittsnamn, storlek och text kollapsar det till ett GDI-anrop per distinkt sträng, lagrad i en TStringList som namn/värde-par
Den andra cachen som lades till bredvid den var inte lika snygg. Renderingspass 5 löser teckensnittspoolen per cell via FontIndex, och dess memo använde parallella dynamiska arrayer med en handunderhållen FontMemoCount. Den första versionen glömde anropa ResetFontMemo i början av varje sida, så räkningen fortsatte klättra över sidor medan arrayerna inte gjorde det, och koden skrev förbi slutet på dem alla. På Win32 kluddade det tyst in i intilliggande heap och avslutade; på Win64 kastade det ett access violation på en skrivning till 0x538 omedelbart. Den generaliserbara läxan: en arraybackad cache som hålls i en variabel på enhetsnivå måste återställas vid ingången av varje pass som använder den, eftersom en stränglista eller ordlista förlåter en missad återställning genom att växa, och parallella arrayer gör det inte
Kontrollera din egen konvertering
Du behöver inte komponenten för att verifiera något av detta. Ta en arbetsbok vars Normal-teckensnitt inte är Calibri 11, läs en bredd från <col width="..."/>, och kör den genom specifikationsformeln två gånger, en gång med MDW 7 och en gång med den MDW din renderare faktiskt mäter för det teckensnittet; om svaren skiljer sig åt och din utdata matchar det första har du hittat driften. Kolumngeometri är en av de delar av en kalkylarksmotor som antingen är osynlig eller det enda någon märker, och att få den rätt betyder att behandla Normal-teckensnittet som en indata till layout snarare än en stildetalj. Om du bygger Delphi- eller C++Builder-applikationer som läser, skriver, renderar och skriver ut Excel-arbetsböcker utan Office installerat hanterar HotXLS Delphi Excel-komponent MDW-mätningen, pagineringsmodellen och PDF-pipelinen bakom en uppsättning VCL-klasser