Technical Article

Pretvorba RTF v PDF v Delphiju s knjižnico losLab PDF

RTF je prisoten že dovolj dolgo, da se pojavlja na mestih, ki jih nihče ni načrtoval: v starejših generatorjih poročil, cevovodih za spajanje dokumentov in arhivih pravnih dokumentov, ki so starejši od sodobnih urejevalnikov besedil. Dinamična pretvorba v PDF je pogosta zahteva, pristop, ki v sistemu Windows dejansko deluje, pa ni namenski razčlenjevalnik RTF, temveč upodabljalna pot, ki jo Windows že ponuja prek TRichEdit in sporočila EM_FORMATRANGE. Izdaja DLL knjižnice losLab PDF izpostavlja virtualni kontekst naprave (device context), ki se prilega neposredno v ta cevovod.

Mehanizem: virtualni DC in EM_FORMATRANGE

Kontrole Rich Edit lahko prelomijo svojo vsebino za kateri koli kontekst naprave, ne le za fizični tiskalnik. Sporočilo EM_FORMATRANGE sporoča kontroli, naj razporedi obseg znakov v podani DC, in vrne položaj zadnjega znaka, ki ga je uspela uvrstiti. Z večkratnim klicanjem in premikanjem cpMin vsakič dobite izpis stran za stranjo. Funkcija GetCanvasDC knjižnice losLab PDF ponuja DC v pomnilniku, ki je prilagojen poljubnim dimenzijam strani; po upodobitvi strani vanjo pa klic LoadFromCanvasDc zajame rezultat kot stran PDF. Temu rečemo celoten cevovod.

Ena stvar, ki jo je treba urediti na začetku: kontrola TRichEdit mora biti prilagojena ciljni strani. Če je kontrola manša ali večja od dimenzij DC, se prelom strani ne bo ujemal s tistim, kar pristane v PDF-ju. Za izpis A4 je standardni pristop nastavitev slikovnih pik kontrole na 210 x 297 mm pri 96 DPI pred nalaganjem datoteke RTF, pri čemer uporabite enake pripomočke za merilo kot za velikost DC.

Izvedba v Delphiju

Spodnji primer uporablja uvozno enoto PDFlibAX_TLB, ki ovija različico DLL knjižnice. Obrazec gosti kontrolo TRichEdit in gumb; upravljalnik OnCreate obrazca določi velikost kontrole in naloži datoteko RTF, klik gumba pa poganja zanko 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.

Kaj zanka počne

Funkcija PrintRtfBox napolni strukturo TFormatRange in jo posreduje kontroli Rich Edit prek SendMessage. Kontrola izriše znake, ki se začnejo na cpMin, se zaustavi, ko se DC napolni, in vrne položaj prvega znaka, ki ni ustrezal. Ko je vračilna vrednost enaka ali večja od celotne dolžine besedila, so bili vsi znaki izrisani in funkcija vrne nič, kar zaključi zanko repeat...until.

Vsaka iteracija ustvari eno datoteka PDF z imenom Output1.pdf, Output2.pdf itd. Če želite namesto tega en sam večstranski dokument, vam API za dodajanje strani knjižnice omogoča, da jih sestavite naknadno, ali pa preuredite zanko tako, da kliče AddPage znotraj seje enega dokumenta. Zgornji vzorec SaveToFile in nato RemovePdfDocument za vsako iteracijo ohranja vršni pomnilnik omejen na velikost ene strani vsebine, kar je pomembno pri zelo dolgih datotekah RTF.

Podrobnosti o velikosti, ki lahko zavedejo

Argument 96 DPI v LoadFromCanvasDc pove knjižnici, pri kakšni ločljivosti zaslona je bil upodobljen DC, tako da lahko izračuna pravilno preslikavo točk v slikovne pike za stran PDF. Če to nastavite napačno, se bo besedilo v izhodu pojavilo v napačni velikosti, čeprav je slika na zaslonu videti pravilna.

Vrednost +100, dodana k RcPage.Right in RcPage.Bottom, je majhen rob zunaj vidnega roba kontrole. Rich Edit uporablja pravokotnik rcPage za odločanje o tem, kje razdeliti strani; brez tega roba se lahko vrstica, ki pade natanko na mejo, podvoji na dveh straneh. To ni magična konstanta: želite, da je dovolj velika, da meja strani čisto pade znotraj postavitve kontrole in ne na zadnjo slikovno piko.

Nazadnje, kontrola mora biti ob zagonu FormCreate že pritrjena na vidno okno obrazca, tako da je njena ročica okna veljavna pred prvim klicem SendMessage. Dinamično ustvarjen TRichEdit med delovanjem potrebuje ekspliciten klic HandleNeeded pred začetkom zanke upodabljanja, če obrazec še ni bil prikazan.

Upravljanje pisav in funkcij RTF

Ker upodabljanje izvaja Windows Rich Edit pogon, zamenjava pisav sledi enakim pravilom kot za prikaz in tiskanje. Pisave, navedene v datoteki RTF, ki so nameščene na računalniku, se bodo izrisale zvesto; manjkajoče pisave se bodo zamenjale tiho, kar lahko spremeni dolžine vrstic in prelom strani. Za produkcijsko serijsko pretvorbo je to vredno izrecno preizkusiti: naložite dokument z vsako pisavo, ki jo uporabljajo vaši viri RTF, in potrdite, da se število strani ujema s predogledom tiskanja.

Tabele, vgrajene slike in večina funkcij oblikovanja obogatenega besedila (Rich Text) delujejo brez posebne obdelave, saj jih Rich Edit upodablja nativno. Eno področje, ki je lahko presenetljivo, je besedilo, ki uporablja prilagojen razmik med odstavki ali zamike prvih vrstic, izražene v enotah twip: notranji koordinatni sistem Rich Edit je v twipih (1/1440 palca), medtem ko so koordinate DC, ki jih nastavite v TFormatRange, v slikovnih pikah pri trenutnem DPI. Kontrola se pretvori interno, če pa RTF gradite programsko, preverite, ali so vaše vrednosti robov v pravi enoti.

Zavedanje DPI in zasloni z visoko ločljivostjo (High-DPI)

Na zaslonu s 150-odstotnim merilom (144 DPI) bo klic ScaleX(210, mmPixel) vrnil večje število slikovnih pik kot na 100-odstotnem zaslonu. PDF knjižnica zabeleži poljubne dimenzije slikovnih pik, ki jih posredujete v GetCanvasDC, in uporabi argument DPI v LoadFromCanvasDc za povratni izračun fizične velikosti strani v PDF-ju. Dokler se posredovana vrednost DPI ujema z ločljivostjo, pri kateri deluje vaša aplikacija, bo izhodna velikost strani pravilna, ne glede na merilo zaslona.

Če vaša aplikacija nima zavedanja o DPI (stara privzeta vrednost), Windows prilagodi velikost zaslonskega DC-ja, vaši izračuni slikovnih pik bodo na zaslonih z visoko ločljivostjo napačni. Najenostavnejši popravek je deklaracija zavedanja o DPI v manifestu aplikacije; aplikacija nato prejme prave piksle naprave, vrednost 96, ki jo posredujete v LoadFromCanvasDc, pa je treba zamenjati z dejanskim DPI zaslona, pridobljenim iz GetDeviceCaps(GetDC(0), LOGPIXELSX). Zgornji primer trdo kodira 96, saj je to primerno za 100 % merilo in ohranja kratek primer.

Izhodna struktura: ena datoteka na stran proti združenemu dokumentu

Zgornja zanka zapiše vsako stran v ločeno datoteko PDF. Ali je to tisto, kar želite, je odvisno od nadaljnje uporabe. Sistemi za ustvarjanje poročil pogosto potrebujejo posamezne strani, saj finalni dokument sestavijo pozneje z združevanjem ali preurejanjem strani. Če želite takoj en sam PDF, vam knjižnica omogoča ustvarjanje večstranskega dokumenta v eni seji: dokument ustvarite enkrat zunaj zanke, pokličete metodo za dodajanje strani namesto SaveToFile znotraj zanke in shranite celoten dokument po izhodu iz zanke. S tem se izognete začasnim datotekam, kar je prava struktura za večiso scenarijev pretvorbe posameznih dokumentov.

Pri velikih datotekah RTF je v zanko smiselno dodati povratne informacije o napredku, saj je hitrost pretvorbe sorazmerna s številom strani, 200-stranski dokument pa lahko traja nekaj sekund. Indeks repeat...until je enostavno razširiti: spremljajte odmik znakov za posodobitev vrstice napredka po vsaki iteraciji z uporabo LastChar, deljenim s skupnim številom znakov iz RichEdit1.GetTextLen.

Metodi GetCanvasDC in LoadFromCanvasDc, prikazani tukaj, sta del knjižnice losLab PDF Library za Delphi in C++Builder.