mirror of
https://github.com/qpdf/qpdf.git
synced 2024-12-22 19:08:59 +00:00
8ee83ca722
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.
374 lines
10 KiB
C++
374 lines
10 KiB
C++
// 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);
|
|
}
|
|
|