Техническая статья

Resolution в HotPDF: единицы рисования и UserWidth

В HotPDF Component THotPDF.Resolution определяет единицу рисования: каждая координата X и Y, каждый отступ, размер, передаваемый в SetFont, и результаты TextWidth и GetWideTextWidth меряются в 1/Resolution дюйма. THPDFPage.Width и Height за ней не следуют и остаются в пунктах, поэтому границы вёрстки берите из read-only UserWidth и UserHeight. Обычный повод трогать Resolution — перенос: движок отчётов, уже думающий в 1/96 или 1/144 дюйма, переезжает легче, когда PDF-сторона говорит на той же единице, чем когда каждое место вызова получает переводной коэффициент. Это работает хорошо, пока вы знаете, какие числа переехали в новую единицу, а какие остались

Что THotPDF.Resolution на самом деле меняет?

THotPDF.Resolution меняет лишь то, как HotPDF читает числа, которые вы передаёте; записываемый PDF тот же. Сеттер — две строки: SetResolution хранит значение и выставляет DocScale := Value / 72. Дальше XProjection и YProjection делят каждую координату на DocScale по дороге в поток содержимого, а SetFont так же делит размер перед записью. User space PDF по умолчанию равен 1/72 дюйма (ISO 32000-1 §8.3.2.3), поэтому при дефолтной Resolution 72 проекция тождественна, а при 144 одна единица рисования — полпункта. Запись /UserUnit не пишется. Тот атрибут страницы, добавленный в PDF 1.6, — отдельная вещь, которую HotPDF выставляет как THPDFPage.SetUserUnit. Деталь, ловящая пришедших с TextOut-туториалов: координаты страницы отсчитываются от верхнего левого угла, Y растёт вниз, потому что YProjection вычисляет верх MediaBox минус масштабированный Y, — и это верно на любой Resolution

Как THotPDF.Resolution определяет единицу рисования в Delphi: сеттер хранит DocScale как Resolution, делённую на 72, затем XProjection, YProjection и SetFont делят каждую координату и размер по дороге в поток содержимого, так что Resolution 72 — тождественное отображение, а Resolution 144 делает единицу рисования полпункта, при том что страница по-прежнему отсчитывается от верхнего левого угла с Y вниз
В выходном файле ничего не сдвигается — меняется лишь смысл передаваемых чисел, поэтому один и тот же поток содержимого выходит и на 72, и на 144
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // 1 единица рисования = 1/144 дюйма
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // один дюйм в единицах рисования
    Page.SetFont('Arial', [fsBold], 28); // 28/144 дюйма, шрифт в 14 pt
    Title := 'INVOICE 2026-0417';
    // Правое выравнивание по краю страницы, меренному в той же единице
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // линейка в 1 pt
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Почему Page.Width расходится с координатами на Resolution 144?

THPDFPage.Width и Height отчитывают страницу в пунктах, что бы ни была Resolution документа, тогда как ваши координаты — в 1/Resolution дюйма, поэтому на 144 страница выглядит вдвое уже, чем есть. Страница A4 читается Width = 595 и Height = 842 на Resolution 72 и всё так же читается 595 и 842 на 144, где правый край на самом деле на X = 1190. UserWidth и UserHeight, добавленные в v2.766.0, возвращают Width * DocScale — размер страницы в той единице, которой вы рисуете. До их появления библиотека путала обе величины внутри, и симптомы на Resolution 144 были зрелищными: абзацы переносились после каждого символа, THPDFTable.Render выталкивал каждую строку на новую страницу, а HTML-импортёр и XFA-флаттенер рисовали содержимое вполовину, вжав сплющенную форму в верхний левый угол. Разметка абзацев, рендеринг таблиц, HTML-импорт, центрирование EMF, клип страницы WMF и диагностические замеры вёрстки теперь все читают размер в user-юнитах. Ваш код вёрстки должен тоже: всё, что сравнивается с координатой рисования (правое поле, тест на разрыв страницы, расчёт центрирования), принадлежит UserWidth и UserHeight и никогда — Width и Height

Ловушка первая: присвоение Width или Height переключает страницу на пункты

Установка Page.Width или Page.Height молча переводит страницу в UserDefined, а страница UserDefined игнорирует DocScale целиком, так что всё нарисованное на ней после — в пунктах, а не в 1/Resolution дюйма. Сеттер старый и по дизайну принимает пункты, поэтому его смысл не трогали. Проекция для страницы UserDefined — голое X + MinX, а SetFont хранит размер как есть. На Resolution 144 итог — страница, чьё содержимое внезапно выходит вдвое крупнее предыдущей страницы. Библиотека сама оступилась ровно тут: страницы продолжения абзацев копировали размер предыдущей страницы через Width, и каждая страница переполнения переключалась на пункты. Те страницы теперь копируют Size, Orientation и Resolution страницы, а к Width и Height откатываются, только когда исходная страница уже UserDefined

Два выхода — смотря что нужно. Если сойдёт стандартный лист, выставьте Page.Size и Page.Orientation и рисуйте дальше в своей единице Resolution. Если вам действительно нужен собственный размер страницы, примите, что это страница в пунктах, и рисуйте в пунктах; там UserWidth равен Width, так что код вёрстки, всегда читающий UserWidth, продолжает работать на обоих сортах страниц. Юнит-тест это прибивает: на Resolution 144 страница A4 отчитывает UserWidth 1190, но после Width := 500 и Height := 400 — 500 и 400. Загруженные страницы ведут себя так же, ведь страница, пересобранная из существующего PDF, знает только свой MediaBox в пунктах и рисует в пунктах. Страницы, созданные этим документом, хранят свои единицы, когда вы уходите и возвращаетесь через CurrentPageNumber, — так было с v2.766.26

Почему Page.Width расходится с координатами на Resolution 144 в HotPDF: Width и Height остаются в пунктах, тогда как рисование идёт в 1/144 дюйма, поэтому страница A4 читается как 595, а её правый край лежит на UserWidth 1190; присвоение Width переключает страницу в UserDefined, игнорирующий DocScale, поэтому абзацы переносятся по символу, таблицы ломаются по строке, а размеры SetFont ужимаются вдвое
Всё, что сравнивается с координатой рисования, принадлежит UserWidth и UserHeight — на UserDefined-странице в пунктах обе величины совпадают, так что один код вёрстки переживает обе

Ловушка вторая: почему размеры шрифтов выходят вполовину?

Размер шрифта, начавшийся пунктами, выходит вдвое меньше на Resolution 144, потому что SetFont принимает свой аргумент размера за единицы рисования и переводит его в пункты перед хранением. Внутри SetFont кладёт в текущий объект шрифта ASize / DocScale * DPI, так что сохранённое значение — всегда пункты. Библиотека оступалась тут дважды: font fallback в WideTextOutBoxEx и страница продолжения абзаца оба возвращали сохранённое пунктовое значение обратно в SetFont, который масштабировал его второй раз и ужимал текст. Ваш код сохранённый размер прочитать не может, но тот же баг всплывает всякий раз, когда пунктовое значение откуда-то ещё доезжает до SetFont: TFont.Size из VCL-формы, размер в описании отчёта, длина CSS pt. Сперва переведите, и включите в коэффициент собственную Resolution страницы и случай UserDefined, как делает воспроизведение метафайла, отыгрывая Canvas страницы (см. статью о том, как HotPDF импортирует векторную графику EMF и WMF):

// Единиц рисования на пункт на текущей странице. Повторяет проекцию
// HotPDF: 1 на странице, размеченной через Width/Height, иначе
// (Resolution документа / 72) * (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 в пунктах; SetFont ждёт единицы рисования
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

Библиотека применяет то же правило к собственным пунктовым константам. Шрифт в 12 пунктов, с которого стартует каждая новая страница, теперь умножается на внутренний коэффициент юнитов-на-пункт, так что это 12 пунктов на любой Resolution. DrawChart, чьи отступы, размеры подписей и толщины линий — жёстко вбитые пункты, теперь гоняется с масштабом, временно выставленным в 1. В единицах рисования, намеренно, остаются публичные дефолты параметров вроде размера модуля DrawQRCode и дефолтного размера табличного шрифта: они часть API-контракта, поэтому на Resolution 144 означают половину того, что на 72. Если вы размерируете отчёты по шаблону, в руководстве о выводе отчётов со шрифтами и изображениями в HotPDF разобрано, откуда эти значения обычно берутся

Как проверить, что вёрстка не зависит от Resolution?

Самая надёжная проверка — байтовое сравнение: отрендерите ту же страницу на Resolution 72 и снова на 144 с удвоенными координатами и размерами, и несжатые потоки содержимого обязаны совпасть. Оба прогона после проекции приземляются на одни пунктовые значения, так что любое расхождение — значение, проскочившее конверсию. Так тестовый набор HotPDF проверяет абзацы, таблицы, HTML-импорт, XFA-сплющивание, дуги, метафайлы и изображения. Та же техника работает и для вашего кода отчётов почти без обвязки:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // читаемые потоки содержимого
    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)
// обязаны дать побайтово идентичные потоки содержимого страницы
Как проверить независимость от Resolution в коде HotPDF под Delphi: отрендерите ту же вёрстку дважды — раз на Resolution 72 с масштабом 1, раз на 144 с удвоенными координатами и размерами шрифтов — и потребуйте побайтово идентичные несжатые потоки содержимого; расхождение указывает на страницу, переведённую в UserDefined через Width, или на непереведённое пунктовое значение, доехавшее до SetFont
Оба прогона после проекции приземляются на одни пунктовые значения, так что любое расхождение — число, проскочившее конверсию, — та же обвязка, на которой стоит тестовый набор HotPDF

Проверяйте операторы, несущие числа: Td, Tm, Tf, re, w и массивы TJ. Байты на уровне файла всё ещё разойдутся в дате создания и /ID, поэтому сравнивайте потоки, а не файлы целиком. Расхождение почти всегда указывает на одну из двух ловушек выше: страницу, переразмеренную через Width, или пунктовое значение, переданное прямо в SetFont. Если вы новичок в самих вызовах рисования, начните с разбора TextOut в HotPDF — размер, стиль и поворот, а потом возвращайтесь и переключайте Resolution, как только вёрстка читает UserWidth. Полные детали API и триальные загрузки — на странице компонента HotPDF Delphi PDF component