Artykuł techniczny

Konwersja RTF do PDF w Delphi z biblioteką losLab PDF

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

PDF: potok RTF do PDF: wymiarowana kontrolka Rich Edit stronicuje swój tekst do wirtualnego canvas DC przez EM_FORMATRANGE, a każdy przebieg jest przechwytywany jako jedna strona PDF
EM_FORMATRANGE układa zakres tekstu RTF w wirtualnym canvas DC, a LoadFromCanvasDC przechwytuje wynik, powtarzając, aż dojdzie do ostatniego znaku

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

PDF: Zagnieżdżone prostokąty rcPage i rc dla EM_FORMATRANGE, oba podane w twipsach przeliczonych z pikseli canvas w DPI, które wymiarowało wirtualne DC
rcPage to cała strona, a rc obszar, którego tekst może używać; oba idą do EM_FORMATRANGE w twipsach, podczas gdy DC pod spodem zostaje w pikselach

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

PDF: Wybór między zapisem jednego pliku PDF na przebieg do późniejszego składania a budowaniem pojedynczego wielostronicowego PDF przez doklejanie stron wewnątrz pętli
Ta sama pętla renderowania karmi albo osobne pliki Output, albo jeden rosnący dokument, wymieniając elastyczność downstream na pliki pośrednie

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