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

source code of poppler/poppler/Outline.cc