Technical Article

Διανυσματικά γραφικά σε PDF με Delphi: Διαδρομές και διαβαθμίσεις

Ο περισσότερος κώδικας Delphi που αγγίζει το PDF αντιμετωπίζει τη μορφή ως κοντέινερ για δύο πράγματα: τμήματα κειμένου και μερικά τοποθετημένα bitmap. Αυτή η άποψη είναι σωστή στο βαθμό που ισχύει, αλλά αφήνει αχρησιμοποίητο το πιο ικανό μέρος της μορφής. Μια σελίδα PDF είναι ένας ανεξάρτητος ανάλυσης 2D καμβάς, βασισμένος στο ίδιο μοντέλο απεικόνισης με το PostScript. Μπορεί να σχεδιάσει γραμμές, καμπύλες, γεμισμένες περιοχές, διαβαθμίσεις και επαναλαμβανόμενα μοτίβα, όλα ως διανύσματα που παραμένουν ευκρινή σε οποιαδήποτε μεγέθυνση και εκτυπώνονται στην πλήρη ανάλυση της συσκευής. Εάν σχεδιάζετε ένα λογότυπο, ένα διάγραμμα, ένα υδατογράφημα ή ένα περίγραμμα πιστοποιητικού, η διανυσματική διαδρομή (vector path) είναι σχεδόν πάντα το σωστό πρωταρχικό στοιχείο, και είναι μικρότερη και πιο ευκρινής από την ψηφιογραφημένη (rasterized) εικόνα στην οποία καταφεύγουν πολλά προγράμματα.

Αυτό το άρθρο εξετάζει το διανυσματικό μοντέλο όπως το ορίζει το ISO 32000-1 και δείχνει τις αντίστοιχες κλήσεις του PDFlibPas. Στόχος είναι να γίνει συγκεκριμένη η προδιαγραφή, επειδή το API χαρτογραφείται στενά σε αυτήν, και η κατανόηση του ενός σας διδάσκει το άλλο.

Η σελίδα είναι μια μηχανή διαδρομών

Το ISO 32000-1 §8.5 περιγράφει τα γραφικά σε δύο φάσεις που δεν επικαλύπτονται ποτέ. Πρώτα κατασκευάζετε μια διαδρομή (path), η οποία είναι καθαρή γεωμετρία χωρίς ορατό αποτέλεσμα. Στη συνέχεια, βάφετε αυτήν τη διαδρομή σε μια ενιαία λειτουργία που σχεδιάζει το περίγραμμά της (stroke), γεμίζει το εσωτερικό της (fill) ή κάνει και τα δύο. Τίποτα δεν εμφανίζεται στη σελίδα κατά την κατασκευή. Η διαδρομή είναι μια αφηρημένη ακολουθία σημείων και τμημάτων που διατηρούνται στην κατάσταση γραφικών (graphics state) μέχρι ένας τελεστής βαφής (painting operator) να την καταναλώσει, οπότε και αποδίδεται και απορρίπτεται.

Μια διαδρομή αποτελείται από μία ή περισσότερες υπο-διαδρομές (subpaths). Μια υπο-διαδρομή ξεκινά από ένα σημείο και μεγαλώνει προσθέτοντας τμήματα: ευθείες γραμμές, κυβικές καμπύλες Bezier και, σε ορισμένες πλατφόρμες, ολόκληρα ορθογώνια που προστίθενται ως δική τους κλειστή υπο-διαδρομή. Στο PDFlibPas ανοίγετε μια διαδρομή με τη StartPath, η οποία ορίζει το σημείο εκκίνησης, και στη συνέχεια την επεκτείνετε με τις AddLineToPath και AddCurveToPath. Κάθε κλήση προχωρά ένα υπονοούμενο τρέχον σημείο, έτσι ώστε το επόμενο τμήμα να συνεχίζει από εκεί που τελείωσε το προηγούμενο. Η ClosePath σχεδιάζει ένα τελικό ευθύγραμμο τμήμα πίσω στην αρχή της υπο-διαδρομής, κάτι που έχει σημασία για το stroke επειδή παράγει μια πραγματική ένωση γραμμών στην κλειστή κορυφή αντί για δύο ελεύθερα άκρα.

// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // straight segment back to (150, 100)
PDF.DrawPath(2);                   // 2 = fill and stroke; path is consumed

Οι καμπύλες χρησιμοποιούν τη AddCurveToPath, η οποία λαμβάνει δύο σημεία ελέγχου Bezier και ένα τελικό σημείο: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Η καμπύλη εκτείνεται από το τρέχον σημείο έως το (EndX, EndY), ελκόμενη προς τα δύο σημεία ελέγχου κατά τη διαδρομή. Τα κυκλικά τόξα είναι διαθέσιμα μέσω της AddArcToPath(CenterX, CenterY, TotalAngle), όπου η ακτίνα λαμβάνεται από την απόσταση μεταξύ του τρέχοντος σημείου και του κέντρου, και η μηχανή εκπέμπει το τόξο ως μια αλυσίδα τμημάτων Bezier. Τα ορθογώνια έχουν μια συντόμευση, την AddBoxToPath(Left, Top, Width, Height), η οποία προσαρτά ένα πλήρες κλειστό ορθογώνιο ως δική του υπο-διαδρομή χωρίς προηγούμενη StartPath.

Δύο κανόνες γεμίσματος και γιατί διαφωνούν

Όταν γεμίζετε μια διαδρομή που τέμνει τον εαυτό της ή περιέχει έναν εσωτερικό βρόχο, ο renderer χρειάζεται έναν κανόνα για να αποφασίσει ποιες περιοχές βρίσκονται μέσα στο σχήμα και ποιες είναι τρύπες. Το ISO 32000-1 §8.5.3.3 ορίζει δύο κανόνες, και αυτοί μπορούν να βάψουν την ίδια γεωμετρία διαφορετικά. Ο κανόνας μη μηδενικής περιέλιξης (nonzero winding rule) μετρά τις υπογεγραμμένες διασταυρώσεις μιας ακτίνας που εκπέμπεται από ένα σημείο δοκιμής προς το άπειρο, προσθέτοντας ένα για κάθε τμήμα που τέμνει από αριστερά προς τα δεξιά και αφαιρώντας ένα για κάθε τμήμα που τέμνει προς την αντίθετη κατεύθυνση. Το σημείο είναι εσωτερικό όταν το σύνολο δεν είναι μηδέν. Ο κανόνας άρτιου-περιττού (even-odd rule) αγνοεί την κατεύθυνση και απλώς μετρά τις διασταυρώσεις, χαρακτηρίζοντας το σημείο ως εσωτερικό όταν το πλήθος είναι περιττό.

Η κλασική περίπτωση όπου αποκλίνουν είναι ένα σχήμα με τρύπα, ένας δακτύλιος. Σχεδιάστε ένα εξωτερικό όριο και ένα εσωτερικό όριο μέσα του. Σύμφωνα με τον κανόνα άρτιου-περιττού, ο εσωτερικός βρόχος δημιουργεί πάντα μια τρύπα, επειδή οποιοδήποτε σημείο μεταξύ των δύο ορίων τέμνεται μία φορά και οποιοδήποτε σημείο μέσα στον εσωτερικό βρόχο τέμνεται δύο φορές. Σύμφωνα με τον κανόνας μη μηδενικής περιέλιξης, η τρύπα εμφανίζεται μόνο εάν ο εσωτερικός βρόχος τυλίγεται προς την αντίθετη κατεύθυνση από τον εξωτερικό. Εάν τυλιχθούν προς την ίδια κατεύθυνση, οι περιελίξεις ενισχύονται αντί να αλληλοακυρώνονται, και η εσωτερική περιοχή γεμίζει συμπαγής. Ένα αστέρι με πέντε κορυφές σχεδιασμένο ως ενιαίο περίγραμμα που τέμνει τον εαυτό του δείχνει τον ίδιο διαχωρισμό: ο κανόνας άρτιου-περιττού αφήνει το κεντρικό πεντάγωνο άδειο, ενώ η μη μηδενική περιέλιξη το γεμίζει.

Το PDFlibPas επιλέγει τον κανόνα από την κλήση που κάνετε για τη βαφή, όχι από κάποια σημαία. Η DrawPath γεμίζει με τον κανόνα μη μηδενικής περιέλιξης. Η DrawPathEvenOdd γεμίζει με τον κανόνα άρτιου-περιττού. Και οι δύο λαμβάνουν την ίδια ακέραιη λειτουργία: 0 σχεδιάζει μόνο το περίγραμμα (stroke), 1 μόνο γεμίζει (fill) και 2 γεμίζει και σχεδιάζει περίγραμμα. Ο κανόνας άρτιου-περιττού είναι το ευκολότερο εργαλείο για τη δημιουργία οπών ακριβώς επειδή δεν απαιτεί από εσάς να διαχειριστείτε την κατεύθυνση της υπο-διαδρομής.

// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // outer
PDF.AddBoxToPath(140, 130, 120,  60);   // inner
PDF.DrawPath(1);                         // 1 = fill, nonzero winding

// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // outer
PDF.AddBoxToPath(140, 330, 120,  60);   // inner cut-out
PDF.DrawPathEvenOdd(1);                  // 1 = fill, even-odd

Οι αξονικές διαβαθμίσεις μεταβάλλουν το χρώμα κατά μήκος μιας γραμμής

Ένα ενιαίο χρώμα γεμίσματος (flat fill color) έχει μία τιμή σε ολόκληρη την περιοχή. Μια διαβάθμιση (gradient) μεταβάλλει το χρώμα συνεχώς, και το απλούστερο είδος είναι η αξονική ή γραμμική διαβάθμιση. Το ISO 32000-1 §8.7.4.5 την καθορίζει ως αξονική σκίαση Τύπου 2 (Type 2 axial shading): δίνετε δύο σημεία που ορίζουν έναν άξονα, ένα αρχικό χρώμα στο πρώτο σημείο και ένα τελικό χρώμα στο δεύτερο, και ο renderer παρεμβάλλει το χρώμα κατά μήκος αυτού του άξονα. Κάθε σημείο στη γεμισμένη περιοχή παίρνει το χρώμα της κάθετης προβολής του στον άξονα, έτσι ώστε η διαβάθμιση να εκτείνεται σε ζώνες κάθετες προς τη γραμμή μεταξύ των δύο σημείων.

Στο PDFlibPas, μια διαβάθμιση είναι ένας ονομαστικός πόρος εγγράφου που δημιουργείτε μία φορά και στη συνέχεια επιλέγετε ως ενεργή βαφή. Η NewRGBAxialShader την καταχωρίζει. Η υπογραφή είναι NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): τα δύο άκρα του άξονα, οι τριάδες RGB σε κάθε άκρο ως τιμές στο εύρος 0 έως 1, και μια σημαία Extend. Με το Extend ορισμένο σε 1, τα τελικά χρώματα συνεχίζουν ως συμπαγές γέμισμα πέρα από τα άκρα του άξονα, κάτι που συνήθως θέλετε, ώστε οι γωνίες μιας περιοχής έξω από τον άξονα να μην μένουν άβαφες. Το 0 τις αφήνει ανέπαφες. Μόλις υπάρξει ο shader, τον συνδέετε με τη SetFillShader για γεμισμένες περιοχές, τη SetLineShader για περιγράμματα ή τη SetTextShader για κείμενο. Η σύνδεση παραμένει ενεργή για τις σχεδιαστικές κλήσεις που ακολουθούν, έτσι ώστε η διαδρομή που θα βάψετε στη συνέχεια να παίρνει τη διαβάθμιση αντί για ένα ενιαίο χρώμα.

// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // start point and start RGB
  0, 260,   1.00, 1.00, 1.00,    // end point and end RGB
  1);                            // 1 = extend ends as solid color

// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, now filled by the shader

Ο άξονας εδώ είναι κατακόρυφος, από y=100 έως y=260 σε ένα σταθερό x, επομένως οι χρωματικές ζώνες εκτείνονται οριζόντια και το ορθογώνιο ξεθωριάζει από μπλε στη βάση του σε λευκό στην κορυφή του. Επειδή ο shader αναγνωρίζεται με βάση το όνομα, ένας ορισμός μπορεί να γεμίσει οποιοδήποτε αριθμό σχημάτων στη σελίδα, και η επιστροφή σε ένα ενιαίο χρώμα είναι απλώς μια άλλη κλήση SetFillColor πριν από την επόμενη διαδρομή.

Τα μοτίβα πλακιδίων επαναλαμβάνουν ένα κελί

Εκεί που μια διαβάθμιση μεταβάλλει ομαλά ένα μόνο χρώμα, ένα μοτίβο πλακιδίων (tiling pattern) επαναλαμβάνει ένα μικρό κομμάτι δημιουργικού σε μια περιοχή. Το ISO 32000-1 §8.7.3.1 ορίζει ένα μοτίβο πλακιδίων ως ένα κελί μοτίβου (pattern cell), ένα ανεξάρτητο κομμάτι περιεχομένου, το οποίο ο renderer επαναλαμβάνει σε ένα σταθερό πλέγμα για να καλύψει την περιοχή που βάφεται. Αυτός είναι ο τρόπος με τον οποίο δημιουργείτε διαγράμμιση για ένα μηχανολογικό γέμισμα, ένα επαναλαμβανόμενο μοτίβο επωνυμίας πίσω από μια επικεφαλίδες ή ένα ανάγλυφο υπόβαθρο που παραμένει διανυσματικά ευκρινές και δεν ζυγίζει σχεδόν τίποτα, ανεξάρτητα από το πόσο μεγάλη είναι η περιοχή, επειδή το κελί αποθηκεύεται μία φορά και αναφέρεται παντού.

Το PDFlibPas δημιουργεί το κελί μοτίβου από καταγεγραμμένο περιεχόμενο σελίδας. Καταγράφετε μια σελίδα ή μια περιοχή με τη CapturePage, μετατρέπετε την καταγραφή σε ονομαστικό μοτίβο με τη NewTilingPatternFromCapturedPage(PatternName, CaptureID) και, στη συνέχεια, επιλέγετε αυτό το μοτίβο ως το τρέχον γέμισμα με τη SetFillTilingPattern(PatternName). Από εκείνο το σημείο και μετά, οποιαδήποτε διαδρομή γεμίζετε βάφεται με το επαναλαμβανόμενο κελί αντί για ένα ενιαίο χρώμα, ακριβώς όπως λειτουργεί το γέμισμα με shader, αλλά με ένα πλακίδιο ως πηγή βαφής. Η ακολουθία είναι πιο περίπλοκη από μια απλή κλήση, επομένως εάν το βήμα καταγραφής σάς είναι άγνωστο, αντιμετωπίστε το μοτίβο ως μια λειτουργία δύο σταδίων: δημιουργήστε πρώτα το καταγεγραμμένο κελί, και στη συνέχεια συνδέστε το ως γέμισμα με το όνομά του πριν σχεδιάσετε την περιοχή που θέλετε να καλύψετε με πλακίδια.

Συνδυάζοντας τα βασικά στοιχεία

Τα κομμάτια συνδυάζονται άμεσα. Μια γεμισμένη καμπύλη Bezier είναι μια διαδρομή καμπυλών που βάφεται με τη DrawPath. Το ίδιο περίγραμμα βαμμένο με τη DrawPathEvenOdd μετά την προσθήκη ενός εσωτερικού βρόχου εμφανίζει μια τρύπα την οποία το γέμισμα περιέλιξης (winding fill) θα είχε κλείσει. Ένα ορθογώνιο γεμισμένο με διαβάθμιση είναι ένα πλαίσιο συνδεδεμένο με έναν shader. Το παρακάτω παράδειγμα σχεδιάζει και τα τρία στη σειρά, έτσι ώστε η διαφορά μεταξύ των δύο κανόνων γεμίσματος να είναι ορατή σε μία σελίδα, και στη συνέχεια τοποθετεί ένα πάνελ διαβάθμισης κάτω από αυτά.

// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = fill

// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300);                              // new subpath: the hole
PDF.AddArcToPath(200, 300, 360);                     // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // hole is punched out

// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
  60, 100,  0.95, 0.55, 0.10,
  60, 200,  0.20, 0.10, 0.40,
  1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);

Δύο λεπτομέρειες αξίζει να κρατήσετε. Η κλήση βαφής αποφασίζει τον κανόνα γεμίσματος, επομένως η επιλογή μεταξύ DrawPath και DrawPathEvenOdd είναι η επιλογή μεταξύ μη μηδενικής περιέλιξης και άρτιου-περιττού, και για σχήματα με τρύπες ο κανόνας άρτιου-περιττού σάς απαλλάσσει από τη σκέψη για την κατεύθυνση της υπο-διαδρομής. Και η κατάσταση γραφικών (graphics state) δειγματίζεται τη στιγμή που βάφετε: ορίστε τα χρώματα, το πάχος της γραμμής και τη σύνδεση του shader πριν από την κλήση βαφής, επειδή αυτή είναι η κατάσταση που διαβάζει η μηχανή. Κατασκευάστε πρώτα, ρυθμίστε την κατάσταση, βάψτε τελευταία, και το διανυσματικό μοντέλο συμπεριφέρεται προβλέψιμα κάθε φορά.

Από εδώ, τα φυσικά επόμενα βήματα είναι η ανάγνωση διανυσμάτων και κειμένου από ένα υπάρχον έγγραφο, που καλύπτεται στο άρθρο μας για την εξαγωγή κειμένου, εικόνας και γραμματοσειράς, και η απόδοση του ίδιου μοντέλου σχεδίασης σε ένα Windows device context για προεπισκόπηση στην οθόνη και εκτύπωση, που καλύπτεται στον οδηγό εκτύπωσης και προεπισκόπησης. Οι κλήσεις διαδρομής, shader και μοτίβου που περιγράφονται εδώ αποστέλλονται ως μέρος της Delphi PDF Library μαζί με τα API κειμένου, εικόνας, φόρμας και υπογραφής που καλύπτονται αλλού σε αυτό το ιστολόγιο.