Il PDF esportato mette ogni confine di colonna mezzo carattere a sinistra di dove Excel lo disegna, e ogni cella con a-capo ora va a capo in un punto diverso. La larghezza colonna Excel non si misura in caratteri o punti. Si misura in unità di Max Digit Width (MDW) del font Normal della cartella di lavoro, e HotXLS misura quel font con GDI prima di ogni build di paginazione. La modalità di guasto è silenziosa: nulla lancia eccezioni, le larghezze memorizzate fanno il round-trip byte per byte, e la geometria resta sbagliata di qualche percento per colonna finché la deriva accumulata non spinge una tabella da una pagina a due
In quale unità si misura la larghezza colonna Excel?
Una larghezza colonna in un worksheet è un conteggio di caratteri cifra del font Normal della cartella di lavoro, non una misura assoluta. ECMA-376 §18.3.1.13 definisce l'attributo width di <col> in termini di Maximum Digit Width di quel font a 96 dpi, e dà la conversione da una larghezza memorizzata a pixel come un'espressione con troncamento sull'MDW. Per Calibri 11, che è ciò che Excel spedisce come stile Normal, l'MDW misura 7 pixel. Passate la larghezza predefinita di 8.43 unità attraverso la formula della specifica con MDW 7 e otterrete esattamente 64 pixel, che sono 48 punti a 96 dpi. Questi sono i numeri che Excel stesso riporta, quindi costituiscono un controllo utile: se la vostra conversione riproduce 8.43 unità in 64 pixel, l'aritmetica è giusta e solo l'input MDW può ancora essere sbagliato
const
// Larghezza massima cifra (MDW) del font corpo predefinito in pixel a 96 dpi.
// Calibri 11 misura 7 px, che riproduce le larghezze in pixel esatte
// che Excel memorizza (8.43 unità -> 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 -> punti
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS tiene quell'aritmetica in esattamente una funzione, nell'unità lxPagination, così c'è un unico posto in cui il righello può essere sbagliato. Il + 5 è il padding che Excel aggiunge per le linee della griglia e i margini delle celle, il * 0.75 converte i pixel a 96 dpi in punti PostScript, e il minimo a MinimumColumnWidth esiste così che una colonna patologicamente stretta lasci comunque una striscia in cui il renderer può disegnare un bordo. Il punto di ingresso pubblico ColumnWidthToPoints mantiene la sua vecchia firma a un solo argomento e inoltra un MDW misurato a questa funzione, che è ciò che ha permesso al cambiamento di comportamento di atterrare senza toccare una sola call site
Perché un font Normal non Calibri sposta ogni confine
La deriva è moltiplicativa, ed è per questo che si legge come un bug di rendering anziché un bug di unità. L'MDW è un fattore sulla larghezza, non un offset. Portate l'MDW da 7 a 8 e la colonna predefinita da 8.43 unità passa da 64 pixel a 72, un salto di 8 pixel o 6 punti su una sola colonna. Dieci colonne così e il bordo destro della tabella si è mosso di quasi un pollice. Le cartelle di lavoro che inciampano su questo sono del tutto ordinarie: qualsiasi cosa generata da uno strumento di report che imprime Arial o Segoe UI nello stile Normal, qualsiasi cosa salvata da un modello di esportazione ERP, qualsiasi cosa che un cliente ha restilizzato una volta e poi dimenticato
Due sistemi di layout imparentati ereditano l'errore anziché causarlo. Le regioni unite sommano le larghezze in punti delle loro colonne membri, così un merge che in Excel stava su una pagina può traboccare dopo la deriva dell'MDW, cosa che vale la pena ricordare quando costruite dei modelli di report con celle unite. Lo shrink-to-fit confronta la larghezza del testo misurata con la stessa larghezza colonna, quindi un MDW sbagliato cambia anche quali celle si restringono e di quanto. La stessa famiglia di confusione di unità ricompare negli anchor dei drawing, dove la geometria delle immagini e lo scaling EMU ha la sua catena di conversione da sbagliare
Come HotXLS misura l'MDW a run time
HotXLS risolve l'MDW dalla cartella di lavoro stessa anziché assumere una costante, e due procedure fanno il lavoro. PaginationApplyNormalFont legge il font dello stile Normal dalla cartella di lavoro e gira all'inizio della build di paginazione, prima che qualsiasi geometria di colonna venga calcolata; prima si azzera su Calibri 11, così una cartella di lavoro senza tabella font non può ereditare stato stantio da una build precedente. Il font dello stile Normal è fonts[0] in styles.xml, esposto dal componente come Workbook.Fonts[0]
// Legge fonts[0] (il font dello stile Normal) dalla workbook del worksheet.
// I worksheet classic senza tabella font mantengono il default 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 seconda procedura, PaginationMeasureMdW, chiede a GDI l'estensione del singolo carattere '0' tramite GetTextExtentPoint32W su una bitmap canvas condivisa off-screen, ricade su tmAveCharWidth da GetTextMetricsW quando la chiamata di estensione fallisce, e ricade su DefaultMDW quando nessuna delle due è disponibile. La sua cache è un singolo slot con chiave (name, size), che suona grezzo finché non guardate il pattern di accesso: una build di paginazione chiede lo stesso font Normal su ogni colonna di ogni pagina, quindi un solo slot ha un hit rate quasi perfetto e costa tre confronti per chiamata
Cosa succede senza tabella font, senza GUI o con un font mancante?
HotXLS degrada alla costante Calibri 11 in ogni caso in cui il vero font Normal non possa essere determinato, e lo fa silenziosamente per scelta di progetto. I worksheet classic BIFF sono il caso comune: i formati legacy non portano alcun pool di font XLSX a cui fonts[0] possa riferirsi, quindi il type guard esce presto e l'MDW predefinito di 7 resta in piedi. Non è una correzione, è il comportamento precedente preservato deliberatamente, così che aggiungere la misurazione al percorso XLSX non potesse fare una regression sull'output del formato classic
La dipendenza da GDI è l'avvertenza onesta. La misurazione gira contro un device context Windows, quindi il percorso presuppone un host Windows con il font installato. In un servizio o in un build agent headless le metriche testo GDI di solito si risolvono comunque, ma un font non installato su quella macchina viene sostituito dal font mapper e voi misurate il sostituto. Non fallisce mai rumorosamente; restituisce un numero plausibile per il typeface sbagliato. Se le esportazioni lato server devono corrispondere a un riferimento desktop, installate sull'host di esportazione i font che i vostri modelli nominano, o fissate il font Normal prima di invocare il percorso di esportazione PDF del worksheet
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Fissa il font Normal così che il MDW misurato su questo host sia quello
// rispetto a cui il layout è stato progettato, non un sostituto del font mapper.
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;
Misure in cache, e quella che è andata in crash su Win64
Una volta che la misurazione del testo è un round trip GDI anziché una moltiplicazione, va messa in cache, e mettere in cache dentro un pass di rendering è dove questo lavoro ha versato sangue. Il loop dello shrink-to-fit scala la dimensione del font a passi di 0.5 pt e ri-misura dopo ogni passo, quindi una singola cella può chiamare PaginationMeasureTextWidth una dozzina di volte con la stessa stringa, e il word wrap la richiama di nuovo per ogni riga candidata. Una memo con chiave nome font, dimensione e testo collassa tutto in una chiamata GDI per stringa distinta, memorizzata in una TStringList come coppie nome/valore
L'altra cache aggiunta accanto non era altrettanto pulita. Il pass di rendering 5 risolve il pool di font per cella tramite FontIndex, e la sua memo usava array dinamici paralleli con un FontMemoCount manutenuto a mano. La prima versione dimenticava di chiamare ResetFontMemo all'inizio di ogni pagina, così il conteggio continuava a salire tra le pagine mentre gli array no, e il codice scriveva oltre la fine di tutti quanti. Su Win32 questo scarabocchiava in silenzio nell'heap adiacente e chiudeva lì; su Win64 sollevava immediatamente un access violation su una scrittura a 0x538. La lezione generalizzabile: una cache basata su array trattenuta in una variabile a livello di unità va azzerata all'ingresso di ogni pass che la usa, perché una string list o un dizionario perdona un azzero mancante crescendo, e gli array paralleli no
Verificare la propria conversione
Non vi serve il componente per verificare nessuna di queste cose. Prendete una cartella di lavoro il cui font Normal non è Calibri 11, leggete una larghezza da <col width="..."/>, e passatela due volte attraverso la formula della specifica, una volta con MDW 7 e una volta con l'MDW che il vostro renderer misura davvero per quel font; se le risposte divergono e il vostro output corrisponde alla prima, avete trovato la deriva. La geometria delle colonne è una di quelle parti di un motore di foglio di calcolo che è o invisibile o l'unica cosa che chiunque noti, e farla giusta significa trattare il font Normal come un input del layout anziché un dettaglio di stile. Se sviluppate applicazioni Delphi o C++Builder che leggono, scrivono, renderizzano e stampano cartelle di lavoro Excel senza Office installato, il componente Excel HotXLS per Delphi gestisce la misurazione MDW, il modello di paginazione e la pipeline PDF dietro un unico set di classi VCL