Add document and object helpers for outlines (bookmarks)

This commit is contained in:
Jay Berkenbilt 2018-12-18 21:08:53 -05:00
parent 30a0c070e4
commit d5d179f441
16 changed files with 5305 additions and 0 deletions

View File

@ -1,3 +1,10 @@
2018-12-19 Jay Berkenbilt <ejb@ql.org>
* Add QPDFOutlineDocumentHelper and QPDFOutlineObjectHelper for
handling outlines (bookmarks) including bidirectionally mapping
between bookmarks and pages. Initially there is no support for
modifying the outlines hierarchy.
2018-12-18 Jay Berkenbilt <ejb@ql.org>
* New method QPDFObjectHandle::getJSON() returns a JSON object

View File

@ -0,0 +1,108 @@
// Copyright (c) 2005-2018 Jay Berkenbilt
//
// This file is part of qpdf.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Versions of qpdf prior to version 7 were released under the terms
// of version 2.0 of the Artistic License. At your option, you may
// continue to consider qpdf to be licensed under those terms. Please
// see the manual for additional information.
#ifndef QPDFOUTLINEDOCUMENTHELPER_HH
#define QPDFOUTLINEDOCUMENTHELPER_HH
#include <qpdf/QPDFDocumentHelper.hh>
#include <qpdf/QPDFOutlineObjectHelper.hh>
#include <qpdf/QPDFNameTreeObjectHelper.hh>
#include <qpdf/QPDF.hh>
#include <map>
#include <list>
#include <set>
#include <qpdf/DLL.h>
// This is a document helper for outlines, also known as bookmarks.
// Outlines are discussed in section 12.3.3 of the PDF spec
// (ISO-32000). With the help of QPDFOutlineObjectHelper, the outlines
// tree is traversed, and a bidirectional map is made between pages
// and outlines. See also QPDFOutlineObjectHelper.
class QPDFOutlineDocumentHelper: public QPDFDocumentHelper
{
public:
QPDF_DLL
QPDFOutlineDocumentHelper(QPDF&);
QPDF_DLL
virtual ~QPDFOutlineDocumentHelper();
QPDF_DLL
bool hasOutlines();
QPDF_DLL
std::list<QPDFOutlineObjectHelper> getTopLevelOutlines();
// If the name is a name object, look it up in the /Dests key of
// the document catalog. If the name is a string, look it up in
// the name tree pointed to by the /Dests key of the names
// dictionary.
QPDF_DLL
QPDFObjectHandle
resolveNamedDest(QPDFObjectHandle name);
// Return a list outlines that are known to target the specified
// page
QPDF_DLL
std::list<QPDFOutlineObjectHelper> getOutlinesForPage(QPDFObjGen const&);
class Accessor
{
friend class QPDFOutlineObjectHelper;
QPDF_DLL
static bool
checkSeen(QPDFOutlineDocumentHelper& dh, QPDFObjGen const& og)
{
return dh.checkSeen(og);
}
};
friend class Accessor;
private:
bool checkSeen(QPDFObjGen const& og);
void initializeByPage();
class Members
{
friend class QPDFOutlineDocumentHelper;
public:
QPDF_DLL
~Members();
private:
Members();
Members(Members const&);
std::list<QPDFOutlineObjectHelper> outlines;
std::set<QPDFObjGen> seen;
QPDFObjectHandle dest_dict;
PointerHolder<QPDFNameTreeObjectHelper> names_dest;
std::map<QPDFObjGen, std::list<QPDFOutlineObjectHelper> > by_page;
};
PointerHolder<Members> m;
};
#endif // QPDFOUTLINEDOCUMENTHELPER_HH

View File

@ -0,0 +1,122 @@
// Copyright (c) 2005-2018 Jay Berkenbilt
//
// This file is part of qpdf.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Versions of qpdf prior to version 7 were released under the terms
// of version 2.0 of the Artistic License. At your option, you may
// continue to consider qpdf to be licensed under those terms. Please
// see the manual for additional information.
#ifndef QPDFOUTLINEOBJECTHELPER_HH
#define QPDFOUTLINEOBJECTHELPER_HH
#include <qpdf/QPDFObjectHelper.hh>
#include <qpdf/QPDFObjGen.hh>
#include <list>
class QPDFOutlineDocumentHelper;
#include <qpdf/DLL.h>
// This is an object helper for outline items. Outlines, also known as
// bookmarks, are described in section 12.3.3 of the PDF spec
// (ISO-32000). See comments below for details.
class QPDFOutlineObjectHelper: public QPDFObjectHelper
{
public:
QPDF_DLL
virtual ~QPDFOutlineObjectHelper()
{
// This must be cleared explicitly to avoid circular references
// that prevent cleanup of pointer holders.
this->m->parent = 0;
}
// All constructors are private. You can only create one of these
// using QPDFOutlineDocumentHelper.
// Return parent pointer. Returns a null pointer if this is a
// top-level outline.
QPDF_DLL
PointerHolder<QPDFOutlineObjectHelper> getParent();
// Return children as a list.
QPDF_DLL
std::list<QPDFOutlineObjectHelper> getKids();
// Return the destination, regardless of whether it is named or
// explicit and whether it is directly provided or in a GoTo
// action. Returns a null object if the destination can't be
// determined. Named destinations can be resolved using the older
// root /Dest dictionary or the current names tree.
QPDF_DLL
QPDFObjectHandle getDest();
// Return the page that the outline points to. Returns a null
// object if the destination page can't be determined.
QPDF_DLL
QPDFObjectHandle getDestPage();
// Returns the value of /Count as present in the object, or 0 if
// not present. If count is positive, the outline is open. If
// negative, it is closed. Either way, the absolute value is the
// number descendant items that would be visible if this were
// open.
QPDF_DLL
int getCount();
// Returns the title as a UTF-8 string. Returns the empty string
// if there is no title.
QPDF_DLL
std::string getTitle();
class Accessor
{
friend class QPDFOutlineDocumentHelper;
static QPDFOutlineObjectHelper
create(QPDFObjectHandle oh, QPDFOutlineDocumentHelper& dh, int depth)
{
return QPDFOutlineObjectHelper(oh, dh, depth);
}
};
friend class Accessor;
private:
QPDF_DLL
QPDFOutlineObjectHelper(QPDFObjectHandle, QPDFOutlineDocumentHelper&, int);
class Members
{
friend class QPDFOutlineObjectHelper;
public:
QPDF_DLL
~Members();
private:
Members(QPDFOutlineDocumentHelper& dh);
Members(Members const&);
QPDFOutlineDocumentHelper& dh;
PointerHolder<QPDFOutlineObjectHelper> parent;
std::list<QPDFOutlineObjectHelper> kids;
};
PointerHolder<Members> m;
};
#endif // QPDFOUTLINEOBJECTHELPER_HH

View File

@ -0,0 +1,137 @@
#include <qpdf/QPDFOutlineDocumentHelper.hh>
#include <qpdf/QTC.hh>
QPDFOutlineDocumentHelper::Members::~Members()
{
}
QPDFOutlineDocumentHelper::Members::Members()
{
}
QPDFOutlineDocumentHelper::QPDFOutlineDocumentHelper(QPDF& qpdf) :
QPDFDocumentHelper(qpdf),
m(new Members())
{
QPDFObjectHandle root = qpdf.getRoot();
if (! root.hasKey("/Outlines"))
{
return;
}
QPDFObjectHandle outlines = root.getKey("/Outlines");
if (! (outlines.isDictionary() && outlines.hasKey("/First")))
{
return;
}
QPDFObjectHandle cur = outlines.getKey("/First");
while (! cur.isNull())
{
this->m->outlines.push_back(
QPDFOutlineObjectHelper::Accessor::create(cur, *this, 1));
cur = cur.getKey("/Next");
}
}
QPDFOutlineDocumentHelper::~QPDFOutlineDocumentHelper()
{
}
bool
QPDFOutlineDocumentHelper::hasOutlines()
{
return ! this->m->outlines.empty();
}
std::list<QPDFOutlineObjectHelper>
QPDFOutlineDocumentHelper::getTopLevelOutlines()
{
return this->m->outlines;
}
void
QPDFOutlineDocumentHelper::initializeByPage()
{
std::list<QPDFOutlineObjectHelper> queue;
queue.insert(queue.end(), this->m->outlines.begin(), this->m->outlines.end());
while (! queue.empty())
{
QPDFOutlineObjectHelper oh = queue.front();
queue.pop_front();
this->m->by_page[oh.getDestPage().getObjGen()].push_back(oh);
std::list<QPDFOutlineObjectHelper> kids = oh.getKids();
queue.insert(queue.end(), kids.begin(), kids.end());
}
}
std::list<QPDFOutlineObjectHelper>
QPDFOutlineDocumentHelper::getOutlinesForPage(QPDFObjGen const& og)
{
if (this->m->by_page.empty())
{
initializeByPage();
}
std::list<QPDFOutlineObjectHelper> result;
if (this->m->by_page.count(og))
{
result = this->m->by_page[og];
}
return result;
}
QPDFObjectHandle
QPDFOutlineDocumentHelper::resolveNamedDest(QPDFObjectHandle name)
{
QPDFObjectHandle result;
if (name.isName())
{
if (! this->m->dest_dict.isInitialized())
{
this->m->dest_dict = this->qpdf.getRoot().getKey("/Dests");
}
if (this->m->dest_dict.isDictionary())
{
QTC::TC("qpdf", "QPDFOutlineDocumentHelper name named dest");
result = this->m->dest_dict.getKey(name.getName());
}
}
else if (name.isString())
{
if (0 == this->m->names_dest.getPointer())
{
QPDFObjectHandle names = this->qpdf.getRoot().getKey("/Names");
if (names.isDictionary())
{
QPDFObjectHandle dests = names.getKey("/Dests");
if (dests.isDictionary())
{
this->m->names_dest =
new QPDFNameTreeObjectHelper(dests);
}
}
}
if (this->m->names_dest.getPointer())
{
if (this->m->names_dest->findObject(name.getUTF8Value(), result))
{
QTC::TC("qpdf", "QPDFOutlineDocumentHelper string named dest");
}
}
}
if (! result.isInitialized())
{
result = QPDFObjectHandle::newNull();
}
return result;
}
bool
QPDFOutlineDocumentHelper::checkSeen(QPDFObjGen const& og)
{
if (this->m->seen.count(og) > 0)
{
return true;
}
this->m->seen.insert(og);
return false;
}

View File

@ -0,0 +1,117 @@
#include <qpdf/QPDFOutlineObjectHelper.hh>
#include <qpdf/QPDFOutlineDocumentHelper.hh>
#include <qpdf/QTC.hh>
QPDFOutlineObjectHelper::Members::~Members()
{
}
QPDFOutlineObjectHelper::Members::Members(QPDFOutlineDocumentHelper& dh) :
dh(dh)
{
}
QPDFOutlineObjectHelper::QPDFOutlineObjectHelper(
QPDFObjectHandle oh, QPDFOutlineDocumentHelper& dh, int depth) :
QPDFObjectHelper(oh),
m(new Members(dh))
{
if (depth > 50)
{
// Not exercised in test suite, but was tested manually by
// temporarily changing max depth to 1.
return;
}
if (QPDFOutlineDocumentHelper::Accessor::checkSeen(
this->m->dh, this->oh.getObjGen()))
{
QTC::TC("qpdf", "QPDFOutlineObjectHelper loop");
return;
}
QPDFObjectHandle cur = oh.getKey("/First");
while (! cur.isNull())
{
QPDFOutlineObjectHelper new_ooh(cur, dh, 1 + depth);
new_ooh.m->parent = new QPDFOutlineObjectHelper(*this);
this->m->kids.push_back(new_ooh);
cur = cur.getKey("/Next");
}
}
PointerHolder<QPDFOutlineObjectHelper>
QPDFOutlineObjectHelper::getParent()
{
return this->m->parent;
}
std::list<QPDFOutlineObjectHelper>
QPDFOutlineObjectHelper::getKids()
{
return this->m->kids;
}
QPDFObjectHandle
QPDFOutlineObjectHelper::getDest()
{
QPDFObjectHandle dest;
QPDFObjectHandle A;
if (this->oh.hasKey("/Dest"))
{
QTC::TC("qpdf", "QPDFOutlineObjectHelper direct dest");
dest = this->oh.getKey("/Dest");
}
else if ((A = this->oh.getKey("/A")).isDictionary() &&
A.getKey("/S").isName() &&
(A.getKey("/S").getName() == "/GoTo") &&
A.hasKey("/D"))
{
QTC::TC("qpdf", "QPDFOutlineObjectHelper action dest");
dest = A.getKey("/D");
}
if (! dest.isInitialized())
{
dest = QPDFObjectHandle::newNull();
}
if (dest.isName() || dest.isString())
{
QTC::TC("qpdf", "QPDFOutlineObjectHelper named dest");
dest = this->m->dh.resolveNamedDest(dest);
}
return dest;
}
QPDFObjectHandle
QPDFOutlineObjectHelper::getDestPage()
{
QPDFObjectHandle dest = getDest();
if ((dest.isArray()) && (dest.getArrayNItems() > 0))
{
return dest.getArrayItem(0);
}
return QPDFObjectHandle::newNull();
}
int
QPDFOutlineObjectHelper::getCount()
{
int count = 0;
if (this->oh.hasKey("/Count"))
{
count = this->oh.getKey("/Count").getIntValue();
}
return count;
}
std::string
QPDFOutlineObjectHelper::getTitle()
{
std::string result;
if (this->oh.hasKey("/Title"))
{
result = this->oh.getKey("/Title").getUTF8Value();
}
return result;
}

View File

@ -46,6 +46,8 @@ SRCS_libqpdf = \
libqpdf/QPDFObjGen.cc \
libqpdf/QPDFObject.cc \
libqpdf/QPDFObjectHandle.cc \
libqpdf/QPDFOutlineDocumentHelper.cc \
libqpdf/QPDFOutlineObjectHelper.cc \
libqpdf/QPDFPageDocumentHelper.cc \
libqpdf/QPDFPageLabelDocumentHelper.cc \
libqpdf/QPDFPageObjectHelper.cc \

View File

@ -361,3 +361,9 @@ qpdf keep files open n 0
qpdf keep files open y 0
qpdf don't disable keep files open 0
QPDFPageLabelDocumentHelper skip first 0
QPDFOutlineObjectHelper direct dest 0
QPDFOutlineObjectHelper action dest 0
QPDFOutlineObjectHelper named dest 0
QPDFOutlineDocumentHelper name named dest 0
QPDFOutlineDocumentHelper string named dest 0
QPDFOutlineObjectHelper loop 0

View File

@ -256,6 +256,24 @@ $td->runtest("no page labels",
{$td->FILE => "no-page-labels.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
show_ntests();
# ----------
$td->notify("--- Outlines ---");
my @outline_files = (
'page-labels-and-outlines',
'outlines-with-actions',
'outlines-with-old-root-dests',
'outlines-with-loop',
);
$n_tests += scalar(@outline_files);
foreach my $f (@outline_files)
{
$td->runtest("outlines: $f",
{$td->COMMAND => "test_driver 49 $f.pdf"},
{$td->FILE => "$f.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
}
show_ntests();
# ----------
$td->notify("--- Page API Tests ---");

View File

@ -0,0 +1,12 @@
page 0: Merschqaberschq (A) 1.2.2 -> 0: /XYZ null null null -> [ 6 0 R /XYZ null null null ]
page 1: Gabeebeebee (name) 1.2.1 -> 1: /FitR 66 714 180 770 -> [ 7 0 R /FitR 66 714 180 770 ]
page 5: Potato 1 -> 5: /XYZ null null null -> [ 11 0 R /XYZ null null null ]
page 11: Mern 1.1 -> 11: /Fit -> [ 17 0 R /Fit ]
page 12: Biherbadem 1.1.1 -> 12: /FitV 100 -> [ 18 0 R /FitV 100 ]
page 12: Gawehwehweh 1.1.2 -> 12: /XYZ null null null -> [ 18 0 R /XYZ null null null ]
page 13: Squash ÷πʬ÷ 1.2 -> 13: /FitH 792 -> [ 19 0 R /FitH 792 ]
page 15: Salad 2 -> 15: /XYZ 66 756 3 -> [ 21 0 R /XYZ 66 756 3 ]
page 18: Glarpenspliel (A, name) 1.1.1.1 -> 18: /XYZ null null null -> [ 24 0 R /XYZ null null null ]
page 19: Hagoogamagoogle 1.1.1.2 -> 19: /XYZ null null null -> [ 25 0 R /XYZ null null null ]
page 22: Jawarnianbvarwash 1.1.2.1 -> 22: /XYZ null null null -> [ 28 0 R /XYZ null null null ]
test 49 done

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
page 5: Potato 1 -> 5: /XYZ null null null -> [ 11 0 R /XYZ null null null ]
page 5: Potato 1 -> 5: /XYZ null null null -> [ 11 0 R /XYZ null null null ]
page 11: Mern 1.1 -> 11: /Fit -> [ 17 0 R /Fit ]
page 12: Biherbadem 1.1.1 -> 12: /FitV 100 -> [ 18 0 R /FitV 100 ]
page 12: Gawehwehweh 1.1.2 -> 12: /XYZ null null null -> [ 18 0 R /XYZ null null null ]
page 15: Salad 2 -> 15: /XYZ 66 756 3 -> [ 21 0 R /XYZ 66 756 3 ]
page 15: Salad 2 -> 15: /XYZ 66 756 3 -> [ 21 0 R /XYZ 66 756 3 ]
page 18: Glarpenspliel (A, name) 1.1.1.1 -> 18: /XYZ null null null -> [ 24 0 R /XYZ null null null ]
page 19: Hagoogamagoogle 1.1.1.2 -> 19: /XYZ null null null -> [ 25 0 R /XYZ null null null ]
page 22: Jawarnianbvarwash 1.1.2.1 -> 22: /XYZ null null null -> [ 28 0 R /XYZ null null null ]
test 49 done

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
page 0: •Merschqaberschq (A) 1.2.2 -> 0: /XYZ null null null -> [ 6 0 R /XYZ null null null ]
page 1: •Gabeebeebee (name) 1.2.1 -> 1: /FitR 66 714 180 770 -> [ 7 0 R /FitR 66 714 180 770 ]
page 5: •Potato 1 -> 5: /XYZ null null null -> [ 11 0 R /XYZ null null null ]
page 11: •Mern 1.1 -> 11: /Fit -> [ 17 0 R /Fit ]
page 12: •Biherbadem 1.1.1 -> 12: /FitV 100 -> [ 18 0 R /FitV 100 ]
page 12: •Gawehwehweh 1.1.2 -> 12: /XYZ null null null -> [ 18 0 R /XYZ null null null ]
page 13: •Squash ÷πʬ÷ 1.2 -> 13: /FitH 792 -> [ 19 0 R /FitH 792 ]
page 15: •Salad 2 -> 15: /XYZ 66 756 3 -> [ 21 0 R /XYZ 66 756 3 ]
page 18: •Glarpenspliel (A, name) 1.1.1.1 -> 18: /XYZ null null null -> [ 24 0 R /XYZ null null null ]
page 19: •Hagoogamagoogle 1.1.1.2 -> 19: /XYZ null null null -> [ 25 0 R /XYZ null null null ]
page 22: •Jawarnianbvarwash 1.1.2.1 -> 22: /XYZ null null null -> [ 28 0 R /XYZ null null null ]
test 49 done

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
page 0: Trepsicle 1.2.2 -> 0: /XYZ null null null -> [ 3 0 R /XYZ null null null ]
page 1: Trepsichord 1.2.1 -> 1: /FitR 66 714 180 770 -> [ 4 0 R /FitR 66 714 180 770 ]
page 5: Isís 1 -> 5: /XYZ null null null -> [ 8 0 R /XYZ null null null ]
page 11: Amanda 1.1 -> 11: /Fit -> [ 14 0 R /Fit ]
page 12: Isosicle 1.1.1 -> 12: /FitV 100 -> [ 15 0 R /FitV 100 ]
page 12: Isosicle 1.1.2 -> 12: /XYZ null null null -> [ 15 0 R /XYZ null null null ]
page 13: Sandy ÷Σανδι÷ 1.2 -> 13: /FitH 792 -> [ 16 0 R /FitH 792 ]
page 15: Trepak 2 -> 15: /XYZ 66 756 3 -> [ 18 0 R /XYZ 66 756 3 ]
page 18: Isosicle 1.1.1.1 -> 18: /XYZ null null null -> [ 21 0 R /XYZ null null null ]
page 19: Isosicle 1.1.1.2 -> 19: /XYZ null null null -> [ 22 0 R /XYZ null null null ]
page 22: Isosicle 1.1.2.1 -> 22: /XYZ null null null -> [ 25 0 R /XYZ null null null ]
test 49 done

View File

@ -9,6 +9,7 @@
#include <qpdf/QPDFNumberTreeObjectHelper.hh>
#include <qpdf/QPDFNameTreeObjectHelper.hh>
#include <qpdf/QPDFPageLabelDocumentHelper.hh>
#include <qpdf/QPDFOutlineDocumentHelper.hh>
#include <qpdf/QUtil.hh>
#include <qpdf/QTC.hh>
#include <qpdf/Pl_StdioFile.hh>
@ -1730,6 +1731,29 @@ void runtest(int n, char const* filename1, char const* arg2)
assert(ntoh.findObject("07 sev\xe2\x80\xa2n", oh));
assert("seven!" == oh.getStringValue());
}
else if (n == 49)
{
// Outlines
std::vector<QPDFPageObjectHelper> pages =
QPDFPageDocumentHelper(pdf).getAllPages();
QPDFOutlineDocumentHelper odh(pdf);
int pageno = 0;
for (std::vector<QPDFPageObjectHelper>::iterator iter = pages.begin();
iter != pages.end(); ++iter, ++pageno)
{
std::list<QPDFOutlineObjectHelper> outlines =
odh.getOutlinesForPage((*iter).getObjectHandle().getObjGen());
for (std::list<QPDFOutlineObjectHelper>::iterator oiter =
outlines.begin();
oiter != outlines.end(); ++oiter)
{
std::cout
<< "page " << pageno << ": "
<< (*oiter).getTitle() << " -> "
<< (*oiter).getDest().unparseResolved() << std::endl;
}
}
}
else
{
throw std::runtime_error(std::string("invalid test ") +