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