Artículo técnico

Resolution de HotPDF: unidades de dibujo y UserWidth

En HotPDF Component, THotPDF.Resolution define la unidad de dibujo: cada coordenada X e Y, cada margen, el tamaño que se pasa a SetFont, y los resultados de TextWidth y GetWideTextWidth se miden en 1/Resolution de pulgada. THPDFPage.Width y Height no lo siguen y se quedan en puntos, así que los límites de layout tienen que salir del UserWidth y el UserHeight de solo lectura. La razón de siempre para tocar Resolution es un port: un motor de reportes que ya piensa en 1/96 o 1/144 de pulgada se muda más fácil cuando el lado PDF habla la misma unidad que cuando cada call site recibe un factor de conversión. Eso funciona bien, mientras usted sepa qué números se mudaron a la unidad nueva y cuáles se quedaron atrás

¿Qué cambia en realidad THotPDF.Resolution?

THotPDF.Resolution cambia solo cómo HotPDF lee los números que usted pasa; el PDF que escribe es el mismo. El setter son dos líneas: SetResolution guarda el valor y pone DocScale := Value / 72. A partir de ahí, XProjection y YProjection dividen cada coordenada por DocScale en el camino hacia el content stream, y SetFont divide el tamaño igual antes de registrarlo. El user space de PDF por defecto es 1/72 de pulgada (ISO 32000-1 §8.3.2.3), así que con la Resolution por defecto de 72 la proyección es la identidad y a 144 una unidad de dibujo es medio punto. No se escribe ninguna entrada /UserUnit. Ese atributo de página, agregado en PDF 1.6, es otra cosa que HotPDF expone como THPDFPage.SetUserUnit. Un detalle que atrapa a la gente que viene de los tutoriales de TextOut: las coordenadas de página corren desde la esquina superior izquierda con la Y creciendo hacia abajo, porque YProjection computa el tope del MediaBox menos la Y escalada, y eso sigue siendo cierto a toda Resolution

Cómo THotPDF.Resolution define la unidad de dibujo en Delphi: el setter guarda DocScale como Resolution dividida por 72, después XProjection, YProjection y SetFont dividen cada coordenada y tamaño en el camino hacia el content stream, así que Resolution 72 es un mapeo identidad y Resolution 144 vuelve media unidad de dibujo un punto mientras la página sigue corriendo desde arriba a la izquierda con Y hacia abajo
Nada se mueve en el archivo de salida — solo cambia el significado de los números que usted pasa, y por eso el mismo content stream aparece a 72 y 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 unidad de dibujo = 1/144 de pulgada
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // una pulgada en unidades de dibujo
    Page.SetFont('Arial', [fsBold], 28); // 28/144 de pulgada, una fuente de 14 pt
    Title := 'INVOICE 2026-0417';
    // Alinee a la derecha contra el borde de página medido en la misma unidad
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // regla 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 qué Page.Width discrepa de mis coordenadas a Resolution 144?

THPDFPage.Width y Height reportan la página en puntos sin importar la Resolution del documento, mientras que sus coordenadas están en 1/Resolution de pulgada, así que a 144 la página se ve de la mitad de ancho de lo que es. Una página A4 lee Width = 595 y Height = 842 a Resolution 72 y sigue leyendo 595 y 842 a 144, donde el borde derecho en realidad está en X = 1190. UserWidth y UserHeight, agregados en la v2.766.0, devuelven Width * DocScale, que es el tamaño de página en la unidad con la que usted dibuja. Antes de que existieran, la librería mezclaba las dos internamente, y los síntomas a Resolution 144 eran dramáticos: los párrafos partían después de cada carácter, THPDFTable.Render empujaba cada fila a una página nueva, y tanto el importador de HTML como el flattener de XFA dibujaban su contenido a mitad de tamaño, con el form aplanado apretado en la esquina superior izquierda. El layout de párrafos, el renderizado de tablas, el import de HTML, el centrado de EMF, el clip de página WMF y los diagnósticos de layout ahora leen todos el tamaño en unidades de usuario. Su propio código de layout debería hacer lo mismo: todo lo que compara contra una coordenada de dibujo (un margen derecho, un test de salto de página, un cálculo de centrado) pertenece a UserWidth y UserHeight, nunca a Width y Height

Trampa uno: asignar Width o Height cambia la página a puntos

Poner Page.Width o Page.Height cambia en silencio la página a UserDefined, y una página UserDefined ignora DocScale por completo, así que todo lo que dibuje después sobre ella va en puntos, no en 1/Resolution de pulgada. El setter es viejo y recibe puntos a propósito, y por eso no se le tocó el significado. La proyección para una página UserDefined es simplemente X + MinX, y SetFont guarda el tamaño sin cambios. A Resolution 144 el resultado es una página cuyo contenido de golpe sale el doble de grande que el de la página anterior. La librería cometió exactamente este error ella misma: las páginas de continuación de párrafo solían copiar el tamaño de la página previa vía Width, y toda página de overflow pasaba a puntos. Esas páginas ahora copian Size, Orientation y la Resolution de página, y solo caen a Width y Height cuando la página original ya era UserDefined

Dos salidas, según lo que necesite. Si una hoja estándar alcanza, fije Page.Size y Page.Orientation y siga dibujando en su unidad de Resolution. Si de verdad necesita un tamaño de página custom, acepte que es una página en puntos y dibuje en puntos; allí UserWidth iguala a Width, así que el código de layout que siempre lee UserWidth sigue funcionando en ambos tipos de página. El test unitario lo deja clavado: a Resolution 144 una página A4 reporta un UserWidth de 1190, pero después de Width := 500 y Height := 400 reporta 500 y 400. Las páginas cargadas se comportan igual, porque una página reconstruida desde un PDF existente solo conoce su MediaBox en puntos y dibuja en puntos. Las páginas que este documento creó conservan sus unidades cuando usted cambia de página y vuelve vía CurrentPageNumber, algo que rige desde la v2.766.26

Por qué Page.Width discrepa de sus coordenadas a Resolution 144 en HotPDF: Width y Height se quedan en puntos mientras el dibujo usa 1/144 de pulgada, así que una página A4 lee 595 pero su borde derecho está en UserWidth 1190, y asignar Width cambia la página a UserDefined, que ignora DocScale, así que los párrafos parten por carácter, las tablas se rompen por fila y los tamaños de SetFont se dan la mitad
Todo lo que compara contra una coordenada de dibujo pertenece a UserWidth y UserHeight — en una página UserDefined en puntos ambas coinciden, así que el mismo código de layout sobrevive en las dos

Trampa dos: ¿por qué los tamaños de fuente salen a mitad de tamaño?

Un tamaño de fuente que nació como puntos sale a mitad de tamaño a Resolution 144 porque SetFont trata su argumento de tamaño como unidades de dibujo y lo convierte a puntos antes de guardarlo. Internamente, SetFont guarda ASize / DocScale * DPI en el objeto de fuente actual, así que el valor guardado siempre son puntos. La librería se tropezó con esto dos veces: el font fallback en WideTextOutBoxEx y la página de continuación de párrafo le devolvían ese valor en puntos guardado a SetFont, que lo escalaba una segunda vez y dejaba el texto a la mitad. Su código no puede leer el tamaño guardado, pero el mismo bug aparece siempre que un valor en puntos que viene de otro lado llega a SetFont: un TFont.Size de un form VCL, un tamaño en una definición de reporte, una longitud pt de CSS. Conviértalo primero, e incluya en el factor la Resolution propia de la página y el caso UserDefined, como hace el playback de metafiles cuando repite el Canvas de la página (vea cómo HotPDF importa gráficos vectoriales EMF y WMF para ese camino):

// Unidades de dibujo por punto en la página actual. Refleja la proyección
// que HotPDF usa: 1 en una página dimensionada vía Width/Height, si no
// (Resolution del documento / 72) * (Resolution de la 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
  // TFont.Size va en puntos; SetFont espera unidades de dibujo
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

La librería aplica la misma regla a sus propias constantes en puntos. La fuente de 12 puntos con la que arranca toda página nueva ahora se multiplica por el factor interno de unidades por punto, así que son 12 puntos a cualquier Resolution. DrawChart, cuyos márgenes, tamaños de etiqueta y anchos de línea son todos puntos hardcodeados, ahora corre con la escala puesta temporalmente en 1. Lo que se queda en unidades de dibujo, a propósito, son los defaults de parámetros públicos como el tamaño de módulo de DrawQRCode y el tamaño de fuente de tabla por defecto: son parte del contrato de la API, así que a Resolution 144 significan la mitad de lo que significan a 72. Si dimensiona reportes desde una plantilla, la guía de salida de reportes con fuentes e imágenes en HotPDF cubre de dónde suelen venir esos valores

¿Cómo verificar que un layout es independiente de la Resolution?

El chequeo más confiable es una comparación de bytes: renderice la misma página a Resolution 72 y otra vez a 144 con cada coordenada y tamaño doblados, y los content streams sin comprimir deben ser idénticos. Ambas corridas caen en los mismos valores en puntos después de la proyección, así que cualquier diferencia es un valor que se saltó la conversión. Así es como la suite de tests de HotPDF chequea párrafos, tablas, import de HTML, aplanado de XFA, arcos, metafiles e imágenes. La misma técnica sirve para su propio código de reportes con un harness mínimo:

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 legibles
    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) y RenderPage('r144.pdf', 144, 2)
// deben producir content streams de página idénticos byte a byte
Cómo verificar la independencia de Resolution en código Delphi de HotPDF: renderice el layout idéntico dos veces, una a Resolution 72 con escala 1 y otra a 144 con cada coordenada y tamaño de fuente doblados, y exija content streams sin comprimir idénticos byte a byte — un desfase apunta a una página cambiada a UserDefined vía Width o a un valor en puntos sin convertir que llegó a SetFont
Ambas corridas caen en los mismos valores en puntos después de la proyección, así que cualquier diferencia es un número que se saltó su conversión — el mismo harness en el que confía la suite de tests de HotPDF

Chequee los operadores que cargan números: Td, Tm, Tf, re, w y los arrays de TJ. Los bytes a nivel de archivo seguirán difiriendo en la fecha de creación y el /ID, así que compare los streams, no los archivos enteros. Un desfase casi siempre apunta a una de las dos trampas de arriba: una página redimensionada vía Width, o un valor en puntos pasado directo a SetFont. Si usted es nuevo en las llamadas de dibujo en sí, arranque con el recorrido de TextOut de HotPDF para tamaño, estilo y rotación, después vuelva y cambie la Resolution cuando su layout lea UserWidth. Los detalles completos de la API y las descargas de prueba están en la página del componente HotPDF Delphi PDF