Format RTF istnieje już na tyle długo, że pojawia się w miejscach, których nikt nie planował: legacy generatory raportów, potoki mail merge, archiwa dokumentów prawnych starsze niż współczesne procesory tekstu. Konwersja do PDF w locie to powtarzające się wymaganie, a podejście, które faktycznie działa na Windows, to nie dedykowany parser RTF, tylko ścieżka renderowania, którą sam Windows dostarcza przez TRichEdit i EM_FORMATRANGE. losLab PDF Library wystawia wirtualny kontekst urządzenia, który wskakuje wprost w ten potok
Mechanizm: wirtualne DC i EM_FORMATRANGE
Kontrolki Rich Edit potrafią stronicować swoją zawartość dla dowolnego kontekstu urządzenia, nie tylko fizycznej drukarki. Komunikat EM_FORMATRANGE każe kontrolce ułożyć zakres znaków w podanym DC i zwraca pozycję ostatniego znaku, który zmieścił się. Wołaj go wielokrotnie, przesuwając cpMin za każdym razem, i dostajesz wyjście strona po stronie. GetCanvasDC w losLab PDF Library dostarcza w pamięci DC wymiarowany na dowolne wymiary strony, jakie podasz; po wyrenderowaniu strony LoadFromCanvasDC przechwytuje wynik jako nowy jednostronicowy dokument PDF. To cały potok
Jedna rzecz do ustalenia na starcie: kontrolka TRichEdit musi być wymiarowana pod stronę docelową. Jeśli kontrolka jest mniejsza albo większa niż wymiary DC, stronicowanie nie zgra się z tym, co wyląduje w PDF. Dla wyjścia A4 standardowe podejście to ustawienie pikselowych wymiarów kontrolki na 210 x 297 mm w tym samym DPI, które potem podasz do LoadFromCanvasDC, przed wczytaniem pliku RTF, z tą samą konwersją milimetrów na piksele, której używasz do wymiarowania DC
Implementacja w Delphi
Poniższy przykład używa modułu importu PDFlibAX_TLB, importu biblioteki typów dla edycji ActiveX biblioteki. Formularz gości TRichEdit i przycisk; handler OnCreate formularza wymiaruje kontrolkę i wczytuje RTF, a klik przycisku napędza pętlę konwersji
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;
// Dopasuj rozmiar kontrolki do A4 w DPI ekranu, żeby stronicowanie pasowało do 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
// Uzyskaj wirtualne DC wymiarowane na A4
Dc := PdfDoc.GetCanvasDC(
MulDiv(210, Screen.PixelsPerInch * 10, 254),
MulDiv(297, Screen.PixelsPerInch * 10, 254));
// Wyrenderuj kolejną stronę zawartości RTF do DC
LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
// Przechwyć zawartość DC jako dokument 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;
// Pozwól kontrolce wyrzucić dane formatowania, które zcache'owała dla 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 i rcPage są w twipsach (1/1440 cala), nie w pikselach: przelicz
// rozmiar strony tym samym DPI, które wymiarowało DC i kontrolkę
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; // rysuj na całej stronie; zmniejsz dla marginesów
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; // sygnalizuje ostatnią stronę
end;
end.
Co robi pętla
Funkcja PrintRtfBox wypełnia strukturę TFormatRange i przekazuje ją do kontrolki Rich Edit za pomocą SendMessage. Kontrolka renderuje znaki począwszy od cpMin, zatrzymując się w momencie zapełnienia DC, i zwraca pozycję pierwszego znaku, który się nie zmieścił. Gdy wartość zwracana jest równa lub większa od całkowitej długości tekstu, oznacza to, że każdy znak został wyrenderowany, a funkcja zwraca zero, co kończy pętlę repeat...until
Każda iteracja produkuje jeden plik PDF o nazwie Output1.pdf, Output2.pdf i tak dalej. Jeśli chcesz zamiast tego jeden wielostronicowy dokument, AddToFileList plus MergeFileList potrafią złożyć pliki per strona po fakcie, albo możesz przebudować pętlę tak, by każdy nowy dokument strony był wchodowany w pierwszy przez MergeDocument. Wzorzec SaveToFile, a potem RemoveDocument w każdej iteracji trzyma szczytową pamięć w granicach zawartości jednej strony, co ma znaczenie dla bardzo długich plików RTF
Szczegóły rozmiaru, z którymi ludzie mają problem
Argument DPI w LoadFromCanvasDC mówi bibliotece, ile twoich pikseli robi cal: strona wychodzi szeroka na szerokość canvas podzieloną przez DPI razy 72 punktów, i tak samo z wysokością. Pomyłka tutaj i tekst pojawi się w złym rozmiarze na wyjściu, mimo że obraz wygląda dobrze na ekranie
Jednostką rc i rcPage jest szczegół, który większość przykładów Delphi tej techniki psuje. EM_FORMATRANGE czyta oba prostokąty w twipsach, 1/1440 cala, choć DC, do którego rysuje, pracuje w pikselach. Wypełnij je wartościami pikselowymi, a kontrolka ułoży tekst na stronie piętnaście razy za małej przy 96 DPI: garść słów na przebieg i pętla, która zamienia dziesięciostronicowy dokument w setki prawie pustych PDF-ów. Konwersja przez MulDiv(Pixels, 1440, Dpi), z DPI, które wymiarowało DC, kładzie łamanie strony tam, gdzie canvas faktycznie się kończy. rcPage to cała strona; rc to obszar, jaki tekst może zająć, więc marginesy biorą się ze zmniejszenia rc wewnątrz niego, również w twipsach. Gdy pętla się kończy, jeszcze jedno EM_FORMATRANGE z nil lParam każe kontrolce uwolnić dane formatowania zcache'owane dla DC
Wreszcie, kontrolka musi być już dołączona do widocznego okna formularza w momencie wykonania FormCreate, aby jej uchwyt okna był prawidłowy przed pierwszym wywołaniem SendMessage. Kontrolka TRichEdit utworzona dynamicznie w czasie wykonywania wymaga jawnego wywołania HandleNeeded przed rozpoczęciem pętli renderowania, jeśli formularz nie został jeszcze pokazany
Obsługa czcionek i funkcji RTF
Ponieważ renderowanie jest wykonywane przez silnik Windows Rich Edit, zastępowanie czcionek przebiega według tych samych zasad, których używa on do wyświetlania i drukowania. Czcionki użyte w pliku RTF, które są zainstalowane na komputerze, zostaną wyrenderowane wiernie; brakujące czcionki zostaną cicho zastąpione, co może przesunąć długości linii i stronicowanie. W przypadku produkcyjnej konwersji wsadowej warto to dokładnie przetestować: załaduj dokument z każdym krojem pisma używanym przez źródła RTF i potwierdź, że liczba stron na wyjściu zgadza się z tym, czego oczekujesz z ręcznego podglądu wydruku
Tabele, osadzone obrazy i większość funkcji formatowania Rich Text działają bez dodatkowej obsługi, bo Rich Edit renderuje je natywnie. Obszar, który potrafi zaskoczyć, to tekst z własnymi odstępami akapitów albo wcięciami pierwszej linii wyrażonymi w twipsach: Rich Edit mierzy w twipsach (1/1440 cala), tej samej jednostce co prostokąty w TFormatRange, podczas gdy DC pod spodem pracuje w pikselach przy bieżącym DPI. Kontrolka mapuje jedno na drugie w trakcie rysowania, ale jeśli budujesz RTF programowo, zweryfikuj, że twoje wartości marginesów też są w twipsach
Świadomość DPI i ekrany o wysokiej rozdzielczości (High-DPI)
Na ekranie ze skalowaniem 150% (144 DPI) konwersja milimetrów na piksele zwraca większą liczbę pikseli niż na ekranie 100%. PDF Library zapamiętuje pikselowe wymiary, które podasz do GetCanvasDC, i używa argumentu DPI w LoadFromCanvasDC, żeby przeliczyć wstecz fizyczny rozmiar strony w PDF. Dopóki wartość DPI, którą podajesz, to ta, której użyłeś do zamiany milimetrów na piksele, rozmiar strony wyjściowej będzie poprawny niezależnie od skalowania ekranu
Jeśli twoja aplikacja nie jest świadoma DPI (stary domyślny stan), Windows skaluje ekranowe DC i twoje obliczenia pikseli będą złe na maszynach o wysokim DPI. Najprostsza łata to zadeklarowanie świadomości DPI w manifeście aplikacji; aplikacja dostaje wtedy prawdziwe piksele urządzenia, a wartość Screen.PixelsPerInch, którą przykład podaje do LoadFromCanvasDC, raportuje rzeczywiste DPI ekranu zamiast zakładać skalowanie 100%
Struktura wyjścia: jeden plik na stronę a połączony dokument
Pętla powyżej zapisuje każdą stronę do osobnego pliku PDF. Czy tego chcesz, zależy od użycia downstream. Systemy generowania raportów często potrzebują pojedynczych stron, bo składają finalny dokument później przez scalanie albo przestawianie stron. Jeśli chcesz jeden PDF od początku, biblioteka pozwala zbudować go w jednej sesji: zachowaj ID dokumentu, który tworzy pierwsze wywołanie LoadFromCanvasDC, i przy każdym kolejnym przebiegu wybierz go przez SelectDocument i zawołaj MergeDocument z ID dokumentu nowej strony zamiast SaveToFile, a potem zapisz raz po wyjściu z pętli. To omija pliki pośrednie i jest właściwą strukturą dla większości scenariuszy konwersji do jednego dokumentu
W przypadku dużych plików RTF warto dodać w pętli jakąś informację o postępie, ponieważ szybkość konwersji jest z grubsza proporcjonalna do liczby stron, a 200-stronicowy dokument może potrwać kilka sekund. Strukturę repeat...until można łatwo rozbudować: śledź przesunięcie znaków aktualizując pasek postępu po każdej iteracji, dzieląc LastChar przez całkowitą liczbę znaków uzyskaną za pomocą RichEdit1.GetTextLen
Metody GetCanvasDC i LoadFromCanvasDC pokazane tutaj są częścią losLab PDF Library dla Delphi i C++Buildera