RTF je dovoljno dugo prisutan da se pojavljuje na mjestima za koja nitko nije planirao: zastarjeli generatori izvješća, procesi spajanja pošte, arhive pravnih dokumenata nastale prije modernih tekstualnih procesora. Pretvorba u PDF u hodu je ponavljajući zahtjev, a pristup koji zaista funkcionira na Windowsima nije namjenski RTF parser, već put renderiranja koji Windows već nudi putem kontrole TRichEdit i poruke EM_FORMATRANGE. DLL izdanje losLab PDF biblioteke izlaže virtualni kontekst uređaja koji se uklapa izravno u taj proces.
Mehanizam: virtualni DC i EM_FORMATRANGE
Kontrole Rich Edit mogu paginirati sadržaj za bilo koji kontekst uređaja, ne samo za fizički pisač. Poruka EM_FORMATRANGE govori kontroli da rasporedi raspon znakova u zadani DC i vraća poziciju zadnjeg znaka kojeg je uspjela smjestiti. Pozovite je više puta, svaki put napredujući cpMin, i dobivate izlaz stranica po stranicu. Metoda GetCanvasDC losLab PDF biblioteke pruža DC u memoriji dimenzioniran prema dimenzijama stranice koje navedete; nakon renderiranja stranice u njega, LoadFromCanvasDc hvata rezultat kao PDF stranicu. To je cijeli proces.
Jednu stvar treba ispravno postaviti od samog početka: kontrola TRichEdit mora biti dimenzionirana da odgovara ciljnoj stranici. Ako je kontrola manja ili veća od dimenzija DC-a, paginacija se neće podudarati s onim što završi u PDF-u. Za izlaz u formatu A4 standardni pristup je postavljanje dimenzija piksela kontrole da odgovaraju 210 x 297 mm pri 96 DPI prije učitavanja RTF datoteke, koristeći iste pomoćne funkcije skaliranja koje ćete koristiti za dimenzioniranje DC-a.
Implementacija u Delphiju
U nastavku se koristi uvozna jedinica PDFlibAX_TLB, koja omotava DLL izdanje biblioteke. Forma sadrži TRichEdit i gumb; rukovatelj OnCreate forme 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);
// 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.
Što petlja radi
Funkcija PrintRtfBox popunjava strukturu TFormatRange i prosljeđuje je Rich Edit kontroli putem SendMessage. Kontrola renderira znakove počevši od cpMin, zaustavlja se kad DC bude pun i vraća poziciju prvog znaka koji nije stao. Kada povratna vrijednost bude jednaka ili veća od ukupne duljine teksta, svi su znakovi renderirani i funkcija vraća nulu, čime se petlja repeat...until završava.
Svaka iteracija proizvodi jednu PDF datoteku s nazivom Output1.pdf, Output2.pdf, i tako dalje. Ako umjesto toga želite jedan višestranski dokument, API biblioteke za dodavanje stranica omogućuje naknadnu montažu, ili možete promijeniti strukturu petlje da poziva AddPage unutar jedne session dokumenta. Uzorak SaveToFile praćen RemovePdfDocument po iteraciji zadržava vršnu memoriju ograničenu na sadržaj jedne stranice, što je važno za vrlo dugačke RTF datoteke.
Detalji dimenzioniranja koji zbunjuju
Argument 96 DPI za LoadFromCanvasDc govori biblioteci pri kojoj razlučivosti zaslona je renderiran DC, kako bi mogla izračunati ispravno mapiranje točaka u piksele za PDF stranicu. Pogrešno postavite ovo i tekst će se pojaviti u krivoj veličini u izlazu, čak i ako slika izgleda ispravno na zaslonu.
Vrijednost +100 dodana na RcPage.Right i RcPage.Bottom je mala margina izvan vidljivog ruba kontrole. Rich Edit koristi pravokutnik rcPage da odluči gdje podijeliti stranice; bez margine, redak koji pada točno na granicu može biti dupliciran kroz dvije stranice. Nije magična konstanta: trebate je dovoljno veliku da granica stranice padne čisto unutar područja rasporeda kontrole, a ne na zadnji piksel.
Naposljetku, kontrola mora već biti priključena na vidljivi prozor forme kada se pokrene FormCreate, kako bi njezina ručica prozora bila valjana prije prvog poziva SendMessage. TRichEdit dinamički stvoren za vrijeme izvođenja treba eksplicitan poziv HandleNeeded prije početka petlje renderiranja ako forma još nije prikazana.
Rukovanje fontovima i RTF značajkama
Budući da renderiranje obavlja Windows Rich Edit engine, zamjena fontova prati ista pravila koja koristi za prikaz i ispis. Fontovi navedeni u RTF datoteci koji su instalirani na računalu renderiraju se vjerno; fontovi koji nedostaju se zamjenjuju tiho, što može pomaknuti duljine redova i paginaciju. Za produkcijsku skupnu pretvorbu vrijedi to eksplicitno testirati: učitajte dokument s vrstapisom koji vaši RTF izvori koriste i potvrdite da broj stranica izlaza odgovara onome što očekujete od ručnog pregleda ispisa.
Tablice, ugrađene slike i većina značajki formatiranja Rich Texta rade bez ikakvog dodatnog rukovanja jer Rich Edit ih renderira nativno. Područje koje može biti iznenađujuće je tekst koji koristi prilagođeni razmak odlomaka ili uvlake prvog retka izražene u twipsima: interni koordinatni sustav Rich Edita je u twipsima (1/1440 inča), dok su koordinate DC-a koje postavljate u TFormatRange u pikselima pri trenutnom DPI. Kontrola interno pretvara, ali ako programski konstruirate RTF, trebali biste provjeriti jesu li vaše vrijednosti margina u ispravnoj jedinici.
Svjesnost DPI i zasloni s visokim DPI
Na zaslonu s 150% skaliranjem (144 DPI), ScaleX(210, mmPixel) vraćat će veći broj piksela nego na zaslonu s 100%. PDF biblioteka bilježi dimenzije piksela koje prosljeđujete GetCanvasDC i koristi DPI argument u LoadFromCanvasDc za povratni izračun fizičke veličine stranice u PDF-u. Sve dok DPI vrijednost koju prosljeđujete odgovara DPI-ju pri kojem vaša aplikacija radi, veličina izlazne stranice bit će ispravna bez obzira na skaliranje zaslona.
Ako vaša aplikacija nije svjesna DPI-ja (stari zadani), Windows skalira DC zaslona i vaši izračuni piksela bit će pogrešni na računalima s visokim DPI. Najjednostavnije rješenje je deklarirati svjesnost DPI u manifestu aplikacije; aplikacija tada prima stvarne piksele uređaja i vrijednost 96 koju prosljeđujete LoadFromCanvasDc treba zamijeniti stvarnim DPI zaslona dobivenim iz GetDeviceCaps(GetDC(0), LOGPIXELSX). Gornji primjer koda tvrdo kodira 96 jer odgovara okruženju s 100% skaliranjem i drži primjer kratkim.
Struktura izlaza: jedna datoteka po stranici ili kombinirani dokument
Gornja petlja zapisuje svaku stranicu u zasebnu PDF datoteku. Hoće li to biti ono što trebate ovisi o daljnjoj upotrebi. Sustavi za generiranje izvješća često trebaju pojedinačne stranice jer naknadno sastavljaju konačni dokument spajanjem ili preraspoređivanjem stranica. Ako od početka trebate jedan PDF, biblioteka vam omogućuje stvaranje dokumenta s više stranica u jednoj sesiji: stvorite dokument jednom izvan petlje, pozovite metodu dodavanja stranice umjesto SaveToFile unutar petlje i spremi te potpuni dokument nakon izlaska iz petlje. Time se izbjegavaju privremene datoteke, a to je prava struktura za većinu scenarija pretvorbe jednog dokumenta.
Za velike RTF datoteke vrijedi dodati neku povratnu informaciju o napretku u petlju, budući da je brzina pretvorbe grubo proporcionalna broju stranica i dokument od 200 stranica može potrajati nekoliko sekundi. Struktura repeat...until lako se proširuje: pratite odmak znakova u ažuriranju trake napretka nakon svake iteracije, koristeći LastChar podijeljeno ukupnim brojem znakova iz RichEdit1.GetTextLen.
Metode GetCanvasDC i LoadFromCanvasDc prikazane ovdje dio su losLab PDF biblioteke za Delphi i C++Builder.