TagTreePointerTest.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.pdf;

import com.itextpdf.io.exceptions.ExceptionUtil;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.tagging.IStructureNode;
import com.itextpdf.kernel.pdf.tagging.PdfNamespace;
import com.itextpdf.kernel.pdf.tagging.PdfStructElem;
import com.itextpdf.kernel.pdf.tagging.PdfStructIdTree;
import com.itextpdf.kernel.pdf.tagging.PdfStructureAttributes;
import com.itextpdf.kernel.pdf.tagging.StandardNamespaces;
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
import com.itextpdf.kernel.pdf.tagutils.AccessibilityProperties;
import com.itextpdf.kernel.pdf.tagutils.DefaultAccessibilityProperties;
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
import com.itextpdf.kernel.pdf.tagutils.TagTreePointer;
import com.itextpdf.kernel.pdf.tagutils.WaitingTagsManager;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.xml.sax.SAXException;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

@Tag("IntegrationTest")
public class TagTreePointerTest extends ExtendedITextTest {

    public static final String sourceFolder = "./src/test/resources/com/itextpdf/kernel/pdf/TagTreePointerTest/";
    public static final String destinationFolder = TestUtil.getOutputPath() + "/kernel/pdf/TagTreePointerTest/";

    @BeforeAll
    public static void beforeClass() {
        createOrClearDestinationFolder(destinationFolder);
    }

    @AfterAll
    public static void afterClass() {
        CompareTool.cleanup(destinationFolder);
    }
    
    @Test
    public void tagTreePointerTest01() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest01.pdf")
                .setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();

        PdfPage page1 = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page1);

        PdfCanvas canvas = new PdfCanvas(page1);

        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.beginText()
              .setFontAndSize(standardFont, 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);

        tagPointer.addTag(StandardRoles.P).addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();
        canvas.setFontAndSize(standardFont, 30)
              .openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag();

        tagPointer.moveToParent().moveToParent();

        canvas.endText()
              .release();

        PdfPage page2 = document.addNewPage();
        tagPointer.setPageForTagging(page2);
        canvas = new PdfCanvas(page2);

        canvas.beginText()
              .setFontAndSize(PdfFontFactory.createFont(StandardFonts.HELVETICA), 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);

        tagPointer.addTag(StandardRoles.P).addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        tagPointer.moveToParent().addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag();

        canvas.endText()
              .release();
        page1.flush();
        page2.flush();

        document.close();

        compareResult("tagTreePointerTest01.pdf", "cmp_tagTreePointerTest01.pdf", "diff01_");
    }

    @Test
    public void tagTreePointerTest02() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest02.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();

        PdfPage page = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page);

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.beginText();
        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.setFontAndSize(standardFont, 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);

        PdfStructureAttributes attributes = new PdfStructureAttributes("random attributes");
        attributes.addTextAttribute("hello", "world");

        tagPointer.addTag(StandardRoles.P).addTag(StandardRoles.SPAN).getProperties()
                .setActualText("Actual text for span is: Hello World")
                .setLanguage("en-GB")
                .addAttributes(attributes);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        canvas.setFontAndSize(standardFont, 30)
              .openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag();

        canvas.endText()
              .release();
        page.flush();

        document.close();

        compareResult("tagTreePointerTest02.pdf", "cmp_tagTreePointerTest02.pdf", "diff02_");
    }

    @Test
    public void tagTreePointerTest03() throws Exception {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest03.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.moveToKid(StandardRoles.TABLE).moveToKid(2, StandardRoles.TR);
        TagTreePointer tagPointerCopy = new TagTreePointer(tagPointer);
        tagPointer.removeTag();

        // tagPointerCopy now points at removed tag

        String exceptionMessage = null;
        try {
            tagPointerCopy.addTag(StandardRoles.SPAN);
        } catch (PdfException e) {
            exceptionMessage = e.getMessage();
        }
        assertEquals(KernelExceptionMessageConstant.TAG_TREE_POINTER_IS_IN_INVALID_STATE_IT_POINTS_AT_REMOVED_ELEMENT_USE_MOVE_TO_ROOT, exceptionMessage);

        tagPointerCopy.moveToRoot().moveToKid(StandardRoles.TABLE);

        tagPointerCopy.moveToKid(StandardRoles.TR);
        TagTreePointer tagPointerCopyCopy = new TagTreePointer(tagPointerCopy);
        tagPointerCopy.flushTag();

        // tagPointerCopyCopy now points at flushed tag

        try {
            tagPointerCopyCopy.addTag(StandardRoles.SPAN);
        } catch (PdfException e) {
            exceptionMessage = e.getMessage();
        }
        assertEquals(KernelExceptionMessageConstant.TAG_TREE_POINTER_IS_IN_INVALID_STATE_IT_POINTS_AT_FLUSHED_ELEMENT_USE_MOVE_TO_ROOT, exceptionMessage);

        try {
            tagPointerCopy.moveToKid(0);
        } catch (PdfException e) {
            exceptionMessage = e.getMessage();
        }
        assertEquals(KernelExceptionMessageConstant.CANNOT_MOVE_TO_FLUSHED_KID, exceptionMessage);

        document.close();
    }

    @Test
    public void tagTreePointerTest04() throws Exception {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest04.pdf").setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.moveToKid(StandardRoles.TABLE).moveToKid(2, StandardRoles.TR);
        tagPointer.removeTag();

        tagPointer.moveToKid(StandardRoles.TR).moveToKid(StandardRoles.TD)
                .moveToKid(StandardRoles.P).moveToKid(StandardRoles.SPAN);
        tagPointer.removeTag()
                .removeTag();

        document.close();

        compareResult("tagTreePointerTest04.pdf", "cmp_tagTreePointerTest04.pdf", "diff04_");
    }

    @Test
    public void tagTreePointerTest05() throws Exception {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest05.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer tagPointer1 = new TagTreePointer(document);
        tagPointer1.moveToKid(2, StandardRoles.TR);

        TagTreePointer tagPointer2 = new TagTreePointer(document);
        tagPointer2.moveToKid(0, StandardRoles.TR);
        tagPointer1.relocateKid(0, tagPointer2);

        tagPointer1 = new TagTreePointer(document).moveToKid(5, StandardRoles.TR).moveToKid(2, StandardRoles.TD).moveToKid(StandardRoles.P).moveToKid(StandardRoles.SPAN);
        tagPointer2.moveToKid(StandardRoles.TD).moveToKid(StandardRoles.P).moveToKid(StandardRoles.SPAN);
        tagPointer2.setNextNewKidIndex(3);
        tagPointer1.relocateKid(4, tagPointer2);

        document.close();

        compareResult("tagTreePointerTest05.pdf", "cmp_tagTreePointerTest05.pdf", "diff05_");
    }

    @Test
    public void tagTreePointerTest06() throws Exception {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest06.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setRole(StandardRoles.PART);
        assertEquals(tagPointer.getRole(), "Part");
        tagPointer.moveToKid(StandardRoles.TABLE).getProperties().setLanguage("en-US");
        tagPointer.moveToKid(StandardRoles.P);
        String actualText1 = "Some looong latin text";
        tagPointer.getProperties().setActualText(actualText1);

        WaitingTagsManager waitingTagsManager = document.getTagStructureContext().getWaitingTagsManager();
//        assertNull(waitingTagsManager.getAssociatedObject(tagPointer));
        Object associatedObj = new Object();
        waitingTagsManager.assignWaitingState(tagPointer, associatedObj);

        tagPointer.moveToRoot().moveToKid(StandardRoles.TABLE).moveToKid(1, StandardRoles.TR).getProperties().setActualText("More latin text");

        waitingTagsManager.tryMovePointerToWaitingTag(tagPointer, associatedObj);
        tagPointer.setRole(StandardRoles.DIV);
        tagPointer.getProperties().setLanguage("en-Us");
        assertEquals(tagPointer.getProperties().getActualText(), actualText1);

        document.close();

        compareResult("tagTreePointerTest06.pdf", "cmp_tagTreePointerTest06.pdf", "diff06_");
    }

    @Test
    public void tagTreePointerTest07() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest07.pdf").setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();

        PdfPage page = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document).setPageForTagging(page);
        tagPointer.addTag(StandardRoles.SPAN);

        PdfCanvas canvas = new PdfCanvas(page);

        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.beginText()
                .setFontAndSize(standardFont, 24)
                .setTextMatrix(1, 0, 0, 1, 32, 512);

        canvas.openTag(tagPointer.getTagReference())
                .showText("Hello ")
                .closeTag();

        canvas.openTag(tagPointer.getTagReference().addProperty(PdfName.E, new PdfString("Big Mister")))
                .showText(" BMr. ")
                .closeTag();

        canvas.setFontAndSize(standardFont, 30)
                .openTag(tagPointer.getTagReference())
                .showText("World")
                .closeTag();

        canvas.endText();

        document.close();

        compareResult("tagTreePointerTest07.pdf", "cmp_tagTreePointerTest07.pdf", "diff07_");
    }

    @Test
    public void tagTreePointerTest08() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagTreePointerTest08.pdf").setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(new PdfReader(sourceFolder + "taggedDocument2.pdf"), writer);

        TagTreePointer pointer = new TagTreePointer(document);
        AccessibilityProperties properties = pointer.moveToKid(StandardRoles.DIV).getProperties();
        String language = properties.getLanguage();
        Assertions.assertEquals("en-Us", language);
        properties.setLanguage("EN-GB");

        pointer.moveToRoot().moveToKid(2, StandardRoles.P).getProperties().setRole(StandardRoles.H6);
        String role = pointer.getProperties().getRole();
        Assertions.assertEquals("H6", role);
        document.close();

        compareResult("tagTreePointerTest08.pdf", "cmp_tagTreePointerTest08.pdf", "diff08_");
    }

    @Test
    public void changeExistedBackedAccessibilityPropertiesTest() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "changeExistedBackedAccessibilityPropertiesTest.pdf",
                new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)).setCompressionLevel(
                CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(new PdfReader(sourceFolder + "taggedDocument2.pdf"), writer);

        TagTreePointer pointer = new TagTreePointer(document);
        AccessibilityProperties properties = pointer.moveToKid(StandardRoles.DIV).getProperties();
        String altDescription = "Alternate Description";
        properties.setAlternateDescription(altDescription);
        Assertions.assertEquals(altDescription, properties.getAlternateDescription());
        String expansion = "expansion";
        properties.setExpansion(expansion);
        Assertions.assertEquals(expansion, properties.getExpansion());
        properties.setNamespace(new PdfNamespace(StandardNamespaces.PDF_2_0));
        Assertions.assertEquals(StandardNamespaces.PDF_2_0, properties.getNamespace().getNamespaceName());
        String phoneme = "phoneme";
        properties.setPhoneme(phoneme);
        Assertions.assertEquals(phoneme, properties.getPhoneme());
        String phoneticAlphabet = "Phonetic Alphabet";
        properties.setPhoneticAlphabet(phoneticAlphabet);
        Assertions.assertEquals(phoneticAlphabet, properties.getPhoneticAlphabet());

        document.close();

        compareResult("changeExistedBackedAccessibilityPropertiesTest.pdf",
                "cmp_changeExistedBackedAccessibilityPropertiesTest.pdf", "diffBackProp01_");
    }

    @Test
    public void removeExistedBackedAccessibilityPropertiesTest() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "removeExistedBackedAccessibilityPropertiesTest.pdf",
                new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)).setCompressionLevel(
                CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(new PdfReader(sourceFolder + "taggedDocument2.pdf"), writer);

        TagTreePointer pointer = new TagTreePointer(document);
        AccessibilityProperties properties = pointer.moveToKid(StandardRoles.DIV).getProperties();
        Assertions.assertNotNull(properties.getAttributesList());
        Assertions.assertNotNull(properties.addAttributes(0, null));
        properties.clearAttributes();
        Assertions.assertTrue(properties.getAttributesList().isEmpty());
        properties.addRef(pointer);
        Assertions.assertFalse(properties.getRefsList().isEmpty());
        properties.clearRefs();
        Assertions.assertTrue(properties.getRefsList().isEmpty());

        document.close();

        compareResult("removeExistedBackedAccessibilityPropertiesTest.pdf",
                "cmp_removeExistedBackedAccessibilityPropertiesTest.pdf", "diffBackProp02_");
    }

    @Test
    public void setDefaultAccessibilityPropertiesTest() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "setDefaultAccessibilityPropertiesTest.pdf",
                new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)).setCompressionLevel(
                CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(new PdfReader(sourceFolder + "taggedDocument2.pdf"), writer);

        TagTreePointer pointer = new TagTreePointer(document);
        AccessibilityProperties properties = new DefaultAccessibilityProperties(StandardRoles.DIV);
        properties.setRole(StandardRoles.H6);
        Assertions.assertEquals(StandardRoles.H6, properties.getRole());
        String actualText = "Test text";
        properties.setActualText(actualText);
        Assertions.assertEquals(actualText, properties.getActualText());
        String language = "EN-GB";
        properties.setLanguage(language);
        Assertions.assertEquals(language, properties.getLanguage());
        String alternateDescription = "Alternate Description";
        properties.setAlternateDescription(alternateDescription);
        Assertions.assertEquals(alternateDescription, properties.getAlternateDescription());
        String expansion = "expansion";
        properties.setExpansion(expansion);
        Assertions.assertEquals(expansion, properties.getExpansion());
        properties.setNamespace(new PdfNamespace(StandardNamespaces.PDF_2_0));
        Assertions.assertEquals(StandardNamespaces.PDF_2_0, properties.getNamespace().getNamespaceName());
        String phoneme = "phoneme";
        properties.setPhoneme(phoneme);
        Assertions.assertEquals(phoneme, properties.getPhoneme());
        String phoneticAlphabet = "phoneticAlphabet";
        properties.setPhoneticAlphabet(phoneticAlphabet);
        Assertions.assertEquals(phoneticAlphabet, properties.getPhoneticAlphabet());
        properties.addRef(pointer);
        Assertions.assertFalse(properties.getRefsList().isEmpty());
        pointer.addTag(properties);

        document.close();

        compareResult("setDefaultAccessibilityPropertiesTest.pdf", "cmp_setDefaultAccessibilityPropertiesTest.pdf",
                "diffDefaultProp01_");
    }

    @Test
    public void removeDefaultAccessibilityPropertiesTest() throws Exception {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "removeDefaultAccessibilityPropertiesTest.pdf",
                new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)).setCompressionLevel(
                CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(new PdfReader(sourceFolder + "taggedDocument2.pdf"), writer);

        TagTreePointer pointer = new TagTreePointer(document);
        AccessibilityProperties properties = new DefaultAccessibilityProperties(StandardRoles.DIV);
        PdfStructureAttributes testAttr = new PdfStructureAttributes("test");
        testAttr.addIntAttribute("N", 4);
        properties.addAttributes(testAttr);
        properties.addAttributes(1, testAttr);
        properties.getAttributesList();
        properties.clearAttributes();
        Assertions.assertTrue(properties.getAttributesList().isEmpty());
        properties.addRef(pointer);
        Assertions.assertFalse(properties.getRefsList().isEmpty());
        properties.clearRefs();
        Assertions.assertTrue(properties.getRefsList().isEmpty());
        pointer.addTag(properties);

        document.close();

        compareResult("removeDefaultAccessibilityPropertiesTest.pdf",
                "cmp_removeDefaultAccessibilityPropertiesTest.pdf", "diffDefaultProp02_");
    }

    @Test
    public void tagStructureFlushingTest01() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureFlushingTest01.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.moveToKid(StandardRoles.TABLE).moveToKid(2, StandardRoles.TR).flushTag();
        tagPointer.moveToKid(3, StandardRoles.TR).moveToKid(StandardRoles.TD).flushTag();
        tagPointer.moveToParent().flushTag();

        String exceptionMessage = null;
        try {
            tagPointer.flushTag();
        } catch(PdfException e) {
            exceptionMessage = e.getMessage();
        }

        document.close();

        assertEquals(KernelExceptionMessageConstant.CANNOT_FLUSH_DOCUMENT_ROOT_TAG_BEFORE_DOCUMENT_IS_CLOSED, exceptionMessage);
        compareResult("tagStructureFlushingTest01.pdf", "taggedDocument.pdf", "diffFlushing01_");
    }

    @Test
    public void tagStructureFlushingTest02() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureFlushingTest02.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        TagStructureContext tagStructure = document.getTagStructureContext();
        tagStructure.flushPageTags(document.getPage(1));

        List<IStructureNode> kids = document.getStructTreeRoot().getKids();
        assertTrue(!((PdfStructElem)kids.get(0)).getPdfObject().isFlushed());
        assertTrue(!((PdfStructElem)kids.get(0).getKids().get(0)).getPdfObject().isFlushed());
        PdfArray rowsTags = (PdfArray) ((PdfStructElem) kids.get(0).getKids().get(0)).getK();
        assertTrue(rowsTags.get(0).isFlushed());

        document.close();

        compareResult("tagStructureFlushingTest02.pdf", "taggedDocument.pdf", "diffFlushing02_");
    }

    @Test
    public void tagStructureFlushingTest03() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureFlushingTest03.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        document.getPage(2).flush();
        document.getPage(1).flush();

        PdfArray kids = document.getStructTreeRoot().getKidsObject();
        assertFalse(kids.get(0).isFlushed());
        assertTrue(kids.getAsDictionary(0).getAsDictionary(PdfName.K).isFlushed());

        document.close();

        compareResult("tagStructureFlushingTest03.pdf", "taggedDocument.pdf", "diffFlushing03_");
    }

    @Test
    public void tagStructureFlushingTest04() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureFlushingTest04.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.moveToKid(StandardRoles.TABLE).moveToKid(2, StandardRoles.TR).flushTag();
        // intended redundant call to flush page tags separately from page. Page tags are flushed when the page is flushed.
        document.getTagStructureContext().flushPageTags(document.getPage(1));
        document.getPage(1).flush();
        tagPointer.moveToKid(5).flushTag();
        document.getPage(2).flush();

        PdfArray kids = document.getStructTreeRoot().getKidsObject();
        assertFalse(kids.get(0).isFlushed());
        assertTrue(kids.getAsDictionary(0).getAsDictionary(PdfName.K).isFlushed());

        document.close();

        compareResult("tagStructureFlushingTest04.pdf", "taggedDocument.pdf", "diffFlushing04_");
    }

    @Test
    public void tagStructureFlushingTest05() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureFlushingTest05.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();

        PdfPage page1 = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page1);

        PdfCanvas canvas = new PdfCanvas(page1);

        tagPointer.addTag(StandardRoles.DIV);

        tagPointer.addTag(StandardRoles.P);
        WaitingTagsManager waitingTagsManager = tagPointer.getContext().getWaitingTagsManager();
        Object pWaitingTagObj = new Object();
        waitingTagsManager.assignWaitingState(tagPointer, pWaitingTagObj);

        canvas.beginText();
        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.setFontAndSize(standardFont, 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);

        tagPointer.addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        canvas.setFontAndSize(standardFont, 30)
              .openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag();

        tagPointer.moveToParent().moveToParent();

        // Flushing /Div tag and it's children. /P tag shall not be flushed, as it is has connected paragraphElement
        // object. On removing connection between paragraphElement and /P tag, /P tag shall be flushed.
        // When tag is flushed, tagPointer begins to point to tag's parent. If parent is also flushed - to the root.
        tagPointer.flushTag();

        waitingTagsManager.tryMovePointerToWaitingTag(tagPointer, pWaitingTagObj);
        tagPointer.addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        canvas.setFontAndSize(standardFont, 30)
              .openTag(tagPointer.getTagReference())
              .showText("again")
              .closeTag();

        waitingTagsManager.removeWaitingState(pWaitingTagObj);
        tagPointer.moveToRoot();

        canvas.endText()
              .release();

        PdfPage page2 = document.addNewPage();
        tagPointer.setPageForTagging(page2);
        canvas = new PdfCanvas(page2);

        tagPointer.addTag(StandardRoles.P);
        canvas.beginText()
              .setFontAndSize(PdfFontFactory.createFont(StandardFonts.HELVETICA), 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);
        tagPointer.addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        tagPointer.moveToParent().addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag();

        canvas.endText()
              .release();
        page1.flush();
        page2.flush();

        document.close();

        compareResult("tagStructureFlushingTest05.pdf", "cmp_tagStructureFlushingTest05.pdf", "diffFlushing05_");
    }

    @Test
    public void tagStructureFlushingTest06() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureFlushingTest06.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();

        PdfPage page1 = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page1);

        PdfCanvas canvas = new PdfCanvas(page1);

        tagPointer.addTag(StandardRoles.DIV);

        tagPointer.addTag(StandardRoles.P);

        canvas.beginText();
        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.setFontAndSize(standardFont, 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);

        tagPointer.addTag(StandardRoles.SPAN);
        WaitingTagsManager waitingTagsManager = document.getTagStructureContext().getWaitingTagsManager();
        Object associatedObj = new Object();
        waitingTagsManager.assignWaitingState(tagPointer, associatedObj);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        canvas.setFontAndSize(standardFont, 30)
              .openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag();

        canvas.endText()
              .release();

        page1.flush();

        tagPointer.relocateKid(0,
                new TagTreePointer(document)
                        .moveToKid(StandardRoles.DIV)
                        .setNextNewKidIndex(0)
                        .addTag(StandardRoles.P)
        );
        tagPointer.removeTag();

        waitingTagsManager.removeWaitingState(associatedObj);
        document.getTagStructureContext().flushPageTags(page1);
        document.getStructTreeRoot().createParentTreeEntryForPage(page1);

        document.close();

        compareResult("tagStructureFlushingTest06.pdf", "cmp_tagStructureFlushingTest06.pdf", "diffFlushing06_");
    }

    @Test
    public void tagStructureRemovingTest01() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureRemovingTest01.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);
        document.removePage(1);
        document.close();

        compareResult("tagStructureRemovingTest01.pdf", "cmp_tagStructureRemovingTest01.pdf", "diffRemoving01_");
    }

    @Test
    public void tagStructureRemovingTest02() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocument.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureRemovingTest02.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);

        PdfPage firstPage = document.getPage(1);
        PdfPage secondPage = document.getPage(2);
        document.removePage(firstPage);
        document.removePage(secondPage);

        PdfPage page = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page);

        PdfCanvas canvas = new PdfCanvas(page);

        tagPointer.addTag(StandardRoles.P);
        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.beginText()
              .setFontAndSize(standardFont, 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);

        tagPointer.addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        canvas.setFontAndSize(standardFont, 30)
              .openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag()
              .endText();

        document.close();

        compareResult("tagStructureRemovingTest02.pdf", "cmp_tagStructureRemovingTest02.pdf", "diffRemoving02_");
    }

    @Test
    public void tagStructureRemovingTest03() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureRemovingTest03.pdf");
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();

        PdfPage page = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page);

        PdfCanvas canvas = new PdfCanvas(page);

        tagPointer.addTag(StandardRoles.P);

        WaitingTagsManager waitingTagsManager = tagPointer.getContext().getWaitingTagsManager();
        Object pWaitingTagObj = new Object();
        waitingTagsManager.assignWaitingState(tagPointer, pWaitingTagObj);

        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.beginText()
              .setFontAndSize(standardFont, 24)
              .setTextMatrix(1, 0, 0, 1, 32, 512);

        tagPointer.addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
              .showText("Hello ")
              .closeTag();

        canvas.setFontAndSize(standardFont, 30)
              .openTag(tagPointer.getTagReference())
              .showText("World")
              .closeTag()
              .endText();

        tagPointer.moveToParent().moveToParent();

        document.removePage(1);

        PdfPage newPage = document.addNewPage();
        canvas = new PdfCanvas(newPage);
        tagPointer.setPageForTagging(newPage);

        waitingTagsManager.tryMovePointerToWaitingTag(tagPointer, pWaitingTagObj);
        tagPointer.addTag(StandardRoles.SPAN);

        canvas.openTag(tagPointer.getTagReference())
                .beginText()
                .setFontAndSize(standardFont, 24)
                .setTextMatrix(1, 0, 0, 1, 32, 512)
                .showText("Hello.")
                .endText()
                .closeTag();

        document.close();

        compareResult("tagStructureRemovingTest03.pdf", "cmp_tagStructureRemovingTest03.pdf", "diffRemoving03_");
    }

    @Test
    public void tagStructureRemovingTest04() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocumentWithAnnots.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "tagStructureRemovingTest04.pdf").setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(reader, writer);
        document.removePage(1);
        document.close();

        compareResult("tagStructureRemovingTest04.pdf", "cmp_tagStructureRemovingTest04.pdf", "diffRemoving04_");
    }

    @Test
    public void structureElementWithIdTest() throws Exception {
        String outfName = "structureElementWithIdTest.pdf";
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + outfName).
                setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        compareResult(outfName, "cmp_" + outfName, "diff01_");
    }

    @Test
    public void structureElementWithIdFromPropsTest() throws IOException {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos1);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        PdfPage page1 = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page1);

        PdfCanvas canvas = new PdfCanvas(page1);

        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.beginText()
                .setFontAndSize(standardFont, 24)
                .setTextMatrix(1, 0, 0, 1, 32, 512);

        // create a tag with an ID, some attributes and other properties
        DefaultAccessibilityProperties spanProps = new DefaultAccessibilityProperties(StandardRoles.SPAN);
        spanProps.setStructureElementIdString("hello-element");
        PdfStructureAttributes attrs = new PdfStructureAttributes("Layout");
        attrs.addEnumAttribute("Placement", "Inline");
        spanProps.addAttributes(attrs);
        spanProps.setActualText("Hello!");
        spanProps.setAlternateDescription("This is a piece of sample text");

        tagPointer.addTag(StandardRoles.P).addTag(spanProps);
        canvas.openTag(tagPointer.getTagReference())
                .showText("Hello!")
                .closeTag();
        tagPointer.moveToParent();

        page1.flush();
        document.close();

        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos1.toByteArray()));
                PdfDocument documentToModify = new PdfDocument(r)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrHello = ctx
                    .getTagPointerById("hello-element".getBytes(StandardCharsets.UTF_8));
            PdfStructureAttributes layoutAttrs = ptrHello.getProperties().getAttributesList().get(0);
            assertEquals("Inline", layoutAttrs.getAttributeAsEnum("Placement"));
        }
    }

    @Test
    public void retrieveStructureElementsByIdTest() throws Exception {
        String infName = "cmp_structureElementWithIdTest.pdf";
        // check that we can retrieve the IDs in the output
        PdfReader r = new PdfReader(sourceFolder + infName);
        PdfDocument readPdfDoc = new PdfDocument(r);
        TagStructureContext ctx = readPdfDoc.getTagStructureContext();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        TagTreePointer ptrHello = ctx.getTagPointerByIdString("hello-element");
        assertArrayEquals(ptrHello.getProperties().getStructureElementId(), helloId);
    }

    @Test
    public void structureElementWithoutIdTest() throws Exception {
        String infName = "cmp_structureElementWithIdTest.pdf";
        PdfReader r = new PdfReader(sourceFolder + infName);
        PdfDocument readPdfDoc = new PdfDocument(r);
        TagStructureContext ctx = readPdfDoc.getTagStructureContext();

        TagTreePointer ptrHello = ctx.getTagPointerByIdString("hello-element");
        // the parent is a P without ID -> we should get null
        ptrHello.moveToParent();
        assertNull(ptrHello.getProperties().getStructureElementId());
    }

    @Test
    public void disambiguateStructureElementsByIdTest() throws Exception {
        String infName = "cmp_structureElementWithIdTest.pdf";
        PdfReader r = new PdfReader(sourceFolder + infName);
        PdfDocument readPdfDoc = new PdfDocument(r);
        TagStructureContext ctx = readPdfDoc.getTagStructureContext();

        TagTreePointer ptrHello = ctx.getTagPointerByIdString("hello-element");
        TagTreePointer ptrWorld = ctx.getTagPointerByIdString("world-element");
        assertFalse(ptrHello.isPointingToSameTag(ptrWorld));
    }

    @Test
    public void structureElementWithNonexistentIdTest() throws Exception {
        String infName = "cmp_structureElementWithIdTest.pdf";
        PdfReader r = new PdfReader(sourceFolder + infName);
        PdfDocument readPdfDoc = new PdfDocument(r);
        TagStructureContext ctx = readPdfDoc.getTagStructureContext();
        TagTreePointer ptrNone = ctx.getTagPointerById("nonexistent-element".getBytes(StandardCharsets.UTF_8));
        assertNull(ptrNone);
    }

    @Test
    public void structureElementRemoveIdTest() throws Exception {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos1);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos1.toByteArray()));
            PdfWriter w = new PdfWriter(baos2);
            PdfDocument documentToModify = new PdfDocument(r, w)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrHello = ctx.getTagPointerById(helloId);
            // remove the ID
            ptrHello.getProperties().setStructureElementId(null);
            assertNull(ctx.getTagPointerById(helloId));
        }
    }

    @Test
    public void structureElementRemoveIdNoopTest() throws Exception {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos1);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos1.toByteArray()));
                PdfWriter w = new PdfWriter(baos2);
                PdfDocument pdfDoc = new PdfDocument(r, w)) {
            PdfStructIdTree tree = pdfDoc.getStructTreeRoot().getIdTree();
            tree.removeEntry(new PdfString("i-dont-exist"));
            assertFalse(tree.isModified());
        }
    }

    @Test
    public void structureElementRemoveIdStringTest() throws Exception {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos1);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos1.toByteArray()));
                PdfWriter w = new PdfWriter(baos2);
                PdfDocument documentToModify = new PdfDocument(r, w)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrHello = ctx.getTagPointerById(helloId);
            // remove the ID
            ptrHello.getProperties().setStructureElementIdString(null);
            assertNull(ctx.getTagPointerById(helloId));
        }
    }

    @Test
    public void structureElementRemoveIdPersist() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        addAndRemoveId(baos);
        // check if the changes were properly persisted
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos.toByteArray()));
                PdfDocument documentRead = new PdfDocument(r)) {

            TagStructureContext ctx = documentRead.getTagStructureContext();
            assertNull(ctx.getTagPointerByIdString("hello-element"));
        }
    }

    @Test
    public void structureElementRemoveIdPersistNoCollateralDamage() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        addAndRemoveId(baos);
        // check if the changes were properly persisted
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos.toByteArray()));
                PdfDocument documentRead = new PdfDocument(r)) {

            TagStructureContext ctx = documentRead.getTagStructureContext();
            byte[] id = "world-element".getBytes(StandardCharsets.UTF_8);
            byte[] retrieved = ctx.getTagPointerById(id).getProperties().getStructureElementId();
            assertArrayEquals(id, retrieved);
        }
    }

    @Test
    public void structureElementModifyIdTest() throws Exception {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos1);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        byte[] helloId2 = "hello2-element".getBytes(StandardCharsets.UTF_8);
        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos1.toByteArray()));
            PdfWriter w = new PdfWriter(baos2);
            PdfDocument documentToModify = new PdfDocument(r, w)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrHello = ctx.getTagPointerById(helloId);
            // modify the ID to a new value
            ptrHello.getProperties().setStructureElementId(helloId2);
            assertTrue(ptrHello.isPointingToSameTag(ctx.getTagPointerById(helloId2)));
        }
    }

    @Test
    public void structureElementModifyIdNoopTest() throws Exception {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos1);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos1.toByteArray()));
                PdfWriter w = new PdfWriter(baos2);
                PdfDocument documentToModify = new PdfDocument(r, w)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrHello = ctx.getTagPointerById(helloId);
            ptrHello.getProperties().setStructureElementId(helloId);
            assertFalse(documentToModify.getStructTreeRoot().getIdTree().isModified());
        }
    }

    @Test
    @LogMessages(messages = {@LogMessage(messageTemplate = IoLogMessageConstant.NAME_ALREADY_EXISTS_IN_THE_NAME_TREE, count = 1)})
    public void structureElementClobberIdWarning() throws Exception {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(baos1);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        byte[] worldId = "world-element".getBytes(StandardCharsets.UTF_8);
        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos1.toByteArray()));
                PdfWriter w = new PdfWriter(baos2);
                PdfDocument documentToModify = new PdfDocument(r, w)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrWorld = ctx.getTagPointerById(worldId);
            // modify the ID to a new value
            ptrWorld.getProperties().setStructureElementId(helloId);
            // this should clobber the old value and trigger a warning
            assertTrue(ptrWorld.isPointingToSameTag(ctx.getTagPointerById(helloId)));
        }
    }

    @Test
    public void structureElementModifyIdNewRegistered() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        addAndModifyStructElemId(baos);
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos.toByteArray()));
                PdfDocument documentRead = new PdfDocument(r)) {

            TagStructureContext ctx = documentRead.getTagStructureContext();
            byte[] id = "hello2-element".getBytes(StandardCharsets.UTF_8);
            byte[] retrieved = ctx.getTagPointerById(id).getProperties().getStructureElementId();
            assertArrayEquals(id, retrieved);
        }
    }

    @Test
    public void structureElementModifyIdOldRemoved() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        addAndModifyStructElemId(baos);
        // check if the changes were properly persisted
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos.toByteArray()));
                PdfDocument documentRead = new PdfDocument(r)) {

            TagStructureContext ctx = documentRead.getTagStructureContext();
            assertNull(ctx.getTagPointerByIdString("hello-element"));
        }
    }

    @Test
    public void structureElementModifyIdNoCollateralDamage() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        addAndModifyStructElemId(baos);
        // check if the changes were properly persisted
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(baos.toByteArray()));
                PdfDocument documentRead = new PdfDocument(r)) {

            TagStructureContext ctx = documentRead.getTagStructureContext();
            byte[] id = "world-element".getBytes(StandardCharsets.UTF_8);
            byte[] retrieved = ctx.getTagPointerById(id).getProperties().getStructureElementId();
            assertArrayEquals(id, retrieved);
        }
    }

    @Test
    public void accessibleAttributesInsertionTest01() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocumentWithAttributes.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "accessibleAttributesInsertionTest01.pdf");
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer pointer = new TagTreePointer(document);

        // 2 attributes
        AccessibilityProperties properties = pointer.moveToKid(0).getProperties();

        PdfStructureAttributes testAttr = new PdfStructureAttributes("test");
        testAttr.addIntAttribute("N", 4);
        properties.addAttributes(testAttr);

        testAttr = new PdfStructureAttributes("test");
        testAttr.addIntAttribute("N", 0);
        properties.addAttributes(0, testAttr);

        testAttr = new PdfStructureAttributes("test");
        testAttr.addIntAttribute("N", 5);
        properties.addAttributes(4, testAttr);

        testAttr = new PdfStructureAttributes("test");
        testAttr.addIntAttribute("N", 2);
        properties.addAttributes(2, testAttr);

        try {
            properties.addAttributes(10, testAttr);
            Assertions.fail();
        } catch (Exception e) {
            Assertions.assertTrue(ExceptionUtil.isOutOfRange(e));
        }

        document.close();

        compareResult("accessibleAttributesInsertionTest01.pdf", "cmp_accessibleAttributesInsertionTest01.pdf", "diffAttributes01_");
    }

    @Test
    public void accessibleAttributesInsertionTest02() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocumentWithAttributes.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "accessibleAttributesInsertionTest02.pdf");
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer pointer = new TagTreePointer(document);

        PdfStructureAttributes testAttrDict = new PdfStructureAttributes("test");

        // 1 attribute array
        pointer.moveToKid(1).getProperties().addAttributes(testAttrDict);

        pointer.moveToRoot();
        // 3 attributes
        pointer.moveToKid(2).getProperties().addAttributes(testAttrDict);

        pointer.moveToRoot();
        // 1 attribute dictionary
        pointer.moveToKid(0).moveToKid(StandardRoles.LI).moveToKid(StandardRoles.LBODY).getProperties().addAttributes(testAttrDict);

        // no attributes
        pointer.moveToKid(StandardRoles.P).moveToKid(StandardRoles.SPAN).getProperties().addAttributes(testAttrDict);

        document.close();

        compareResult("accessibleAttributesInsertionTest02.pdf", "cmp_accessibleAttributesInsertionTest02.pdf", "diffAttributes02_");
    }

    @Test
    public void accessibleAttributesInsertionTest03() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocumentWithAttributes.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "accessibleAttributesInsertionTest03.pdf");
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer pointer = new TagTreePointer(document);

        PdfDictionary testAttrDict = new PdfDictionary();

        // 1 attribute array
        pointer.moveToKid(1).getProperties().addAttributes(0, new PdfStructureAttributes(testAttrDict));

        pointer.moveToRoot();
        // 3 attributes
        pointer.moveToKid(2).getProperties().addAttributes(0, new PdfStructureAttributes(testAttrDict));

        pointer.moveToRoot();
        // 1 attribute dictionary
        pointer.moveToKid(0).moveToKid(StandardRoles.LI).moveToKid(StandardRoles.LBODY).getProperties().addAttributes(0, new PdfStructureAttributes(testAttrDict));

        // no attributes
        pointer.moveToKid(StandardRoles.P).moveToKid(StandardRoles.SPAN).getProperties().addAttributes(0, new PdfStructureAttributes(testAttrDict));

        document.close();

        compareResult("accessibleAttributesInsertionTest03.pdf", "cmp_accessibleAttributesInsertionTest03.pdf", "diffAttributes03_");
    }

    @Test
    public void accessibleAttributesInsertionTest04() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocumentWithAttributes.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "accessibleAttributesInsertionTest04.pdf");
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer pointer = new TagTreePointer(document);

        PdfDictionary testAttrDict = new PdfDictionary();

        // 1 attribute array
        pointer.moveToKid(1).getProperties().addAttributes(1, new PdfStructureAttributes(testAttrDict));

        pointer.moveToRoot();
        // 3 attributes
        pointer.moveToKid(2).getProperties().addAttributes(3, new PdfStructureAttributes(testAttrDict));

        pointer.moveToRoot();
        // 1 attribute dictionary
        pointer.moveToKid(0).moveToKid(StandardRoles.LI).moveToKid(StandardRoles.LBODY).getProperties().addAttributes(1, new PdfStructureAttributes(testAttrDict));

        document.close();

        compareResult("accessibleAttributesInsertionTest04.pdf", "cmp_accessibleAttributesInsertionTest04.pdf", "diffAttributes04_");
    }

    @Test
    public void accessibleAttributesInsertionTest05() throws IOException, InterruptedException, SAXException, ParserConfigurationException {
        PdfReader reader = new PdfReader(sourceFolder + "taggedDocumentWithAttributes.pdf");
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationFolder + "accessibleAttributesInsertionTest05.pdf");
        PdfDocument document = new PdfDocument(reader, writer);

        TagTreePointer pointer = new TagTreePointer(document);

        PdfDictionary testAttrDict = new PdfDictionary();

        try {
            // 1 attribute array
            pointer.moveToKid(1).getProperties().addAttributes(5, new PdfStructureAttributes(testAttrDict));
            Assertions.fail();
        } catch (Exception e) {
            Assertions.assertTrue(ExceptionUtil.isOutOfRange(e));
        }

        pointer.moveToRoot();
        try {
            // 3 attributes
            pointer.moveToKid(2).getProperties().addAttributes(5, new PdfStructureAttributes(testAttrDict));
            Assertions.fail();
        } catch (Exception e) {
            Assertions.assertTrue(ExceptionUtil.isOutOfRange(e));
        }

        pointer.moveToRoot();
        try {
            // 1 attribute dictionary
            pointer.moveToKid(0).moveToKid(StandardRoles.LI).moveToKid(StandardRoles.LBODY).getProperties().addAttributes(5, new PdfStructureAttributes(testAttrDict));
            Assertions.fail();
        } catch (Exception e) {
            Assertions.assertTrue(ExceptionUtil.isOutOfRange(e));
        }

        try {
            // no attributes
            pointer.moveToKid(StandardRoles.P).moveToKid(StandardRoles.SPAN).getProperties().addAttributes(5, new PdfStructureAttributes(testAttrDict));
            Assertions.fail();
        } catch (Exception e) {
            Assertions.assertTrue(ExceptionUtil.isOutOfRange(e));
        }

        document.close();

        compareResult("accessibleAttributesInsertionTest05.pdf", "cmp_accessibleAttributesInsertionTest05.pdf", "diffAttributes05_");
    }

    @Test
    public void defaultNamespaceTest() {
        try (PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream(),
                new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)))) {
            document.setTagged();
            document.getStructTreeRoot().addNamespace(new PdfNamespace(StandardNamespaces.PDF_2_0));
            TagTreePointer pointer = new TagTreePointer(document);
            AccessibilityProperties properties = pointer.addTag(StandardRoles.DIV).getProperties();
            PdfNamespace defaultNoDoc = PdfNamespace.getDefault(null);
            properties.setNamespace(defaultNoDoc);
            Assertions.assertEquals(StandardNamespaces.PDF_1_7, defaultNoDoc.getNamespaceName());
            PdfNamespace defaultNew = PdfNamespace.getDefault(document);
            properties.setNamespace(defaultNew);
            Assertions.assertEquals(StandardNamespaces.PDF_1_7, defaultNew.getNamespaceName());
            Assertions.assertNotEquals(defaultNoDoc.getPdfObject(), defaultNew.getPdfObject());
            PdfNamespace defaultSame = PdfNamespace.getDefault(document);
            properties.setNamespace(defaultSame);
            Assertions.assertEquals(StandardNamespaces.PDF_1_7, defaultSame.getNamespaceName());
            Assertions.assertEquals(defaultNew.getPdfObject(), defaultSame.getPdfObject());
        }
    }

    private void compareResult(String outFileName, String cmpFileName, String diffNamePrefix)
            throws IOException, InterruptedException, ParserConfigurationException, SAXException {
        CompareTool compareTool = new CompareTool();
        String outPdf = destinationFolder + outFileName;
        String cmpPdf = sourceFolder + cmpFileName;

        String contentDifferences = compareTool.compareByContent(outPdf,
                cmpPdf, destinationFolder, diffNamePrefix);
        String taggedStructureDifferences = compareTool.compareTagStructures(outPdf, cmpPdf);

        String errorMessage = "";
        errorMessage += taggedStructureDifferences == null ? "" : taggedStructureDifferences + "\n";
        errorMessage += contentDifferences == null ? "" : contentDifferences;
        if (!errorMessage.isEmpty()) {
            fail(errorMessage);
        }
    }

    private static void addContentWithIds(PdfDocument document) throws IOException {

        PdfPage page1 = document.addNewPage();
        TagTreePointer tagPointer = new TagTreePointer(document);
        tagPointer.setPageForTagging(page1);

        PdfCanvas canvas = new PdfCanvas(page1);

        PdfFont standardFont = PdfFontFactory.createFont(StandardFonts.COURIER);
        canvas.beginText()
                .setFontAndSize(standardFont, 24)
                .setTextMatrix(1, 0, 0, 1, 32, 512);

        DefaultAccessibilityProperties paraProps
                = new DefaultAccessibilityProperties(StandardRoles.P);
        tagPointer.addTag(paraProps).addTag(StandardRoles.SPAN);

        tagPointer.getProperties().setStructureElementIdString("hello-element");
        canvas.openTag(tagPointer.getTagReference())
                .showText("Hello ")
                .closeTag();
        tagPointer.moveToParent().addTag(StandardRoles.SPAN);

        tagPointer.getProperties().setStructureElementIdString("world-element");
        canvas.setFontAndSize(standardFont, 30)
                .openTag(tagPointer.getTagReference())
                .showText("World")
                .closeTag();

        tagPointer.moveToParent();

        canvas.endText().release();

        page1.flush();
    }

    private void addAndRemoveId(OutputStream baos) throws Exception {
        ByteArrayOutputStream preBaos = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(preBaos);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(preBaos.toByteArray()));
                PdfWriter w = new PdfWriter(baos);
                PdfDocument documentToModify = new PdfDocument(r, w)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrHello = ctx.getTagPointerById(helloId);
            // remove the ID
            ptrHello.getProperties().setStructureElementId(null);
        }
    }

    private void addAndModifyStructElemId(OutputStream baos) throws Exception {
        ByteArrayOutputStream preBaos = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(preBaos);
        PdfDocument document = new PdfDocument(writer);
        document.setTagged();
        addContentWithIds(document);
        document.close();

        byte[] helloId = "hello-element".getBytes(StandardCharsets.UTF_8);
        byte[] helloId2 = "hello2-element".getBytes(StandardCharsets.UTF_8);
        try(PdfReader r = new PdfReader(new ByteArrayInputStream(preBaos.toByteArray()));
                PdfWriter w = new PdfWriter(baos);
                PdfDocument documentToModify = new PdfDocument(r, w)) {

            TagStructureContext ctx = documentToModify.getTagStructureContext();
            TagTreePointer ptrHello = ctx.getTagPointerById(helloId);
            // modify the ID to a new value
            ptrHello.getProperties().setStructureElementId(helloId2);
        }
    }
}