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

HotPDF Resolution в Delphi: единици за чертане и UserWidth

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

Какво всъщност променя THotPDF.Resolution?

THotPDF.Resolution променя само това как HotPDF чете числата, които подавате; PDF-ът, който записва, е същият. Setter-ът е два реда: SetResolution пази стойността и задава DocScale := Value / 72. Оттам нататък XProjection и YProjection делят всяка координата на DocScale по пътя към content stream-а, а SetFont дели размера по същия начин, преди да го запише. PDF user space по подразбиране е 1/72 инч (ISO 32000-1 §8.3.2.3), така че при подразбиращата се Resolution 72 проекцията е идентична, а при 144 една единица за чертане е половин точка. Никакъв запис /UserUnit не се записва. Този page атрибут, добавен в PDF 1.6, е отделно нещо, което HotPDF излага като THPDFPage.SetUserUnit. Една подробност, хващаща хората от TextOut уроците: координатите на страницата тръгват от горния ляв ъгъл с Y, растящ надолу, защото YProjection смята върха на MediaBox минус мащабираното Y, и това важи при всяка Resolution

Как THotPDF.Resolution дефинира единицата за чертане в Delphi: setter-ът пази DocScale като Resolution, разделена на 72, после XProjection, YProjection и SetFont делят всяка координата и размер по пътя към content stream-а, така че Resolution 72 е идентично мапване, а Resolution 144 прави една единица за чертане половин точка, докато страницата си тръгва от горния ляв ъгъл с Y надолу
Нищо в изходния файл не се мести — сменя се само значението на числата, които подавате, затова същият content stream се появява и при 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 клипът на страницата и диагностиката на подредбата вече всички четат размера в потребителски единици. И вашият код за подредба трябва: всичко, което сравнява с координата за чертане (десен margin, тест за пренасяне на страница, сметка за центриране), принадлежи на UserWidth и UserHeight, никога на Width и Height

Капан първи: задаването на Width или Height превключва страницата на пунктове

Задаването на Page.Width или Page.Height тихо превръща страницата в UserDefined, а UserDefined страница игнорира DocScale изцяло, така че всичко, което рисувате върху нея след това, е в пунктове, а не в 1/Resolution инч. Setter-ът е стар и приема пунктове нарочно, затова значението му беше оставено на мира. Проекцията за UserDefined страница е просто X + MinX, а SetFont пази размера непроменен. При Resolution 144 резултатът е страница, чието съдържание изведнъж излиза два пъти по-голямо от страницата преди нея. Библиотеката сама направи точно тази грешка: страниците за продължение на абзац копираха размера на предишната страница през Width и всяка страница с преливане превключваше на пунктове. Тези страници вече копират Size, Orientation и Resolution на страницата, а се връщат към Width и Height само когато оригиналната страница вече е била UserDefined

Два изхода, според това какво ви трябва. Ако стандартен лист свърши работа, задайте Page.Size и Page.Orientation и си чертете в единицата на вашата Resolution. Ако наистина ви трябва персонален размер на страница, приемете, че това е страница в пунктове, и чертайте в пунктове; UserWidth там е равно на Width, така че код за подредба, който винаги чете UserWidth, продължава да работи и на двата вида страница. Unit тестът го закова: при 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 обект, така че записаната стойност е винаги в пунктове. Библиотеката се спъна в това два пъти: font fallback-ът в WideTextOutBoxEx и страницата за продължение на абзац подадоха тази записана пунктовa стойност обратно на SetFont, който я мащабира втори път и преполови текста. Вашият код не може да прочете записания размер, но същият бъг излиза наяве всеки път, когато пунктовa стойност отвънка достигне SetFont: TFont.Size от VCL форма, размер в дефиниция на отчет, CSS дължина pt. Превърнете го първо и включете в фактора собствената Resolution на страницата и случая UserDefined, както прави възпроизвеждането на metafile, когато изпълнява отново 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, чиито margins, размери на етикети и дебелини на линии са всички hard-коднати пунктове, вече се пуска със scale, временно зададен на 1. Това, което остава в единици за чертане нарочно, са подразбиращите се стойности на публични параметри като размера на модула на DrawQRCode и подразбиращият се размер на шрифта на таблица: те са част от API договора, така че при Resolution 144 значат половината от това, което значат при 72. Ако оразмерявате отчети от шаблон, ръководството за отчетен изход със шрифтове и изображения в HotPDF покрива откъде обикновено идват тези стойности

Как проверявате, че подредбата е независима от Resolution?

Най-надеждната проверка е сравнение на байтове: рендирайте същата страница при Resolution 72 и пак при 144 с всяка координата и размер удвоени, и некомпресираните content stream-ове трябва да са идентични. И двата пуска стигат до същите стойности в пунктове след проекцията, така че всяка разлика е стойност, прескачила преобразуването. Така тестовият набор на HotPDF проверява абзаци, таблици, HTML импорт, XFA изравняване, дъги, metafile-и и изображения. Същата техника работи и за вашия собствен отчетен код почти без тестова обвивка:

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 stream-ове
    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) и RenderPage('r144.pdf', 144, 2)
// трябва да дадат байт-идентични content stream-ове на страницата
Как да проверите независимостта от Resolution в HotPDF Delphi код: рендирайте еднаквата подредба два пъти, веднъж при Resolution 72 с мащаб 1 и веднъж при 144 с всяка координата и размер на шрифт удвоени, после изисквайте байт-идентични некомпресирани content stream-ове — разминаване сочи страница, превключена на UserDefined чрез Width, или непревърната стойност в пунктове, стигнала SetFont
И двата пуска стигат до същите стойности в пунктове след проекцията, така че всяка разлика е число, прескачало преобразуването си — същата тестова скела, на която разчита тестовият набор на HotPDF

Проверете операторите, които носят числа: Td, Tm, Tf, re, w и масивите TJ. Байтовете на ниво файл пак ще се разминат в датата на създаване и /ID, затова сравнявайте stream-овете, не цели файлове. Разминаване сочи почти винаги някой от двата капана по-горе: страница, преоразмерена през Width, или стойност в пунктове, подадена направо в SetFont. Ако сте нови в самите извиквания за чертане, започнете с упражнението TextOut на HotPDF за размер, стил и ротация, после се върнете и превключете Resolution, щом подредбата ви чете UserWidth. Пълните API подробности и trial изтегляния са на страницата на HotPDF Delphi PDF компонента