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

Πλήρης στοίχιση για κείμενο PDF στο Delphi με το HotPDF

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

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

Τι απαιτεί πραγματικά η πλήρης στοίχιση

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

Ο κανόνας που ξεχωρίζει την καλή από την κακή στοίχιση είναι το πού βάζετε τη χαλαρότητα. Ένα σύστημα γραφής που γράφει λέξεις με κενά μεταξύ τους, όπως τα αγγλικά και η υπόλοιπη λατινική οικογένεια, έχει φυσικές ενώσεις σε κάθε κενό μεταξύ των λέξεων. Η διεύρυνση αυτών των κενών είναι αόρατη στο μάτι επειδή οι αναγνώστες ήδη αποδέχονται ότι τα κενά των λέξεων ποικίλλουν. Ένα σύστημα γραφής που γράφει χωρίς κενά λέξεων, όπως οι κινεζικοί χαρακτήρες Han, τα ιαπωνικά kana ή τα κορεατικά Hangul, δεν έχει τέτοιες ενώσεις. Εκεί η χαλαρότητα πρέπει να απλωθεί ομοιόμορφα μεταξύ των γειτονικών γλυφικών, που είναι η αρχή που οι Ιάπωνες στοιχειοθέτες ονομάζουν kintou-waritsuke, ομοιόμορφα διαστήματα. Το να βάζετε τέντωμα κενών λέξεων λατινικού στυλ σε μια γραμμή CJK, ή το να στριμώχνετε όλη τη χαλαρότητα στο ένα μέρος που μια γραμμή CJK τυχαίνει να περιέχει ένα κενό, παράγει τα ποτάμια και τα κενά που χαρακτηρίζουν το ερασιτεχνικό αποτέλεσμα

Πώς το HotPDF αποφασίζει πού πάει ο χώρος

Το HotPDF παίρνει αυτή την απόφαση ανά κενό, όχι ανά γραμμή. Όταν στοιχίζει μια γραμμή, διατρέχει κάθε γειτονικό ζεύγος γλυφικών και ρωτά αν βρίσκεται ανάμεσά τους ένα τεντώσιμο όριο. Ένα όριο είναι τεντώσιμο όταν οποιαδήποτε πλευρά είναι κενό ή tab, η λατινική περίπτωση, ή όταν και οι δύο πλευρές είναι χαρακτήρες CJK που μπορούν να σπάσουν, η περίπτωση του ομοιόμορφου διαστήματος. Μετράει αυτά τα όρια, διαιρεί τη χαλαρότητα της γραμμής εξίσου μεταξύ τους, και προσθέτει αυτό το μερίδιο σε κάθε κενό που πληροί τις προϋποθέσεις

Η συνέπεια προκύπτει φυσικά. Μια αγγλική γραμμή έχει τεντώσιμα όρια μόνο στα κενά των λέξεών της, οπότε όλη η χαλαρότητα καταλήγει εκεί και οι λέξεις απλώνονται μακριά ενώ τα γράμματα μέσα σε κάθε λέξη διατηρούν το φυσικό τους διάστημα. Μια γραμμή Han ή kana έχει ένα τεντώσιμο όριο μεταξύ σχεδόν κάθε ζεύγους γλυφικών, οπότε η χαλαρότητα κατανέμεται ομοιόμορφα σε όλη τη γραμμή, ακριβώς το ομοιόμορφο διάστημα μεταξύ των γλυφικών που απαιτούν αυτά τα συστήματα γραφής. Μια γραμμή που είναι μια μεμονωμένη μακριά λατινική λέξη χωρίς εσωτερικό κενό δεν έχει κανένα τεντώσιμο όριο, οπότε το HotPDF την αφήνει στο φυσικό της πλάτος αντί να σχίσει τη λέξη γράμμα προς γράμμα. Η ίδια λογική χειρίζεται μικτά τμήματα Λατινικών και CJK σε μία γραμμή χωρίς ειδική αντιμετώπιση, επειδή η απόφαση είναι τοπική για κάθε όριο

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

Γιατί η τελευταία γραμμή αφήνεται μόνη της

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

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

Το κόστος μέτρησης που έκανε τη στοίχιση αργή

Για να στοιχίσετε μια γραμμή πρέπει να γνωρίζετε το ακριβές πλάτος της, και πρέπει να γνωρίζετε την πρόοδο (advance) κάθε γλυφικού ώστε να μπορέσετε να τοποθετήσετε τον επιπλέον χώρο με ακρίβεια. Η πρώτη υλοποίηση πήρε αυτούς τους αριθμούς με τον προφανή τρόπο. Μέτρησε ολόκληρη τη γραμμή με ένα πλήρες ερώτημα πλάτους Unicode, έπειτα μέτρησε πρόθεμα μετά από πρόθεμα για να ανακτήσει την πρόοδο κάθε γλυφικού με αφαίρεση. Για μια γραμμή Ν γλυφικών αυτό είναι Ν+1 κλήσεις στη μηχανή μέτρησης, και κάθε κλήση είναι μια πλήρης διαδρομή μετ' επιστροφής στο GDI, ζητώντας από το λειτουργικό σύστημα να διαμορφώσει και να μετρήσει το κείμενο και να επιστρέψει την απάντηση

Ανά γραμμή αυτό ακούγεται φθηνό. Σε όλη τη σελίδα δεν είναι. Πάρτε μια πυκνή σελίδα Α4 με κείμενο σώματος, περίπου σαράντα πέντε γραμμές από περίπου ογδόντα χαρακτήρες η καθεμία. Στο Ν+1 διαδρομές ανά γραμμή αυτό είναι περίπου 81 διαδρομές για κάθε γραμμή και περίπου 3.645 για τη σελίδα, σχεδόν όλες τους ξοδεμένες στην επαναμέτρηση κειμένου που η μηχανή είχε ήδη κοιτάξει στιγμές νωρίτερα. Σε μια μαζική εργασία παραγωγής χιλιάδων σελίδων, αυτή η επιβάρυνση κυριαρχεί στον χρόνο διάταξης, και κάθε διαδρομή διασχίζει το όριο μεταξύ της διαδικασίας σας και του υποσυστήματος γραφικών

Μία κλήση αντί για Ν συν ένα

Η επιδιόρθωση είναι το είδος της αλλαγής που φαίνεται μικρή και αποδίδει μεγάλα οφέλη. Το GDI μπορεί ήδη να αναφέρει το συνολικό πλάτος μιας συμβολοσειράς και τη θέση κάθε γλυφικού σε ένα μεμονωμένο ερώτημα. Το HotPDF το εκθέτει αυτό μέσω του GetWideCharAdvances, το οποίο γεμίζει έναν πίνακα με τη φυσική πρόοδο κάθε γλυφικού, συμπεριλαμβανομένου του kerning, και επιστρέφει το συνολικό πλάτος, σε μία κλήση αντί για Ν+1. Η ρουτίνα στοίχισης, _HPDFEmitJustifiedWideLine εσωτερικά, ζητά όλες τις προόδους μία φορά, υπολογίζει τη χαλαρότητα, την κατανέμει στα τεντώσιμα όρια και εκπέμπει τη γραμμή

Για την ίδια αυτή σελίδα Α4 η μέτρηση ανά γραμμή πέφτει από περίπου 81 διαδρομές σε μία, οπότε η σελίδα πέφτει από περίπου 3.645 διαδρομές σε περίπου 45, κοντά σε μια μείωση κατά ογδόντα φορές. Το αποτέλεσμα είναι πανομοιότυπο ανά byte, επειδή τίποτα σχετικά με τη μέτρηση δεν άλλαξε εκτός από το πόσες φορές ζητείται. Η ίδια μηχανή GDI, οι ίδιες μετρικές γραμματοσειράς, το ίδιο kerning τροφοδοτούν τους ίδιους αριθμούς. Μόνο ο αριθμός των διαδρομών έπεσε. Όταν μια μέτρηση είναι ήδη σωστή, η σωστή βελτιστοποίηση είναι να σταματήσετε να τη ζητάτε επανειλημμένα, όχι να την προσεγγίσετε

Πώς η γραμμή φτάνει στη σελίδα

Μόλις κατανεμηθεί η χαλαρότητα, το HotPDF εκπέμπει τη γραμμή με το ExtTextOut και έναν πίνακα προόδου ανά γλυφικό, τον πίνακα Dx. Κάθε καταχώριση είναι η απόσταση από την αρχή ενός γλυφικού στο επόμενο, που είναι η φυσική πρόοδος αυτού του γλυφικού συν το μερίδιό του από τη χαλαρότητα όταν το ακολουθεί ένα τεντώσιμο όριο. Αυτό αντιστοιχίζεται απευθείας στο μοντέλο απεικόνισης του PDF. Το τοποθετημένο κείμενο γράφεται με τον τελεστή TJ, έναν πίνακα που παρεμβάλλει διαδοχές γλυφικών με ρητές οριζόντιες ρυθμίσεις, και οι τιμές Dx γίνονται ακριβώς αυτές οι ρυθμίσεις. Αυτός είναι ο λόγος για τον οποίο ο επιπλέον χώρος καταλήγει μεταξύ των γλυφικών σε ακριβείς υπο-σημειακές θέσεις αντί να προσποιείται με χαρακτήρες γεμίσματος (padding), και γιατί μια στοιχισμένη γραμμή HotPDF μετράται σωστά αν ένα εργαλείο στη συνέχεια τη διαβάσει ξανά

Δεν καλείτε το ExtTextOut μόνοι σας για στοιχισμένες παραγράφους. Το σημείο εισόδου είναι το WideTextOutBox, το οποίο αναδιπλώνει μια συμβολοσειρά Unicode σε ένα κουτί και εφαρμόζει τη στοίχιση που ζητάτε. Χωρίζει το κείμενο σε γραμμές που ταιριάζουν στο πλάτος του κουτιού, τοποθετεί κάθε γραμμή κάτω από το ύψος του κουτιού, και επιστρέφει τον αριθμό των χαρακτήρων που κατάφερε να χωρέσει πριν ξεμείνει από κατακόρυφο χώρο. Η στοίχιση επιλέγεται από το justification enum

type
  THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);

Οι τρεις πρώτες είναι αυτονόητες αριστερή, κεντραρισμένη και δεξιά στοίχιση. Η τέταρτη, jtJustify, είναι η πλήρης στοίχιση και στα δύο άκρα που περιγράφεται εδώ, και είναι η τιμή που διαβάζει το WideTextOutBox για να ενεργοποιήσει το διάστημα με επίγνωση του συστήματος γραφής

Στοίχιση μιας παραγράφου στην πράξη

Ένα πλήρες παράδειγμα δημιουργεί ένα έγγραφο, ορίζει μια γραμματοσειρά και ρίχνει μια παράγραφο σε ένα κουτί με πλήρη στοίχιση. Ο ίδιος κώδικας στοιχίζει κείμενο Λατινικών και CJK χωρίς αλλαγή σημαίας, επειδή η επίγνωση του συστήματος γραφής ζει κάτω από το API

uses
  HPDFDoc;

procedure JustifyParagraph;
var
  Pdf: THotPDF;
  Body: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Justified.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', 11);

    Body :=
      'Full justification spreads the slack on each filled line so both ' +
      'edges meet the column, while the last line keeps its natural width. ' +
      'For scripts with word gaps the space lands between words; for ' +
      'scripts without them it spreads evenly between glyphs.';

    // X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
    Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Για να σχεδιάσετε το ίδιο μπλοκ στοιχισμένο αριστερά, κεντραρισμένο ή στοιχισμένο δεξιά, αλλάξτε μόνο το τελικό όρισμα σε jtLeft, jtCenter, ή jtRight. Η αναδίπλωση, η τοποθέτηση γραμμής και η τιμή επιστροφής παραμένουν ίδιες. Το μετρημένο πλάτος που καθοδηγεί και τις τέσσερις διαδρομές προέρχεται από το GetWideTextWidth, το ερώτημα πλάτους με επίγνωση Unicode που μετράει ένα WideString σωστά εκεί που η παλαιότερη μέτρηση ανά byte θα υπολόγιζε λάθος οτιδήποτε πέρα από το Latin-1, το οποίο είναι αυτό που κάνει το κουτί να αναδιπλώνει το κείμενο CJK και surrogate-pair στο σωστό μέρος εξαρχής

Η στοίχιση είναι ένα στρώμα μιας ευρύτερης στοίβας μορφοποίησης κειμένου. Όταν μια γραμμή περιέχει συστήματα γραφής που αναδιατάσσουν ή ενώνουν τα γλυφικά τους, οι αποφάσεις για τα διαστήματα εδώ κάθονται πάνω από την εργασία που περιγράφεται στο άρθρο μας για τη διαμόρφωση κειμένου σε πολύπλοκα συστήματα γραφής (complex-script text shaping), και όταν μια γραμματοσειρά φέρει τυπογραφικές παραλλαγές που θέλετε να επιλέξετε, δείτε πώς να οδηγήσετε τις υφολογικές εναλλακτικές OpenType GSUB (stylistic alternates). Όλα αυτά αποστέλλονται στο HotPDF Component για Delphi και C++Builder, μαζί με τα ευρύτερα API κειμένου, διάταξης και εγγράφων που καλύπτονται σε όλο αυτό το ιστολόγιο