mirror of
https://github.com/qpdf/qpdf.git
synced 2024-11-01 03:12:29 +00:00
Add QPDFPageLabelDocumentHelper
This commit is contained in:
parent
f38df27aa3
commit
6ef9e31233
@ -1,5 +1,12 @@
|
|||||||
2018-12-18 Jay Berkenbilt <ejb@ql.org>
|
2018-12-18 Jay Berkenbilt <ejb@ql.org>
|
||||||
|
|
||||||
|
* Add QPDFPageLabelDocumentHelper class. This is a document helper
|
||||||
|
class that provides useful methods for dealing with page labels.
|
||||||
|
It abstracts the fact that they are stored as number trees and
|
||||||
|
deals with interpolating intermediate values that are not in the
|
||||||
|
tree. It also has helper functions used by the qpdf command line
|
||||||
|
tool to preserve page labels when merging and splitting files.
|
||||||
|
|
||||||
* Add QPDFNumberTreeObjectHelper class. This class provides useful
|
* Add QPDFNumberTreeObjectHelper class. This class provides useful
|
||||||
methods for dealing with number trees, which are discussed in
|
methods for dealing with number trees, which are discussed in
|
||||||
section 7.9.7 of the PDF spec (ISO-32000). Page label dictionaries
|
section 7.9.7 of the PDF spec (ISO-32000). Page label dictionaries
|
||||||
|
100
include/qpdf/QPDFPageLabelDocumentHelper.hh
Normal file
100
include/qpdf/QPDFPageLabelDocumentHelper.hh
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// 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 QPDFPAGELABELDOCUMENTHELPER_HH
|
||||||
|
#define QPDFPAGELABELDOCUMENTHELPER_HH
|
||||||
|
|
||||||
|
#include <qpdf/QPDFDocumentHelper.hh>
|
||||||
|
|
||||||
|
#include <qpdf/QPDF.hh>
|
||||||
|
#include <qpdf/QPDFNumberTreeObjectHelper.hh>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <qpdf/DLL.h>
|
||||||
|
|
||||||
|
// Page labels are discussed in the PDF spec (ISO-32000) in section
|
||||||
|
// 12.4.2.
|
||||||
|
//
|
||||||
|
// Page labels are implemented as a number tree. Each key is a page
|
||||||
|
// index, numbered from 0. The values are dictionaries with the
|
||||||
|
// following keys, all optional:
|
||||||
|
//
|
||||||
|
// * /Type: if present, must be /PageLabel
|
||||||
|
// * /S: one of /D, /R, /r, /A, or /a for decimal, upper-case and
|
||||||
|
// lower-case Roman numeral, or upper-case and lower-case alphabetic
|
||||||
|
// * /P: if present, a fixed prefix string that is prepended to each
|
||||||
|
// page number
|
||||||
|
// * /St: the starting number, or 1 if not specified
|
||||||
|
|
||||||
|
class QPDFPageLabelDocumentHelper: public QPDFDocumentHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QPDF_DLL
|
||||||
|
QPDFPageLabelDocumentHelper(QPDF&);
|
||||||
|
|
||||||
|
QPDF_DLL
|
||||||
|
bool hasPageLabels();
|
||||||
|
|
||||||
|
// Return a page label dictionary representing the page label for
|
||||||
|
// the given page. The page does not need to appear explicitly in
|
||||||
|
// the page label dictionary. This method will adjust /St as
|
||||||
|
// needed to produce a label that is suitable for the page.
|
||||||
|
QPDF_DLL
|
||||||
|
QPDFObjectHandle getLabelForPage(long long page_idx);
|
||||||
|
|
||||||
|
// Append to the incoming vector a list of objects suitable for
|
||||||
|
// inclusion in a /PageLabels dictionary's /Nums field. start_idx
|
||||||
|
// and end_idx are the indexes to the starting and ending pages
|
||||||
|
// (inclusive) in the original file, and new_start_idx is the
|
||||||
|
// index to the first page in the new file. For example, if pages
|
||||||
|
// 10 through 12 of one file are being copied to a new file as
|
||||||
|
// pages 6 through 8, you would call getLabelsForPageRange(10, 12,
|
||||||
|
// 6), which would return as many entries as are required to add
|
||||||
|
// to the new file's PageLabels. This method fabricates a suitable
|
||||||
|
// entry even if the original document has no page labels. This
|
||||||
|
// behavior facilitates using this function to incrementally build
|
||||||
|
// up a page labels tree when merging files.
|
||||||
|
QPDF_DLL
|
||||||
|
void
|
||||||
|
getLabelsForPageRange(long long start_idx, long long end_idx,
|
||||||
|
long long new_start_idx,
|
||||||
|
std::vector<QPDFObjectHandle>& new_labels);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Members
|
||||||
|
{
|
||||||
|
friend class QPDFPageLabelDocumentHelper;
|
||||||
|
|
||||||
|
public:
|
||||||
|
QPDF_DLL
|
||||||
|
~Members();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Members();
|
||||||
|
Members(Members const&);
|
||||||
|
|
||||||
|
PointerHolder<QPDFNumberTreeObjectHelper> labels;
|
||||||
|
};
|
||||||
|
|
||||||
|
PointerHolder<Members> m;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // QPDFPAGELABELDOCUMENTHELPER_HH
|
125
libqpdf/QPDFPageLabelDocumentHelper.cc
Normal file
125
libqpdf/QPDFPageLabelDocumentHelper.cc
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#include <qpdf/QPDFPageLabelDocumentHelper.hh>
|
||||||
|
#include <qpdf/QTC.hh>
|
||||||
|
|
||||||
|
QPDFPageLabelDocumentHelper::Members::~Members()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QPDFPageLabelDocumentHelper::Members::Members()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QPDFPageLabelDocumentHelper::QPDFPageLabelDocumentHelper(QPDF& qpdf) :
|
||||||
|
QPDFDocumentHelper(qpdf),
|
||||||
|
m(new Members())
|
||||||
|
{
|
||||||
|
QPDFObjectHandle root = qpdf.getRoot();
|
||||||
|
if (root.hasKey("/PageLabels"))
|
||||||
|
{
|
||||||
|
this->m->labels = new QPDFNumberTreeObjectHelper(
|
||||||
|
root.getKey("/PageLabels"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
QPDFPageLabelDocumentHelper::hasPageLabels()
|
||||||
|
{
|
||||||
|
return 0 != this->m->labels.getPointer();
|
||||||
|
}
|
||||||
|
|
||||||
|
QPDFObjectHandle
|
||||||
|
QPDFPageLabelDocumentHelper::getLabelForPage(long long page_idx)
|
||||||
|
{
|
||||||
|
QPDFObjectHandle result(QPDFObjectHandle::newNull());
|
||||||
|
if (! hasPageLabels())
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
QPDFNumberTreeObjectHelper::numtree_number offset = 0;
|
||||||
|
QPDFObjectHandle label;
|
||||||
|
if (! this->m->labels->findObjectAtOrBelow(page_idx, label, offset))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (! label.isDictionary())
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
QPDFObjectHandle S = label.getKey("/S"); // type (D, R, r, A, a)
|
||||||
|
QPDFObjectHandle P = label.getKey("/P"); // prefix
|
||||||
|
QPDFObjectHandle St = label.getKey("/St"); // starting number
|
||||||
|
long long start = 1;
|
||||||
|
if (St.isInteger())
|
||||||
|
{
|
||||||
|
start = St.getIntValue();
|
||||||
|
}
|
||||||
|
start += offset;
|
||||||
|
result = QPDFObjectHandle::newDictionary();
|
||||||
|
result.replaceOrRemoveKey("/S", S);
|
||||||
|
result.replaceOrRemoveKey("/P", P);
|
||||||
|
result.replaceOrRemoveKey("/St", QPDFObjectHandle::newInteger(start));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
QPDFPageLabelDocumentHelper::getLabelsForPageRange(
|
||||||
|
long long start_idx, long long end_idx, long long new_start_idx,
|
||||||
|
std::vector<QPDFObjectHandle>& new_labels)
|
||||||
|
{
|
||||||
|
// Start off with a suitable label for the first page. For every
|
||||||
|
// remaining page, if that page has an explicit entry, copy it.
|
||||||
|
// Otherwise, let the subsequent page just sequence from the prior
|
||||||
|
// entry. If there is no entry for the first page, fabricate one
|
||||||
|
// that would match how the page would look in a new file in which
|
||||||
|
// it also didn't have an explicit label.
|
||||||
|
QPDFObjectHandle label = getLabelForPage(start_idx);
|
||||||
|
if (label.isNull())
|
||||||
|
{
|
||||||
|
label = QPDFObjectHandle::newDictionary();
|
||||||
|
label.replaceKey(
|
||||||
|
"/St", QPDFObjectHandle::newInteger(1 + new_start_idx));
|
||||||
|
}
|
||||||
|
// See if the new label is redundant based on the previous entry
|
||||||
|
// in the vector. If so, don't add it.
|
||||||
|
size_t size = new_labels.size();
|
||||||
|
bool skip_first = false;
|
||||||
|
if (size >= 2)
|
||||||
|
{
|
||||||
|
QPDFObjectHandle last = new_labels[size - 1];
|
||||||
|
QPDFObjectHandle last_idx = new_labels[size - 2];
|
||||||
|
if (last_idx.isInteger() && last.isDictionary() &&
|
||||||
|
(label.getKey("/S").unparse() == last.getKey("/S").unparse()) &&
|
||||||
|
(label.getKey("/P").unparse() == last.getKey("/P").unparse()) &&
|
||||||
|
label.getKey("/St").isInteger() &&
|
||||||
|
last.getKey("/St").isInteger())
|
||||||
|
{
|
||||||
|
long long int st_delta =
|
||||||
|
label.getKey("/St").getIntValue() -
|
||||||
|
last.getKey("/St").getIntValue();
|
||||||
|
long long int idx_delta =
|
||||||
|
new_start_idx - last_idx.getIntValue();
|
||||||
|
if (st_delta == idx_delta)
|
||||||
|
{
|
||||||
|
QTC::TC("qpdf", "QPDFPageLabelDocumentHelper skip first");
|
||||||
|
skip_first = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (! skip_first)
|
||||||
|
{
|
||||||
|
new_labels.push_back(QPDFObjectHandle::newInteger(new_start_idx));
|
||||||
|
new_labels.push_back(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
long long int idx_offset = new_start_idx - start_idx;
|
||||||
|
for (long long i = start_idx + 1; i <= end_idx; ++i)
|
||||||
|
{
|
||||||
|
if (this->m->labels->hasIndex(i) &&
|
||||||
|
(label = getLabelForPage(i)).isDictionary())
|
||||||
|
{
|
||||||
|
new_labels.push_back(QPDFObjectHandle::newInteger(i + idx_offset));
|
||||||
|
new_labels.push_back(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,7 @@ SRCS_libqpdf = \
|
|||||||
libqpdf/QPDFObject.cc \
|
libqpdf/QPDFObject.cc \
|
||||||
libqpdf/QPDFObjectHandle.cc \
|
libqpdf/QPDFObjectHandle.cc \
|
||||||
libqpdf/QPDFPageDocumentHelper.cc \
|
libqpdf/QPDFPageDocumentHelper.cc \
|
||||||
|
libqpdf/QPDFPageLabelDocumentHelper.cc \
|
||||||
libqpdf/QPDFPageObjectHelper.cc \
|
libqpdf/QPDFPageObjectHelper.cc \
|
||||||
libqpdf/QPDFSystemError.cc \
|
libqpdf/QPDFSystemError.cc \
|
||||||
libqpdf/QPDFTokenizer.cc \
|
libqpdf/QPDFTokenizer.cc \
|
||||||
|
@ -234,6 +234,24 @@ $td->runtest("number trees",
|
|||||||
{$td->FILE => "number-tree.out", $td->EXIT_STATUS => 0},
|
{$td->FILE => "number-tree.out", $td->EXIT_STATUS => 0},
|
||||||
$td->NORMALIZE_NEWLINES);
|
$td->NORMALIZE_NEWLINES);
|
||||||
|
|
||||||
|
show_ntests();
|
||||||
|
# ----------
|
||||||
|
$td->notify("--- Page Labels ---");
|
||||||
|
$n_tests += 3;
|
||||||
|
|
||||||
|
$td->runtest("complex page labels",
|
||||||
|
{$td->COMMAND => "test_driver 47 page-labels-num-tree.pdf"},
|
||||||
|
{$td->FILE => "page-labels-num-tree.out", $td->EXIT_STATUS => 0},
|
||||||
|
$td->NORMALIZE_NEWLINES);
|
||||||
|
$td->runtest("no zero entry for page labels",
|
||||||
|
{$td->COMMAND => "test_driver 47 page-labels-no-zero.pdf"},
|
||||||
|
{$td->FILE => "page-labels-no-zero.out", $td->EXIT_STATUS => 0},
|
||||||
|
$td->NORMALIZE_NEWLINES);
|
||||||
|
$td->runtest("no page labels",
|
||||||
|
{$td->COMMAND => "test_driver 47 minimal.pdf"},
|
||||||
|
{$td->FILE => "no-page-labels.out", $td->EXIT_STATUS => 0},
|
||||||
|
$td->NORMALIZE_NEWLINES);
|
||||||
|
|
||||||
show_ntests();
|
show_ntests();
|
||||||
# ----------
|
# ----------
|
||||||
$td->notify("--- Page API Tests ---");
|
$td->notify("--- Page API Tests ---");
|
||||||
|
2
qpdf/qtest/qpdf/no-page-labels.out
Normal file
2
qpdf/qtest/qpdf/no-page-labels.out
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
1 << /St 2 >>
|
||||||
|
test 47 done
|
15
qpdf/qtest/qpdf/page-labels-no-zero.out
Normal file
15
qpdf/qtest/qpdf/page-labels-no-zero.out
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
1 << /St 2 >>
|
||||||
|
3 << /P (blank) /St 1 >>
|
||||||
|
4 << /P (X-) /S /A /St 17 >>
|
||||||
|
6 << /P () /St 1 >>
|
||||||
|
7 << /S /R /St 3 >>
|
||||||
|
10 << /S /D /St 1 >>
|
||||||
|
12 << /S /a /St 1 >>
|
||||||
|
13 << /S /a /St 3 >>
|
||||||
|
16 << /P (q.) /S /D /St 6 >>
|
||||||
|
20 << /P (www) /St 1 >>
|
||||||
|
21 << /S /D /St 12 >>
|
||||||
|
23 << /S /D /St 16059 >>
|
||||||
|
24 << /S /R /St 50 >>
|
||||||
|
30 << /S /r /St 54 >>
|
||||||
|
test 47 done
|
1364
qpdf/qtest/qpdf/page-labels-no-zero.pdf
Normal file
1364
qpdf/qtest/qpdf/page-labels-no-zero.pdf
Normal file
File diff suppressed because it is too large
Load Diff
15
qpdf/qtest/qpdf/page-labels-num-tree.out
Normal file
15
qpdf/qtest/qpdf/page-labels-num-tree.out
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
1 << /S /r /St 1 >>
|
||||||
|
3 << /P (blank) /St 1 >>
|
||||||
|
4 << /P (X-) /S /A /St 17 >>
|
||||||
|
6 << /P () /St 1 >>
|
||||||
|
7 << /S /R /St 3 >>
|
||||||
|
10 << /S /D /St 1 >>
|
||||||
|
12 << /S /a /St 1 >>
|
||||||
|
13 << /S /a /St 3 >>
|
||||||
|
16 << /P (q.) /S /D /St 6 >>
|
||||||
|
20 << /P (www) /St 1 >>
|
||||||
|
21 << /S /D /St 12 >>
|
||||||
|
23 << /S /D /St 16059 >>
|
||||||
|
24 << /S /R /St 50 >>
|
||||||
|
30 << /S /r /St 54 >>
|
||||||
|
test 47 done
|
1417
qpdf/qtest/qpdf/page-labels-num-tree.pdf
Normal file
1417
qpdf/qtest/qpdf/page-labels-num-tree.pdf
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@
|
|||||||
#include <qpdf/QPDFPageObjectHelper.hh>
|
#include <qpdf/QPDFPageObjectHelper.hh>
|
||||||
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
|
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
|
||||||
#include <qpdf/QPDFNumberTreeObjectHelper.hh>
|
#include <qpdf/QPDFNumberTreeObjectHelper.hh>
|
||||||
|
#include <qpdf/QPDFPageLabelDocumentHelper.hh>
|
||||||
#include <qpdf/QUtil.hh>
|
#include <qpdf/QUtil.hh>
|
||||||
#include <qpdf/QTC.hh>
|
#include <qpdf/QTC.hh>
|
||||||
#include <qpdf/Pl_StdioFile.hh>
|
#include <qpdf/Pl_StdioFile.hh>
|
||||||
@ -1690,6 +1691,21 @@ void runtest(int n, char const* filename1, char const* arg2)
|
|||||||
assert("six" == oh.getStringValue());
|
assert("six" == oh.getStringValue());
|
||||||
assert(2 == offset);
|
assert(2 == offset);
|
||||||
}
|
}
|
||||||
|
else if (n == 47)
|
||||||
|
{
|
||||||
|
// Test page labels.
|
||||||
|
QPDFPageLabelDocumentHelper pldh(pdf);
|
||||||
|
size_t npages = pdf.getRoot().getKey("/Pages").
|
||||||
|
getKey("/Count").getIntValue();
|
||||||
|
std::vector<QPDFObjectHandle> labels;
|
||||||
|
pldh.getLabelsForPageRange(0, npages - 1, 1, labels);
|
||||||
|
assert(labels.size() % 2 == 0);
|
||||||
|
for (size_t i = 0; i < labels.size(); i+= 2)
|
||||||
|
{
|
||||||
|
std::cout << labels.at(i).getIntValue() << " "
|
||||||
|
<< labels.at(i+1).unparse() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw std::runtime_error(std::string("invalid test ") +
|
throw std::runtime_error(std::string("invalid test ") +
|
||||||
|
Loading…
Reference in New Issue
Block a user