Τεχνικό Άρθρο

Προεπισκόπηση εκτύπωσης PDF Library for Delphi και έξοδος Device-Context στο Delphi

Η απόδοση μιας σελίδας PDF πάνω σε device context των Windows για προεπισκόπηση εκτύπωσης βάζει τρία συστήματα συντεταγμένων στην ίδια γραμμή κώδικα, και σπάνια συμφωνούν. Η σελίδα PDF μετριέται σε points με την αρχή κάτω αριστερά. Το DC της οθόνης μετριέται σε pixels με την αρχή πάνω αριστερά και έναν συντελεστή ζουμ που επιλέγετε εσείς. Το DC του εκτυπωτή, εκείνο που υποτίθεται ότι προβλέπει η προεπισκόπηση, μετρά pixels στην ανάλυση της συσκευής αλλά τοποθετεί την αρχή του στη γωνία της εκτυπώσιμης περιοχής, όχι στη γωνία του φύλλου. Κάντε λάθος σε οποιοδήποτε από αυτά και η προεπισκόπηση δείχνει μια χαρά ενώ η τυπωμένη σελίδα βγαίνει μετατοπισμένη, κλιμακωμένη ή κομμένη σε κάποια άκρη. Το σύνηθες σύμπτωμα είναι μια φόρμα με περίγραμμα που στην προεπισκόπηση φαίνεται κεντραρισμένη και τυπώνεται με τις πάνω και τις αριστερές γραμμές κομμένες, γιατί ο εκτυπωτής laser δεν μπορεί να βάλει μελάνι στα εξωτερικά λίγα χιλιοστά και κανείς δεν το είπε στην προεπισκόπηση. Η losLab PDF Library (PDF Library for Delphi) καλύπτει ολόκληρη τη διαδρομή με κλήσεις απόδοσης σε device context, ένα επίπεδο ρύθμισης εικονικού εκτυπωτή και bitmaps προεπισκόπησης παραγόμενα από τις ίδιες τις μετρήσεις του εκτυπωτή, που είναι το κομμάτι το οποίο κάνει την προεπισκόπηση ειλικρινή ως προς εκείνο το περιθώριο

Η γεωμετρία του χαρτιού δεν είναι η εκτυπώσιμη γεωμετρία

Δύο ορθογώνια περιγράφουν κάθε στόχο εκτύπωσης, και η απόσταση ανάμεσά τους είναι εκεί που ζουν τα περισσότερα σφάλματα προεπισκόπησης. Το ορθογώνιο του χαρτιού είναι το φυσικό φύλλο. Το εκτυπώσιμο ορθογώνιο είναι η μικρότερη περιοχή στην οποία μπορεί πραγματικά να φτάσει η μηχανή εκτύπωσης, με εσοχή ένα περιθώριο υλικού που διαφέρει ανά μοντέλο εκτυπωτή και ενίοτε ανά δίσκο. Το επίπεδο εκτύπωσης της βιβλιοθήκης μετρά και τα δύο. Η υποκείμενη κλάση TPLPrinter εκθέτει τα PageWidth και PageHeight για την εκτυπώσιμη περιοχή, τα FullPageWidth και FullPageHeight για ολόκληρο το φύλλο, και τα PrintOffsetX με PrintOffsetY για το κενό ανάμεσα στις αρχές τους, όλα σε pixels συσκευής στην ανάλυση που αναφέρει το GetDPI. Μια ειλικρινής προεπισκόπηση κλιμακώνει ακριβώς αυτούς τους αριθμούς προς τα κάτω στην ανάλυση της οθόνης, αντί να ζωγραφίζει τη σελίδα μέσα σε όποιο ορθογώνιο τυχαίνει να έχει το control. Παραλείψτε αυτό το βήμα και η προεπισκόπηση υποθέτει σιωπηλά μηδενικό περιθώριο, που είναι η μόνη τιμή την οποία δεν χρησιμοποιεί κανένας πραγματικός εκτυπωτής

Διάγραμμα του PDF Library for Delphi για το πλήρες φύλλο χαρτιού έναντι του μικρότερου ορθογωνίου εκτύπωσης, με τα PrintOffsetX και PrintOffsetY να σηματοδοτούν το περιθώριο υλικού μεταξύ των αρχών τους
Το ορθογώνιο χαρτίου είναι το φυσικό φύλλο ενώ το ορθογώνιο εκτύπωσης είναι όσα η μηχανή εκτύπωσης μπορεί να φτάσει, και το διάστημα ανάμεσα στις αρχές τους φιλοξενεί τα περισσότερα σφάλματα προεπισκόπησης

Προεπισκόπηση στην οθόνη μέσω RenderPageToDC

Για ένα control προεπισκόπησης στην οθόνη, το RenderPageToDC(DPI, Page, DC) ζωγραφίζει μια σελίδα του φορτωμένου εγγράφου απευθείας σε οποιοδήποτε device context GDI, είτε αυτό είναι καμβάς TPaintBox, bitmap εκτός οθόνης ή DC metafile. Το όρισμα DPI ορίζει το ζουμ. Το 96 προσεγγίζει προβολή 100% σε κλασική οθόνη, και ο διπλασιασμός του διπλασιάζει το αποδιδόμενο μέγεθος

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // τα τρία αυτά είναι επίμονη κατάσταση της βιβλιοθήκης, όχι παράμετροι ανά κλήση:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

Η παγίδα είναι ότι η διαδρομή απόδοσης σε DC καθοδηγείται από επίμονη κατάσταση της βιβλιοθήκης και όχι από παραμέτρους ανά κλήση. Τα SetRenderDCOffset, SetRenderDCErasePage και SetRenderCropType επιμένουν το καθένα μέχρι κάτι να τα αλλάξει, οπότε ένας βρόχος μικρογραφιών που τρέχει αφότου ο χρήστης ρύθμισε τη μεγεθυμένη προβολή κληρονομεί όποιο offset ή crop άφησε πίσω της η προηγούμενη διαδρομή κώδικα. Το σύμπτωμα είναι μια προεπισκόπηση που παρεκκλίνει μόνο σε συγκεκριμένες ακολουθίες πλοήγησης, κάτι τόσο βασανιστικό στην αναπαραγωγή όσο γίνεται ένα σφάλμα. Το να ορίζετε όλη τη σχετική κατάσταση στην αρχή του handler ζωγραφικής, όπως παραπάνω, δεν κοστίζει τίποτα και εξαλείφει ολόκληρη την κατηγορία. Ένας δεύτερος πολλαπλασιαστής κρύβεται δίπλα. Η πραγματική ανάλυση εξόδου είναι η κλίμακα απόδοσης επί το όρισμα DPI, και παρότι το SetRenderScale έχει προεπιλογή 1.0, επιμένει και αυτό μόλις αλλάξει, οπότε μια λειτουργία εξαγωγής που το ανέβασε ξανακλιμακώνει αθόρυβα κάθε επόμενη προεπισκόπηση μέχρι κάτι να το επαναφέρει

Οι viewers με κύλιση και οι μερικές επανασχεδιάσεις έχουν ειδική παραλλαγή. Το RenderPageToDCClip δέχεται προδιαγραφή clip μαζί με το device context, ώστε η ακύρωση μιας ζώνης του παραθύρου να επανασχεδιάζει μόνο εκείνη τη ζώνη αντί να ξαναψηφιοποιεί ολόκληρη τη σελίδα. Σε υψηλό ζουμ πάνω σε σελίδες μεγάλου μεγέθους, αυτή είναι η διαφορά ανάμεσα σε έναν viewer που ακολουθεί τη μπάρα κύλισης και σε έναν που μουτζουρώνει πίσω της

Μια εργασία εκτύπωσης που ταιριάζει με την προεπισκόπηση

Η πλευρά της εκτύπωσης δουλεύει μέσω εικονικού εκτυπωτή. Το NewCustomPrinter κλωνοποιεί έναν εκτυπωτή του συστήματος σε ιδιωτική ρύθμιση της βιβλιοθήκης, και το SetupPrinter προσαρμόζει αυτόν τον κλώνο χωρίς να αγγίζει το DevMode ολόκληρου του μηχανήματος: το χαρτί μπαίνει ως ρύθμιση 1 (μια σταθερά DMPAPER_*) και ο προσανατολισμός ως ρύθμιση 11. Το κέρδος είναι η απομόνωση. Μια υπηρεσία μπορεί να τυπώνει ετικέτες A4 ενώ ο προεπιλεγμένος εκτυπωτής του ξενιστή μένει σε Letter, και τίποτα δεν χρειάζεται επαναφορά μετά

PDF Library for Delphi: Ροή από τον προεπιλεγμένο εκτυπωτή συστήματος μέσω NewCustomPrinter και SetupPrinter σε απομονωμένη εργασία εκτύπωσης που δεν αγγίζει ποτέ το DevMode όλης της μηχανής
Η SetupPrinter αναπροσανατολίζει έναν κλώνο ιδιωτικό για τη βιβλιοθήκη ώστε μια υπηρεσία να εκτυπώνει A4 ενώ ο προεπιλεγμένος εκτυπωτής του οικοδεσπότη κρατά το DevMode Letter του ανέπαφο
var
  Pdf: TPDFlib;
  Virt: WideString;
  Opt: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
    Pdf.SetupPrinter(Virt, 1, 9);        // ρύθμιση 1 = χαρτί, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // ρύθμιση 11 = προσανατολισμός, 1 = κατακόρυφος
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // προσαρμογή στο χαρτί, αυτόματη περιστροφή + κεντράρισμα
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

Το PrintOptions αξίζει προσεκτική ανάγνωση. Επιστρέφει ένα handle επιλογών που πρέπει να περάσετε στο PrintDocument ή στο PrintPages· δεν είναι περιβάλλουσα κατάσταση. Το να χτίσετε τις επιλογές και ύστερα να ξεχάσετε να περάσετε το handle αποτυγχάνει σιωπηλά. Η εργασία τυπώνεται με προεπιλογές, και κανείς δεν το προσέχει μέχρι τη στιγμή που, ενώ αναμενόταν πολιτική προσαρμογής στο χαρτί, μια υπερμεγέθης σελίδα βγαίνει κομμένη. Το όρισμα κλιμάκωσης σελίδας είναι εκεί που ζει αυτή η πολιτική. Η απουσία κλιμάκωσης διατηρεί τη διαστασιακή ακρίβεια, κάτι που μετράει για φόρμες που μετριούνται με χάρακα. Η προσαρμογή στο χαρτί ξανακλιμακώνει τα πάντα στο φύλλο. Η συρρίκνωση των μεγάλων σελίδων αφήνει ήσυχες τις κανονικές σελίδες και παρεμβαίνει μόνο όταν μια σελίδα ξεπερνά την εκτυπώσιμη περιοχή, που είναι συνήθως η σωστή προεπιλογή για ένα μεικτό σύνολο εγγράφων. Η σημαία αυτόματης περιστροφής και κεντραρίσματος χειρίζεται τις οριζόντιες σελίδες χωρίς δεύτερη διαδρομή κώδικα

Οι εφαρμογές που διαχειρίζονται ήδη ένα TPrinter μέσω της ροής διαλόγου του VCL μπορούν να το παραδώσουν απευθείας. Τα PrintDocumentToPrinterObject και PrintPagesToPrinterObject δέχονται το ρυθμισμένο instance TPrinter, κάτι που κρατά τον τυπικό διάλογο εκτύπωσης ως επιφάνεια ρύθμισης προς τον χρήστη ενώ η βιβλιοθήκη χειρίζεται την απόδοση των σελίδων. Η ανάμειξη των δύο προσεγγίσεων σε μία διαδρομή κώδικα τείνει να επαναφέρει ακριβώς τη γεωμετρική παρέκκλιση που όλη αυτή η δουλειά υποτίθεται ότι εξαλείφει, οπότε διαλέξτε μία. Η διαδρομή του εικονικού εκτυπωτή ταιριάζει σε υπηρεσίες χωρίς επίβλεψη· η διαδρομή του TPrinter ταιριάζει σε διαδραστικές εφαρμογές

Η επιλεκτική έξοδος δουλεύει με τον ίδιο τρόπο. Το PrintPages δέχεται συμβολοσειρά εύρους, οπότε περνώντας το όνομα του εικονικού εκτυπωτή, το '2-5,12' και το handle επιλογών τυπώνονται οι σελίδες 2 έως 5 και η 12 με το συμβόλαιο γεωμετρίας άθικτο, και η ίδια σύνταξη οδηγεί τις παραλλαγές εκτύπωσης σε αρχείο. Αυτές οι παραλλαγές αρχείου είναι η πρακτική απάντηση για περιβάλλον χωρίς επίβλεψη και χωρίς συνδεδεμένη φυσική συσκευή: έλεγχος παλινδρόμησης της γεωμετρίας εκτύπωσης σε έναν build server που δεν έχει καμία ουρά οδηγού. Αποδώστε το ίδιο έγγραφο με τις ίδιες επιλογές σε ένα artifact αρχείου σε κάθε build, και μια παλινδρόμηση γεωμετρίας γίνεται diff αντί για αναφορά πελάτη τρεις εβδομάδες αργότερα

Bitmaps προεπισκόπησης με τις μετρήσεις του ίδιου του εκτυπωτή

Μια προεπισκόπηση αποδοσμένη στα 96 DPI πάνω σε υποτιθέμενο μέγεθος σελίδας απαντά σε λάθος ερώτημα. Δείχνει πώς φαίνεται η σελίδα, όχι τι θα βάλει αυτός ο εκτυπωτής πάνω σε αυτό το χαρτί. Το GetPrintPreviewBitmapToString κλείνει το κενό χτίζοντας την προεπισκόπηση από τον ίδιο προσαρμοσμένο εκτυπωτή και το ίδιο handle επιλογών με την τελική εργασία, ώστε το μέγεθος χαρτιού, ο προσανατολισμός, η πολιτική κλιμάκωσης, η περιστροφή και το περιθώριο του υλικού να τροφοδοτούν όλα το bitmap. Αυτό που επιστρέφεται είναι αυτό που θα δείχνει το φύλλο

PDF Library for Delphi: Αντίθεση μεταξύ προεπισκόπησης οθόνης με υποτιθέμενο μέγεθος σελίδας που ανακριβώνει τα περιθώρια και bitmap πιστού στον εκτυπωτή δομημένου από το handle εκτυπωτή και επιλογών της ίδιας της εργασίας
Η GetPrintPreviewBitmapToString σχεδιάζει την προεπισκόπηση από τον ίδιο προσαρμοσμένο εκτυπωτή και handle επιλογών με την τελική εργασία, οπότε το bitmap δείχνει τα περιθώρια και την περιστροφή που θα λάβει πραγματικά το φύλλο
procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
  Data: AnsiString;
  Strm: TMemoryStream;
  Bmp: TBitmap;
begin
  Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
  Strm := TMemoryStream.Create;
  try
    Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
    Strm.Position := 0;
    Bmp := TBitmap.Create;
    try
      Bmp.LoadFromStream(Strm);
      PreviewImage.Picture.Assign(Bmp);
    finally
      Bmp.Free;
    end;
  finally
    Strm.Free;
  end;
end;

Το όρισμα MaxDimension περιορίζει τη μεγάλη πλευρά του bitmap. Τα 1200 pixels μένουν ευκρινή για έναν διάλογο προεπισκόπησης και κρατούν τη μνήμη μετρημένη ακόμη και για μηχανολογικά σχέδια μεγέθους E, όπου μια απόδοση σε πλήρη ανάλυση στα 600 DPI του εκτυπωτή θα έφτανε σε gigabytes

Απομνημόνευση των επιλογών εκτυπωτή του χρήστη

Οι διάλογοι εκτύπωσης που ξεχνούν τις ρυθμίσεις τους ανάμεσα σε συνεδρίες παράγουν δικά τους tickets υποστήριξης. Το ζεύγος DevMode, τα GetPrinterDevModeToString και SetPrinterDevModeFromString, σειριοποιεί την πλήρη ρύθμιση οδηγού ενός εκτυπωτή σε μια αδιαφανή συμβολοσειρά που μπορείτε να φυλάξετε στις προτιμήσεις χρήστη και να επαναφέρετε την επόμενη συνεδρία, μαζί με τις ειδικές για τον οδηγό επιλογές που κανένα γενικό API δεν μπαίνει στον κόπο να μοντελοποιήσει. Διατηρήστε τον εκτυπωτή κατ' όνομα από το GetPrinterNames, ποτέ κατά δείκτη λίστας. Η σειρά των δεικτών αλλάζει κάθε φορά που προστίθεται ή αφαιρείται εκτυπωτής, οπότε ένας αποθηκευμένος δείκτης δείχνει αθόρυβα σε λάθος συσκευή την επόμενη φορά που μετακινείται η λίστα. Το GetDefaultPrinterName καλύπτει την εφεδρεία όταν η θυμημένη συσκευή έχει εξαφανιστεί εντελώς

Η επιλογή δίσκου συμπληρώνει την ιστορία της διατήρησης. Το GetPrinterBins αναφέρει τις πηγές χαρτιού που εκθέτει ένας οδηγός, κάτι που μετράει για ροές με επιστολόχαρτο όπου η πρώτη σελίδα τραβιέται από τον δίσκο του επιστολόχαρτου και οι υπόλοιπες από απλό χαρτί. Αυτή είναι πολιτική που οι χρήστες περιμένουν από την εφαρμογή να τη θυμάται μαζί με όλα τα άλλα, και μια εργασία εκτύπωσης που προσγειώνεται σε λάθος χαρτί διαβάζεται ως σφάλμα ακόμη κι όταν κάθε byte του PDF ήταν σωστό

Κρατήστε μία μηχανή για προεπισκόπηση και εκτύπωση

Μια τελευταία απόφαση διέπει αθόρυβα την πιστότητα. Η επιλογή μηχανής απόδοσης ισχύει τόσο για τους προορισμούς οθόνης όσο και για τους προορισμούς εκτυπωτή, οπότε ο πειρασμός είναι να κάνετε προεπισκόπηση με μια γρήγορη μηχανή και εκτύπωση με μια ακριβή. Αντισταθείτε. Το να οδηγείτε την προεπισκόπηση και την εργασία μέσα από διαφορετικές μηχανές επαναφέρει ακριβώς εκείνη την παρέκκλιση πιστότητας που η προεπισκόπηση με τα δεδομένα του εκτυπωτή χτίστηκε για να εξαλείψει, και μάλιστα με τρόπο που φαίνεται μόνο στο χαρτί. Οι συμβιβασμοί ανάμεσα στην ενσωματωμένη μηχανή, την Cairo και την PDFium ζυγίζονται στην πολυμηχανική απόδοση PDF σε Delphi· διαλέξτε μία και χρησιμοποιήστε τη και στις δύο πλευρές

Τα έγγραφα που είναι πολύ μεγάλα για να φορτωθούν άνετα πριν από την εκτύπωση μπορούν να ανοιχτούν μέσω της διαδρομής άμεσης πρόσβασης που περιγράφεται στη συγχώνευση, διαχωρισμό και άμεση πρόσβαση μεγάλων PDF, η οποία αποδίδει σελίδες σε device context από handle αρχείου χωρίς να χτίζει το δέντρο του εγγράφου. Η πλήρης αναφορά του API εκτύπωσης βρίσκεται στη σελίδα προϊόντος losLab PDF Library for Delphi