Формат RTF існує достатньо довго, щоб з'являтися там, де ніхто не планував: у застарілих генераторах звітів, конвеєрах злиття пошти, архівах юридичних документів, створених ще до появи сучасних текстових процесорів. Конвертація його у PDF на льоту є регулярною вимогою, і підхід, який дійсно працює на Windows, полягає не у використанні спеціалізованого парсера RTF, а у шляху рендерингу, який сама Windows вже надає через TRichEdit та EM_FORMATRANGE. DLL-редакція losLab PDF Library надає віртуальний контекст пристрою, який ідеально вписується в цей конвеєр
Механізм: віртуальний DC та EM_FORMATRANGE
Елементи керування Rich Edit можуть розбивати свій вміст на сторінки для будь-якого контексту пристрою, а не лише для фізичного принтера. Повідомлення EM_FORMATRANGE вказує елементу керування розмістити діапазон символів у заданому DC і повертає позицію останнього символу, який вдалося туди вмістити. Викликайте його багаторазово, щоразу просуваючи cpMin, і ви отримаєте посторінковий вивід. Метод GetCanvasDC від losLab PDF Library надає DC у пам'яті, розмір якого відповідає заданим вами розмірам сторінки; після рендерингу сторінки в нього LoadFromCanvasDc захоплює результат як сторінку PDF. У цьому і полягає весь конвеєр
Одне, що потрібно зробити правильно з самого початку: розмір елемента керування TRichEdit має відповідати цільовій сторінці. Якщо елемент керування менший або більший за розміри DC, розбиття на сторінки не співпадатиме з тим, що в результаті потрапить у PDF. Для виводу у форматі A4 стандартний підхід полягає у встановленні розмірів елемента керування в пікселях, що відповідають 210 х 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 awareness та дисплеї з високим DPI
На дисплеї, що працює з масштабуванням 150% (144 DPI), ScaleX(210, mmPixel) поверне більшу кількість пікселів, ніж на дисплеї 100%. Бібліотека PDF записує ті розміри в пікселях, які ви передаєте в GetCanvasDC, і використовує аргумент DPI у LoadFromCanvasDc для зворотного обчислення фізичного розміру сторінки в PDF. Поки значення DPI, яке ви передаєте, відповідає DPI, при якому працює ваш додаток, вихідний розмір сторінки буде правильним незалежно від масштабування дисплея
Якщо ваш додаток не має DPI awareness (старе налаштування за замовчуванням), Windows масштабує екранний DC, і ваші розрахунки пікселів будуть неправильними на машинах із високим DPI. Найпростіше виправлення полягає в оголошенні DPI awareness у маніфесті додатка; після цього додаток отримуватиме справжні пікселі пристрою, а число 96, яке ви передаєте в LoadFromCanvasDc, слід замінити на фактичний DPI дисплея, отриманий з GetDeviceCaps(GetDC(0), LOGPIXELSX). У наведеному вище прикладі коду жорстко закодовано 96, оскільки це підходить для середовища зі 100% масштабуванням і робить приклад коротким
Структура виводу: один файл на сторінку чи об'єднаний документ
Наведений вище цикл записує кожну сторінку в окремий файл PDF. Чи це саме те, що вам потрібно, залежить від подальшого використання. Системи генерації звітів часто потребують окремих сторінок, оскільки вони збирають остаточний документ пізніше шляхом об'єднання або зміни порядку сторінок. Якщо ви хочете отримати єдиний PDF від самого початку, бібліотека дозволяє створити документ із декількома сторінками за один сеанс: створіть документ один раз поза циклом, викликайте метод додавання сторінки замість SaveToFile всередині циклу, і збережіть повний документ після виходу з циклу. Це дозволяє уникнути створення проміжних файлів і є правильною структурою для більшості сценаріїв конвертації в єдиний документ
Для великих файлів RTF варто додати певний зворотний зв'язок щодо прогресу в циклі, оскільки швидкість конвертації приблизно пропорційна кількості сторінок, і документ на 200 сторінок може зайняти кілька секунд. Структуру repeat...until легко розширити: відстежуйте зсув символів в оновленні індикатора виконання після кожної ітерації, використовуючи LastChar, поділений на загальну кількість символів з RichEdit1.GetTextLen
Методи GetCanvasDC та LoadFromCanvasDc, показані тут, є частиною losLab PDF Library для Delphi та C++Builder