From 8ee83ca722baad9434119bb72d620dfd8e6103c4 Mon Sep 17 00:00:00 2001 From: "iskander.sharipov" Date: Tue, 16 Aug 2016 17:03:02 +0300 Subject: 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. --- contrib/pdf-rotate.cc | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 contrib/pdf-rotate.cc (limited to 'contrib') diff --git a/contrib/pdf-rotate.cc b/contrib/pdf-rotate.cc new file mode 100644 index 00000000..00556687 --- /dev/null +++ b/contrib/pdf-rotate.cc @@ -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 +// +// This program requires a c++11 compiler but otherwise has no +// external dependencies beyond QPDF itself. + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +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(e); + } catch (...) { + print_error(ErrorCode::UnexpectedException); + return static_cast(ErrorCode::UnexpectedException); + } + + return static_cast(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 []`\n" + ": path to input pdf file\n" + ": any 90 divisible angle; + or - forces relative rotation\n" + ": path for output pdf which will be created\n" + "note: must be distinct from \n" + "\n" + "optional page selector arg:\n" + "one of two possible formats: " + "1) `--range=-` pages in range (use `z` for last page)\n" + "2) `--pages=,...,` 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 " or or arg is missing"; + case ErrorCode::InputFileNotExist: + return " file not found (not exists or can not be accessed)"; + case ErrorCode::InvalidDegreeArg: + return " invalid value given"; + case ErrorCode::InvalidPageSelectorArg: + return " 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 range +struct PageRange { + PageRange(int from, vector& pages): + from_page{from}, to_page{static_cast(pages.size())}, pages{pages} { + check_invariants(); + } + + PageRange(int from, int to, vector& 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& 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); +} + -- cgit v1.2.3-54-g00ecf