Teknisk artikkel

Konvertering av RTF til PDF i Delphi med losLab PDF Library

RTF har eksistert lenge nok til at det dukker opp på steder ingen hadde planlagt for: eldre rapportgeneratorer, flette-pipelines for e-post, og juridiske dokumentarkiver som er eldre enn moderne tekstbehandlere. Å konvertere det til PDF i sanntid ("on the fly") er et tilbakevendende krav, og tilnærmingen som faktisk fungerer på Windows, er ikke en dedikert RTF-parser, men den gjengivelsesveien Windows selv allerede tilbyr gjennom TRichEdit og EM_FORMATRANGE. DLL-utgaven av losLab PDF Library eksponerer en virtuell enhetskontekst (device context, DC) som passer direkte inn i den pipelinen

Mekanismen: virtuell DC og EM_FORMATRANGE

Rich Edit-kontroller kan paginere innholdet sitt for en hvilken som helst enhetskontekst, ikke bare en fysisk skriver. EM_FORMATRANGE-meldingen ber kontrollen om å legge ut et utvalg tegn i en gitt DC og returnerer posisjonen til det siste tegnet den klarte å få plass til. Kall den gjentatte ganger, og før frem cpMin hver gang, så får du side-for-side-utdata. losLab PDF Librarys GetCanvasDC tilbyr en DC i minnet dimensjonert til de sidedimensjonene du spesifiserer; etter å ha gjengitt en side i den, fanger LoadFromCanvasDc resultatet som en PDF-side. Det er hele pipelinen

Én ting å få riktig med én gang: TRichEdit-kontrollen må dimensjoneres slik at den samsvarer med målsiden. Hvis kontrollen er mindre eller større enn DC-dimensjonene, vil ikke pagineringen stemme overens med det som ender opp i PDF-en. For A4-utdata er standardtilnærmingen å angi kontrollens pikseldimensjoner slik at de samsvarer med 210 x 297 mm ved 96 DPI før du laster inn RTF-filen, ved å bruke de samme skaleringshjelperne du vil bruke for å dimensjonere DC-en

Delphi-implementasjon

Det følgende bruker importenheten PDFlibAX_TLB, som pakker inn DLL-utgaven av biblioteket. Skjemaet er vert for en TRichEdit og en knapp; skjemaets OnCreate-håndterer dimensjonerer kontrollen og laster inn RTF-en, og knappeklikket driver konverteringsløkken

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.

Hva løkken gjør

PrintRtfBox fyller TFormatRange-strukturen og sender den til Rich Edit-kontrollen via SendMessage. Kontrollen gjengir tegn med start fra cpMin, stopper når DC-en fylles, og returnerer posisjonen til det første tegnet som ikke fikk plass. Når returverdien tilsvarer eller overskrider den totale tekstlengden, har hvert tegn blitt gjengitt og funksjonen returnerer null, noe som avslutter repeat...until-løkken

Hver iterasjon produserer én PDF-fil med navnet Output1.pdf, Output2.pdf, og så videre. Hvis du heller vil ha ett enkelt dokument med flere sider, lar bibliotekets API for å legge til sider (page-append API) deg sette dem sammen i etterkant, eller du kan omstrukturere løkken for å kalle AddPage innenfor en enkelt dokumentøkt. Mønsteret ovenfor med SaveToFile per iterasjon etterfulgt av RemovePdfDocument, holder toppen av minneforbruket begrenset til innholdet på én side, noe som har betydning for svært lange RTF-filer

Dimensjoneringsdetaljer som spenner ben på folk

96 DPI-argumentet til LoadFromCanvasDc forteller biblioteket med hvilken skjermoppløsning DC-en ble gjengitt, slik at det kan beregne riktig punkt-til-piksel-kartlegging (point-to-pixel mapping) for PDF-siden. Gjør du dette feil, vil tekst vises med feil størrelse i utdataene, selv om bildet ser riktig ut på skjermen

+100 som legges til RcPage.Right og RcPage.Bottom er en liten margin utenfor kontrollens synlige kant. Rich Edit bruker rcPage-rektangelet til å avgjøre hvor sider skal deles; uten marginen kan en linje som faller nøyaktig på grensen, bli duplisert på tvers av to sider. Det er ikke en magisk konstant: du vil ha den stor nok til at sidegrensen faller rent innenfor kontrollens utleggsområde (layout area) fremfor på den siste pikselen

Til slutt må kontrollen allerede være koblet til et synlig skjemavindu (form window) når FormCreate kjører, slik at vindushåndtaket (window handle) er gyldig før det første kallet til SendMessage. En TRichEdit opprettet dynamisk under kjøring (runtime) trenger et eksplisitt HandleNeeded-kall før gjengivelsesløkken begynner hvis skjemaet ennå ikke er vist

Håndtering av fonter og RTF-funksjoner

Fordi gjengivelsen utføres av Windows Rich Edit-motoren, følger fonterstatning (font substitution) de samme reglene den bruker for visning og utskrift. Fonter som refereres i RTF-filen og som er installert på maskinen, vil gjengis nøyaktig; fonter som mangler, vil bli erstattet i stillhet, noe som kan forskyve linjelengder og paginering. For batchkonvertering i produksjon er dette verdt å teste eksplisitt: last inn et dokument med hver skrifttype (typeface) RTF-kildene dine bruker, og bekreft at antall utdatasider samsvarer med det du forventer fra en manuell forhåndsvisning for utskrift

Tabeller, innebygde bilder og de fleste Rich Text-formateringsfunksjoner fungerer uten ekstra håndtering fordi Rich Edit gjengir dem innebygd. Det ene området som kan være overraskende, er tekst som bruker tilpasset avstand mellom avsnitt eller førstelinjeinnrykk uttrykt i twips: Rich Edits interne koordinatsystem er i twips (1/1440 tomme), mens DC-koordinatene du angir i TFormatRange, er i piksler ved gjeldende DPI. Kontrollen konverterer internt, men hvis du konstruerer RTF-en programmatisk, bør du bekrefte at marginverdiene dine er i riktig enhet

DPI-bevissthet og skjermer med høy DPI

På en skjerm som kjører med 150 % skalering (144 DPI), vil ScaleX(210, mmPixel) returnere et større pikselantall enn på en 100 % skjerm. PDF Library registrerer de pikseldimensjonene du sender til GetCanvasDC og bruker DPI-argumentet i LoadFromCanvasDc for å regne seg tilbake til den fysiske sidestørrelsen i PDF-en. Så lenge DPI-verdien du sender samsvarer med DPI-en applikasjonen din kjører med, vil sidestørrelsen i utdataene være korrekt uavhengig av skjermskaleringen

Hvis applikasjonen din er DPI-ubevisst (DPI-unaware, den gamle standarden), skalerer Windows skjermens DC, og pikselberegningene dine vil bli feil på maskiner med høy DPI. Den enkleste løsningen er å deklarere DPI-bevissthet (DPI awareness) i applikasjonsmanifestet; applikasjonen mottar da sanne enhetspiksler (device pixels), og de 96 du sender til LoadFromCanvasDc bør byttes ut med den faktiske skjerm-DPI-en hentet fra GetDeviceCaps(GetDC(0), LOGPIXELSX). Kodesnutten ovenfor hardkoder 96 fordi det passer for et miljø med 100 % skalering, og for å holde eksemplet kort

Utdatastruktur: én fil per side versus et kombinert dokument

Løkken ovenfor skriver hver side til en separat PDF-fil. Om det er det du ønsker, avhenger av videre bruk. Rapportgenereringssystemer trenger ofte individuelle sider fordi de setter sammen det endelige dokumentet senere ved å slå sammen eller omorganisere sider. Hvis du vil ha én enkelt PDF fra starten av, lar biblioteket deg opprette et dokument med flere sider i en enkelt økt: opprett dokumentet én gang utenfor løkken, kall metoden for å legge til sider i stedet for SaveToFile inne i løkken, og lagre hele dokumentet etter at løkken er ferdig. Dette unngår mellomliggende filer og er den riktige strukturen for de fleste scenarioer med konvertering av enkeltdokumenter

For store RTF-filer er det verdt å legge til noen tilbakemeldinger om fremdriften i løkken, siden konverteringshastigheten er omtrent proporsjonal med sideantallet og et 200-siders dokument kan ta noen få sekunder. repeat...until-strukturen er enkel å utvide: spor tegnavviket (character offset) i en fremdriftsindikatoroppdatering (progress bar update) etter hver iterasjon, ved å bruke LastChar delt på det totale tegnantallet fra RichEdit1.GetTextLen

GetCanvasDC- og LoadFromCanvasDc-metodene som vises her, er en del av losLab PDF Library for Delphi og C++Builder