Technical Article

Конвертиране на RTF в PDF в Delphi с PDF библиотеката на losLab

RTF форматът съществува достатъчно дълго, за да се появи на места, за които никой не е планирал: стари генератори на отчети, конвейери за поща по шаблон, архиви с правни документи, създадени преди появата на съвременните текстообработващи програми. Конвертирането му в PDF в движение е повтаряща се задача, и подходът, който реално работи под Windows, не е специализиран RTF парсер, а пътят за изобразяване, предоставен директно от самия Windows чрез TRichEdit и EM_FORMATRANGE. DLL изданието на PDF библиотеката на losLab излага виртуален контекст на устройство, който се вписва директно в тази верига.

Механизмът: виртуален DC и EM_FORMATRANGE

Rich Edit контролите могат да разпределят съдържанието си за всеки контекст на устройство (DC), не само за физически принтер. Съобщението EM_FORMATRANGE нарежда на контролата да наредобрази диапазон от знаци в даден DC и връща позицията на последния знак, който е успяла да вмести. Извиквайте го многократно, увеличавайки cpMin всеки път, и получавате изход страница по страница. Методът GetCanvasDC на PDF библиотеката на losLab предоставя DC в паметта, оразмерен спрямо зададените от вас размери на страницата. След изобразяването на страница в него, LoadFromCanvasDc улавя резултата като PDF страница. Това е цялата верига за обработка.

Едно нещо, което трябва да се направи правилно от самото начало: контролата TRichEdit трябва да бъде оразмерена да съответства на целевата страница. Ако контролата е по-малка или по-голяма от размерите на DC, разпределението на страниците няма да съвпадне с крайния PDF. За изход в А4 стандартният подход е да зададете пикселните размери на контролата така, че да съответстват на 210 x 297 мм при 96 DPI, преди да заредите RTF файла, като използвате същите спомагателни функции за мащабиране, с които ще оразмерите DC.

Имплементация в 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 и я предава на Rich Edit контролата чрез SendMessage. Контролата изобразява знаците, започвайки от cpMin, спира, когато DC се запълни, и връща позицията на първия знак, който не е вместен. Когато върнатата стойност е равна на или надвишава общата дължина на текста, всички знаци са изобразени и функцията връща нула, което прекратява цикъла repeat...until.

Всяка итерация генерира един PDF файл с имена Output1.pdf, Output2.pdf и т.н. Ако искате вместо това един многостраничен документ, API за добавяне на страници на библиотеката ви позволява да ги сглобите след това, или можете да преструктурирате цикъла, за да извиква AddPage в рамките на една сесия на документа. Моделът на SaveToFile, следван от RemovePdfDocument при всяка итерация, ограничава пиковата памет до съдържанието на една страница, което е важно при много дълги RTF файлове.

Детайли за оразмеряването, които объркват разработчиците

Аргументът 96 DPI за LoadFromCanvasDc указва на библиотеката при каква екранна разделителна способност е изобразен DC, за да може тя да изчисли правилното съответствие точки-пиксели за PDF страницата. Грешна стойност тук ще доведе до неправилен размер на текста в изходния файл, дори ако изображението изглежда правилно на екрана.

Добавеното +100 към RcPage.Right и RcPage.Bottom е малко поле извън видимия ръб на контролата. Rich Edit използва правоъгълника rcPage, за да реши къде да разделя страниците; без това поле, ред, попадащ точно на границата, може да се дублира в две страниц. Това не е магическа константа: стойността трябва да бъде достатъчно голяма, за да може границата на страницата да попада чисто вътре в областта на оформлението на контролата, а не на последния пиксел.

Освен това, контролата трябва вече да е прикрепена към видим прозорец на форма при изпълнение на FormCreate, за да бъде манипулаторът й на прозорец валиден преди първото извикване на SendMessage. TRichEdit, създаден динамично по време на изпълнение, се нуждае от изрично извикване на HandleNeeded преди началото на цикъла за изобразяване, ако формата не е показана все още.

Работа с шрифтове и функции на RTF

Тъй като изобразяването се извършва от механизма Windows Rich Edit, заместването на шрифтове следва същите правила, които той използва за показване и печат. Шрифтовете, посочени в RTF файла, инсталирани на машината, ще се изобразят вярно; липсващите шрифтове ще бъдат заменени безшумно, което може да промени дължините на редовете и разпределението на страниците. За производствено пакетно конвертиране това си струва да се тества изрично: заредете документ с всеки шрифт, използван от вашите RTF източници, и потвърдете, че броят на страниците в изходния файл съответства на очакваното при ръчен преглед преди печат.

Таблиците, вградените изображения и повечето функции за форматиране на Rich Text работят без допълнителна обработка, тъй като Rich Edit ги изобразява нативно. Областта, която може да бъде изненадваща, е текстът с персонализирано разстояние между абзаците или отстъпи на първия ред, изразени в туипове: вътрешната координатна система на Rich Edit е в туипове (1/1440 инча), докато DC координатите, зададени в TFormatRange, са в пиксели при текущото DPI. Контролата конвертира вътрешно, но ако изграждате RTF програмно, трябва да проверите дали стойностите на вашите полета са в правилната единица.

DPI съзнание и дисплеи с висок DPI

На дисплей, работещ при мащабиране 150% (144 DPI), ScaleX(210, mmPixel) ще върне по-голям брой пиксели, отколкото на 100% дисплей. PDF библиотеката записва пикселните размери, предадени на GetCanvasDC, и използва DPI аргумента в LoadFromCanvasDc, за да изчисли обратно физическия размер на страницата в PDF. Докато DPI стойността, която предавате, съответства на DPI, при което работи вашето приложение, размерът на изходната страница ще бъде правилен независимо от мащабирането на дисплея.

Ако вашето приложение не е DPI-съзнателно (старото поведение по подразбиране), Windows мащабира екранния DC и пикселните ви изчисления ще бъдат грешни на машини с висок DPI. Най-простото решение е да декларирате DPI съзнание в манифеста на приложението; тогава приложението получава истински пиксели на устройството и числото 96, предадено на LoadFromCanvasDc, трябва да се замени с действителния DPI на дисплея, получен от GetDeviceCaps(GetDC(0), LOGPIXELSX). Примерният код по-горе кодира твърдо 96, тъй като е подходящ за среда с мащабиране 100% и прави примера по-кратък.

Структура на изходния файл: по един файл на страница или обединен документ

Цикълът по-горе записва всяка страница в отделен PDF файл. Дали това е желаното поведение зависи от последващото използване. Системите за генериране на отчети често се нуждаят от отделни страници, тъй като по-късно сглобяват крайния документ чрез обединяване или пренареждане на страниците. Ако от самото начало искате един PDF, библиотеката ви позволява да създадете документ с множество страници в една сесия: създайте документа веднъж извън цикъла, извиквайте метода за добавяне на страница вместо SaveToFile вътре в цикъла и запазете пълния документ след края на цикъла. Това избягва междинните файлове и е правилната структура за повечето сценарии за конвертиране на единичен документ.

За големи RTF файлове си струва да добавите известна обратна връзка за напредъка в цикъла, тъй като скоростта на конвертиране е приблизително пропорционална на броя на страниците и документ от 200 страници може да отнеме няколко секунди. Структурата repeat...until е лесна за разширяване: проследявайте отместването на знаците в актуализация на лента за напредък след всяка итерация, използвайки LastChar, разделен на общия брой знаци от RichEdit1.GetTextLen.

Методите GetCanvasDC и LoadFromCanvasDc, показани тук, са част от PDF библиотеката на losLab за Delphi и C++Builder.