Műszaki cikk

RTF-ből PDF-be konvertálás Delphi-ben a losLab PDF Library segítségével

Az RTF már elég régóta létezik ahhoz, hogy olyan helyeken is megjelenjen, ahol senki sem tervezte: régi jelentésgenerátorokban (legacy report generators), körlevél (mail merge) csővezetékekben, jogi dokumentumarchívumokban, amelyek megelőzik a modern szövegszerkesztőket. Menet közbeni PDF-be konvertálása visszatérő követelmény, és az a megközelítés, amely ténylegesen működik a Windows rendszeren, nem egy dedikált RTF-elemző (parser), hanem az a renderelési útvonal, amelyet maga a Windows már biztosít a TRichEdit és az EM_FORMATRANGE révén. A losLab PDF Library DLL kiadása egy virtuális eszközkontextust tesz elérhetővé, amely közvetlenül illeszkedik ebbe a csővezetékbe

A mechanizmus: virtuális DC és EM_FORMATRANGE

A Rich Edit vezérlők képesek tördelni a tartalmukat bármilyen eszközkontextushoz, nem csak egy fizikai nyomtatóhoz. Az EM_FORMATRANGE üzenet arra utasítja a vezérlőt, hogy helyezzen el egy karaktertartományt egy adott DC-ben, és visszaadja az utolsó karakter pozícióját, amelyet sikerült beillesztenie. Hívja meg ismételten, minden alkalommal előreléptetve a cpMin-t, és oldalankénti kimenetet kap. A losLab PDF Library GetCanvasDC-je egy memóriabeli DC-t biztosít, amely a megadott oldalméretekhez van igazítva; miután egy oldalt renderelt bele, a LoadFromCanvasDc PDF-oldalként rögzíti az eredményt. Ez az egész csővezeték

Egy dolgot azonnal tisztázni kell: a TRichEdit vezérlőt a céloldalnak megfelelően kell méretezni. Ha a vezérlő kisebb vagy nagyobb, mint a DC méretei, a tördelés nem fog egyezni azzal, ami a PDF-be kerül. A4-es kimenet esetén a szabványos megközelítés az, hogy a vezérlő pixelméreteit 210 x 297 mm-nek megfelelően állítjuk be 96 DPI-n az RTF-fájl betöltése előtt, ugyanazokat a méretezési segédprogramokat használva, amelyeket a DC méretezéséhez használ

Delphi implementáció

A következőkben a PDFlibAX_TLB importáló egységet (import unit) használjuk, amely a könyvtár DLL kiadását csomagolja be. Az űrlap egy TRichEdit-et és egy gombot tartalmaz; az űrlap OnCreate kezelője méretezi a vezérlőt és betölti az RTF-et, a gombkattintás pedig hajtja a konverziós ciklust

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.

Mit csinál a ciklus

A PrintRtfBox kitölti a TFormatRange struktúrát, és átadja a Rich Edit vezérlőnek a SendMessage-en keresztül. A vezérlő a cpMin-nél kezdve rendereli a karaktereket, megáll, amikor a DC megtelik, és visszaadja az első karakter pozícióját, amely nem fért be. Amikor a visszatérési érték eléri vagy meghaladja a teljes szöveghosszt, minden karakter renderelve lett, és a függvény nullát ad vissza, ami befejezi a repeat...until ciklust

Minden iteráció egy-egy PDF-fájlt hoz létre, amelyek neve Output1.pdf, Output2.pdf és így tovább. Ha ehelyett egyetlen többoldalas dokumentumot szeretne, a könyvtár oldal-hozzáfűzési (page-append) API-ja lehetővé teszi, hogy utólag összeállítsa őket, vagy átstrukturálhatja a ciklust úgy, hogy az AddPage-t hívja meg egyetlen dokumentum-munkameneten belül. A fenti iterációnkénti SaveToFile, majd az azt követő RemovePdfDocument minta egy oldalnyi tartalomnál tartja a csúcs memóriát (peak memory), ami a nagyon hosszú RTF-fájlok esetében számít

Méretezési részletek, amelyek buktatót jelenthetnek

A LoadFromCanvasDc 96 DPI argumentuma megmondja a könyvtárnak, hogy a DC milyen képernyőfelbontásban lett renderelve, így ki tudja számítani a PDF oldal helyes pont-pixel leképezését (point-to-pixel mapping). Ha ezt elrontja, a szöveg rossz méretben jelenik meg a kimeneten, annak ellenére, hogy a kép a képernyőn helyesnek tűnik

Az RcPage.Right és az RcPage.Bottom értékéhez hozzáadott +100 egy kis margó a vezérlő látható szélén túl. A Rich Edit az rcPage rect (téglalapot) használja annak eldöntésére, hogy hol vágja ketté az oldalakat; a margó nélkül egy olyan sor, amely pontosan a határon esik, duplikálódhat két oldalon. Ez nem egy mágikus konstans: elég nagynak kell lennie ahhoz, hogy az oldalhatár tisztán a vezérlő elrendezési területére (layout area) essen, ne pedig az utolsó pixelre

Végül, a vezérlőt már hozzá kell csatolni egy látható űrlap ablakhoz (form window), amikor a FormCreate lefut, hogy az ablakleírója (window handle) érvényes legyen a SendMessage első hívása előtt. A futásidőben dinamikusan létrehozott TRichEdit-nek egy kifejezett HandleNeeded hívásra van szüksége a renderelési ciklus megkezdése előtt, ha az űrlap még nem jelent meg

Betűtípusok és RTF funkciók kezelése

Mivel a renderelést a Windows Rich Edit motor (engine) végzi, a betűtípus helyettesítés (font substitution) ugyanazokat a szabályokat követi, amelyeket a megjelenítéshez és a nyomtatáshoz használ. Az RTF fájlban hivatkozott betűtípusok, amelyek telepítve vannak a gépen, hűen fognak megjelenni; a hiányzó betűtípusok csendben helyettesítve lesznek, ami eltolhatja a sorok hosszát és a tördelést. Az éles (production) kötegelt konverzió esetén ezt érdemes kifejezetten tesztelni: töltsön be egy dokumentumot az RTF források által használt összes betűtípussal, és ellenőrizze, hogy a kimeneti oldalszám megegyezik-e a kézi nyomtatási előnézetből várttal

A táblázatok, a beágyazott képek és a legtöbb Rich Text formázási funkció minden extra kezelés nélkül működik, mert a Rich Edit natívan rendereli őket. Az egyetlen terület, ami meglepő lehet, a twips-ben kifejezett egyéni bekezdésközöket (paragraph spacing) vagy első sor behúzásokat (first-line indents) használó szöveg: a Rich Edit belső koordináta rendszere twips-ben (1/1440 hüvelyk) van, míg a TFormatRange-ben beállított DC koordináták pixelekben az aktuális DPI-n. A vezérlő belsőleg konvertál, de ha programozottan állítja össze az RTF-et, ellenőriznie kell, hogy a margó értékei a megfelelő mértékegységben vannak-e

DPI tudatosság és nagy DPI-s (high-DPI) kijelzők

Egy 150%-os (144 DPI) léptékkel futó kijelzőn a ScaleX(210, mmPixel) nagyobb pixelszámot ad vissza, mint egy 100%-os kijelzőn. A PDF Library rögzíti, hogy milyen pixelméreteket ad át a GetCanvasDC-nek, és a LoadFromCanvasDc DPI argumentumát használja a PDF fizikai oldalméretének visszaszámítására. Amíg a megadott DPI érték megegyezik a DPI-vel, amelyen az alkalmazás fut, a kimeneti oldal mérete helyes lesz, függetlenül a kijelző léptékezésétől

Ha az alkalmazás nem DPI tudatos (DPI-unaware, a régi alapértelmezés), a Windows átméretezi a képernyő DC-jét, és a pixelszámítások hibásak lesznek a magas DPI-s gépeken. A legegyszerűbb javítás, ha a DPI-tudatosságot (DPI awareness) az alkalmazás jegyzékében (application manifest) deklarálja; az alkalmazás ezután valódi eszközpixeleket (device pixels) kap, és a LoadFromCanvasDc-nek átadott 96-ot ki kell cserélni a tényleges kijelző DPI-vel, amelyet a GetDeviceCaps(GetDC(0), LOGPIXELSX) segítségével szerez meg. A fenti kódpélda mereven 96-ot (hardcode) használ, mert az megfelelő a 100%-os léptékezési környezethez, és a példát röviden tartja

Kimeneti struktúra: oldalanként egy fájl kontra kombinált dokumentum

A fenti ciklus minden oldalt külön PDF fájlba ír. Hogy ez az-e, amit szeretne, a későbbi (downstream) felhasználástól függ. A jelentésgeneráló rendszerek gyakran igényelnek egyedi oldalakat, mert a végső dokumentumot később az oldalak egyesítésével vagy átrendezésével állítják össze. Ha kezdettől fogva egyetlen PDF-et szeretne, a könyvtár lehetővé teszi, hogy többoldalas dokumentumot hozzon létre egyetlen munkamenetben (session): hozza létre a dokumentumot egyszer a cikluson kívül, hívja meg az oldal hozzáadása (page-add) módszert a SaveToFile helyett a cikluson belül, majd mentse a teljes dokumentumot, miután a ciklus kilép. Ez elkerüli a köztes fájlokat, és ez a megfelelő struktúra a legtöbb egy-dokumentumos (single-document) konverziós forgatókönyv esetében

A nagy RTF fájlok esetében érdemes egy kis folyamat visszajelzést (progress feedback) adni a ciklushoz, mivel a konverziós arány nagyjából arányos az oldalszámmal, és egy 200 oldalas dokumentum néhány másodpercet is igénybe vehet. A repeat...until struktúra könnyen bővíthető: kövesse nyomon a karakter eltolást (character offset) egy folyamatjelző (progress bar) frissítésében minden iteráció után, a LastChar elosztva a RichEdit1.GetTextLen teljes karakterszámával

Az itt látható GetCanvasDC és LoadFromCanvasDc módszerek a Delphi és C++Builder nyelvekhez készült losLab PDF Library részét képezik