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

Επαλήθευση του δέντρου δομής PDF/UA στο Delphi με το PDFium

Η προεπιθεώρησή σας αναφέρει ότι το αρχείο είναι καθαρό ως προς PDF/UA. Το veraPDF ανοίγει το ίδιο αρχείο και επισημαίνει ένα Figure χωρίς εναλλακτικό κείμενο στην παράγραφο 7.3. Και τα δύο εργαλεία έχουν δίκιο, και το κενό ανάμεσά τους είναι ακριβώς το πρόβλημα του ελέγχου προσβασιμότητας με σάρωση bytes. Ένα πέρασμα σε επίπεδο byte επιβεβαιώνει ότι το αρχείο λέει ότι έχει ετικέτες: εντοπίζει το /StructTreeRoot, το /MarkInfo /Marked true, το pdfuaid:part στο XMP packet, τον τίτλο του εγγράφου και τη γλώσσα. Αυτά είναι δείκτες μορφής, και είναι απαραίτητοι. Δεν σας λένε τίποτα για το αν το πραγματικό Figure στη σελίδα τέσσερα περιέχει μια περιγραφή που μπορεί να διαβάσει μεγαλόφωνα ένας screen reader. Η απάντηση ζει στο tag tree, και για να τη βρείτε πρέπει να περάσετε από το δέντρο

Το PDFium Component είναι μια εγγενής βιβλιοθήκη PDF VCL για Delphi και C++Builder, και το ValidatePdfUa κάνει και τα δύο περάσματα. Το πέρασμα σε επίπεδο byte χειρίζεται τους δείκτες μορφής. Πάνω του χτίζεται ένα πέρασμα δέντρου δομής που φορτώνει το ζωντανό tagged tree, διατρέχει κάθε στοιχείο και ελέγχει το μικρό σύνολο κανόνων περιεχομένου υψηλής βεβαιότητας, όπου ένα λείπον χαρακτηριστικό σημαίνει πραγματικό ελάττωμα προσβασιμότητας και όχι αισθητική προτίμηση. Αυτό το άρθρο αφορά αυτό το δεύτερο πέρασμα, τι ελέγχει, γιατί η λογική των κανόνων είναι καθαρή συνάρτηση χωρίς DLL από κάτω της, και πού σταματά επίτηδες νωρίτερα

Γιατί μια σάρωση bytes δεν μπορεί να δει ένα Alt που λείπει

Το ISO 14289-1 (PDF/UA-1) είναι ένα στρώμα απαιτήσεων πάνω από το ISO 32000. Ορισμένες από αυτές τις απαιτήσεις είναι δομικές και ορατές στο ακατέργαστο αρχείο: ο κατάλογος πρέπει να δηλώνει ένα δέντρο δομής, οι προτιμήσεις προβολής πρέπει να ορίζουν DisplayDocTitle, οι γραμματοσειρές πρέπει να είναι ενσωματωμένες. Ένας token scanner που αφαιρεί τα σώματα των ροών και ταιριάζει name tokens με οριοθέτες μπορεί να επαληθεύσει όλα αυτά, και του PDFium ValidatePdfUaCompliance κάνει ακριβώς αυτό για ρήτρες όπως 7.1, 7.18 και 7.21

Αλλά "κάθε Figure έχει εναλλακτικό κείμενο" δεν είναι ιδιότητα της σύνταξης του αρχείου. Είναι ιδιότητα της λογικής δομής του δέντρου ετικετοποιημένων στοιχείων που χαρτογραφεί το περιεχόμενο στο νόημα. Η καταχώριση Alt ενός Figure μπορεί να βρίσκεται στο λεξικό του στοιχείου δομής, να παρέχεται μέσω ενός /ActualText span ή να προέρχεται από έναν προσαρμοσμένο τύπο αντιστοιχισμένο σε ρόλο. Δεν μπορείτε να τη βρείτε αξιόπιστα κάνοντας grep για /Alt στο byte stream, επειδή αυτή η συμβολοσειρά εμφανίζεται σε άσχετα συμφραζόμενα, μπορεί να είναι συμπιεσμένη μέσα σε object stream και δεν σας λέει τίποτα για το ποιο στοιχείο δομής ανήκει. Ο ειλικρινής τρόπος να απαντήσετε στο ερώτημα είναι να ρωτήσετε το ίδιο το structure tree του εγγράφου, στοιχείο προς στοιχείο, όπως ακριβώς το ίδιο επίπεδο που αξιολογούν τα veraPDF και PAC. Αυτό είναι το πλαίσιο γύρω από το οποίο χτίζονται οι έλεγχοι Tier-1 του PDFium: σάρωση bytes για τη μορφή, περιήγηση του δέντρου για το περιεχόμενο

Ανάγνωση του ζωντανού δέντρου ετικετών

Η ακατέργαστη πηγή είναι TPdf.GetStructureElements (επίσης διαθέσιμη ως StructureElements ιδιότητα), η οποία επιστρέφει ένα TPdfStructureElements, δηλαδή έναν επίπεδο πίνακα από TPdfStructureElement εγγραφές με σειρά εγγράφου. Κάθε εγγραφή είναι η προβολή ενός στοιχείου δομής μέσα από τις accessor functions του PDFium, με τα πεδία που χρειάζονται πραγματικά οι κανόνες προσβασιμότητας:

type
  TPdfStructureElement = record
    Level: Integer;            // depth in the tag tree
    ParentIndex: Integer;      // index of parent element, or -1
    TypeName: WString;         // standard /S name: Figure, Formula, Note...
    Title: WString;            // /T
    AlternateText: WString;    // /Alt   (FPDF_StructElement_GetAltText)
    ActualText: WString;       // /ActualText
    Expansion: WString;        // /E
    ID: WString;               // /ID    (FPDF_StructElement_GetID)
    Language: WString;         // /Lang
    MarkedContentIDs: TPdfIntegerArray;
    // ... child bookkeeping fields
  end;

Το TypeName πεδίο είναι αυτό πάνω στο οποίο περιστρέφεται ο validator. Προέρχεται από το FPDF_StructElement_GetType, το οποίο επιστρέφει τον standard structure type του στοιχείου, δηλαδή το /S όνομά του, αφού το PDFium επιλύσει το role map. Το AlternateText προέρχεται από το FPDF_StructElement_GetAltText, το ActualText από το FPDF_StructElement_GetActualText, και το ID από το FPDF_StructElement_GetID. Επειδή ο πίνακας είναι επίπεδος και ταξινομημένος, ο validator μπορεί να συλλογιστεί ολόκληρο το έγγραφο με τη μία αντί να κάνει αναδρομή, κάτι που μετρά για τον έναν κανόνα που είναι καθολικός και όχι ανά στοιχείο

Ο έλεγχος είναι καθαρή συνάρτηση, και αυτό είναι σκόπιμο

Η λογική των κανόνων δεν βρίσκεται μέσα στη μέθοδο που μιλά με το DLL. Είναι μια αυτόνομη, δημόσια, καθαρή συνάρτηση:

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Δέχεται έναν επίπεδο πίνακα στοιχείων και επιστρέφει ένα σύνολο ζητημάτων. Δεν καλεί καμία συνάρτηση του PDFium, δεν ανοίγει κανένα έγγραφο, δεν αγγίζει καμία καθολική κατάσταση. Αυτός ο διαχωρισμός είναι σκόπιμος και αποδίδει διπλά. Πρώτον, δοκιμασιμότητα: μπορείτε να φτιάξετε έναν συνθετικό TPdfStructureElements πίνακα σε unit test, με ένα Figure χωρίς Alt, μια Formula της οποίας το μόνο προσβάσιμο κείμενο είναι στο ActualText, και δύο Notes που μοιράζονται ένα ID, και να κάνετε assert στο σύνολο αποτελεσμάτων χωρίς pdfium.dll να υπάρχει καθόλου το PDFium. Η λογική των κανόνων επαληθεύεται offline, ενώ η διέλευση μέσω του DLL επαληθεύεται ξεχωριστά με ένα smoke test πάνω σε ζωντανό έγγραφο που παραλείπεται όταν η βιβλιοθήκη λείπει

Δεύτερον, σαφήνεια ευθύνης. TPdf.ValidatePdfUa αναλαμβάνει το δύσκολο μέρος, το φόρτωμα κάθε σελίδας, την άντληση των στοιχείων της και τη συγκέντρωσή τους, και στη συνέχεια δίνει έναν καθαρό πίνακα στον καθαρό έλεγχο. Το "Get the data" (DLL, side effects, lifetime) και το "judge the rules" (pure, deterministic) δεν μπλέκονται ποτέ. Όταν ένας κανόνας χρειάζεται αλλαγή, αλλάζετε μια συνάρτηση που δεν έχει καθόλου I/O μέσα της

Τι ελέγχουν πραγματικά οι τρεις κανόνες

Το πέρασμα του structure tree παράγει τρεις τιμές ζητημάτων, οι οποίες προστίθενται στο τέλος του TPdfUaValidationIssues ώστε το enum να παραμένει ABI-stable για τους υπάρχοντες καλούντες: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, και pvuaiNoteMissingId. Το σώμα είναι αρκετά μικρό ώστε να μπορεί κανείς να το αναλύσει πλήρως:

for I := 0 to High(Elements) do
begin
  T := string(Elements[I].TypeName);
  if T = 'Figure' then
  begin
    // §7.3 — a Figure needs an alternate representation:
    // an Alt entry OR ActualText. Flag only when BOTH are empty.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFigureMissingAlt);
  end
  else if T = 'Formula' then
  begin
    // §7.7 — same rule as Figure: Alt OR ActualText.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFormulaMissingAlt);
  end
  else if T = 'Note' then
  begin
    // §7.9 — every Note must have a unique ID.
    NoteId := string(Elements[I].ID);
    if NoteId = '' then
      Include(Result, pvuaiNoteMissingId)
    else
      for J := 0 to I - 1 do
        if (string(Elements[J].TypeName) = 'Note') and
           (string(Elements[J].ID) = NoteId) then
        begin
          Include(Result, pvuaiNoteMissingId);
          Break;
        end;
  end;
end;

Η ρήτρα 7.3 αφορά τα figures: ένα Figure στοιχείο πρέπει να παρέχει ένα εναλλακτικό κείμενο. Η αρχική έκδοση αυτού του ελέγχου εξέταζε μόνο την εγγραφή Alt, κάτι που τον έκανε αυστηρότερο από τους reference validators. Το PDF/UA δέχεται ένα figure του οποίου το προσβάσιμο κείμενο παρέχεται μέσω του ActualText αντί αυτού, επειδή το replacement text είναι έγκυρη εναλλακτική αναπαράσταση, έτσι ο κανόνας επισημαίνει ένα Figure μόνο όταν και τα δύο Alt και ActualText είναι κενά. Η ρήτρα 7.7 καλύπτει τις formulas, και μετά την ίδια διόρθωση χρησιμοποιεί το ίδιο Alt-or-ActualText test. Ένα δείγμα από το conformance corpus που έδινε σε μια Formula το προσβάσιμο κείμενό της μόνο μέσω ActualText απορριπτόταν λανθασμένα, μέχρι το Formula branch να ευθυγραμμιστεί με το Figure branch

Η ρήτρα 7.9 είναι διαφορετική ως προς το είδος της. Ένα Note πρέπει να έχει ένα /ID και αυτό το ID πρέπει να είναι μοναδικό σε όλο το έγγραφο. Ένα λείπον ID είναι αποτυχία ανά στοιχείο. Ένα διπλότυπο ID είναι σχέση ανάμεσα σε δύο στοιχεία, και γι’ αυτό έχει σημασία ο επίπεδος πίνακας: για κάθε Note, ο έλεγχος σαρώνει προς τα πίσω τα στοιχεία που έχουν ήδη δει και επισημαίνει σύγκρουση με οποιοδήποτε προηγούμενο Note που φέρει το ίδιο ID. Το κόστος είναι το προφανές O(n²) ως προς το πλήθος των Notes, κάτι αμελητέο για κάθε πραγματικό έγγραφο, και κρατά τη συνάρτηση σε έναν μόνο ευανάγνωστο βρόχο χωρίς βοηθητικό index που πρέπει να παραμένει συγχρονισμένο

Συγκέντρωση σε όλες τις σελίδες ώστε η μοναδικότητα να είναι καθολική

Το PDFium εκθέτει τα structure elements ανά σελίδα, όχι ανά έγγραφο, οπότε η orchestration στο ValidatePdfUa πρέπει να τα συγκεντρώσει πριν εκτελεστούν οι κανόνες. Διασχίζει κάθε σελίδα με FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, ανεξάρτητα από τη σελίδα που έχει ανοιχτή εκείνη τη στιγμή το component, και προσθέτει τα στοιχεία κάθε σελίδας σε έναν πίνακα. Μόνο τότε καλεί τον καθαρό έλεγχο:

// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
   (not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
  AllElems := nil;
  PageTotal := FPDF_GetPageCount(FDocument);
  for I := 0 to PageTotal - 1 do
  begin
    Page := FPDF_LoadPage(FDocument, I);
    if Page = nil then Continue;
    try
      PageElems := GetStructureElementsForPage(Page);
    finally
      FPDF_ClosePage(Page);
    end;
    // append PageElems into AllElems ...
  end;
  Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;

Η συγκέντρωση είναι αυτό που κάνει σωστό τον έλεγχο μοναδικότητας της 7.9. Δύο Notes σε διαφορετικές σελίδες μπορούν να μοιράζονται ένα ID. Αν κάνατε validation σελίδα με σελίδα, δεν θα βλέπατε ποτέ τη σύγκρουση, επειδή το σύνολο στοιχείων κάθε σελίδας φαίνεται εσωτερικά συνεπές. Η δημιουργία ενός πίνακα σε επίπεδο εγγράφου είναι ο μόνος τρόπος να γίνει ορατό το διπλότυπο. Αξίζει να σημειωθεί και η δικλείδα στην αρχή: το tree walk εκτελείται μόνο όταν το byte-level pass δεν δεν αναφέρει pvuaiMissingStructTreeRoot. Ένα έγγραφο χωρίς tags δεν έχει δέντρο να διασχίσει και έχει ήδη επισημανθεί για το ότι λείπει το structure root, οπότε τα φορτώματα ανά σελίδα παραλείπονται εντελώς. Το deep pass δεν κοστίζει τίποτα στα έγγραφα που δεν μπορούν να ωφεληθούν από αυτό

Συντηρητικό από σχεδιασμό: αν χάσει κάτι, το κάνει σιωπηλά, ποτέ δεν φωνάζει χωρίς λόγο

Η πιο σημαντική ιδιότητα αυτού του validator είναι αυτό που αρνείται να κάνει. Ταιριάζει μόνο με τα standard /S type names που FPDF_StructElement_GetType επιστρέφει απευθείας, Figure, Formula, Note. Ένα έγγραφο που ορίζει έναν custom type και τον role-maps σε Figure θα, ανάλογα με το πώς επιλύει το PDFium τον τύπο, αναφέρει το δικό του όνομα. Όταν συμβεί αυτό, ο έλεγχος δεν το αναγνωρίζει και μένει σιωπηλός. Αυτό είναι false negative, και είναι η επιδιωκόμενη συμπεριφορά. Ο σχεδιαστικός κανόνας είναι να να υποεκθέτει παρά να παράγει ποτέ ψευδώς θετικό αποτέλεσμα, επειδή ένα εργαλείο προελέγχου που βλέπει λύκους εκεί όπου δεν υπάρχουν σε συμμορφωμένα αρχεία μαθαίνει τους χρήστες του να το αγνοούν, και ένας επικυρωτής που αγνοείται είναι χειρότερος από το να μην υπάρχει καθόλου. Οι διακοσμητικές εικόνες ζουν στη ροή των artifacts, όχι στο δέντρο δομής, οπότε δεν εμφανίζονται ποτέ ως Figures εξαρχής· δεν θα λάβετε παράπονο για "missing Alt" σχετικά με έναν κανόνα φόντου που έχει επισημανθεί σωστά ως artifact

Αυτός είναι επίσης ο λόγος που το πεδίο περιορίζεται σε τρεις κανόνες. Η ένθεση επιπέδων επικεφαλίδων (ρήτρα 7.4), το εύρος των κεφαλίδων πινάκων (7.5) και ο εντοπισμός κύκλου στο role map (7.1) είναι όλα έγκυρες απαιτήσεις PDF/UA, αλλά ο σωστός έλεγχός τους χρειάζεται πραγματική ανάλυση γραφήματος και γνωρισμάτων, και ο αφελής έλεγχος παράγει ακριβώς τα ψευδώς θετικά που απαγορεύει ο σχεδιασμός, επειδή το PDF/UA επιτρέπει μοτίβα επικεφαλίδων όπως H1, H2, H3, H3 που ένας απλός κανόνας "πρέπει να αυξάνεται αυστηρά" θα απέρριπτε λανθασμένα. Αυτοί οι έλεγχοι αφήνονται σε εξειδικευμένα εργαλεία συμμόρφωσης. Το σύνολο Tier-1 είναι το υποσύνολο όπου ένα ελλείπον γνώρισμα είναι σαφές

Το όριο, διατυπωμένο καθαρά

Αξίζει να γνωρίζετε δύο περιορισμούς πριν το εντάξετε σε μια πύλη έκδοσης. Πρώτον, ο ελεγκτής είναι τόσο καλός όσο αυτά που μπορεί να διαβάσει το PDFium από το στοιχείο δομής. Μερικά αρχεία του corpus συμμόρφωσης που περνούν από τους validators αναφοράς χρησιμοποιούν έναν μηχανισμό εναλλακτικού κειμένου που το PDFium δεν εμφανίζει, οπότε FPDF_StructElement_GetAltText επιστρέφει κενό παρόλο που το αρχείο είναι πραγματικά συμμορφωμένο. Ο καθαρός ελεγκτής τότε επισημαίνει "σωστά" ένα missing Alt πάνω σε ελλιπή δεδομένα, ένα ψευδώς θετικό που προέρχεται από την κάλυψη των accessors της DLL, όχι από τη λογική του κανόνα. Η χαλάρωση του κανόνα ώστε να απορροφήσει αυτές τις περιπτώσεις θα τον τύφλωνε επίσης απέναντι στις πραγματικές αστοχίες που προορίζεται να εντοπίζει, οπότε τεκμηριώνονται ως γνωστός περιορισμός του PDFium αντί να καλύπτονται πρόχειρα

Δεύτερον, αυτό είναι προέλεγχος, όχι πιστοποίηση. Το Tier-1 εντοπίζει με υψηλή βεβαιότητα τα σφάλματα περιεχομένου που μια σάρωση byte δεν μπορεί δομικά να δει, και το κάνει χωρίς ψευδείς συναγερμούς, αλλά η πλήρης συμμόρφωση PDF/UA, συμπεριλαμβανομένης της σημασιολογίας των επικεφαλίδων, της δομής του πίνακα και της ορθότητας της σειράς ανάγνωσης, εξακολουθεί να ανήκει σε έναν πλήρη validator και τελικά σε έναν ανθρώπινο αναθεωρητή. Χρησιμοποιήστε ValidatePdfUa για να αποτυγχάνουν γρήγορα και οικονομικά τα προφανή ελαττώματα στο δικό σας pipeline, και έπειτα αφήστε το veraPDF ή το PAC να έχουν τον τελικό λόγο. Η ίδια διέλευση του δέντρου δομής στηρίζει τη δημιουργία ενός προσβάσιμου PDF reader στο Delphi, όπου το tag tree καθοδηγεί τη σειρά ανάγνωσης και το εκφωνούμενο κείμενο, και συμπληρώνει την εργασία σε επίπεδο μεταδεδομένων στο reviewing PDF annotations from Delphi

Τα structure-tree APIs και ο ValidatePdfUa validator που παρουσιάζονται εδώ συνοδεύουν το PDFium Component για Delphi και C++Builder (VCL) και Lazarus/FPC (LCL). Η σελίδα του προϊόντος συνδέει την πλήρη αναφορά API, συμπεριλαμβανομένης της ολοκληρωμένης TPdfStructureElement διάταξης εγγραφής και της απαρίθμησης ζητημάτων πίσω από αυτούς τους ελέγχους