Techninis straipsnis

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

RTF egzistuoja pakankamai ilgai, kad atsirastų ten, kur niekas neplanavo: pasenusiuose ataskaitų generatoriuose, pašto sujungimo konvejeriuose, teisinių dokumentų archyvuose, kurie sukurti dar prieš šiuolaikines teksto apdorojimo programas. Jų konvertavimas į PDF realiuoju laiku yra pasikartojantis reikalavimas, o metodas, kuris iš tikrųjų veikia „Windows“ sistemoje, yra ne specialus RTF analizatorius, bet atvaizdavimo kelias, kurį pati „Windows“ jau pateikia per TRichEdit ir EM_FORMATRANGE. „losLab PDF Library“ DLL leidimas atskleidžia virtualų įrenginio kontekstą (DC), kuris tiesiogiai įsiterpia į šį konvejerį

Mechanizmas: virtualus DC ir EM_FORMATRANGE

„Rich Edit“ valdikliai gali padalinti savo turinį į puslapius bet kokiam įrenginio kontekstui, ne tik fiziniam spausdintuvui. Pranešimas EM_FORMATRANGE nurodo valdikliui išdėstyti simbolių diapazoną į nurodytą DC ir grąžina paskutinio simbolio, kurį pavyko sutalpinti, poziciją. Kviečiant jį pakartotinai ir kiekvieną kartą padidinant cpMin, gaunamas puslapių išvedimas. „losLab PDF Library“ funkcija GetCanvasDC pateikia atmintyje esantį DC, kurio dydis atitinka jūsų nurodytus puslapio matmenis; atvaizdavus į jį puslapį, LoadFromCanvasDc užfiksuoja rezultatą kaip PDF puslapį. Tai ir yra visas konvejeris

Vienas dalykas, kurį reikia teisingai nustatyti iš anksto: TRichEdit valdiklio dydis turi atitikti tikslinį puslapį. Jei valdiklis yra mažesnis arba didesnis nei DC matmenys, puslapių numeravimas nesutaps su tuo, kas atsidurs PDF faile. Standartinis A4 išvesties metodas yra nustatyti valdiklio pikselių matmenis taip, kad jie atitiktų 210 x 297 mm esant 96 DPI, prieš įkeliant RTF failą, naudojant tuos pačius mastelio keitimo pagalbininkus, kuriuos naudosite norėdami nustatyti DC dydį

Diegimas sistemoje „Delphi“

Toliau pateiktame pavyzdyje naudojamas importavimo modulis PDFlibAX_TLB, apimantis bibliotekos DLL leidimą. Formoje yra TRichEdit ir mygtukas; formos tvarkyklė OnCreate nustato valdiklio dydį ir įkelia RTF, o mygtuko paspaudimas valdo konvertavimo ciklą

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

PrintRtfBox užpildo struktūrą TFormatRange ir perduoda ją „Rich Edit“ valdikliui per SendMessage. Valdiklis atvaizduoja simbolius pradedant nuo cpMin, sustoja, kai DC užsipildo, ir grąžina pirmojo nesutilpusio simbolio poziciją. Kai grąžinta reikšmė yra lygi arba viršija bendrą teksto ilgį, visi simboliai atvaizduojami, o funkcija grąžina nulį, kuris nutraukia ciklą repeat...until

Kiekviena iteracija sukuria vieną PDF failą, pavadintą Output1.pdf, Output2.pdf ir t. t. Jei vietoje to norite vieno kelių puslapių dokumento, bibliotekos puslapių pridėjimo API leidžia juos surinkti po fakto, arba galite pertvarkyti ciklą, kad iškviestumėte AddPage vieno dokumento seanso metu. Aukščiau aprašytas iteracinis šablonas su SaveToFile ir po to einančiu RemovePdfDocument, išlaiko didžiausią atminties kiekį, apribotą vieno puslapio turiniu, o tai svarbu labai ilgiems RTF failams

Matmenų detalės, kurios sukelia problemų vartotojams

Parametras 96 DPI funkcijose LoadFromCanvasDc nurodo bibliotekai, kokia ekrano skiriamąja geba buvo atvaizduotas DC, todėl ji gali apskaičiuoti teisingą taškų ir pikselių atvaizdavimą PDF puslapiui. Jei tai padarysite neteisingai, išvestyje tekstas bus rodomas netinkamo dydžio, net jei vaizdas ekrane atrodo teisingas

Skaičius +100, pridėtas prie RcPage.Right ir RcPage.Bottom, yra nedidelė paraštė už matomo valdiklio krašto. „Rich Edit“ naudoja rcPage stačiakampį, kad nuspręstų, kur padalinti puslapius; be šios paraštės eilutė, kuri patenka tiksliai ties riba, gali būti dubliuojama dviejuose puslapiuose. Tai nėra stebuklinga konstanta: jūs norite, kad ji būtų pakankamai didelė, jog puslapio riba švariai patektų į valdiklio išdėstymo sritį, o ne į paskutinį pikselį

Galiausiai, valdiklis jau turi būti prijungtas prie matomo formos lango, kai vykdoma FormCreate, kad jo lango rankena (angl. handle) būtų galiojanti prieš pirmą kartą iškviečiant SendMessage. Prieš prasidedant atvaizdavimo ciklui, dinamiškai sukurtai TRichEdit reikalingas aiškus HandleNeeded iškvietimas, jei forma dar nebuvo parodyta

Šriftų ir RTF funkcijų valdymas

Kadangi atvaizdavimą atlieka „Windows Rich Edit“ variklis, šriftų keitimui taikomos tos pačios taisyklės, kurias jis naudoja rodymui ir spausdinimui. RTF faile nurodyti šriftai, įdiegti kompiuteryje, bus atvaizduojami tiksliai; trūkstami šriftai bus tyliai pakeisti, o tai gali pakeisti eilučių ilgius ir puslapių padalijimą. Gamybiniam paketiniam konvertavimui tai verta patikrinti atskirai: įkelkite dokumentą su kiekvienu šriftu, kurį naudoja jūsų RTF šaltiniai, ir patikrinkite, ar išvesties puslapių skaičius atitinka tai, ko tikitės iš rankinės spausdinimo peržiūros

Lentelės, įterpti vaizdai ir dauguma raiškiojo teksto (angl. Rich Text) formatavimo funkcijų veikia be jokio papildomo apdorojimo, nes „Rich Edit“ juos atvaizduoja natūraliai. Viena sritis, kuri gali nustebinti, yra tekstas, naudojantis pasirinktinius pastraipų tarpus arba pirmos eilutės įtraukas, išreikštas „twip“ vienetais: vidinė „Rich Edit“ koordinačių sistema yra išreikšta „twip“ vienetais (1/1440 colio), o DC koordinatės, kurias nustatote TFormatRange, yra pikseliais esant dabartiniam DPI. Valdiklis konvertuoja viduje, bet jei kuriate RTF programiškai, turėtumėte patikrinti, ar paraščių reikšmės yra teisingais vienetais

DPI suvokimas ir didelės raiškos (DPI) ekranai

Ekrane, veikiančiame su 150 % masteliu (144 DPI), ScaleX(210, mmPixel) grąžins didesnį pikselių skaičių nei ekrane, kurio mastelis 100 %. „PDF Library“ įrašo bet kokius pikselių matmenis, kuriuos perduodate GetCanvasDC, ir naudoja DPI argumentą LoadFromCanvasDc, kad atgaline tvarka apskaičiuotų fizinį PDF puslapio dydį. Tol, kol jūsų perduota DPI reikšmė atitinka DPI, kuriuo veikia jūsų programa, išvesties puslapio dydis bus teisingas, nepaisant ekrano mastelio

Jei jūsų programa nepalaiko DPI (senasis numatytasis nustatymas), „Windows“ masteliuoja ekrano DC, todėl jūsų pikselių skaičiavimai bus neteisingi didelio DPI kompiuteriuose. Paprasčiausias pataisymas yra paskelbti DPI palaikymą programos manifeste; tada programa gauna tikrus įrenginio pikselius ir 96, kuriuos perduodate LoadFromCanvasDc, turėtų būti pakeisti faktiniu ekrano DPI, gautu iš GetDeviceCaps(GetDC(0), LOGPIXELSX). Aukščiau esančiame kodo pavyzdyje įrašyta 96, nes tai tinka 100 % mastelio keitimo aplinkai ir išlaiko pavyzdį trumpą

Išvesties struktūra: po vieną failą puslapiui, palyginti su kombinuotu dokumentu

Aukščiau esantis ciklas kiekvieną puslapį įrašo į atskirą PDF failą. Ar to norite, priklauso nuo tolesnio naudojimo. Ataskaitų generavimo sistemoms dažnai reikia atskirų puslapių, nes galutinį dokumentą jos surenka vėliau, sujungdamos arba perrikiuodamos puslapius. Jei norite vieno PDF nuo pat pradžių, biblioteka leidžia sukurti dokumentą su keliais puslapiais per vieną seansą: sukurkite dokumentą vieną kartą už ciklo ribų, ciklo viduje iškvieskite puslapio pridėjimo metodą, o ne SaveToFile, ir išsaugokite visą dokumentą užbaigus ciklą. Tai padeda išvengti tarpinių failų ir yra tinkama struktūra daugeliui vieno dokumento konvertavimo scenarijų

Dideliems RTF failams verta įtraukti tam tikrą eigos atsiliepimą (angl. progress feedback) cikle, nes konvertavimo greitis yra maždaug proporcingas puslapių skaičiui, o 200 puslapių dokumentas gali užtrukti kelias sekundes. repeat...until struktūrą lengva išplėsti: po kiekvienos iteracijos stebėkite simbolių poslinkį eigos juostos atnaujinime, naudodami LastChar, padalytą iš bendro simbolių skaičiaus, gauto iš RichEdit1.GetTextLen

Čia parodyti metodai GetCanvasDC ir LoadFromCanvasDc yra „losLab PDF Library“, skirtos „Delphi“ ir „C++Builder“, dalis