Формат RTF существует достаточно долго, чтобы появляться там, где никто не планировал: в устаревших генераторах отчётов, конвейерах слияния почты, архивах юридических документов, которые предшествуют современным текстовым процессорам. Преобразование его в PDF на лету — частое требование, и подход, который действительно работает в Windows, заключается не в использовании специализированного парсера RTF, а в пути рендеринга, который сама Windows уже предоставляет через TRichEdit и EM_FORMATRANGE. Библиотека losLab PDF предоставляет виртуальный контекст устройства, который напрямую встраивается в этот конвейер
Механизм: виртуальный DC и EM_FORMATRANGE
Элементы управления Rich Edit могут разбивать своё содержимое на страницы для любого контекста устройства (DC), а не только для физического принтера. Сообщение EM_FORMATRANGE указывает элементу управления расположить диапазон символов в заданном DC и возвращает позицию последнего символа, который удалось поместить. Вызывайте его повторно, каждый раз увеличивая cpMin, и вы получите постраничный вывод. GetCanvasDC библиотеки losLab PDF предоставляет DC в памяти с размерами страницы, которые вы укажете; после рендеринга страницы в него LoadFromCanvasDC захватывает результат как новый одностраничный PDF-документ. Вот и весь конвейер
С самого начала нужно усвоить одну вещь: элемент управления TRichEdit должен быть изменён в размерах в соответствии с целевой страницей. Если элемент управления меньше или больше размеров DC, разбиение на страницы не будет совпадать с тем, что окажется в PDF. Для вывода в формате A4 стандартный подход заключается в установке пиксельных размеров элемента управления в соответствии с 210 x 297 мм при том же DPI, который вы позже передадите в LoadFromCanvasDC, перед загрузкой файла RTF, с тем же преобразованием миллиметров в пиксели, каким вы размеряете DC
Реализация на Delphi
Далее используется модуль импорта PDFlibAX_TLB — импорт библиотеки типов для ActiveX-редакции библиотеки. Форма содержит 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: IPDFlibrary;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
PdfDoc := CoPDFlibrary.Create;
// Задайте размер элемента как A4 при DPI экрана, чтобы разбивка на страницы совпадала с DC
RichEdit1.Width := MulDiv(210, Screen.PixelsPerInch * 10, 254);
RichEdit1.Height := MulDiv(297, Screen.PixelsPerInch * 10, 254);
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
// Получаем виртуальный DC с размером A4
Dc := PdfDoc.GetCanvasDC(
MulDiv(210, Screen.PixelsPerInch * 10, 254),
MulDiv(297, Screen.PixelsPerInch * 10, 254));
// Отрисовываем следующую страницу RTF-содержимого в DC
LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
// Захватываем содержимое DC как PDF-документ
PdfDoc.LoadFromCanvasDC(Screen.PixelsPerInch, 0);
PdfDocId := PdfDoc.SelectedDocument;
PdfDoc.SaveToFile(
ExtractFilePath(Application.ExeName)
+ 'Output' + IntToStr(PageNumber) + '.pdf');
PdfDoc.RemoveDocument(PdfDocId);
Inc(PageNumber);
until LastChar = 0;
// Позволяем элементу сбросить данные форматирования, закэшированные для DC
SendMessage(RichEdit1.Handle, EM_FORMATRANGE, 0, 0);
end;
function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
FirstChar: Integer): Integer;
var
Fr: TFormatRange;
Dpi, PageWidthTw, PageHeightTw, NextCharPosition: Integer;
begin
// rc и rcPage в твипах (1/1440 дюйма), не в пикселях: пересчитайте
// размер страницы с тем же DPI, которым размерились DC и элемент
Dpi := Screen.PixelsPerInch;
PageWidthTw := MulDiv(rtfBox.Width, 1440, Dpi);
PageHeightTw := MulDiv(rtfBox.Height, 1440, Dpi);
Fr.hdc := hDc;
Fr.hdcTarget := hDc;
Fr.rcPage := Rect(0, 0, PageWidthTw, PageHeightTw);
Fr.rc := Fr.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; // сигнализирует о последней странице
end;
end.
Что делает цикл
PrintRtfBox заполняет структуру TFormatRange и передает ее элементу управления Rich Edit через SendMessage. Элемент управления рендерит символы, начиная с cpMin, останавливаясь при заполнении DC, и возвращает позицию первого символа, который не поместился. Когда возвращаемое значение равно или превышает общую длину текста, каждый символ был отрендерен, и функция возвращает ноль, что завершает цикл repeat...until
Каждая итерация создаёт один PDF-файл с именами Output1.pdf, Output2.pdf и так далее. Если вместо этого вы хотите получить единый многостраничный документ, AddToFileList плюс MergeFileList могут собрать постраничные файлы постфактум, или вы можете перестроить цикл так, чтобы каждый новый одностраничный документ вливался в первый через MergeDocument. Паттерн SaveToFile с последующим RemoveDocument на каждой итерации, показанный выше, удерживает пиковое потребление памяти в пределах объёма содержимого одной страницы, что имеет значение для очень длинных файлов RTF
Детали изменения размеров, которые сбивают с толку
Аргумент DPI в LoadFromCanvasDC сообщает библиотеке, сколько ваших пикселей составляют дюйм: страница выходит шириной в ширину канвы, делённую на DPI и умноженную на 72 пункта, и так же по высоте. Ошибитесь — и текст в выводе появится неправильного размера, даже если изображение на экране выглядит правильно
Единица rc и rcPage — деталь, которую большинство Delphi-примеров этой техники берёт неверно. EM_FORMATRANGE читает оба прямоугольника в твипах, 1/1440 дюйма, хотя DC, в который он рисует, работает в пикселях. Заполните их пиксельными значениями, и элемент разложит текст на странице в пятнадцать раз меньше при 96 DPI: горстка слов за проход и цикл, превращающий документ в десять страниц в сотни почти пустых PDF. Преобразование через MulDiv(Pixels, 1440, Dpi) с тем DPI, которым размерился DC, кладёт разрыв страницы туда, где канва реально кончается. rcPage — полная страница; rc — область, которую может занимать текст, так что поля берутся из сжатия rc внутри неё, тоже в твипах. Когда цикл завершён, ещё один EM_FORMATRANGE с nil lParam велит элементу освободить данные форматирования, закэшированные для DC
Наконец, элемент управления должен быть уже прикреплен к видимому окну формы при выполнении FormCreate, чтобы его дескриптор окна был действительным до первого вызова SendMessage. Динамически созданный во время выполнения TRichEdit требует явного вызова HandleNeeded до начала цикла рендеринга, если форма еще не была показана
Работа со шрифтами и функциями RTF
Поскольку рендеринг выполняется движком Windows Rich Edit, подстановка шрифтов следует тем же правилам, которые он использует для отображения и печати. Шрифты, указанные в файле RTF и установленные на компьютере, будут отрендерены точно; отсутствующие шрифты будут заменены без уведомления, что может привести к изменению длины строк и разбиения на страницы. При производственном пакетном преобразовании это стоит протестировать отдельно: загрузите документ с каждым шрифтом, который используют ваши источники RTF, и убедитесь, что количество страниц на выходе совпадает с тем, что вы ожидаете от ручного предварительного просмотра
Таблицы, встроенные изображения и большинство функций форматирования Rich Text работают без дополнительной обработки, поскольку Rich Edit рендерит их изначально. Единственная область, которая может удивить, — это текст с настраиваемыми интервалами абзацев или отступами первой строки, выраженными в твипах: Rich Edit меряет в твипах (1/1440 дюйма) — тех же единицах, что и прямоугольники в TFormatRange, — тогда как лежащий под ним DC работает в пикселях при текущем DPI. Элемент отображает одно в другое при рисовании, но если вы формируете RTF программно, вам стоит убедиться, что ваши значения полей тоже в твипах
Учет DPI и дисплеи с высоким DPI
На дисплее с масштабированием 150% (144 DPI) преобразование миллиметров в пиксели вернёт большее количество пикселей, чем на дисплее с 100%. Библиотека PDF записывает любые пиксельные размеры, которые вы передаёте в GetCanvasDC, и использует аргумент DPI в LoadFromCanvasDC для обратного расчёта физического размера страницы в PDF. Пока значение DPI, которое вы передаёте, — то же, которым вы переводили миллиметры в пиксели, размер страницы на выходе будет правильным независимо от масштабирования дисплея
Если ваше приложение не учитывает DPI (прежнее поведение по умолчанию), Windows масштабирует экранный DC, и ваши расчёты пикселей будут неверными на машинах с высоким DPI. Самое простое решение — объявить поддержку DPI в манифесте приложения; тогда приложение получает реальные пиксели устройства, а значение Screen.PixelsPerInch, которое пример передаёт в LoadFromCanvasDC, отчитывает реальный DPI дисплея вместо допущения 100% масштабирования
Структура вывода: один файл на страницу против объединенного документа
Приведённый выше цикл записывает каждую страницу в отдельный PDF-файл. Нужно ли вам именно это, определяется последующим использованием. Системы генерации отчётов часто нуждаются в отдельных страницах, поскольку собирают финальный документ позже объединением или перестановкой страниц. Если вы хотите получить единый PDF с самого начала, библиотека позволяет собрать его за один сеанс: сохраните ID документа, который создаёт первый вызов LoadFromCanvasDC, и на каждом следующем проходе выбирайте его через SelectDocument и вызывайте MergeDocument с ID документа новой страницы вместо SaveToFile, а после выхода из цикла сохраните один раз. Это позволяет избежать промежуточных файлов и является правильной структурой для большинства сценариев преобразования в один документ
Для больших файлов RTF стоит добавить обратную связь о прогрессе в цикле, так как скорость преобразования примерно пропорциональна количеству страниц, и документ на 200 страниц может занять несколько секунд. Структуру repeat...until легко расширить: отслеживайте смещение символа в обновлении индикатора прогресса после каждой итерации, используя значение LastChar, деленное на общее количество символов из RichEdit1.GetTextLen
Показанные здесь методы GetCanvasDC и LoadFromCanvasDC являются частью библиотеки losLab PDF для Delphi и C++Builder