RTF postoji dovoljno dugo da se pojavljuje na mjestima za koja nitko nije planirao: u naslijeđenim generatorima izvješća (legacy report generators), procesima za cirkularna pisma (mail merge), arhivama pravnih dokumenata koje prethode modernim programima za obradu teksta. Pretvaranje u PDF u hodu (on the fly) čest je zahtjev, a pristup koji zapravo radi na Windowsu nije namjenski RTF parser, već putanja za iscrtavanje (rendering path) koju sam Windows već pruža putem TRichEdit i EM_FORMATRANGE. DLL izdanje losLab PDF Library izlaže kontekst virtualnog uređaja (virtual device context) koji se izravno uklapa u taj cjevovod (pipeline)
Mehanizam: virtualni DC i EM_FORMATRANGE
Rich Edit kontrole mogu izvršiti paginaciju svog sadržaja za bilo koji kontekst uređaja (DC), a ne samo za fizički pisač. Poruka EM_FORMATRANGE govori kontroli da rasporedi raspon znakova u zadani DC i vraća poziciju posljednjeg znaka koji je uspjela smjestiti. Pozivajte ga više puta, unapređujući cpMin svaki put, i dobit ćete ispis stranicu po stranicu. GetCanvasDC u losLab PDF Library pruža DC u memoriji dimenzioniran na dimenzije stranice koje navedete; nakon iscrtavanja stranice u njega, LoadFromCanvasDc bilježi rezultat kao PDF stranicu. To je cijeli cjevovod
Jedna stvar koju treba ispraviti na samom početku: kontrola TRichEdit mora biti dimenzionirana tako da odgovara ciljnoj stranici. Ako je kontrola manja ili veća od DC dimenzija, paginacija se neće uskladiti s onim što završi u PDF-u. Za A4 izlaz standardni je pristup postaviti dimenzije piksela kontrole da odgovaraju 210 x 297 mm na 96 DPI prije učitavanja RTF datoteke, koristeći iste pomoćnike za mjerilo (scale helpers) koje ćete koristiti za dimenzioniranje DC-a
Implementacija u Delphiju
Slijedeći primjer koristi jedinicu za uvoz PDFlibAX_TLB, koja omotava DLL izdanje knjižnice. Obrazac sadrži TRichEdit i gumb (button); rukovatelj (handler) OnCreate na obrascu dimenzionira kontrolu i učitava RTF, a klik na gumb pokreće petlju pretvorbe
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);
// Dimenzioniraj kontrolu na A4 na razlučivosti zaslona (DPI) tako da se paginacija podudara s DC-om
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
// Dobij virtualni DC dimenzioniran na A4
Dc := PdfDoc.GetCanvasDC(
Round(ScaleX(210, mmPixel)),
Round(ScaleY(297, mmPixel)));
// Iscrtaj sljedeću stranicu RTF sadržaja u DC
LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
// Zabilježi sadržaj DC-a kao PDF dokument
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; // označava zadnju stranicu
end;
end.
Što petlja radi
PrintRtfBox ispunjava strukturu TFormatRange i prosljeđuje ju Rich Edit kontroli putem SendMessage. Kontrola iscrtava znakove počevši od cpMin, zaustavlja se kada se DC napuni i vraća poziciju prvog znaka koji nije stao. Kada je povratna vrijednost jednaka ili veća od ukupne duljine teksta, svaki je znak iscrtan, a funkcija vraća nulu, što prekida petlju repeat...until
Svaka iteracija proizvodi jednu PDF datoteku s nazivom Output1.pdf, Output2.pdf i tako dalje. Ako umjesto toga želite jedan dokument s više stranica, API za dodavanje stranica ove knjižnice omogućuje vam da ih sastavite naknadno, ili možete restrukturirati petlju da poziva AddPage unutar jedne sesije dokumenta. Gore navedeni obrazac koji po iteraciji izvršava SaveToFile a zatim RemovePdfDocument održava vršnu upotrebu memorije ograničenom na količinu sadržaja jedne stranice, što je bitno za vrlo dugačke RTF datoteke
Detalji dimenzioniranja koji zbunjuju ljude
Argument 96 DPI u funkciji LoadFromCanvasDc govori knjižnici na kojoj je razlučivosti zaslona iscrtan DC, kako bi mogla izračunati ispravno preslikavanje točaka u piksele (point-to-pixel mapping) za PDF stranicu. Pogriješite li u ovome, tekst će se na izlazu prikazati u pogrešnoj veličini, iako slika na zaslonu izgleda ispravno
+100 dodano na RcPage.Right i RcPage.Bottom mala je margina izvan vidljivog ruba kontrole. Rich Edit koristi pravokutnik rcPage kako bi odlučio gdje će podijeliti stranice; bez margine, linija koja pada točno na granicu može se duplicirati preko dvije stranice. To nije magična konstanta: želite da bude dovoljno velika da granica stranice čisto padne unutar područja rasporeda kontrole umjesto na posljednji piksel
Konačno, kontrola već mora biti pridružena vidljivom prozoru obrasca kada se izvodi FormCreate tako da njezina ručica (handle) prozora bude valjana prije prvog poziva SendMessage. TRichEdit dinamički stvoren u vrijeme izvođenja treba eksplicitan poziv HandleNeeded prije početka petlje za iscrtavanje, ako obrazac još nije prikazan
Upravljanje fontovima i značajkama RTF-a
Budući da iscrtavanje obavlja Windows Rich Edit engine, zamjena fontova slijedi ista pravila koja se koriste za prikaz i ispis. Fontovi na koje se referencira u RTF datoteci, a koji su instalirani na stroju, renderirat će se vjerno; fontovi koji nedostaju bit će zamijenjeni tiho, što može pomaknuti duljinu linija i paginaciju. Za produkcijsku konverziju u serijama (batch conversion) ovo je vrijedno eksplicitno testirati: učitajte dokument sa svakom obitelji fontova (typeface) koju koriste vaši RTF izvori i potvrdite da broj izlaznih stranica odgovara onome što očekujete od ručnog pregleda ispisa
Tablice, ugrađene slike i većina značajki oblikovanja Rich Text formata rade bez ikakvog dodatnog rukovanja jer ih Rich Edit izvorno iscrtava (renders natively). Jedino područje koje može iznenaditi je tekst koji koristi prilagođeni razmak između odlomaka ili uvlačenje u prvom retku izraženo u twipsovima: Rich Edit-ov interni koordinatni sustav je u twipsovima (1/1440 inča), dok su DC koordinate koje postavite u TFormatRange u pikselima pri trenutnom DPI-ju. Kontrola to interno konvertira, ali ako programski konstruirate RTF, trebali biste provjeriti jesu li vrijednosti vaših margina u pravoj mjernoj jedinici
DPI svijest i zasloni visoke razlučivosti (high-DPI)
Na zaslonu koji radi sa 150% skaliranjem (144 DPI), ScaleX(210, mmPixel) vratit će veći broj piksela nego na zaslonu sa 100% skaliranjem. PDF Library bilježi sve dimenzije piksela koje proslijedite funkciji GetCanvasDC i koristi DPI argument u funkciji LoadFromCanvasDc za izračun natrag (back-calculate) na fizičku veličinu stranice u PDF-u. Sve dok se proslijeđena DPI vrijednost podudara s DPI razlučivošću na kojoj radi vaša aplikacija, veličina izlazne stranice bit će ispravna bez obzira na skaliranje zaslona
Ako vaša aplikacija nije DPI svjesna (DPI-unaware, što je stari zadani standard), Windows skalira DC na zaslonu i vaši izračuni piksela bit će pogrešni na strojevima s visokim DPI (high-DPI). Najjednostavniji je popravak deklarirati DPI svijest u manifestu aplikacije; aplikacija tada prima stvarne piksele uređaja i vrijednost 96 koju prosljeđujete u LoadFromCanvasDc treba biti zamijenjena stvarnom razlučivošću zaslona dobivenom iz GetDeviceCaps(GetDC(0), LOGPIXELSX). Kod primjera gore tvrdo kodira 96 jer je prikladno za okolinu od 100% skaliranja i održava primjer kratkim
Struktura izlaza: jedna datoteka po stranici u odnosu na kombinirani dokument
Gornja petlja zapisuje svaku stranicu u zasebnu PDF datoteku. Je li to ono što želite, ovisi o kasnijoj upotrebi. Sustavima za generiranje izvješća često su potrebne pojedinačne stranice jer kasnijim spajanjem ili preuređivanjem stranica sastavljaju konačni dokument. Ako od početka želite jedan PDF, knjižnica vam omogućuje stvaranje dokumenta s više stranica u jednoj sesiji: stvorite dokument jednom izvan petlje, pozovite metodu za dodavanje stranice (page-add method) umjesto SaveToFile unutar petlje, i spremite cijeli dokument nakon izlaska iz petlje. Time se izbjegavaju posredničke datoteke i to je prava struktura za većinu scenarija konverzije jednog dokumenta
Za velike RTF datoteke vrijedi dodati povratne informacije o napretku (progress feedback) u petlju, budući da je stopa konverzije otprilike proporcionalna broju stranica, a dokument od 200 stranica može potrajati nekoliko sekundi. Strukturu repeat...until lako je proširiti: pratite pomak znakova u ažuriranje trake napretka (progress bar) nakon svake iteracije, koristeći LastChar podijeljen s ukupnim brojem znakova iz RichEdit1.GetTextLen
Ovdje prikazane metode GetCanvasDC i LoadFromCanvasDc dio su losLab PDF Library za Delphi i C++Builder