Οι συντεταγμένες PDF είναι σε σημεία (points), οι συντεταγμένες εκτυπωτή είναι σε μονάδες συσκευής (device units) και τα δύο δεν έχουν καμία σχέση μεταξύ τους μέχρι να τα μετατρέψετε σκόπιμα. Αυτή η αναντιστοιχία είναι η ρίζα των περισσότερων κακών εκτυπώσεων σε εφαρμογές Delphi: ο κώδικας στέλνει το σωστό αρχείο, αλλά η σελίδα βγαίνει περικομμένη, τεντωμένη (stretched) ή κενή. Το PDFium Component χειρίζεται την πλευρά της απόδοσης (rendering) καθαρά· η υδραυλική (plumbing) του εκτυπωτή είναι τυπική VCL. Τα δύο ταιριάζουν μαζί με μια μέτρια ποσότητα κώδικα, εφόσον κατανοήσετε τι περιμένει η κάθε πλευρά
Πώς λειτουργεί η διοχέτευση (pipeline) "απόδοση-και-μετά-εκτύπωση" (render-then-print)
Το PDFium Component δεν μιλάει απευθείας στους εκτυπωτές. Το μοτίβο είναι: αποδώστε (render) μια σελίδα σε ένα TBitmap στην ανάλυση που θέλετε, και στη συνέχεια μεταφέρετε αυτό το bitmap στον καμβά του εκτυπωτή με την StretchDIBits. Η TPdf.RenderPage επιστρέφει ένα bitmap που ανήκει στον καλούντα (caller-owned), οπότε εσείς ελέγχετε τις διαστάσεις pixel. Περάστε το [rePrinting] στο σύνολο επιλογών (options set) και το PDFium αλλάζει τη διαδρομή απόδοσής (rendering path) του σε μία που παραλείπει τα εφέ που αφορούν μόνο την οθόνη (όπως το LCD subpixel hinting) και χειρίζεται σωστά το MediaBox της σελίδας για έξοδο εκτύπωσης. Αν παραλείψετε το rePrinting, αυτό που στέλνετε στον εκτυπωτή είναι μια απόδοση οθόνης (screen render), η οποία φαίνεται μια χαρά σε μια οθόνη (monitor) αλλά τείνει να παράγει πιο "μαλακή" (softer) έξοδο σε εκτυπωτές υψηλού DPI, επειδή οι αποφάσεις υπόδειξης (hinting) που λαμβάνονται για οθόνες 96 DPI δεν ταιριάζουν σε εκτυπώσεις 300 ή 600 DPI
Το TPdf.Active είναι η μόνη πύλη που πρέπει να ελέγξετε πριν αγγίξετε οποιαδήποτε ιδιότητα της σελίδας. Το στοιχείο (component) καταπίνει τα σφάλματα φόρτωσης σιωπηλά: ο ορισμός του Active := True σε ένα κατεστραμμένο αρχείο ή ένα αρχείο που προστατεύεται με κωδικό πρόσβασης δεν εγείρει εξαίρεση· απλώς αφήνει το Active ως False. Πάντα να το ελέγχετε μετά την ανάθεση. Η ανάγνωση του PageCount ή του PageWidth σε ένα ανενεργό έγγραφο επιστρέφει μηδέν, γεγονός που παράγει σιωπηλά no-ops (αδράνειες) που είναι πολύ δύσκολο να διαγνωστούν μόλις φτάσουν στον ουρά εκτύπωσης (spooler)
Ένας ελάχιστος βρόχος εκτύπωσης (minimal print loop)
Η απλούστερη λειτουργική περίπτωση φορτώνει ένα αρχείο, ανοίγει μια εργασία εκτύπωσης, επαναλαμβάνει (iterates) τις σελίδες και κλείνει. Η μόνη δύσκολη λεπτομέρεια είναι ότι η Printer.NewPage δεν πρέπει να καλείται πριν από την πρώτη σελίδα, εξ ου και η σημαία (flag) FirstPage. Η μεταφορά StretchDIBits περνάει μέσα από τα GetDIBSizes και GetDIB για να τραβήξει (pull) ανεξάρτητα από τη συσκευή bits από τη λαβή (handle) του bitmap, και στη συνέχεια τα ζωγραφίζει στον καμβά του εκτυπωτή στο πλήρες μέγεθος της σελίδας:
procedure PrintPdfFile(const FileName: string);
var
Pdf: TPdf;
I: Integer;
Bitmap: TBitmap;
InfoHeaderSize, ImageSize: DWORD;
InfoHeader: PBitmapInfo;
Image: Pointer;
FirstPage: Boolean;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
if not Pdf.Active then
Exit; // load failed silently; bail out
Printer.Title := Pdf.Title;
Printer.BeginDoc;
try
FirstPage := True;
for I := 1 to Pdf.PageCount do
begin
if FirstPage then
FirstPage := False
else
Printer.NewPage;
Pdf.PageNumber := I;
// Render at printer resolution; rePrinting adjusts the render path
Bitmap := Pdf.RenderPage(
0, 0,
Printer.PageWidth,
Printer.PageHeight,
ro0,
[rePrinting]
);
try
GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
InfoHeader := AllocMem(InfoHeaderSize);
try
Image := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
StretchDIBits(
Printer.Canvas.Handle,
0, 0, Printer.PageWidth, Printer.PageHeight,
0, 0, Bitmap.Width, Bitmap.Height,
Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
);
finally
FreeMem(Image);
end;
finally
FreeMem(InfoHeader);
end;
finally
Bitmap.Free;
end;
end;
finally
Printer.EndDoc;
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Το να περάσετε τα Printer.PageWidth και Printer.PageHeight ως διαστάσεις του bitmap, σημαίνει ότι αποδίδετε στο εγγενές μέγεθος pixel του εκτυπωτή, το οποίο λαμβάνει ήδη υπόψη το DPI της συσκευής. Στη συνέχεια, η κλήση StretchDIBits αντιστοιχίζει αυτά τα pixel 1:1 πάνω στη σελίδα. Αυτό σας δίνει την καλύτερη εφικτή πιστότητα χωρίς καμία ρητή αριθμητική DPI, αλλά λειτουργεί μόνο όταν η σελίδα PDF και το φυσικό χαρτί τυχαίνει να έχουν το ίδιο μέγεθος. Όταν διαφέρουν, χρειάζεστε ρητή κλιμάκωση (scaling)
Κλιμάκωση (Scaling) όταν διαφέρουν τα μεγέθη της σελίδας και του χαρτιού
Μια σελίδα PDF σε κατακόρυφο προσανατολισμό A4 (A4 portrait) δεν προσαρμόζεται αυτόματα σε έναν εκτυπωτή US Letter, και μια σελίδα τοπίου (landscape) που τροφοδοτείται σε έναν εκτυπωτή με κατακόρυφο προσανατολισμό (portrait-oriented) θα περικοπεί (clip). Η τυπική προσέγγιση είναι να υπολογίσετε έναν ομοιόμορφο συντελεστή κλίμακας (uniform scale factor) από την αναλογία των pixels του εκτυπωτή προς τα σημεία (points) του PDF, και στη συνέχεια να τον εφαρμόσετε και στις δύο διαστάσεις ώστε να διατηρηθεί η αναλογία διαστάσεων (aspect ratio). Τα Pdf.PageWidth και Pdf.PageHeight εκθέτουν τις τρέχουσες διαστάσεις της σελίδας σε σημεία (points), όπου ένα σημείο είναι 1/72 της ίντσας. Ο πολλαπλασιασμός με ένα DPI-στόχο και η διαίρεση με το 72 μετατρέπεται σε pixels σε αυτήν την ανάλυση. Πάρτε το Min των αναλογιών X και Y για να βρείτε τη μεγαλύτερη κλίμακα που εξακολουθεί να χωράει εντός της εκτυπώσιμης περιοχής:
// Fit PDF page to printable area, preserving aspect ratio
var
ScaleX, ScaleY, Scale: Double;
DestWidth, DestHeight: Integer;
Dpi: Integer;
begin
Dpi := 300; // target render resolution
Pdf.PageNumber := PageIndex;
ScaleX := Printer.PageWidth / (Pdf.PageWidth * Dpi / 72);
ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
Scale := Min(ScaleX, ScaleY);
// Clamp to 1.0 for shrink-to-fit only (no enlargement)
if Scale > 1.0 then Scale := 1.0;
DestWidth := Round(Pdf.PageWidth * Dpi / 72 * Scale);
DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);
Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
[rePrinting, reAnnotations]);
// ... transfer with StretchDIBits as above
end;
Η απόδοση σε Dpi = 300 ταιριάζει στους περισσότερους εκτυπωτές γραφείου. Στα 600 DPI, το bitmap για μία μόνο σελίδα A4 φτάνει περίπου τα 34 megapixel, το οποίο είναι περίπου 100 MB ως bitmap 32-bit· το κέρδος στην ποιότητα για συνηθισμένα έγγραφα κειμένου είναι ελάχιστο και το κόστος μνήμης ανά σελίδα είναι σημαντικό. Κρατήστε τα 600 DPI για τυπογραφεία ή για τεχνικά σχέδια με πολλά διανύσματα (vector-heavy), όπου πραγματικά έχει σημασία
Η σημαία (flag) reAnnotations στο δεύτερο μπλοκ κώδικα είναι ανεξάρτητη από το rePrinting. Συμπεριλάβετέ τη όταν ο χρήστης περιμένει να εμφανιστούν στο χαρτί σφραγίδες, επισημάνσεις (highlights) και πλαίσια σχολίων. Παραλείψτε τη για έξοδο μόνο περιεχομένου. Και οι δύο σημαίες μπορούν να συνδυαστούν ελεύθερα
Περιστροφή σελίδας
Το PDFium αποθηκεύει την περιστροφή σελίδας στο PDF ως καταχώρηση /Rotate, η οποία είναι προσβάσιμη μέσω της Pdf.PageRotation, η οποία επιστρέφει μια τιμή TRotation (ro0, ro90, ro180, ro270). Το σύστημα συντεταγμένων του εκτυπωτή αντιστρέφει τις περιστροφές 90 και 270 μοιρών σε σχέση με την οθόνη. Εάν περάσετε την ακατέργαστη τιμή PageRotation απευθείας στη RenderPage χωρίς καμία προσαρμογή, οι σελίδες τοπίου (landscape) που είναι ενσωματωμένες σε ένα έγγραφο κατακόρυφου προσανατολισμού (portrait) θα εκτυπωθούν ανάποδα στους περισσότερους οδηγούς (drivers) εκτυπωτών των Windows. Η λύση είναι μια απλή εναλλαγή (swap) πριν από την κλήση απόδοσης (render call): αντιστοιχίστε το ro90 σε ro270 και το ro270 πίσω σε ro90, αφήνοντας τα ro0 και ro180 αμετάβλητα
Επαληθεύστε αυτή τη συμπεριφορά στον συγκεκριμένο εκτυπωτή-στόχο σας πριν από την παράδοση (shipping). Η συμπεριφορά των προγραμμάτων οδήγησης (drivers) όσον αφορά την περιστροφή δεν είναι ομοιόμορφη μεταξύ των κατασκευαστών, και ορισμένοι οδηγοί εφαρμόζουν τη δική τους διόρθωση περιστροφής σε επίπεδο GDI. Εάν δείτε διπλή περιστροφή, αφαιρέστε την εναλλαγή (swap)· εάν δεν δείτε καμία διόρθωση, προσθέστε την. Ένα έγγραφο μικτού προσανατολισμού, με εναλλασσόμενες σελίδες κατακόρυφου προσανατολισμού (portrait) και τοπίου (landscape), είναι ο πιο γρήγορος τρόπος για να πιάσετε οποιαδήποτε από τις δύο καταστάσεις αποτυχίας (failure mode) κατά τη διάρκεια των δοκιμών
Διαχείριση μνήμης σε μια μακρά εργασία εκτύπωσης
Κάθε κλήση στην RenderPage κατανέμει ένα νέο TBitmap που ανήκει στον καλούντα και πρέπει να απελευθερωθεί (free). Στον παραπάνω βρόχο, το μπλοκ try/finally Bitmap.Free το χειρίζεται σωστά για μία σελίδα τη φορά. Μην συσσωρεύετε bitmaps σε πολλές σελίδες: μια απόδοση 300-DPI ενός εγγράφου 200 σελίδων θα κατανάλωνε gigabytes πριν η πρώτη σελίδα φτάσει στην ουρά εκτύπωσης (spooler). Απελευθερώστε (free) κάθε bitmap πριν προχωρήσετε στην επόμενη σελίδα
Το ζεύγος AllocMem / FreeMem μέσα στο μπλοκ μεταφοράς ακολουθεί τον ίδιο κανόνα. Η GetDIBSizes σας λέει πόση μνήμη χρειάζονται η κεφαλίδα (header) DIB και τα δεδομένα pixel· εσείς κατανέμετε, γεμίζετε, ζωγραφίζετε και απελευθερώνετε όλα μέσα στο πεδίο εφαρμογής (scope) μιας σελίδας. Αν αφήσετε οποιοδήποτε από τα μπλοκ να διαρρεύσει (leak), η εργασία εκτύπωσης θα εξαντλήσει τον σωρό της διεργασίας (process heap) σε έγγραφα μεγαλύτερα από μερικές δεκάδες σελίδες
Αν χρειάζεται να εκτελείτε εργασίες εκτύπωσης σε νήμα παρασκηνίου (background thread), διατηρήστε το TPdf και όλες τις κλήσεις εκτυπωτή VCL στο ίδιο νήμα. Το ίδιο το TPdf δεν είναι ασφαλές για νήματα (thread-safe) μεταξύ παρουσιών (instances) που μοιράζονται την παγκόσμια κατάσταση του PDFium DLL· το ασφαλέστερο μοντέλο είναι ένα TPdf ανά νήμα, με το καθένα να φορτώνει το δικό του αντίγραφο του αρχείου
Το API απόδοσης (rendering) και εγγράφου που εμφανίζεται εδώ αποτελεί μέρος του PDFium Component για Delphi και C++Builder