RTF har eksisteret længe nok til, at det dukker op steder, ingen havde planlagt: ældre rapportgeneratorer, flettekæder, juridiske dokumentarkiver, der går forud for moderne tekstbehandlere. Konvertering til PDF i farten er et tilbagevendende krav, og den tilgang, der rent faktisk fungerer på Windows, er ikke en dedikeret RTF-parser, men den gengivelsessti, Windows selv allerede leverer via TRichEdit og EM_FORMATRANGE. losLab PDF Library DLL-udgaven afslører en virtuel enhedskontekst, der sættes direkte ind i den pipeline
Mekanismen: virtuel DC og EM_FORMATRANGE
Rich Edit-kontrolelementer kan paginere deres indhold til enhver enhedskontekst, ikke kun en fysisk printer. Beskeden EM_FORMATRANGE beder kontrolelementet om at lægge en række tegn ud i en given DC og returnerer positionen for det sidste tegn, den formåede at passe ind. Kald den gentagne gange, fremfør cpMin hver gang, og du får side for side output. losLab PDF Library's GetCanvasDC giver en in-memory DC dimensioneret til de sidedimensioner, du angiver; efter at have gengivet en side i den, fanger LoadFromCanvasDc resultatet som en PDF-side. Det er hele pipelinen
Én ting skal gøres rigtigt på forhånd: TRichEdit-kontrolelementet skal dimensioneres til at matche målsiden. Hvis kontrolelementet er mindre eller større end DC-dimensionerne, vil pagineringen ikke stemme overens med, hvad der ender i PDF'en. For A4-output er standardtilgangen at indstille kontrolelementets pixeldimensioner til at matche 210 x 297 mm ved 96 DPI før indlæsning af RTF-filen, ved at bruge de samme skaleringshjælpere, som du vil bruge til at dimensionere DC'en
Delphi-implementering
Følgende bruger PDFlibAX_TLB importenheden, som indpakker DLL-udgaven af biblioteket. Formularen er vært for en TRichEdit og en knap; formularens OnCreate-handler dimensionerer kontrolelementet og indlæser RTF'en, og knapklikket 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.
Hvad løkken gør
PrintRtfBox udfylder TFormatRange-strukturen og overfører den til Rich Edit-kontrolelementet via SendMessage. Kontrolelementet gengiver tegn fra cpMin, stopper, når DC'en fyldes, og returnerer positionen for det første tegn, der ikke passede. Når returværdien er lig med eller overstiger den samlede tekstlængde, er hvert tegn blevet gengivet, og funktionen returnerer nul, hvilket afslutter repeat...until-løkken
Hver iteration producerer en PDF-fil med navnet Output1.pdf, Output2.pdf og så videre. Hvis du ønsker et enkelt flersidet dokument i stedet, lader bibliotekets sidetilføjelses-API dig samle dem bagefter, eller du kan omstrukturere løkken til at kalde AddPage inden for en enkelt dokumentsession. Mønsteret SaveToFile efterfulgt af RemovePdfDocument pr. iteration ovenfor holder spidshukommelsen afgrænset til indholdet af én side, hvilket har betydning for meget lange RTF-filer
Dimensioneringsdetaljer, der spænder ben for folk
Argumentet 96 DPI til LoadFromCanvasDc fortæller biblioteket, ved hvilken skærmopløsning DC'en blev gengivet, så den kan beregne den korrekte punkt-til-pixel-kortlægning for PDF-siden. Gør dette forkert, og teksten vises i den forkerte størrelse i outputtet, selvom billedet ser korrekt ud på skærmen
Den +100, der føjes til RcPage.Right og RcPage.Bottom, er en lille margin ud over kontrolelementets synlige kant. Rich Edit bruger rcPage-rektanglet til at beslutte, hvor sider skal opdeles; uden marginen kan en linje, der falder præcist ved grænsen, blive duplikeret på tværs af to sider. Det er ikke en magisk konstant: du vil have den stor nok til, at sidegrænsen falder rent inde i kontrolelementets layoutområde i stedet for på den sidste pixel
Endelig skal kontrolelementet allerede være knyttet til et synligt formularvindue, når FormCreate kører, så dets vindueshåndtag er gyldigt før det første opkald til SendMessage. En TRichEdit oprettet dynamisk under kørsel har brug for et eksplicit HandleNeeded-kald før gengivelsesløkken begynder, hvis formularen endnu ikke er blevet vist
Håndtering af skrifttyper og RTF-funktioner
Fordi gengivelsen udføres af Windows Rich Edit-motoren, følger udskiftning af skrifttype de samme regler, som den bruger til visning og udskrivning. Skrifttyper, der refereres til i RTF-filen, og som er installeret på maskinen, vil blive gengivet trofast; skrifttyper, der mangler, vil blive udskiftet stille, hvilket kan forskyde linjelængder og paginering. Ved batchkonvertering i produktion er dette værd at teste eksplicit: indlæs et dokument med hver skrifttype, dine RTF-kilder bruger, og bekræft, at output-sideantallet stemmer overens med det, du forventer fra et manuelt udskriftseksempel
Tabeller, indlejrede billeder og de fleste Rich Text-formateringsfunktioner fungerer uden nogen ekstra håndtering, fordi Rich Edit gengiver dem indbygget. Det ene område, der kan være overraskende, er tekst, der bruger tilpasset afsnitsafstand eller førstelinjeindrykning udtrykt i twips: Rich Edits interne koordinatsystem er i twips (1/1440 tomme), mens de DC-koordinater, du indstiller i TFormatRange, er i pixels ved den aktuelle DPI. Kontrolelementet konverterer internt, men hvis du konstruerer RTF'en programmatisk, bør du kontrollere, at dine marginværdier er i den rigtige enhed
DPI-bevidsthed og high-DPI-skærme
På en skærm, der kører ved 150% skalering (144 DPI), returnerer ScaleX(210, mmPixel) et større antal pixels end på en 100% skærm. PDF-biblioteket registrerer de pixeldimensioner, du overfører til GetCanvasDC, og bruger DPI-argumentet i LoadFromCanvasDc til at tilbageberegne den fysiske sidestørrelse i PDF'en. Så længe den DPI-værdi, du overfører, stemmer overens med den DPI, din applikation kører ved, vil output-sidestørrelsen være korrekt, uanset skærmens skalering
Hvis din applikation er DPI-ubevidst (den gamle standard), skalerer Windows skærmens DC, og dine pixelberegninger vil være forkerte på high-DPI-maskiner. Den enkleste løsning er at deklarere DPI-bevidsthed i applikationens manifest; applikationen modtager derefter ægte enhedspixels, og de 96, du overfører til LoadFromCanvasDc, skal erstattes med den faktiske skærm-DPI opnået fra GetDeviceCaps(GetDC(0), LOGPIXELSX). Kodeeksemplet ovenfor hårdkoder 96, fordi det passer til et miljø med 100% skalering og holder eksemplet kort
Output-struktur: én fil pr. side versus et kombineret dokument
Løkken ovenfor skriver hver side til en separat PDF-fil. Om det er det, du vil, afhænger af downstream-brugen. Systemer til rapportgenerering har ofte brug for individuelle sider, fordi de samler det endelige dokument senere ved at flette eller omordne sider. Hvis du vil have en enkelt PDF fra starten, lader biblioteket dig oprette et dokument med flere sider i en enkelt session: Opret dokumentet én gang uden for løkken, kalde sidetilføjelsesmetoden i stedet for SaveToFile inde i løkken, og gem det komplette dokument, når løkken afsluttes. Dette undgår de mellemliggende filer og er den rigtige struktur for de fleste enkeltdokuments-konverteringsscenarier
For store RTF-filer er det værd at tilføje noget fremskridtsfeedback i løkken, da konverteringshastigheden stort set er proportional med sideantal, og et 200-siders dokument kan tage et par sekunder. repeat...until-strukturen er nem at udvide: Spor tegnforskydningen i en statuslinjeopdatering efter hver iteration ved hjælp af LastChar divideret med det samlede antal tegn fra RichEdit1.GetTextLen
Metoderne GetCanvasDC og LoadFromCanvasDc vist her er en del af losLab PDF Library til Delphi og C++Builder