O PDF exportado coloca cada limite de coluna meio caractere à esquerda de onde o Excel o desenha, e cada célula com quebra automática agora quebra em um lugar diferente. A largura de coluna do Excel não é medida em caracteres nem em pontos. Ela é medida em unidades de Max Digit Width (MDW) da fonte Normal da pasta de trabalho, e o HotXLS mede essa fonte com GDI antes de cada construção de paginação. O modo de falha é silencioso: nada lança exceção, as larguras armazenadas fazem round-trip byte a byte, e a geometria continua deslocada por alguns por cento por coluna até que o desvio acumulado empurre uma tabela de uma página para duas
Em qual unidade a largura de coluna do Excel é medida?
A largura de coluna em uma planilha é uma contagem de caracteres de dígito da fonte Normal da pasta de trabalho, não uma medida absoluta. A ECMA-376 §18.3.1.13 define o atributo width de <col> em termos do Maximum Digit Width dessa fonte a 96 dpi, e dá a conversão de uma largura armazenada de volta para pixels como uma expressão de truncamento sobre o MDW. Para o Calibri 11, que é o que o Excel distribui como estilo Normal, o MDW mede 7 pixels. Passe a largura padrão de 8.43 unidades pela fórmula da especificação com MDW 7 e você obtém exatamente 64 pixels, que são 48 pontos a 96 dpi. Esses são os números que o próprio Excel relata, então eles servem como uma verificação útil: se sua conversão reproduz 8.43 unidades em 64 pixels, a aritmética está certa e só a entrada do MDW ainda pode estar errada
const
// Maximum digit width (MDW) da fonte de corpo padrão, em pixels a 96 dpi.
// Calibri 11 mede 7 px, o que reproduz as larguras exatas em pixels
// que o Excel armazena (8.43 unidades -> 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 de 96 dpi -> pontos
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
O HotXLS mantém essa aritmética em exatamente uma função, na unit lxPagination, então há um único lugar onde a régua pode estar errada. O + 5 é o padding que o Excel adiciona para linhas de grade e margens de célula, o * 0.75 converte pixels de 96 dpi para pontos PostScript, e o piso em MinimumColumnWidth existe para que uma coluna patologicamente estreita ainda deixe uma faixa em que o renderizador possa desenhar uma borda. O ponto de entrada público ColumnWidthToPoints mantém sua antiga assinatura de um argumento e encaminha um MDW medido para esta função, e foi isso que permitiu que a mudança de comportamento aterrissasse sem tocar em um único call site
Por que uma fonte Normal que não é Calibri move cada limite
O desvio é multiplicativo, e é por isso que ele parece um bug de renderização em vez de um bug de unidades. O MDW é um fator sobre a largura, não um deslocamento. Empurre o MDW de 7 para 8 e a coluna padrão de 8.43 unidades vai de 64 pixels para 72, um salto de 8 pixels ou 6 pontos em uma coluna. Dez colunas disso e a borda direita da tabela se moveu quase uma polegada. Pastas de trabalho que disparam isso são inteiramente comuns: qualquer coisa gerada por uma ferramenta de relatório que carimba Arial ou Segoe UI no estilo Normal, qualquer coisa salva de um template de exportação de ERP, qualquer coisa que um cliente reformulou uma vez e esqueceu
Dois sistemas de layout relacionados herdam o erro em vez de causá-lo. Regiões mescladas somam as larguras em pontos de suas colunas membro, então uma mesclagem que cabia em uma página no Excel pode estourar após o desvio do MDW, o que vale lembrar quando você constrói templates de relatório com células mescladas. Shrink-to-fit compara a largura de texto medida com a mesma largura de coluna, então o MDW errado também muda quais células encolhem e o quanto encolhem. A mesma família de confusão de unidades aparece em âncoras de desenho, onde geometria de imagem e escala EMU tem sua própria cadeia de conversão para errar
Como o HotXLS mede o MDW em tempo de execução
O HotXLS resolve o MDW a partir da própria pasta de trabalho em vez de assumir uma constante, e dois procedures fazem o trabalho. PaginationApplyNormalFont lê a fonte do estilo Normal da pasta de trabalho e roda no topo da construção de paginação, antes de qualquer geometria de coluna ser computada; ele redefine para Calibri 11 primeiro, então uma pasta de trabalho sem tabela de fontes não pode herdar estado obsoleto de uma construção anterior. A fonte do estilo Normal é fonts[0] em styles.xml, exposta pelo componente como Workbook.Fonts[0]
// Lê fonts[0] (a fonte do estilo Normal) da pasta de trabalho da worksheet.
// Worksheets clássicas sem tabela de fontes mantêm o padrão 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;
O segundo procedure, PaginationMeasureMdW, pede ao GDI a extensão do único caractere '0' por meio de GetTextExtentPoint32W em um canvas de bitmap off-screen compartilhado, faz fallback para tmAveCharWidth de GetTextMetricsW quando a chamada de extensão falha, e faz fallback para DefaultMDW quando nenhum dos dois está disponível. Seu cache é um slot único chaveado por (name, size), o que soa grosseiro até você olhar o padrão de acesso: uma construção de paginação pede a mesma fonte Normal em cada coluna de cada página, então um slot tem uma taxa de acerto quase perfeita e custa três comparações por chamada
O que acontece sem tabela de fontes, sem GUI ou com fonte ausente?
O HotXLS degrada para a constante Calibri 11 em todos os casos em que a fonte Normal real não pode ser determinada, e o faz silenciosamente por design. Worksheets BIFF clássicas são o caso comum: os formatos legados não carregam um pool de fontes XLSX para fonts[0] referenciar, então o type guard sai cedo e o MDW padrão de 7 permanece. Isso não é uma correção, é o comportamento anterior preservado deliberadamente, para que adicionar medição ao caminho XLSX não pudesse regredir a saída do formato clássico
A dependência de GDI é a ressalva honesta. A medição roda contra um device context do Windows, então o caminho assume um host Windows com a fonte instalada. Em um serviço ou build agent headless, as métricas de texto do GDI geralmente ainda resolvem, mas uma fonte que não está instalada naquela máquina é substituída pelo font mapper e você acaba medindo o substituto. Nunca falha ruidosamente; retorna um número plausível para o typeface errado. Se exportações do lado do servidor precisam corresponder a uma referência de desktop, instale no host de exportação as fontes que seus templates nomeiam, ou fixe a fonte Normal antes de invocar o caminho de exportação PDF de worksheet
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Fixa a fonte Normal para que o MDW medido neste host seja aquele
// contra o qual o layout foi projetado, não um substituto do 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;
Caches de medição, e o que travou no Win64
Uma vez que a medição de texto é um round trip de GDI em vez de uma multiplicação, ela precisa ser cacheada, e cachear dentro de um render pass é onde este trabalho derramou sangue. O loop shrink-to-fit reduz o tamanho da fonte em incrementos de 0.5 pt e mede novamente após cada passo, então uma célula pode chamar PaginationMeasureTextWidth uma dúzia de vezes com a mesma string, e o word wrap a chama de novo por linha candidata. Um memo chaveado por nome de fonte, tamanho e texto colapsa isso para uma chamada GDI por string distinta, armazenado em uma TStringList como pares nome/valor
O outro cache adicionado junto não foi tão arrumado. O render pass 5 resolve o pool de fontes por célula por FontIndex, e seu memo usava dynamic arrays paralelos com um FontMemoCount mantido à mão. A primeira versão esqueceu de chamar ResetFontMemo no início de cada página, então a contagem continuou subindo entre páginas enquanto os arrays não subiam, e o código escreveu além do fim de todos eles. No Win32 isso rabiscava silenciosamente no heap adjacente e terminava; no Win64 levantou um access violation imediatamente, em uma escrita para 0x538. A lição generalizável: um cache suportado por array mantido em uma variável de nível de unit deve ser redefinido na entrada de cada pass que o usa, porque uma string list ou um dicionário perdoa uma redefinição ausente crescendo, e arrays paralelos não
Verificando sua própria conversão
Você não precisa do componente para verificar nada disso. Pegue uma pasta de trabalho cuja fonte Normal não seja Calibri 11, leia uma largura de <col width="..."/>, e passe pela fórmula da especificação duas vezes, uma com MDW 7 e uma com o MDW que seu renderizador realmente mede para essa fonte; se as respostas diferirem e sua saída corresponder à primeira, você encontrou o desvio. A geometria de coluna é uma daquelas partes de um motor de planilha que ou é invisível ou é a única coisa que todos notam, e acertá-la significa tratar a fonte Normal como uma entrada do layout em vez de um detalhe de estilo. Se você constrói aplicações Delphi ou C++Builder que leem, escrevem, renderizam e imprimem pastas de trabalho do Excel sem o Office instalado, o componente Excel Delphi HotXLS cuida da medição do MDW, do modelo de paginação e do pipeline de PDF por trás de um único conjunto de classes VCL