mirror of
https://github.com/qpdf/qpdf.git
synced 2024-05-29 00:10:54 +00:00
Add page rotation example in contrib
This is added to contrib rather than examples because it requires c++-11 and lacks a test suite, but it is still useful enough to include with the distribution.
This commit is contained in:
parent
841f967a5f
commit
8ee83ca722
|
@ -83,6 +83,12 @@
|
|||
* CVE-2017-9210: Fix infinite loop caused by attempting to unparse
|
||||
an object for inclusion in the text of an exception.
|
||||
|
||||
2016-09-10 Jay Berkenbilt <ejb@ql.org>
|
||||
|
||||
* Include pdf-rotate.cc example in contrib. Thanks Iskander
|
||||
Sharipov <Iskander.Sharipov@tatar.ru> for contributing this
|
||||
program.
|
||||
|
||||
2015-11-10 Jay Berkenbilt <ejb@ql.org>
|
||||
|
||||
* 6.0.0: release
|
||||
|
|
4
README
4
README
|
@ -160,7 +160,9 @@ exercises most of the public interface. There are additional example
|
|||
programs in the examples directory. Reading all the source files in
|
||||
the qpdf directory (including the qpdf command-line tool and some test
|
||||
drivers) along with the code in the examples directory will give you a
|
||||
complete picture of every aspect of the public interface.
|
||||
complete picture of every aspect of the public interface. You may also
|
||||
check programs in the contrib directory. These are not part of QPDF
|
||||
but have been contributed by other developers.
|
||||
|
||||
|
||||
Additional Notes on Test Suite
|
||||
|
|
373
contrib/pdf-rotate.cc
Normal file
373
contrib/pdf-rotate.cc
Normal file
|
@ -0,0 +1,373 @@
|
|||
// This program is not part of QPDF, but it is made available with the
|
||||
// QPDF software under the same terms as QPDF itself.
|
||||
//
|
||||
// Author: Iskander Sharipov <Iskander.Sharipov@tatar.ru>
|
||||
//
|
||||
// This program requires a c++11 compiler but otherwise has no
|
||||
// external dependencies beyond QPDF itself.
|
||||
|
||||
#include <qpdf/QPDF.hh>
|
||||
#include <qpdf/QPDFWriter.hh>
|
||||
|
||||
#include <vector>
|
||||
#include <regex>
|
||||
#include <algorithm>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
using std::vector;
|
||||
|
||||
typedef QPDFObjectHandle QpdfObject;
|
||||
|
||||
/*
|
||||
* Rotates clockwise/counter-clockwise selected pages or page range.
|
||||
* It is also capable of setting absolute page rotation.
|
||||
* Check `usage` for details.
|
||||
*
|
||||
* User should check program return value to handle errors.
|
||||
* Check `ErrorCode` for enumeration and `error_message` for descriptions.
|
||||
*/
|
||||
|
||||
enum class ErrorCode: int {
|
||||
Success, // <- Not an error
|
||||
InvalidArgCount,
|
||||
InputFileNotExist,
|
||||
InvalidDegreeArg,
|
||||
InvalidPageSelectorArg,
|
||||
InvalidRangeSelector,
|
||||
InvalidPagesSelector,
|
||||
BadPageNumber,
|
||||
BadLowRangeBound,
|
||||
BadHighRangeBound,
|
||||
UnexpectedException,
|
||||
InternalRangeError,
|
||||
};
|
||||
|
||||
struct Rotation {
|
||||
Rotation() = default;
|
||||
Rotation(const char* digits);
|
||||
|
||||
int degree;
|
||||
bool absolute;
|
||||
};
|
||||
|
||||
struct Arguments {
|
||||
const char* in_file_name;
|
||||
const char* out_file_name;
|
||||
char* pages = nullptr;
|
||||
const char* range = nullptr;
|
||||
Rotation degree;
|
||||
};
|
||||
|
||||
void print_error(ErrorCode code);
|
||||
Arguments parse_arguments(int argc, char* argv[]);
|
||||
void rotate_pages(QPDF& in, QPDF& out, char* selector, Rotation);
|
||||
void rotate_page_range(QPDF& in, QPDF& out, const char* range, Rotation);
|
||||
void rotate_all_pages(QPDF& in, QPDF& out, Rotation);
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
try {
|
||||
Arguments args = parse_arguments(argc, argv);
|
||||
|
||||
QPDF in_pdf;
|
||||
in_pdf.processFile(args.in_file_name);
|
||||
QPDF out_pdf;
|
||||
out_pdf.emptyPDF();
|
||||
|
||||
if (args.pages) {
|
||||
rotate_pages(in_pdf, out_pdf, args.pages, args.degree);
|
||||
} else if (args.range) {
|
||||
rotate_page_range(in_pdf, out_pdf, args.range, args.degree);
|
||||
} else {
|
||||
rotate_all_pages(in_pdf, out_pdf, args.degree);
|
||||
}
|
||||
|
||||
QPDFWriter out_writer{out_pdf, args.out_file_name};
|
||||
out_writer.write();
|
||||
} catch (ErrorCode e) {
|
||||
print_error(e);
|
||||
return static_cast<int>(e);
|
||||
} catch (...) {
|
||||
print_error(ErrorCode::UnexpectedException);
|
||||
return static_cast<int>(ErrorCode::UnexpectedException);
|
||||
}
|
||||
|
||||
return static_cast<int>(ErrorCode::Success);
|
||||
}
|
||||
|
||||
const int minExpectedArgs = 4;
|
||||
const int degreeArgMaxLen = 3;
|
||||
|
||||
int try_parse_int(const char* digits, ErrorCode failure_code) {
|
||||
char* tail;
|
||||
auto result = strtol(digits, &tail, 10);
|
||||
auto len = tail - digits;
|
||||
|
||||
if (len == 0 || errno == ERANGE) {
|
||||
throw failure_code;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Rotation::Rotation(const char* digits) {
|
||||
absolute = isdigit(*digits);
|
||||
degree = try_parse_int(digits, ErrorCode::InvalidDegreeArg);
|
||||
|
||||
if (degree % 90 != 0) {
|
||||
throw ErrorCode::InvalidDegreeArg;
|
||||
}
|
||||
}
|
||||
|
||||
// If error message printing is not required, compile with -DNO_OUTPUT
|
||||
#ifdef NO_OUTPUT
|
||||
void usage() {}
|
||||
void printError(ErrorCode) {}
|
||||
#else
|
||||
void usage() {
|
||||
puts(
|
||||
"usage: `qpdf-rotate <in-pdf> <degree> <out-pdf> [<page-selector>]`\n"
|
||||
"<in-pdf>: path to input pdf file\n"
|
||||
"<degree>: any 90 divisible angle; + or - forces relative rotation\n"
|
||||
"<out-pdf>: path for output pdf which will be created\n"
|
||||
"note: <in-pdf> must be distinct from <out-pdf>\n"
|
||||
"\n"
|
||||
"optional page selector arg:\n"
|
||||
"one of two possible formats: "
|
||||
"1) `--range=<low>-<high>` pages in range (use `z` for last page)\n"
|
||||
"2) `--pages=<p1>,...,<pn>` only specified pages (trailing comma is OK)\n"
|
||||
"note: (2) option needs sorted comma separated list\n"
|
||||
"\n"
|
||||
"example: `qpdf-rotate foo.pdf +90 bar.pdf --range=1-10\n"
|
||||
"example: `qpdf-rotate foo.pdf 0 bar.pdf --pages=1,2,3,7,10`\n"
|
||||
"example: `qpdf-rotate foo.pdf -90 bar.pdf --range=5-z"
|
||||
);
|
||||
}
|
||||
|
||||
const char* error_message(ErrorCode code) {
|
||||
switch (code) {
|
||||
case ErrorCode::InvalidArgCount:
|
||||
return "<in-pdf> or <degree> or <out-pdf> arg is missing";
|
||||
case ErrorCode::InputFileNotExist:
|
||||
return "<in-pdf> file not found (not exists or can not be accessed)";
|
||||
case ErrorCode::InvalidDegreeArg:
|
||||
return "<degree> invalid value given";
|
||||
case ErrorCode::InvalidPageSelectorArg:
|
||||
return "<page-selector> invalid value given";
|
||||
case ErrorCode::InvalidRangeSelector:
|
||||
return "invalid range selector";
|
||||
case ErrorCode::InvalidPagesSelector:
|
||||
return "invalid pages selector";
|
||||
case ErrorCode::BadLowRangeBound:
|
||||
return "bad low range boundary";
|
||||
case ErrorCode::BadHighRangeBound:
|
||||
return "bad high range boundary";
|
||||
case ErrorCode::UnexpectedException:
|
||||
return "unexpected exception during execution";
|
||||
case ErrorCode::InternalRangeError:
|
||||
return "internal range error";
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void print_error(ErrorCode code) {
|
||||
fprintf(stderr, "%s\n", error_message(code));
|
||||
}
|
||||
#endif
|
||||
|
||||
void validate_range_selector(const char* selector) {
|
||||
if (!std::regex_match(selector, std::regex("^\\d+\\-(\\d+|z)$"))) {
|
||||
throw ErrorCode::InvalidRangeSelector;
|
||||
}
|
||||
}
|
||||
|
||||
void validate_pages_selector(const char* selector) {
|
||||
const char* p = selector;
|
||||
|
||||
while (*p && isdigit(*p)) {
|
||||
while (isdigit(*p)) ++p;
|
||||
if (*p && *p == ',') ++p;
|
||||
}
|
||||
|
||||
if (*p != '\0') {
|
||||
throw ErrorCode::InvalidPagesSelector;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool file_exists(const char* name) {
|
||||
struct stat file_info;
|
||||
return stat(name, &file_info) == 0;
|
||||
}
|
||||
|
||||
bool has_substr(const char* haystack, const char* needle) {
|
||||
return strncmp(haystack, needle, strlen(needle)) == 0;
|
||||
}
|
||||
|
||||
const char* fetch_range_selector(const char* selector) {
|
||||
const char* value = strchr(selector, '=') + 1;
|
||||
|
||||
validate_range_selector(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
char* fetch_pages_selector(char* selector) {
|
||||
char* value = strchr(selector, '=') + 1;
|
||||
|
||||
validate_pages_selector(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
Arguments parse_arguments(int argc, char* argv[]) {
|
||||
if (argc < minExpectedArgs) {
|
||||
if (argc == 1) { // Launched without args
|
||||
usage();
|
||||
}
|
||||
|
||||
throw ErrorCode::InvalidArgCount;
|
||||
}
|
||||
|
||||
enum Argv: int {
|
||||
ProgramName,
|
||||
InputFile,
|
||||
Degree,
|
||||
OutputFile,
|
||||
PageSelector
|
||||
};
|
||||
|
||||
Arguments args;
|
||||
|
||||
args.in_file_name = argv[Argv::InputFile];
|
||||
args.out_file_name = argv[Argv::OutputFile];
|
||||
|
||||
if (!file_exists(args.in_file_name)) throw ErrorCode::InputFileNotExist;
|
||||
|
||||
args.degree = Rotation{argv[Argv::Degree]};
|
||||
|
||||
if (argc > minExpectedArgs) { // Page selector given as an argument
|
||||
char* page_selector_arg = argv[Argv::PageSelector];
|
||||
|
||||
if (has_substr(page_selector_arg, "--range=")) {
|
||||
args.range = fetch_range_selector(page_selector_arg);
|
||||
} else if (has_substr(page_selector_arg, "--pages=")) {
|
||||
args.pages = fetch_pages_selector(page_selector_arg);
|
||||
} else {
|
||||
throw ErrorCode::InvalidPageSelectorArg;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
// Simple wrapper around vector<QpdfObject> range
|
||||
struct PageRange {
|
||||
PageRange(int from, vector<QpdfObject>& pages):
|
||||
from_page{from}, to_page{static_cast<int>(pages.size())}, pages{pages} {
|
||||
check_invariants();
|
||||
}
|
||||
|
||||
PageRange(int from, int to, vector<QpdfObject>& pages):
|
||||
from_page{from}, to_page{to}, pages{pages} {
|
||||
check_invariants();
|
||||
}
|
||||
|
||||
void check_invariants() {
|
||||
if (from_page < 1 || to_page < 1) {
|
||||
throw ErrorCode::InternalRangeError;
|
||||
}
|
||||
}
|
||||
|
||||
QpdfObject* begin() const noexcept { return pages.data() + from_page - 1; }
|
||||
QpdfObject* end() const noexcept { return pages.data() + to_page; }
|
||||
|
||||
private:
|
||||
vector<QpdfObject>& pages;
|
||||
int to_page;
|
||||
int from_page;
|
||||
};
|
||||
|
||||
QpdfObject calculate_degree(QpdfObject& page, Rotation rotation) {
|
||||
int degree = rotation.degree;
|
||||
|
||||
if (!rotation.absolute && page.hasKey("/Rotate")) {
|
||||
int old_degree = page.getKey("/Rotate").getNumericValue();
|
||||
degree += old_degree;
|
||||
}
|
||||
|
||||
return QpdfObject::newInteger(degree);
|
||||
}
|
||||
|
||||
void add_rotated_page(QPDF& pdf, QpdfObject& page, Rotation rotation) {
|
||||
page.replaceKey("/Rotate", calculate_degree(page, rotation));
|
||||
pdf.addPage(page, false);
|
||||
}
|
||||
|
||||
void add_rotated_pages(QPDF& pdf, PageRange pages, Rotation rotation) {
|
||||
for (auto page : pages) {
|
||||
add_rotated_page(pdf, page, rotation);
|
||||
}
|
||||
}
|
||||
|
||||
void add_pages(QPDF& pdf, PageRange pages) {
|
||||
for (auto page : pages) {
|
||||
pdf.addPage(page, false);
|
||||
}
|
||||
}
|
||||
|
||||
void rotate_pages(QPDF& in, QPDF& out, char* selector, Rotation rotation) {
|
||||
static const int unparsed = -1;
|
||||
|
||||
auto pages = in.getAllPages();
|
||||
auto digits = strtok(selector, ",");
|
||||
int n = unparsed;
|
||||
|
||||
for (int page_n = 0; page_n < pages.size(); ++page_n) {
|
||||
if (digits && n == unparsed) {
|
||||
n = try_parse_int(digits, ErrorCode::BadPageNumber) - 1;
|
||||
}
|
||||
|
||||
if (n == page_n) {
|
||||
digits = strtok(nullptr, ",");
|
||||
n = unparsed;
|
||||
add_rotated_page(out, pages[page_n], rotation);
|
||||
} else {
|
||||
out.addPage(pages[page_n], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rotate_page_range(QPDF& in, QPDF& out, const char* range, Rotation rotation) {
|
||||
auto pages = in.getAllPages();
|
||||
|
||||
int from_page = try_parse_int(range, ErrorCode::BadLowRangeBound);
|
||||
int to_page;
|
||||
|
||||
if (range[(strlen(range)-1)] == 'z') {
|
||||
to_page = pages.size();
|
||||
} else {
|
||||
to_page = try_parse_int(strchr(range, '-') + 1, ErrorCode::BadHighRangeBound);
|
||||
}
|
||||
|
||||
if (from_page > 1) {
|
||||
add_pages(out, PageRange{1, from_page - 1, pages});
|
||||
}
|
||||
add_rotated_pages(out, PageRange{from_page, to_page, pages}, rotation);
|
||||
if (to_page < pages.size()) {
|
||||
add_pages(out, PageRange(to_page + 1, pages));
|
||||
}
|
||||
}
|
||||
|
||||
void rotate_all_pages(QPDF& in, QPDF& out, Rotation rotation) {
|
||||
auto pages = in.getAllPages();
|
||||
add_rotated_pages(out, PageRange{1, pages}, rotation);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user