No HotPDF Component, o THotPDF.Resolution define a unidade de desenho: todas as coordenadas X e Y, todas as margens, o tamanho passado ao SetFont, e os resultados do TextWidth e do GetWideTextWidth medem-se em 1/Resolution de polegada. O THPDFPage.Width e o Height não o seguem e continuam em pontos, por isso os limites de layout têm de vir do UserWidth e do UserHeight só de leitura. A razão habitual para tocar no Resolution é um port: um motor de relatórios que já pensa em 1/96 ou 1/144 de polegada é mais fácil de transportar quando o lado PDF fala a mesma unidade do que quando cada sítio de chamada leva um fator de conversão. Isso funciona bem, desde que saiba que números passaram à unidade nova e quais ficaram para trás
O que é que o THotPDF.Resolution muda de facto?
O THotPDF.Resolution muda só a forma como o HotPDF lê os números que lhe passa; o PDF que escreve é o mesmo. O setter são duas linhas: o SetResolution guarda o valor e põe DocScale := Value / 72. A partir daí, o XProjection e o YProjection dividem cada coordenada pelo DocScale a caminho do content stream, e o SetFont divide o tamanho da mesma forma antes de o registar. O user space do PDF tem 1/72 de polegada por predefinição (ISO 32000-1 §8.3.2.3), por isso ao Resolution predefinido de 72 a projeção é a identidade e a 144 uma unidade de desenho é meio ponto. Nenhuma entrada /UserUnit é escrita. Esse atributo de página, acrescentado no PDF 1.6, é outra coisa que o HotPDF expõe como THPDFPage.SetUserUnit. Um pormenor que apanha quem vem dos tutoriais do TextOut: as coordenadas de página correm do canto superior esquerdo com o Y a crescer para baixo, porque o YProjection calcula o topo do MediaBox menos o Y escalado, e isso mantém-se a todo o Resolution
var
Pdf: THotPDF;
Page: THPDFPage;
Margin: Single;
Title: WideString;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Resolution := 144; // 1 unidade de desenho = 1/144 de polegada
Pdf.BeginDoc;
Page := Pdf.CurrentPage; // A4: Width = 595, UserWidth = 1190
Margin := 144; // uma polegada em unidades de desenho
Page.SetFont('Arial', [fsBold], 28); // 28/144 de polegada, uma fonte de 14 pt
Title := 'INVOICE 2026-0417';
// Alinhe à direita contra a margem da página medida na mesma unidade
Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
Margin, 0, Title);
Page.SetLineWidth(2); // fio de 1 pt
Page.MoveTo(Margin, Margin + 48);
Page.LineTo(Page.UserWidth - Margin, Margin + 48);
Page.Stroke;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Porque é que o Page.Width discorda das minhas coordenadas a Resolution 144?
O THPDFPage.Width e o Height reportam a página em pontos independentemente do Resolution do documento, enquanto as suas coordenadas andam em 1/Resolution de polegada, por isso a 144 a página parece metade da largura real. Uma página A4 lê Width = 595 e Height = 842 ao Resolution 72 e continua a ler 595 e 842 a 144, onde a margem direita está na verdade em X = 1190. O UserWidth e o UserHeight, acrescentados na v2.766.0, devolvem Width * DocScale, que é o tamanho da página na unidade com que desenha. Antes de existirem, a biblioteca misturava os dois internamente, e os sintomas ao Resolution 144 eram dramáticos: parágrafos quebravam após cada carácter, o THPDFTable.Render empurrava cada linha para uma página nova, e tanto o importador de HTML como o achatador de XFA desenhavam o seu conteúdo a meia escala, o formulário achatado amontoado no canto superior esquerdo. O layout de parágrafos, a renderização de tabelas, a importação de HTML, a centragem de EMF, o clip de página WMF, e os diagnósticos de layout agora todos leem o tamanho em unidades de utilizador. O seu próprio código de layout também devia: tudo o que compara contra uma coordenada de desenho (uma margem direita, um teste de quebra de página, um cálculo de centragem) pertence ao UserWidth e ao UserHeight, nunca ao Width e ao Height
Armadilha um: atribuir Width ou Height muda a página para pontos
Pôr Page.Width ou Page.Height muda em silêncio a página para UserDefined, e uma página UserDefined ignora o DocScale por inteiro, por isso tudo o que desenhar nela depois vem em pontos, e não em 1/Resolution de polegada. O setter é antigo e recebe pontos por desenho, e foi por isso que se lhe deixou o significado. A projeção para uma página UserDefined é um simples X + MinX, e o SetFont guarda o tamanho tal e qual. Ao Resolution 144 o resultado é uma página cujo conteúdo de repente sai duas vezes maior que o da página anterior. A biblioteca cometeu exatamente este erro por si própria: as páginas de continuação de parágrafo copiavam o tamanho da página anterior através do Width, e toda a página de overflow mudava para pontos. Essas páginas agora copiam Size, Orientation, e o Resolution da página, e só recuam para Width e Height quando a página original já era UserDefined
Duas saídas, dependendo do que precisa. Se uma folha standard serve, defina Page.Size e Page.Orientation e continue a desenhar na sua unidade de Resolution. Se realmente precisa de um tamanho de página à medida, aceite que é uma página em pontos e desenhe em pontos; o UserWidth é igual ao Width aí, por isso código de layout que leia sempre o UserWidth continua a funcionar em ambos os tipos de página. O teste unitário prega isto: ao Resolution 144 uma página A4 reporta um UserWidth de 1190, mas depois de Width := 500 e Height := 400 reporta 500 e 400. Páginas carregadas comportam-se da mesma forma, porque uma página reconstruída de um PDF existente só conhece o seu MediaBox em pontos e desenha em pontos. Páginas que este documento criou mantêm as suas próprias unidades quando sai e volta através do CurrentPageNumber, o que é o caso desde a v2.766.26
Armadilha dois: porque é que os tamanhos de fonte saem a metade?
Um tamanho de fonte que começou em pontos sai a metade ao Resolution 144 porque o SetFont trata o seu argumento de tamanho como unidades de desenho e converte-o para pontos antes de o guardar. Internamente, o SetFont guarda ASize / DocScale * DPI no objeto de fonte corrente, por isso o valor guardado são sempre pontos. A biblioteca tropeçou nisto duas vezes: o fallback de fontes no WideTextOutBoxEx e a página de continuação de parágrafo entregavam ambos esse valor em pontos guardado de volta ao SetFont, que o escalava uma segunda vez e partia o texto ao meio. O seu código não consegue ler o tamanho guardado, mas o mesmo bug aparece sempre que um valor em pontos de outro sítio chega ao SetFont: um TFont.Size de um formulário VCL, um tamanho numa definição de relatório, um comprimento pt de CSS. Converta-o primeiro, e inclua o Resolution próprio da página e o caso UserDefined no fator, como a reprodução de metafiles faz quando reproduz o Canvas da página (veja como o HotPDF importa gráficos vetoriais EMF e WMF para esse caminho):
// Unidades de desenho por ponto na página corrente. Espelha a projeção
// que o HotPDF usa: 1 numa página dimensionada por Width/Height, caso
// contrário (Resolution do documento / 72) * (Resolution da página / 72)
function UnitsPerPoint(Pdf: THotPDF): Single;
begin
if Pdf.CurrentPage.Size = UserDefined then
Result := 1
else
Result := (Pdf.Resolution / 72) * (Pdf.CurrentPage.Resolution / 72);
end;
procedure SetFontFromVcl(Pdf: THotPDF; Font: TFont);
begin
// O TFont.Size anda em pontos; o SetFont espera unidades de desenho
Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
Font.Size * UnitsPerPoint(Pdf));
end;
A biblioteca aplica a mesma regra às suas próprias constantes em pontos. A fonte de 12 pontos com que toda a página nova começa agora é multiplicada pelo fator interno de unidades por ponto, por isso são 12 pontos a qualquer Resolution. O DrawChart, cujas margens, tamanhos de legendas e larguras de fio são todos pontos cravados no código, agora corre com a escala temporariamente posta em 1. O que fica em unidades de desenho, de propósito, são as predefinições de parâmetros públicos como o tamanho de módulo do DrawQRCode e o tamanho de fonte de tabela predefinido: fazem parte do contrato da API, por isso ao Resolution 144 significam metade do que significam a 72. Se dimensiona relatórios a partir de um modelo, o guia de output de relatórios com fontes e imagens no HotPDF cobre donde esses valores costumam vir
Como verificar que um layout é independente do Resolution?
A verificação mais fiável é uma comparação de bytes: renderize a mesma página ao Resolution 72 e outra vez a 144 com todas as coordenadas e tamanhos duplicados, e os content streams não comprimidos têm de ser idênticos. Ambas as corridas aterram nos mesmos valores em pontos depois da projeção, por isso qualquer diferença é um valor que saltou a conversão. É assim que a suite de testes do HotPDF verifica parágrafos, tabelas, importação de HTML, achatamento de XFA, arcos, metafiles e imagens. A mesma técnica funciona para o seu próprio código de relatórios com quase nenhum arnês:
procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.Compression := cmNone; // content streams legíveis
Pdf.FileName := FileName;
Pdf.Resolution := Res;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 10 * K);
Pdf.CurrentPage.TextOut(36 * K, 36 * K, 0, 'Line 1');
Pdf.CurrentPage.Rectangle(36 * K, 60 * K, 200 * K, 40 * K);
Pdf.CurrentPage.Stroke;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
// RenderPage('r72.pdf', 72, 1) and RenderPage('r144.pdf', 144, 2)
// têm de produzir content streams de página idênticos byte a byte
Verifique os operadores que transportam números: Td, Tm, Tf, re, w e as matrizes TJ. Os bytes ao nível do ficheiro vão continuar a divergir na data de criação e no /ID, por isso compare os streams, e não ficheiros inteiros. Um desacordo quase sempre aponta para uma das duas armadilhas acima: uma página redimensionada através do Width, ou um valor em pontos passado direitinho ao SetFont. Se é novo nas chamadas de desenho em si, comece pelo passo a passo do TextOut do HotPDF para tamanho, estilo e rotação, depois volte e mude o Resolution quando o seu layout ler UserWidth. Os detalhes completos da API e transferências de avaliação estão na página do componente PDF Delphi HotPDF