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

Ανάγνωση και εγγραφή marked content PDF σε Delphi

Το marked content είναι ο μηχανισμός που ορίζει το ISO 32000-1 §14.6 για την επισήμανση περιεχομένου σελίδας, και το tagged PDF και το PDF/UA είναι και τα δύο χτισμένα πάνω του. Το PDFium Component το εκθέτει απευθείας: η PageObjectMarks διαβάζει κάθε ετικέτα BDC και τη λίστα ιδιοτήτων της από ένα αντικείμενο σελίδας, η AddPageObjectMark γράφει μία, η RemovePageObjectMark διαγράφει μία, και η PageObjectMarkedContentID αναφέρει το MCID που συνδέει το περιεχόμενο με το δέντρο δομής

Μέχρι το δέντρο δομής να μπορεί να ενωθεί πίσω με το περιεχόμενο που περιγράφει, η εργαλειοθηκή προσβασιμότητας είναι εικασία. Το δέντρο δομής λέει «αυτός είναι ένας τίτλος»· το MCID λέει ποια σημάδια σε ποια σελίδα είναι πραγματικά αυτός ο τίτλος. Και τα δύο μισά πρέπει να είναι αναγνώσιμα πριν μια εφαρμογή μπορέσει να ελέγξει, να επισκευάσει ή να αναφέρει πάνω στην επισήμανση

Τι είναι ένα σημάδι, σε bytes;

Ένας τελεστής BDC με ένα όνομα ετικέτας και μια προαιρετική λίστα ιδιοτήτων, κλεισμένος από EMC. Στο ρεύμα περιεχομένου μοιάζει με /P <</MCID 3>> BDC ... EMC: η ετικέτα /P ονομάζει τον ρόλο, το λεξικό φέρει ιδιότητες, και ό,τι βρίσκεται μεταξύ των τελεστών είναι το marked content. Ένα αντικείμενο σελίδας μέσα σε αυτό το διάστημα φέρει το σημάδι, που είναι αυτό που παραδίδει το PDFium και που το PDFium Component μετατρέπει σε εγγραφή

Η TPdfContentMark κρατά έναν χειριστή, την Name της ετικέτας, και έναν πίνακα από TPdfContentMarkParam. Κάθε παράμετρος έχει ένα Key, ένα Kind και ένα ουσιαστικό πεδίο τιμής που επιλέγεται από αυτό το είδος: pmpInt, pmpFloat, pmpString ή pmpBlob. Το είδος προέρχεται από την ίδια αναφορά τύπου του PDFium παρά από όποιο getter τύχαιν να πετύχει, που είναι η διαφορά μεταξύ ανάγνωσης μιας λίστας ιδιοτήτων και εικασίας πάνω της

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

Γιατί το pmpUnknown σημαίνει δύο διαφορετικά πράγματα

Το pmpUnknown επιστρέφεται όταν το PDFium αναφέρει FPDF_OBJECT_UNKNOWN, και το PDFium επίσης το επιστρέφει για ένα κλειδί που δεν υπάρχει. Οι δύο περιπτώσεις δεν μπορούν να διακριθούν σε αυτό το επίπεδο, και το να προσποιηθείτε το αντίθετο θα ήταν χειρότερο από το να το πείτε

Η πρακτική συνέπεια για τον κώδικά σας: αντιμετωπίστε το pmpUnknown ως «καμία usable τιμή εδώ» παρά ως έναν τύπο που μπορεί εν πάση περιπτώσει να αποκωδικοποιήσετε. Αν μια ιδιότητα έχει σημασία για τη ροή εργασίας σας, επαληθεύστε ότι είναι παρούσα με ένα είδος που αναγνωρίζετε, και μην συμπεραίνετε απουσία από ένα άγνωστο — ένα σημάδι του οποίου η λίστα ιδιοτήτων δεν μπορείτε να διαβάσετε είναι ένα σημάδι που πρέπει να αναφέρετε, όχι ένα που πρέπει να αποδεχτείτε σιωπηλά

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

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

Αυτή είναι η ίδια πειθαρχία που ισχύει για τους χειριστές αντικειμένων σελίδας γενικά στο PDFium, και πιάνει τους ανθρώπους στο ίδιο σημείο: ένα στοιχείο ελέγχου λίστας συμπληρωμένο με εγγραφές σημείων, ένας χρήστης που πλοηγείται σε άλλη σελίδα, και μια κατάρρευση που φαίνεται άσχετη με την πλοήγηση. Αντιγράψτε τις τιμές που χρειάζεστε — το όνομα, τα κλειδιά, τους αριθμούς — και αφήστε τον χειριστή. Οι σημειώσεις για τους χειριστές αντικειμένων σελίδας που μπαίνουν σε stale μετά από μετασχηματισμό καλύπτουν τον γενικό κανόνα και πώς δαγκώνει αλλού

Προσθήκη ενός σημείου, και το βήμα αποθήκευσης που είναι εύκολο να παραλειφθεί

Η AddPageObjectMark δέχεται τον δείκτη αντικειμένου σελίδας, ένα όνομα ετικέτας και ένα πλήρες σύνολο παραμέτρων. Οι παράμετροι γράφονται ως σύνολο παρά να γίνουν patch ένα κλειδί τη φορά, που είναι γιατί η TPdfContentMarkParam δεν έχει φρουρούς Has* — η περίπτωση «ενημέρωσε ένα πεδίο μιας υπάρχουσας εγγραφής» που θα φρουρούσαν αυτοί δεν προκύπτει

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

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

Τι κάνει και τι δεν κάνει αυτό σε ένα έγγραφο

Τα σημάδια μόνα τους δεν κάνουν ένα tagged PDF. Ένα συμμορφούμενο tagged έγγραφο χρειάζεται ένα δέντρο δομής του οποίου τα στοιχεία αναφέρονται σε αυτά τα MCID, μια καταχώριση /MarkInfo που δηλώνει το έγγραφο marked, και ονόματα ρόλων που σημαίνουν αυτό που λέει το πρότυπο. Η εγγραφή ενός σημείου /P με ένα MCID στο οποίο δεν δείχνει κανένα στοιχείο δομής σας δίνει περιεχόμενο που διεκδικεί ότι είναι tagged και ένα δέντρο δομής που δεν το αναφέρει ποτέ

Εκεί που το marked content κερδίζει πραγματικά τη θέση του σε αυτό το επίπεδο είναι η επιθεώρηση και η επισκευή: έλεγχος ποια αντικείμενα σελίδας είναι tagged, εύρεση artifacts που θα έπρεπε να είχαν επισημανθεί ως τέτοια, ή αντιστοίχιση MCID έναντι ενός δέντρου δομής για εύρεση των ορφανών. Για το μισό δέντρο δομής αυτής της εργασίας, δείτε την ανάλυση της επικύρωσης δέντρου δομής PDF/UA, και για την εμπειρία ανάγνωσης για την οποία τελικά είναι οι ετικέτες, τις σημειώσεις για την κατασκευή ενός προσβάσιμου αναγνώστη PDF σε Delphi

Το PDFium Component δίνει σε εφαρμογές Delphi, C++Builder και Lazarus ένα VCL API υψηλού επιπέδου πάνω από τη μηχανή PDFium, με marked content, δέντρα δομής και επικύρωση προσβασιμότητας προσβάσιμα από κοινό κώδικα Pascal — δείτε τη σελίδα προϊόντος PDFium Component για την πλήρη επιφάνεια API