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

Απόδοση PDF με πολλαπλές μηχανές στο Delphi: Ενσωματωμένη, Cairo και PDFium με PDF Library for Delphi

Τρεις rasterizers μπορούν να διαβάσουν το ίδιο PDF και να διαφωνήσουν για το τι λέει. Η ενσωματωμένη μηχανή του PDF Library for Delphi είναι εκείνη που έρχεται χωρίς επιπλέον αρχεία και αποδίδει τα πάντα με επάρκεια, γι' αυτό και κερδίζει τη θέση της προεπιλογής. Η Cairo φέρνει διαφορετικό μηχανισμό διαφάνειας και εξομάλυνσης, και τείνει να είναι εκείνη στην οποία καταφεύγει κανείς όταν τα soft masks ή τα blend modes βγαίνουν λάθος αλλού. Η PDFium κουβαλά τον κώδικα απόδοσης του Chrome, οπότε μια σελίδα που φαίνεται σωστή μέσα σε browser συνήθως φαίνεται σωστή και υπό την PDFium, με κόστος ένα ογκώδες DLL και ένα bitness που επιμένει να ταιριάζει. Καμία από τις τρεις δεν είναι σωστή αφηρημένα. Η ορθότητα κρίνεται ανά έγγραφο, και ο μόνος έντιμος τρόπος να μάθετε ποια μηχανή χειρίζεται σωστά ένα δεδομένο σώμα εγγράφων είναι να το περάσετε από την καθεμία τους

Αυτό ακριβώς συνηγορεί υπέρ του να αντιμετωπίζεται η μηχανή ως επιλογή χρόνου εκτέλεσης και όχι χρόνου μεταγλώττισης. Το PDF Library for Delphi, η βιβλιοθήκη PDF για Delphi και C++Builder από τη losLab, τοποθετεί και τις τρεις πίσω από μία ενιαία επιφάνεια απόδοσης, ώστε η απόφαση να κοστίζει έναν ακέραιο αντί για μια διακλάδωση κώδικα. Τα υπόλοιπα καταλήγουν στο να επιλέγετε ανάμεσά τους με ασφάλεια, να επιβεβαιώνετε ποιες μηχανές κουβαλά πραγματικά ένα εγκατεστημένο binary και να μην αφήνετε την κατάσταση απόδοσης να δηλητηριάζει αθόρυβα την επόμενη εργασία

Τρεις rasterizers πίσω από μία επιφάνεια κλήσεων

Η βιβλιοθήκη αριθμεί τις μηχανές της. Η μηχανή 1 είναι ο ενσωματωμένος renderer, η προεπιλογή, με επιλογές εξομάλυνσης GDI+ στα Windows. Η μηχανή 2 είναι η Cairo και η μηχανή 3 η PDFium, και οι δύο επιλέγονται σε χρόνο εκτέλεσης μέσω της SelectRenderer. Οι δύο εξωτερικές μηχανές φορτώνονται από DLL των οποίων τις διαδρομές δίνετε με τις SetCairoFileName και SetPDFiumFileName πριν τις επιλέξετε. Όποια μηχανή κι αν είναι ενεργή, η δουλειά περνά από τις ίδιες κλήσεις: RenderPageToFile, RenderPageToStream, RenderDocumentToFile. Η εναλλαγή μηχανών μετακινεί έναν αριθμό· ο υπόλοιπος κώδικας απόδοσής σας δεν το αντιλαμβάνεται ποτέ

Το μοντέλο προορισμού φτάνει πολύ πιο πέρα από τα bitmap. Η κλάση του renderer στοχεύει επίσης metafiles (WMF, EMF, EMF+), EPS, απευθείας device contexts, εκτυπωτές και HTML5, με τη Cairo και την PDFium να εμφανίζονται ως πρόσθετοι προορισμοί μόνο όταν έχουν μεταγλωττιστεί μέσα. Η έξοδος raster είναι εκεί όπου οι τρεις μηχανές αποκλίνουν πιο ορατά, οπότε αυτήν χρησιμοποιούν τα παραδείγματα εδώ

Τρεις μηχανές απόδοσης PDF πίσω από μία επιφάνεια κλήσεων: το SelectRenderer εναλλάσσεται μεταξύ της ενσωματωμένης μηχανής, του Cairo και του PDFium, ενώ ο κώδικας εφαρμογής συνεχίζει να καλεί τις ίδιες συναρτήσεις απόδοσης
Η SelectRenderer ανταλλάσσει έναν ακέραιο για να μεταφέρει την εργασία ανάμεσα στις μηχανές ενσωματωμένη, Cairo και PDFium. Ο κώδικας εφαρμογής συνεχίζει να καλεί RenderPageToFile και λοιπές όποια μηχανή κι αν παρήγαγε τα pixel

Ποτέ μην υποθέτετε ότι μια μηχανή υπάρχει: ελέγξτε την στην εκκίνηση

Η Cairo και η PDFium είναι δυνατότητες υπό συνθήκη μεταγλώττισης, πράγμα που σημαίνει ότι ένα binary μπορεί να χτιστεί εντελώς χωρίς αυτές. Όταν συμβαίνει αυτό, το αίτημα για τη μηχανή 2 ή 3 δεν προκαλεί τίποτα. Η SelectRenderer απλώς επιστρέφει τιμή διαφορετική από το ID που ζητήσατε, και ο κώδικας που αγνοεί την επιστρεφόμενη τιμή συνεχίζει να αποδίδει με όποια μηχανή ήταν ήδη ενεργή. Η άμυνα είναι ένας έλεγχος στην εκκίνηση που ζητά από κάθε μηχανή να δηλώσει την ταυτότητά της και καταγράφει την απάντηση:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // η μηχανή 1 είναι πάντα παρούσα
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // επαναφορά της προεπιλογής πριν από την πραγματική δουλειά
end;

Τρέξτε αυτόν τον έλεγχο μία φορά στην εκκίνηση και γράψτε το αποτέλεσμά του στο log δίπλα σε κάθε εργασία απόδοσης. Το πιο συχνό ερώτημα όταν ένας πελάτης αναφέρει διαφορά στην απόδοση είναι ποιες μηχανές έχει πραγματικά η εγκατάστασή του, και μια μονογραμμική απάντηση που κάθεται στο log το ξεκαθαρίζει χωρίς συνεδρία απομακρυσμένης επιφάνειας εργασίας. Μια χρήσιμη παρενέργεια: αν η ίδια η SetPDFiumFileName επιστρέψει 0, ξέρετε ήδη ότι το πρόβλημα είναι το DLL (λάθος διαδρομή, λάθος bitness, μια εξάρτηση που λείπει) και όχι ένα binary μεταγλωττισμένο χωρίς υποστήριξη PDFium, επειδή η κλήση της διαδρομής δεν επίλυσε τίποτα προτού καν τρέξει η SelectRenderer

Δέκα μορφές εξόδου πίσω από έναν ακέραιο Options

Η παράμετρος Options στις κλήσεις απόδοσης επιλέγει την κωδικοποίηση εξόδου: 0 είναι BMP, 1 JPEG, 2 WMF, 3 EMF, 4 EPS, 5 PNG, 6 GIF, 7 TIFF, 8 EMF+ και 9 HTML5. Το PNG (5) είναι η λογική προεπιλογή για προεπισκοπήσεις και εικόνες σελίδων προς αρχειοθέτηση. Το JPEG (1), σε συνδυασμό με την SetJPEGQuality, είναι η καλύτερη επιλογή για φωτογραφικές σαρώσεις όπου το μέγεθος του αρχείου μετράει περισσότερο από τις κοφτερές ακμές

Μία μορφή κρύβει μια απαίτηση για τη ροή προορισμού. Η διαδρομή BMP γράφει πρώτα τα δεδομένα της εικόνας και έπειτα αναζητά πίσω στη μετατόπιση 0x26 για να διορθώσει τα πεδία ανάλυσης στην κεφαλίδα. Στρέψτε την σε μια ροή μόνο προς τα εμπρός, σε ένα περίβλημα συμπίεσης ή σε ένα δικτυακό socket, και η κλήση αποτυγχάνει με τρόπο που διαβάζεται σαν σφάλμα μηχανής αλλά δεν είναι. Όταν ένας μη αναζητήσιμος προορισμός είναι αναπόφευκτος, αποδώστε PNG αντί γι' αυτό, ή περάστε το BMP μέσα από μια ροή μνήμης και αντιγράψτε το παρακάτω μόλις ολοκληρωθεί

Το DPI που περνάτε δεν είναι το DPI που παίρνετε

Κάθε κλήση απόδοσης δέχεται ένα όρισμα DPI, αλλά η ανάλυση που παίρνετε στην πραγματικότητα είναι αυτή η τιμή πολλαπλασιασμένη με την καθολική κλίμακα απόδοσης. Η SetRenderScale ξεκινά στο 1.0, και μόλις την αλλάξετε ο νέος συντελεστής εφαρμόζεται σιωπηλά σε κάθε μεταγενέστερη απόδοση σε εκείνο το instance:

PDF.SetRenderScale(2.0);                    // κάθε μεταγενέστερη απόδοση διπλασιάζεται
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // στην πράξη 300 DPI
PDF.SetRenderScale(1.0);                    // επαναφορά, αλλιώς οι μικρογραφίες βγαίνουν τεράστιες

Η ίδια εμμονή ισχύει για την SetRenderCropType και για τη ρύθμιση ποιότητας JPEG. Σε μια υπηρεσία που παράγει μικρογραφίες, προεπισκοπήσεις και εικόνες σε ανάλυση εκτύπωσης από ένα κοινό instance, αυτές οι υπολειμματικές ρυθμίσεις είναι ο πραγματικός λόγος πίσω από το περιστασιακό δελτίο «οι μικρογραφίες έγιναν ξαφνικά 40 MB». Δύο καθαρές διέξοδοι: επαναφέρετε τη σχετική κατάσταση στην αρχή κάθε λειτουργίας, ή αφιερώστε ξεχωριστό instance σε κάθε προφίλ εξόδου ώστε τίποτα να μη διαρρέει ανάμεσά τους

PDF Library for Delphi: Διάγραμμα ροής δοκιμής μηχανών κατά την εκκίνηση: κάθε renderer επιβεβαιώνει τη διαδρομή DLL του και την απάντησή του στο SelectRenderer πριν καταγραφεί σύνοψη διαθεσιμότητας δίπλα σε κάθε εργασία απόδοσης
Μια αποτυχημένη κλήση path ενοχοποιεί τη DLL, ενώ ένα αλληλοσυγκρουόμενο αποτέλεσμα SelectRenderer σημαίνει ότι το δυαδικό δεν μεταγλωττίστηκε ποτέ με τη μηχανή. Η δοκιμή τρέχει μία φορά και η μονόγραμμη σύνοψή της τακτοποιεί τις περισσότερες ερωτήσεις απόδοσης πελατών

Ρυθμίστε την προεπιλεγμένη μηχανή πριν καταφύγετε σε άλλη

Ένα εκπληκτικό μερίδιο των αιτημάτων «χρειαζόμαστε άλλη μηχανή» αποδεικνύεται τελικά πρόβλημα ρυθμίσεων μεταμφιεσμένο. Ο ενσωματωμένος renderer εκθέτει τη συμπεριφορά εξομάλυνσής του μέσω της SetGDIPlusOptions και της ευρύτερης οικογένειας SetRenderOptions, ενώ η SetGDIPlusFileName σας επιτρέπει να τον στρέψετε σε συγκεκριμένο runtime GDI+ όταν ένα περιβάλλον εγκατάστασης διαθέτει κάποιο ασυνήθιστο. Οδοντωτά γραμμικά σχέδια σε χαμηλό DPI, θολό κείμενο σε μικρογραφίες, ζωνώσεις σε διαβαθμίσεις: όλα αυτά ανταποκρίνονται σε αυτά τα κουμπιά, και το γύρισμά τους δεν κοστίζει τίποτα στον installer. Η προσθήκη της Cairo ή της PDFium, αντίθετα, σημαίνει να διανέμετε περισσότερα DLL, να παρακολουθείτε μια δεύτερη ή τρίτη παραλλαγή bitness και να αναλαμβάνετε την υποχρέωση να τα ενημερώνετε

Έτσι, ένα παράπονο ποιότητας έχει μια φυσική σειρά ενεργειών. Αναπαραγάγετέ το πρώτα στο ακριβές DPI και στην ακριβή κλίμακα του πελάτη, αφού τις μισές φορές η διαφορά εξατμίζεται μόλις αυτά ταιριάξουν. Δοκιμάστε στη συνέχεια τις επιλογές εξομάλυνσης της ενσωματωμένης μηχανής. Μόνο τότε βάλτε τη σελίδα δίπλα δίπλα ανάμεσα στις μηχανές με κάθε άλλη μεταβλητή σταθερή: αποδώστε την σε PNG μέσω των μηχανών 1, 2 και 3 στο ίδιο DPI και επισυνάψτε και τις τρεις. Συνήθως οι δύο από τις τρεις συμφωνούν, και αυτή η πλειοψηφία σας λέει αν η εξαίρεση είναι το έγγραφο που ερμηνεύεται διαφορετικά ή η δική σας προσδοκία αναφοράς που είναι λανθασμένη. Τρεις συγκεκριμένες εικόνες λύνουν μια διαφωνία περί «λανθασμένης απόδοσης» πολύ γρηγορότερα από μια παράγραφο επιθέτων

Μια αλυσίδα εφεδρείας που εξηγεί τον εαυτό της

Μόλις ο έλεγχος και η πειθαρχία κατάστασης μπουν στη θέση τους, η ίδια η αλυσίδα εφεδρείας είναι σύντομη. Ο εντοπισμός μιας αποτυχίας στηρίζεται στην LastRenderError, η οποία κρατά το κείμενο μηνύματος της ίδιας της μηχανής για την πιο πρόσφατη απόδοση και είναι κενή όταν η απόδοση πέτυχε:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // πρώτα η ενσωματωμένη
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // η PDFium ως βαριά εφεδρεία
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

Δύο σχεδιαστικά σημεία έχουν βάρος εδώ. Η αλυσίδα καταγράφει γιατί έγινε κάθε εναλλαγή, επειδή μια γραμμή log που λέει «αυτή η σελίδα υποχώρησε στην PDFium από την έκδοση 3.7» είναι σήμα παλινδρόμησης που θέλετε να το βλέπετε ως τάση στην παρακολούθηση αντί να χάνεται. Η ίδια η σειρά της εφεδρείας είναι πολιτική που αξίζει να επιλέγεται ανά φόρτο εργασίας. Η ενσωματωμένη μηχανή εγκαθίσταται χωρίς επιπλέον DLL, πράγμα που την κάνει τη σωστή πρώτη δοκιμή στις περισσότερες εγκαταστάσεις, ενώ τα έγγραφα που βρίθουν από ομάδες διαφάνειας ή ασυνήθιστες σκιάσεις είναι ο συνήθης λόγος για τον οποίο μια ομάδα καλωδιώνει εξαρχής εναλλακτική μηχανή. Καμία μηχανή δεν είναι γενικά η ταχύτερη, που είναι και όλο το νόημα της επιλογής ανά κλήση: μετρήστε την καθεμία πάνω σε ένα δείγμα των πραγματικών σας εγγράφων στο πραγματικό σας DPI, και επανεξετάστε αυτή τη μέτρηση κάθε φορά που αλλάζουν τα DLL των μηχανών ή το μείγμα των εγγράφων. Το σώμα των εγγράφων κερδίζει τη διαφωνία κάθε φορά

Αλυσίδα εναλλακτικών απόδοσης PDF: η ενσωματωμένη μηχανή δοκιμάζει πρώτη, οι αποτυχίες καταγράφονται, το PDFium ξαναδοκιμάζει, και μια εξαίρεση αναφέρει όταν όλες οι διαθέσιμες μηχανές αποτύχουν σε μια σελίδα
Κάθε προσπάθεια ελέγχει το LastRenderError και καταγράφει την αιτία πριν αλλάξει μηχανή. Μόνο όταν κάθε εγκατεστημένη μηχανή έχει αποτύχει η αλυσίδα προκαλεί εξαίρεση, με τις συλλεγμένες αιτίες ήδη γραμμένες στο log

Πέρα από μεμονωμένες σελίδες: παρτίδες TIFF και ζωντανά device contexts

Δύο γείτονες των κλήσεων ανά σελίδα ολοκληρώνουν την εργαλειοθήκη. Η RenderAsMultipageTIFFToFile αποδίδει μια έκφραση περιοχής σελίδων κατευθείαν σε ένα TIFF πολλών σελίδων, τη φυσική μορφή για παραδόσεις αρχειοθέτησης προς συστήματα διαχείρισης εγγράφων που προηγούνται του PDF. Η RenderPageToDC ζωγραφίζει απευθείας πάνω σε ένα device context των Windows για στοιχεία ελέγχου προεπισκόπησης, διεπόμενη από τη δική της τριάδα εμμενουσών ρυθμίσεων (SetRenderDCOffset, SetRenderDCErasePage, συν τον τύπο περικοπής) που χρειάζονται την ίδια πειθαρχία επαναφοράς με τον συντελεστή κλίμακας. Η προεπισκόπηση στην οθόνη και η απόδοση στη διαδρομή εκτύπωσης κουβαλούν αρκετές δικές τους παγίδες ώστε να δικαιολογούν ξεχωριστό άρθρο, με σύνδεσμο παρακάτω

Πού να συνεχίσετε

Μια συνήθεια που αξίζει να κρατήσετε: επειδή η SelectRenderer ισχύει για κάθε μεταγενέστερη κλήση στο instance, μια μεμονωμένη πεισματάρα σελίδα μπορεί να ξαναδοκιμαστεί σε άλλη μηχανή ενώ το υπόλοιπο έγγραφο μένει στην προεπιλογή. Για τη ζωγραφική της προεπισκόπησης, την επιλογή εκτυπωτή και τον χειρισμό του DevMode, συνεχίστε με το άρθρο για την προεπισκόπηση εκτύπωσης και το device context. Όταν οι αποδόσεις τροφοδοτούν μια ροή μεγάλου όγκου πάνω σε πολύ μεγάλα αρχεία, η προσέγγιση με handles στον οδηγό άμεσης πρόσβασης ταιριάζει φυσικά με την απόδοση ανά σελίδα μέσω της DARenderPageToFile

Η συσκευασία των μηχανών, οι υποστηριζόμενες μορφές και τα δοκιμαστικά builds περιγράφονται αναλυτικά στη σελίδα προϊόντος PDF Library for Delphi