Le PDF exporté place chaque limite de colonne un demi-caractère à gauche de l’endroit où Excel la dessine, et chaque cellule renvoyée à la ligne casse désormais à un autre endroit. La largeur de colonne Excel ne se mesure ni en caractères ni en points. Elle se mesure en unités de largeur max de chiffre (MDW) de la police Normal du classeur, et HotXLS mesure cette police avec GDI avant chaque construction de pagination. Le mode de défaillance est silencieux : rien ne lève, les largeurs enregistrées font l’aller-retour octet pour octet, et la géométrie reste décalée de quelques pour cent par colonne jusqu’à ce que la dérive accumulée pousse un tableau d’une page sur deux
Dans quelle unité se mesure la largeur de colonne Excel ?
Une largeur de colonne dans une feuille de calcul est un décompte de chiffres de la police Normal du classeur, pas une mesure absolue. ECMA-376 §18.3.1.13 définit l’attribut width de <col> en fonction de la largeur max de chiffre (Maximum Digit Width) de cette police à 96 dpi, et donne la conversion d’une largeur enregistrée vers les pixels comme une expression tronquante sur le MDW. Pour Calibri 11, la police qu’Excel livre comme style Normal, le MDW mesure 7 pixels. Passez la largeur par défaut de 8.43 unités dans la formule de la spécification avec un MDW de 7 et vous obtenez exactement 64 pixels, soit 48 points à 96 dpi. Ce sont les nombres qu’Excel lui-même rapporte, si bien qu’ils font un contrôle utile : si votre conversion reproduit 8.43 unités en 64 pixels, l’arithmétique est juste et seule l’entrée MDW peut encore être fausse
const
// Largeur max de chiffre (MDW) de la police de corps par défaut en pixels à 96 dpi.
// Calibri 11 mesure 7 px, ce qui reproduit les largeurs en pixels exactes
// qu'Excel enregistre (8.43 unités -> 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; // pixels 96 dpi -> points
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS garde cette arithmétique dans exactement une fonction, dans l’unité lxPagination, si bien qu’il y a un seul endroit où la règle peut se tromper. Le + 5 est le remplissage qu’Excel ajoute pour les filets et les marges de cellule, le * 0.75 convertit les pixels 96 dpi en points PostScript, et le plancher à MinimumColumnWidth existe pour qu’une colonne pathologiquement étroite laisse encore une bande où le moteur de rendu peut dessiner une bordure. Le point d’entrée public ColumnWidthToPoints garde sa vieille signature à un argument et transmet un MDW mesuré à cette fonction, ce qui a permis au changement de comportement d’atterrir sans toucher un seul site d’appel
Pourquoi une police Normal autre que Calibri déplace chaque limite
La dérive est multiplicative, et c’est pourquoi elle se lit comme un bogue de rendu plutôt que comme un bogue d’unités. Le MDW est un facteur sur la largeur, pas un décalage. Poussez le MDW de 7 à 8 et la colonne par défaut de 8.43 unités passe de 64 pixels à 72, un saut de 8 pixels ou 6 points sur une colonne. Dix colonnes de cela et le bord droit du tableau a bougé de presque un pouce. Les classeurs qui déclenchent cela sont tout à fait ordinaires : tout ce qu’un outil de reporting génère en tamponnant Arial ou Segoe UI dans le style Normal, tout ce qui sort d’un modèle d’export ERP, tout ce qu’un client a restylé une fois puis oublié
Deux systèmes de mise en page apparentés héritent de l’erreur au lieu de la causer. Les régions fusionnées font la somme des largeurs en points de leurs colonnes membres, si bien qu’une fusion qui tenait sur une page dans Excel peut déborder après une dérive de MDW, ce qui vaut la peine de se rappeler quand vous construisez des modèles de rapport à cellules fusionnées. Le mode Réduire pour ajuster compare la largeur de texte mesurée à cette même largeur de colonne, si bien qu’un MDW faux change aussi quelles cellules rétrécissent et de combien. La même famille de confusion d’unités apparaît dans les ancres de dessin, où la géométrie d’image et la mise à l’échelle EMU a sa propre chaîne de conversion où se tromper
Comment HotXLS mesure le MDW à l’exécution
HotXLS résout le MDW depuis le classeur lui-même au lieu de supposer une constante, et deux procédures font le travail. PaginationApplyNormalFont lit la police du style Normal hors du classeur et tourne au sommet de la construction de pagination, avant que toute géométrie de colonne ne soit calculée ; elle se réinitialise d’abord sur Calibri 11, si bien qu’un classeur sans table de polices ne peut pas hériter d’un état périmé d’une construction précédente. La police du style Normal est fonts[0] dans styles.xml, exposée par le composant comme Workbook.Fonts[0]
// Lit fonts[0] (la police du style Normal) hors du classeur de la feuille de calcul.
// Les feuilles classiques sans table de polices gardent la valeur par défaut Calibri 11.
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;
La seconde procédure, PaginationMeasureMdW, demande à GDI l’étendue du caractère unique '0' par le biais de GetTextExtentPoint32W sur un canevas bitmap hors écran partagé, retombe sur tmAveCharWidth de GetTextMetricsW quand l’appel d’étendue échoue, et retombe sur DefaultMDW quand ni l’un ni l’autre n’est disponible. Son cache est un emplacement unique indexé par (name, size), ce qui sonne grossier jusqu’à ce que vous regardiez le motif d’accès : une construction de pagination demande la même police Normal sur chaque colonne de chaque page, si bien qu’un seul emplacement a un taux de réussite quasi parfait et coûte trois comparaisons par appel
Que se passe-t-il sans table de polices, sans GUI, ou avec une police manquante ?
HotXLS se dégrade vers la constante Calibri 11 dans chaque cas où la vraie police Normal ne peut pas être déterminée, et il le fait silencieusement par conception. Les feuilles BIFF classiques sont le cas courant : les formats patrimoniaux ne portent aucun pool de polices XLSX auquel fonts[0] pourrait se référer, si bien que le garde de type sort tôt et le MDW par défaut de 7 tient. Ce n’est pas une correction, c’est le comportement précédent préservé délibérément, pour que l’ajout de la mesure au chemin XLSX ne puisse pas faire régresser la sortie au format classique
La dépendance GDI est l’avertissement honnête. La mesure tourne contre un contexte de périphérique Windows, si bien que le chemin suppose un hôte Windows avec la police installée. Dans un service ou un agent de construction sans tête, les métriques de texte GDI se résolvent généralement encore, mais une police qui n’est pas installée sur cette machine est remplacée par le mappeur de polices et vous mesurez le remplaçant à la place. Il n’échoue jamais bruyamment ; il renvoie un nombre plausible pour la mauvaise police. Si les exports côté serveur doivent correspondre à une référence de bureau, installez sur l’hôte d’export les polices que vos modèles nomment, ou figez la police Normal avant d’invoquer le chemin d’export PDF de feuille de calcul
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Figez la police Normal pour que le MDW mesuré sur cet hôte soit celui
// contre lequel la mise en page a été conçue, pas un remplaçant du mappeur de polices.
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;
Caches de mesure, et celui qui a planté sur Win64
Une fois que la mesure de texte est un aller-retour GDI plutôt qu’une multiplication, elle doit être mise en cache, et c’est dans la mise en cache à l’intérieur d’une passe de rendu que ce travail a saigné. La boucle de Réduire pour ajuster descend la taille de police par pas de 0.5 pt et re-mesure après chaque pas, si bien qu’une cellule peut appeler PaginationMeasureTextWidth une douzaine de fois avec la même chaîne, et le retour à la ligne des mots l’appelle encore par ligne candidate. Un mémo indexé par nom de police, taille et texte réduit cela à un appel GDI par chaîne distincte, stocké dans une TStringList comme paires nom/valeur
L’autre cache ajouté à côté n’était pas aussi propre. La passe de rendu 5 résout le pool de polices par cellule via FontIndex, et son mémo utilisait des tableaux dynamiques parallèles avec un FontMemoCount maintenu à la main. La première version oubliait d’appeler ResetFontMemo au début de chaque page, si bien que le compteur continuait de grimper à travers les pages tandis que les tableaux non, et le code écrivait au-delà de la fin de tous. Sur Win32 cela gribouillait tranquillement dans le tas adjacent et terminait ; sur Win64 cela levait une violation d’accès sur une écriture à 0x538 immédiatement. La leçon généralisable : un cache adossé à des tableaux détenu dans une variable de niveau unité doit être réinitialisé à l’entrée de chaque passe qui l’utilise, parce qu’une liste de chaînes ou un dictionnaire pardonne une réinitialisation manquante en grandissant et des tableaux parallèles non
Vérifier votre propre conversion
Vous n’avez pas besoin du composant pour vérifier tout cela. Prenez un classeur dont la police Normal n’est pas Calibri 11, lisez une largeur depuis <col width="..."/>, et passez-la deux fois dans la formule de la spécification, une fois avec un MDW de 7 et une fois avec le MDW que votre moteur de rendu mesure réellement pour cette police ; si les réponses diffèrent et que votre sortie correspond à la première, vous avez trouvé la dérive. La géométrie de colonne est une de ces parties d’un moteur de tableur qui est soit invisible soit la seule chose que tout le monde remarque, et la faire correctement signifie traiter la police Normal comme une entrée de la mise en page plutôt que comme un détail de style. Si vous construisez des applications Delphi ou C++Builder qui lisent, écrivent, rendent et impriment des classeurs Excel sans Office installé, le composant tableur HotXLS pour Delphi gère la mesure du MDW, le modèle de pagination et le pipeline PDF derrière un seul ensemble de classes VCL