O PDF exportado põe cada limite de coluna meio carácter à esquerda de onde o Excel o desenha, e cada célula com quebra de linha agora parte num sítio diferente. A largura de coluna do Excel não se mede em caracteres nem em pontos. Mede-se em unidades Max Digit Width (MDW) do tipo Normal do livro, e o HotXLS mede esse tipo de letra com GDI antes de cada construção de paginação. O modo de falha é silencioso: nada lança exceções, as larguras guardadas fazem a viagem de ida e volta byte a byte, e a geometria continua desviada por alguns por cento por coluna até a deriva acumulada empurrar uma tabela de uma página para duas
Em que unidade se mede a largura de coluna do Excel?
Uma largura de coluna numa folha de cálculo é uma contagem de caracteres dígito do tipo Normal do livro, não uma medida absoluta. A ECMA-376 §18.3.1.13 define o atributo width de <col> em termos do Maximum Digit Width desse tipo de letra a 96 dpi, e dá a conversão de uma largura guardada de volta para pixels como uma expressão de truncação sobre o MDW. Para o Calibri 11, que é o que o Excel distribui como estilo Normal, o MDW mede 7 pixels. Passe a largura predefinida de 8.43 unidades pela fórmula da especificação com MDW 7 e obtém exatamente 64 pixels, que são 48 pontos a 96 dpi. Esses são os números que o próprio Excel reporta, pelo que são uma verificação útil: se a sua conversão reproduz 8.43 unidades em 64 pixels, a aritmética está certa e só a entrada de MDW ainda pode estar errada
const
// Largura máxima do dígito (MDW) do tipo de corpo predefinido em pixels a 96 dpi.
// O Calibri 11 mede 7 px, o que reproduz as larguras exatas em pixels
// que o Excel guarda (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 unidade lxPagination, por isso há um único sítio onde a régua pode estar errada. O + 5 é o preenchimento que o Excel acrescenta para linhas de grelha e margens de célula, o * 0.75 converte pixels de 96 dpi em pontos PostScript, e o piso em MinimumColumnWidth existe para que uma coluna patologicamente estreita ainda deixe uma faixa onde o renderizador possa desenhar um contorno. O ponto de entrada público ColumnWidthToPoints mantém a sua assinatura antiga de um argumento e encaminha um MDW medido para esta função, o que permitiu que a mudança de comportamento aterrasse sem tocar num único sítio de chamada
Porque é que um tipo Normal sem Calibri move todos os limites
A deriva é multiplicativa, o que explica parecer um erro de renderização em vez de um erro de unidades. O MDW é um fator sobre a largura, não um desvio. Empurre o MDW de 7 para 8 e a coluna predefinida de 8.43 unidades passa de 64 pixels para 72, um salto de 8 pixels ou 6 pontos numa coluna. Dez colunas disso e o limite direito da tabela já se moveu quase uma polegada. Os livros que tropeçam nisto são completamente ordinários: tudo o que foi gerado por uma ferramenta de relatórios que carimba Arial ou Segoe UI no estilo Normal, tudo o que foi guardado de um modelo de exportação de ERP, tudo o que um cliente reestilizou uma vez e esqueceu
Dois sistemas de layout relacionados herdam o erro em vez de o causar. As regiões unidas somam as larguras em pontos das suas colunas membro, pelo que uma junção que cabia numa página no Excel pode transbordar depois da deriva de MDW, o que vale a pena recordar quando constrói modelos de relatórios com células unidas. O shrink-to-fit compara a largura do texto medida com a mesma largura de coluna, pelo que um MDW errado também muda quais células encolhem e por quanto. A mesma família de confusão de unidades aparece nas âncoras de desenho, onde a geometria de imagens e escala EMU tem a 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 do próprio livro em vez de assumir uma constante, e dois procedimentos fazem o trabalho. O PaginationApplyNormalFont lê o tipo do estilo Normal do livro e corre no topo da construção de paginação, antes de qualquer geometria de coluna ser calculada; repõe primeiro o Calibri 11, pelo que um livro sem tabela de tipos de letra não pode herdar estado obsoleto de uma construção anterior. O tipo do estilo Normal é fonts[0] em styles.xml, exposto pelo componente como Workbook.Fonts[0]
// Lê fonts[0] (o tipo do estilo Normal) do livro da folha de cálculo.
// Folhas clássicas sem tabela de tipos de letra mantêm a predefiniçã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 procedimento, PaginationMeasureMdW, pede ao GDI a extensão do único carácter '0' através de GetTextExtentPoint32W numa canvas de bitmap partilhada fora do ecrã, recorre a tmAveCharWidth de GetTextMetricsW quando a chamada de extensão falha, e recorre a DefaultMDW quando nenhum está disponível. A sua cache é uma única ranhura chaveada por (name, size), o que soa grosseiro até se olhar para o padrão de acesso: uma construção de paginação pede o mesmo tipo Normal em cada coluna de cada página, pelo que uma ranhura tem uma taxa de acerto quase perfeita e custa três comparações por chamada
O que acontece sem tabela de tipos de letra, sem GUI, ou com um tipo em falta?
O HotXLS degrada para a constante Calibri 11 em todos os casos em que o tipo Normal real não pode ser determinado, e fá-lo silenciosamente por design. As folhas de cálculo BIFF clássicas são o caso comum: os formatos legados não transportam um pool de tipos de letra XLSX para o fonts[0] referir, pelo que a guarda de tipo sai cedo e o MDW predefinido de 7 mantém-se. Isso não é uma correção, é o comportamento anterior preservado deliberadamente, para que acrescentar medição ao caminho XLSX não pudesse regredir a saída do formato clássico
A dependência do GDI é a ressalva honesta. A medição corre contra um contexto de dispositivo Windows, pelo que o caminho assume um anfitrião Windows com o tipo de letra instalado. Num serviço ou num agente de construção headless, as métricas de texto do GDI em geral ainda resolvem, mas um tipo de letra que não esteja instalado nessa máquina é substituído pelo font mapper e mede o substituto em vez disso. Nunca falha ruidosamente; devolve um número plausível para o tipo de letra errado. Se as exportações do lado do servidor têm de corresponder a uma referência de secretária, instale os tipos de letra que os seus modelos nomeiam no anfitrião de exportação, ou fixe o tipo Normal antes de invocar o caminho de exportação PDF da folha de cálculo
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Fixe o tipo Normal para que o MDW medido neste anfitrião seja o
// contra o qual o layout foi desenhado, 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 a que estoirou no Win64
Uma vez que a medição de texto é uma viagem de ida e volta ao GDI em vez de uma multiplicação, tem de ser posta em cache, e pôr em cache dentro de uma passagem de renderização é onde este trabalho cobrou o seu preço. O ciclo de shrink-to-fit desce o tamanho do tipo de letra em incrementos de 0.5 pt e volta a medir após cada passo, pelo que uma célula pode chamar PaginationMeasureTextWidth uma dúzia de vezes com a mesma cadeia, e a quebra de linha por palavras chama-a outra vez por linha candidata. Um memo chaveado por nome do tipo, tamanho e texto colapsa isso numa chamada GDI por cadeia distinta, guardado num TStringList como pares nome/valor
A outra cache acrescentada ao lado não foi tão arrumada. A passagem de renderização 5 resolve o pool de tipos de letra por célula por FontIndex, e o seu memo usava arrays dinâmicos paralelos com um FontMemoCount mantido à mão. A primeira versão esqueceu-se de chamar ResetFontMemo no início de cada página, pelo que a contagem continuou a subir entre páginas enquanto os arrays não subiram, e o código escreveu para além do fim de todos eles. Em Win32 isso rabiscava silenciosamente no heap adjacente e terminava; em Win64 lançou imediatamente uma violação de acesso numa escrita para 0x538. A lição generalizável: uma cache suportada por arrays guardada numa variável ao nível da unidade tem de ser reposta à entrada de cada passagem que a usa, porque uma lista de cadeias ou um dicionário perdoa uma reposição em falta crescendo, e arrays paralelos não
Verificar a sua própria conversão
Não precisa do componente para verificar nada disto. Pegue num livro cujo tipo Normal não é o Calibri 11, leia uma largura de <col width="..."/>, e passe-a duas vezes pela fórmula da especificação, uma com MDW 7 e outra com o MDW que o seu renderizador realmente mede para esse tipo de letra; se as respostas diferirem e a sua saída corresponder à primeira, encontrou a deriva. A geometria de colunas é uma daquelas partes de um motor de folhas de cálculo que ou é invisível ou é a única coisa que alguém nota, e acertar nela significa tratar o tipo Normal como uma entrada do layout em vez de um detalhe de estilo. Se constrói aplicações Delphi ou C++Builder que leem, escrevem, renderizam e imprimem livros Excel sem o Office instalado, o componente Excel HotXLS para Delphi trata da medição de MDW, do modelo de paginação e do pipeline PDF por trás de um único conjunto de classes VCL