Das exportierte PDF legt jede Spaltengrenze ein halbes Zeichen links von dort, wo Excel sie zeichnet, und jede umgebrochene Zelle bricht jetzt an anderer Stelle. Excel-Spaltenbreite wird nicht in Zeichen oder Punkten gemessen. Sie wird in Max-Digit-Width-Einheiten (MDW) des Normal-Fonts der Arbeitsmappe gemessen, und HotXLS misst diesen Font mit GDI vor jedem Paginierungsbau. Das Fehlerbild ist still: Nichts wirft, die gespeicherten Breiten round-trippen byteidentisch, und die Geometrie weicht trotzdem um einige Prozent pro Spalte ab, bis der akkumulierte Drift eine einseitige Tabelle auf zwei Seiten drückt
In welcher Einheit ist die Excel-Spaltenbreite?
Eine Spaltenbreite in einem Worksheet ist eine Ziffernzeichen-Zählung des Normal-Fonts der Arbeitsmappe, keine absolute Messung. ECMA-376 §18.3.1.13 definiert das width-Attribut von <col> über die Maximum Digit Width dieses Fonts bei 96 dpi und gibt die Konvertierung von einer gespeicherten Breite zurück zu Pixeln als truncierenden Ausdruck über MDW. Für Calibri 11, was Excel als Normal-Stil ausliefert, misst MDW 7 Pixel. Speist man die Default-Breite von 8.43 Einheiten durch die Spezifikationsformel mit MDW 7, erhält man exakt 64 Pixel, was bei 96 dpi 48 Punkten entspricht. Das sind die Zahlen, die Excel selbst meldet, also eine nützliche Probe: Reproduziert Ihre Konvertierung 8.43 Einheiten zu 64 Pixeln, ist die Arithmetik richtig, und nur die MDW-Eingabe kann noch falsch sein
const
// Maximum Digit Width (MDW) des Standard-Body-Fonts in Pixeln bei 96 dpi.
// Calibri 11 misst 7 px, was die exakten Pixelbreiten reproduziert,
// die Excel speichert (8.43 Einheiten -> 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-Pixel -> Punkte
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS hält diese Arithmetik in genau einer Funktion, in der Unit lxPagination, sodass es einen einzigen Ort gibt, an dem das Lineal falsch sein kann. Das + 5 ist das Padding, das Excel für Gitterlinien und Zellenränder zulegt, das * 0.75 konvertiert 96-dpi-Pixel zu PostScript-Punkten, und die Untergrenze bei MinimumColumnWidth existiert, damit eine pathologisch schmale Spalte noch einen Streifen übrig lässt, in den der Renderer einen Rand zeichnen kann. Der öffentliche Einsprungpunkt ColumnWidthToPoints behält seine alte Ein-Argument-Signatur und reicht einen gemessenen MDW an diese Funktion weiter, was es ermöglichte, dass die Verhaltensänderung landete, ohne eine einzige Aufrufstelle zu berühren
Warum ein Normal-Font ungleich Calibri jede Grenze verschiebt
Der Drift ist multiplikativ, weshalb er wie ein Rendering-Bug aussieht statt wie ein Einheiten-Bug. MDW ist ein Faktor auf der Breite, kein Offset. Schiebt man MDW von 7 auf 8, geht die Default-Spalte von 8.43 Einheiten von 64 Pixeln auf 72, ein Sprung von 8 Pixeln oder 6 Punkten an einer Spalte. Zehn Spalten davon, und die rechte Tabellenkante hat sich um gut einen Zoll bewegt. Arbeitsmappen, die darauf hereinstolpern, sind völlig gewöhnlich: alles, was ein Reporting-Tool erzeugt, das Arial oder Segoe UI in den Normal-Stil stempelt, alles, was aus einer ERP-Exportvorlage gespeichert wurde, alles, was ein Kunde einmal umgestylt und vergessen hat
Zwei verwandte Layoutsysteme erben den Fehler, statt ihn zu verursachen. Verbundene Bereiche summieren die Punkt-Breiten ihrer Member-Spalten, also kann eine Verbindung, die in Excel auf eine Seite passte, nach MDW-Drift überlaufen — das lohnt sich zu merken, wenn Sie Berichtsvorlagen mit verbundenen Zellen bauen. Shrink-to-fit vergleicht die gemessene Textbreite gegen dieselbe Spaltenbreite, also verändert der falsche MDW auch, welche Zellen schrumpfen und um wie viel. Dieselbe Familie von Einheitenverwirrung taucht in Drawing-Ankern auf, wo Bildgeometrie und EMU-Skalierung ihre eigene Konvertierungskette hat, die man falsch nehmen kann
Wie HotXLS MDW zur Laufzeit misst
HotXLS löst MDW aus der Arbeitsmappe selbst auf, statt eine Konstante anzunehmen, und zwei Prozeduren machen die Arbeit. PaginationApplyNormalFont liest den Normal-Stil-Font aus der Arbeitsmappe und läuft am Anfang des Paginierungsbaus, bevor irgendeine Spaltengeometrie berechnet wird; es setzt zuerst auf Calibri 11 zurück, sodass eine Arbeitsmappe ohne Font-Tabelle keinen veralteten Zustand von einem vorherigen Bau erben kann. Der Normal-Stil-Font ist fonts[0] in styles.xml, von der Komponente als Workbook.Fonts[0] exponiert
// Liest fonts[0] (den Normal-Stil-Font) aus der Workbook des Worksheets.
// Classic-Worksheets ohne Font-Tabelle behalten den 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;
Die zweite Prozedur, PaginationMeasureMdW, fragt GDI nach der Ausdehnung des einzelnen Zeichens '0' über GetTextExtentPoint32W auf einer geteilten Off-Screen-Bitmap-Canvas, fällt auf tmAveCharWidth aus GetTextMetricsW zurück, wenn der Extent-Aufruf scheitert, und fällt auf DefaultMDW zurück, wenn beides nicht verfügbar ist. Ihr Cache ist ein einziger Slot mit dem Schlüssel (name, size), was primitiv klingt, bis man das Zugriffsmuster ansieht: Ein Paginierungsbau fragt nach demselben Normal-Font an jeder Spalte jeder Seite, also hat ein Slot eine nahezu perfekte Trefferquote und kostet drei Vergleiche pro Aufruf
Was geschieht ohne Font-Tabelle, ohne GUI oder mit fehlendem Font?
HotXLS degradiert in jedem Fall, in dem der echte Normal-Font nicht bestimmt werden kann, auf die Calibri-11-Konstante, und zwar by design stillschweigend. Classic-BIFF-Worksheets sind der häufigste: Die Legacy-Formate tragen keinen XLSX-Font-Pool, auf den sich fonts[0] beziehen könnte, also tritt der Type-Guard früh aus und der Default-MDW von 7 steht. Das ist kein Fix, es ist das vorherige Verhalten, das bewusst bewahrt wurde, damit das Hinzufügen von Messung zum XLSX-Pfad die Classic-Format-Ausgabe nicht regressieren konnte
Die GDI-Abhängigkeit ist die ehrliche Einschränkung. Die Messung läuft gegen einen Windows-Device-Context, also setzt der Pfad einen Windows-Host mit installiertem Font voraus. In einem Dienst oder einem headless Build-Agent lösen GDI-Textmetriken im Allgemeinen weiterhin auf, aber ein Font, der auf dieser Maschine nicht installiert ist, wird vom Font-Mapper ersetzt, und Sie messen den Ersatz. Es scheitert nie laut; es liefert eine plausible Zahl für den falschen Schriftschnitt. Wenn serverseitige Exporte einem Desktop-Referenzmatch entsprechen müssen, installieren Sie die Fonts, die Ihre Vorlagen nennen, auf dem Export-Host, oder pinnen Sie den Normal-Font, bevor Sie den Worksheet-PDF-Export-Pfad aufrufen
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Pinnen Sie den Normal-Font, damit der auf diesem Host gemessene MDW
// der ist, gegen den das Layout entworfen wurde, kein Font-Mapper-Ersatz.
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;
Messcaches — und der eine, der auf Win64 abstürzte
Sobald Textmessung ein GDI-Round-trip statt einer Multiplikation ist, muss sie gecacht werden, und das Cachen innerhalb eines Render-Durchgangs ist die Stelle, an der diese Arbeit Blut kostete. Die Shrink-to-fit-Schleife stuft die Fontgröße in 0.5-pt-Schritten herab und misst nach jedem Schritt neu, also kann eine Zelle PaginationMeasureTextWidth ein Dutzend Mal mit demselben String aufrufen, und Word-Wrap ruft sie erneut pro Kandidatenzeile. Ein Memo mit dem Schlüssel Fontname, Größe und Text kollabiert das auf einen GDI-Aufruf pro distinktem String, gespeichert in einer TStringList als Name/Wert-Paare
Der andere Cache, der daneben hinzukam, war nicht so ordentlich. Render-Durchgang 5 löst den Font-Pool pro Zelle über FontIndex auf, und sein Memo benutzte parallele dynamische Arrays mit einem handgeführten FontMemoCount. Die erste Version vergaß, ResetFontMemo am Anfang jeder Seite aufzurufen, also kletterte der Zähler über Seiten weiter, während die Arrays nicht mitkletterten, und der Code schrieb über das Ende von allen ihnen hinaus. Auf Win32 kritzelte das stillschweigend in benachbarten Heap und war fertig; auf Win64 warf es sofort eine Access Violation bei einem Schreibzugriff auf 0x538. Die verallgemeinerbare Lektion: Ein Array-gestützter Cache in einer Unit-Level-Variable muss am Eintritt jedes Durchgangs, der ihn benutzt, zurückgesetzt werden, denn eine Stringliste oder ein Dictionary verzeiht ein fehlendes Reset durch Wachsen, und parallele Arrays tun es nicht
Ihre eigene Konvertierung prüfen
Sie brauchen die Komponente nicht, um irgendetwas davon zu verifizieren. Nehmen Sie eine Arbeitsmappe, deren Normal-Font nicht Calibri 11 ist, lesen Sie eine Breite aus <col width="..."/> und führen Sie sie zweimal durch die Spezifikationsformel, einmal mit MDW 7 und einmal mit dem MDW, das Ihr Renderer für diesen Font tatsächlich misst; wenn die Antworten differieren und Ihre Ausgabe der ersten entspricht, haben Sie den Drift gefunden. Spaltengeometrie ist einer jener Teile einer Spreadsheet-Engine, der entweder unsichtbar ist oder das Einzige, das irgendjemand bemerkt, und sie richtig zu treffen heißt, den Normal-Font als Eingabe ins Layout zu behandeln statt als Styling-Detail. Wenn Sie Delphi- oder C++Builder-Anwendungen bauen, die Excel-Arbeitsmappen lesen, schreiben, rendern und drucken, ohne dass Office installiert ist, übernimmt die HotXLS Delphi Excel component die MDW-Messung, das Paginierungsmodell und die PDF-Pipeline hinter einem Satz VCL-Klassen