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+...@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" _______________________________________________ poppler mailing list poppler@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/poppler