RTF bestaat al lang genoeg dat het opduikt op plekken waar niemand het gepland had: legacy rapportgeneratoren, mail merge-pijplijnen, juridische documentarchieven van vóór de moderne tekstverwerkers. Het ter plekke naar PDF converteren is een terugkerende eis, en de aanpak die op Windows werkelijk werkt is geen dedicated RTF-parser maar de renderroute die Windows zelf al aanbiedt via TRichEdit en EM_FORMATRANGE. De losLab PDF Library stelt een virtuele device context beschikbaar die direct in die pijplijn past
Het mechanisme: virtuele DC en EM_FORMATRANGE
Rich Edit-besturingselementen kunnen hun inhoud pagineren voor elke device context, niet alleen een fysieke printer. Het bericht EM_FORMATRANGE zegt tegen het besturingselement dat hij een bereik van tekens in een gegeven DC moet inpassen, en geeft de positie van het laatste teken terug dat hij kwijt kon. Roep hem herhaaldelijk aan en schuif cpMin telkens door, en u krijgt uitvoer pagina voor pagina. De GetCanvasDC van de losLab PDF Library levert een DC in het geheugen met de pagina-afmetingen die u opgeeft; nadat u een pagina erin heeft gerenderd, legt LoadFromCanvasDC het resultaat vast als een nieuw PDF-document van één pagina. Dat is de hele pijplijn
Eén ding moet meteen goed: het TRichEdit-besturingselement moet worden ingesteld op de afmetingen van de doelpagina. Is het besturingselement kleiner of groter dan de DC-afmetingen, dan loopt de paginering niet gelijk met wat er uiteindelijk in de PDF belandt. Voor A4-uitvoer is de standaardaanpak de pixeldimensies van het besturingselement gelijk te stellen aan 210 x 297 mm bij dezelfde DPI die u later aan LoadFromCanvasDC doorgeeft, vóór het laden van het RTF-bestand, met dezelfde millimeter-naar-pixelconversie waarmee u de DC instelt
Delphi-implementatie
Het volgende gebruikt de importunit PDFlibAX_TLB, de type library-import voor de ActiveX-editie van de library. Het formulier herbergt een TRichEdit en een knop; de OnCreate-handler van het formulier stelt het besturingselement in 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: IPDFlibrary;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
PdfDoc := CoPDFlibrary.Create;
// Stel het besturingselement op A4 in bij scherm-DPI zodat de paginering met de DC meevalt
RichEdit1.Width := MulDiv(210, Screen.PixelsPerInch * 10, 254);
RichEdit1.Height := MulDiv(297, Screen.PixelsPerInch * 10, 254);
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(
MulDiv(210, Screen.PixelsPerInch * 10, 254),
MulDiv(297, Screen.PixelsPerInch * 10, 254));
// 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(Screen.PixelsPerInch, 0);
PdfDocId := PdfDoc.SelectedDocument;
PdfDoc.SaveToFile(
ExtractFilePath(Application.ExeName)
+ 'Output' + IntToStr(PageNumber) + '.pdf');
PdfDoc.RemoveDocument(PdfDocId);
Inc(PageNumber);
until LastChar = 0;
// Laat het besturingselement de formatteringsdata die hij voor de DC cachte vrijgeven
SendMessage(RichEdit1.Handle, EM_FORMATRANGE, 0, 0);
end;
function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
FirstChar: Integer): Integer;
var
Fr: TFormatRange;
Dpi, PageWidthTw, PageHeightTw, NextCharPosition: Integer;
begin
// rc en rcPage zijn in twips (1/1440 inch), geen pixels: zet de
// paginagrootte om met dezelfde DPI die de DC en het besturingselement instelde
Dpi := Screen.PixelsPerInch;
PageWidthTw := MulDiv(rtfBox.Width, 1440, Dpi);
PageHeightTw := MulDiv(rtfBox.Height, 1440, Dpi);
Fr.hdc := hDc;
Fr.hdcTarget := hDc;
Fr.rcPage := Rect(0, 0, PageWidthTw, PageHeightTw);
Fr.rc := Fr.rcPage; // teken op de hele pagina; verklein voor marges
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 levert één PDF-bestand op met de naam Output1.pdf, Output2.pdf, enzovoort. Wilt u liever één document met meerdere pagina’s, dan kan AddToFileList plus MergeFileList de per-pagina-bestanden achteraf samenvoegen, of u bouwt de lus om zodat elk nieuw pagina-document in het eerste wordt gevouwen met MergeDocument. Het patroon hierboven met per iteratie SaveToFile gevolgd door RemoveDocument houdt het piekgeheugen begrensd op de inhoud van één pagina, wat telt bij heel lange RTF-bestanden
Dimensiedetails waar mensen over struikelen
Het DPI-argument van LoadFromCanvasDC vertelt de library hoeveel van uw pixels een inch zijn: de pagina komt eruit als canvasbreedte gedeeld door DPI keer 72 punten breed, en hetzelfde voor de hoogte. Zit het eruit naast, dan verschijnt tekst op de verkeerde grootte in de uitvoer terwijl het beeld op het scherm correct oogt
De eenheid van rc en rcPage is het detail waar de meeste Delphi-samples van deze techniek de mist in gaan. EM_FORMATRANGE leest beide rechthoeken in twips, 1/1440 inch, ook al werkt de DC waarin hij tekent in pixels. Vult u ze met pixelwaarden, dan zet het besturingselement tekst neer op een pagina die bij 96 DPI vijftien keer te klein is: een handvol woorden per passe, en een lus die een document van tien pagina’s omzet in honderden bijna lege PDF’s. Converteer met MulDiv(Pixels, 1440, Dpi), met de DPI die de DC instelde, dan valt de paginabreak waar de canvas werkelijk eindigt. rcPage is de volledige pagina; rc is het gebied dat tekst mag bezetten, dus marges komen uit het laten krimpen van rc erin, ook in twips. Als de lus klaar is, zegt nog één EM_FORMATRANGE met een nil lParam tegen het besturingselement dat hij de formatteringsdata die hij voor de DC cachte mag vrijgeven
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 behandeling, omdat Rich Edit ze native rendert. Het ene gebied dat kan verrassen is tekst met eigen alinea-afstand of eerste-regel-inspringingen uitgedrukt in twips: Rich Edit meet in twips (1/1440 inch), dezelfde eenheid als de rechthoeken in TFormatRange, terwijl de DC eronder in pixels werkt bij de huidige DPI. Het besturingselement mapt het ene op het andere tijdens het tekenen, maar als u de RTF programmatisch opbouwt, controleert u best dat uw marge waarden ook in twips zijn
DPI-bewustzijn en schermen met hoge DPI
Op een scherm dat draait op 150% schaling (144 DPI) geeft de millimeter-naar-pixelconversie 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 dezelfde is waarmee u millimeters naar pixels hebt omgezet, is de uitvoerpaginagrootte correct, ongeacht de schermschaling
Is uw toepassing DPI-onbewust (de oude standaard), dan schaalt Windows de scherm-DC en zijn uw pixelberekeningen fout op machines met hoge DPI. De eenvoudigste oplossing is DPI-bewustzijn declareren in het toepassingsmanifest; de toepassing ontvangt dan echte apparaatpixels, en de Screen.PixelsPerInch-waarde die de sample aan LoadFromCanvasDC doorgeeft rapporteert de echte scherm-DPI in plaats van 100% schaling te veronderstellen
Uitvoerstructuur: één bestand per pagina versus één gecombineerd document
De lus hierboven schrijft elke pagina weg 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. Wilt u vanaf het begin één PDF, dan laat de library u hem in één sessie bouwen: houd het ID van het document dat de eerste LoadFromCanvasDC-aanroep maakt vast, selecteer het bij elke volgende passe met SelectDocument en roep MergeDocument aan met het document-ID van de nieuwe pagina in plaats van SaveToFile, en sla daarna één keer op na het einde van de lus. 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