Comment puis-je créer un PDF accessible avec la bibliothèque Java PDFBox 2.0.8 qui est également vérifiable avec l'outil PAC 2?


Arrière-plan

J'ai un petit projet sur GitHub dans lequel j'essaie de créer une section 508 conforme (section508.gov) PDF qui a des éléments de formulaire dans une structure de table complexe. L'outil recommandé pour vérifier ces fichiers PDF est à http://www.access-for-all.ch/en/pdf-lab/pdf-accessibility-checker-pac.html et le PDF de sortie de mon programme passe la plupart de ces vérifications. Je saurai également à quoi chaque champ est destiné à l'exécution, donc en ajoutant des balises aux éléments de structure ne devrait pas être un problème.

Le Problème

L'outil PAC 2 semble avoir un problème avec deux éléments particuliers dans le PDF de sortie. En particulier, les annotations de widget de mes boutons radio ne sont pas imbriquées dans un élément de structure de formulaire et mon contenu marqué n'est pas étiqueté (cellules de texte et de tableau). PAC 2 vérifie l'élément de structure P qui se trouve dans la cellule en haut à gauche mais pas le contenu marqué ...

Cependant, le CIP 2 identifie le marqué content en tant qu'erreur (c'est-à-dire objet Texte/Chemin non tagué). De plus, les widgets de bouton radio sont détectés, mais il ne semble pas y avoir d'API pour les ajouter à un élément de structure de formulaire.

Ce Que J'Ai Essayé

J'ai regardé plusieurs questions sur ce site et d'autres sur le sujet, y compris celle-ci Tagged PDF with PDFBox, mais il semble qu'il n'y ait presque pas d'exemples pour PDF/UA et très peu de documentation utile (que j'ai trouvée). La plupart des des conseils utiles que j'ai trouvés ont été sur des sites qui expliquent les spécifications pour les PDF étiquetés comme https://taggedpdf.com/508-pdf-help-center/object-not-tagged/.

La Question

Est-il possible de créer un PDF vérifiable PAC 2 avec Apache PDFBox qui inclut du contenu marqué et des annotations de widget de bouton radio? Si c'est possible, est-ce faisable en utilisant des API PDFBox de niveau supérieur (non obsolètes)?

Note latérale: C'est en fait ma première question StackExchange (Bien que j'ai beaucoup utilisé le site) et j'espère que tout est en ordre! N'hésitez pas à ajouter toutes les modifications nécessaires et à poser toutes les questions que je pourrais avoir besoin de clarifier. En outre, j'ai un exemple de programme sur GitHub qui génère mon document PDF à https://github.com/chris271/UAPDFBox .

Edit 1: Lien direct vers Document PDF de sortie

*EDIT 2 : Après avoir utilisé certaines des API PDFBox de niveau inférieur et visualisé des flux de données brutes pour des PDF entièrement conformes avec PDFDebugger, j'ai pu générer unPDF avec une structure de contenu presque identique par rapport àla structure de contenu du PDF conforme ... Cependant, les mêmes erreurs apparaissent que les objets texte ne sont pas étiquetés et je ne peux vraiment pas décider où aller à partir d'ici... Toute orientation serait grandement appréciée!

Modifier 3: Comparaison de contenu PDF brut côte à côte.

Edit 4: Structure interne de la PDF

PDF généré

Et le PDF conforme

compatible PDF

Edit 5: J'ai réussi à corriger les erreurs PAC 2 pour les objets path/text balisés grâce en partie aux suggestions de Tilman Hausherr! J'ajouterai une réponse si je parviens à résoudre les problèmes concernant "les widgets d'annotation ne sont pas imbriqués dans les éléments de structure de formulaire".

Author: GurpusMaximus, 2018-04-06

1 answers

Après avoir parcouru une grande quantité de PDF Spec et de nombreux exemples PDFBox, j'ai pu résoudre tous les problèmes signalés par PAC 2. Plusieurs étapes ont été nécessaires pour créer le PDF vérifié (avec une structure de table complexe) et le code source complet est disponible ici sur github. Je vais essayer de faire un aperçu des principales parties du code ci-dessous. (Certains appels de méthode ne seront pas expliqués ici!)

Étape 1 (Configuration des métadonnées)

Divers informations de configuration comme le titre et la langue du document

//Setup new document
    pdf = new PDDocument();
    acroForm = new PDAcroForm(pdf);
    pdf.getDocumentInformation().setTitle(title);
    //Adjust other document metadata
    PDDocumentCatalog documentCatalog = pdf.getDocumentCatalog();
    documentCatalog.setLanguage("English");
    documentCatalog.setViewerPreferences(new PDViewerPreferences(new COSDictionary()));
    documentCatalog.getViewerPreferences().setDisplayDocTitle(true);
    documentCatalog.setAcroForm(acroForm);
    documentCatalog.setStructureTreeRoot(structureTreeRoot);
    PDMarkInfo markInfo = new PDMarkInfo();
    markInfo.setMarked(true);
    documentCatalog.setMarkInfo(markInfo);

Intégrez toutes les polices directement dans les ressources.

//Set AcroForm Appearance Characteristics
    PDResources resources = new PDResources();
    defaultFont = PDType0Font.load(pdf,
            new PDTrueTypeFont(PDType1Font.HELVETICA.getCOSObject()).getTrueTypeFont(), true);
    resources.put(COSName.getPDFName("Helv"), defaultFont);
    acroForm.setNeedAppearances(true);
    acroForm.setXFA(null);
    acroForm.setDefaultResources(resources);
    acroForm.setDefaultAppearance(DEFAULT_APPEARANCE);

Ajouter des métadonnées XMP pour les spécifications PDF/UA.

//Add UA XMP metadata based on specs at https://taggedpdf.com/508-pdf-help-center/pdfua-identifier-missing/
    XMPMetadata xmp = XMPMetadata.createXMPMetadata();
    xmp.createAndAddDublinCoreSchema();
    xmp.getDublinCoreSchema().setTitle(title);
    xmp.getDublinCoreSchema().setDescription(title);
    xmp.createAndAddPDFAExtensionSchemaWithDefaultNS();
    xmp.getPDFExtensionSchema().addNamespace("http://www.aiim.org/pdfa/ns/schema#", "pdfaSchema");
    xmp.getPDFExtensionSchema().addNamespace("http://www.aiim.org/pdfa/ns/property#", "pdfaProperty");
    xmp.getPDFExtensionSchema().addNamespace("http://www.aiim.org/pdfua/ns/id/", "pdfuaid");
    XMPSchema uaSchema = new XMPSchema(XMPMetadata.createXMPMetadata(),
            "pdfaSchema", "pdfaSchema", "pdfaSchema");
    uaSchema.setTextPropertyValue("schema", "PDF/UA Universal Accessibility Schema");
    uaSchema.setTextPropertyValue("namespaceURI", "http://www.aiim.org/pdfua/ns/id/");
    uaSchema.setTextPropertyValue("prefix", "pdfuaid");
    XMPSchema uaProp = new XMPSchema(XMPMetadata.createXMPMetadata(),
            "pdfaProperty", "pdfaProperty", "pdfaProperty");
    uaProp.setTextPropertyValue("name", "part");
    uaProp.setTextPropertyValue("valueType", "Integer");
    uaProp.setTextPropertyValue("category", "internal");
    uaProp.setTextPropertyValue("description", "Indicates, which part of ISO 14289 standard is followed");
    uaSchema.addUnqualifiedSequenceValue("property", uaProp);
    xmp.getPDFExtensionSchema().addBagValue("schemas", uaSchema);
    xmp.getPDFExtensionSchema().setPrefix("pdfuaid");
    xmp.getPDFExtensionSchema().setTextPropertyValue("part", "1");
    XmpSerializer serializer = new XmpSerializer();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    serializer.serialize(xmp, baos, true);
    PDMetadata metadata = new PDMetadata(pdf);
    metadata.importXMPMetadata(baos.toByteArray());
    pdf.getDocumentCatalog().setMetadata(metadata);

Étape 2 (Configuration de la structure des balises de document)

Vous devrez ajouter l'élément de structure racine et tous les éléments de structure nécessaires en tant qu'enfants à l'élément racine.

//Adds a DOCUMENT structure element as the structure tree root.
void addRoot() {
    PDStructureElement root = new PDStructureElement(StandardStructureTypes.DOCUMENT, null);
    root.setAlternateDescription("The document's root structure element.");
    root.setTitle("PDF Document");
    pdf.getDocumentCatalog().getStructureTreeRoot().appendKid(root);
    currentElem = root;
    rootElem = root;
}

Chaque élément de contenu marqué (texte et graphiques d'arrière-plan) devra avoir un MCID et une balise associée pour référence dans l'arbre parent qui sera expliqué à l'étape 3.

//Assign an id for the next marked content element.
private void setNextMarkedContentDictionary(String tag) {
    currentMarkedContentDictionary = new COSDictionary();
    currentMarkedContentDictionary.setName("Tag", tag);
    currentMarkedContentDictionary.setInt(COSName.MCID, currentMCID);
    currentMCID++;
}

Les artefacts (graphiques d'arrière-plan) ne seront pas détectés par le lecteur d'écran. Le texte doit être détectable, donc un élément de structure P est utilisé ici lors de l'ajout de texte.

            //Set up the next marked content element with an MCID and create the containing TD structure element.
            PDPageContentStream contents = new PDPageContentStream(
                    pdf, pages.get(pageIndex), PDPageContentStream.AppendMode.APPEND, false);
            currentElem = addContentToParent(null, StandardStructureTypes.TD, pages.get(pageIndex), currentRow);

            //Make the actual cell rectangle and set as artifact to avoid detection.
            setNextMarkedContentDictionary(COSName.ARTIFACT.getName());
            contents.beginMarkedContent(COSName.ARTIFACT, PDPropertyList.create(currentMarkedContentDictionary));

            //Draws the cell itself with the given colors and location.
            drawDataCell(table.getCell(i, j).getCellColor(), table.getCell(i, j).getBorderColor(),
                    x + table.getRows().get(i).getCellPosition(j),
                    y + table.getRowPosition(i),
                    table.getCell(i, j).getWidth(), table.getRows().get(i).getHeight(), contents);
            contents.endMarkedContent();
            currentElem = addContentToParent(COSName.ARTIFACT, StandardStructureTypes.P, pages.get(pageIndex), currentElem);
            contents.close();
            //Draw the cell's text as a P structure element
            contents = new PDPageContentStream(
                    pdf, pages.get(pageIndex), PDPageContentStream.AppendMode.APPEND, false);
            setNextMarkedContentDictionary(COSName.P.getName());
            contents.beginMarkedContent(COSName.P, PDPropertyList.create(currentMarkedContentDictionary));
            //... Code to draw actual text...//
            //End the marked content and append it's P structure element to the containing TD structure element.
            contents.endMarkedContent();
            addContentToParent(COSName.P, null, pages.get(pageIndex), currentElem);
            contents.close();

Les widgets d'annotation (objets de formulaire dans ce cas) devront être imbriqués dans les éléments de structure de formulaire.

//Add a radio button widget.
            if (!table.getCell(i, j).getRbVal().isEmpty()) {
                PDStructureElement fieldElem = new PDStructureElement(StandardStructureTypes.FORM, currentElem);
                radioWidgets.add(addRadioButton(
                        x + table.getRows().get(i).getCellPosition(j) -
                                radioWidgets.size() * 10 + table.getCell(i, j).getWidth() / 4,
                        y + table.getRowPosition(i),
                        table.getCell(i, j).getWidth() * 1.5f, 20,
                        radioValues, pageIndex, radioWidgets.size()));
                fieldElem.setPage(pages.get(pageIndex));
                COSArray kArray = new COSArray();
                kArray.add(COSInteger.get(currentMCID));
                fieldElem.getCOSObject().setItem(COSName.K, kArray);
                addWidgetContent(annotationRefs.get(annotationRefs.size() - 1), fieldElem, StandardStructureTypes.FORM, pageIndex);
            }

//Add a text field in the current cell.
            if (!table.getCell(i, j).getTextVal().isEmpty()) {
                PDStructureElement fieldElem = new PDStructureElement(StandardStructureTypes.FORM, currentElem);
                addTextField(x + table.getRows().get(i).getCellPosition(j),
                        y + table.getRowPosition(i),
                        table.getCell(i, j).getWidth(), table.getRows().get(i).getHeight(),
                        table.getCell(i, j).getTextVal(), pageIndex);
                fieldElem.setPage(pages.get(pageIndex));
                COSArray kArray = new COSArray();
                kArray.add(COSInteger.get(currentMCID));
                fieldElem.getCOSObject().setItem(COSName.K, kArray);
                addWidgetContent(annotationRefs.get(annotationRefs.size() - 1), fieldElem, StandardStructureTypes.FORM, pageIndex);
            }

Étape 3

Une fois que tous les éléments de contenu ont été écrits dans le flux de contenu et la balise la structure a été configurée, il est nécessaire de revenir en arrière et d'ajouter l'arbre parent à la racine de l'arbre de structure. Remarque: Certains appels de méthode (addWidgetContent () et addContentToParent ()) dans le code ci-dessus configurent les objets COSDictionary nécessaires.

//Adds the parent tree to root struct element to identify tagged content
void addParentTree() {
    COSDictionary dict = new COSDictionary();
    nums.add(numDictionaries);
    for (int i = 1; i < currentStructParent; i++) {
        nums.add(COSInteger.get(i));
        nums.add(annotDicts.get(i - 1));
    }
    dict.setItem(COSName.NUMS, nums);
    PDNumberTreeNode numberTreeNode = new PDNumberTreeNode(dict, dict.getClass());
    pdf.getDocumentCatalog().getStructureTreeRoot().setParentTreeNextKey(currentStructParent);
    pdf.getDocumentCatalog().getStructureTreeRoot().setParentTree(numberTreeNode);
}

Si toutes les annotations de widget et le contenu marqué ont été ajoutés correctement à l'arbre de structure et à l'arbre parent, vous devriez obtenir quelque chose comme ceci à partir de PAC 2 et PDFDebugger.

PDF vérifié

Débogueur

Merci à Tilman Hausherr de m'avoir pointé dans la bonne direction pour résoudre ce problème! Je vais très probablement apporter quelques modifications à cette réponse pour plus de clarté, comme recommandé par d'autres.

Modifier 1:

Si vous voulez avoir une structure de table comme celle que j'ai générée, vous devrez également ajouter un balisage de table correct pour vous conformer pleinement à la norme 508... Le 'Champ', 'ColSpan', 'RowSpan', ou "en-Têtes" attributs doivent être correctement ajouté à chaque cellule du tableau élément de structure similaire à ce ou ce. Le but principal de ce balisage est de permettre à un logiciel de lecture d'écran comme JAWS de lire le contenu du tableau de manière compréhensible. Ces attributs peuvent être ajoutés de la même manière que ci-dessous...

private void addTableCellMarkup(Cell cell, int pageIndex, PDStructureElement currentRow) {
    COSDictionary cellAttr = new COSDictionary();
    cellAttr.setName(COSName.O, "Table");
    if (cell.getCellMarkup().isHeader()) {
        currentElem = addContentToParent(null, StandardStructureTypes.TH, pages.get(pageIndex), currentRow);
        currentElem.getCOSObject().setString(COSName.ID, cell.getCellMarkup().getId());
        if (cell.getCellMarkup().getScope().length() > 0) {
            cellAttr.setName(COSName.getPDFName("Scope"), cell.getCellMarkup().getScope());
        }
        if (cell.getCellMarkup().getColspan() > 1) {
            cellAttr.setInt(COSName.getPDFName("ColSpan"), cell.getCellMarkup().getColspan());
        }
        if (cell.getCellMarkup().getRowSpan() > 1) {
            cellAttr.setInt(COSName.getPDFName("RowSpan"), cell.getCellMarkup().getRowSpan());
        }
    } else {
        currentElem = addContentToParent(null, StandardStructureTypes.TD, pages.get(pageIndex), currentRow);
    }
    if (cell.getCellMarkup().getHeaders().length > 0) {
        COSArray headerA = new COSArray();
        for (String s : cell.getCellMarkup().getHeaders()) {
            headerA.add(new COSString(s));
        }
        cellAttr.setItem(COSName.getPDFName("Headers"), headerA);
    }
    currentElem.getCOSObject().setItem(COSName.A, cellAttr);
}

Assurez-vous de faire quelque chose comme currentElem.setAlternateDescription(currentCell.getText()); sur chacun des éléments de structure avec du texte marqué contenu pour JAWS pour lire le texte.

Remarque: Chacun des champs (bouton radio et zone de texte) aura besoin d'un nom unique pour éviter de définir plusieurs valeurs de champ. GitHub a été mis à jour avec un exemple PDF plus complexe avec un balisage de table et des champs de formulaire améliorés!

 5
Author: GurpusMaximus, 2018-05-04 13:34:50