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