مقاله فنی

گزارش‌های PDF در Delphi با HotPDF: تابع TextOut، فونت‌ها و تصاویر

تولید یک گزارش در نهایت به قراردادن سه چیز روی یک صفحه و هم‌رأی‌کردن آن‌ها دربارهٔ جای قرارگیری‌شان خلاصه می‌شود: متن در مختصات مشخص، فونت‌هایی که روی سرور همان‌طور که روی دسکتاپ شما رندر می‌شوند، و تصاویری که اندازه‌شان درست جا می‌افتد. هر کار دیگری که یک کتابخانهٔ گزارش‌سازی انجام می‌دهد حول همین سه چیز چیده شده است. HotPDF، کتابخانهٔ تولید PDF از losLab برای Delphi و C++Builder، هرکدام از این‌ها را به‌صورت یک فراخوانی مستقیم روی شیء page در اختیارتان می‌گذارد، و تنها اصطکاک واقعی سیستم مختصات زیرین است، که برخلاف جهت canvas مربوط به VCL که به آن عادت دارید حرکت می‌کند. اول این جهت‌گیری را حل کنید تا بقیهٔ کار چیدمان دیگر با شما نجنگد

جایگذاری متن و مبدأ پایین-چپ

اولین گزارش تقریباً همهٔ افراد وارونه بیرون می‌آید. عنوان نزدیک لبهٔ پایین می‌نشیند و هر خط زیر آن به سمت بالا بالا می‌رود. هیچ‌چیز خراب نیست. فضای کاربر PDF، که در ISO 32000-1 §8.3 تعریف شده، مبدأ را در گوشهٔ پایین-چپ می‌گذارد با Y که به سمت بالا رشد می‌کند، که تصویر آینه‌ایِ canvas مربوط به GDI است که در آن Y از بالا-چپ به پایین رشد می‌کند. پنج دقیقه صرف‌کردن برای آشتی با این موضوع، چیدمانی را نجات می‌دهد که در غیر این‌صورت باید بعد از این‌که اعداد دیگر معنا نمی‌دهند دوباره بنویسید

دیاگرام HotPDF مقایسهٔ مبدأ مختصات بالا-چپ VCL با مبدأ پایین-چپ PDF؛ TextOut عنوانی را 50 پوینت از بالای صفحهٔ Letter در Y برابر 792 منهای 50 می‌نشاند
فضای کاربر PDF آینه VCL canvas است، پس عنوانی 50 نقطه از بالای صفحه Letter برابر است با TextOut(50, 792 - 50, 0, 'INVOICE') و همان تبدیل هر مختصات گزارش را شهودی نگه می‌دارد

فراخوانی محوری شیء page، TextOut(X, Y, Angle, Text) است. X و Y متن را بر حسب پوینت از گوشهٔ پایین-چپ مکان‌یابی می‌کنند، و Angle آن را بر حسب درجه می‌چرخاند، که همین‌طور یک مهر مورب DRAFT یا COPY بدون هیچ پشتیبانی ویژه‌ای رسم می‌شود. ترفندی که به شهود آموزش‌دیده با VCL اجازه می‌دهد کار کند، این است که Y را به‌صورت ارتفاع صفحه منهای فاصله‌ای که از بالا می‌خواهید بیان کنید:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // ۵۰ پوینت از بالای Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // مهر چرخانده‌شده
    Pdf.AddPage;                                                // CurrentPage اکنون به اینجا اشاره می‌کند
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // وضعیت فونت منتقل نمی‌شود
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

دو رفتار وضعیت‌دار (stateful) در آن قطعه‌کد مسئول بیشتر باگ‌هایی هستند که فقط در صفحهٔ دوم ظاهر می‌شوند. AddPage اشارهٔ CurrentPage را به صفحه‌ای که تازه ساخته تغییر می‌دهد، پس یک ارجاع صفحه که پیش‌تر کش کرده‌اید دیگر جایی که انتظار دارید رسم نمی‌کند. انتخاب فونت هم به ازای هر صفحه است، نه به ازای کل سند. اگر پس از یک AddPage، فراخوانی SetFont را رد کنید، اولین TextOut روی صفحهٔ تازه به هر پیش‌فرضی که صفحه با آن شروع شده برمی‌گردد، نه فونت بولد عنوانی که سه صفحه قبل تنظیم کرده بودید. عادت امن این است که «شروع یک صفحهٔ جدید» و «بازبرقراری وضعیت متن» را به‌عنوان یک گام جداناپذیر در حلقهٔ گزارش در نظر بگیرید

فونت‌هایی که روی سرور هم وجود دارند، نه فقط روی دسکتاپ شما

بیشتر مشکلات فونت در واقع مشکلات استقرار (deployment) هستند که لباس مبدل پوشیده‌اند. دستگاه توسعهٔ شما فونت شرکتی را نصب دارد، پس گزارش روی صفحهٔ شما درست به‌نظر می‌رسد و ارسال می‌شود. میزبان تولید (production) کار را زیر یک حساب سرویس اجرا می‌کند که هرگز آن فونت را نصب نداشته، رندرکننده بی‌سروصدا چیزی را که می‌تواند پیدا کند جایگزین می‌کند، و اولین بار که کسی متوجه می‌شود، مشتری‌ای است که می‌پرسد چرا سربرگ عوض شده. راه بیرون‌رفتن این است که دیگر به پوشهٔ فونت سیستم‌عامل اعتماد نکنید و فونت را از فایلی که installer شما روی دیسک می‌گذارد بارگذاری کنید. فراخوانی ثبت یونیکد HotPDF یک مسیر می‌گیرد و دقیقاً همین کار را انجام می‌دهد:

دیاگرام مسئلهٔ استقرار فونت PDF در Delphi؛ سرور عملیاتی فونت غایب را بی‌سروصدا جایگزین می‌کند، درحالی‌که RegisterUnicodeTTF فایل TTF را از فایل مستقر بار و در PDF تعبیه می‌کند
اتکا به دایرکتوری فونت سیستم‌عامل وقتی می‌شکند که حساب سرویس production فونت را نداشته باشد، در حالی که بارگذاری TTF از یک فایل مستقرشده گلیف‌ها را درون‌سازی می‌کند و هر میزبان یکسان رندر می‌کند
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));

TextOut مستقیماً یک WideString می‌پذیرد، که بیش از آنچه در نگاه اول به‌نظر می‌رسد اهمیت دارد. یک نام مشتری با تلفظ ویژه (accent)، یک خیابان آلمانی، یک شهر لهستانی: این‌ها موارد حاشیه‌ای نیستند، بلکه محتوای عادی یک جدول مشتریان‌اند، و از همان فراخوانی برچسب‌های ASCII که hard-code می‌کنید عبور می‌کنند، تا زمانی که فونت ثبت‌شده واقعاً گلیف‌ها را داشته باشد. یک محدودیت نسخه هم همراه فونت‌های embed‌شده می‌آید: سند باید PDF 1.5 یا جدیدتر باشد، پس اگر یک نیازمندی نامرتبط شما را به یک نسخهٔ قدیمی‌تر میخکوب کرده، همین همان چیزی است که بی‌سروصدا خراب می‌شود. اسکریپت‌های راست‌به‌چپ مانند عربی و عبری به شکل‌دهی (shaping) واقعی نیاز دارند نه یک جستجوی مستقیم گلیف، و این یک خط‌لولهٔ مخصوص خودش دارد؛ مقالهٔ ما دربارهٔ شکل‌دهی متن اسکریپت‌های پیچیده با HotPDF را ببینید

وقتی هیچ فونت نصب‌شده‌ای نمی‌تواند چیزی را که نیاز دارید بیان کند - مثلاً کاراکترهای MICR روی یک چک یا یک مجموعه نماد اختصاصی - فونت‌های Type 3 این شکاف را پر می‌کنند. هر گلیف را به‌صورت یک content stream کوچک از طریق RegisterType3Font و AddType3Glyph تعریف می‌کنید. این یک گوشهٔ تخصصی از API است و به‌ندرت سراغش می‌روید، اما بسیار تمیزتر از پخش‌کردن صدها bitmap کوچک نماد در سرتاسر یک صفحه است

تصاویر: آرگومان‌های میانی عرض و ارتفاع‌اند، نه یک گوشه

مدیریت تصویر به دو مرحله تقسیم می‌شود، و جدا نگه‌داشتن آن‌ها کل نکته است. AddImage یک TBitmap یا TJPEGImage می‌گیرد، آن را یک‌بار embed می‌کند، و یک اندیس برمی‌گرداند. تصاویر PNG باید پیش از رسیدن به آن‌جا به یک bitmap رمزگشایی شوند. سپس ShowImage آن اندیس را هرجا و هر تعداد بار که بخواهید رسم می‌کند. ترتیب آرگومان‌های ShowImage همان‌جایی است که ارزش دارد آهسته‌تر بخوانید:

دیاگرام خط لولهٔ تصویر HotPDF؛ AddImage بیت‌مپ را یک‌بار تعبیه و اندیسی برمی‌گرداند، ShowImage آن را با عرض و ارتفاع می‌نشاند و ترتیب آرگومان‌ها جفت گوشه نیست
AddImage پیکسل‌ها را یک بار درون‌سازی می‌کند و هر فراخوانی ShowImage همان اندیس را دوباره به‌کار می‌گیرد، و آرگومان‌های میانی یک width و height هستند نه مختصات گوشه مقابل
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // رمزگشایی PNG به یک bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // بدون افت کیفیت برای تصاویر با رنگ تخت
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle) — نه (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

دو عدد بعد از موقعیت، یک عرض و یک ارتفاع‌اند. آن‌ها مختصات گوشهٔ مقابل نیستند، و آرگومان انتهایی یک زاویهٔ چرخش بر حسب درجه است. اگر امضا را به‌صورت یک جعبهٔ X1/Y1/X2/Y2 بخوانید، یک لوگوی ۱۲۰ در ۴۰ که در (۵۰, ۷۰۰) قرار گرفته، به‌جای آن از آن‌جا تا (۱۲۰, ۴۰) کشیده می‌شود و روی بیشتر صفحه پخش می‌شود. خروجی این اشتباه را آشکار می‌کند در حالی که کد منبع کاملاً معقول به‌نظر می‌رسد، و همین باعث می‌شود یک بعدازظهر تلف شود. KeepImageAspectRatio پیش‌فرض True دارد، پس یک جعبه با نسبت‌های اشتباه تصویر را letterbox می‌کند به‌جای این‌که تحریفش کند؛ فقط زمانی آن را به False تغییر دهید که واقعاً قصد کشیدن (stretch) دارید

جداکردن ثبت از جایگذاری، در کارهای طولانی جواب می‌دهد. چون AddImage پیکسل‌ها را یک‌بار embed می‌کند و هر ShowImage با آن اندیس به همان شیء embed‌شده اشاره می‌کند، جایی که AddImage را فراخوانی می‌کنید حجم فایل را تعیین می‌کند. آن را درون حلقهٔ صفحه برای یک صورت‌حساب ۵۰۰ صفحه‌ای فراخوانی کنید و همان لوگو ۵۰۰ بار embed می‌شود. آن را یک‌بار پیش از حلقه فراخوانی کنید، اندیس را نگه دارید، و لوگو فقط یک‌بار ذخیره می‌شود. یک دیکشنری کوچک که با مسیر asset کلید خورده کافی است تا مطمئن شوید هر تصویر متمایز دقیقاً یک‌بار ثبت می‌شود

انتخاب codec اهرم دیگر حجم است. محتوای عکاسی‌شده، مثل پیوست‌های اسکن‌شده و مشابه آن‌ها، جایش در JPEG است: icJpeg را به AddImage بدهید و JpegQuality را حدود ۸۵ پایین بیاورید، چون این property از ۱۰۰ شروع می‌شود و تفاوت در ۸۵ روی یک صفحهٔ چاپی نامرئی است. آثار هنری با رنگ تخت مانند لوگوها، نمودارها و ترسیم‌های خطی جایشان در icFlate است، جایی که فشرده‌سازی بدون افت از قبل فشرده است و JPEG دور لبه‌های سخت یک ringing قابل‌مشاهده پخش می‌کند. یک اجرای صورت‌حساب که روی هر صفحه یک عکس با کیفیت کامل می‌گذارد می‌تواند تا چند گیگابایت متورم شود؛ همان محتوا با JPEG 85 حدود یک‌دهم آن حجم می‌نشیند، و هیچ خواننده‌ای متوجه نمی‌شود

خط‌ها، جعبه‌ها و سایه‌زنی با ابتدایی‌های path

خط افقی زیر هدر یک جدول و جعبهٔ خاکستری پشت یک رقم مجموع نیازی نیست تصویر باشند. آن‌ها را به‌صورت وکتور رسم کنید تا در هر بزرگ‌نمایی واضح بمانند، چاپ تیز داشته باشند، و تقریباً چیزی به حجم فایل اضافه نکنند. HotPDF از همان مدلی پیروی می‌کند که content streamهای خام PDF استفاده می‌کنند: یک path بسازید، سپس یک operator را فراخوانی کنید که آن را رنگ‌آمیزی می‌کند

// خط افقی زیر هدر جدول
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// جعبه سایه‌دار مجموع: X، Y، عرض، ارتفاع
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

این ترتیب اختیاری نیست: وضعیت رنگ را تنظیم کنید، path را بسازید، سپس Stroke یا Fill را فراخوانی کنید. یک path که می‌سازید اما هرگز رنگ‌آمیزی نمی‌کنید هیچ چیزی به صفحه اضافه نمی‌کند، که تقریباً همیشه پاسخ این است که چرا یک خط «نمایش داده نمی‌شود». SetRGBFillColor یک TColor واحد می‌گیرد، پس ثابت‌های آشنای VCL مانند clNavy و clBlack مستقیماً جا می‌افتند، و Rectangle از همان آرگومان‌های عرض-و-ارتفاع مکان‌گذاری تصویر استفاده می‌کند، نه دو گوشه. یک هشدار دربارهٔ خطوط نازک: هر چیزی زیر تقریباً نیم پوینت می‌تواند روی مانیتور شیک به‌نظر برسد و بعد روی یک پرینتر اداری ۶۰۰ dpi ناپدید شود، پس ۰٫۷۵ پوینت یک کف معقول برای هر خطی است که باید در برابر چاپ‌شدن دوام بیاورد

صفحه‌بندی در برابر داده‌های واقعی، نه داده‌های نمونه

یک جزئیات که باید پیش از قطعی‌شدن چیدمان درست انجام شود: ستون‌های عددی باید روی لبهٔ راست خود تراز شوند، و راه انجام این کار این است که عرض رندرشدهٔ هر مقدار را اندازه بگیرید و آن را از مرز ستون به عقب مکان‌گذاری کنید، نه این‌که رشته را با فاصله‌های پیشرو پد کنید. پدکردن با فاصله فقط در یک فونت monospaced تراز می‌شود، و هیچ‌کس یک گزارش مالی را با فونت monospaced تنظیم نمی‌کند. ابتدا مقادیر را از میان روتین‌های آگاه به locale در Delphi مانند FormatFloat عبور دهید، تا جداکنندهٔ هزارگانی که عرضش را اندازه می‌گیرید همانی باشد که locale مشتری واقعاً نمایش خواهد داد

خطر صفحه‌بندی این است که آن را در برابر مجموعه‌دادهٔ نمایشی می‌نویسید، جایی که ده ردیف کوتاه در یک صفحه جا می‌شوند و حلقه هرگز مجبور به شکستن نیست. تولید (production) مشتری‌ای به شما می‌دهد که نام شرکتش ۱۴۰ کاراکتر طول دارد و یک صورت‌حساب با ۴۰۰۰ ردیف جزئیات، و حالا حلقه باید هر بار درست بشکند. الگویی که دوام می‌آورد یک نشانگر Y واحد است که با کم‌کردن ارتفاع هر ردیف به سمت پایین حرکت می‌کند، و یک بررسی که همان لحظه‌ای که نشانگر می‌خواهد از حاشیهٔ پایین عبور کند یک صفحهٔ جدید شروع می‌کند. «پایین» در این‌جا یعنی کاهش Y، که همان‌جایی است که مبدأ پایین-چپ همچنان خلاف شهود باقی می‌ماند. همهٔ این را در یک روتین نگه دارید که همچنین SetFont را دوباره صادر می‌کند و هدر جاری را روی صفحهٔ جدید دوباره رسم می‌کند، و باگ‌های off-by-one-page هرگز پایگاهی پیدا نمی‌کنند. وقتی همان گزارش‌ها باید قوانین آرشیوی یا دسترس‌پذیری را هم رعایت کنند، انتخاب‌هایی که همین‌جا می‌کنید - چه فونت‌هایی را embed می‌کنید، آیا خروجی tagged است، از چه فضاهای رنگی استفاده می‌کنید - همان‌هایی‌اند که آن استانداردها کنترل می‌کنند؛ راهنمای PDF/A، PDF/X و PDF/UA در HotPDF پیش از این‌که قالب سفت شود، ارزش خواندن دارد

هر فراخوانی‌ای که این‌جا نشان داده شد - مکان‌گذاری متن، ثبت فونت، embed‌کردن تصویر و رسم path - در HotPDF Delphi Component برای Delphi و C++Builder ارائه می‌شود، که مرجع آن کل API خروجی را به‌همراه ویژگی‌های فرم، رمزگذاری و امضایی که در کنارش قرار دارند مستند می‌کند