diff --git a/ChangeLog b/ChangeLog index 0bb24590..dc6fd816 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +2018-03-04 Jay Berkenbilt + + * On the command line when specifying page ranges, support + preceding a page number by "r" to indicate that it should be + counted from the end. For example, the range r3-r1 would indicate + the last three pages of a document. + 2018-03-03 Jay Berkenbilt * Ignore zlib data check errors while uncompressing streams. This diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 7f9404c6..6003cabe 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -818,13 +818,15 @@ make The page range is a set of numbers separated by commas, ranges of - numbers separated dashes, or combinations of those. The character - “z” represents the last page. Pages can appear in any - order. Ranges can appear with a high number followed by a low - number, which causes the pages to appear in reverse. Repeating a - number will cause an error, but you can use the workaround - discussed above should you really want to include the same page - twice. + numbers separated dashes, or combinations of those. The character + “z” represents the last page. A number preceded by an + “r” indicates to count from the end, so + “r3-r1” would be the last three pages of the document. + Pages can appear in any order. Ranges can appear with a high + number followed by a low number, which causes the pages to appear + in reverse. Repeating a number will cause an error, but you can + use the workaround discussed above should you really want to + include the same page twice. Example page ranges: @@ -840,6 +842,17 @@ make z-1: all pages in the document in reverse + + + r3-r1: the last three pages of the document + + + + + r1-r3: the last three pages of the document + in reverse order + + diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 17130e8d..caeda8e4 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -358,11 +358,12 @@ input.\n\ \n\ The page range is a set of numbers separated by commas, ranges of\n\ numbers separated dashes, or combinations of those. The character\n\ -\"z\" represents the last page. Pages can appear in any order. Ranges\n\ -can appear with a high number followed by a low number, which causes the\n\ -pages to appear in reverse. Repeating a number will cause an error, but\n\ -the manual discusses a workaround should you really want to include the\n\ -same page twice.\n\ +\"z\" represents the last page. A number preceded by an \"r\" indicates\n\ +to count from the end, so \"r3-r1\" would be the last three pages of the\n\ +document. Pages can appear in any order. Ranges can appear with a\n\ +high number followed by a low number, which causes the pages to appear in\n\ +reverse. Repeating a number will cause an error, but the manual discusses\n\ +a workaround should you really want to include the same page twice.\n\ \n\ If the page range is omitted, the range of 1-z is assumed. qpdf decides\n\ that the page range is omitted if the range argument is either -- or a\n\ @@ -577,6 +578,22 @@ static void show_encryption(QPDF& pdf, Options& o) } } +static int maybe_from_end(int num, bool from_end, int max) +{ + if (from_end) + { + if (num > max) + { + num = 0; + } + else + { + num = max + 1 - num; + } + } + return num; +} + static std::vector parse_numrange(char const* range, int max, bool throw_error = false) { @@ -593,6 +610,7 @@ static std::vector parse_numrange(char const* range, int max, st_after_number } state = st_top; bool last_separator_was_dash = false; int cur_number = 0; + bool from_end = false; while (*p) { char ch = *p; @@ -616,14 +634,25 @@ static std::vector parse_numrange(char const* range, int max, state = st_after_number; cur_number = max; } + else if (ch == 'r') + { + if (! (state == st_top)) + { + throw std::runtime_error("r not expected"); + } + state = st_in_number; + from_end = true; + } else if ((ch == ',') || (ch == '-')) { if (! ((state == st_in_number) || (state == st_after_number))) { throw std::runtime_error("unexpected separator"); } + cur_number = maybe_from_end(cur_number, from_end, max); work.push_back(cur_number); cur_number = 0; + from_end = false; if (ch == ',') { state = st_top; @@ -649,6 +678,7 @@ static std::vector parse_numrange(char const* range, int max, } if ((state == st_in_number) || (state == st_after_number)) { + cur_number = maybe_from_end(cur_number, from_end, max); work.push_back(cur_number); } else diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index c6e39fb8..69d5a5de 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -1092,9 +1092,19 @@ my @nrange_tests = ( ["1-10,1234,5", "qpdf: error in numeric range 1-10,1234,5: number 1234 out of range", 2], - ["1,3,5-10,z-13,13,9,z,2", - "numeric range 1,3,5-10,z-13,13,9,z,2" . - " -> 1 3 5 6 7 8 9 10 15 14 13 13 9 15 2", + ["1,r,3", + "qpdf: error in numeric range 1,r,3: number 16 out of range", + 2], + ["1,r16,3", + "qpdf: error in numeric range 1,r16,3: number 0 out of range", + 2], + ["1,3,5-10,z-13,13,9,z,2,r2-r4", + "numeric range 1,3,5-10,z-13,13,9,z,2,r2-r4" . + " -> 1 3 5 6 7 8 9 10 15 14 13 13 9 15 2 14 13 12", + 0], + ["r1-r15", # r\d+ at end + "numeric range r1-r15" . + " -> 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1", 0], ); $n_tests += scalar(@nrange_tests);