Commit 8ee83ca722baad9434119bb72d620dfd8e6103c4

Authored by iskander.sharipov
Committed by Jay Berkenbilt
1 parent 841f967a

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.
ChangeLog
@@ -83,6 +83,12 @@ @@ -83,6 +83,12 @@
83 * CVE-2017-9210: Fix infinite loop caused by attempting to unparse 83 * CVE-2017-9210: Fix infinite loop caused by attempting to unparse
84 an object for inclusion in the text of an exception. 84 an object for inclusion in the text of an exception.
85 85
  86 +2016-09-10 Jay Berkenbilt <ejb@ql.org>
  87 +
  88 + * Include pdf-rotate.cc example in contrib. Thanks Iskander
  89 + Sharipov <Iskander.Sharipov@tatar.ru> for contributing this
  90 + program.
  91 +
86 2015-11-10 Jay Berkenbilt <ejb@ql.org> 92 2015-11-10 Jay Berkenbilt <ejb@ql.org>
87 93
88 * 6.0.0: release 94 * 6.0.0: release
@@ -160,7 +160,9 @@ exercises most of the public interface. There are additional example @@ -160,7 +160,9 @@ exercises most of the public interface. There are additional example
160 programs in the examples directory. Reading all the source files in 160 programs in the examples directory. Reading all the source files in
161 the qpdf directory (including the qpdf command-line tool and some test 161 the qpdf directory (including the qpdf command-line tool and some test
162 drivers) along with the code in the examples directory will give you a 162 drivers) along with the code in the examples directory will give you a
163 -complete picture of every aspect of the public interface. 163 +complete picture of every aspect of the public interface. You may also
  164 +check programs in the contrib directory. These are not part of QPDF
  165 +but have been contributed by other developers.
164 166
165 167
166 Additional Notes on Test Suite 168 Additional Notes on Test Suite
contrib/pdf-rotate.cc 0 → 100644
  1 +// This program is not part of QPDF, but it is made available with the
  2 +// QPDF software under the same terms as QPDF itself.
  3 +//
  4 +// Author: Iskander Sharipov <Iskander.Sharipov@tatar.ru>
  5 +//
  6 +// This program requires a c++11 compiler but otherwise has no
  7 +// external dependencies beyond QPDF itself.
  8 +
  9 +#include <qpdf/QPDF.hh>
  10 +#include <qpdf/QPDFWriter.hh>
  11 +
  12 +#include <vector>
  13 +#include <regex>
  14 +#include <algorithm>
  15 +
  16 +#include <cstdio>
  17 +#include <cstdlib>
  18 +#include <cstring>
  19 +#include <cctype>
  20 +#include <cerrno>
  21 +
  22 +#include <sys/stat.h>
  23 +
  24 +using std::vector;
  25 +
  26 +typedef QPDFObjectHandle QpdfObject;
  27 +
  28 +/*
  29 + * Rotates clockwise/counter-clockwise selected pages or page range.
  30 + * It is also capable of setting absolute page rotation.
  31 + * Check `usage` for details.
  32 + *
  33 + * User should check program return value to handle errors.
  34 + * Check `ErrorCode` for enumeration and `error_message` for descriptions.
  35 + */
  36 +
  37 +enum class ErrorCode: int {
  38 + Success, // <- Not an error
  39 + InvalidArgCount,
  40 + InputFileNotExist,
  41 + InvalidDegreeArg,
  42 + InvalidPageSelectorArg,
  43 + InvalidRangeSelector,
  44 + InvalidPagesSelector,
  45 + BadPageNumber,
  46 + BadLowRangeBound,
  47 + BadHighRangeBound,
  48 + UnexpectedException,
  49 + InternalRangeError,
  50 +};
  51 +
  52 +struct Rotation {
  53 + Rotation() = default;
  54 + Rotation(const char* digits);
  55 +
  56 + int degree;
  57 + bool absolute;
  58 +};
  59 +
  60 +struct Arguments {
  61 + const char* in_file_name;
  62 + const char* out_file_name;
  63 + char* pages = nullptr;
  64 + const char* range = nullptr;
  65 + Rotation degree;
  66 +};
  67 +
  68 +void print_error(ErrorCode code);
  69 +Arguments parse_arguments(int argc, char* argv[]);
  70 +void rotate_pages(QPDF& in, QPDF& out, char* selector, Rotation);
  71 +void rotate_page_range(QPDF& in, QPDF& out, const char* range, Rotation);
  72 +void rotate_all_pages(QPDF& in, QPDF& out, Rotation);
  73 +
  74 +int main(int argc, char* argv[]) {
  75 + try {
  76 + Arguments args = parse_arguments(argc, argv);
  77 +
  78 + QPDF in_pdf;
  79 + in_pdf.processFile(args.in_file_name);
  80 + QPDF out_pdf;
  81 + out_pdf.emptyPDF();
  82 +
  83 + if (args.pages) {
  84 + rotate_pages(in_pdf, out_pdf, args.pages, args.degree);
  85 + } else if (args.range) {
  86 + rotate_page_range(in_pdf, out_pdf, args.range, args.degree);
  87 + } else {
  88 + rotate_all_pages(in_pdf, out_pdf, args.degree);
  89 + }
  90 +
  91 + QPDFWriter out_writer{out_pdf, args.out_file_name};
  92 + out_writer.write();
  93 + } catch (ErrorCode e) {
  94 + print_error(e);
  95 + return static_cast<int>(e);
  96 + } catch (...) {
  97 + print_error(ErrorCode::UnexpectedException);
  98 + return static_cast<int>(ErrorCode::UnexpectedException);
  99 + }
  100 +
  101 + return static_cast<int>(ErrorCode::Success);
  102 +}
  103 +
  104 +const int minExpectedArgs = 4;
  105 +const int degreeArgMaxLen = 3;
  106 +
  107 +int try_parse_int(const char* digits, ErrorCode failure_code) {
  108 + char* tail;
  109 + auto result = strtol(digits, &tail, 10);
  110 + auto len = tail - digits;
  111 +
  112 + if (len == 0 || errno == ERANGE) {
  113 + throw failure_code;
  114 + }
  115 +
  116 + return result;
  117 +}
  118 +
  119 +Rotation::Rotation(const char* digits) {
  120 + absolute = isdigit(*digits);
  121 + degree = try_parse_int(digits, ErrorCode::InvalidDegreeArg);
  122 +
  123 + if (degree % 90 != 0) {
  124 + throw ErrorCode::InvalidDegreeArg;
  125 + }
  126 +}
  127 +
  128 +// If error message printing is not required, compile with -DNO_OUTPUT
  129 +#ifdef NO_OUTPUT
  130 +void usage() {}
  131 +void printError(ErrorCode) {}
  132 +#else
  133 +void usage() {
  134 + puts(
  135 + "usage: `qpdf-rotate <in-pdf> <degree> <out-pdf> [<page-selector>]`\n"
  136 + "<in-pdf>: path to input pdf file\n"
  137 + "<degree>: any 90 divisible angle; + or - forces relative rotation\n"
  138 + "<out-pdf>: path for output pdf which will be created\n"
  139 + "note: <in-pdf> must be distinct from <out-pdf>\n"
  140 + "\n"
  141 + "optional page selector arg:\n"
  142 + "one of two possible formats: "
  143 + "1) `--range=<low>-<high>` pages in range (use `z` for last page)\n"
  144 + "2) `--pages=<p1>,...,<pn>` only specified pages (trailing comma is OK)\n"
  145 + "note: (2) option needs sorted comma separated list\n"
  146 + "\n"
  147 + "example: `qpdf-rotate foo.pdf +90 bar.pdf --range=1-10\n"
  148 + "example: `qpdf-rotate foo.pdf 0 bar.pdf --pages=1,2,3,7,10`\n"
  149 + "example: `qpdf-rotate foo.pdf -90 bar.pdf --range=5-z"
  150 + );
  151 +}
  152 +
  153 +const char* error_message(ErrorCode code) {
  154 + switch (code) {
  155 + case ErrorCode::InvalidArgCount:
  156 + return "<in-pdf> or <degree> or <out-pdf> arg is missing";
  157 + case ErrorCode::InputFileNotExist:
  158 + return "<in-pdf> file not found (not exists or can not be accessed)";
  159 + case ErrorCode::InvalidDegreeArg:
  160 + return "<degree> invalid value given";
  161 + case ErrorCode::InvalidPageSelectorArg:
  162 + return "<page-selector> invalid value given";
  163 + case ErrorCode::InvalidRangeSelector:
  164 + return "invalid range selector";
  165 + case ErrorCode::InvalidPagesSelector:
  166 + return "invalid pages selector";
  167 + case ErrorCode::BadLowRangeBound:
  168 + return "bad low range boundary";
  169 + case ErrorCode::BadHighRangeBound:
  170 + return "bad high range boundary";
  171 + case ErrorCode::UnexpectedException:
  172 + return "unexpected exception during execution";
  173 + case ErrorCode::InternalRangeError:
  174 + return "internal range error";
  175 +
  176 + default:
  177 + return "";
  178 + }
  179 +}
  180 +
  181 +void print_error(ErrorCode code) {
  182 + fprintf(stderr, "%s\n", error_message(code));
  183 +}
  184 +#endif
  185 +
  186 +void validate_range_selector(const char* selector) {
  187 + if (!std::regex_match(selector, std::regex("^\\d+\\-(\\d+|z)$"))) {
  188 + throw ErrorCode::InvalidRangeSelector;
  189 + }
  190 +}
  191 +
  192 +void validate_pages_selector(const char* selector) {
  193 + const char* p = selector;
  194 +
  195 + while (*p && isdigit(*p)) {
  196 + while (isdigit(*p)) ++p;
  197 + if (*p && *p == ',') ++p;
  198 + }
  199 +
  200 + if (*p != '\0') {
  201 + throw ErrorCode::InvalidPagesSelector;
  202 + }
  203 +}
  204 +
  205 +inline bool file_exists(const char* name) {
  206 + struct stat file_info;
  207 + return stat(name, &file_info) == 0;
  208 +}
  209 +
  210 +bool has_substr(const char* haystack, const char* needle) {
  211 + return strncmp(haystack, needle, strlen(needle)) == 0;
  212 +}
  213 +
  214 +const char* fetch_range_selector(const char* selector) {
  215 + const char* value = strchr(selector, '=') + 1;
  216 +
  217 + validate_range_selector(value);
  218 +
  219 + return value;
  220 +}
  221 +
  222 +char* fetch_pages_selector(char* selector) {
  223 + char* value = strchr(selector, '=') + 1;
  224 +
  225 + validate_pages_selector(value);
  226 +
  227 + return value;
  228 +}
  229 +
  230 +Arguments parse_arguments(int argc, char* argv[]) {
  231 + if (argc < minExpectedArgs) {
  232 + if (argc == 1) { // Launched without args
  233 + usage();
  234 + }
  235 +
  236 + throw ErrorCode::InvalidArgCount;
  237 + }
  238 +
  239 + enum Argv: int {
  240 + ProgramName,
  241 + InputFile,
  242 + Degree,
  243 + OutputFile,
  244 + PageSelector
  245 + };
  246 +
  247 + Arguments args;
  248 +
  249 + args.in_file_name = argv[Argv::InputFile];
  250 + args.out_file_name = argv[Argv::OutputFile];
  251 +
  252 + if (!file_exists(args.in_file_name)) throw ErrorCode::InputFileNotExist;
  253 +
  254 + args.degree = Rotation{argv[Argv::Degree]};
  255 +
  256 + if (argc > minExpectedArgs) { // Page selector given as an argument
  257 + char* page_selector_arg = argv[Argv::PageSelector];
  258 +
  259 + if (has_substr(page_selector_arg, "--range=")) {
  260 + args.range = fetch_range_selector(page_selector_arg);
  261 + } else if (has_substr(page_selector_arg, "--pages=")) {
  262 + args.pages = fetch_pages_selector(page_selector_arg);
  263 + } else {
  264 + throw ErrorCode::InvalidPageSelectorArg;
  265 + }
  266 + }
  267 +
  268 + return args;
  269 +}
  270 +
  271 +// Simple wrapper around vector<QpdfObject> range
  272 +struct PageRange {
  273 + PageRange(int from, vector<QpdfObject>& pages):
  274 + from_page{from}, to_page{static_cast<int>(pages.size())}, pages{pages} {
  275 + check_invariants();
  276 + }
  277 +
  278 + PageRange(int from, int to, vector<QpdfObject>& pages):
  279 + from_page{from}, to_page{to}, pages{pages} {
  280 + check_invariants();
  281 + }
  282 +
  283 + void check_invariants() {
  284 + if (from_page < 1 || to_page < 1) {
  285 + throw ErrorCode::InternalRangeError;
  286 + }
  287 + }
  288 +
  289 + QpdfObject* begin() const noexcept { return pages.data() + from_page - 1; }
  290 + QpdfObject* end() const noexcept { return pages.data() + to_page; }
  291 +
  292 +private:
  293 + vector<QpdfObject>& pages;
  294 + int to_page;
  295 + int from_page;
  296 +};
  297 +
  298 +QpdfObject calculate_degree(QpdfObject& page, Rotation rotation) {
  299 + int degree = rotation.degree;
  300 +
  301 + if (!rotation.absolute && page.hasKey("/Rotate")) {
  302 + int old_degree = page.getKey("/Rotate").getNumericValue();
  303 + degree += old_degree;
  304 + }
  305 +
  306 + return QpdfObject::newInteger(degree);
  307 +}
  308 +
  309 +void add_rotated_page(QPDF& pdf, QpdfObject& page, Rotation rotation) {
  310 + page.replaceKey("/Rotate", calculate_degree(page, rotation));
  311 + pdf.addPage(page, false);
  312 +}
  313 +
  314 +void add_rotated_pages(QPDF& pdf, PageRange pages, Rotation rotation) {
  315 + for (auto page : pages) {
  316 + add_rotated_page(pdf, page, rotation);
  317 + }
  318 +}
  319 +
  320 +void add_pages(QPDF& pdf, PageRange pages) {
  321 + for (auto page : pages) {
  322 + pdf.addPage(page, false);
  323 + }
  324 +}
  325 +
  326 +void rotate_pages(QPDF& in, QPDF& out, char* selector, Rotation rotation) {
  327 + static const int unparsed = -1;
  328 +
  329 + auto pages = in.getAllPages();
  330 + auto digits = strtok(selector, ",");
  331 + int n = unparsed;
  332 +
  333 + for (int page_n = 0; page_n < pages.size(); ++page_n) {
  334 + if (digits && n == unparsed) {
  335 + n = try_parse_int(digits, ErrorCode::BadPageNumber) - 1;
  336 + }
  337 +
  338 + if (n == page_n) {
  339 + digits = strtok(nullptr, ",");
  340 + n = unparsed;
  341 + add_rotated_page(out, pages[page_n], rotation);
  342 + } else {
  343 + out.addPage(pages[page_n], false);
  344 + }
  345 + }
  346 +}
  347 +
  348 +void rotate_page_range(QPDF& in, QPDF& out, const char* range, Rotation rotation) {
  349 + auto pages = in.getAllPages();
  350 +
  351 + int from_page = try_parse_int(range, ErrorCode::BadLowRangeBound);
  352 + int to_page;
  353 +
  354 + if (range[(strlen(range)-1)] == 'z') {
  355 + to_page = pages.size();
  356 + } else {
  357 + to_page = try_parse_int(strchr(range, '-') + 1, ErrorCode::BadHighRangeBound);
  358 + }
  359 +
  360 + if (from_page > 1) {
  361 + add_pages(out, PageRange{1, from_page - 1, pages});
  362 + }
  363 + add_rotated_pages(out, PageRange{from_page, to_page, pages}, rotation);
  364 + if (to_page < pages.size()) {
  365 + add_pages(out, PageRange(to_page + 1, pages));
  366 + }
  367 +}
  368 +
  369 +void rotate_all_pages(QPDF& in, QPDF& out, Rotation rotation) {
  370 + auto pages = in.getAllPages();
  371 + add_rotated_pages(out, PageRange{1, pages}, rotation);
  372 +}
  373 +