Technisch artikel

Excel-kolombreedte en Max Digit Width (MDW) in Delphi

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

De HotXLS-kolombreedteconversieketen in Delphi, die de gemeten Max Digit Width van het Normal-lettertype van de werkmap in de specificatieformule voert zodat een opgeslagen breedte van 8.43 eenheden eerst 64 pixels en daarna 48 punten wordt
De opgeslagen breedte is een cijfertelling, dus de gemeten MDW van het Normal-lettertype is een invoer voor de formule in plaats van een stijldetail, en de rondreis 8.43 naar 64 naar 48 controleert de rekenkunde

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

Twee HotXLS-kolomlinialen vergeleken, één gemeten met een MDW van 7 pixels en één met 8, tonend hoe de sprong per kolom van 64 naar 72 pixels zich opstapelt over tien kolommen terwijl samengevoegde gebieden en shrink-to-fit de fout erven
Omdat MDW een factor is in plaats van een offset, verplaatst één verkeerde meting elke kolomgrens, en samengevoegde gebieden en shrink-to-fit erven de drift zonder dat er ook maar iets gooit

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

Hoe HotXLS het Normal-lettertype van de werkmap vaststelt, diens Max Digit Width via GDI meet met twee fallbacks en het resultaat cached, naast de twee memo-caches per renderpass en de resetregel die een cache op parallelle arrays nodig heeft
MDW wordt uit de werkmap afgeleid en per lettertype één keer met GDI gemeten, daarna gecached op sleutel, terwijl de renderpass-memo's tonen waarom een cache op parallelle arrays bij de entree van elke pass gereset moet worden

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