مقاله فنی

تبدیل RTF به PDF در Delphi با losLab PDF Library

فرمت RTF به اندازه کافی قدیمی است که در مکان‌هایی ظاهر شود که هیچ‌کس برایشان برنامه‌ریزی نکرده است: تولیدکننده‌های گزارش قدیمی، خطوط لوله ادغام پستی، بایگانی‌های اسناد حقوقی که به قبل از واژه‌پردازهای مدرن برمی‌گردند. تبدیل در لحظه آن به PDF یک نیاز مکرر است، و رویکردی که در ویندوز واقعاً کار می‌کند، یک تجزیه‌کننده اختصاصی RTF نیست، بلکه مسیر رندرینگی است که خود ویندوز از طریق TRichEdit و EM_FORMATRANGE فراهم می‌کند. نسخه DLL کتابخانه losLab PDF Library یک زمینه دستگاه (device context) مجازی را در معرض قرار می‌دهد که مستقیماً در این خط لوله قرار می‌گیرد

مکانیزم: DC مجازی و EM_FORMATRANGE

کنترل‌های Rich Edit می‌توانند محتوای خود را برای هر زمینه دستگاهی صفحه‌بندی کنند، نه فقط یک چاپگر فیزیکی. پیام EM_FORMATRANGE به کنترل می‌گوید محدوده‌ای از کاراکترها را در یک DC طرح‌بندی کند و موقعیت آخرین کاراکتری را که موفق به جای دادن آن شده است برمی‌گرداند. این پیام را به طور مکرر فراخوانی کنید و در هر بار cpMin را جلو ببرید تا خروجی صفحه به صفحه را دریافت کنید. متد GetCanvasDC در losLab PDF Library یک DC درون‌حافظه‌ای با اندازه‌ای که برای ابعاد صفحه مشخص می‌کنید ارائه می‌دهد؛ پس از رندر کردن یک صفحه در آن، LoadFromCanvasDc نتیجه را به عنوان یک صفحه PDF ضبط می‌کند. این تمام خط لوله است

یک چیز که از قبل باید به درستی تنظیم شود: کنترل TRichEdit باید به گونه‌ای اندازه‌بندی شود که با صفحه هدف مطابقت داشته باشد. اگر کنترل کوچک‌تر یا بزرگ‌تر از ابعاد DC باشد، صفحه‌بندی با آنچه در نهایت در PDF قرار می‌گیرد مطابقت نخواهد داشت. برای خروجی A4 رویکرد استاندارد این است که قبل از بارگیری فایل RTF، با استفاده از همان کمک‌کننده‌های مقیاسی که برای اندازه‌بندی DC استفاده خواهید کرد، ابعاد پیکسلی کنترل را به گونه‌ای تنظیم کنید که با 210 در 297 میلی‌متر در 96 DPI مطابقت داشته باشد

پیاده‌سازی Delphi

کد زیر از واحد واردسازی PDFlibAX_TLB استفاده می‌کند، که نسخه DLL کتابخانه را دربرمی‌گیرد. فرم میزبان یک TRichEdit و یک دکمه است؛ کنترل‌کننده OnCreate فرم، کنترل را اندازه‌بندی و RTF را بارگیری می‌کند، و کلیک دکمه حلقه تبدیل را هدایت می‌کند

unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;

type
  TForm1 = class(TForm)
    RichEdit1: TRichEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
      FirstChar: Integer): Integer;
  end;

var
  Form1: TForm1;
  PdfDoc: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // Size the control to A4 at screen DPI so pagination matches the DC
  RichEdit1.Width  := Round(ScaleX(210, mmPixel));
  RichEdit1.Height := Round(ScaleY(297, mmPixel));
  RichEdit1.Lines.LoadFromFile(
    ExtractFilePath(Application.ExeName) + 'document.rtf');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Dc: HDC;
  PageNumber, LastChar, PdfDocId: Integer;
begin
  PageNumber := 1;
  LastChar   := 0;
  repeat
    // Obtain a virtual DC sized to A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Render the next page of RTF content into the DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Capture the DC contents as a PDF document
    PdfDoc.LoadFromCanvasDc(96, 0);
    PdfDocId := PdfDoc.SelectedPdfDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemovePdfDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  RcDrawTo, RcPage: TRect;
  Fr: TFormatRange;
  NextCharPosition: Integer;
begin
  RcPage.Left   := 0;
  RcPage.Top    := 0;
  RcPage.Right  := rtfBox.Left + rtfBox.Width  + 100;
  RcPage.Bottom := rtfBox.Top  + rtfBox.Height + 100;

  RcDrawTo.Left   := rtfBox.Left;
  RcDrawTo.Top    := rtfBox.Top;
  RcDrawTo.Right  := rtfBox.Left + rtfBox.Width;
  RcDrawTo.Bottom := rtfBox.Top  + rtfBox.Height;

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rc          := RcDrawTo;
  Fr.rcPage      := RcPage;
  Fr.chrg.cpMin  := FirstChar;
  Fr.chrg.cpMax  := -1;

  NextCharPosition :=
    SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
  if NextCharPosition < Length(rtfBox.Text) then
    Result := NextCharPosition
  else
    Result := 0;  // signals last page
end;

end.

حلقه چه کاری انجام می‌دهد

تابع PrintRtfBox ساختار TFormatRange را پر می‌کند و آن را از طریق SendMessage به کنترل Rich Edit ارسال می‌کند. این کنترل کاراکترها را با شروع از cpMin رندر می‌کند، زمانی که DC پر می‌شود متوقف شده و موقعیت اولین کاراکتری را که جا نشده است برمی‌گرداند. وقتی مقدار بازگشتی برابر یا بیشتر از طول کل متن باشد، تک‌تک کاراکترها رندر شده‌اند و تابع صفر برمی‌گرداند، که به حلقه repeat...until پایان می‌دهد

هر تکرار یک فایل PDF به نام‌های Output1.pdf، Output2.pdf و غیره تولید می‌کند. اگر به جای آن یک سند چندصفحه‌ای واحد می‌خواهید، API افزودن صفحه کتابخانه به شما اجازه می‌دهد تا آن‌ها را پس از واقعیت (after the fact) مونتاژ کنید، یا می‌توانید ساختار حلقه را تغییر دهید تا AddPage را درون یک جلسه (session) سند واحد فراخوانی کند. الگوی SaveToFile در هر تکرار و به دنبال آن RemovePdfDocument در بالا، حداکثر حافظه را محدود به محتوای یک صفحه نگه می‌دارد، که برای فایل‌های بسیار طولانی RTF مهم است

جزئیات اندازه‌بندی که افراد را دچار مشکل می‌کند

آرگومان 96 DPI در LoadFromCanvasDc به کتابخانه می‌گوید که DC با چه رزولوشن صفحه‌ای رندر شده است، تا بتواند نگاشت صحیح point به pixel را برای صفحه PDF محاسبه کند. اگر این کار را اشتباه انجام دهید، متن با وجود اینکه روی صفحه نمایش تصویر درست به نظر می‌رسد، با اندازه اشتباهی در خروجی ظاهر خواهد شد

اضافه شدن +100 به RcPage.Right و RcPage.Bottom یک حاشیه کوچک فراتر از لبه قابل‌مشاهده کنترل است. Rich Edit از مستطیل rcPage برای تصمیم‌گیری در مورد محل شکستن صفحات استفاده می‌کند؛ بدون این حاشیه، خطی که دقیقاً در مرز قرار می‌گیرد ممکن است در دو صفحه کپی شود. این یک ثابت جادویی نیست: شما می‌خواهید این حاشیه به اندازه‌ای بزرگ باشد که مرز صفحه به جای آخرین پیکسل، دقیقاً درون ناحیه طرح‌بندی کنترل قرار گیرد

در نهایت، این کنترل باید قبلاً به یک پنجره فرم قابل‌مشاهده متصل شده باشد زمانی که FormCreate اجرا می‌شود تا دستگیره پنجره آن قبل از اولین فراخوانی به SendMessage معتبر باشد. یک TRichEdit که به صورت پویا در زمان اجرا ایجاد می‌شود، اگر فرم هنوز نمایش داده نشده باشد به یک فراخوانی صریح HandleNeeded قبل از شروع حلقه رندر نیاز دارد

مدیریت فونت‌ها و ویژگی‌های RTF

از آنجایی که رندر توسط موتور Windows Rich Edit انجام می‌شود، جایگزینی فونت از همان قوانینی پیروی می‌کند که برای نمایش و چاپ استفاده می‌کند. فونت‌های ارجاع‌داده‌شده در فایل RTF که روی ماشین نصب شده‌اند با وفاداری رندر می‌شوند؛ فونت‌های مفقود بی‌سروصدا جایگزین می‌شوند، که می‌تواند طول خطوط و صفحه‌بندی را تغییر دهد. برای تبدیل دسته‌ای در محیط تولید ارزش آن را دارد که این موضوع صراحتاً آزمایش شود: یک سند را با هر تایپ‌فیسی که منابع RTF شما استفاده می‌کنند بارگیری کنید و تأیید کنید که تعداد صفحات خروجی با آنچه از یک پیش‌نمایش چاپ دستی انتظار دارید مطابقت دارد

جداول، تصاویر تعبیه‌شده و اکثر ویژگی‌های قالب‌بندی Rich Text بدون هیچ مدیریت اضافی کار می‌کنند زیرا Rich Edit آن‌ها را به صورت بومی رندر می‌کند. تنها حوزه‌ای که ممکن است غافلگیرکننده باشد، متنی است که از فاصله‌گذاری سفارشی پاراگراف یا تورفتگی خط اول که با واحد twip بیان شده‌اند استفاده می‌کند: سیستم مختصات داخلی Rich Edit در twip (1/1440 اینچ) است، در حالی که مختصات DC که شما در TFormatRange تنظیم می‌کنید در واحد پیکسل با DPI فعلی است. این کنترل تبدیل‌ها را به صورت داخلی انجام می‌دهد، اما اگر RTF را به صورت برنامه‌نویسی می‌سازید باید بررسی کنید که مقادیر حاشیه شما در واحد درستی باشند

آگاهی از DPI و نمایشگرهای high-DPI

در نمایشگری که با مقیاس 150٪ (144 DPI) اجرا می‌شود، ScaleX(210, mmPixel) تعداد پیکسل بزرگ‌تری را نسبت به یک نمایشگر 100٪ برمی‌گرداند. PDF Library هر ابعاد پیکسلی را که شما به GetCanvasDC می‌فرستید ثبت می‌کند و از آرگومان DPI در LoadFromCanvasDc برای محاسبه معکوس اندازه فیزیکی صفحه در PDF استفاده می‌کند. تا زمانی که مقدار DPI که ارسال می‌کنید با DPI که برنامه شما با آن در حال اجراست مطابقت داشته باشد، اندازه صفحه خروجی صرف‌نظر از مقیاس‌بندی نمایشگر درست خواهد بود

اگر برنامه شما DPI-unaware (ناآگاه از DPI - پیش‌فرض قدیمی) است، ویندوز DC صفحه را مقیاس‌بندی می‌کند و محاسبات پیکسلی شما روی ماشین‌های high-DPI اشتباه خواهد بود. ساده‌ترین راه‌حل اعلام آگاهی از DPI در مانیفست برنامه است؛ سپس برنامه پیکسل‌های واقعی دستگاه را دریافت می‌کند و عدد 96 که به LoadFromCanvasDc ارسال می‌کنید باید با DPI واقعی نمایشگر که از GetDeviceCaps(GetDC(0), LOGPIXELSX) به دست می‌آید جایگزین شود. در نمونه کد بالا 96 به صورت سخت‌کد نوشته شده است زیرا برای یک محیط مقیاس 100٪ مناسب است و مثال را کوتاه نگه می‌دارد

ساختار خروجی: یک فایل در هر صفحه در مقابل یک سند ترکیبی

حلقه بالا هر صفحه را در یک فایل PDF مجزا می‌نویسد. اینکه آیا این همان چیزی است که می‌خواهید بستگی به کاربرد پایین‌دستی آن دارد. سیستم‌های تولید گزارش اغلب به صفحات مجزا نیاز دارند زیرا در نهایت سند را بعداً با ادغام یا مرتب‌سازی مجدد صفحات مونتاژ می‌کنند. اگر از ابتدا یک PDF واحد می‌خواهید، کتابخانه به شما اجازه می‌دهد تا سندی با چندین صفحه در یک جلسه (session) واحد ایجاد کنید: سند را یک بار بیرون از حلقه ایجاد کنید، داخل حلقه متد افزودن صفحه را به جای SaveToFile فراخوانی کنید، و سند کامل را بعد از خروج از حلقه ذخیره کنید. این کار از ایجاد فایل‌های میانی جلوگیری می‌کند و ساختار مناسبی برای اکثر سناریوهای تبدیل تک‌سندی است

برای فایل‌های بزرگ RTF ارزش آن را دارد که در داخل حلقه مقداری بازخورد پیشرفت اضافه کنید، زیرا نرخ تبدیل تقریباً متناسب با تعداد صفحات است و یک سند 200 صفحه‌ای ممکن است چند ثانیه طول بکشد. گسترش ساختار repeat...until آسان است: آفست کاراکتر را در به‌روزرسانی نوار پیشرفت پس از هر تکرار ردیابی کنید، با استفاده از LastChar تقسیم بر تعداد کل کاراکترها از RichEdit1.GetTextLen

متدهای GetCanvasDC و LoadFromCanvasDc نشان داده شده در اینجا بخشی از losLab PDF Library برای Delphi و C++Builder هستند