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

Πώς Λειτουργούν τα Γραφικά του PDF: Ροές Περιεχομένου και Χειριστές

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

Η ροή περιεχομένου είναι postfix bytecode

Μια ροή περιεχομένου είναι μια επίπεδη ακολουθία byte τελεστέων (operands) ακολουθούμενη από χειριστές. Οι τελεστέοι έρχονται πρώτοι, ο χειριστής που τους καταναλώνει έρχεται τελευταίος, το οποίο είναι το αντίστροφο μιας κλήσης συνάρτησης και πανομοιότυπο με μια μηχανή στοίβας: ωθήστε τους αριθμούς και μετά δώστε το ρήμα. Δεν υπάρχει φωλιάζον (nesting), καμία σύνταξη έκφρασης, καμία μεταβλητή. Ένα περίγραμμα τριγώνου είναι πέντε γραμμές από αυτό:

100 100 m    % moveto: start a new subpath at (100, 100)
200 200 l    % lineto: add a segment to (200, 200)
300 100 l    % lineto: add a segment to (300, 100)
h            % closepath: connect back to the start
S            % stroke: paint the path outline

Οι χειριστές είναι λακωνικοί επίτηδες. Μια πραγματική σελίδα είναι χιλιάδες από αυτούς, συνήθως συμπιεσμένοι με FlateDecode. Το κόστος αυτής της συμπαγούς μορφής είναι ότι η ροή δεν φέρει καμία δομή που μπορείτε να υποβάλετε ερώτημα: ένας θεατής δεν μπορεί να ρωτήσει "πού είναι η επικεφαλίδα σε αυτήν τη σελίδα", μπορεί μόνο να εκτελέσει το πρόγραμμα και να δει τι μελάνι προσγειώνεται πού. Αυτός είναι ο βασικός λόγος για τον οποίο η εξαγωγή κειμένου από αυθαίρετα PDF είναι δύσκολη

Η αρχή είναι κάτω-αριστερά και το Y αυξάνεται προς τα πάνω

Πριν οποιαδήποτε συντεταγμένη βγάλει νόημα, πρέπει να γνωρίζετε πού είναι το (0, 0). Το PDF τοποθετεί την αρχή στην κάτω αριστερή γωνία της σελίδας, με το X να αυξάνεται προς τα δεξιά και το Y να αυξάνεται προς τα πάνω, μετρούμενο σε στιγμές (points) με 72 στιγμές ανά ίντσα (ISO 32000-2 §8.3.2). Σε μια σελίδα US Letter η επάνω άκρη βρίσκεται στο y = 792, όχι στο y = 0. Όποιος προέρχεται από γραφικά οθόνης, όπου η αρχή είναι πάνω-αριστερά και το Y αυξάνεται προς τα κάτω, το καταλαβαίνει ανάποδα με την πρώτη προσπάθεια και σχεδιάζει την πρώτη γραμμή έξω από το κάτω μέρος της σελίδας. Η μονάδα είναι επίσης ανεξάρτητη από το μέσο: 72 μονάδες είναι μία ίντσα είτε η σελίδα αποδίδεται σε οθόνη τηλεφώνου είτε σε imagesetter

Οι περισσότερες βιβλιοθήκες σχεδίασης σελίδων κληρονομούν άμεσα αυτήν τη σύμβαση. Στο HotPDF, για παράδειγμα, το TextOut και οι κλήσεις διαδρομής μετρούν όλα από κάτω-αριστερά σε στιγμές (points), επομένως μια τιμή κοντά στο ύψος της σελίδας τοποθετεί το περιεχόμενο στην κορυφή:

// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // emits the moveto/lineto/stroke operators

Αυτή η ακολουθία κλήσεων μεταγλωττίζεται ακριβώς στους χειριστές m, l και S παραπάνω. Η βιβλιοθήκη είναι ένας δακτυλογράφος για τη ροή περιεχομένου, τίποτα περισσότερο, και γνωρίζοντας τι εκπέμπει είναι αυτό που σας επιτρέπει να συλλογιστείτε σχετικά με την έξοδο όταν ένα σχήμα καταλήγει κάπου που δεν περιμένατε

Δημιουργήστε τη διαδρομή, μετά ζωγραφίστε την

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

Οι χειριστές κατασκευής είναι λίγοι. Το m ξεκινά μια νέα υποδιαδρομή σε ένα σημείο. Το l προσθέτει ένα ευθύγραμμο τμήμα. Το c προσθέτει μια κυβική καμπύλη Bezier από έξι τελεστέους, δύο σημεία ελέγχου και ένα τελικό σημείο. Το re είναι μια συντόμευση που προσθέτει ένα ολόκληρο ορθογώνιο από μια τετράδα x, y, πλάτος, ύψος. Το h κλείνει την τρέχουσα υποδιαδρομή πίσω στην αρχή της. Κανένα από αυτά δεν βάζει μελάνι στη σελίδα· συσσωρεύουν μόνο γεωμετρία

200 250 m                    % start the subpath
300 350 400 450 500 250 c    % cubic Bezier: two control points, then endpoint
150 200 re                   % a 150 x 200 rectangle, added as its own subpath
h                            % close

Το αρχικό παράδειγμα χρησιμοποιούσε την πλέον παρωχημένη παραλλαγή y του χειριστή καμπύλης· το c με τα τρία σαφή σημεία του είναι η μορφή που θα δείτε στην πράξη και αυτή που πρέπει να αναζητήσετε. Μόλις υπάρξει η διαδρομή, ένας χειριστής ζωγραφικής την ολοκληρώνει. Το λεξιλόγιο είναι μικρό και αξίζει να το απομνημονεύσετε, επειδή κάθε σχήμα σε κάθε σελίδα τελειώνει με ένα από αυτά:

  • Το S διαγράφει το περίγραμμα της διαδρομής χρησιμοποιώντας το τρέχον πλάτος γραμμής και το χρώμα πινελιάς
  • Το f γεμίζει το εσωτερικό χρησιμοποιώντας το τρέχον χρώμα γεμίσματος και τον κανόνα μηδενικής περιέλιξης (nonzero winding rule)
  • Το f* γεμίζει χρησιμοποιώντας τον κανόνα άρτιου-περιττού (even-odd rule), που έχει σημασία για σχήματα που αυτοτέμνονται και σχήματα με τρύπες
  • Το B γεμίζει και στη συνέχεια δημιουργεί πινελιά σε μία λειτουργία· το b κλείνει τη διαδρομή πρώτα
  • Το n δεν ζωγραφίζει τίποτα, το οποίο είναι το πώς μια διαδρομή γίνεται περιοχή αποκοπής (clip region) χωρίς να αφήσει ορατό σημάδι

Ο κανόνας περιέλιξης (winding rule) είναι το μέρος που οι άνθρωποι κάνουν λάθος. Ο μηδενικός κανόνας (f, B) μετράει τις διασταυρώσεις μιας ακτίνας από το σημείο ελέγχου (με πρόσημο) και γεμίζει οπουδήποτε η καταμέτρηση δεν είναι μηδέν, επομένως μια τρύπα παραμένει κενή μόνο εάν η υποδιαδρομή της τυλίγεται αντίθετα από την εξωτερική. Ο άρτιος-περιττός (f*, B*) εναλλάσσεται σε κάθε διασταύρωση ανεξάρτητα από την κατεύθυνση. Εάν ένα σχήμα "ντόνατ" βγαίνει συμπαγές, ο εσωτερικός κύκλος τυλίγεται με τον ίδιο τρόπο όπως ο εξωτερικός, και είτε τον αντιστρέφετε είτε αλλάζετε σε άρτιο-περιττό

Το χρώμα είναι λειτουργία, όχι παράμετρος

Το χρώμα σε μια ροή περιεχομένου είναι κολλώδες (sticky). Ορίζετε ένα χρώμα και παραμένει ορισμένο μέχρι να ορίσετε ένα άλλο ή να επαναφέρετε μια προηγούμενη κατάσταση, γι' αυτό μια αλλαγή χρώματος χωρίς παρενθέσεις (unbracketed) χρωματίζει σιωπηλά οτιδήποτε σχεδιάζεται μετά από αυτήν. Το PDF διατηρεί επίσης το χρώμα γεμίσματος και το χρώμα πινελιάς ως δύο ανεξάρτητες ρυθμίσεις, με πεζούς χειριστές για το γέμισμα και κεφαλαίους για την πινελιά. Οι χρωματικοί χώροι της συσκευής (device color spaces) έχουν ο καθένας τη δική του συντόμευση:

0.5 g                % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg       % DeviceRGB fill
0.8 0.2 0.1 RG       % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k    % DeviceCMYK fill

Το DeviceRGB ταιριάζει στην έξοδο οθόνης, το DeviceCMYK είναι αυτό που περιμένει η παραγωγή εκτύπωσης και το DeviceGray είναι η μικρότερη επιλογή για μονόχρωμο περιεχόμενο. Οι χώροι της συσκευής (device spaces) είναι βολικοί αλλά αβαθμονόμητοι: η ίδια τριάδα RGB μπορεί να αποδοθεί διαφορετικά σε δύο οθόνες, το οποίο είναι το πρόβλημα που υπάρχουν για να λύσουν οι χρωματικοί χώροι που βασίζονται στο ICC και οι προθέσεις εξόδου του PDF/A. Για εργασίες με κρίσιμο χρώμα, επιλέγετε έναν βαθμονομημένο χώρο με cs και CS και ορίζετε τα στοιχεία με sc και scn, αλλά για συνηθισμένα έγγραφα οι συντομεύσεις της συσκευής κάνουν τη δουλειά. Μια βιβλιοθήκη τα τυλίγει σε κλήσεις με τύπο. Το HotPDF, για παράδειγμα, παίρνει ένα μόνο TColor και εκπέμπει τους αντίστοιχους χειριστές:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, width, height
Pdf.CurrentPage.Fill;

Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50);           // x, y, radius
Pdf.CurrentPage.Fill;

Η κατάσταση γραφικών και η στοίβα q/Q

Ό,τι δεν είναι η ίδια η διαδρομή ζει στην κατάσταση γραφικών (graphics state): τρέχων πίνακας μετασχηματισμού, χρώματα γεμίσματος και πινελιάς, πλάτος γραμμής, μοτίβο παύλας, περιοχή αποκοπής, άλφα. Η κατάσταση είναι καθολική και μεταβλητή, επομένως ο μόνος ασφαλής τρόπος για να κάνετε μια τοπική αλλαγή είναι να αποθηκεύσετε όλο το πράγμα, να το τροποποιήσετε, να σχεδιάσετε και να το επαναφέρετε. Αυτό κάνουν τα q και Q. Το q ωθεί ένα αντίγραφο της τρέχουσας κατάστασης σε μια στοίβα· το Q το αναδύει (pops), απορρίπτοντας κάθε αλλαγή που έγινε από το αντίστοιχο q

q                    % save the entire graphics state
2 0 0 2 100 100 cm   % concatenate a transform: scale 2x, translate to (100,100)
0.8 g                % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q                    % restore: transform and color revert

Τα μη ισορροπημένα q και Q είναι ένας συνηθισμένος τρόπος με τον οποίο μια χειροκίνητη ή ραμμένη ροή περιεχομένου πάει στραβά. Ένα αδέσποτο q χωρίς το αντίστοιχο Q αφήνει τη στοίβα βαθιά όταν τελειώνει η σελίδα· ένα επιπλέον Q δημιουργεί υποχείλιση (underflow). Είτε έτσι είτε αλλιώς, ένα πρόγραμμα προβολής μπορεί να κρατήσει μια παλιά αποκοπή ή μετασχηματισμό σε ισχύ και το περιεχόμενο εξαφανίζεται ή προσγειώνεται σε λάθος μέρος. Όταν τα γραφικά εξαφανίζονται χωρίς λόγο που μπορεί να εξηγήσει η διαδρομή, ελέγξτε πρώτα τη στοίβα κατάστασης

Ο CTM μετασχηματίζει κάθε συντεταγμένη

Ο τρέχων πίνακας μετασχηματισμού (CTM) βρίσκεται μεταξύ των αριθμών στους χειριστές σας και της πραγματικής σελίδας. Κάθε συντεταγμένη πολλαπλασιάζεται με τον CTM πριν σχεδιαστεί οτιδήποτε, επομένως η αλλαγή του πίνακα αλλάζει το πού και πώς εμφανίζεται κάθε επόμενη σχεδίαση χωρίς να αγγίξετε καμία συντεταγμένη διαδρομής. Ο χειριστής cm συνενώνει έναν νέο πίνακα στον τρέχοντα, παίρνοντας έξι τελεστέους που αντιστοιχούν στον αφινικό (affine) πίνακα [a b c d e f]:

1 0 0 1 100 50 cm        % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm         % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm   % rotate 45 degrees (cos/sin in a, b, c, d)

Δύο πράγματα μπερδεύουν τον κόσμο. Πρώτον, το cm συνθέτει παρά αντικαθιστά, επομένως οι μετασχηματισμοί συσσωρεύονται και η σειρά έχει σημασία: η κλιμάκωση και μετά η μετάφραση (translation) δεν είναι το ίδιο με τη μετάφραση και μετά την κλιμάκωση. Δεύτερον, η περιστροφή και η κλιμάκωση περιστρέφονται γύρω από την τρέχουσα αρχή, όχι το κέντρο του σχήματός σας, οπότε για να περιστρέψετε κάτι στη θέση του το μεταφράζετε στην αρχή, το περιστρέφετε, και μετά το μεταφράζετε πίσω, όλα τυλιγμένα σε q/Q. Αυτός ο ίδιος πίνακας είναι που τοποθετεί τις εικόνες, το τελευταίο κομμάτι που αξίζει να δείτε

Οι εικόνες και το επαναχρησιμοποιήσιμο περιεχόμενο είναι XObjects

Οι εικόνες Raster δεν ζουν ενσωματωμένες (inline) στη ροή περιεχομένου. Αποθηκεύονται ως XObjects εικόνας, εξωτερικά αντικείμενα με το δικό τους λεξικό που περιγράφει πλάτος, ύψος, βάθος bit, χρωματικό χώρο και φίλτρο συμπίεσης, και η ροή περιεχομένου απλώς αναφέρεται σε αυτά. Μια φωτογραφία με υποστήριξη JPEG δηλώνει τον εαυτό της ως εξής:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % the image data is a JPEG stream
>>

Ένα XObject εικόνας σχεδιάζεται στο μοναδιαίο τετράγωνο: καταλαμβάνει πάντα την περιοχή από (0, 0) έως (1, 1) στον χώρο χρήστη. Δεν του περνάτε θέση ή μέγεθος. Αντίθετα, ορίζετε τον CTM ώστε το μοναδιαίο τετράγωνο να αντιστοιχεί στο ορθογώνιο που θέλετε, και μετά το καλείτε με το Do. Γι' αυτό η τοποθέτηση μιας εικόνας είναι πάντα ένας μετασχηματισμός ακολουθούμενος από μια κλήση, τυλιγμένα σε αποθήκευση/επαναφορά (save/restore) ώστε η κλίμακα να μην διαρρεύσει στην επόμενη λειτουργία:

q
640 0 0 480 50 300 cm    % map the unit square to a 640x480 box at (50, 300)
/Photo Do                % paint the image XObject
Q

Ο ίδιος μηχανισμός Do οδηγεί τα form XObjects, τα οποία κρατούν ένα επαναχρησιμοποιήσιμο κομμάτι γραφικών, ένα λογότυπο ή μια επαναλαμβανόμενη σφραγίδα, ως τη δική τους ροή περιεχομένου με ένα πλαίσιο οριοθέτησης (bounding box). Καθορίστε το μία φορά, καλέστε το πολλές φορές με διαφορετικό CTM, και τα byte εμφανίζονται στο αρχείο μόνο μία φορά. Οι περισσότερες βιβλιοθήκες το κρύβουν αυτό πίσω από μια μόνο κλήση τοποθέτησης: Το HotPDF καταχωρεί ένα bitmap με AddImage και το τοποθετεί με το ShowImage, λαμβάνοντας ένα ρητό x, y, πλάτος και ύψος αντί να σας ζητά να δημιουργήσετε τον πίνακα με το χέρι:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bottom-left), width, height, rotation angle
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

Κάτω από αυτή τη μία γραμμή η βιβλιοθήκη γράφει το λεξικό image XObject, ρυθμίζει τον CTM για να μεγεθύνει και να τοποθετήσει το μοναδιαίο τετράγωνο, και εκπέμπει Do. Το μοντέλο από κάτω είναι αυτό που αξίζει να γνωρίζετε, επειδή εξηγεί κάθε περίεργο αποτέλεσμα: μια τεντωμένη εικόνα είναι ένας CTM με αταίριαστους συντελεστές κλίμακας, ένα λογότυπο πανομοιότυπο σε σαράντα σελίδες είναι ένα form XObject που καλείται σαράντα φορές, και μια εικόνα που αποδίδεται ανάποδα είναι μια αλλαγή προσήμου στον πίνακα, όχι ένα κατεστραμμένο αρχείο

Πού οδηγεί αυτό

Το μοντέλο γραφικών είναι μικρό μόλις δείτε το σχήμα του. Μια ροή περιεχομένου είναι postfix bytecode που εκτελείται ενάντια σε μια μεταβλητή κατάσταση· οι συντεταγμένες ξεκινούν στο κάτω-αριστερό μέρος και περνούν μέσα από τον CTM· οι διαδρομές κατασκευάζονται σιωπηλά και ζωγραφίζονται με έναν σκόπιμο χειριστή· οι ρυθμίσεις χρώματος και γραμμής παραμένουν μέχρι να τις κλείσετε σε παρενθέσεις με τα q/Q· οι εικόνες και τα επαναχρησιμοποιήσιμα γραφικά είναι XObjects που τοποθετούνται μετασχηματίζοντας ένα μοναδιαίο τετράγωνο. Σχεδόν κάθε μπερδεμένο αποτέλεσμα απόδοσης ανάγεται σε έναν από αυτούς τους πέντε κανόνες. Αν θέλετε να δείτε πώς κάθονται αυτοί οι χειριστές γραφικών μέσα στο μεγαλύτερο μοντέλο αντικειμένων, τα λεξικά σελίδων και ο πίνακας διασταυρούμενων αναφορών που δείχνουν σε αυτά, η τεχνική επισκόπηση της δομής αρχείων PDF καλύπτει αυτό το επίπεδο, και η δημιουργία ενός απλού εγγράφου PDF από το μηδέν περπατά τα byte από άκρη σε άκρη. Η σχεδίαση κειμένου ζει στη δική της οικογένεια χειριστών και έχει τις δικές της παγίδες, οι οποίες καλύπτονται στο συνοδευτικό άρθρο για τον χειρισμό κειμένου και γραμματοσειρών PDF

Οι κλήσεις σχεδίασης του Delphi που εμφανίζονται εδώ, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage και ShowImage, αποτελούν μέρος του HotPDF Component για Delphi και C++Builder, το οποίο εκπέμπει αυτούς τους χειριστές ροής περιεχομένου για εσάς