Το RTF υπάρχει αρκετό καιρό ώστε να εμφανίζεται σε μέρη που κανείς δεν είχε προγραμματίσει: συστήματα παραγωγής αναφορών παλαιού τύπου, αγωγούς συγχώνευσης αλληλογραφίας (mail merge), αρχεία νομικών εγγράφων που προηγούνται των σύγχρονων επεξεργαστών κειμένου. Η άμεση (on the fly) μετατροπή του σε PDF είναι μια επαναλαμβανόμενη απαίτηση, και η προσέγγιση που λειτουργεί πραγματικά στα Windows δεν είναι ένας αποκλειστικός αναλυτής RTF αλλά η διαδρομή απόδοσης (rendering path) που τα ίδια τα Windows παρέχουν ήδη μέσω των TRichEdit και EM_FORMATRANGE. Η έκδοση DLL της Βιβλιοθήκης PDF της losLab εκθέτει ένα εικονικό πλαίσιο συσκευής (device context) που προσαρμόζεται απευθείας σε αυτόν τον αγωγό
Ο μηχανισμός: εικονικό DC και EM_FORMATRANGE
Τα στοιχεία ελέγχου Rich Edit μπορούν να σελιδοποιήσουν το περιεχόμενό τους για οποιοδήποτε πλαίσιο συσκευής, όχι μόνο για φυσικό εκτυπωτή. Το μήνυμα EM_FORMATRANGE λέει στο στοιχείο ελέγχου να διατάξει μια σειρά χαρακτήρων σε ένα δεδομένο DC και επιστρέφει τη θέση του τελευταίου χαρακτήρα που κατάφερε να χωρέσει. Καλέστε το επανειλημμένα, προωθώντας το cpMin κάθε φορά, και λαμβάνετε έξοδο σελίδα προς σελίδα. Η GetCanvasDC της Βιβλιοθήκης PDF της losLab παρέχει ένα DC στη μνήμη προσαρμοσμένο στις διαστάσεις σελίδας που καθορίζετε· μετά την απόδοση μιας σελίδας σε αυτό, η LoadFromCanvasDc καταγράφει το αποτέλεσμα ως σελίδα PDF. Αυτός είναι όλος ο αγωγός
Ένα πράγμα που πρέπει να γίνει σωστά από την αρχή: το στοιχείο ελέγχου TRichEdit πρέπει να έχει μέγεθος που να ταιριάζει με τη σελίδα-στόχο. Εάν το στοιχείο ελέγχου είναι μικρότερο ή μεγαλύτερο από τις διαστάσεις του DC, η σελιδοποίηση δεν θα ευθυγραμμιστεί με αυτό που καταλήγει στο PDF. Για έξοδο A4 η τυπική προσέγγιση είναι να ορίσετε τις διαστάσεις σε pixel του στοιχείου ελέγχου ώστε να ταιριάζουν με 210 x 297 mm στα 96 DPI πριν φορτώσετε το αρχείο RTF, χρησιμοποιώντας τους ίδιους βοηθούς κλίμακας (scale helpers) που θα χρησιμοποιήσετε για να ορίσετε το μέγεθος του DC
Υλοποίηση στο Delphi
Τα παρακάτω χρησιμοποιούν τη μονάδα εισαγωγής PDFlibAX_TLB, η οποία περιβάλλει την έκδοση DLL της βιβλιοθήκης. Η φόρμα φιλοξενεί ένα TRichEdit και ένα κουμπί· ο χειριστής (handler) OnCreate της φόρμας ρυθμίζει το μέγεθος του στοιχείου ελέγχου και φορτώνει το RTF, και το κλικ στο κουμπί οδηγεί τον βρόχο μετατροπής
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.
Τι κάνει ο βρόχος
Η PrintRtfBox γεμίζει τη δομή TFormatRange και την περνά στο στοιχείο ελέγχου Rich Edit μέσω της SendMessage. Το στοιχείο ελέγχου αποδίδει χαρακτήρες ξεκινώντας από το cpMin, σταματώντας όταν το DC γεμίσει, και επιστρέφει τη θέση του πρώτου χαρακτήρα που δεν ταίριαξε. Όταν η τιμή επιστροφής ισούται ή υπερβαίνει το συνολικό μήκος κειμένου, κάθε χαρακτήρας έχει αποδοθεί και η συνάρτηση επιστρέφει μηδέν, κάτι που τερματίζει τον βρόχο repeat...until
Κάθε επανάληψη παράγει ένα αρχείο PDF με το όνομα Output1.pdf, Output2.pdf, και ούτω καθεξής. Εάν θέλετε αντ' αυτού ένα ενιαίο έγγραφο πολλών σελίδων, το API προσάρτησης σελίδων της βιβλιοθήκης σας επιτρέπει να τα συναρμολογήσετε εκ των υστέρων, ή μπορείτε να αναδιαρθρώσετε τον βρόχο για να καλείτε την AddPage μέσα σε μια μοναδική συνεδρία (session) εγγράφου. Το μοτίβο SaveToFile ανά επανάληψη, ακολουθούμενο από την RemovePdfDocument παραπάνω, διατηρεί τη μέγιστη μνήμη οριοθετημένη στην αξία περιεχομένου μιας σελίδας, γεγονός που έχει σημασία για πολύ μεγάλα αρχεία RTF
Λεπτομέρειες αλλαγής μεγέθους που μπερδεύουν τον κόσμο
Το όρισμα 96 DPI στην LoadFromCanvasDc λέει στη βιβλιοθήκη σε ποια ανάλυση οθόνης αποδόθηκε το DC, ώστε να μπορεί να υπολογίσει τη σωστή αντιστοίχιση point-σε-pixel για τη σελίδα PDF. Εάν το κάνετε λάθος, το κείμενο θα εμφανιστεί με λάθος μέγεθος στην έξοδο, παρόλο που η εικόνα φαίνεται σωστή στην οθόνη
Το +100 που προστίθεται στο RcPage.Right και το RcPage.Bottom είναι ένα μικρό περιθώριο πέρα από την ορατή άκρη του στοιχείου ελέγχου. Η λειτουργία Rich Edit χρησιμοποιεί το ορθογώνιο rcPage για να αποφασίσει πού θα χωρίσει τις σελίδες· χωρίς το περιθώριο, μια γραμμή που πέφτει ακριβώς στο όριο μπορεί να διπλασιαστεί σε δύο σελίδες. Δεν είναι μια μαγική σταθερά: το θέλετε αρκετά μεγάλο ώστε το όριο της σελίδας να πέφτει καθαρά μέσα στην περιοχή διάταξης του στοιχείου ελέγχου αντί στο τελευταίο pixel
Τέλος, το στοιχείο ελέγχου πρέπει να είναι ήδη προσαρτημένο σε ένα ορατό παράθυρο φόρμας (visible form window) όταν εκτελείται η FormCreate, έτσι ώστε η λαβή του παραθύρου του να είναι έγκυρη πριν από την πρώτη κλήση της SendMessage. Ένα TRichEdit που δημιουργείται δυναμικά στο χρόνο εκτέλεσης (runtime) χρειάζεται μια ρητή κλήση HandleNeeded πριν ξεκινήσει ο βρόχος απόδοσης εάν η φόρμα δεν έχει εμφανιστεί ακόμα
Χειρισμός γραμματοσειρών και λειτουργιών RTF
Επειδή η απόδοση γίνεται από τον κινητήρα Windows Rich Edit, η υποκατάσταση γραμματοσειράς (font substitution) ακολουθεί τους ίδιους κανόνες που χρησιμοποιεί για την εμφάνιση και την εκτύπωση. Οι γραμματοσειρές που αναφέρονται στο αρχείο RTF και είναι εγκατεστημένες στο μηχάνημα θα αποδοθούν πιστά· γραμματοσειρές που λείπουν θα υποκατασταθούν σιωπηλά, γεγονός που μπορεί να μετατοπίσει το μήκος των γραμμών και τη σελιδοποίηση. Για παρτίδες (batch) μετατροπών στην παραγωγή αξίζει να το ελέγξετε ρητά: φορτώστε ένα έγγραφο με κάθε γραμματοσειρά που χρησιμοποιούν οι πηγές RTF σας και επιβεβαιώστε ότι ο αριθμός σελίδων εξόδου ταιριάζει με αυτό που περιμένετε από μια χειροκίνητη προεπισκόπηση εκτύπωσης
Οι πίνακες, οι ενσωματωμένες εικόνες και τα περισσότερα χαρακτηριστικά μορφοποίησης Rich Text λειτουργούν χωρίς καμία επιπλέον ρύθμιση επειδή η λειτουργία Rich Edit τα αποδίδει εγγενώς. Ο μόνος τομέας που μπορεί να είναι εκπληκτικός είναι το κείμενο που χρησιμοποιεί προσαρμοσμένη απόσταση παραγράφων (paragraph spacing) ή εσοχές πρώτης γραμμής (first-line indents) εκφρασμένες σε twip: το εσωτερικό σύστημα συντεταγμένων του Rich Edit είναι σε twip (1/1440 της ίντσας), ενώ οι συντεταγμένες DC που ορίζετε στο TFormatRange είναι σε pixel στο τρέχον DPI. Το στοιχείο ελέγχου κάνει μετατροπή εσωτερικά, αλλά αν κατασκευάζετε το RTF προγραμματιστικά, θα πρέπει να επαληθεύσετε ότι οι τιμές περιθωρίων σας είναι στη σωστή μονάδα
Επίγνωση DPI και οθόνες υψηλού DPI
Σε μια οθόνη που τρέχει σε κλιμάκωση (scaling) 150% (144 DPI), η ScaleX(210, mmPixel) θα επιστρέψει μεγαλύτερο αριθμό pixel από ό,τι σε μια οθόνη 100%. Η Βιβλιοθήκη PDF καταγράφει όποιες διαστάσεις σε pixel περνάτε στην GetCanvasDC και χρησιμοποιεί το όρισμα DPI στην LoadFromCanvasDc για να υπολογίσει αντίστροφα το φυσικό μέγεθος της σελίδας στο PDF. Εφόσον η τιμή DPI που περνάτε ταιριάζει με το DPI στο οποίο εκτελείται η εφαρμογή σας, το μέγεθος σελίδας εξόδου θα είναι σωστό ανεξάρτητα από την κλιμάκωση της οθόνης
Εάν η εφαρμογή σας δεν έχει επίγνωση DPI (η παλιά προεπιλογή), τα Windows κλιμακώνουν το DC της οθόνης και οι υπολογισμοί pixel σας θα είναι λανθασμένοι σε μηχανήματα υψηλού DPI. Η απλούστερη λύση είναι να δηλώσετε την επίγνωση DPI (DPI awareness) στο manifest της εφαρμογής· η εφαρμογή λαμβάνει τότε αληθινά pixel συσκευής και το 96 που περνάτε στην LoadFromCanvasDc θα πρέπει να αντικατασταθεί με το πραγματικό DPI της οθόνης που λαμβάνεται από την GetDeviceCaps(GetDC(0), LOGPIXELSX). Το δείγμα κώδικα παραπάνω ενσωματώνει σκληρά (hardcodes) το 96 επειδή είναι κατάλληλο για περιβάλλον κλιμάκωσης 100% και διατηρεί το παράδειγμα σύντομο
Δομή εξόδου: ένα αρχείο ανά σελίδα έναντι ενός συνδυασμένου εγγράφου
Ο παραπάνω βρόχος γράφει κάθε σελίδα σε ξεχωριστό αρχείο PDF. Το αν είναι αυτό που θέλετε εξαρτάται από τη μεταγενέστερη (downstream) χρήση. Τα συστήματα δημιουργίας αναφορών χρειάζονται συχνά μεμονωμένες σελίδες επειδή συναρμολογούν το τελικό έγγραφο αργότερα με συγχώνευση ή αναδιάταξη σελίδων. Αν θέλετε ένα μόνο PDF από την αρχή, η βιβλιοθήκη σάς επιτρέπει να δημιουργήσετε ένα έγγραφο με πολλές σελίδες σε μία μόνο συνεδρία (session): δημιουργήστε το έγγραφο μία φορά έξω από τον βρόχο, καλέστε τη μέθοδο προσθήκης σελίδας αντί για την SaveToFile μέσα στον βρόχο, και αποθηκεύστε το πλήρες έγγραφο μετά την έξοδο από τον βρόχο. Αυτό αποφεύγει τα ενδιάμεσα αρχεία και είναι η σωστή δομή για τα περισσότερα σενάρια μετατροπής μεμονωμένων εγγράφων
Για μεγάλα αρχεία RTF αξίζει να προσθέσετε κάποια ανατροφοδότηση προόδου στον βρόχο, καθώς ο ρυθμός μετατροπής είναι περίπου ανάλογος με τον αριθμό των σελίδων και ένα έγγραφο 200 σελίδων μπορεί να διαρκέσει μερικά δευτερόλεπτα. Η δομή repeat...until επεκτείνεται εύκολα: παρακολουθήστε τη μετατόπιση χαρακτήρων σε μια ενημέρωση της γραμμής προόδου μετά από κάθε επανάληψη, χρησιμοποιώντας το LastChar διαιρεμένο με τον συνολικό αριθμό χαρακτήρων από το RichEdit1.GetTextLen
Οι μέθοδοι GetCanvasDC και LoadFromCanvasDc που παρουσιάζονται εδώ αποτελούν μέρος της losLab PDF Library για Delphi και C++Builder