Artykuł techniczny

Konwersja RTF do PDF w Delphi z biblioteką losLab PDF Library

Format RTF istnieje już na tyle długo, że pojawia się w miejscach, których nikt nie planował: w starszych generatorach raportów, potokach korespondencji seryjnej, archiwach dokumentów prawnych, które poprzedzają współczesne procesory tekstu. Konwersja do PDF w locie jest powtarzającym się wymaganiem, a podejściem, które faktycznie sprawdza się w systemie Windows, nie jest dedykowany parser RTF, ale ścieżka renderowania, którą sam Windows już zapewnia poprzez TRichEdit i EM_FORMATRANGE. Edycja DLL biblioteki losLab PDF Library udostępnia wirtualny kontekst urządzenia, który pasuje bezpośrednio do tego potoku

Mechanizm: wirtualne DC i EM_FORMATRANGE

Kontrolki Rich Edit mogą stronicować swoją zawartość dla dowolnego kontekstu urządzenia (DC), nie tylko fizycznej drukarki. Komunikat EM_FORMATRANGE instruuje kontrolkę, aby sformatowała zakres znaków do danego DC i zwraca pozycję ostatniego znaku, który udało się zmieścić. Wywołując to wielokrotnie, za każdym razem przesuwając cpMin, otrzymujemy dane wyjściowe strona po stronie. Funkcja GetCanvasDC z biblioteki losLab PDF Library zapewnia kontekst DC w pamięci o wymiarach strony, jakie określisz; po wyrenderowaniu do niego strony, LoadFromCanvasDc przechwytuje wynik jako stronę PDF. To jest cały potok

Jedna rzecz, którą należy ustalić na samym początku: kontrolka TRichEdit musi być dopasowana rozmiarem do docelowej strony. Jeśli kontrolka jest mniejsza lub większa niż wymiary DC, stronicowanie nie będzie odpowiadać temu, co znajdzie się w dokumencie PDF. Dla formatu wyjściowego A4 standardowym podejściem jest ustawienie wymiarów kontrolki w pikselach tak, aby odpowiadały 210 x 297 mm przy 96 DPI przed załadowaniem pliku RTF, używając tych samych funkcji pomocniczych do skalowania, co do rozmiaru DC

Implementacja w Delphi

Poniższy przykład wykorzystuje moduł importu PDFlibAX_TLB, który opakowuje edycję DLL biblioteki. Formularz zawiera TRichEdit i przycisk; procedura obsługi zdarzenia OnCreate formularza ustawia rozmiar kontrolki i ładuje RTF, a kliknięcie przycisku steruje 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: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // Dopasuj rozmiar kontrolki do A4 w DPI ekranu, aby stronicowanie odpowiadało 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
    // Uzyskaj wirtualne DC o rozmiarze A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Renderuj kolejną stronę zawartości RTF do DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Przechwyć zawartość DC jako dokument PDF
    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;  // 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 tworzy jeden plik PDF o nazwie Output1.pdf, Output2.pdf i tak dalej. Jeśli wolisz uzyskać jeden wielostronicowy dokument, interfejs API biblioteki do dołączania stron pozwala na ich złożenie po fakcie, albo można przebudować pętlę tak, aby wywoływać AddPage w ramach pojedynczej sesji dokumentu. Zastosowany powyżej wzorzec iteracyjnego wywoływania SaveToFile i RemovePdfDocument utrzymuje szczytowe zużycie pamięci na poziomie równym wartości zawartości jednej strony, co ma znaczenie w przypadku bardzo długich plików RTF

Szczegóły rozmiaru, z którymi ludzie mają problem

Argument 96 DPI w LoadFromCanvasDc mówi bibliotece, przy jakiej rozdzielczości ekranu wyrenderowano DC, dzięki czemu może ona obliczyć poprawne mapowanie punktów na piksele dla strony PDF. Jeśli to pominiesz lub podasz źle, tekst pojawi się w złym rozmiarze w pliku wyjściowym, nawet jeśli obraz na ekranie wygląda poprawnie

Wartość +100 dodana do RcPage.Right i RcPage.Bottom to niewielki margines poza widoczną krawędzią kontrolki. Rich Edit używa prostokąta rcPage, aby zdecydować, gdzie podzielić strony; bez tego marginesu linia, która wypada dokładnie na granicy, może zostać zduplikowana na dwóch stronach. To nie jest magiczna stała: chcesz, aby margines był na tyle duży, aby granica strony wypadła wyraźnie wewnątrz obszaru układu kontrolki, a nie na ostatnim pikselu

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 żadnej dodatkowej obsługi, ponieważ Rich Edit renderuje je natywnie. Jedynym obszarem, który może być zaskakujący, jest tekst z niestandardowymi odstępami akapitów lub wcięciami pierwszego wiersza wyrażonymi w twipach: wewnętrzny układ współrzędnych kontrolki Rich Edit to twipy (1/1440 cala), podczas gdy współrzędne DC ustawione w TFormatRange są w pikselach przy bieżącym DPI. Kontrolka konwertuje to wewnętrznie, ale jeśli konstruujesz RTF programowo, powinieneś sprawdzić, czy wartości marginesów są w odpowiedniej jednostce

Świadomość DPI i ekrany o wysokiej rozdzielczości (High-DPI)

Na ekranie ze skalowaniem ustawionym na 150% (144 DPI) funkcja ScaleX(210, mmPixel) zwróci większą liczbę pikseli niż na ekranie o współczynniku 100%. Biblioteka PDF rejestruje, jakie wymiary w pikselach przekazujesz do GetCanvasDC, i używa argumentu DPI w LoadFromCanvasDc do odwrotnego obliczenia fizycznego rozmiaru strony w dokumencie PDF. Dopóki wartość DPI, którą podajesz, odpowiada wartości DPI, z którą działa twoja aplikacja, rozmiar strony na wyjściu będzie prawidłowy, niezależnie od skalowania ekranu

Jeśli twoja aplikacja nie jest świadoma DPI (stare domyślne zachowanie), system Windows skaluje DC ekranu, a obliczenia pikseli będą błędne na urządzeniach o wysokiej rozdzielczości. Najprostszą poprawką jest zadeklarowanie świadomości DPI w manifeście aplikacji; aplikacja otrzymuje wtedy rzeczywiste piksele urządzenia, a wartość 96 przekazywaną do LoadFromCanvasDc należy zastąpić rzeczywistym DPI wyświetlacza uzyskanym za pomocą GetDeviceCaps(GetDC(0), LOGPIXELSX). Powyższy przykład kodu przypisuje na stałe 96, ponieważ jest to odpowiednie dla środowiska o skalowaniu 100% i pozwala utrzymać krótki przykład

Struktura wyjściowa: jeden plik na stronę a połączony dokument

Powyższa pętla zapisuje każdą stronę w osobnym pliku PDF. To, czy tego właśnie chcesz, zależy od tego, jak zostaną użyte później. Systemy generowania raportów często wymagają pojedynczych stron, ponieważ finalny dokument składają później przez łączenie lub zmianę kolejności. Jeśli od razu wolisz uzyskać jeden plik PDF, biblioteka pozwala na utworzenie dokumentu z wieloma stronami podczas jednej sesji: utwórz dokument raz przed pętlą, wewnątrz pętli wywołaj metodę dodawania strony zamiast SaveToFile, a po wyjściu z pętli zapisz gotowy, pełny dokument. Pozwala to uniknąć plików pośrednich i jest odpowiednią strukturą dla większości scenariuszy konwersji do pojedynczego 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 oraz LoadFromCanvasDc przedstawione w tym przykładzie stanowią część biblioteki losLab PDF Library dla Delphi i C++Builder