Add page rotation (fixes #132)

This commit is contained in:
Jay Berkenbilt 2017-08-12 13:22:46 -04:00
parent d926d78059
commit cfa2eb97fb
9 changed files with 1126 additions and 0 deletions

View File

@ -1,5 +1,9 @@
2017-08-12 Jay Berkenbilt <ejb@ql.org>
* Add QPDFObjectHandle::rotatePage to apply rotation to a page
object. Add --rotate option to qpdf to specify page rotation from
the command line.
* Provide --verbose option that causes qpdf to print an indication
of what files it is writing.

View File

@ -507,6 +507,13 @@ class QPDFObjectHandle
QPDF_DLL
void addPageContents(QPDFObjectHandle contents, bool first);
// Rotate a page. If relative is false, set the rotation of the
// page to angle. Otherwise, add angle to the rotation of the
// page. Angle must be a multiple of 90. Adding 90 to the rotation
// rotates clockwise by 90 degrees.
QPDF_DLL
void rotatePage(int angle, bool relative);
// Initializers for objects. This Factory class gives the QPDF
// class specific permission to call factory methods without
// making it a friend of the whole QPDFObjectHandle class.

View File

@ -673,6 +673,62 @@ QPDFObjectHandle::addPageContents(QPDFObjectHandle new_contents, bool first)
this->replaceKey("/Contents", contents);
}
void
QPDFObjectHandle::rotatePage(int angle, bool relative)
{
assertPageObject();
if ((angle % 90) != 0)
{
throw std::runtime_error(
"QPDF::rotatePage called with an"
" angle that is not a multiple of 90");
}
int new_angle = angle;
if (relative)
{
int old_angle = 0;
bool found_rotate = false;
QPDFObjectHandle cur_obj = *this;
bool searched_parent = false;
std::set<QPDFObjGen> visited;
while (! found_rotate)
{
if (visited.count(cur_obj.getObjGen()))
{
// Don't get stuck in an infinite loop
break;
}
if (! visited.empty())
{
searched_parent = true;
}
visited.insert(cur_obj.getObjGen());
if (cur_obj.getKey("/Rotate").isInteger())
{
found_rotate = true;
old_angle = cur_obj.getKey("/Rotate").getIntValue();
}
else if (cur_obj.getKey("/Parent").isDictionary())
{
cur_obj = cur_obj.getKey("/Parent");
}
else
{
break;
}
}
QTC::TC("qpdf", "QPDFObjectHandle found old angle",
searched_parent ? 0 : 1);
if ((old_angle % 90) != 0)
{
old_angle = 0;
}
new_angle += old_angle;
}
new_angle = (new_angle + 360) % 360;
replaceKey("/Rotate", QPDFObjectHandle::newInteger(new_angle));
}
std::string
QPDFObjectHandle::unparse()
{

View File

@ -348,6 +348,26 @@ make
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--rotate=[+|-]angle:page-range</option></term>
<listitem>
<para>
Apply rotation to specified pages. The
<option>page-range</option> portion of the option value has
the same format as page ranges in <xref
linkend="ref.page-selection"/>. The <option>angle</option>
portion of the parameter may be either 90, 180, or 270. If
preceded by <option>+</option> or <option>-</option>, the
angle is added to or subtracted from the specified pages'
original rotations. Otherwise the pages' rotations are set to
the exact value. For example, the command <command>qpdf in.pdf
out.pdf --rotate=+90:2,4,6 --rotate=180:7-8</command> would
rotate pages 2, 4, and 6 90 degrees clockwise from their
original rotation and force the rotation of pages 7 through 9
to 180 degrees regardless of their original rotation.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--pages options --</option></term>
<listitem>

View File

@ -37,6 +37,18 @@ struct PageSpec
char const* range;
};
struct RotationSpec
{
RotationSpec(int angle = 0, bool relative = false) :
angle(angle),
relative(relative)
{
}
int angle;
bool relative;
};
struct Options
{
Options() :
@ -151,6 +163,7 @@ struct Options
bool show_page_images;
bool check;
std::vector<PageSpec> page_specs;
std::map<std::string, RotationSpec> rotations;
bool require_outfile;
char const* infilename;
char const* outfilename;
@ -209,6 +222,8 @@ Basic Options\n\
--encrypt options -- generate an encrypted file\n\
--decrypt remove any encryption on the file\n\
--pages options -- select specific pages from one or more files\n\
--rotate=[+|-]angle:page-range\n\
rotate each specified page 90, 180, or 270 degrees\n\
--split-pages=[n] write each output page to a separate file\n\
\n\
If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n\
@ -219,6 +234,13 @@ parameters will be copied, including both user and owner passwords, even\n\
if the user password is used to open the other file. This works even if\n\
the owner password is not known.\n\
\n\
The --rotate flag can be used to specify pages to rotate pages either\n\
90, 180, or 270 degrees. The page range is specified in the same\n\
format as with the --pages option, described below. Repeat the option\n\
to rotate multiple groups of pages. If the angle is preceded by + or -,\n\
it is added to or subtracted from the original rotation. Otherwise, the\n\
rotation angle is set explicitly to the given value.\n\
\n\
If --split-pages is specified, each page is written to a separate output\n\
file. File names are generated as follows:\n\
* If the string %d appears in the output file name, it is replaced with a\n\
@ -1148,6 +1170,62 @@ static void handle_help_verison(int argc, char* argv[])
}
}
static void parse_rotation_parameter(Options& o, std::string const& parameter)
{
std::string angle_str;
std::string range;
size_t colon = parameter.find(':');
int relative = 0;
if (colon != std::string::npos)
{
if (colon > 0)
{
angle_str = parameter.substr(0, colon);
if (angle_str.length() > 0)
{
char first = angle_str.at(0);
if ((first == '+') || (first == '-'))
{
relative = ((first == '+') ? 1 : -1);
angle_str = angle_str.substr(1);
}
else if (! QUtil::is_digit(angle_str.at(0)))
{
angle_str = "";
}
}
}
if (colon + 1 < parameter.length())
{
range = parameter.substr(colon + 1);
}
}
bool range_valid = false;
try
{
parse_numrange(range.c_str(), 0, true);
range_valid = true;
}
catch (std::runtime_error)
{
// ignore
}
if (range_valid &&
((angle_str == "90") || (angle_str == "180") || (angle_str == "270")))
{
int angle = atoi(angle_str.c_str());
if (relative == -1)
{
angle = -angle;
}
o.rotations[range] = RotationSpec(angle, (relative != 0));
}
else
{
usage("invalid parameter to rotate: " + parameter);
}
}
static void parse_options(int argc, char* argv[], Options& o)
{
for (int i = 1; i < argc; ++i)
@ -1237,6 +1315,10 @@ static void parse_options(int argc, char* argv[], Options& o)
usage("--pages: no page specifications given");
}
}
else if (strcmp(arg, "rotate") == 0)
{
parse_rotation_parameter(o, parameter);
}
else if (strcmp(arg, "stream-data") == 0)
{
if (parameter == 0)
@ -1829,6 +1911,29 @@ static void handle_page_specs(QPDF& pdf, Options& o,
}
}
static void handle_rotations(QPDF& pdf, Options& o)
{
std::vector<QPDFObjectHandle> pages = pdf.getAllPages();
int npages = static_cast<int>(pages.size());
for (std::map<std::string, RotationSpec>::iterator iter =
o.rotations.begin();
iter != o.rotations.end(); ++iter)
{
std::string const& range = (*iter).first;
RotationSpec const& rspec = (*iter).second;
std::vector<int> to_rotate = parse_numrange(range.c_str(), npages);
for (std::vector<int>::iterator i2 = to_rotate.begin();
i2 != to_rotate.end(); ++i2)
{
int pageno = *i2 - 1;
if ((pageno >= 0) && (pageno < npages))
{
pages.at(pageno).rotatePage(rspec.angle, rspec.relative);
}
}
}
}
static void set_encryption_options(QPDF& pdf, Options& o, QPDFWriter& w)
{
int R = 0;
@ -2124,6 +2229,10 @@ int main(int argc, char* argv[])
{
handle_page_specs(pdf, o, page_heap);
}
if (! o.rotations.empty())
{
handle_rotations(pdf, o);
}
if (o.outfilename == 0)
{

View File

@ -295,3 +295,4 @@ QPDF ignore second extra space in xref entry 0
QPDF ignore length error xref entry 0
QPDF_encryption pad short parameter 0
QPDFWriter ignore self-referential object stream 0
QPDFObjectHandle found old angle 1

View File

@ -778,6 +778,26 @@ foreach my $d (@sp_cases)
}
}
show_ntests();
# ----------
$td->notify("--- Rotate Pages ---");
$n_tests += 2;
# XXX do absolute, positive, and negative on ranges that include
# inherited and non-inherited.
# Pages 11-15 inherit /Rotate 90
# Pages 1 and 2 have explicit /Rotate 270
# Pages 16 and 17 have explicit /Rotate 180
$td->runtest("page rotation",
{$td->COMMAND => "qpdf --static-id to-rotate.pdf a.pdf" .
" --rotate=+90:1,4,11,16" .
" --rotate=180:2,5,12-13" .
" --rotate=-90:3,15,17,18"},
{$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
{$td->FILE => "a.pdf"},
{$td->FILE => "rotated.pdf"});
show_ntests();
# ----------
$td->notify("--- Numeric range parsing tests ---");

BIN
qpdf/qtest/qpdf/rotated.pdf Normal file

Binary file not shown.

View File

@ -0,0 +1,909 @@
%PDF-1.4
%¿÷¢þ
%QDF-1.0
1 0 obj
<<
/Pages 3 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/CreationDate (D:20120721200217)
/Producer (Apex PDFWriter)
>>
endobj
3 0 obj
<<
/Count 20
/Kids [
4 0 R
5 0 R
6 0 R
7 0 R
8 0 R
9 0 R
10 0 R
11 0 R
12 0 R
13 0 R
14 0 R
15 0 R
16 0 R
17 0 R
18 0 R
19 0 R
]
/Type /Pages
>>
endobj
%% Page 1
4 0 obj
<<
/Contents 20 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Rotate 270
/Type /Page
>>
endobj
%% Page 2
5 0 obj
<<
/Contents 23 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Rotate 270
/Type /Page
>>
endobj
%% Page 3
6 0 obj
<<
/Contents 25 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 4
7 0 obj
<<
/Contents 27 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 5
8 0 obj
<<
/Contents 29 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 6
9 0 obj
<<
/Contents 31 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 7
10 0 obj
<<
/Contents 33 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 8
11 0 obj
<<
/Contents 35 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 9
12 0 obj
<<
/Contents 37 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 10
13 0 obj
<<
/Contents 39 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Count 5
/Kids [
41 0 R
42 0 R
43 0 R
44 0 R
45 0 R
]
/Parent 3 0 R
/Rotate 90
/Type /Pages
>>
endobj
%% Page 16
15 0 obj
<<
/Contents 46 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Rotate 180
/Type /Page
>>
endobj
%% Page 17
16 0 obj
<<
/Contents 48 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Rotate 180
/Type /Page
>>
endobj
%% Page 18
17 0 obj
<<
/Contents 50 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 19
18 0 obj
<<
/Contents 52 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 20
19 0 obj
<<
/Contents 54 0 R
/MediaBox [
0
0
612
792
]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Contents for page 1
20 0 obj
<<
/Length 21 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 1) Tj ET
endstream
endobj
21 0 obj
47
endobj
22 0 obj
<<
/BaseFont /Times-Roman
/Encoding /WinAnsiEncoding
/Subtype /Type1
/Type /Font
>>
endobj
%% Contents for page 2
23 0 obj
<<
/Length 24 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 2) Tj ET
endstream
endobj
24 0 obj
47
endobj
%% Contents for page 3
25 0 obj
<<
/Length 26 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 3) Tj ET
endstream
endobj
26 0 obj
47
endobj
%% Contents for page 4
27 0 obj
<<
/Length 28 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 4) Tj ET
endstream
endobj
28 0 obj
47
endobj
%% Contents for page 5
29 0 obj
<<
/Length 30 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 5) Tj ET
endstream
endobj
30 0 obj
47
endobj
%% Contents for page 6
31 0 obj
<<
/Length 32 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 6) Tj ET
endstream
endobj
32 0 obj
47
endobj
%% Contents for page 7
33 0 obj
<<
/Length 34 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 7) Tj ET
endstream
endobj
34 0 obj
47
endobj
%% Contents for page 8
35 0 obj
<<
/Length 36 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 8) Tj ET
endstream
endobj
36 0 obj
47
endobj
%% Contents for page 9
37 0 obj
<<
/Length 38 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 9) Tj ET
endstream
endobj
38 0 obj
47
endobj
%% Contents for page 10
39 0 obj
<<
/Length 40 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 10) Tj ET
endstream
endobj
40 0 obj
48
endobj
%% Page 11
41 0 obj
<<
/Contents 56 0 R
/MediaBox [
0
0
612
792
]
/Parent 14 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 12
42 0 obj
<<
/Contents 58 0 R
/MediaBox [
0
0
612
792
]
/Parent 14 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 13
43 0 obj
<<
/Contents 60 0 R
/MediaBox [
0
0
612
792
]
/Parent 14 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 14
44 0 obj
<<
/Contents 62 0 R
/MediaBox [
0
0
612
792
]
/Parent 14 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Page 15
45 0 obj
<<
/Contents 64 0 R
/MediaBox [
0
0
612
792
]
/Parent 14 0 R
/Resources <<
/Font <<
/F1 22 0 R
>>
/ProcSet [
/PDF
/Text
]
>>
/Type /Page
>>
endobj
%% Contents for page 16
46 0 obj
<<
/Length 47 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 16) Tj ET
endstream
endobj
47 0 obj
48
endobj
%% Contents for page 17
48 0 obj
<<
/Length 49 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 17) Tj ET
endstream
endobj
49 0 obj
48
endobj
%% Contents for page 18
50 0 obj
<<
/Length 51 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 18) Tj ET
endstream
endobj
51 0 obj
48
endobj
%% Contents for page 19
52 0 obj
<<
/Length 53 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 19) Tj ET
endstream
endobj
53 0 obj
48
endobj
%% Contents for page 20
54 0 obj
<<
/Length 55 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 20) Tj ET
endstream
endobj
55 0 obj
48
endobj
%% Contents for page 11
56 0 obj
<<
/Length 57 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 11) Tj ET
endstream
endobj
57 0 obj
48
endobj
%% Contents for page 12
58 0 obj
<<
/Length 59 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 12) Tj ET
endstream
endobj
59 0 obj
48
endobj
%% Contents for page 13
60 0 obj
<<
/Length 61 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 13) Tj ET
endstream
endobj
61 0 obj
48
endobj
%% Contents for page 14
62 0 obj
<<
/Length 63 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 14) Tj ET
endstream
endobj
63 0 obj
48
endobj
%% Contents for page 15
64 0 obj
<<
/Length 65 0 R
>>
stream
BT /F1 15 Tf 72 720 Td (Original page 15) Tj ET
endstream
endobj
65 0 obj
48
endobj
xref
0 66
0000000000 65535 f
0000000025 00000 n
0000000079 00000 n
0000000165 00000 n
0000000408 00000 n
0000000651 00000 n
0000000894 00000 n
0000001123 00000 n
0000001352 00000 n
0000001581 00000 n
0000001810 00000 n
0000002040 00000 n
0000002270 00000 n
0000002501 00000 n
0000002721 00000 n
0000002879 00000 n
0000003124 00000 n
0000003369 00000 n
0000003600 00000 n
0000003831 00000 n
0000004074 00000 n
0000004178 00000 n
0000004198 00000 n
0000004330 00000 n
0000004434 00000 n
0000004477 00000 n
0000004581 00000 n
0000004624 00000 n
0000004728 00000 n
0000004771 00000 n
0000004875 00000 n
0000004918 00000 n
0000005022 00000 n
0000005065 00000 n
0000005169 00000 n
0000005212 00000 n
0000005316 00000 n
0000005359 00000 n
0000005463 00000 n
0000005507 00000 n
0000005612 00000 n
0000005643 00000 n
0000005875 00000 n
0000006107 00000 n
0000006339 00000 n
0000006571 00000 n
0000006816 00000 n
0000006921 00000 n
0000006965 00000 n
0000007070 00000 n
0000007114 00000 n
0000007219 00000 n
0000007263 00000 n
0000007368 00000 n
0000007412 00000 n
0000007517 00000 n
0000007561 00000 n
0000007666 00000 n
0000007710 00000 n
0000007815 00000 n
0000007859 00000 n
0000007964 00000 n
0000008008 00000 n
0000008113 00000 n
0000008157 00000 n
0000008262 00000 n
trailer <<
/Info 2 0 R
/Root 1 0 R
/Size 66
/ID [<e032a88c7a987db6ca3abee555506ccc><0c6f92bbd30230ec62bc832f23eddc35>]
>>
startxref
8282
%%EOF