[poppler] poppler/Catalog.cc poppler/Catalog.h poppler/Outline.cc poppler/Outline.h poppler/PDFDoc.cc qt5/tests qt6/tests

GitLab Mirror gitlab-mirror at kemper.freedesktop.org
Tue Jul 6 22:58:27 UTC 2021


 poppler/Catalog.cc                   |   37 ++
 poppler/Catalog.h                    |    2 
 poppler/Outline.cc                   |  469 +++++++++++++++++++++++++++++++++--
 poppler/Outline.h                    |   62 +++-
 poppler/PDFDoc.cc                    |    2 
 qt5/tests/CMakeLists.txt             |    1 
 qt5/tests/check_internal_outline.cpp |  436 ++++++++++++++++++++++++++++++++
 qt6/tests/CMakeLists.txt             |    1 
 qt6/tests/check_internal_outline.cpp |  436 ++++++++++++++++++++++++++++++++
 9 files changed, 1402 insertions(+), 44 deletions(-)

New commits:
commit fa494b780ab69ef04ba7447ab6d8fc3b46373e59
Author: RM <rm+git at arcsin.org>
Date:   Mon May 3 12:22:16 2021 -0400

    Modify internal API to allow addition and modification of outlines into a PDF. Tests in the qt5/qt6 directories.
    
    duplicate qt5 outline test in qt6 directory

diff --git a/poppler/Catalog.cc b/poppler/Catalog.cc
index 542f449a..aa1dbf48 100644
--- a/poppler/Catalog.cc
+++ b/poppler/Catalog.cc
@@ -941,6 +941,43 @@ unsigned int Catalog::getMarkInfo()
     return markInfo;
 }
 
+Object *Catalog::getCreateOutline()
+{
+
+    catalogLocker();
+    Object catDict = xref->getCatalog();
+
+    // If there is no Object in the outline variable,
+    // check if there is an Outline dict in the catalog
+    if (outline.isNone()) {
+        if (catDict.isDict()) {
+            Object outline_obj = catDict.dictLookup("Outlines");
+            if (outline_obj.isDict()) {
+                return &outline;
+            }
+        } else {
+            // catalog is not a dict, give up?
+            return &outline;
+        }
+    }
+
+    // If there is an Object in variable, make sure it's a dict
+    if (outline.isDict()) {
+        return &outline;
+    }
+
+    // setup an empty outline dict
+    outline = Object(new Dict(doc->getXRef()));
+    outline.dictSet("Type", Object(objName, "Outlines"));
+    outline.dictSet("Count", Object(0));
+
+    const Ref outlineRef = doc->getXRef()->addIndirectObject(&outline);
+    catDict.dictAdd("Outlines", Object(outlineRef));
+    xref->setModifiedObject(&catDict, { xref->getRootNum(), xref->getRootGen() });
+
+    return &outline;
+}
+
 Object *Catalog::getOutline()
 {
     catalogLocker();
diff --git a/poppler/Catalog.h b/poppler/Catalog.h
index 587ad1cf..254ea0d3 100644
--- a/poppler/Catalog.h
+++ b/poppler/Catalog.h
@@ -204,6 +204,8 @@ public:
     bool indexToLabel(int index, GooString *label);
 
     Object *getOutline();
+    // returns the existing outline or new one if it doesn't exist
+    Object *getCreateOutline();
 
     Object *getAcroForm() { return &acroForm; }
     void addFormToAcroForm(const Ref formRef);
diff --git a/poppler/Outline.cc b/poppler/Outline.cc
index d7814d6b..6e45b626 100644
--- a/poppler/Outline.cc
+++ b/poppler/Outline.cc
@@ -31,6 +31,7 @@
 
 #include "goo/gmem.h"
 #include "goo/GooString.h"
+#include "PDFDoc.h"
 #include "XRef.h"
 #include "Link.h"
 #include "PDFDocEncoding.h"
@@ -39,14 +40,17 @@
 
 //------------------------------------------------------------------------
 
-Outline::Outline(const Object *outlineObj, XRef *xref)
+Outline::Outline(Object *outlineObjA, XRef *xrefA, PDFDoc *docA)
 {
+    outlineObj = outlineObjA;
+    xref = xrefA;
+    doc = docA;
     items = nullptr;
     if (!outlineObj->isDict()) {
         return;
     }
     const Object &first = outlineObj->dictLookupNF("First");
-    items = OutlineItem::readItemList(nullptr, &first, xref);
+    items = OutlineItem::readItemList(nullptr, &first, xref, doc);
 }
 
 Outline::~Outline()
@@ -59,15 +63,349 @@ Outline::~Outline()
     }
 }
 
+static void insertChildHelper(const std::string &itemTitle, int destPageNum, unsigned int pos, Ref parentObjRef, PDFDoc *doc, XRef *xref, std::vector<OutlineItem *> &items)
+{
+    std::vector<OutlineItem *>::const_iterator it;
+    if (pos >= items.size()) {
+        it = items.end();
+    } else {
+        it = items.begin() + pos;
+    }
+
+    Array *a = new Array(xref);
+    Ref *pageRef = doc->getCatalog()->getPageRef(destPageNum);
+    if (pageRef != nullptr) {
+        a->add(Object(*pageRef));
+    } else {
+        // if the page obj doesn't exist put the page number
+        // PDF32000-2008 12.3.2.2 Para 2
+        // as if it's a "Remote-Go-To Actions"
+        // it's not strictly valid, but most viewers seem
+        // to handle it without crashing
+        // alternately, could put 0, or omit it
+        a->add(Object(destPageNum - 1));
+    }
+    a->add(Object(objName, "Fit"));
+
+    Object outlineItem = Object(new Dict(xref));
+
+    GooString *g = new GooString(itemTitle);
+    outlineItem.dictSet("Title", Object(g));
+    outlineItem.dictSet("Dest", Object(a));
+    outlineItem.dictSet("Count", Object(1));
+    outlineItem.dictAdd("Parent", Object(parentObjRef));
+
+    // add one to the main outline Object's count
+    Object parentObj = xref->fetch(parentObjRef);
+    int parentCount = parentObj.dictLookup("Count").getInt();
+    parentObj.dictSet("Count", Object(parentCount + 1));
+    xref->setModifiedObject(&parentObj, parentObjRef);
+
+    Object prevItemObject;
+    Object nextItemObject;
+
+    Ref outlineItemRef = xref->addIndirectObject(&outlineItem);
+
+    // the next two statements fix up the parent object
+    // for clarity we separate this out
+    if (it == items.begin()) {
+        // we will be the first item in the list
+        // fix our parent
+        parentObj.dictSet("First", Object(outlineItemRef));
+    }
+    if (it == items.end()) {
+        // we will be the last item on the list
+        // fix up our parent
+        parentObj.dictSet("Last", Object(outlineItemRef));
+    }
+
+    if (it == items.end()) {
+        if (!items.empty()) {
+            // insert at the end, we handle this separately
+            prevItemObject = xref->fetch((*(it - 1))->getRef());
+            prevItemObject.dictSet("Next", Object(outlineItemRef));
+            outlineItem.dictSet("Prev", Object((*(it - 1))->getRef()));
+            xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
+        }
+    } else {
+        nextItemObject = xref->fetch((*it)->getRef());
+        nextItemObject.dictSet("Prev", Object(outlineItemRef));
+        xref->setModifiedObject(&nextItemObject, (*it)->getRef());
+
+        outlineItem.dictSet("Next", Object((*(it))->getRef()));
+
+        if (it != items.begin()) {
+            prevItemObject = xref->fetch((*(it - 1))->getRef());
+            prevItemObject.dictSet("Next", Object(outlineItemRef));
+            outlineItem.dictSet("Prev", Object((*(it - 1))->getRef()));
+            xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
+        }
+    }
+
+    OutlineItem *item = new OutlineItem(outlineItem.getDict(), outlineItemRef, nullptr, xref, doc);
+
+    items.insert(it, item);
+}
+
+void Outline::insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos)
+{
+    Ref outlineObjRef = xref->getCatalog().dictLookupNF("Outlines").getRef();
+    insertChildHelper(itemTitle, destPageNum, pos, outlineObjRef, doc, xref, *items);
+}
+
+// ref is a valid reference to a list
+// walk the list and free any children
+// returns the number items deleted (just in case)
+static int recursiveRemoveList(Ref ref, XRef *xref)
+{
+    int count = 0;
+    bool done = false;
+
+    Ref nextRef;
+    Object tempObj;
+
+    while (!done) {
+        tempObj = xref->fetch(ref);
+
+        if (!tempObj.isDict()) {
+            // something horrible has happened
+            break;
+        }
+
+        const Object &firstRef = tempObj.dictLookupNF("First");
+        if (firstRef.isRef()) {
+            count += recursiveRemoveList(firstRef.getRef(), xref);
+        }
+
+        const Object &nextObjRef = tempObj.dictLookupNF("Next");
+        if (nextObjRef.isRef()) {
+            nextRef = nextObjRef.getRef();
+        } else {
+            done = true;
+        }
+        xref->removeIndirectObject(ref);
+        count++;
+        ref = nextRef;
+    }
+    return count;
+}
+
+static void removeChildHelper(unsigned int pos, PDFDoc *doc, XRef *xref, std::vector<OutlineItem *> &items)
+{
+    std::vector<OutlineItem *>::const_iterator it;
+    if (pos >= items.size()) {
+        // position is out of range, do nothing
+        return;
+    } else {
+        it = items.begin() + pos;
+    }
+
+    //  relink around this node
+    Object itemObject = xref->fetch((*it)->getRef());
+    Object parentObj = itemObject.dictLookup("Parent");
+    Object prevItemObject = itemObject.dictLookup("Prev");
+    Object nextItemObject = itemObject.dictLookup("Next");
+
+    // delete 1 from the parent Count if it's positive
+    Object countObj = parentObj.dictLookup("Count");
+    int count = countObj.getInt();
+    if (count > 0) {
+        count--;
+        parentObj.dictSet("Count", Object(count));
+        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
+    }
+
+    if (!prevItemObject.isNull() && !nextItemObject.isNull()) {
+        // deletion is in the middle
+        prevItemObject.dictSet("Next", Object((*(it + 1))->getRef()));
+        xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
+
+        nextItemObject.dictSet("Prev", Object((*(it - 1))->getRef()));
+        xref->setModifiedObject(&nextItemObject, (*(it + 1))->getRef());
+    } else if (prevItemObject.isNull() && nextItemObject.isNull()) {
+        // deletion is only child
+        parentObj.dictRemove("First");
+        parentObj.dictRemove("Last");
+        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
+    } else if (prevItemObject.isNull()) {
+        // deletion at the front
+        parentObj.dictSet("First", Object((*(it + 1))->getRef()));
+        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
+
+        nextItemObject.dictRemove("Prev");
+        xref->setModifiedObject(&nextItemObject, (*(it + 1))->getRef());
+    } else {
+        // deletion at the end
+        parentObj.dictSet("Last", Object((*(it - 1))->getRef()));
+        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
+        prevItemObject.dictRemove("Next");
+        xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
+    }
+
+    // free any children
+    const Object &firstRef = itemObject.dictLookupNF("First");
+    if (firstRef.isRef()) {
+        recursiveRemoveList(firstRef.getRef(), xref);
+    }
+
+    // free the pdf objects and the representation
+    xref->removeIndirectObject((*it)->getRef());
+    OutlineItem *oi = *it;
+    items.erase(it);
+    // deletion of the OutlineItem will delete all child
+    // outline items in its destructor
+    delete oi;
+}
+
+void Outline::removeChild(unsigned int pos)
+{
+    removeChildHelper(pos, doc, xref, *items);
+}
+
+//------------------------------------------------------------------------
+
+int Outline::addOutlineTreeNodeList(const std::vector<OutlineTreeNode> &nodeList, Ref &parentRef, Ref &firstRef, Ref &lastRef)
+{
+    firstRef = Ref::INVALID();
+    lastRef = Ref::INVALID();
+    if (nodeList.empty()) {
+        return 0;
+    }
+
+    int itemCount = 0;
+    Ref prevNodeRef = Ref::INVALID();
+
+    for (auto &node : nodeList) {
+
+        Array *a = new Array(doc->getXRef());
+        Ref *pageRef = doc->getCatalog()->getPageRef(node.destPageNum);
+        if (pageRef != nullptr) {
+            a->add(Object(*pageRef));
+        } else {
+            // if the page obj doesn't exist put the page number
+            // PDF32000-2008 12.3.2.2 Para 2
+            // as if it's a "Remote-Go-To Actions"
+            // it's not strictly valid, but most viewers seem
+            // to handle it without crashing
+            // alternately, could put 0, or omit it
+            a->add(Object(node.destPageNum - 1));
+        }
+        a->add(Object(objName, "Fit"));
+
+        Object outlineItem = Object(new Dict(doc->getXRef()));
+        Ref outlineItemRef = doc->getXRef()->addIndirectObject(&outlineItem);
+
+        if (firstRef == Ref::INVALID()) {
+            firstRef = outlineItemRef;
+        }
+        lastRef = outlineItemRef;
+
+        GooString *g = new GooString(node.title);
+        outlineItem.dictSet("Title", Object(g));
+        outlineItem.dictSet("Dest", Object(a));
+        itemCount++;
+
+        if (prevNodeRef != Ref::INVALID()) {
+            outlineItem.dictSet("Prev", Object(prevNodeRef));
+
+            // maybe easier way to fix up the previous object
+            Object prevOutlineItem = xref->fetch(prevNodeRef);
+            prevOutlineItem.dictSet("Next", Object(outlineItemRef));
+            xref->setModifiedObject(&prevOutlineItem, prevNodeRef);
+        }
+        prevNodeRef = outlineItemRef;
+
+        Ref firstChildRef;
+        Ref lastChildRef;
+        itemCount += addOutlineTreeNodeList(node.children, outlineItemRef, firstChildRef, lastChildRef);
+
+        if (firstChildRef != Ref::INVALID()) {
+            outlineItem.dictSet("First", Object(firstChildRef));
+            outlineItem.dictSet("Last", Object(lastChildRef));
+        }
+        outlineItem.dictSet("Count", Object(itemCount));
+        outlineItem.dictAdd("Parent", Object(parentRef));
+    }
+    return itemCount;
+}
+
+/* insert an outline into a PDF
+   outline->setOutline({ {"page 1", 1,
+                                         { { "1.1", 1, {} } }   },
+                            {"page 2", 2, {} },
+                            {"page 3", 3, {} },
+                            {"page 4", 4,{ { "4.1", 4, {} },
+                                           { "4.2", 4, {} },
+                                         },
+                            }
+                       });
+ */
+
+void Outline::setOutline(const std::vector<OutlineTreeNode> &nodeList)
+{
+    // check if outlineObj is an object, if it's not make sure it exists
+    if (!outlineObj->isDict()) {
+        outlineObj = doc->getCatalog()->getCreateOutline();
+
+        // make sure it was created
+        if (!outlineObj->isDict()) {
+            return;
+        }
+    }
+
+    Ref outlineObjRef = xref->getCatalog().dictLookupNF("Outlines").getRef();
+    Ref firstChildRef;
+    Ref lastChildRef;
+
+    // free any OutlineItem objects that will be replaced
+    const Object &firstChildRefObj = outlineObj->dictLookupNF("First");
+    if (firstChildRefObj.isRef()) {
+        recursiveRemoveList(firstChildRefObj.getRef(), xref);
+    }
+
+    const int count = addOutlineTreeNodeList(nodeList, outlineObjRef, firstChildRef, lastChildRef);
+
+    // modify the parent Outlines dict
+    if (firstChildRef != Ref::INVALID()) {
+        outlineObj->dictSet("First", Object(firstChildRef));
+        outlineObj->dictSet("Last", Object(lastChildRef));
+    } else {
+        // nothing was inserted into the outline, so just remove the
+        // child references in the top-level outline
+        outlineObj->dictRemove("First");
+        outlineObj->dictRemove("Last");
+    }
+    outlineObj->dictSet("Count", Object(count));
+    xref->setModifiedObject(outlineObj, outlineObjRef);
+
+    // reload the outline object from the xrefs
+
+    if (items) {
+        for (auto entry : *items) {
+            delete entry;
+        }
+        delete items;
+    }
+    const Object &first = outlineObj->dictLookupNF("First");
+    // we probably want to allow readItemList to create an empty list
+    // but for now just check and do it ourselves here
+    if (first.isRef()) {
+        items = OutlineItem::readItemList(nullptr, &first, xref, doc);
+    } else {
+        items = new std::vector<OutlineItem *>();
+    }
+}
+
 //------------------------------------------------------------------------
 
-OutlineItem::OutlineItem(const Dict *dict, int refNumA, OutlineItem *parentA, XRef *xrefA)
+OutlineItem::OutlineItem(const Dict *dict, Ref refA, OutlineItem *parentA, XRef *xrefA, PDFDoc *docA)
 {
     Object obj1;
 
-    refNum = refNumA;
+    ref = refA;
     parent = parentA;
     xref = xrefA;
+    doc = docA;
     title = nullptr;
     kids = nullptr;
 
@@ -89,10 +427,6 @@ OutlineItem::OutlineItem(const Dict *dict, int refNumA, OutlineItem *parentA, XR
         }
     }
 
-    firstRef = dict->lookupNF("First").copy();
-    lastRef = dict->lookupNF("Last").copy();
-    nextRef = dict->lookupNF("Next").copy();
-
     startsOpen = false;
     obj1 = dict->lookup("Count");
     if (obj1.isInt()) {
@@ -109,50 +443,133 @@ OutlineItem::~OutlineItem()
             delete entry;
         }
         delete kids;
+        kids = nullptr;
     }
     if (title) {
         gfree(title);
     }
 }
 
-std::vector<OutlineItem *> *OutlineItem::readItemList(OutlineItem *parent, const Object *firstItemRef, XRef *xrefA)
+std::vector<OutlineItem *> *OutlineItem::readItemList(OutlineItem *parent, const Object *firstItemRef, XRef *xrefA, PDFDoc *docA)
 {
     auto items = new std::vector<OutlineItem *>();
 
-    char *alreadyRead = (char *)gmalloc(xrefA->getNumObjects());
-    memset(alreadyRead, 0, xrefA->getNumObjects());
+    // could be a hash (unordered_map) too for better avg case check
+    // small number of objects expected, likely doesn't matter
+    std::set<Ref> alreadyRead;
 
     OutlineItem *parentO = parent;
     while (parentO) {
-        alreadyRead[parentO->refNum] = 1;
+        alreadyRead.insert(parentO->getRef());
         parentO = parentO->parent;
     }
 
-    const Object *p = firstItemRef;
-    while (p->isRef() && (p->getRefNum() >= 0) && (p->getRefNum() < xrefA->getNumObjects()) && !alreadyRead[p->getRefNum()]) {
-        Object obj = p->fetch(xrefA);
+    Object tempObj = firstItemRef->copy();
+    while (tempObj.isRef() && (tempObj.getRefNum() >= 0) && (tempObj.getRefNum() < xrefA->getNumObjects()) && alreadyRead.find(tempObj.getRef()) == alreadyRead.end()) {
+        Object obj = tempObj.fetch(xrefA);
         if (!obj.isDict()) {
             break;
         }
-        alreadyRead[p->getRefNum()] = 1;
-        OutlineItem *item = new OutlineItem(obj.getDict(), p->getRefNum(), parent, xrefA);
+        alreadyRead.insert(tempObj.getRef());
+        OutlineItem *item = new OutlineItem(obj.getDict(), tempObj.getRef(), parent, xrefA, docA);
         items->push_back(item);
-        p = &item->nextRef;
+        tempObj = obj.dictLookupNF("Next").copy();
+    }
+    return items;
+}
+
+void OutlineItem::open()
+{
+    if (!kids) {
+        Object itemDict = xref->fetch(ref);
+        const Object &firstRef = itemDict.dictLookupNF("First");
+        kids = readItemList(this, &firstRef, xref, doc);
     }
+}
+
+void OutlineItem::setTitle(const std::string &titleA)
+{
+    gfree(title);
 
-    gfree(alreadyRead);
+    Object dict = xref->fetch(ref);
+    GooString *g = new GooString(titleA);
+    titleLen = TextStringToUCS4(g, &title);
+    dict.dictSet("Title", Object(g));
+    xref->setModifiedObject(&dict, ref);
+}
 
-    if (items->empty()) {
-        delete items;
-        items = nullptr;
+bool OutlineItem::setPageDest(int i)
+{
+    Object dict = xref->fetch(ref);
+    Object obj1;
+
+    if (i < 1) {
+        return false;
     }
 
-    return items;
+    obj1 = dict.dictLookup("Dest");
+    if (!obj1.isNull()) {
+        int arrayLength = obj1.arrayGetLength();
+        for (int index = 0; index < arrayLength; index++) {
+            obj1.arrayRemove(0);
+        }
+        obj1.arrayAdd(Object(i - 1));
+        obj1.arrayAdd(Object(objName, "Fit"));
+
+        // unique_ptr will destroy previous on assignment
+        action = LinkAction::parseDest(&obj1);
+    } else {
+        obj1 = dict.dictLookup("A");
+        if (!obj1.isNull()) {
+            // RM 20210505 Implement
+        } else {
+        }
+        return false;
+    }
+
+    xref->setModifiedObject(&dict, ref);
+    return true;
 }
 
-void OutlineItem::open()
+void OutlineItem::insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos)
 {
-    if (!kids) {
-        kids = readItemList(this, &firstRef, xref);
+    open();
+    insertChildHelper(itemTitle, destPageNum, pos, ref, doc, xref, *kids);
+}
+
+void OutlineItem::removeChild(unsigned int pos)
+{
+    open();
+    removeChildHelper(pos, doc, xref, *kids);
+}
+
+void OutlineItem::setStartsOpen(bool value)
+{
+    startsOpen = value;
+    Object dict = xref->fetch(ref);
+    Object obj1 = dict.dictLookup("Count");
+    if (obj1.isInt()) {
+        const int count = obj1.getInt();
+        if ((count > 0 && !value) || (count < 0 && value)) {
+            // states requires change of sign
+            dict.dictSet("Count", Object(-count));
+            xref->setModifiedObject(&dict, ref);
+        }
     }
 }
+
+bool OutlineItem::hasKids()
+{
+    open();
+    return !kids->empty();
+}
+
+const std::vector<OutlineItem *> *OutlineItem::getKids()
+{
+    open();
+
+    if (!kids || kids->empty())
+        return nullptr;
+    else
+        return kids;
+}
diff --git a/poppler/Outline.h b/poppler/Outline.h
index 51a06fb0..8e0e7a48 100644
--- a/poppler/Outline.h
+++ b/poppler/Outline.h
@@ -30,6 +30,7 @@
 #include "CharTypes.h"
 #include "poppler_private_export.h"
 
+class PDFDoc;
 class GooString;
 class XRef;
 class LinkAction;
@@ -37,54 +38,81 @@ class OutlineItem;
 
 //------------------------------------------------------------------------
 
-class Outline
+class POPPLER_PRIVATE_EXPORT Outline
 {
+    PDFDoc *doc;
+    XRef *xref;
+    Object *outlineObj; // outline dict in catalog
+
 public:
-    Outline(const Object *outlineObj, XRef *xref);
+    Outline(Object *outlineObj, XRef *xref, PDFDoc *doc);
     ~Outline();
 
     Outline(const Outline &) = delete;
     Outline &operator=(const Outline &) = delete;
 
-    const std::vector<OutlineItem *> *getItems() const { return items; }
+    const std::vector<OutlineItem *> *getItems() const
+    {
+        if (!items || items->empty())
+            return nullptr;
+        else
+            return items;
+    }
+
+    struct OutlineTreeNode
+    {
+        std::string title;
+        int destPageNum;
+        std::vector<OutlineTreeNode> children;
+    };
+
+    // insert/remove child don't propagate changes to 'Count' up the entire
+    // tree
+    void setOutline(const std::vector<OutlineTreeNode> &nodeList);
+    void insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos);
+    void removeChild(unsigned int pos);
 
 private:
-    std::vector<OutlineItem *> *items; // nullptr if document has no outline,
+    std::vector<OutlineItem *> *items; // nullptr if document has no outline
+    int addOutlineTreeNodeList(const std::vector<OutlineTreeNode> &nodeList, Ref &parentRef, Ref &firstRef, Ref &lastRef);
 };
 
 //------------------------------------------------------------------------
 
 class POPPLER_PRIVATE_EXPORT OutlineItem
 {
+    friend Outline;
+
 public:
-    OutlineItem(const Dict *dict, int refNumA, OutlineItem *parentA, XRef *xrefA);
+    OutlineItem(const Dict *dict, Ref refA, OutlineItem *parentA, XRef *xrefA, PDFDoc *docA);
     ~OutlineItem();
-
     OutlineItem(const OutlineItem &) = delete;
     OutlineItem &operator=(const OutlineItem &) = delete;
-
-    static std::vector<OutlineItem *> *readItemList(OutlineItem *parent, const Object *firstItemRef, XRef *xrefA);
-
-    void open();
-
+    static std::vector<OutlineItem *> *readItemList(OutlineItem *parent, const Object *firstItemRef, XRef *xrefA, PDFDoc *docA);
     const Unicode *getTitle() const { return title; }
+    void setTitle(const std::string &titleA);
     int getTitleLength() const { return titleLen; }
+    bool setPageDest(int i);
     // OutlineItem keeps the ownership of the action
     const LinkAction *getAction() const { return action.get(); }
+    void setStartsOpen(bool value);
     bool isOpen() const { return startsOpen; }
-    bool hasKids() const { return firstRef.isRef(); }
-    const std::vector<OutlineItem *> *getKids() const { return kids; }
+    bool hasKids();
+    void open();
+    const std::vector<OutlineItem *> *getKids();
+    int getRefNum() const { return ref.num; }
+    Ref getRef() const { return ref; }
+    void insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos);
+    void removeChild(unsigned int pos);
 
 private:
-    int refNum;
+    Ref ref;
     OutlineItem *parent;
+    PDFDoc *doc;
     XRef *xref;
     Unicode *title;
     int titleLen;
     std::unique_ptr<LinkAction> action;
-    Object firstRef;
-    Object lastRef;
-    Object nextRef;
     bool startsOpen;
     std::vector<OutlineItem *> *kids; // nullptr if this item is closed or has no kids
 };
diff --git a/poppler/PDFDoc.cc b/poppler/PDFDoc.cc
index 90de9b8b..941f7a51 100644
--- a/poppler/PDFDoc.cc
+++ b/poppler/PDFDoc.cc
@@ -1933,7 +1933,7 @@ Outline *PDFDoc::getOutline()
     if (!outline) {
         pdfdocLocker();
         // read outline
-        outline = new Outline(catalog->getOutline(), xref);
+        outline = new Outline(catalog->getOutline(), xref, this);
     }
 
     return outline;
diff --git a/qt5/tests/CMakeLists.txt b/qt5/tests/CMakeLists.txt
index c3decb92..8293a3a1 100644
--- a/qt5/tests/CMakeLists.txt
+++ b/qt5/tests/CMakeLists.txt
@@ -70,6 +70,7 @@ qt5_add_qtest(check_qt5_permissions check_permissions.cpp)
 qt5_add_qtest(check_qt5_search check_search.cpp)
 qt5_add_qtest(check_qt5_actualtext check_actualtext.cpp)
 qt5_add_qtest(check_qt5_lexer check_lexer.cpp)
+qt5_add_qtest(check_qt5_internal_outline check_internal_outline.cpp)
 qt5_add_qtest(check_qt5_goostring check_goostring.cpp)
 qt5_add_qtest(check_qt5_object check_object.cpp)
 qt5_add_qtest(check_qt5_stroke_opacity check_stroke_opacity.cpp)
diff --git a/qt5/tests/check_internal_outline.cpp b/qt5/tests/check_internal_outline.cpp
new file mode 100644
index 00000000..0119d909
--- /dev/null
+++ b/qt5/tests/check_internal_outline.cpp
@@ -0,0 +1,436 @@
+#include <QtTest/QtTest>
+
+#include "Outline.h"
+#include "PDFDoc.h"
+#include "PDFDocFactory.h"
+
+class TestInternalOutline : public QObject
+{
+    Q_OBJECT
+public:
+    TestInternalOutline(QObject *parent = nullptr) : QObject(parent) { }
+private slots:
+    void testCreateOutline();
+    void testSetOutline();
+    void testInsertChild();
+    void testRemoveChild();
+    void testSetTitleAndSetPageDest();
+};
+
+void TestInternalOutline::testCreateOutline()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an empty outline and save the file
+    outline->setOutline({});
+    outlineItems = outline->getItems();
+    // no items will result in a nullptr rather than a 0 length list
+    QVERIFY(outlineItems == nullptr);
+    doc->saveAs(&gooTempFileName);
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline with no items
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+}
+
+static std::string getTitle(const OutlineItem *item)
+{
+    const Unicode *u = item->getTitle();
+    std::string s;
+    for (int i = 0; i < item->getTitleLength(); i++) {
+        s.append(1, (char)u[i]);
+    }
+    return s;
+}
+
+void TestInternalOutline::testSetOutline()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline(
+            { { "1", 1, { { "1.1", 1, {} }, { "1.2", 2, {} }, { "1.3", 3, { { "1.3.1", 1, {} }, { "1.3.2", 2, {} }, { "1.3.3", 3, {} }, { "1.3.4", 4, {} } } }, { "1.4", 4, {} } } }, { "2", 2, {} }, { "3", 3, {} }, { "4", 4, {} } });
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    doc->saveAs(&gooTempFileName);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    outlineItems = outline->getItems();
+
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->size() == 4);
+
+    OutlineItem *item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+
+    // c_str() is used so QCOMPARE prints string correctly on disagree
+    QCOMPARE(getTitle(item).c_str(), "1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "4");
+
+    outlineItems = outlineItems->at(0)->getKids();
+    QVERIFY(outlineItems != nullptr);
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.4");
+
+    outlineItems = outlineItems->at(2)->getKids();
+    QVERIFY(outlineItems != nullptr);
+
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.4");
+}
+
+void TestInternalOutline::testInsertChild()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+    QTemporaryFile tempFile2;
+    QVERIFY(tempFile2.open());
+    tempFile2.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+    const std::string tempFileName2 = tempFile2.fileName().toStdString();
+    const GooString gooTempFileName2 { tempFileName2 };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline({});
+    doc->saveAs(&gooTempFileName);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline with no items
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    // nullptr for 0-length
+    QVERIFY(outline->getItems() == nullptr);
+
+    // insert first one to empty
+    outline->insertChild("2", 1, 0);
+    // insert at the end
+    outline->insertChild("3", 1, 1);
+    // insert at the start
+    outline->insertChild("1", 1, 0);
+
+    // add an item to "2"
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->at(1));
+    outlineItems->at(1)->insertChild("2.1", 2, 0);
+    outlineItems->at(1)->insertChild("2.2", 2, 1);
+    outlineItems->at(1)->insertChild("2.4", 2, 2);
+
+    outlineItems->at(1)->insertChild("2.3", 2, 2);
+
+    // save the file
+    doc->saveAs(&gooTempFileName2);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    outlineItems = outline->getItems();
+
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->size() == 3);
+
+    OutlineItem *item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+
+    // c_str() is used so QCOMPARE prints string correctly on disagree
+    QCOMPARE(getTitle(item).c_str(), "1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "3");
+
+    outlineItems = outlineItems->at(1)->getKids();
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.4");
+}
+
+void TestInternalOutline::testRemoveChild()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    QTemporaryFile tempFile2;
+    QVERIFY(tempFile2.open());
+    tempFile2.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+    const std::string tempFileName2 = tempFile2.fileName().toStdString();
+    const GooString gooTempFileName2 { tempFileName2 };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline({ { "1", 1, { { "1.1", 1, {} }, { "1.2", 2, {} }, { "1.3", 3, { { "1.3.1", 1, {} }, { "1.3.2", 2, {} }, { "1.3.3", 3, {} }, { "1.3.4", 4, {} } } }, { "1.4", 4, {} } } },
+                          { "2", 2, { { "2.1", 1, {} } } },
+                          { "3", 3, { { "3.1", 1, {} }, { "3.2", 2, { { "3.2.1", 1, {} } } } } },
+                          { "4", 4, {} } });
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    doc->saveAs(&gooTempFileName);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    // remove "3"
+    outline->removeChild(2);
+    // remove "1.3.1"
+    outline->getItems()->at(0)->getKids()->at(2)->removeChild(0);
+    // remove "1.3.4"
+    outline->getItems()->at(0)->getKids()->at(2)->removeChild(2);
+    // remove "2.1"
+    outline->getItems()->at(1)->removeChild(0);
+
+    // save the file
+    doc->saveAs(&gooTempFileName2);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    outlineItems = outline->getItems();
+
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->size() == 3);
+
+    OutlineItem *item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+
+    // c_str() is used so QCOMPARE prints string correctly on disagree
+    QCOMPARE(getTitle(item).c_str(), "1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "4");
+
+    outlineItems = outlineItems->at(0)->getKids();
+    outlineItems = outlineItems->at(2)->getKids();
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.2");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.3");
+
+    // verify "2.1" is removed, lst length 0 is returned as a nullptr
+    QVERIFY(outline->getItems()->at(1)->getKids() == nullptr);
+}
+
+void TestInternalOutline::testSetTitleAndSetPageDest()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    QTemporaryFile tempFile2;
+    QVERIFY(tempFile2.open());
+    tempFile2.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+    const std::string tempFileName2 = tempFile2.fileName().toStdString();
+    const GooString gooTempFileName2 { tempFileName2 };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline({ { "1", 1, { { "1.1", 1, {} }, { "1.2", 2, {} }, { "1.3", 3, { { "1.3.1", 1, {} }, { "1.3.2", 2, {} }, { "1.3.3", 3, {} }, { "1.3.4", 4, {} } } }, { "1.4", 4, {} } } },
+                          { "2", 2, { { "2.1", 1, {} } } },
+                          { "3", 3, { { "3.1", 1, {} }, { "3.2", 2, { { "3.2.1", 1, {} } } } } },
+                          { "4", 4, {} } });
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    doc->saveAs(&gooTempFileName);
+
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    // change "1.3.1"
+    OutlineItem *item = outline->getItems()->at(0)->getKids()->at(2)->getKids()->at(0);
+    QCOMPARE(getTitle(item).c_str(), "1.3.1");
+
+    item->setTitle("Changed to a different title");
+
+    item = outline->getItems()->at(2);
+    {
+        const LinkAction *action = item->getAction();
+        QVERIFY(action->getKind() == actionGoTo);
+        const LinkGoTo *gotoAction = dynamic_cast<const LinkGoTo *>(action);
+        const LinkDest *dest = gotoAction->getDest();
+        QVERIFY(dest->isPageRef() == false);
+        QCOMPARE(dest->getPageNum(), 3);
+
+        item->setPageDest(1);
+    }
+
+    // save the file
+    doc->saveAs(&gooTempFileName2);
+    outline = nullptr;
+    item = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
+    QVERIFY(doc.get());
+
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    item = outline->getItems()->at(0)->getKids()->at(2)->getKids()->at(0);
+    QCOMPARE(getTitle(item).c_str(), "Changed to a different title");
+    {
+        item = outline->getItems()->at(2);
+        const LinkAction *action = item->getAction();
+        QVERIFY(action->getKind() == actionGoTo);
+        const LinkGoTo *gotoAction = dynamic_cast<const LinkGoTo *>(action);
+        const LinkDest *dest = gotoAction->getDest();
+        QVERIFY(dest->isPageRef() == false);
+        QCOMPARE(dest->getPageNum(), 1);
+    }
+}
+
+QTEST_GUILESS_MAIN(TestInternalOutline)
+#include "check_internal_outline.moc"
diff --git a/qt6/tests/CMakeLists.txt b/qt6/tests/CMakeLists.txt
index 72cc4860..d72614f3 100644
--- a/qt6/tests/CMakeLists.txt
+++ b/qt6/tests/CMakeLists.txt
@@ -60,6 +60,7 @@ qt6_add_qtest(check_qt6_permissions check_permissions.cpp)
 qt6_add_qtest(check_qt6_search check_search.cpp)
 qt6_add_qtest(check_qt6_actualtext check_actualtext.cpp)
 qt6_add_qtest(check_qt6_lexer check_lexer.cpp)
+qt6_add_qtest(check_qt6_internal_outline check_internal_outline.cpp)
 qt6_add_qtest(check_qt6_goostring check_goostring.cpp)
 qt6_add_qtest(check_qt6_object check_object.cpp)
 qt6_add_qtest(check_qt6_stroke_opacity check_stroke_opacity.cpp)
diff --git a/qt6/tests/check_internal_outline.cpp b/qt6/tests/check_internal_outline.cpp
new file mode 100644
index 00000000..0119d909
--- /dev/null
+++ b/qt6/tests/check_internal_outline.cpp
@@ -0,0 +1,436 @@
+#include <QtTest/QtTest>
+
+#include "Outline.h"
+#include "PDFDoc.h"
+#include "PDFDocFactory.h"
+
+class TestInternalOutline : public QObject
+{
+    Q_OBJECT
+public:
+    TestInternalOutline(QObject *parent = nullptr) : QObject(parent) { }
+private slots:
+    void testCreateOutline();
+    void testSetOutline();
+    void testInsertChild();
+    void testRemoveChild();
+    void testSetTitleAndSetPageDest();
+};
+
+void TestInternalOutline::testCreateOutline()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an empty outline and save the file
+    outline->setOutline({});
+    outlineItems = outline->getItems();
+    // no items will result in a nullptr rather than a 0 length list
+    QVERIFY(outlineItems == nullptr);
+    doc->saveAs(&gooTempFileName);
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline with no items
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+}
+
+static std::string getTitle(const OutlineItem *item)
+{
+    const Unicode *u = item->getTitle();
+    std::string s;
+    for (int i = 0; i < item->getTitleLength(); i++) {
+        s.append(1, (char)u[i]);
+    }
+    return s;
+}
+
+void TestInternalOutline::testSetOutline()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline(
+            { { "1", 1, { { "1.1", 1, {} }, { "1.2", 2, {} }, { "1.3", 3, { { "1.3.1", 1, {} }, { "1.3.2", 2, {} }, { "1.3.3", 3, {} }, { "1.3.4", 4, {} } } }, { "1.4", 4, {} } } }, { "2", 2, {} }, { "3", 3, {} }, { "4", 4, {} } });
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    doc->saveAs(&gooTempFileName);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    outlineItems = outline->getItems();
+
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->size() == 4);
+
+    OutlineItem *item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+
+    // c_str() is used so QCOMPARE prints string correctly on disagree
+    QCOMPARE(getTitle(item).c_str(), "1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "4");
+
+    outlineItems = outlineItems->at(0)->getKids();
+    QVERIFY(outlineItems != nullptr);
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.4");
+
+    outlineItems = outlineItems->at(2)->getKids();
+    QVERIFY(outlineItems != nullptr);
+
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.4");
+}
+
+void TestInternalOutline::testInsertChild()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+    QTemporaryFile tempFile2;
+    QVERIFY(tempFile2.open());
+    tempFile2.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+    const std::string tempFileName2 = tempFile2.fileName().toStdString();
+    const GooString gooTempFileName2 { tempFileName2 };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline({});
+    doc->saveAs(&gooTempFileName);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline with no items
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    // nullptr for 0-length
+    QVERIFY(outline->getItems() == nullptr);
+
+    // insert first one to empty
+    outline->insertChild("2", 1, 0);
+    // insert at the end
+    outline->insertChild("3", 1, 1);
+    // insert at the start
+    outline->insertChild("1", 1, 0);
+
+    // add an item to "2"
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->at(1));
+    outlineItems->at(1)->insertChild("2.1", 2, 0);
+    outlineItems->at(1)->insertChild("2.2", 2, 1);
+    outlineItems->at(1)->insertChild("2.4", 2, 2);
+
+    outlineItems->at(1)->insertChild("2.3", 2, 2);
+
+    // save the file
+    doc->saveAs(&gooTempFileName2);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    outlineItems = outline->getItems();
+
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->size() == 3);
+
+    OutlineItem *item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+
+    // c_str() is used so QCOMPARE prints string correctly on disagree
+    QCOMPARE(getTitle(item).c_str(), "1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "3");
+
+    outlineItems = outlineItems->at(1)->getKids();
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.3");
+    item = outlineItems->at(3);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2.4");
+}
+
+void TestInternalOutline::testRemoveChild()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    QTemporaryFile tempFile2;
+    QVERIFY(tempFile2.open());
+    tempFile2.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+    const std::string tempFileName2 = tempFile2.fileName().toStdString();
+    const GooString gooTempFileName2 { tempFileName2 };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline({ { "1", 1, { { "1.1", 1, {} }, { "1.2", 2, {} }, { "1.3", 3, { { "1.3.1", 1, {} }, { "1.3.2", 2, {} }, { "1.3.3", 3, {} }, { "1.3.4", 4, {} } } }, { "1.4", 4, {} } } },
+                          { "2", 2, { { "2.1", 1, {} } } },
+                          { "3", 3, { { "3.1", 1, {} }, { "3.2", 2, { { "3.2.1", 1, {} } } } } },
+                          { "4", 4, {} } });
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    doc->saveAs(&gooTempFileName);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    // remove "3"
+    outline->removeChild(2);
+    // remove "1.3.1"
+    outline->getItems()->at(0)->getKids()->at(2)->removeChild(0);
+    // remove "1.3.4"
+    outline->getItems()->at(0)->getKids()->at(2)->removeChild(2);
+    // remove "2.1"
+    outline->getItems()->at(1)->removeChild(0);
+
+    // save the file
+    doc->saveAs(&gooTempFileName2);
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
+    QVERIFY(doc.get());
+
+    // ensure the re-opened file has an outline
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    outlineItems = outline->getItems();
+
+    QVERIFY(outlineItems != nullptr);
+    QVERIFY(outlineItems->size() == 3);
+
+    OutlineItem *item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+
+    // c_str() is used so QCOMPARE prints string correctly on disagree
+    QCOMPARE(getTitle(item).c_str(), "1");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "2");
+    item = outlineItems->at(2);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "4");
+
+    outlineItems = outlineItems->at(0)->getKids();
+    outlineItems = outlineItems->at(2)->getKids();
+    item = outlineItems->at(0);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.2");
+    item = outlineItems->at(1);
+    QVERIFY(item != nullptr);
+    QCOMPARE(getTitle(item).c_str(), "1.3.3");
+
+    // verify "2.1" is removed, lst length 0 is returned as a nullptr
+    QVERIFY(outline->getItems()->at(1)->getKids() == nullptr);
+}
+
+void TestInternalOutline::testSetTitleAndSetPageDest()
+{
+    QTemporaryFile tempFile;
+    QVERIFY(tempFile.open());
+    tempFile.close();
+
+    QTemporaryFile tempFile2;
+    QVERIFY(tempFile2.open());
+    tempFile2.close();
+
+    const std::string tempFileName = tempFile.fileName().toStdString();
+    const GooString gooTempFileName { tempFileName };
+    const std::string tempFileName2 = tempFile2.fileName().toStdString();
+    const GooString gooTempFileName2 { tempFileName2 };
+
+    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
+    QVERIFY(doc.get());
+
+    // ensure the file has no existing outline
+    Outline *outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+    auto *outlineItems = outline->getItems();
+    QVERIFY(outlineItems == nullptr);
+
+    // create an outline and save the file
+    outline->setOutline({ { "1", 1, { { "1.1", 1, {} }, { "1.2", 2, {} }, { "1.3", 3, { { "1.3.1", 1, {} }, { "1.3.2", 2, {} }, { "1.3.3", 3, {} }, { "1.3.4", 4, {} } } }, { "1.4", 4, {} } } },
+                          { "2", 2, { { "2.1", 1, {} } } },
+                          { "3", 3, { { "3.1", 1, {} }, { "3.2", 2, { { "3.2.1", 1, {} } } } } },
+                          { "4", 4, {} } });
+    outlineItems = outline->getItems();
+    QVERIFY(outlineItems != nullptr);
+    doc->saveAs(&gooTempFileName);
+
+    outline = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
+    QVERIFY(doc.get());
+
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    // change "1.3.1"
+    OutlineItem *item = outline->getItems()->at(0)->getKids()->at(2)->getKids()->at(0);
+    QCOMPARE(getTitle(item).c_str(), "1.3.1");
+
+    item->setTitle("Changed to a different title");
+
+    item = outline->getItems()->at(2);
+    {
+        const LinkAction *action = item->getAction();
+        QVERIFY(action->getKind() == actionGoTo);
+        const LinkGoTo *gotoAction = dynamic_cast<const LinkGoTo *>(action);
+        const LinkDest *dest = gotoAction->getDest();
+        QVERIFY(dest->isPageRef() == false);
+        QCOMPARE(dest->getPageNum(), 3);
+
+        item->setPageDest(1);
+    }
+
+    // save the file
+    doc->saveAs(&gooTempFileName2);
+    outline = nullptr;
+    item = nullptr;
+
+    /******************************************************/
+
+    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
+    QVERIFY(doc.get());
+
+    outline = doc->getOutline();
+    QVERIFY(outline != nullptr);
+
+    item = outline->getItems()->at(0)->getKids()->at(2)->getKids()->at(0);
+    QCOMPARE(getTitle(item).c_str(), "Changed to a different title");
+    {
+        item = outline->getItems()->at(2);
+        const LinkAction *action = item->getAction();
+        QVERIFY(action->getKind() == actionGoTo);
+        const LinkGoTo *gotoAction = dynamic_cast<const LinkGoTo *>(action);
+        const LinkDest *dest = gotoAction->getDest();
+        QVERIFY(dest->isPageRef() == false);
+        QCOMPARE(dest->getPageNum(), 1);
+    }
+}
+
+QTEST_GUILESS_MAIN(TestInternalOutline)
+#include "check_internal_outline.moc"


More information about the poppler mailing list