Ένα προσβάσιμο PDF στηρίζεται σε μία δομή που η ορατή σελίδα δεν δείχνει ποτέ: το structure tree που ορίζει το ISO 32000-1 §14.7. Είναι μια λογική ιεραρχία από επικεφαλίδες, παραγράφους, πίνακες και εικόνες, στρωμένη πάνω από το ζωγραφισμένο περιεχόμενο και αντιστοιχισμένη σε τυπικούς ρόλους μέσω ενός role map. Ένας αναγνώστης οθόνης διαβάζει αυτό το δέντρο, όχι τα σημάδια στη σελίδα. Χωρίς αυτό, ένα παραγόμενο τιμολόγιο που δείχνει άψογο είναι σημασιολογικά κενό, γιατί το content stream καταγράφει σειρά σχεδίασης και τίποτε άλλο. Το σύνολο μπορεί να ανακοινωθεί πριν από τις γραμμές των ειδών, το υποσέλιδο μπορεί να κόψει μια παράγραφο στη μέση, ο πίνακας των ειδών μπορεί να καταρρεύσει σε έναν αδιαφοροποίητο χείμαρρο λέξεων. Το κόστος της πρόληψης γέρνει ασύμμετρα υπέρ σας. Το να εκπέμπετε δομή καθώς σχεδιάζετε είναι λεπτά κώδικα· το να την προσθέσετε εκ των υστέρων σε ολοκληρωμένα έγγραφα είναι έργο αποκατάστασης. Η losLab PDF Library (PDF Library for Delphi) εκθέτει το δέντρο σε Delphi και C++Builder μέσα από ένα μικρό σύνολο κλήσεων που τυλίγουν κάθε λειτουργία σχεδίασης στον λογικό της ρόλο
Πώς το marked content δένεται με το structure tree
Δύο επίπεδα συνεργάζονται. Μέσα στο content stream, οι λειτουργίες σχεδίασης περικλείονται σε ακολουθίες marked content, καθεμία με έναν ακέραιο MCID. Στον κατάλογο του εγγράφου, το structure tree αντιστοιχίζει αυτά τα MCID σε μια ιεραρχία τυποποιημένων στοιχείων (H1, P, Table, Figure) με χαρακτηριστικά όπως εναλλακτικό κείμενο και γλώσσα. Οι προσαρμοσμένοι τύποι στοιχείων επιτρέπονται, αλλά ο καθένας πρέπει να ανάγεται σε τυπικό ρόλο μέσω του role map (ISO 32000-1 §14.8.4). Το περιεχόμενο που δεν φέρει κανένα νόημα, όπως γραμμές, φόντα και επαναλαμβανόμενα στοιχεία σελίδας, σημειώνεται ως artifact ώστε η υποστηρικτική τεχνολογία να το προσπερνά αντί να το διαβάζει στη μέση μιας πρότασης
Το PDF Library for Delphi συντηρεί και τα δύο επίπεδα πίσω από ένα ζεύγος αγκυλών. Το BeginTag ανοίγει ένα στοιχείο δομής και ξεκινά την ακολουθία marked content, οι κλήσεις σχεδίασης προσγειώνονται μέσα της, και το EndTag κλείνει και τα δύο. Η λογιστική που μπερδεύει το χειροποίητο tagging, δηλαδή τα MCID, το δέντρο γονέων και οι αναφορές σελίδων, γίνεται εσωτερικά, εκεί που δεν μπορείτε να την κάνετε λάθος
Δύο διακόπτες σε επίπεδο εγγράφου πλαισιώνουν τη δουλειά προτού ανοίξει οποιοδήποτε tag. Το SetMarkInfo γράφει τη σημαία καταλόγου που δηλώνει το έγγραφο ως tagged, και το IsTaggedPDF τη διαβάζει πίσω, που είναι η φθηνή πρώτη ανίχνευση όταν αποφασίζετε αν ένα εισερχόμενο αρχείο έχει δομή που αξίζει να διατηρηθεί. Η γλώσσα έχει δύο σημεία εισόδου. Το SetDocumentLanguage ορίζει από μόνο του την προεπιλογή του εγγράφου, ενώ το SetPDFUAMode την ορίζει ως μέρος της ενεργοποίησης πλήρους εξόδου PDF/UA. Ένα αρχείο μπορεί να είναι χρήσιμα tagged χωρίς να διεκδικεί συμμόρφωση PDF/UA, και μια σταδιακή υιοθέτηση ξεκινά συχνά ακριβώς από εκεί
Tagging την ώρα της σχεδίασης, όχι μετά
Το μοτίβο παραγωγής που δουλεύει είναι να αντιμετωπίζετε το ζεύγος tag ως μέρος της υπογραφής κάθε κλήσης σχεδίασης, ποτέ ως μεταγενέστερο πέρασμα:
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1); // αρχή πάνω αριστερά
Lib.SetPDFUAMode('en-US'); // ανεβάζει την έκδοση αποθήκευσης σε PDF 1.7
Lib.SetInformation(1, 'Service Manual'); // το /Title είναι υποχρεωτικό για PDF/UA
Lib.AddRoleMap('ManualTitle', 'H1'); // προσαρμοσμένος τύπος -> τυπικός ρόλος
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
Lib.DrawText(72, 96, 'Service Manual');
Lib.EndTag;
Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
Lib.AddImageFromFile('gearbox.png', 0);
Lib.EndTag;
Lib.BeginArtifact('Layout'); // διακόσμηση σελίδας: εκτός ανάγνωσης
// ... σχεδιάστε γραμμές και απόχρωση φόντου ...
Lib.EndArtifact;
Lib.SaveToFile('manual.pdf');
finally
Lib.Free;
end;
end;
Τρεις κλήσεις σε εκείνη την ακολουθία έχουν βάρος συμμόρφωσης. Το SetPDFUAMode ενεργοποιεί την έξοδο PDF/UA και ανεβάζει σιωπηλά την έκδοση του εγγράφου σε PDF 1.7, κάτι που συγκρούεται με το καρφίτσωμα έκδοσης. Ένα έγγραφο κλειδωμένο σε PDF 1.4 με LockSaveVersion αρνείται να αποθηκευτεί και επιστρέφει κωδικό σφάλματος 602 μόλις ενεργοποιηθεί η λειτουργία UA, μια σύγκρουση που τείνει να εμφανίζεται όταν τα αρχειακά προφίλ και οι απαιτήσεις προσβασιμότητας ρυθμίζονται από διαφορετικές ομάδες. Το SetInformation(1, ...) γράφει τον τίτλο του εγγράφου, τον οποίο το ISO 14289 περιμένει να δείχνουν οι viewers στη θέση του ονόματος αρχείου· η απουσία του είναι ένα από τα πιο συχνά ευρήματα PDF/UA στην πράξη. Το AddRoleMap καταχωρίζει τον προσαρμοσμένο τύπο ManualTitle ως H1, και η παράλειψή του αφήνει τα διαγνωστικά που περιγράφονται παρακάτω να επισημαίνουν έναν μη αντιστοιχισμένο ρόλο
Τα επίπεδα επικεφαλίδων αξίζουν συνειδητή πολιτική, όχι αυθόρμητες επιλογές που γίνονται για την εμφάνιση μιας σελίδας. Οι χρήστες αναγνωστών οθόνης πηδούν από ενότητα σε ενότητα με συντόμευση επικεφαλίδας, οπότε ένα πρότυπο που πάει από H1 σε H3 επειδή το ενδιάμεσο επίπεδο φαινόταν υπερβολικά μεγάλο στο οπτικό σχέδιο σπάει αθόρυβα εκείνη την πλοήγηση, και καμία οπτική επιθεώρηση δεν θα το πιάσει ποτέ. Είναι ακριβώς το ελάττωμα που υπάρχει για να κατονομάζει το διαγνωστικό HEADING-LEVEL-SKIP. Αντιστοιχίστε μία φορά, σε ένα σημείο, τα οπτικά στυλ κάθε προτύπου σε μια σταθερή κλίμακα επικεφαλίδων, και η παρέκκλιση δεν ξεκινά ποτέ
Πίνακες που ένας αναγνώστης οθόνης μπορεί όντως να πλοηγήσει
Οι σχεδιασμένες γραμμές πλέγματος δεν σημαίνουν τίποτα εκτός οθόνης. Αυτό που πλοηγούν οι αναγνώστες οθόνης είναι δομικές σχέσεις: ποια κελιά είναι κεφαλίδες, τι διέπει κάθε κεφαλίδα, και πώς τα κελιά δεδομένων δένονται με τις κεφαλίδες σε ακανόνιστες διατάξεις. Οι κλήσεις χαρακτηριστικών των στοιχείων δομής χειρίζονται και τα τρία:
Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column'); // ισχύει μόνο όσο είναι ανοιχτό αυτό το TH
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2); // η κεφαλίδα καλύπτει στήλη τιμής και μονάδας
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part'); // ρητή σύνδεση για ακανόνιστους πίνακες
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table
Ο κανόνας της σειράς είναι αυστηρός και επιβάλλεται σιωπηλά. Κάθε κλήση SetStructElem* εφαρμόζεται στο tag που είναι ανοιχτό εκείνη τη στιγμή, ανάμεσα στο BeginTag και το EndTag του, και επιστρέφει 0 χωρίς να πετάξει τίποτα όταν δεν υπάρχει ανοιχτό tag ή όταν το χαρακτηριστικό δεν ισχύει για το τρέχον. Μια κλήση σε λάθος θέση απλώς εξαφανίζεται. Το να τυλίγετε τις τιμές επιστροφής σε assertions κατά την ανάπτυξη πιάνει την παρέκκλιση όσο μπορείτε ακόμη να τη δείτε· αν την αφήσετε, ένα scope που λείπει φαίνεται μόνο όταν ένας έλεγχος προσβασιμότητας περάσει έναν πραγματικό αναγνώστη οθόνης πάνω από τον πίνακα. Τα ID στοιχείων που περνούν μέσω BeginTagEx2 τροφοδοτούν το ID tree (ISO 32000-1 §14.7.4), και αυτό είναι που κάνει εξαρχής επιλύσιμη τη σύνδεση του SetStructElemHeaders
Η ίδια οικογένεια χαρακτηριστικών καλύπτει τα υπόλοιπα στα οποία στηρίζεται η υποστηρικτική τεχνολογία. Το SetStructElemListNumbering δηλώνει πώς επισημαίνονται τα στοιχεία λίστας, ώστε ένας αναγνώστης οθόνης να ανακοινώνει τη θέση μέσα στη λίστα αντί να απαγγέλλει τα glyphs των κουκκίδων. Το SetStructElemBBox καταγράφει το bounding box εικόνων και πινάκων, το οποίο χρησιμοποιούν οι προβολές αναδίπλωσης για να τοποθετήσουν το περιεχόμενο. Το SetStructElemActualText παρέχει κείμενο αντικατάστασης για αποσπάσματα των οποίων τα glyphs δεν αντιστοιχούν σε αναγνώσιμους χαρακτήρες, όπως ένα αρχιγράμμα συναρμολογημένο από διανυσματικά σχήματα. Το καθένα ακολουθεί τον ίδιο κανόνα: δένεται με το ανοιχτό tag, ή εξαφανίζεται
Artifacts, γλώσσα και η πύλη διαγνωστικών πριν από την αποθήκευση
Τα επαναλαμβανόμενα στοιχεία σελίδας, δηλαδή οι τρέχουσες κεφαλίδες, τα σημάδια δίπλωσης, τα υδατογραφήματα και οι αποχρώσεις φόντου, ανήκουν μέσα σε αγκύλες BeginArtifact και EndArtifact ώστε να μην μπαίνουν ποτέ στη ροή ανάγνωσης. Η γλώσσα κληρονομείται. Η προεπιλογή του εγγράφου προέρχεται από το όρισμα του SetPDFUAMode, και ένα απόσπασμα σε άλλη γλώσσα την υπερισχύει ανά στοιχείο μέσω BeginTagEx ή SetStructElemLang. Αυτό είναι που κρατά προφέρσιμο ένα γαλλικό απόσπασμα μέσα σε ένα αγγλικό εγχειρίδιο
Πριν από την αποθήκευση, το GetPDFUADiagnostics τρέχει τους δομικούς ελέγχους της βιβλιοθήκης πάνω στο έγγραφο που βρίσκεται στη μνήμη και επιστρέφει τα ευρήματα ως κείμενο, όπου η κενή συμβολοσειρά σημαίνει ότι δεν βρέθηκε τίποτα. Οι κωδικοί κατονομάζουν απευθείας τα κλασικά λάθη σύνταξης: FIGURE-NO-ALT για εικόνα χωρίς εναλλακτικό κείμενο, HEADING-LEVEL-SKIP για ένα H3 που ακολουθεί ένα H1, ROLEMAP-UNMAPPED για προσαρμοσμένο τύπο που δεν καταχωρίστηκε ποτέ. Συνδέστε το με το build (παραγωγή του συνόλου εγγράφων, αποτυχία του βήματος σε μη κενά διαγνωστικά) και οι παλινδρομήσεις προσβασιμότητας γίνονται αστοχίες σε ύφος χρόνου μεταγλώττισης αντί για ευρήματα ελέγχου μήνες αργότερα. Η πλήρης ετυμηγορία συμμόρφωσης ανήκει ακόμη στο preflight πάνω στο αποθηκευμένο αρχείο, που καλύπτεται στο preflight PDF/A και PDF/UA σε Delphi, γιατί ορισμένες κανονικοποιήσεις εφαρμόζονται μόνο κατά τη σειριοποίηση
Η πλοήγηση των annotations έχει το δικό της κουμπί. Το PDF/UA περιμένει η διέλευση με πληκτρολόγιο σε πεδία φόρμας και συνδέσμους να ακολουθεί τη σειρά της δομής, και το SetTabOrderMode γράφει την καταχώριση σειράς tab σε επίπεδο σελίδας που τιμούν οι viewers, με το GetTabOrderMode διαθέσιμο για τον έλεγχο εισερχόμενων αρχείων. Είναι το είδος της απαίτησης που κανείς δεν προσέχει μέχρι που ένας χρήστης μόνο με πληκτρολόγιο αναφέρει το σφάλμα, και κοστίζει μία κλήση ανά έγγραφο για να γίνει σωστά
Τα structure trees δεν επιβιώνουν από κάθε συγχώνευση
Τα tagged έγγραφα παραμένουν tagged μόνο όταν κάθε επόμενο βήμα επεξεργασίας διατηρεί το δέντρο, και η κοφτερή άκρη μέσα στο PDF Library for Delphi είναι η οικογένεια merge-list. Το MergeFileListFast ανταλλάσσει τη διατήρηση του structure tree με ταχύτητα. Αυτή είναι η σωστή ανταλλαγή για παρτίδες σαρωμένων εικόνων και η λάθος για tagged αναφορές, γιατί η έξοδος ανοίγει μια χαρά, αποδίδεται πανομοιότυπα και έχει χάσει αθόρυβα το επίπεδο προσβασιμότητάς της. Χρησιμοποιήστε το προεπιλεγμένο MergeFileList ή την αυστηρή παραλλαγή όποτε κάποια είσοδος είναι tagged, και κάντε το IsTaggedPDF μέρος των assertions μετά τη συναρμολόγηση ώστε μια ισοπεδωμένη παρτίδα να μην μπορεί να φύγει χωρίς να το προσέξει κάποιος. Οι ροές συναρμολόγησης για μεγάλα σύνολα εγγράφων κουβαλούν περισσότερους τέτοιους συμβιβασμούς, που εξετάζονται στη συγχώνευση, διαχωρισμό και άμεση πρόσβαση μεγάλων PDF
Ο βρόχος επαλήθευσης κλείνει έξω από τη βιβλιοθήκη: ανοίξτε την έξοδο στον Acrobat, επιθεωρήστε τον πίνακα tags και διαβάστε τουλάχιστον ένα έγγραφο ανά οικογένεια προτύπων με πραγματικό αναγνώστη οθόνης. Τα διαγνωστικά πιάνουν δομικά λάθη· μόνο ένα ανθρώπινο αυτί πιάνει μια σειρά ανάγνωσης που είναι τεχνικά έγκυρη και πρακτικά ακατανόητη. Τα builds αξιολόγησης και η πλήρης αναφορά του API tagging βρίσκονται στη σελίδα προϊόντος losLab PDF Library for Delphi