1 //========================================================================
2 //
3 // Outline.cc
4 //
5 // Copyright 2002-2003 Glyph & Cog, LLC
6 //
7 //========================================================================
8 
9 //========================================================================
10 //
11 // Modified under the Poppler project - http://poppler.freedesktop.org
12 //
13 // All changes made under the Poppler project to this file are licensed
14 // under GPL version 2 or later
15 //
16 // Copyright (C) 2005 Marco Pesenti Gritti <mpg@redhat.com>
17 // Copyright (C) 2008, 2016-2019, 2021 Albert Astals Cid <aacid@kde.org>
18 // Copyright (C) 2009 Nick Jones <nick.jones@network-box.com>
19 // Copyright (C) 2016 Jason Crain <jason@aquaticape.us>
20 // Copyright (C) 2017 Adrian Johnson <ajohnson@redneon.com>
21 // Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich
22 // Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de>
23 // Copyright (C) 2019, 2020 Oliver Sander <oliver.sander@tu-dresden.de>
24 // Copyright (C) 2021 RM <rm+git@arcsin.org>
25 //
26 // To see a description of the changes please see the Changelog file that
27 // came with your tarball or type make ChangeLog if you are building from git
28 //
29 //========================================================================
30 
31 #include <config.h>
32 
33 #include "goo/gmem.h"
34 #include "goo/GooString.h"
35 #include "PDFDoc.h"
36 #include "XRef.h"
37 #include "Link.h"
38 #include "PDFDocEncoding.h"
39 #include "Outline.h"
40 #include "UTF.h"
41 
42 //------------------------------------------------------------------------
43 
Outline(Object * outlineObjA,XRef * xrefA,PDFDoc * docA)44 Outline::Outline(Object *outlineObjA, XRef *xrefA, PDFDoc *docA)
45 {
46     outlineObj = outlineObjA;
47     xref = xrefA;
48     doc = docA;
49     items = nullptr;
50     if (!outlineObj->isDict()) {
51         return;
52     }
53     const Object &first = outlineObj->dictLookupNF("First");
54     items = OutlineItem::readItemList(nullptr, &first, xref, doc);
55 }
56 
~Outline()57 Outline::~Outline()
58 {
59     if (items) {
60         for (auto entry : *items) {
61             delete entry;
62         }
63         delete items;
64     }
65 }
66 
insertChildHelper(const std::string & itemTitle,int destPageNum,unsigned int pos,Ref parentObjRef,PDFDoc * doc,XRef * xref,std::vector<OutlineItem * > & items)67 static void insertChildHelper(const std::string &itemTitle, int destPageNum, unsigned int pos, Ref parentObjRef, PDFDoc *doc, XRef *xref, std::vector<OutlineItem *> &items)
68 {
69     std::vector<OutlineItem *>::const_iterator it;
70     if (pos >= items.size()) {
71         it = items.end();
72     } else {
73         it = items.begin() + pos;
74     }
75 
76     Array *a = new Array(xref);
77     Ref *pageRef = doc->getCatalog()->getPageRef(destPageNum);
78     if (pageRef != nullptr) {
79         a->add(Object(*pageRef));
80     } else {
81         // if the page obj doesn't exist put the page number
82         // PDF32000-2008 12.3.2.2 Para 2
83         // as if it's a "Remote-Go-To Actions"
84         // it's not strictly valid, but most viewers seem
85         // to handle it without crashing
86         // alternately, could put 0, or omit it
87         a->add(Object(destPageNum - 1));
88     }
89     a->add(Object(objName, "Fit"));
90 
91     Object outlineItem = Object(new Dict(xref));
92 
93     GooString *g = new GooString(itemTitle);
94     outlineItem.dictSet("Title", Object(g));
95     outlineItem.dictSet("Dest", Object(a));
96     outlineItem.dictSet("Count", Object(1));
97     outlineItem.dictAdd("Parent", Object(parentObjRef));
98 
99     // add one to the main outline Object's count
100     Object parentObj = xref->fetch(parentObjRef);
101     int parentCount = parentObj.dictLookup("Count").getInt();
102     parentObj.dictSet("Count", Object(parentCount + 1));
103     xref->setModifiedObject(&parentObj, parentObjRef);
104 
105     Object prevItemObject;
106     Object nextItemObject;
107 
108     Ref outlineItemRef = xref->addIndirectObject(outlineItem);
109 
110     // the next two statements fix up the parent object
111     // for clarity we separate this out
112     if (it == items.begin()) {
113         // we will be the first item in the list
114         // fix our parent
115         parentObj.dictSet("First", Object(outlineItemRef));
116     }
117     if (it == items.end()) {
118         // we will be the last item on the list
119         // fix up our parent
120         parentObj.dictSet("Last", Object(outlineItemRef));
121     }
122 
123     if (it == items.end()) {
124         if (!items.empty()) {
125             // insert at the end, we handle this separately
126             prevItemObject = xref->fetch((*(it - 1))->getRef());
127             prevItemObject.dictSet("Next", Object(outlineItemRef));
128             outlineItem.dictSet("Prev", Object((*(it - 1))->getRef()));
129             xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
130         }
131     } else {
132         nextItemObject = xref->fetch((*it)->getRef());
133         nextItemObject.dictSet("Prev", Object(outlineItemRef));
134         xref->setModifiedObject(&nextItemObject, (*it)->getRef());
135 
136         outlineItem.dictSet("Next", Object((*(it))->getRef()));
137 
138         if (it != items.begin()) {
139             prevItemObject = xref->fetch((*(it - 1))->getRef());
140             prevItemObject.dictSet("Next", Object(outlineItemRef));
141             outlineItem.dictSet("Prev", Object((*(it - 1))->getRef()));
142             xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
143         }
144     }
145 
146     OutlineItem *item = new OutlineItem(outlineItem.getDict(), outlineItemRef, nullptr, xref, doc);
147 
148     items.insert(it, item);
149 }
150 
insertChild(const std::string & itemTitle,int destPageNum,unsigned int pos)151 void Outline::insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos)
152 {
153     Ref outlineObjRef = xref->getCatalog().dictLookupNF("Outlines").getRef();
154     insertChildHelper(itemTitle, destPageNum, pos, outlineObjRef, doc, xref, *items);
155 }
156 
157 // ref is a valid reference to a list
158 // walk the list and free any children
159 // returns the number items deleted (just in case)
recursiveRemoveList(Ref ref,XRef * xref)160 static int recursiveRemoveList(Ref ref, XRef *xref)
161 {
162     int count = 0;
163     bool done = false;
164 
165     Ref nextRef;
166     Object tempObj;
167 
168     while (!done) {
169         tempObj = xref->fetch(ref);
170 
171         if (!tempObj.isDict()) {
172             // something horrible has happened
173             break;
174         }
175 
176         const Object &firstRef = tempObj.dictLookupNF("First");
177         if (firstRef.isRef()) {
178             count += recursiveRemoveList(firstRef.getRef(), xref);
179         }
180 
181         const Object &nextObjRef = tempObj.dictLookupNF("Next");
182         if (nextObjRef.isRef()) {
183             nextRef = nextObjRef.getRef();
184         } else {
185             done = true;
186         }
187         xref->removeIndirectObject(ref);
188         count++;
189         ref = nextRef;
190     }
191     return count;
192 }
193 
removeChildHelper(unsigned int pos,PDFDoc * doc,XRef * xref,std::vector<OutlineItem * > & items)194 static void removeChildHelper(unsigned int pos, PDFDoc *doc, XRef *xref, std::vector<OutlineItem *> &items)
195 {
196     std::vector<OutlineItem *>::const_iterator it;
197     if (pos >= items.size()) {
198         // position is out of range, do nothing
199         return;
200     } else {
201         it = items.begin() + pos;
202     }
203 
204     //  relink around this node
205     Object itemObject = xref->fetch((*it)->getRef());
206     Object parentObj = itemObject.dictLookup("Parent");
207     Object prevItemObject = itemObject.dictLookup("Prev");
208     Object nextItemObject = itemObject.dictLookup("Next");
209 
210     // delete 1 from the parent Count if it's positive
211     Object countObj = parentObj.dictLookup("Count");
212     int count = countObj.getInt();
213     if (count > 0) {
214         count--;
215         parentObj.dictSet("Count", Object(count));
216         xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
217     }
218 
219     if (!prevItemObject.isNull() && !nextItemObject.isNull()) {
220         // deletion is in the middle
221         prevItemObject.dictSet("Next", Object((*(it + 1))->getRef()));
222         xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
223 
224         nextItemObject.dictSet("Prev", Object((*(it - 1))->getRef()));
225         xref->setModifiedObject(&nextItemObject, (*(it + 1))->getRef());
226     } else if (prevItemObject.isNull() && nextItemObject.isNull()) {
227         // deletion is only child
228         parentObj.dictRemove("First");
229         parentObj.dictRemove("Last");
230         xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
231     } else if (prevItemObject.isNull()) {
232         // deletion at the front
233         parentObj.dictSet("First", Object((*(it + 1))->getRef()));
234         xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
235 
236         nextItemObject.dictRemove("Prev");
237         xref->setModifiedObject(&nextItemObject, (*(it + 1))->getRef());
238     } else {
239         // deletion at the end
240         parentObj.dictSet("Last", Object((*(it - 1))->getRef()));
241         xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
242         prevItemObject.dictRemove("Next");
243         xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
244     }
245 
246     // free any children
247     const Object &firstRef = itemObject.dictLookupNF("First");
248     if (firstRef.isRef()) {
249         recursiveRemoveList(firstRef.getRef(), xref);
250     }
251 
252     // free the pdf objects and the representation
253     xref->removeIndirectObject((*it)->getRef());
254     OutlineItem *oi = *it;
255     items.erase(it);
256     // deletion of the OutlineItem will delete all child
257     // outline items in its destructor
258     delete oi;
259 }
260 
removeChild(unsigned int pos)261 void Outline::removeChild(unsigned int pos)
262 {
263     removeChildHelper(pos, doc, xref, *items);
264 }
265 
266 //------------------------------------------------------------------------
267 
addOutlineTreeNodeList(const std::vector<OutlineTreeNode> & nodeList,Ref & parentRef,Ref & firstRef,Ref & lastRef)268 int Outline::addOutlineTreeNodeList(const std::vector<OutlineTreeNode> &nodeList, Ref &parentRef, Ref &firstRef, Ref &lastRef)
269 {
270     firstRef = Ref::INVALID();
271     lastRef = Ref::INVALID();
272     if (nodeList.empty()) {
273         return 0;
274     }
275 
276     int itemCount = 0;
277     Ref prevNodeRef = Ref::INVALID();
278 
279     for (auto &node : nodeList) {
280 
281         Array *a = new Array(doc->getXRef());
282         Ref *pageRef = doc->getCatalog()->getPageRef(node.destPageNum);
283         if (pageRef != nullptr) {
284             a->add(Object(*pageRef));
285         } else {
286             // if the page obj doesn't exist put the page number
287             // PDF32000-2008 12.3.2.2 Para 2
288             // as if it's a "Remote-Go-To Actions"
289             // it's not strictly valid, but most viewers seem
290             // to handle it without crashing
291             // alternately, could put 0, or omit it
292             a->add(Object(node.destPageNum - 1));
293         }
294         a->add(Object(objName, "Fit"));
295 
296         Object outlineItem = Object(new Dict(doc->getXRef()));
297         Ref outlineItemRef = doc->getXRef()->addIndirectObject(outlineItem);
298 
299         if (firstRef == Ref::INVALID()) {
300             firstRef = outlineItemRef;
301         }
302         lastRef = outlineItemRef;
303 
304         GooString *g = new GooString(node.title);
305         outlineItem.dictSet("Title", Object(g));
306         outlineItem.dictSet("Dest", Object(a));
307         itemCount++;
308 
309         if (prevNodeRef != Ref::INVALID()) {
310             outlineItem.dictSet("Prev", Object(prevNodeRef));
311 
312             // maybe easier way to fix up the previous object
313             Object prevOutlineItem = xref->fetch(prevNodeRef);
314             prevOutlineItem.dictSet("Next", Object(outlineItemRef));
315             xref->setModifiedObject(&prevOutlineItem, prevNodeRef);
316         }
317         prevNodeRef = outlineItemRef;
318 
319         Ref firstChildRef;
320         Ref lastChildRef;
321         itemCount += addOutlineTreeNodeList(node.children, outlineItemRef, firstChildRef, lastChildRef);
322 
323         if (firstChildRef != Ref::INVALID()) {
324             outlineItem.dictSet("First", Object(firstChildRef));
325             outlineItem.dictSet("Last", Object(lastChildRef));
326         }
327         outlineItem.dictSet("Count", Object(itemCount));
328         outlineItem.dictAdd("Parent", Object(parentRef));
329     }
330     return itemCount;
331 }
332 
333 /* insert an outline into a PDF
334    outline->setOutline({ {"page 1", 1,
335                                          { { "1.1", 1, {} } }   },
336                             {"page 2", 2, {} },
337                             {"page 3", 3, {} },
338                             {"page 4", 4,{ { "4.1", 4, {} },
339                                            { "4.2", 4, {} },
340                                          },
341                             }
342                        });
343  */
344 
setOutline(const std::vector<OutlineTreeNode> & nodeList)345 void Outline::setOutline(const std::vector<OutlineTreeNode> &nodeList)
346 {
347     // check if outlineObj is an object, if it's not make sure it exists
348     if (!outlineObj->isDict()) {
349         outlineObj = doc->getCatalog()->getCreateOutline();
350 
351         // make sure it was created
352         if (!outlineObj->isDict()) {
353             return;
354         }
355     }
356 
357     Ref outlineObjRef = xref->getCatalog().dictLookupNF("Outlines").getRef();
358     Ref firstChildRef;
359     Ref lastChildRef;
360 
361     // free any OutlineItem objects that will be replaced
362     const Object &firstChildRefObj = outlineObj->dictLookupNF("First");
363     if (firstChildRefObj.isRef()) {
364         recursiveRemoveList(firstChildRefObj.getRef(), xref);
365     }
366 
367     const int count = addOutlineTreeNodeList(nodeList, outlineObjRef, firstChildRef, lastChildRef);
368 
369     // modify the parent Outlines dict
370     if (firstChildRef != Ref::INVALID()) {
371         outlineObj->dictSet("First", Object(firstChildRef));
372         outlineObj->dictSet("Last", Object(lastChildRef));
373     } else {
374         // nothing was inserted into the outline, so just remove the
375         // child references in the top-level outline
376         outlineObj->dictRemove("First");
377         outlineObj->dictRemove("Last");
378     }
379     outlineObj->dictSet("Count", Object(count));
380     xref->setModifiedObject(outlineObj, outlineObjRef);
381 
382     // reload the outline object from the xrefs
383 
384     if (items) {
385         for (auto entry : *items) {
386             delete entry;
387         }
388         delete items;
389     }
390     const Object &first = outlineObj->dictLookupNF("First");
391     // we probably want to allow readItemList to create an empty list
392     // but for now just check and do it ourselves here
393     if (first.isRef()) {
394         items = OutlineItem::readItemList(nullptr, &first, xref, doc);
395     } else {
396         items = new std::vector<OutlineItem *>();
397     }
398 }
399 
400 //------------------------------------------------------------------------
401 
OutlineItem(const Dict * dict,Ref refA,OutlineItem * parentA,XRef * xrefA,PDFDoc * docA)402 OutlineItem::OutlineItem(const Dict *dict, Ref refA, OutlineItem *parentA, XRef *xrefA, PDFDoc *docA)
403 {
404     Object obj1;
405 
406     ref = refA;
407     parent = parentA;
408     xref = xrefA;
409     doc = docA;
410     title = nullptr;
411     kids = nullptr;
412 
413     obj1 = dict->lookup("Title");
414     if (obj1.isString()) {
415         const GooString *s = obj1.getString();
416         titleLen = TextStringToUCS4(s->toStr(), &title);
417     } else {
418         titleLen = 0;
419     }
420 
421     obj1 = dict->lookup("Dest");
422     if (!obj1.isNull()) {
423         action = LinkAction::parseDest(&obj1);
424     } else {
425         obj1 = dict->lookup("A");
426         if (!obj1.isNull()) {
427             action = LinkAction::parseAction(&obj1);
428         }
429     }
430 
431     startsOpen = false;
432     obj1 = dict->lookup("Count");
433     if (obj1.isInt()) {
434         if (obj1.getInt() > 0) {
435             startsOpen = true;
436         }
437     }
438 }
439 
~OutlineItem()440 OutlineItem::~OutlineItem()
441 {
442     if (kids) {
443         for (auto entry : *kids) {
444             delete entry;
445         }
446         delete kids;
447         kids = nullptr;
448     }
449     if (title) {
450         gfree(title);
451     }
452 }
453 
readItemList(OutlineItem * parent,const Object * firstItemRef,XRef * xrefA,PDFDoc * docA)454 std::vector<OutlineItem *> *OutlineItem::readItemList(OutlineItem *parent, const Object *firstItemRef, XRef *xrefA, PDFDoc *docA)
455 {
456     auto items = new std::vector<OutlineItem *>();
457 
458     // could be a hash (unordered_map) too for better avg case check
459     // small number of objects expected, likely doesn't matter
460     std::set<Ref> alreadyRead;
461 
462     OutlineItem *parentO = parent;
463     while (parentO) {
464         alreadyRead.insert(parentO->getRef());
465         parentO = parentO->parent;
466     }
467 
468     Object tempObj = firstItemRef->copy();
469     while (tempObj.isRef() && (tempObj.getRefNum() >= 0) && (tempObj.getRefNum() < xrefA->getNumObjects()) && alreadyRead.find(tempObj.getRef()) == alreadyRead.end()) {
470         Object obj = tempObj.fetch(xrefA);
471         if (!obj.isDict()) {
472             break;
473         }
474         alreadyRead.insert(tempObj.getRef());
475         OutlineItem *item = new OutlineItem(obj.getDict(), tempObj.getRef(), parent, xrefA, docA);
476         items->push_back(item);
477         tempObj = obj.dictLookupNF("Next").copy();
478     }
479     return items;
480 }
481 
open()482 void OutlineItem::open()
483 {
484     if (!kids) {
485         Object itemDict = xref->fetch(ref);
486         const Object &firstRef = itemDict.dictLookupNF("First");
487         kids = readItemList(this, &firstRef, xref, doc);
488     }
489 }
490 
setTitle(const std::string & titleA)491 void OutlineItem::setTitle(const std::string &titleA)
492 {
493     gfree(title);
494 
495     Object dict = xref->fetch(ref);
496     GooString *g = new GooString(titleA);
497     titleLen = TextStringToUCS4(g->toStr(), &title);
498     dict.dictSet("Title", Object(g));
499     xref->setModifiedObject(&dict, ref);
500 }
501 
setPageDest(int i)502 bool OutlineItem::setPageDest(int i)
503 {
504     Object dict = xref->fetch(ref);
505     Object obj1;
506 
507     if (i < 1) {
508         return false;
509     }
510 
511     obj1 = dict.dictLookup("Dest");
512     if (!obj1.isNull()) {
513         int arrayLength = obj1.arrayGetLength();
514         for (int index = 0; index < arrayLength; index++) {
515             obj1.arrayRemove(0);
516         }
517         obj1.arrayAdd(Object(i - 1));
518         obj1.arrayAdd(Object(objName, "Fit"));
519 
520         // unique_ptr will destroy previous on assignment
521         action = LinkAction::parseDest(&obj1);
522     } else {
523         obj1 = dict.dictLookup("A");
524         if (!obj1.isNull()) {
525             // RM 20210505 Implement
526         } else {
527         }
528         return false;
529     }
530 
531     xref->setModifiedObject(&dict, ref);
532     return true;
533 }
534 
insertChild(const std::string & itemTitle,int destPageNum,unsigned int pos)535 void OutlineItem::insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos)
536 {
537     open();
538     insertChildHelper(itemTitle, destPageNum, pos, ref, doc, xref, *kids);
539 }
540 
removeChild(unsigned int pos)541 void OutlineItem::removeChild(unsigned int pos)
542 {
543     open();
544     removeChildHelper(pos, doc, xref, *kids);
545 }
546 
setStartsOpen(bool value)547 void OutlineItem::setStartsOpen(bool value)
548 {
549     startsOpen = value;
550     Object dict = xref->fetch(ref);
551     Object obj1 = dict.dictLookup("Count");
552     if (obj1.isInt()) {
553         const int count = obj1.getInt();
554         if ((count > 0 && !value) || (count < 0 && value)) {
555             // states requires change of sign
556             dict.dictSet("Count", Object(-count));
557             xref->setModifiedObject(&dict, ref);
558         }
559     }
560 }
561 
hasKids()562 bool OutlineItem::hasKids()
563 {
564     open();
565     return !kids->empty();
566 }
567 
getKids()568 const std::vector<OutlineItem *> *OutlineItem::getKids()
569 {
570     open();
571 
572     if (!kids || kids->empty())
573         return nullptr;
574     else
575         return kids;
576 }
577