Articol tehnic

Conversie RTF în PDF în Delphi cu losLab PDF Library

RTF există de destul de mult timp încât apare în locuri pentru care nimeni nu a planificat: generatoare de rapoarte moștenite, lanțuri de îmbinare a corespondenței, arhive de documente juridice mai vechi decât procesoarele de text moderne. Convertirea lui în PDF din mers este o cerință recurentă, iar abordarea care chiar funcționează pe Windows nu este un analizor RTF dedicat, ci calea de randare pe care Windows o oferă deja prin TRichEdit și EM_FORMATRANGE. Ediția DLL a losLab PDF Library expune un context de dispozitiv virtual care se potrivește direct în acel lanț

Mecanismul: DC virtual și EM_FORMATRANGE

Controalele Rich Edit își pot pagina conținutul pentru orice context de dispozitiv, nu doar pentru o imprimantă fizică. Mesajul EM_FORMATRANGE îi spune controlului să așeze un interval de caractere într-un DC dat și returnează poziția ultimului caracter pe care a reușit să îl încadreze. Apelați-l în mod repetat, avansând de fiecare dată cpMin, și obțineți ieșire pagină cu pagină. GetCanvasDC din losLab PDF Library oferă un DC în memorie, dimensionat la orice dimensiuni de pagină specificați; după randarea unei pagini în el, LoadFromCanvasDc captează rezultatul ca pagină PDF. Acesta este tot lanțul

Un lucru de nimerit de la început: controlul TRichEdit trebuie dimensionat astfel încât să se potrivească paginii-țintă. Dacă controlul este mai mic sau mai mare decât dimensiunile DC-ului, paginarea nu se va alinia cu ce ajunge în PDF. Pentru ieșire A4, abordarea standard este să setați dimensiunile în pixeli ale controlului astfel încât să corespundă cu 210 x 297 mm la 96 DPI, înainte de a încărca fișierul RTF, folosind aceleași funcții ajutătoare de scalare cu care veți dimensiona DC-ul

PDF: lanțul de conversie RTF în PDF: un control Rich Edit dimensionat își paginează textul într-un DC de pânză virtuală prin EM_FORMATRANGE, iar fiecare trecere este captată ca o pagină PDF
EM_FORMATRANGE așază un interval de text RTF în DC-ul de pânză virtuală, iar LoadFromCanvasDc captează rezultatul, repetând până la ultimul caracter

Implementare în Delphi

Ce urmează folosește unitatea de import PDFlibAX_TLB, care învelește ediția DLL a bibliotecii. Formularul găzduiește un TRichEdit și un buton; handlerul OnCreate al formularului dimensionează controlul și încarcă RTF-ul, iar clicul pe buton conduce bucla de conversie

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);
  // Dimensionați controlul la A4 la DPI-ul ecranului, ca paginarea să se potrivească DC-ului
  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
    // Obțineți un DC virtual dimensionat la A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Randați următoarea pagină de conținut RTF în DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Captați conținutul DC-ului ca document PDF
    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;  // semnalează ultima pagină
end;

end.

Ce face bucla

PrintRtfBox completează structura TFormatRange și o transmite controlului Rich Edit prin SendMessage. Controlul randează caractere începând de la cpMin, se oprește când DC-ul se umple și returnează poziția primului caracter care nu a încăput. Când valoarea returnată este egală cu lungimea totală a textului sau o depășește, fiecare caracter a fost randat, iar funcția returnează zero, ceea ce încheie bucla repeat...until

Fiecare iterație produce câte un fișier PDF numit Output1.pdf, Output2.pdf și așa mai departe. Dacă doriți în schimb un singur document cu mai multe pagini, API-ul de adăugare de pagini al bibliotecii vă lasă să le asamblați ulterior sau puteți restructura bucla ca să apeleze AddPage într-o singură sesiune de document. Tiparul de mai sus, cu SaveToFile urmat de RemovePdfDocument la fiecare iterație, ține memoria de vârf mărginită la conținutul unei singure pagini, ceea ce contează pentru fișiere RTF foarte lungi

Detalii de dimensionare care încurcă lumea

Argumentul 96 DPI dat lui LoadFromCanvasDc îi spune bibliotecii la ce rezoluție de ecran a fost randat DC-ul, ca să poată calcula corect corespondența punct-pixel pentru pagina PDF. Greșiți asta și textul va apărea la dimensiunea greșită în ieșire, chiar dacă imaginea arată corect pe ecran

+100 adăugat lui RcPage.Right și RcPage.Bottom este o mică margine dincolo de marginea vizibilă a controlului. Rich Edit folosește dreptunghiul rcPage ca să decidă unde împarte paginile; fără margine, o linie care cade exact pe limită poate fi duplicată pe două pagini. Nu este o constantă magică: o vreți destul de mare încât limita de pagină să cadă curat în interiorul zonei de aranjare a controlului, nu pe ultimul pixel

PDF: dreptunghiurile imbricate rcPage și rc pentru EM_FORMATRANGE, cu marginea suplimentară de o sută de pixeli care împiedică duplicarea liniilor de limită între pagini
rcPage se întinde dincolo de dreptunghiul de desenare al controlului, astfel încât o linie care cade pe întreruperea de pagină nu poate fi tăiată sau duplicată

În fine, controlul trebuie să fie deja atașat la o fereastră de formular vizibilă atunci când rulează FormCreate, ca identificatorul lui de fereastră să fie valid înainte de primul apel SendMessage. Un TRichEdit creat dinamic la execuție are nevoie de un apel explicit HandleNeeded înainte să înceapă bucla de randare, dacă formularul nu a fost încă afișat

Tratarea fonturilor și a funcționalităților RTF

Pentru că randarea este făcută de motorul Rich Edit din Windows, substituirea fonturilor urmează aceleași reguli pe care el le folosește pentru afișare și tipărire. Fonturile referite în fișierul RTF care sunt instalate pe mașină se vor randa fidel; cele lipsă vor fi substituite în tăcere, ceea ce poate deplasa lungimile de rând și paginarea. Pentru conversia în lot din producție, merită testat explicit: încărcați un document cu fiecare font folosit de sursele dumneavoastră RTF și confirmați că numărul de pagini din ieșire corespunde cu ce așteptați de la o previzualizare manuală de tipărire

Tabelele, imaginile încorporate și majoritatea funcționalităților de formatare Rich Text funcționează fără nicio tratare suplimentară, pentru că Rich Edit le randează nativ. Singura zonă care poate surprinde este textul care folosește spațiere de paragraf sau indentare de primă linie personalizate, exprimate în twips: sistemul intern de coordonate al Rich Edit este în twips (1/1440 de țol), în timp ce coordonatele DC pe care le setați în TFormatRange sunt în pixeli, la DPI-ul curent. Controlul convertește intern, dar dacă construiți RTF-ul programatic, ar trebui să verificați că valorile marginilor sunt în unitatea potrivită

Conștiența de DPI și ecranele high-DPI

Pe un ecran care rulează la scalare de 150% (144 DPI), ScaleX(210, mmPixel) va returna un număr de pixeli mai mare decât pe un ecran la 100%. PDF Library înregistrează orice dimensiuni în pixeli îi transmiteți lui GetCanvasDC și folosește argumentul de DPI din LoadFromCanvasDc ca să calculeze înapoi dimensiunea fizică a paginii din PDF. Cât timp valoarea DPI pe care o transmiteți corespunde cu DPI-ul la care rulează aplicația dumneavoastră, dimensiunea paginii din ieșire va fi corectă, indiferent de scalarea ecranului

Dacă aplicația dumneavoastră nu este conștientă de DPI (vechea valoare implicită), Windows scalează DC-ul de ecran, iar calculele dumneavoastră de pixeli vor fi greșite pe mașinile high-DPI. Cea mai simplă remediere este să declarați conștiența de DPI în manifestul aplicației; aplicația primește atunci pixeli reali de dispozitiv, iar 96 pe care îl transmiteți lui LoadFromCanvasDc ar trebui înlocuit cu DPI-ul real al ecranului, obținut din GetDeviceCaps(GetDC(0), LOGPIXELSX). Exemplul de cod de mai sus fixează 96 în cod pentru că este potrivit pentru un mediu cu scalare la 100% și păstrează exemplul scurt

Structura ieșirii: un fișier per pagină față de un document combinat

Bucla de mai sus scrie fiecare pagină într-un fișier PDF separat. Dacă asta vă doriți depinde de utilizarea din aval. Sistemele de generare de rapoarte au adesea nevoie de pagini individuale, pentru că asamblează documentul final mai târziu, prin îmbinarea sau reordonarea paginilor. Dacă vreți un singur PDF de la bun început, biblioteca vă lasă să creați un document cu mai multe pagini într-o singură sesiune: creați documentul o dată, în afara buclei, apelați metoda de adăugare de pagină în locul lui SaveToFile în interiorul buclei și salvați documentul complet după ce bucla se încheie. Astfel evitați fișierele intermediare, iar aceasta este structura potrivită pentru majoritatea scenariilor de conversie într-un singur document

PDF: alegerea între scrierea câte unui fișier PDF per trecere, pentru asamblare ulterioară, și construirea unui singur PDF cu mai multe pagini prin adăugarea paginilor în interiorul buclei
Aceeași buclă de randare alimentează fie fișiere Output separate, fie un singur document care crește, punând în balanță flexibilitatea din aval și fișierele intermediare

Pentru fișiere RTF mari merită adăugată o formă de feedback de progres în buclă, fiindcă rata de conversie este aproximativ proporțională cu numărul de pagini, iar un document de 200 de pagini poate dura câteva secunde. Structura repeat...until este ușor de extins: urmăriți poziția în caractere printr-o actualizare de bară de progres după fiecare iterație, folosind LastChar împărțit la numărul total de caractere obținut din RichEdit1.GetTextLen

Metodele GetCanvasDC și LoadFromCanvasDc arătate aici fac parte din losLab PDF Library pentru Delphi și C++Builder