Coverage Report

Created: 2026-08-25 06:40

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