Artigo Técnico

Resolution no HotPDF: unidades de desenho e UserWidth

No HotPDF Component, a THotPDF.Resolution define a unidade de desenho: toda coordenada X e Y, toda margem, o tamanho passado ao SetFont, e os resultados de TextWidth e GetWideTextWidth são medidos em 1/Resolution inch. THPDFPage.Width e Height não a seguem e ficam em points, então os limites de layout precisam vir da UserWidth e da UserHeight somente leitura. O motivo habitual para mexer na Resolution é um port: um motor de relatórios que já pensa em 1/96 ou 1/144 inch é mais fácil de mover quando o lado do PDF fala a mesma unidade do que quando todo call site ganha um fator de conversão. Isso funciona bem, desde que você saiba quais números se mudaram para a unidade nova e quais ficaram para trás

O que a THotPDF.Resolution muda de fato?

A THotPDF.Resolution muda só como o HotPDF lê os números que você passa; o PDF que ele grava é o mesmo. O setter tem duas linhas: SetResolution guarda o valor e define DocScale := Value / 72. De então em diante, XProjection e YProjection dividem toda coordenada pelo DocScale a caminho do content stream, e o SetFont divide o tamanho do mesmo jeito antes de registrá-lo. O user space do PDF tem default de 1/72 inch (ISO 32000-1 §8.3.2.3), então na Resolution default de 72 a projeção é a identidade e a 144 uma unidade de desenho é meio point. Nenhuma entrada de /UserUnit é gravada. Esse atributo de página, acrescentado no PDF 1.6, é uma coisa separada que o HotPDF expõe como THPDFPage.SetUserUnit. Um detalhe que pega quem vem dos tutoriais de TextOut: as coordenadas de página correm do canto superior esquerdo com Y crescendo para baixo, porque o YProjection computa o topo da MediaBox menos o Y escalado, e isso vale em toda Resolution

Como a THotPDF.Resolution define a unidade de desenho em Delphi: o setter guarda DocScale como Resolution dividida por 72, então XProjection, YProjection e SetFont dividem toda coordenada e tamanho a caminho do content stream, então Resolution 72 é um mapeamento identidade e Resolution 144 faz uma unidade de desenho meio point enquanto a página ainda corre do topo esquerdo com Y para baixo
Nada no arquivo de saída se move — só o significado dos números que você passa muda, e é por isso que o mesmo content stream aparece a 72 e a 144
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 inch
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // um inch em unidades de desenho
    Page.SetFont('Arial', [fsBold], 28); // 28/144 inch, uma fonte de 14 pt
    Title := 'INVOICE 2026-0417';
    // Alinhe à direita contra a borda da página medida na mesma unidade
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // linha de 1 pt
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Por que o Page.Width discorda das minhas coordenadas na Resolution 144?

O THPDFPage.Width e o Height reportam a página em points não importa qual seja a Resolution do documento, enquanto as suas coordenadas estão em 1/Resolution inch, então a 144 a página parece metade da largura que realmente é. Uma página A4 lê Width = 595 e Height = 842 na Resolution 72 e ainda lê 595 e 842 a 144, onde a borda direita de fato está em X = 1190. UserWidth e UserHeight, acrescentadas na v2.766.0, retornam Width * DocScale, que é o tamanho da página na unidade em que você desenha. Antes delas existirem, a biblioteca misturava as duas internamente, e os sintomas na Resolution 144 eram dramáticos: parágrafos quebravam depois de cada caractere, o THPDFTable.Render empurrava cada row para uma página nova, e tanto o importador HTML quanto o flattener de XFA desenhavam o conteúdo deles na metade do tamanho, o form achatado amontoado no canto superior esquerdo. Layout de parágrafo, renderização de tabelas, import HTML, centralização de EMF, o clip de página WMF e diagnósticos de layout agora todos leem o tamanho em user unit. O seu próprio código de layout também deveria: qualquer coisa que compare contra uma coordenada de desenho (uma margem direita, um teste de quebra de página, um cálculo de centralização) pertence à UserWidth e à UserHeight, nunca ao Width e ao Height

Armadilha um: atribuir Width ou Height muda a página para points

Definir Page.Width ou Page.Height muda silenciosamente a página para UserDefined, e uma página UserDefined ignora o DocScale por completo, então tudo que você desenhar nela depois fica em points, não em 1/Resolution inch. O setter é antigo e recebe points de propósito, e é por isso que o significado dele ficou intocado. A projeção para uma página UserDefined é simplesmente X + MinX, e o SetFont guarda o tamanho sem mudança. Na 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 esse erro ela mesma: páginas de continuação de parágrafo costumavam copiar o tamanho da página anterior pelo Width, e toda página de overflow mudava para points. Essas páginas agora copiam Size, Orientation e a Resolution da página em vez disso, e só caem para Width e Height quando a página original já era UserDefined

Duas saídas, dependendo do que você precisa. Se uma folha padrão serve, defina Page.Size e Page.Orientation e continue desenhando na unidade da sua Resolution. Se você realmente precisa de um tamanho de página customizado, aceite que ela é uma página em points e desenhe em points; a UserWidth iguala o Width ali, então código de layout que sempre lê a UserWidth continua funcionando nos dois tipos de página. O teste unitário crava isso: na Resolution 144 uma página A4 reporta uma UserWidth de 1190, mas depois de Width := 500 e Height := 400 ela reporta 500 e 400. Páginas carregadas se comportam do mesmo jeito, porque uma página reconstruída de um PDF existente só conhece a MediaBox dela em points e desenha em points. Páginas que este documento criou mantêm as unidades próprias delas quando você sai e volta pelo CurrentPageNumber, o que é o caso desde a v2.766.26

Por que o Page.Width discorda das suas coordenadas na Resolution 144 no HotPDF: Width e Height ficam em points enquanto o desenho usa 1/144 inch, então uma página A4 lê 595 mas a borda direita dela está na UserWidth 1190, e atribuir Width muda a página para UserDefined, que ignora o DocScale, então parágrafos quebram por caractere, tabelas quebram por row e tamanhos de SetFont caem à metade
Qualquer coisa que compare contra uma coordenada de desenho pertence à UserWidth e à UserHeight — numa página UserDefined em points as duas coincidem, então o mesmo código de layout sobrevive nas duas

Armadilha dois: por que tamanhos de fonte saem com metade do tamanho?

Um tamanho de fonte que começou como points sai com metade do tamanho na Resolution 144 porque o SetFont trata o argumento de tamanho dele como unidades de desenho e o converte para points antes de guardar. Internamente, o SetFont guarda ASize / DocScale * DPI no objeto de fonte atual, então o valor guardado é sempre points. A biblioteca tropeçou nisso duas vezes: o fallback de fonte no WideTextOutBoxEx e a página de continuação de parágrafo ambos entregavam esse valor em points guardado de volta ao SetFont, que o escalava uma segunda vez e reduzia o texto à metade. O seu código não consegue ler o tamanho guardado, mas o mesmo bug aparece sempre que um valor em points de outro lugar chega ao SetFont: um TFont.Size de um form VCL, um tamanho numa definição de relatório, um comprimento pt de CSS. Converta primeiro, e inclua a Resolution da própria página e o caso UserDefined no fator, como o playback de metafile faz quando ele repoduz o Canvas da página (veja como o HotPDF importa gráficos vetoriais EMF e WMF para esse caminho):

// Unidades de desenho por point na página atual. Espelha a projeção
// que o HotPDF usa: 1 numa página dimensionada por Width/Height, caso contrário
// (document Resolution / 72) * (page Resolution / 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
  // TFont.Size está em points; 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 próprias constantes em points dela. A fonte de 12 points com que toda página nova começa agora é multiplicada pelo fator interno de unidades por point, então ela tem 12 points em qualquer Resolution. O DrawChart, cujas margens, tamanhos de label e larguras de linha são todos points hard-coded, agora roda com a escala temporariamente definida como 1. O que fica em unidades de desenho, de propósito, são defaults de parâmetros públicos como o tamanho de módulo do DrawQRCode e o tamanho de fonte default de tabela: eles fazem parte do contrato da API, então na Resolution 144 significam metade do que significam a 72. Se você dimensiona relatórios a partir de um template, o guia de saída de relatório com fontes e imagens no HotPDF cobre de onde esses valores normalmente vêm

Como você verifica que um layout é independente de Resolution?

A checagem mais confiável é uma comparação de bytes: renderize a mesma página na Resolution 72 e de novo a 144 com toda coordenada e tamanho dobrados, e os content streams descomprimidos precisam ser idênticos. As duas execuções caem nos mesmos valores em points depois da projeção, então qualquer diferença é um valor que pulou a conversão. É assim que a suíte de testes do HotPDF confere parágrafos, tabelas, import HTML, achatamento de XFA, arcos, metafiles e imagens. A mesma técnica funciona para o seu próprio código de relatório com quase nenhum harness:

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) e RenderPage('r144.pdf', 144, 2)
// precisam produzir content streams de página idênticos byte a byte
Como verificar independência de Resolution em código Delphi do HotPDF: renderize o layout idêntico duas vezes, uma na Resolution 72 com escala 1 e outra a 144 com toda coordenada e tamanho de fonte dobrados, depois exija content streams descomprimidos idênticos byte a byte — uma divergência aponta para uma página mudada para UserDefined pelo Width ou um valor em points sem conversão chegando ao SetFont
As duas execuções caem nos mesmos valores em points depois da projeção, então qualquer diferença é um número que pulou a conversão dele — o mesmo harness de que a suíte de testes do HotPDF depende

Confira os operadores que carregam números: Td, Tm, Tf, re, w e os arrays de TJ. Bytes a nível de arquivo ainda vão divergir na data de criação e no /ID, então compare os streams, não arquivos inteiros. Uma divergência quase sempre aponta para uma das duas armadilhas acima: uma página redimensionada pelo Width, ou um valor em points passado direto ao SetFont. Se você é novo nas chamadas de desenho em si, comece pelo passo a passo de TextOut do HotPDF para tamanho, estilo e rotação, depois volte e mude a Resolution quando o seu layout ler UserWidth. Detalhes completos de API e downloads de teste estão na página do componente HotPDF Delphi PDF