Technical Article

Konverzija RTF u PDF u Delphi-ju uz losLab PDF biblioteku

RTF postoji dovoljno dugo da se pojavljuje na mestima koje niko nije planirao: legaсni generatori izvestaja, cevovodi za spajanje posta, pravni arhivi dokumenata koji prethode modernim procesorima teksta. Konverzija u PDF u hodu je ponavljajuci zahtev, a pristup koji zaista funkcionise na Windows-u nije namenski RTF parser vec put renderovanja koji Windows sam vec pruza putem TRichEdit i EM_FORMATRANGE. DLL izdanje losLab PDF biblioteke izlaze virtualni kontekst uredjaja koji se direktno uklapa u taj cevovod.

Mehanizam: virtualni DC i EM_FORMATRANGE

Rich Edit kontrole mogu paginirati sadrzaj za bilo koji kontekst uredjaja, a ne samo fizicki stampaс. Poruka EM_FORMATRANGE govori kontroli da rasporedi opseg znakova u zadati DC i vraca poziciju poslednjeg znaka koji je uspela da smesti. Pozivajte je ponavljajuci, napredujuci cpMin svaki put, i dobijate izlaz stranica po stranicu. GetCanvasDC losLab PDF biblioteke pruza DC u memoriji dimenzioniran na dimenzije stranice koje odredite; posle renderovanja stranice u njega, LoadFromCanvasDc hvata rezultat kao PDF stranicu. To je ceo cevovod.

Jedna stvar ispravna od pocetka: TRichEdit kontrola mora biti dimenzionirana da odgovara ciljnoj stranici. Ako je kontrola manja ili veca od dimenzija DC-a, paginacija nece biti uskladjenta s onim sto zavrsi u PDF-u. Za A4 izlaz standardni pristup je postavljanje dimenzija piksela kontrole da odgovaraju 210 x 297 mm pri 96 DPI pre ucitavanja RTF datoteke, koristeci iste pomocnike skaliranja koje cete koristiti za dimenzioniranje DC-a.

Delphi implementacija

Sledece koristi uvoznu jedinicu PDFlibAX_TLB, koja obmotava DLL izdanje biblioteke. Forma sadrzi TRichEdit i dugme; rukovaoс OnCreate forme dimenzionira kontrolu i ucitava RTF, a klik na dugme pokrece petlju konverzije.

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.

Sta petlja radi

PrintRtfBox popunjava strukturu TFormatRange i prosledjuje je Rich Edit kontroli putem SendMessage. Kontrola renderuje znakove pocevsi od cpMin, zaustavljajuci se kada se DC napuni, i vraca poziciju prvog znaka koji nije stao. Kada povratna vrednost dostigne ili prekoraci ukupnu duzinu teksta, svaki znak je renderovan i funkcija vraca nulu, sto prekida petlju repeat...until.

Svaka iteracija producira jednu PDF datoteku pod imenom Output1.pdf, Output2.pdf i tako dalje. Ako zelite jedan dokument s vise stranica, API za dodavanje stranica biblioteke vam omogucava da ih sastavite naknadno, ili mozete restrukturirati petlju da poziva AddPage unutar jedne sesije dokumenta. Obrazac SaveToFile pracen RemovePdfDocument po iteraciji gore drzi vrsnu memoriju ogranicenu na sadrzaj jedne stranice, sto je vazno za veoma duge RTF datoteke.

Detalji dimenzioniranja koji zbunjuju ljude

Argument 96 DPI za LoadFromCanvasDc govori biblioteci pri kojoj rezoluciji ekrana je DC bio renderovan, kako bi mogla da izracuna ispravno mapiranje tacaka u piksele za PDF stranicu. Pogresno postavljanje ovoga ce prouzrokovati da tekst izgleda u pogresnoj velicini u izlazu, cak i ako slika izgleda ispravno na ekranu.

+100 dodat RcPage.Right i RcPage.Bottom je mala margina izvan vidljive ivice kontrole. Rich Edit koristi pravougaonik rcPage da odluci gde rastaviti stranice; bez margine, red koji pada tacno na granicu moze se duplirati kroz dve stranice. Nije magicna konstanta: zelite da bude dovoljno velika da granica stranice padne cisto unutar podrucja rasporeda kontrole, a ne na poslednjem pikselu.

Konacno, kontrola mora biti vec pridruzena vidljivom prozoru forme kada FormCreate se izvrsava, kako bi njen rukovaoс prozora bio vazecan pre prvog poziva SendMessage. TRichEdit kreiran dinamicki pri izvrsavanju treba eksplicitan poziv HandleNeeded pre pocetka petlje renderovanja ako forma jos nije prikazana.

Obrada fontova i RTF funkcija

Buduci da renderovanje obavlja Windows Rich Edit motor, zamena fontova prati ista pravila koja koristi za prikaz i stampanje. Fontovi na koje se poziva RTF datoteka koji su instalirani na masini renderovace se verno; fontovi koji nedostaju bice tiho zamenjeni, sto moze pomeriti duzine redova i paginaciju. Za serijsku konverziju u produkciji ovo vredi eksplicitno testirati: ucitajte dokument sa svakim slovima pisma koje vasi RTF izvori koriste i potvrdite da se broj stranica izlaza podudara sa ocekivanjem od rucnog pregleda stampe.

Tabele, ugradene slike i vecina funkcija formatiranja bogatog teksta rade bez ikakve dodatne obrade jer ih Rich Edit renderuje nativno. Jedna oblast koja moze biti iznenadjujuca je tekst koji koristi prilagodjen razmak izmedju pasusa ili uvlake prve linije izrazene u tačkama razlomka (twips): Rich Edit-ov interni koordinatni sistem je u tačkama razlomka (1/1440 inca), dok su DC koordinate koje postavljate u TFormatRange u pikselima pri trenutnom DPI. Kontrola interno konvertuje, ali ako konstruisite RTF programatski trebali biste proveriti da su vase vrednosti margina u ispravnoj jedinici.

DPI svesnost i ekrani s visokim DPI

Na ekranu koji radi pri skaliranju od 150% (144 DPI), ScaleX(210, mmPixel) ce vratiti veci broj piksela nego na ekranu od 100%. PDF biblioteka beleži bilo koje dimenzije piksela koje prosledite GetCanvasDC i koristi DPI argument u LoadFromCanvasDc za povratni izracun fizicke velicine stranice u PDF-u. Sve dok vrednost DPI koju prosledite odgovara DPI-ju pri kom vasa aplikacija radi, velicina izlazne stranice ce biti ispravna bez obzira na skaliranje prikaza.

Ako vasa aplikacija nije svesna DPI-ja (stara podrazumevana), Windows skalira DC ekrana i vasi izracuni piksela bice pogresni na masinama s visokim DPI. Najjednostavnije resenje je deklarisanje DPI svesnosti u manifestu aplikacije; aplikacija tada prima tacne piksele uredjaja i vrednost 96 koju prosledjujete LoadFromCanvasDc treba zameniti stvarnim prikazom DPI dobijenim od GetDeviceCaps(GetDC(0), LOGPIXELSX). Uzorak koda gore tvrdo kodira 96 jer je primenjivo za okruzenje skaliranja od 100% i cuva primer kratkim.

Struktura izlaza: jedna datoteka po stranici nasuprot kombinovanom dokumentu

Petlja gore zapisuje svaku stranicu u zasebnu PDF datoteku. Da li je to ono sto zelite zavisi od upotrebe nizvodno. Sistemi za generisanje izvestaja cesto trebaju pojedinacne stranice jer naknadno sklapaju konacni dokument spajanjem ili preureddivanjem stranica. Ako zelite jedan PDF od pocetka, biblioteka vam omogucava da kreirate dokument s vise stranica u jednoj sesiji: kreirajte dokument jednom izvan petlje, pozovite metod dodavanja stranice umesto SaveToFile unutar petlje i sacuvajte kompletni dokument posle sto petlja izadje. Ovo izbegava medjuformatne datoteke i ispravna je struktura za vecinu scenarija konverzije jednog dokumenta.

Za velike RTF datoteke vredi dodati neke povratne informacije o napretku u petlji, buduci da je brzina konverzije priblizno proporcionalna broju stranica, a dokument od 200 stranica moze trajati nekoliko sekundi. Struktura repeat...until je laka za prosirenje: pratite pomak znakova u azuriranju trake napretka posle svake iteracije, koristeci LastChar podeljen ukupnim brojem znakova iz RichEdit1.GetTextLen.

Metode GetCanvasDC i LoadFromCanvasDc prikazane ovde deo su losLab PDF biblioteke za Delphi i C++Builder.