مقاله فنی

Resolution در HotPDF در Delphi: واحدهای رسم و UserWidth

در HotPDF Component، THotPDF.Resolution واحد رسم را تعریف می‌کند: هر مختصات X و Y، هر حاشیه، اندازه‌ای که به SetFont می‌دهی، و نتایج TextWidth و GetWideTextWidth در 1/Resolution اینچ اندازه‌گیری می‌شوند. THPDFPage.Width و Height از آن پیروی نمی‌کنند و در نقطه می‌مانند، پس مرزهای چیدمان باید از UserWidth و UserHeight فقط‌خواندنی بیایند. دلیل معمول برای دست زدن به Resolution یک پورت است: یک موتور گزارش که از قبل به 1/96 یا 1/144 اینچ فکر می‌کند وقتی سمت PDF همان واحد را صحبت کند راحت‌تر منتقل می‌شود تا وقتی هر call site یک ضریب تبدیل بگیرد. این خوب کار می‌کند، به شرطی که بدانی کدام اعداد به واحد جدید رفته‌اند و کدام‌ها جا مانده‌اند

THotPDF.Resolution واقعاً چه چیزی را عوض می‌کند؟

THotPDF.Resolution فقط عوض می‌کند HotPDF اعدادی را که می‌دهی چطور بخواند؛ PDF ای که می‌نویسد همان است. setter دو خط است: SetResolution مقدار را ذخیره می‌کند و DocScale := Value / 72 را می‌گذارد. از آن به بعد، XProjection و YProjection هر مختصات را در مسیر ورود به content stream بر DocScale تقسیم می‌کنند و SetFont هم اندازه را به همان شکل قبل از ثبت تقسیم می‌کند. user space مربوط به PDF به‌صورت پیش‌فرض 1/72 اینچ است (ISO 32000-1 §8.3.2.3)، پس در Resolution پیش‌فرض یعنی 72 تصویر کردن همانی است و در 144 یک واحد رسم نصف نقطه است. هیچ درایهٔ /UserUnit ای نوشته نمی‌شود. آن attribute صفحه که در 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/144 اینچ
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: برابر Width = 595 و UserWidth = 1190
    Margin := 144;                       // یک اینچ در واحدهای رسم
    Page.SetFont('Arial', [fsBold], 28); // 28/144 اینچ، یک فونت 14 نقطه‌ای
    Title := 'INVOICE 2026-0417';
    // راست‌چین نسبت به لبهٔ صفحه اندازه‌گیری‌شده در همان واحد
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // خط 1 نقطه‌ای
    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 در Resolution 72 می‌خواند Width = 595 و Height = 842 و در 144 هم همان 595 و 842 را می‌خواند، جایی که لبهٔ راست واقعاً در X = 1190 است. UserWidth و UserHeight که در v2.766.0 اضافه شدند برمی‌گردانند Width * DocScale را، که اندازهٔ صفحه در واحدی است که با آن می‌کشی. قبل از اینکه وجود داشته باشند کتابخانه این دو را داخلی قاطی می‌کرد و علائم در Resolution 144 سینمایی بودند: پاراگراف‌ها بعد از هر نویسه می‌شکستند، THPDFTable.Render هر ردیف را به یک صفحهٔ جدید می‌فرستاد، و هم importer از HTML و هم flattener از XFA محتوایشان را در نصف اندازه می‌کشیدند، فرم تخت‌شده به گوشهٔ بالا-چپ ازدحام می‌کرد. چیدمان پاراگراف و رندر جدول و import از HTML و وسط‌چین کردن EMF و clip صفحهٔ WMF و تشخیص‌های چیدمان حالا همه اندازهٔ واحد-کاربر را می‌خوانند. کد چیدمان خودت هم باید همین‌طور باشد: هر چیزی که با یک مختصات رسم مقایسه می‌شود (یک حاشیهٔ راست، یک تست شکست صفحه، یک محاسبهٔ وسط‌چین) جایش روی 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 صفحه را کپی می‌کنند و فقط وقتی صفحهٔ اصلی از قبل UserDefined بوده به Width و Height برمی‌گردند

دو راه خروج، بسته به اینکه چه می‌خواهی. اگر یک برگهٔ استاندارد کفایت می‌کند، 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 را در شیء فونت جاری ذخیره می‌کند، پس مقدار ذخیره‌شده همیشه نقطه است. کتابخانه دو بار روی همین سکندری خورد: fallback فونت در WideTextOutBoxEx و صفحهٔ ادامهٔ پاراگراف هر دو آن مقدار نقطه‌ای ذخیره‌شده را به SetFont برمی‌گرداندند که بار دوم مقیاسش می‌کرد و متن را نصف می‌کرد. کد تو نمی‌تواند مقدار ذخیره‌شده را بخواند، اما همین باگ هر وقت یک مقدار نقطه‌ای از جای دیگر به SetFont برسد خودش را نشان می‌دهد: یک TFont.Size از یک فرم VCL، یک اندازه در تعریف گزارش، یک طول pt از CSS. اول تبدیلش کن و Resolution خود صفحه و حالت UserDefined را هم در ضریب بگنجان، همان‌طور که پخش metafile موقع پخش Canvas صفحه انجام می‌دهد (برای آن مسیر چگونگی import گرافیک برداری EMF و WMF در HotPDF را ببین):

// واحدهای رسم به‌ازای هر نقطه روی صفحهٔ جاری. آینهٔ همان تصویری
// است که HotPDF استفاده می‌کند: 1 روی صفحه‌ای که با Width/Height اندازه گرفته شده، وگرنه
// (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 در نقطه است؛ SetFont واحد رسم انتظار دارد
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

کتابخانه همان قاعده را روی ثابت‌های نقطه‌ای خودش اعمال می‌کند. فونت 12 نقطه‌ای که هر صفحهٔ جدید با آن شروع می‌شود حالا در ضریب داخلی واحد-به-ازای-نقطه ضرب می‌شود، پس در هر Resolution ای 12 نقطه است. DrawChart که حاشیه‌ها و اندازه‌های برچسب و ضخامت خط‌هایش همه نقطه‌های هاردکد هستند حالا با مقیاس موقتاً روی 1 اجرا می‌شود. آنچه عمداً در واحد رسم می‌ماند پیش‌فرض‌های پارامتر عمومی مثل اندازهٔ ماژول DrawQRCode و اندازهٔ فونت پیش‌فرض جدول است: بخشی از قرارداد API هستند، پس در Resolution 144 یعنی نصف آنچه در 72 معنی می‌دهند. اگر گزارش‌ها را از روی یک قالب اندازه می‌گیری، راهنمای خروجی گزارش با فونت و تصویر در HotPDF پوشش می‌دهد این مقادیر معمولاً از کجا می‌آیند

چطور راستی‌آزمایی کنی که یک چیدمان مستقل از Resolution است؟

قابل‌اتکاترین چک یک مقایسهٔ بایتی است: همان صفحه را در Resolution 72 و دوباره در 144 با هر مختصات و اندازه دو برابر رندر کن و content streamهای فشرده‌نشده باید یکسان باشند. هر دو اجرا بعد از تصویر شدن روی همان مقادیر نقطه‌ای فرود می‌آیند، پس هر تفاوتی یک مقداری است که تبدیل را رد کرده. سویت تست HotPDF پاراگراف‌ها و جدول‌ها و import از 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های فشرده‌نشده را بایت‌به‌بایت یکسان بخواه؛ یک عدم تطابق به صفحه‌ای اشاره می‌کند که از طریق Width به UserDefined رفته یا یک مقدار نقطه‌ای تبدیل‌نشده که به SetFont رسیده
هر دو اجرا بعد از تصویر شدن روی همان مقادیر نقطه‌ای فرود می‌آیند، پس هر تفاوتی عددی است که تبدیلش را رد کرده؛ همان هارنسی که سویت تست HotPDF به آن تکیه دارد

عملگرهایی که عدد حمل می‌کنند را چک کن: Td و Tm و Tf و re و w و آرایه‌های TJ. بایت‌های سطح فایل همچنان در تاریخ ساخت و /ID تفاوت خواهند داشت، پس streamها را مقایسه کن نه فایل‌های کامل را. یک عدم تطابق تقریباً همیشه به یکی از دو دام بالا اشاره می‌کند: صفحه‌ای که از طریق Width تغییر اندازه داده، یا یک مقدار نقطه‌ای که مستقیم به SetFont رفته. اگر با خود فراخوانی‌های رسم تازه‌کاری، از آموزش گام‌به‌گام TextOut در HotPDF برای اندازه، استایل و چرخش شروع کن، بعد برگرد و وقتی چیدمانت UserWidth را می‌خواند یک بار Resolution را سوئیچ کن. جزئیات کامل API و دانلودهای آزمایشی در صفحهٔ HotPDF Delphi PDF component است