RTF съществува достатъчно дълго, за да се появява на места, които никой не е планирал: стари генератори на отчети, конвейери за сливане на поща (mail merge pipelines), архиви с правни документи, които предхождат съвременните текстови процесори. Преобразуването му в PDF в движение е повтарящо се изискване, и подходът, който действително работи под Windows, не е специализиран RTF парсер, а пътят за рендиране, който самият Windows вече предоставя чрез TRichEdit и EM_FORMATRANGE. DLL изданието на losLab PDF Library предоставя виртуален контекст на устройството (virtual device context), който се вписва директно в този конвейер
Механизмът: виртуален DC и EM_FORMATRANGE
Контролите Rich Edit могат да страницират съдържанието си за всеки контекст на устройството (device context), не само за физически принтер. Съобщението EM_FORMATRANGE казва на контролата да разположи диапазон от символи в даден DC и връща позицията на последния символ, който е успяла да побере. Извиквайте го многократно, като придвижвате напред cpMin всеки път, и получавате изход страница по страница. Методът GetCanvasDC на losLab PDF Library предоставя DC в паметта, оразмерен до каквито и размери на страницата да посочите; след рендиране на страница в него, LoadFromCanvasDc улавя резултата като PDF страница. Това е целият конвейер
Едно нещо, което трябва да направите правилно от самото начало: контролата TRichEdit трябва да бъде оразмерена така, че да съответства на целевата страница. Ако контролата е по-малка или по-голяма от размерите на DC, страницирането няма да съвпадне с това, което попада в PDF файла. За A4 изход стандартният подход е да зададете размерите на контролата в пиксели да съответстват на 210 x 297 mm при 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, така че тя да може да изчисли правилното картографиране от точки към пиксели (point-to-pixel mapping) за PDF страницата. Сбъркайте това и текстът ще се появи с грешен размер в изхода, въпреки че изображението изглежда правилно на екрана
Стойността +100, добавена към RcPage.Right и RcPage.Bottom, е малък марж извън видимия ръб на контролата. Rich Edit използва правоъгълника rcPage, за да реши къде да раздели страниците; без маржа, ред, който попада точно на границата, може да бъде дублиран на две страници. Това не е магическа константа: искате той да бъде достатъчно голям, така че границата на страницата да попадне чисто вътре в областта за оформление на контролата, а не върху последния пиксел
И накрая, контролата вече трябва да е прикрепена към видим прозорец на форма (visible form window), когато се изпълнява FormCreate, така че нейният манипулатор на прозорец (window handle) да е валиден преди първото извикване на SendMessage. TRichEdit, създаден динамично по време на изпълнение, се нуждае от изрично извикване на HandleNeeded, преди цикълът на рендиране да започне, ако формата все още не е показана
Работа с шрифтове и функции на RTF
Тъй като рендирането се извършва от машината Windows Rich Edit, заместването на шрифтове следва същите правила, които тя използва за показване и печат. Шрифтовете, споменати в RTF файла, които са инсталирани на машината, ще се рендират вярно; липсващите шрифтове ще бъдат заменени тихо, което може да промени дължините на редовете и страницирането. За производствено групово преобразуване (production batch conversion) това си струва да се тества изрично: заредете документ с всеки шрифт, който вашите RTF източници използват, и потвърдете, че броят на страниците в изхода съвпада с това, което очаквате от ръчен преглед преди печат (print preview)
Таблиците, вградените изображения и повечето функции за форматиране на Rich Text работят без допълнителна обработка, защото Rich Edit ги рендира нативно. Единствената област, която може да бъде изненадваща, е текстът, който използва персонализирано разстояние между абзаците или отстъпи на първия ред, изразени в туипове (twips): вътрешната координатна система на Rich Edit е в туипове (1/1440 инча), докато координатите на DC, които задавате в TFormatRange, са в пиксели при текущия DPI. Контролата преобразува вътрешно, но ако конструирате RTF програмно, трябва да потвърдите, че стойностите на маржовете ви са в правилната единица
DPI осведоменост и дисплеи с висок DPI
На дисплей, работещ при 150% мащабиране (144 DPI), ScaleX(210, mmPixel) ще върне по-голям брой пиксели, отколкото на 100% дисплей. PDF Library записва каквито и размери в пиксели да предадете на GetCanvasDC и използва аргумента DPI в LoadFromCanvasDc, за да изчисли обратно физическия размер на страницата в PDF файла. Докато стойността на DPI, която предавате, съвпада с DPI, при който работи вашето приложение, размерът на изходната страница ще бъде правилен независимо от мащабирането на дисплея
Ако вашето приложение е DPI-неосведомено (DPI-unaware, старият стандарт), Windows мащабира екранния DC и вашите изчисления на пиксели ще бъдат грешни на машини с висок DPI. Най-простото решение е да декларирате DPI осведоменост в манифеста на приложението; след това приложението получава истински пиксели на устройството и 96, които предавате на LoadFromCanvasDc, трябва да бъдат заменени с действителния DPI на дисплея, получен от GetDeviceCaps(GetDC(0), LOGPIXELSX). Примерът с код по-горе твърдо кодира 96, защото е подходящо за 100% среда на мащабиране и запазва примера кратък
Структура на изхода: един файл на страница срещу комбиниран документ
Цикълът по-горе записва всяка страница в отделен PDF файл. Дали това е, което искате, зависи от по-нататъшната употреба. Системите за генериране на отчети често се нуждаят от отделни страници, защото те сглобяват окончателния документ по-късно чрез сливане или пренареждане на страници. Ако искате един PDF от самото начало, библиотеката ви позволява да създадете документ с множество страници в една сесия: създайте документа веднъж извън цикъла, извикайте метода за добавяне на страница вместо SaveToFile вътре в цикъла и запазете пълния документ след излизане от цикъла. Това избягва междинните файлове и е правилната структура за повечето сценарии за преобразуване на един документ
За големи RTF файлове си струва да добавите обратна връзка за напредъка в цикъла, тъй като скоростта на преобразуване е приблизително пропорционална на броя на страниците и документ от 200 страници може да отнеме няколко секунди. Структурата repeat...until е лесна за разширяване: проследявайте отместването на символите в актуализация на лентата за напредък (progress bar) след всяка итерация, използвайки LastChar, разделено на общия брой символи от RichEdit1.GetTextLen
Методите GetCanvasDC и LoadFromCanvasDc, показани тук, са част от losLab PDF Library за Delphi и C++Builder