Technical Article

RTF konvertavimas į PDF su „losLab PDF Library“ „Delphi“ aplinkoje

RTF formatas gyvuoja pakankamai ilgai, todėl jis vis dar naudojamas ten, kur niekas to neplanavo: senose ataskaitų generavimo sistemose, laiškų suliejimo procesuose ar teisinių dokumentų archyvuose, sukuriate dar prieš atsirandant šiuolaikinėms teksto rengyklėms. Greitas jo konvertavimas į PDF yra dažnas reikalavimas. Būdas, kuris iš tikrųjų veikia „Windows“ aplinkoje, yra ne atskira RTF analizavimo programa, o pačios „Windows“ sistemos atvaizdavimo kelias per TRichEdit ir EM_FORMATRANGE. „losLab PDF Library“ DLL versija suteikia virtualų įrenginio kontekstą (device context, DC), kuris idealiai integruojamas į šį procesą.

Mechanizmas: virtualus DC ir EM_FORMATRANGE

„Rich Edit“ valdikliai gali suskaidyti turinį puslapiais bet kokiam įrenginio kontekstui, ne tik fiziniam spausdintuvui. Pranešimas EM_FORMATRANGE nurodo valdikliui išdėstyti tam tikrą simbolių rėžį nurodytame DC ir grąžina paskutinio tilpusio simbolio poziciją. Kviečiant šią funkciją pakartotinai ir kas kartą padidinant cpMin reikšmę, gaunamas puslapis po puslapio formatuotas rezultatas. „losLab PDF Library“ metodas GetCanvasDC pateikia atmintyje esantį DC, kurio dydis atitinka nurodytus puslapio matmenis. Atvaizdavus puslapį jame, LoadFromCanvasDc užfiksuoja rezultatą kaip PDF puslapį. Tai yra visas konvertavimo kelias.

Svarbu iš karto užtikrinti vieną dalyką: TRichEdit valdiklio dydis turi atitikti tikslo puslapio matmenis. Jeigu valdiklis yra mažesnis arba didesnis už DC matmenis, puslapių skaidymas nesutaps su tuo, kas bus sugeneruota PDF faile. A4 formatui standartinis sprendimas yra nustatyti valdiklio taškų (pikselių) matmenis, atitinkančius 210 x 297 mm prie 96 DPI, prieš įkeliant RTF failą, naudojant tas pačias mastelio pagalbines funkcijas, kurias naudosite DC dydžiui nustatyti.

Delphi realizacija

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.

Ką daro ciklas

Metodas PrintRtfBox užpildo TFormatRange struktūrą ir perduoda ją „Rich Edit“ valdikliui per SendMessage. Valdiklis atvaizduoja simbolius pradedant nuo cpMin taško, sustoja, kai DC užsipildo, ir grąžina pirmojo netilpusio simbolio poziciją. Kai grąžina reikšmė sutampa su visu teksto ilgiu arba jį viršija, visi simboliai yra sėkmingai atvaizduoti ir funkcija grąžina nulį, kas užbaigia repeat...until ciklą.

Kiekviena iteracija sukuria po vieną PDF failą pavadinimais Output1.pdf, Output2.pdf ir t. t. Jeigu vietoje to norite vieno daugiapuslapio dokumento, bibliotekos puslapių sujungimo API leidžia juos sujungti vėliau, arba galite pertvarkyti ciklą, kad iškviestumėte AddPage vieno dokumento sesijos metu. Pavyzdyje parodytas šablonas, kai kiekvienos iteracijos metu kviečiamas SaveToFile, o po jo – RemovePdfDocument, leidžia išlaikyti minimalų atminties suvartojimą (apribotą vieno puslapio turiniu), kas labai svarbu apdorojant itin ilgus RTF failus.

Matmenų nustatymo niuansai

Metodui LoadFromCanvasDc perduodamas 96 DPI argumentas praneša bibliotekai, kokia ekrano raiška buvo atvaizduotas DC, kad ji galėtų apskaičiuoti teisingą taškų ir pikselių santykį PDF puslapyje. Nurodžius neteisingą reikšmę, tekstas išvesties faile bus netinkamo dydžio, net jeigu vaizdas ekrane atrodė teisingai.

Prie savybių RcPage.Right ir RcPage.Bottom pridedama reikšmė +100 yra nedidelė paraštė už matomų valdiklio kraštų. „Rich Edit“ naudoja rcPage stačiakampį puslapių skaidymo riboms nustatyti. Be šios paraštės, eilutė, kuri patenka tiksliai ant ribos, gali būti dubliuojama dviejuose puslapiuose. Tai nėra magiška konstanta: paraštė turi būti pakankamai didelė, kad puslapio riba būtų aiškiai valdiklio išdėstymo srityje, o ne ant paskutinio pikselio.

Galiausiai, valdiklis jau turi būti susietas su matomu formos langu vykdant FormCreate, kad jo lango rodyklė (handle) būtų validi prieš pirmąjį SendMessage iškvietimą. Dinamiškai vykdymo metu sukurtam TRichEdit valdikliui reikalingas aiškus HandleNeeded iškvietimas prieš pradedant braižymo ciklą, jeigu forma dar nebuvo parodyta.

Šriftų ir RTF funkcijų palaikymas

Kadangi atvaizdavimą atlieka „Windows Rich Edit“ variklis, šriftų keitimui taikomos tos pačios taisyklės, kaip ir ekrano peržiūrai bei spausdinimui. Šriftai, nurodyti RTF faile ir įdiegti kompiuteryje, bus atvaizduojami tiksliai. Trūkstami šriftai bus pakeisti automatiškai ir nepastebimai, kas gali pakeisti eilučių ilgį bei puslapių skaidymą. Vykdant paketinį konvertavimą gamybinėje aplinkoje, tai verta išbandyti atskirai: įkelkite dokumentą su visais jūsų šaltiniuose naudojamais šriftų tipais ir įsitikinkite, kad sugeneruotų puslapių skaičius sutampa su spausdinimo peržiūros rezultatais.

Lentelės, įterpti paveikslėliai ir dauguma kitų teksto formatavimo funkcijų veikia be papildomų pastangų, nes „Rich Edit“ jas atvaizduoja natūraliai. Netikėtumų gali kilti tik su tekstu, kuriame naudojami nestandartiniai pastraipų tarpai arba pirmosios eilutės įtraukos, išreikštos „twip“ vienetais: „Rich Edit“ vidinė koordinačių sistema naudoja „twip“ (1/1440 colio), o DC koordinatės, kurias nustatote TFormatRange struktūroje, yra nurodomos pikseliais pagal esamą DPI. Valdiklis atlieka konvertavimą viduje, tačiau, jeigu RTF kodą generuojate programiškai, turėtumėte įsitikinti, kad paraščių reikšmės nurodytos tinkamais vienetais.

DPI parinktys ir didelės raiškos (High-DPI) ekranai

Ekrane, kuriame nustatytas 150 % mastelis (144 DPI), funkcija ScaleX(210, mmPixel) grąžins didesnį pikselių skaičių nei ekrane su 100 % masteliu. PDF biblioteka užfiksuoja pikselių matmenis, kuriuos perduodate metodui GetCanvasDC, ir naudoja DPI argumentą metode LoadFromCanvasDc, kad atgaline data apskaičiuotų fizinį PDF puslapio dydį. Tol, kol jūsų perduodama DPI reikšmė sutampa su ta, su kuria veikia jūsų programa, sugeneruoto puslapio dydis bus teisingas, nepriklausomai nuo ekrano mastelio nustatymų.

Jeigu jūsų programa nepalaiko DPI parinkčių (senasis numatytasis režimas), „Windows“ pritaiko ekrano DC mastelį ir jūsų pikselių skaičiavimai didelės raiškos ekranuose bus neteisingi. Paprasčiausias sprendimas – deklaruoti programos DPI palaikymą (DPI awareness) programos manifeste. Tokiu atveju programa gaus tikruosius įrenginio pikselius, o skaičių 96, kurį perduodate metodui LoadFromCanvasDc, reikėtų pakeisti ekrano DPI reikšme, gauta per GetDeviceCaps(GetDC(0), LOGPIXELSX). Pateiktame kodo pavyzdyje yra įrašyta fiksuota reikšmė 96, nes ji tinka 100 % mastelio aplinkai ir leidžia išlaikyti trumpesnį kodo pavyzdį.

Išvesties struktūra: atskiras failas kiekvienam puslapiui ar vienas bendras dokumentas

Aukščiau parodytas ciklas kiekvieną puslapį įrašo į atskirą PDF failą. Ar toks būdas jums tinka, priklauso nuo tolesnio dokumentų naudojimo. Ataskaitų generavimo sistemoms dažnai reikalingi atskiri puslapiai, nes galutinis dokumentas surenkamas vėliau, sujungiant arba keičiant puslapių tvarką. Jeigu iš karto norite vieno bendro PDF, biblioteka leidžia sukurti kelių puslapių dokumentą vienoje sesijoje: sukurkite dokumentą vieną kartą prieš ciklą, ciklo viduje kvieškite puslapio pridėjimo metodą vietoje SaveToFile ir išsaugokite galutinį dokumentą ciklui pasibaigus. Tai leidžia išvengti tarpinių failų kūrimo ir yra tinkamiausias sprendimas daugeliui vieno dokumento konvertavimo scenarijų.

Dideliems RTF failams verta cikle numatyti progreso indikaciją, nes konvertavimo greitis yra proporcingas puslapių skaičiui, o 200 puslapių dokumento apdorojimas gali užtrukti kelias sekundes. Struktūrą repeat...until lengva papildyti: po kiekvienos iteracijos atnaujinkite progreso juostą (progress bar), naudodami santykį LastChar padalintą iš viso simbolių skaičiaus iš RichEdit1.GetTextLen.

Čia demonstruojami metodai GetCanvasDC ir LoadFromCanvasDc yra losLab PDF Library bibliotekos dalis, skirta Delphi ir C++Builder.