Technisch artikel

RTF naar PDF-conversie in Delphi met de losLab PDF Library

RTF bestaat al lang genoeg om op plekken op te duiken waar niemand op had gerekend: legacy rapportgeneratoren, mailmerge-pipelines, archieven met juridische documenten die dateren van vóór moderne tekstverwerkers. Het on-the-fly converteren ervan naar PDF is een terugkerende vereiste, en de aanpak die op Windows daadwerkelijk werkt, is niet een speciale RTF-parser maar het renderpad dat Windows zelf al biedt via TRichEdit en EM_FORMATRANGE. De DLL-editie van de losLab PDF Library biedt een virtuele apparaatcontext (device context) die direct op die pipeline aansluit

Het mechanisme: virtuele DC en EM_FORMATRANGE

Rich Edit-besturingselementen kunnen hun inhoud pagineren voor elke apparaatcontext, niet alleen voor een fysieke printer. Het bericht EM_FORMATRANGE draagt het besturingselement op om een reeks tekens in een gegeven DC uit te lijnen, en retourneert de positie van het laatste teken dat nog paste. Roep dit herhaaldelijk aan, waarbij u cpMin telkens opschuift, en u krijgt pagina-voor-pagina-uitvoer. De GetCanvasDC van de losLab PDF Library levert een in-memory DC met de paginadimensies die u opgeeft; nadat een pagina daarin is gerenderd, legt LoadFromCanvasDc het resultaat vast als een PDF-pagina. Dat is de hele pipeline

Eén ding moet u vooraf goed doen: het TRichEdit-besturingselement moet zodanig worden geformatteerd dat het overeenkomt met de doelpagina. Als het besturingselement kleiner of groter is dan de DC-dimensies, sluit de paginering niet aan op wat uiteindelijk in de PDF terechtkomt. Voor A4-uitvoer is de standaardaanpak om de pixelafmetingen van het besturingselement gelijk te stellen aan 210 x 297 mm bij 96 DPI, voordat het RTF-bestand wordt geladen, met dezelfde schaalhulpfuncties die u ook gebruikt om de DC te formatteren

PDF: RTF naar PDF-pijplijn: een op maat gezette Rich Edit-control pagineert zijn tekst naar een virtuele canvas DC via EM_FORMATRANGE en elke passe wordt vastgelegd als één PDF-pagina
EM_FORMATRANGE legt een bereik van RTF-tekst in de virtuele canvas DC en LoadFromCanvasDc vangt het resultaat, herhaald tot het laatste teken is bereikt

Delphi-implementatie

Het onderstaande voorbeeld gebruikt de importunit PDFlibAX_TLB, die de DLL-editie van de bibliotheek omhult. Het formulier bevat een TRichEdit en een knop; de OnCreate-handler van het formulier formatteert het besturingselement en laadt de RTF, en de klik op de knop stuurt de conversielus aan

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);
  // Formatteer het besturingselement op A4 bij scherm-DPI zodat de paginering overeenkomt met de 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
    // Verkrijg een virtuele DC met A4-formaat
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Render de volgende pagina RTF-inhoud in de DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Leg de inhoud van de DC vast als een 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;  // geeft de laatste pagina aan
end;

end.

Wat de lus doet

PrintRtfBox vult de structuur TFormatRange en geeft deze via SendMessage door aan het Rich Edit-besturingselement. Het besturingselement rendert tekens vanaf cpMin, stopt zodra de DC vol is, en retourneert de positie van het eerste teken dat niet meer paste. Wanneer de retourwaarde gelijk is aan of groter is dan de totale tekstlengte, is elk teken gerenderd en retourneert de functie nul, waarmee de repeat...until-lus wordt beëindigd

Elke iteratie produceert één PDF-bestand met de naam Output1.pdf, Output2.pdf, enzovoort. Als u in plaats daarvan één document met meerdere pagina's wilt, kunt u met de page-append-API van de bibliotheek deze achteraf samenvoegen, of u kunt de lus herstructureren om AddPage aan te roepen binnen één documentsessie. Het patroon van SaveToFile gevolgd door RemovePdfDocument per iteratie, zoals hierboven, houdt het piekgeheugenverbruik beperkt tot de inhoud van één pagina, wat van belang is bij zeer lange RTF-bestanden

Dimensiedetails waar mensen over struikelen

Het argument 96 DPI bij LoadFromCanvasDc vertelt de bibliotheek bij welke schermresolutie de DC is gerenderd, zodat deze de juiste punt-naar-pixel-toewijzing voor de PDF-pagina kan berekenen. Doet u dit verkeerd, dan verschijnt tekst in de uitvoer op de verkeerde grootte, ook al ziet de afbeelding er op het scherm correct uit

De +100 die wordt opgeteld bij RcPage.Right en RcPage.Bottom is een kleine marge voorbij de zichtbare rand van het besturingselement. Rich Edit gebruikt de rect rcPage om te bepalen waar pagina's worden gesplitst; zonder deze marge kan een regel die precies op de grens valt, op twee pagina's worden gedupliceerd. Het is geen magische constante: u wilt dat deze groot genoeg is zodat de paginagrens netjes binnen het lay-outgebied van het besturingselement valt, in plaats van op de laatste pixel

PDF: Geneste rcPage- en rc-rechthoeken voor EM_FORMATRANGE met de extra marge van honderd pixels die voorkomt dat grenslijnen over pagina's worden gedupliceerd
rcPage steekt voorbij de tekenrechthoek van de control zodat een regel die op de paginabreak landt niet kan worden gesplitst of gedupliceerd

Tot slot moet het besturingselement al aan een zichtbaar formuliervenster zijn gekoppeld wanneer FormCreate wordt uitgevoerd, zodat de window handle geldig is vóór de eerste aanroep van SendMessage. Een TRichEdit die tijdens runtime dynamisch wordt aangemaakt, vereist een expliciete aanroep van HandleNeeded voordat de renderlus begint, als het formulier nog niet is getoond

Omgaan met lettertypen en RTF-functies

Omdat het renderen wordt uitgevoerd door de Windows Rich Edit-engine, volgt lettertypevervanging dezelfde regels die worden gebruikt voor weergave en afdrukken. Lettertypen die in het RTF-bestand worden aangeroepen en op de machine zijn geïnstalleerd, worden getrouw gerenderd; ontbrekende lettertypen worden stilzwijgend vervangen, wat regellengtes en paginering kan verschuiven. Voor batchconversie in productie is het de moeite waard dit expliciet te testen: laad een document met elk lettertype dat uw RTF-bronnen gebruiken en controleer of het aantal uitvoerpagina's overeenkomt met wat u verwacht op basis van een handmatig afdrukvoorbeeld

Tabellen, ingesloten afbeeldingen en de meeste Rich Text-opmaakfuncties werken zonder extra afhandeling, omdat Rich Edit ze native rendert. Het enige gebied dat kan verrassen, is tekst met aangepaste alinea-afstanden of inspringingen van de eerste regel die in twips zijn uitgedrukt: het interne coördinatensysteem van Rich Edit werkt in twips (1/1440 inch), terwijl de DC-coördinaten die u instelt in TFormatRange in pixels bij de huidige DPI zijn. Het besturingselement converteert dit intern, maar als u de RTF programmatisch opbouwt, moet u controleren of uw marge-waarden in de juiste eenheid staan

DPI-bewustzijn en schermen met hoge DPI

Op een scherm dat draait op 150% schaling (144 DPI) retourneert ScaleX(210, mmPixel) een groter aantal pixels dan op een scherm met 100%. De PDF Library registreert welke pixelafmetingen u ook aan GetCanvasDC doorgeeft, en gebruikt het DPI-argument in LoadFromCanvasDc om de fysieke paginagrootte in de PDF terug te rekenen. Zolang de DPI-waarde die u doorgeeft overeenkomt met de DPI waarop uw toepassing draait, is de uitvoerpaginagrootte correct, ongeacht de schermschaling

Als uw toepassing DPI-onbewust is (de oude standaard), schaalt Windows de scherm-DC en zijn uw pixelberekeningen onjuist op machines met hoge DPI. De eenvoudigste oplossing is om DPI-bewustzijn te declareren in het toepassingsmanifest; de toepassing ontvangt dan echte apparaatpixels, en de 96 die u doorgeeft aan LoadFromCanvasDc moet dan worden vervangen door de daadwerkelijke schermresolutie, verkregen via GetDeviceCaps(GetDC(0), LOGPIXELSX). Het codevoorbeeld hierboven hardcodeert 96 omdat dit passend is voor een omgeving met 100% schaling en het voorbeeld kort houdt

Uitvoerstructuur: één bestand per pagina versus één gecombineerd document

De lus hierboven schrijft elke pagina naar een apart PDF-bestand. Of dat is wat u wilt, hangt af van het gebruik verderop in de keten. Systemen voor rapportgeneratie hebben vaak afzonderlijke pagina's nodig, omdat ze het uiteindelijke document later samenstellen door pagina's samen te voegen of te herschikken. Als u vanaf het begin één PDF wilt, kunt u met de bibliotheek een document met meerdere pagina's in één sessie aanmaken: maak het document eenmalig aan buiten de lus, roep binnen de lus de methode voor het toevoegen van pagina's aan in plaats van SaveToFile, en sla het volledige document op nadat de lus is beëindigd. Dit vermijdt de tussenliggende bestanden en is de juiste structuur voor de meeste conversiescenario's met één document

Bij grote RTF-bestanden is het de moeite waard om voortgangsfeedback aan de lus toe te voegen, aangezien de conversiesnelheid ruwweg evenredig is met het aantal pagina's en een document van 200 pagina's een paar seconden kan duren. De structuur repeat...until is eenvoudig uit te breiden: houd na elke iteratie de tekenoffset bij in een update van een voortgangsbalk, met LastChar gedeeld door het totale aantal tekens uit RichEdit1.GetTextLen

De hier getoonde methoden GetCanvasDC en LoadFromCanvasDc maken deel uit van de losLab PDF Library voor Delphi en C++Builder

PDF: Kiezen tussen het schrijven van één PDF-bestand per passe voor latere assemblage en het bouwen van één PDF met meerdere pagina's door pagina's binnen de lus toe te voegen
Dezelfde renderlus voedt of afzonderlijke Output-bestanden of één groeiend document, wat downstream-flexibiliteit inruilt tegen tussenbestanden