Technický článek

Převod RTF do PDF v Delphi s losLab PDF Library

Formát RTF je tu už dostatečně dlouho na to, aby se objevoval v místech, kde to nikdo neplánoval: starší generátory sestav, systémy pro hromadnou korespondenci, archivy právních dokumentů, které předcházejí moderním textovým procesorům. Jeho převod do PDF za běhu je opakujícím se požadavkem a přístup, který ve Windows skutečně funguje, není dedikovaný parser RTF, ale vykreslovací cesta, kterou samotná Windows již poskytují prostřednictvím TRichEdit a EM_FORMATRANGE. Verze DLL knihovny losLab PDF Library zpřístupňuje kontext virtuálního zařízení, který do této pipeline přímo zapadá

Mechanismus: virtuální DC a EM_FORMATRANGE

Ovládací prvky Rich Edit mohou stránkovat svůj obsah pro jakýkoli kontext zařízení, nejen pro fyzickou tiskárnu. Zpráva EM_FORMATRANGE říká ovládacímu prvku, aby rozmístil rozsah znaků do daného DC, a vrací pozici posledního znaku, který se mu podařilo umístit. Zavolejte ji opakovaně a pokaždé posuňte cpMin, a získáte výstup stránku po stránce. Funkce GetCanvasDC v knihovně losLab PDF Library poskytuje DC v paměti dimenzované na jakékoli vámi zadané rozměry stránky; po vykreslení stránky do něj zachytí LoadFromCanvasDc výsledek jako stránku PDF. To je celá pipeline

Jedna věc, kterou je třeba vyřešit hned na začátku: velikost ovládacího prvku TRichEdit musí odpovídat cílové stránce. Pokud je ovládací prvek menší nebo větší než rozměry DC, stránkování se nebude shodovat s tím, co skončí v PDF. Pro výstup ve formátu A4 je standardním přístupem před načtením souboru RTF nastavit rozměry ovládacího prvku v pixelech tak, aby odpovídaly 210 x 297 mm při 96 DPI, pomocí stejných pomocníků pro měřítko, které použijete k určení velikosti DC

Implementace v Delphi

Následující kód používá importní unitu PDFlibAX_TLB, která obaluje verzi DLL knihovny. Formulář hostí TRichEdit a tlačítko; obslužná rutina OnCreate formuláře upraví velikost ovládacího prvku a načte RTF, a kliknutí na tlačítko řídí konverzní smyčku

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);
  // Upravit velikost ovládacího prvku na A4 při DPI obrazovky, aby stránkování odpovídalo 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
    // Získat virtuální DC o velikosti A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Vykreslit další stránku obsahu RTF do DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Zachytit obsah DC jako dokument 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;  // signalizuje poslední stránku
end;

end.

Co smyčka dělá

Funkce PrintRtfBox vyplní strukturu TFormatRange a předá ji ovládacímu prvku Rich Edit prostřednictvím SendMessage. Ovládací prvek vykresluje znaky počínaje od cpMin, zastaví se, když se DC zaplní, a vrátí pozici prvního znaku, který se nevešel. Když se vrácená hodnota rovná nebo překračuje celkovou délku textu, byl vykreslen každý znak a funkce vrátí nulu, čímž ukončí smyčku repeat...until

Každá iterace vytvoří jeden soubor PDF s názvem Output1.pdf, Output2.pdf a tak dále. Pokud místo toho chcete jeden vícestránkový dokument, rozhraní API knihovny pro připojování stránek vám je umožní sestavit dodatečně, nebo můžete restrukturalizovat smyčku tak, aby volala AddPage v rámci jedné relace dokumentu. Výše uvedený vzor použití SaveToFile v každé iteraci následovaný RemovePdfDocument udržuje maximální využití paměti omezené na obsah jedné stránky, na čemž záleží u velmi dlouhých souborů RTF

Detaily týkající se velikosti, na kterých se lidé zaseknou

Argument 96 DPI pro LoadFromCanvasDc říká knihovně, při jakém rozlišení obrazovky bylo DC vykresleno, takže může vypočítat správné mapování bodů na pixely pro stránku PDF. Pokud to uděláte špatně, text se na výstupu zobrazí ve špatné velikosti, i když obraz vypadá na obrazovce správně

Přidání +100 k RcPage.Right a RcPage.Bottom je malý okraj za viditelným okrajem ovládacího prvku. Rich Edit používá obdélník rcPage k rozhodnutí, kde rozdělit stránky; bez okraje se může řádek, který padne přesně na hranici, duplikovat na dvou stránkách. Není to magická konstanta: chcete ji dostatečně velkou, aby hranice stránky padla čistě dovnitř oblasti rozvržení ovládacího prvku spíše než na poslední pixel

A konečně, ovládací prvek už musí být připojen k viditelnému oknu formuláře, když běží FormCreate, aby byl jeho identifikátor okna platný před prvním voláním SendMessage. Dynamicky vytvořený TRichEdit za běhu potřebuje explicitní volání HandleNeeded před začátkem vykreslovací smyčky, pokud formulář ještě nebyl zobrazen

Zpracování písem a funkcí RTF

Protože vykreslování provádí engine Windows Rich Edit, nahrazování písem se řídí stejnými pravidly, jaká používá pro zobrazení a tisk. Písma odkazovaná v souboru RTF, která jsou na stroji nainstalována, se vykreslí věrně; chybějící písma se tiše nahradí, což může posunout délky řádků a stránkování. Pro produkční dávkovou konverzi stojí za to toto výslovně otestovat: načtěte dokument s každým typem písma, který používají vaše zdroje RTF, a potvrďte, že počet výstupních stránek odpovídá tomu, co očekáváte od ručního náhledu tisku

Tabulky, vložené obrázky a většina funkcí formátování Rich Text fungují bez jakéhokoli dalšího zacházení, protože je Rich Edit vykresluje nativně. Jedinou oblastí, která může být překvapivá, je text, který používá vlastní rozestupy odstavců nebo odsazení prvního řádku vyjádřené v twipech: vnitřní souřadnicový systém Rich Editu je v twipech (1/1440 palce), zatímco souřadnice DC, které nastavíte v TFormatRange, jsou v pixelech při aktuálním DPI. Ovládací prvek je převádí interně, ale pokud RTF konstruujete programově, měli byste ověřit, že hodnoty vašich okrajů jsou ve správné jednotce

Povědomí o DPI a displeje s vysokým DPI

Na displeji běžícím při 150% škálování (144 DPI) vrátí ScaleX(210, mmPixel) větší počet pixelů než na displeji se 100%. Knihovna PDF Library zaznamenává jakékoliv rozměry v pixelech, které předáte GetCanvasDC, a k následnému zpětnému výpočtu pro fyzickou velikost PDF dokumentu se obrátí u proměnné LoadFromCanvasDc podle předaného argumentu DPI. Dokud je přitom navíc argument s proměnnou DPI podle dané relace nastaven roven zrovna parametru DPI z režimu, ze kterého Vaše spouštěcí aplikace operuje, celková hodnota ve výstupu pro tisk odpovídá přesně požadované fyzické stránce bez návaznosti ohledně parametru pro rozlišení na výstupu obrazovky

Bude-li chybět údaj s parametrem odkazujícím se jako DPI u (dříve spíše standardní nastavení jako starý výchozí formát pro DPI-unaware) aplikace, Microsoft Windows na displejích pro DC zmenší zobrazení podle monitorů podporujících škálu pro zobrazení ze skupiny u tzv. high-DPI (s vysokým DPI) a pro propočet na tyto parametry ve snaze stanovení pixelů obdrží špatné parametry v chybné velikosti. V tu chvíli jako ten nejobyčejnější opravený přepis pro řešení funguje určení deklarace parametru pod označením DPI z deklarovaného seznamu u application manifestu; program si následně načítá správně vyhodnocené informace týkající se hodnoty přímo z pixelů podle fyzického zobrazení příslušného monitoru s tím, že oněch hodnotových 96 při propisu skrz proměnnou pro LoadFromCanvasDc bude žádoucí přepsat do správně odpovídající hodnoty za parametr displeje pro zobrazení DPI obdrženou právě přes aplikaci GetDeviceCaps(GetDC(0), LOGPIXELSX). Názorná úkazka se zde uvádí s pevně definovaným kódem nastaveným na 96 z důvodu snahy vytvořit pochopitelný kratší (krátký) přepis kódu k zachování pochopitelnosti kvůli demonstraci chování v nastavení se zaměřením zprostředkovat škálu za standardního propočtu ve 100 % prostředí

Struktura výstupu: jeden soubor na stránku versus kombinovaný dokument

Smyčka uvedená výše zapisuje každou ze stran ve stavu nezávisle oddělených dokumentů coby souborů ve formátu PDF. Jestli si tento způsob takovýmto způsobem požadujete odnést, bude nadále určováno zamýšleným modelem podle uplatnění v rámci downstream (následných postupech). Procesy určené k zhotovení reportů bývají zpravidla vyžadovány při nezávisle samostatných listech za sebou jdoucích jednotlivých postupných kroků pro další nakládání z důvodu, jelikož ke kompilaci konečného uceleného výstupu většinou přistupují nakonec společně se sloučením (mergingem), čímž nakonec určují, s jakým finálním výsledkem pak dokument přes reordering stran bude působit jako výsledek. V případě, kdy uvažujete raději naopak s výchozím nastavením se strukturou od základu se zapojeným jediným PDF (jako jediný ucelený) výstup s formátem rovnou zpočátku od začátku, se knihovna navíc navrhuje sama k dispozici ohledně variant o vyprodukování více stránek sdružených naráz formou k obalení s jedinou spouštěcí relací k obsloužení, stačí tak jen vytvořit dokument naráz samostatně jednorázově pro změnu těsně v místě před volanou cyklovou procedurou a po implementaci z metod uvnitř pro připojování s SaveToFile namísto spouštění dojde (dočkáme se propojení za ucelení takového kroku od procedury na komplet po vystoupení k vnějšímu okraji vně) k ukončení nad celou vytvořenou entitou se smyčkou po uložení kompletně dodělaných dat společně z úkonů od ukončení odchozího povelu venku ze smyčky. V takovém případě nezavoláme vůbec pro existenci nějakou z vložených vrstev v mezipřechodech při pomocných prozatímních souborech mezi voláními a proto se naopak dočkáme jako přesně ideálního výchozího stavu k nasazení převážně u větších objemů do celků pod parametry spíše odpovídající pro provedení formou o jednokusových dokumentech přes nasazení pro uplatnění a konverzi s těmito typicky převáděnými strukturovanými variantami souborů s příběhovými záchyty (scénáři)

U rozsáhlých souborů RTF stojí za to do smyčky přidat nějakou zpětnou vazbu o průběhu, protože rychlost konverze je zhruba úměrná počtu stránek a zpracování 200stránkového dokumentu může trvat několik sekund. Strukturu repeat...until lze snadno rozšířit: sledujte offset znaků v aktualizaci indikátoru průběhu (progress bar) po každé iteraci pomocí hodnoty LastChar dělené celkovým počtem znaků z RichEdit1.GetTextLen

Zde zobrazené metody GetCanvasDC a LoadFromCanvasDc jsou součástí knihovny losLab PDF Library pro Delphi a C++Builder