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