De geëxporteerde PDF zet elke kolomgrens een halve tekenbreedte links van waar Excel haar tekent, en elke omlopende cel breekt nu op een andere plek. Excel-kolombreedte wordt niet gemeten in tekens of punten. Ze wordt gemeten in Max Digit Width (MDW)-eenheden van het Normal-lettertype van de werkmap, en HotXLS meet dat lettertype met GDI vóór elke paginabuild. De faalmodus is geruisloos: niets gooit een exception, de opgeslagen breedtes komen byte-voor-byte terug, en de geometrie zit nog steeds een paar procent per kolom ernaast totdat de opgetelde drift een tabel van één pagina op twee duwt
In welke eenheid staat de Excel-kolombreedte?
Een kolombreedte in een werkblad is een telling van cijfertekens van het Normal-lettertype van de werkmap, geen absolute meting. ECMA-376 §18.3.1.13 definieert het attribuut width van <col> in termen van de Maximum Digit Width van dat lettertype bij 96 dpi, en geeft de conversie van een opgeslagen breedte terug naar pixels als een afkapexpressie over MDW. Voor Calibri 11, het lettertype waarmee Excel de Normal-stijl levert, meet MDW 7 pixels. Stuur de standaardbreedte van 8,43 eenheden door de specificatieformule met MDW 7 en u krijgt exact 64 pixels, wat 48 punten is bij 96 dpi. Dat zijn de getallen die Excel zelf meldt, dus ze vormen een nuttige controle: reproduceert uw conversie 8,43 eenheden naar 64 pixels, dan klopt de rekenkunde en kan alleen de MDW-invoer nog fout zijn
const
// Maximum digit width (MDW) van het standaard body-lettertype in pixels bij 96 dpi.
// Calibri 11 meet 7 px, wat de exacte pixelbreedten reproduceert
// die Excel opslaat (8.43 eenheden -> 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 -> punten
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS houdt die rekenkunde in precies één functie, in de unit lxPagination, dus er is één plek waar de lat verkeerd kan zitten. De + 5 is de padding die Excel toevoegt voor rasterlijnen en celmarges, de * 0.75 zet 96-dpi-pixels om naar PostScript-punten, en het vloerniveau bij MinimumColumnWidth bestaat zodat een pathologisch smalle kolom nog een strook overlaat waarin de renderer een rand kan tekenen. Het publieke instappunt ColumnWidthToPoints behoudt zijn oude signatuur met één argument en stuurt een gemeten MDW door naar deze functie, precies wat de gedragswijziging liet landen zonder ook maar één call site aan te raken
Waarom een Normal-lettertype anders dan Calibri elke grens verschuift
De drift is multiplicatief, en daarom leest ze als een rendering-bug in plaats van een eenhedenbug. MDW is een factor op de breedte, geen offset. Duw MDW van 7 naar 8 en de standaardkolom van 8,43 eenheden gaat van 64 naar 72 pixels, een sprong van 8 pixels of 6 punten op één kolom. Tien zulke kolommen en de rechterrand van de tabel is een dikke twee centimeter opgeschoven. Werkmappen die dit triggeren zijn volstrekt gewoon: alles wat een rapportagetool genereert dat Arial of Segoe UI in de Normal-stijl stempelt, alles wat uit een ERP-exportsjabloon wordt opgeslagen, alles wat een klant één keer heeft omgestyled en daarna vergeten is
Twee verwante layoutsysteem erven de fout in plaats van haar te veroorzaken. Samengevoegde gebieden sommen de puntbreedtes van hun lidkolommen, dus een merge die in Excel op één pagina paste kan na MDW-drift overlopen, wat de moeite van onthouden waard is wanneer u rapportsjablonen met samengevoegde cellen bouwt. Shrink-to-fit vergelijkt de gemeten tekstbreedte met dezelfde kolombreedte, dus de verkeerde MDW verandert ook welke cellen krimpen en hoeveel. Dezelfde familie van eenhedenverwarring duikt op in tekeningankers, waar beeldgeometrie en EMU-schaling een eigen conversieketen heeft om verkeerd te gaan
Hoe HotXLS MDW meet tijdens runtime
HotXLS leidt MDW af uit de werkmap zelf in plaats van een constante aan te nemen, en twee procedures doen dat werk. PaginationApplyNormalFont leest het Normal-stijllettertype uit de werkmap en draait bovenaan de paginabuild, nog voordat enige kolomgeometrie is berekend; die reset eerst naar Calibri 11, zodat een werkmap zonder fonttabel geen verouderde status van een vorige build kan erven. Het Normal-stijllettertype is fonts[0] in styles.xml, door de component blootgesteld als Workbook.Fonts[0]
// Leest fonts[0] (het Normal-stijllettertype) uit de werkmap van het werkblad.
// Klassieke werkbladen zonder fonttabel behouden de Calibri 11-standaard.
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;
De tweede procedure, PaginationMeasureMdW, vraagt GDI om de omvang van het ene teken '0' via GetTextExtentPoint32W op een gedeelde off-screen bitmapcanvas, valt terug op tmAveCharWidth uit GetTextMetricsW wanneer de omvang-aanroep faalt, en valt terug op DefaultMDW wanneer geen van beide beschikbaar is. Haar cache is één sleuf met als sleutel (naam, grootte), wat primitief klinkt totdat u naar het toegangspatroon kijkt: een paginabuild vraagt om hetzelfde Normal-lettertype bij elke kolom van elke pagina, dus één sleuf heeft een vrijwel perfecte hitratio en kost drie vergelijkingen per aanroep
Wat gebeurt er zonder fonttabel, zonder GUI of met een ontbrekend lettertype?
HotXLS degradeert naar de constante Calibri 11 in elk geval waarin het echte Normal-lettertype niet kan worden vastgesteld, en doet dat van ontwerp uit geruisloos. Klassieke BIFF-werkbladen zijn het gewone geval: de legacy-formaten dragen geen XLSX-fontpool waar fonts[0] naar kan verwijzen, dus de typeguard eindigt vroegtijdig en de standaard-MDW van 7 blijft staan. Dat is geen fix, het is het vorige gedrag dat bewust behouden bleef, zodat het toevoegen van meting aan het XLSX-pad de klassieke uitvoer niet kon aantasten
De GDI-afhankelijkheid is de eerlijke kanttekening. Meting draait tegen een Windows-device context, dus het pad veronderstelt een Windows-host met het lettertype geïnstalleerd. In een service of een headless build-agent lossen GDI-tekstmetrieken meestal nog op, maar een lettertype dat niet op die machine is geïnstalleerd wordt gesubstitueerd door de font mapper en u meet de vervanger. Het faalt nooit luid; het geeft een geloofwaardig getal terug voor het verkeerde lettertype. Als server-side exports moeten matchen met een desktopreferentie, installeer dan de lettertypen die uw sjablonen noemen op de exorthost, of pin het Normal-lettertype voordat u het werkblad-PDF-exportpad aanroept
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Pin het Normal-lettertype zodat de MDW die op deze host wordt
// gemeten degene is waar de layout op ontworpen is, niet een
// font-mapper-vervanger.
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;
Metingcaches, en de cache die crashte op Win64
Zodra tekstmeting een GDI-rondreis is in plaats van een vermenigvuldiging, moet ze worden gecached, en cachen binnen een renderpass is waar dit werk zijn tol eiste. De shrink-to-fit-lus verlaagt de lettertypegrootte in stappen van 0,5 pt en meet opnieuw na elke stap, dus één cel kan PaginationMeasureTextWidth een dozijn keer met dezelfde string aanroepen, en word wrap roept haar opnieuw aan per kandidaatregel. Een memo met als sleutel lettertypenaam, grootte en tekst vouwt dat samen tot één GDI-aanroep per unieke string, opgeslagen in een TStringList als naam/waarde-paren
De andere cache die erlangs bij kwam was niet zo netjes. Renderpass 5 lost de fontpool per cel op via FontIndex, en haar memo gebruikte parallelle dynamische arrays met een handmatig bijgehouden FontMemoCount. De eerste versie vergat ResetFontMemo aan het begin van elke pagina aan te roepen, dus de teller bleef over pagina's heen klimmen terwijl de arrays dat niet deden, en de code schreef voorbij het einde van ze allemaal. Op Win32 krabbelde dat geruisloos in aangrenzende heap en klaarde het; op Win64 gaf het onmiddellijk een access violation bij een schrijfpoging naar 0x538. De generaliseerbare les: een array-backed cache in een variabele op unitniveau moet bij de entree van elke pass die haar gebruikt worden gereset, want een stringlist of dictionary vergeeft een gemiste reset door te groeien, en parallelle arrays niet
Controleer uw eigen conversie
U hebt de component niet nodig om dit te verifiëren. Neem een werkmap waarvan het Normal-lettertype niet Calibri 11 is, lees een breedte uit <col width="..."/>, en stuur haar twee keer door de specificatieformule: één keer met MDW 7 en één keer met de MDW die uw renderer voor dat lettertype feitelijk meet; als de antwoorden verschillen en uw uitvoer bij de eerste past, hebt u de drift gevonden. Kolomgeometrie is zo'n onderdeel van een spreadsheet-engine dat of onzichtbaar is, of het enige dat iemand opmerkt, en het goed doen betekent het Normal-lettertype behandelen als invoer voor de layout in plaats van als stijldetail. Als u Delphi- of C++Builder-applicaties bouwt die Excel-werkmappen lezen, schrijven, renderen en afdrukken zonder Office geïnstalleerd, verzorgt de HotXLS Delphi Excel component de MDW-meting, het paginamodel en de PDF-pipeline achter één set VCL-klassen