RTF har funnits tillräckligt länge för att det ska dyka upp på platser ingen planerat för: gamla rapportgeneratorer, kopplingar för e-postutskick (mail merge), juridiska dokumentarkiv från tiden före moderna ordbehandlare. Att konvertera det till PDF i farten är ett återkommande krav, och den metod som faktiskt fungerar i Windows är inte en dedikerad RTF-tolkare, utan den renderingsväg som Windows självt redan tillhandahåller genom TRichEdit och EM_FORMATRANGE. DLL-utgåvan av losLab PDF Library exponerar en virtuell enhetskontext (device context) som passar direkt in i den pipelinen
Mekanismen: virtuell DC och EM_FORMATRANGE
Rich Edit-kontroller kan paginera sitt innehåll för vilken enhetskontext som helst, inte bara en fysisk skrivare. Meddelandet EM_FORMATRANGE säger till kontrollen att lägga ut ett intervall av tecken i en given DC och returnerar positionen för det sista tecknet den lyckades få plats med. Anropa den upprepade gånger, flytta fram cpMin varje gång, så får du en utdata sida för sida. LosLab PDF Librarys GetCanvasDC ger en in-memory DC som anpassas till vilka sidmått du än anger; efter att ha renderat en sida i den, fångar LoadFromCanvasDc resultatet som en PDF-sida. Det är hela pipelinen
En sak att få rätt på en gång: TRichEdit-kontrollen måste storleksanpassas för att matcha målsidan. Om kontrollen är mindre eller större än DC-måtten, kommer pagineringen inte att stämma överens med vad som hamnar i PDF:en. För A4-utdata är standardmetoden att ställa in kontrollens pixeldimensioner så att de matchar 210 x 297 mm vid 96 DPI innan RTF-filen laddas, med hjälp av samma skaleringshjälpare (scale helpers) som du kommer att använda för att storleksanpassa DC:n
Delphi-implementering
Följande använder importenheten PDFlibAX_TLB, som omsluter DLL-utgåvan av biblioteket. Formuläret är värd för en TRichEdit och en knapp; formulärets händelsehanterare för OnCreate anpassar kontrollens storlek och laddar RTF:en, och knappklickningen driver konverteringsloopen
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.
Vad loopen gör
PrintRtfBox fyller strukturen TFormatRange och skickar den till Rich Edit-kontrollen via SendMessage. Kontrollen renderar tecken med början vid cpMin, stannar när DC:n fylls och returnerar positionen för det första tecknet som inte fick plats. När returvärdet är lika med eller överstiger den totala textlängden har varje tecken renderats och funktionen returnerar noll, vilket avslutar repeat...until-loopen
Varje iteration producerar en PDF-fil med namnet Output1.pdf, Output2.pdf, och så vidare. Om du istället vill ha ett enda flersidigt dokument, låter bibliotekets API för att lägga till sidor dig sätta ihop dem i efterhand, eller så kan du omstrukturera loopen för att anropa AddPage inom en enda dokumentsession. Mönstret med SaveToFile per iteration följt av RemovePdfDocument ovan håller minnestopparna begränsade till en sidas innehåll, vilket har betydelse för mycket långa RTF-filer
Storleksdetaljer som sätter krokben för folk
Argumentet på 96 DPI till LoadFromCanvasDc talar om för biblioteket vid vilken skärmupplösning DC:n renderades, så att det kan beräkna rätt punkt-till-pixel-mappning för PDF-sidan. Får du detta fel kommer texten att visas i fel storlek i utdata även om bilden ser korrekt ut på skärmen
De +100 som läggs till i RcPage.Right och RcPage.Bottom är en liten marginal utanför kontrollens synliga kant. Rich Edit använder rektangeln rcPage för att bestämma var sidor ska delas; utan marginalen kan en rad som hamnar exakt vid gränsen dupliceras över två sidor. Det är ingen magisk konstant: du vill ha den tillräckligt stor för att sidgränsen ska falla snyggt inuti kontrollens layoutområde snarare än på den sista pixeln
Slutligen måste kontrollen redan vara ansluten till ett synligt formulärfönster när FormCreate körs så att dess fönsterhandtag (window handle) är giltigt före det första anropet till SendMessage. En TRichEdit som skapas dynamiskt vid körning behöver ett uttryckligt anrop till HandleNeeded innan renderloopen börjar om formuläret ännu inte har visats
Hantering av typsnitt och RTF-funktioner
Eftersom renderingen görs av Windows Rich Edit-motorn, följer typsnittsersättningen samma regler som används för visning och utskrift. Typsnitt som refereras till i RTF-filen och som är installerade på maskinen kommer att renderas naturtroget; typsnitt som saknas kommer att ersättas tyst, vilket kan förskjuta radlängder och paginering. För batchkonvertering i produktion är detta värt att testa uttryckligen: ladda ett dokument med varje typsnitt (typeface) som dina RTF-källor använder och bekräfta att antalet sidor i utdata matchar vad du förväntar dig från en manuell förhandsgranskning
Tabeller, inbäddade bilder och de flesta Rich Text-formateringsfunktioner fungerar utan extra hantering eftersom Rich Edit renderar dem inbyggt (natively). Det enda området som kan vara överraskande är text som använder anpassat styckeavstånd eller indrag på första raden uttryckt i twips: Rich Edits interna koordinatsystem är i twips (1/1440 tum), medan DC-koordinaterna du ställer in i TFormatRange är i pixlar vid aktuell DPI. Kontrollen konverterar internt, men om du konstruerar RTF:en programmatiskt bör du verifiera att dina marginalvärden är i rätt enhet
DPI-medvetenhet och hög-DPI-skärmar
På en bildskärm som körs med 150 % skalning (144 DPI) kommer ScaleX(210, mmPixel) att returnera ett större antal pixlar än på en skärm med 100 %. PDF-biblioteket registrerar vilka pixeldimensioner du än skickar till GetCanvasDC och använder DPI-argumentet i LoadFromCanvasDc för att baklänges beräkna den fysiska sidstorleken i PDF:en. Så länge det DPI-värde du skickar med matchar den DPI som din applikation körs i, kommer utmatningens sidstorlek att vara korrekt oavsett bildskärmens skalning
Om din applikation inte är DPI-medveten (det gamla standardvärdet), skalar Windows skärmens DC och dina pixelberäkningar blir fel på hög-DPI-maskiner. Den enklaste fixen är att deklarera DPI-medvetenhet i applikationsmanifestet; applikationen tar då emot sanna enhetspixlar och de 96 som du skickar till LoadFromCanvasDc bör ersättas med den faktiska bildskärmens DPI som erhålls från GetDeviceCaps(GetDC(0), LOGPIXELSX). Kodexemplet ovan hårdkodar 96 eftersom det är lämpligt för en miljö med 100 % skalning och håller exemplet kort
Utmatningsstruktur: en fil per sida gentemot ett kombinerat dokument
Loopen ovan skriver varje sida till en separat PDF-fil. Huruvida det är vad du vill ha beror på den nedströms användningen. Rapportgenereringssystem behöver ofta enskilda sidor eftersom de sätter ihop det slutliga dokumentet senare genom att sammanfoga eller ordna om sidor. Om du vill ha en enda PDF från början, låter biblioteket dig skapa ett dokument med flera sidor i en enda session: skapa dokumentet en gång utanför loopen, anropa metoden för att lägga till sidor istället för SaveToFile inuti loopen, och spara det kompletta dokumentet efter att loopen har avslutats. Detta undviker mellanliggande filer och är rätt struktur för de flesta konverteringsscenarier för enskilda dokument
För stora RTF-filer är det värt att lägga till någon form av förloppsfeedback i loopen, eftersom konverteringshastigheten är ungefär proportionell mot sidantalet och ett dokument på 200 sidor kan ta några sekunder. Strukturen repeat...until är enkel att utöka: spåra teckenförskjutningen i en förloppsindikatoruppdatering (progress bar) efter varje iteration, med hjälp av LastChar delat med det totala antalet tecken från RichEdit1.GetTextLen
Metoderna GetCanvasDC och LoadFromCanvasDc som visas här är en del av losLab PDF Library för Delphi och C++Builder