Commit 5b2e543089e24aae0557835234ef7f733446dc5b

Authored by Jay Berkenbilt
1 parent 6cf04b0a

Honor repeated overlay/underlay

ChangeLog
  1 +2024-01-10 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Allow --overlay and --underlay to be repeated. They may appear
  4 + multiple times on the command-line and will be stacked in the
  5 + order in which they appear. In QPDFJob JSON, the overlay and
  6 + underlay keys may contain arrays. For compatibility, they may also
  7 + contain a single dictionary.
  8 +
1 2024-01-09 Jay Berkenbilt <ejb@ql.org> 9 2024-01-09 Jay Berkenbilt <ejb@ql.org>
2 10
3 * Add new command-line arguments --file and --range which can be 11 * Add new command-line arguments --file and --range which can be
include/qpdf/QPDFJob.hh
@@ -514,14 +514,16 @@ class QPDFJob @@ -514,14 +514,16 @@ class QPDFJob
514 void handlePageSpecs(QPDF& pdf, std::vector<std::unique_ptr<QPDF>>& page_heap); 514 void handlePageSpecs(QPDF& pdf, std::vector<std::unique_ptr<QPDF>>& page_heap);
515 bool shouldRemoveUnreferencedResources(QPDF& pdf); 515 bool shouldRemoveUnreferencedResources(QPDF& pdf);
516 void handleRotations(QPDF& pdf); 516 void handleRotations(QPDF& pdf);
517 - void getUOPagenos(UnderOverlay& uo, std::map<int, std::vector<int>>& pagenos); 517 + void getUOPagenos(
  518 + std::vector<UnderOverlay>& uo, std::map<int, std::map<size_t, std::vector<int>>>& pagenos);
518 void handleUnderOverlay(QPDF& pdf); 519 void handleUnderOverlay(QPDF& pdf);
519 std::string doUnderOverlayForPage( 520 std::string doUnderOverlayForPage(
520 QPDF& pdf, 521 QPDF& pdf,
521 UnderOverlay& uo, 522 UnderOverlay& uo,
522 - std::map<int, std::vector<int>>& pagenos, 523 + std::map<int, std::map<size_t, std::vector<int>>>& pagenos,
523 size_t page_idx, 524 size_t page_idx,
524 - std::map<int, QPDFObjectHandle>& fo, 525 + size_t uo_idx,
  526 + std::map<int, std::map<size_t, QPDFObjectHandle>>& fo,
525 std::vector<QPDFPageObjectHelper>& pages, 527 std::vector<QPDFPageObjectHelper>& pages,
526 QPDFPageObjectHelper& dest_page); 528 QPDFPageObjectHelper& dest_page);
527 void validateUnderOverlay(QPDF& pdf, UnderOverlay* uo); 529 void validateUnderOverlay(QPDF& pdf, UnderOverlay* uo);
@@ -696,8 +698,8 @@ class QPDFJob @@ -696,8 +698,8 @@ class QPDFJob
696 size_t oi_min_height{DEFAULT_OI_MIN_HEIGHT}; 698 size_t oi_min_height{DEFAULT_OI_MIN_HEIGHT};
697 size_t oi_min_area{DEFAULT_OI_MIN_AREA}; 699 size_t oi_min_area{DEFAULT_OI_MIN_AREA};
698 size_t ii_min_bytes{DEFAULT_II_MIN_BYTES}; 700 size_t ii_min_bytes{DEFAULT_II_MIN_BYTES};
699 - UnderOverlay underlay{"underlay"};  
700 - UnderOverlay overlay{"overlay"}; 701 + std::vector<UnderOverlay> underlay;
  702 + std::vector<UnderOverlay> overlay;
701 UnderOverlay* under_overlay{nullptr}; 703 UnderOverlay* under_overlay{nullptr};
702 std::vector<PageSpec> page_specs; 704 std::vector<PageSpec> page_specs;
703 std::map<std::string, RotationSpec> rotations; 705 std::map<std::string, RotationSpec> rotations;
job.sums
@@ -9,12 +9,12 @@ include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa8 @@ -9,12 +9,12 @@ include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa8
9 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 9 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62
10 job.yml 53cad86659db6722e8f415aacb19fc51ab81bb1589c3cb8f65ec893bb4bf5566 10 job.yml 53cad86659db6722e8f415aacb19fc51ab81bb1589c3cb8f65ec893bb4bf5566
11 libqpdf/qpdf/auto_job_decl.hh 20d6affe1e260f5a1af4f1d82a820b933835440ff03020e877382da2e8dac6c6 11 libqpdf/qpdf/auto_job_decl.hh 20d6affe1e260f5a1af4f1d82a820b933835440ff03020e877382da2e8dac6c6
12 -libqpdf/qpdf/auto_job_help.hh 5808d936f6cd41af278ca298ed0c0762ce0a16956cbe1757a40e4443485cf31e 12 +libqpdf/qpdf/auto_job_help.hh e4bb9e097516f35b4dbc676e1de99f294d8f42912541c8e3844ea401e44336ef
13 libqpdf/qpdf/auto_job_init.hh 19d1da7c4c0c635bd1c5db8d5f17df8edad3442f8eba006adb075cec295fa158 13 libqpdf/qpdf/auto_job_init.hh 19d1da7c4c0c635bd1c5db8d5f17df8edad3442f8eba006adb075cec295fa158
14 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd 14 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd
15 libqpdf/qpdf/auto_job_json_init.hh a87256c082427ec0318223762472970b2eced535c0c8b0288d45c8cdaaf62f74 15 libqpdf/qpdf/auto_job_json_init.hh a87256c082427ec0318223762472970b2eced535c0c8b0288d45c8cdaaf62f74
16 libqpdf/qpdf/auto_job_schema.hh 5dac568dff39614e161a0af59a0f328f1e28edf69b96f08bb76fd592d51bb053 16 libqpdf/qpdf/auto_job_schema.hh 5dac568dff39614e161a0af59a0f328f1e28edf69b96f08bb76fd592d51bb053
17 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 17 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580
18 -manual/cli.rst 0e6a957defa4839abb9a69414de6a5ec5524fd6ff56fe9abf8f241bee54813e2  
19 -manual/qpdf.1 7250b4e26033fca6b6b9cb23a51e1f46c26f8033663901d4af06b451e287e814 18 +manual/cli.rst 98219ac9942824b78119cca7cd75691f7c98a31ed3c8b4f108d60a699087c418
  19 +manual/qpdf.1 2544e085c5f0f92e242944eea3bc5736e1036f67595a7a7c988f4ea8d75da901
20 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b 20 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b
libqpdf/QPDFJob.cc
@@ -1834,9 +1834,6 @@ QPDFJob::processInputSource( @@ -1834,9 +1834,6 @@ QPDFJob::processInputSource(
1834 void 1834 void
1835 QPDFJob::validateUnderOverlay(QPDF& pdf, UnderOverlay* uo) 1835 QPDFJob::validateUnderOverlay(QPDF& pdf, UnderOverlay* uo)
1836 { 1836 {
1837 - if (uo->filename.empty()) {  
1838 - return;  
1839 - }  
1840 QPDFPageDocumentHelper main_pdh(pdf); 1837 QPDFPageDocumentHelper main_pdh(pdf);
1841 int main_npages = QIntC::to_int(main_pdh.getAllPages().size()); 1838 int main_npages = QIntC::to_int(main_pdh.getAllPages().size());
1842 processFile(uo->pdf, uo->filename.c_str(), uo->password.get(), true, false); 1839 processFile(uo->pdf, uo->filename.c_str(), uo->password.get(), true, false);
@@ -1878,14 +1875,15 @@ std::string @@ -1878,14 +1875,15 @@ std::string
1878 QPDFJob::doUnderOverlayForPage( 1875 QPDFJob::doUnderOverlayForPage(
1879 QPDF& pdf, 1876 QPDF& pdf,
1880 UnderOverlay& uo, 1877 UnderOverlay& uo,
1881 - std::map<int, std::vector<int>>& pagenos, 1878 + std::map<int, std::map<size_t, std::vector<int>>>& pagenos,
1882 size_t page_idx, 1879 size_t page_idx,
1883 - std::map<int, QPDFObjectHandle>& fo, 1880 + size_t uo_idx,
  1881 + std::map<int, std::map<size_t, QPDFObjectHandle>>& fo,
1884 std::vector<QPDFPageObjectHelper>& pages, 1882 std::vector<QPDFPageObjectHelper>& pages,
1885 QPDFPageObjectHelper& dest_page) 1883 QPDFPageObjectHelper& dest_page)
1886 { 1884 {
1887 int pageno = 1 + QIntC::to_int(page_idx); 1885 int pageno = 1 + QIntC::to_int(page_idx);
1888 - if (!pagenos.count(pageno)) { 1886 + if (!(pagenos.count(pageno) && pagenos[pageno].count(uo_idx))) {
1889 return ""; 1887 return "";
1890 } 1888 }
1891 1889
@@ -1899,13 +1897,13 @@ QPDFJob::doUnderOverlayForPage( @@ -1899,13 +1897,13 @@ QPDFJob::doUnderOverlayForPage(
1899 std::string content; 1897 std::string content;
1900 int min_suffix = 1; 1898 int min_suffix = 1;
1901 QPDFObjectHandle resources = dest_page.getAttribute("/Resources", true); 1899 QPDFObjectHandle resources = dest_page.getAttribute("/Resources", true);
1902 - for (int from_pageno: pagenos[pageno]) { 1900 + for (int from_pageno: pagenos[pageno][uo_idx]) {
1903 doIfVerbose([&](Pipeline& v, std::string const& prefix) { 1901 doIfVerbose([&](Pipeline& v, std::string const& prefix) {
1904 v << " " << uo.which << " " << from_pageno << "\n"; 1902 v << " " << uo.which << " " << from_pageno << "\n";
1905 }); 1903 });
1906 auto from_page = pages.at(QIntC::to_size(from_pageno - 1)); 1904 auto from_page = pages.at(QIntC::to_size(from_pageno - 1));
1907 - if (0 == fo.count(from_pageno)) {  
1908 - fo[from_pageno] = pdf.copyForeignObject(from_page.getFormXObjectForPage()); 1905 + if (fo[from_pageno].count(uo_idx) == 0) {
  1906 + fo[from_pageno][uo_idx] = pdf.copyForeignObject(from_page.getFormXObjectForPage());
1909 } 1907 }
1910 1908
1911 // If the same page is overlaid or underlaid multiple times, we'll generate multiple names 1909 // If the same page is overlaid or underlaid multiple times, we'll generate multiple names
@@ -1913,13 +1911,13 @@ QPDFJob::doUnderOverlayForPage( @@ -1913,13 +1911,13 @@ QPDFJob::doUnderOverlayForPage(
1913 std::string name = resources.getUniqueResourceName("/Fx", min_suffix); 1911 std::string name = resources.getUniqueResourceName("/Fx", min_suffix);
1914 QPDFMatrix cm; 1912 QPDFMatrix cm;
1915 std::string new_content = dest_page.placeFormXObject( 1913 std::string new_content = dest_page.placeFormXObject(
1916 - fo[from_pageno], name, dest_page.getTrimBox().getArrayAsRectangle(), cm); 1914 + fo[from_pageno][uo_idx], name, dest_page.getTrimBox().getArrayAsRectangle(), cm);
1917 dest_page.copyAnnotations(from_page, cm, dest_afdh, make_afdh(from_page)); 1915 dest_page.copyAnnotations(from_page, cm, dest_afdh, make_afdh(from_page));
1918 if (!new_content.empty()) { 1916 if (!new_content.empty()) {
1919 resources.mergeResources("<< /XObject << >> >>"_qpdf); 1917 resources.mergeResources("<< /XObject << >> >>"_qpdf);
1920 auto xobject = resources.getKey("/XObject"); 1918 auto xobject = resources.getKey("/XObject");
1921 if (xobject.isDictionary()) { 1919 if (xobject.isDictionary()) {
1922 - xobject.replaceKey(name, fo[from_pageno]); 1920 + xobject.replaceKey(name, fo[from_pageno][uo_idx]);
1923 } 1921 }
1924 ++min_suffix; 1922 ++min_suffix;
1925 content += new_content; 1923 content += new_content;
@@ -1929,73 +1927,104 @@ QPDFJob::doUnderOverlayForPage( @@ -1929,73 +1927,104 @@ QPDFJob::doUnderOverlayForPage(
1929 } 1927 }
1930 1928
1931 void 1929 void
1932 -QPDFJob::getUOPagenos(QPDFJob::UnderOverlay& uo, std::map<int, std::vector<int>>& pagenos)  
1933 -{  
1934 - size_t idx = 0;  
1935 - size_t from_size = uo.from_pagenos.size();  
1936 - size_t repeat_size = uo.repeat_pagenos.size();  
1937 - for (int to_pageno: uo.to_pagenos) {  
1938 - if (idx < from_size) {  
1939 - pagenos[to_pageno].push_back(uo.from_pagenos.at(idx));  
1940 - } else if (repeat_size) {  
1941 - pagenos[to_pageno].push_back(uo.repeat_pagenos.at((idx - from_size) % repeat_size)); 1930 +QPDFJob::getUOPagenos(
  1931 + std::vector<QPDFJob::UnderOverlay>& uos,
  1932 + std::map<int, std::map<size_t, std::vector<int>>>& pagenos)
  1933 +{
  1934 + size_t uo_idx = 0;
  1935 + for (auto const& uo: uos) {
  1936 + size_t page_idx = 0;
  1937 + size_t from_size = uo.from_pagenos.size();
  1938 + size_t repeat_size = uo.repeat_pagenos.size();
  1939 + for (int to_pageno: uo.to_pagenos) {
  1940 + if (page_idx < from_size) {
  1941 + pagenos[to_pageno][uo_idx].push_back(uo.from_pagenos.at(page_idx));
  1942 + } else if (repeat_size) {
  1943 + pagenos[to_pageno][uo_idx].push_back(
  1944 + uo.repeat_pagenos.at((page_idx - from_size) % repeat_size));
  1945 + }
  1946 + ++page_idx;
1942 } 1947 }
1943 - ++idx; 1948 + ++uo_idx;
1944 } 1949 }
1945 } 1950 }
1946 1951
1947 void 1952 void
1948 QPDFJob::handleUnderOverlay(QPDF& pdf) 1953 QPDFJob::handleUnderOverlay(QPDF& pdf)
1949 { 1954 {
1950 - validateUnderOverlay(pdf, &m->underlay);  
1951 - validateUnderOverlay(pdf, &m->overlay);  
1952 - if ((nullptr == m->underlay.pdf) && (nullptr == m->overlay.pdf)) { 1955 + if (m->underlay.empty() && m->overlay.empty()) {
1953 return; 1956 return;
1954 } 1957 }
1955 - std::map<int, std::vector<int>> underlay_pagenos;  
1956 - getUOPagenos(m->underlay, underlay_pagenos);  
1957 - std::map<int, std::vector<int>> overlay_pagenos;  
1958 - getUOPagenos(m->overlay, overlay_pagenos);  
1959 - std::map<int, QPDFObjectHandle> underlay_fo;  
1960 - std::map<int, QPDFObjectHandle> overlay_fo;  
1961 - std::vector<QPDFPageObjectHelper> upages;  
1962 - if (m->underlay.pdf.get()) {  
1963 - upages = QPDFPageDocumentHelper(*(m->underlay.pdf)).getAllPages(); 1958 + for (auto& uo: m->underlay) {
  1959 + validateUnderOverlay(pdf, &uo);
1964 } 1960 }
1965 - std::vector<QPDFPageObjectHelper> opages;  
1966 - if (m->overlay.pdf.get()) {  
1967 - opages = QPDFPageDocumentHelper(*(m->overlay.pdf)).getAllPages(); 1961 + for (auto& uo: m->overlay) {
  1962 + validateUnderOverlay(pdf, &uo);
1968 } 1963 }
1969 1964
1970 - QPDFPageDocumentHelper main_pdh(pdf);  
1971 - std::vector<QPDFPageObjectHelper> main_pages = main_pdh.getAllPages();  
1972 - size_t main_npages = main_pages.size(); 1965 + // First map key is 1-based page number. Second is index into the overlay/underlay vector. Watch
  1966 + // out to not reverse the keys or be off by one.
  1967 + std::map<int, std::map<size_t, std::vector<int>>> underlay_pagenos;
  1968 + std::map<int, std::map<size_t, std::vector<int>>> overlay_pagenos;
  1969 + getUOPagenos(m->underlay, underlay_pagenos);
  1970 + getUOPagenos(m->overlay, overlay_pagenos);
1973 doIfVerbose([&](Pipeline& v, std::string const& prefix) { 1971 doIfVerbose([&](Pipeline& v, std::string const& prefix) {
1974 v << prefix << ": processing underlay/overlay\n"; 1972 v << prefix << ": processing underlay/overlay\n";
1975 }); 1973 });
1976 - for (size_t i = 0; i < main_npages; ++i) { 1974 +
  1975 + auto get_pages = [](std::vector<UnderOverlay>& v,
  1976 + std::vector<std::vector<QPDFPageObjectHelper>>& v_out) {
  1977 + for (auto const& uo: v) {
  1978 + if (uo.pdf) {
  1979 + v_out.push_back(QPDFPageDocumentHelper(*(uo.pdf)).getAllPages());
  1980 + }
  1981 + }
  1982 + };
  1983 + std::vector<std::vector<QPDFPageObjectHelper>> upages;
  1984 + get_pages(m->underlay, upages);
  1985 + std::vector<std::vector<QPDFPageObjectHelper>> opages;
  1986 + get_pages(m->overlay, opages);
  1987 +
  1988 + std::map<int, std::map<size_t, QPDFObjectHandle>> underlay_fo;
  1989 + std::map<int, std::map<size_t, QPDFObjectHandle>> overlay_fo;
  1990 + QPDFPageDocumentHelper main_pdh(pdf);
  1991 + auto main_pages = main_pdh.getAllPages();
  1992 + size_t main_npages = main_pages.size();
  1993 + for (size_t page_idx = 0; page_idx < main_npages; ++page_idx) {
  1994 + auto pageno = QIntC::to_int(page_idx) + 1;
1977 doIfVerbose( 1995 doIfVerbose(
1978 - [&](Pipeline& v, std::string const& prefix) { v << " page " << 1 + i << "\n"; });  
1979 - auto pageno = QIntC::to_int(i) + 1;  
1980 - if (!(underlay_pagenos.count(pageno) || overlay_pagenos.count(pageno))) { 1996 + [&](Pipeline& v, std::string const& prefix) { v << " page " << pageno << "\n"; });
  1997 + if (underlay_pagenos[pageno].empty() && overlay_pagenos[pageno].empty()) {
1981 continue; 1998 continue;
1982 } 1999 }
1983 // This code converts the original page, any underlays, and any overlays to form XObjects. 2000 // This code converts the original page, any underlays, and any overlays to form XObjects.
1984 // Then it concatenates display of all underlays, the original page, and all overlays. Prior 2001 // Then it concatenates display of all underlays, the original page, and all overlays. Prior
1985 // to 11.3.0, the original page contents were wrapped in q/Q, but this didn't work if the 2002 // to 11.3.0, the original page contents were wrapped in q/Q, but this didn't work if the
1986 - // original page had unbalanced q/Q operators. See github issue #904.  
1987 - auto& dest_page = main_pages.at(i); 2003 + // original page had unbalanced q/Q operators. See GitHub issue #904.
  2004 + auto& dest_page = main_pages.at(page_idx);
1988 auto dest_page_oh = dest_page.getObjectHandle(); 2005 auto dest_page_oh = dest_page.getObjectHandle();
1989 auto this_page_fo = dest_page.getFormXObjectForPage(); 2006 auto this_page_fo = dest_page.getFormXObjectForPage();
1990 // The resulting form xobject lazily reads the content from the original page, which we are 2007 // The resulting form xobject lazily reads the content from the original page, which we are
1991 - // going to replace. Therefore we have to explicitly copy it. 2008 + // going to replace. Therefore, we have to explicitly copy it.
1992 auto content_data = this_page_fo.getRawStreamData(); 2009 auto content_data = this_page_fo.getRawStreamData();
1993 this_page_fo.replaceStreamData(content_data, QPDFObjectHandle(), QPDFObjectHandle()); 2010 this_page_fo.replaceStreamData(content_data, QPDFObjectHandle(), QPDFObjectHandle());
1994 auto resources = 2011 auto resources =
1995 dest_page_oh.replaceKeyAndGetNew("/Resources", "<< /XObject << >> >>"_qpdf); 2012 dest_page_oh.replaceKeyAndGetNew("/Resources", "<< /XObject << >> >>"_qpdf);
1996 resources.getKey("/XObject").replaceKeyAndGetNew("/Fx0", this_page_fo); 2013 resources.getKey("/XObject").replaceKeyAndGetNew("/Fx0", this_page_fo);
1997 - auto content = doUnderOverlayForPage(  
1998 - pdf, m->underlay, underlay_pagenos, i, underlay_fo, upages, dest_page); 2014 + size_t uo_idx{0};
  2015 + std::string content;
  2016 + for (auto& underlay: m->underlay) {
  2017 + content += doUnderOverlayForPage(
  2018 + pdf,
  2019 + underlay,
  2020 + underlay_pagenos,
  2021 + page_idx,
  2022 + uo_idx,
  2023 + underlay_fo,
  2024 + upages[uo_idx],
  2025 + dest_page);
  2026 + ++uo_idx;
  2027 + }
1999 content += dest_page.placeFormXObject( 2028 content += dest_page.placeFormXObject(
2000 this_page_fo, 2029 this_page_fo,
2001 "/Fx0", 2030 "/Fx0",
@@ -2003,8 +2032,19 @@ QPDFJob::handleUnderOverlay(QPDF&amp; pdf) @@ -2003,8 +2032,19 @@ QPDFJob::handleUnderOverlay(QPDF&amp; pdf)
2003 true, 2032 true,
2004 false, 2033 false,
2005 false); 2034 false);
2006 - content += doUnderOverlayForPage(  
2007 - pdf, m->overlay, overlay_pagenos, i, overlay_fo, opages, dest_page); 2035 + uo_idx = 0;
  2036 + for (auto& overlay: m->overlay) {
  2037 + content += doUnderOverlayForPage(
  2038 + pdf,
  2039 + overlay,
  2040 + overlay_pagenos,
  2041 + page_idx,
  2042 + uo_idx,
  2043 + overlay_fo,
  2044 + opages[uo_idx],
  2045 + dest_page);
  2046 + ++uo_idx;
  2047 + }
2008 dest_page_oh.replaceKey("/Contents", pdf.newStream(content)); 2048 dest_page_oh.replaceKey("/Contents", pdf.newStream(content));
2009 } 2049 }
2010 } 2050 }
@@ -3057,9 +3097,10 @@ QPDFJob::writeOutfile(QPDF&amp; pdf) @@ -3057,9 +3097,10 @@ QPDFJob::writeOutfile(QPDF&amp; pdf)
3057 try { 3097 try {
3058 QUtil::remove_file(backup.c_str()); 3098 QUtil::remove_file(backup.c_str());
3059 } catch (QPDFSystemError& e) { 3099 } catch (QPDFSystemError& e) {
3060 - *m->log->getError() << m->message_prefix << ": unable to delete original file ("  
3061 - << e.what() << ");" << " original file left in " << backup  
3062 - << ", but the input was successfully replaced\n"; 3100 + *m->log->getError()
  3101 + << m->message_prefix << ": unable to delete original file (" << e.what() << ");"
  3102 + << " original file left in " << backup
  3103 + << ", but the input was successfully replaced\n";
3063 } 3104 }
3064 } 3105 }
3065 } 3106 }
libqpdf/QPDFJob_config.cc
@@ -1010,14 +1010,16 @@ QPDFJob::PagesConfig::password(std::string const&amp; arg) @@ -1010,14 +1010,16 @@ QPDFJob::PagesConfig::password(std::string const&amp; arg)
1010 std::shared_ptr<QPDFJob::UOConfig> 1010 std::shared_ptr<QPDFJob::UOConfig>
1011 QPDFJob::Config::overlay() 1011 QPDFJob::Config::overlay()
1012 { 1012 {
1013 - o.m->under_overlay = &o.m->overlay; 1013 + o.m->overlay.emplace_back("overlay");
  1014 + o.m->under_overlay = &o.m->overlay.back();
1014 return std::shared_ptr<UOConfig>(new UOConfig(this)); 1015 return std::shared_ptr<UOConfig>(new UOConfig(this));
1015 } 1016 }
1016 1017
1017 std::shared_ptr<QPDFJob::UOConfig> 1018 std::shared_ptr<QPDFJob::UOConfig>
1018 QPDFJob::Config::underlay() 1019 QPDFJob::Config::underlay()
1019 { 1020 {
1020 - o.m->under_overlay = &o.m->underlay; 1021 + o.m->underlay.emplace_back("underlay");
  1022 + o.m->under_overlay = &o.m->underlay.back();
1021 return std::shared_ptr<UOConfig>(new UOConfig(this)); 1023 return std::shared_ptr<UOConfig>(new UOConfig(this));
1022 } 1024 }
1023 1025
libqpdf/qpdf/auto_job_help.hh
@@ -711,6 +711,9 @@ of the primary output until it runs out of pages, and any extra pages are @@ -711,6 +711,9 @@ of the primary output until it runs out of pages, and any extra pages are
711 ignored. You can also give a page range with --repeat to cause 711 ignored. You can also give a page range with --repeat to cause
712 those pages to be repeated after the original pages are exhausted. 712 those pages to be repeated after the original pages are exhausted.
713 713
  714 +This options are repeatable. Pages will be stacked in order of
  715 +appearance: first underlays, then the original page, then overlays.
  716 +
714 Run qpdf --help=page-ranges for help with page ranges. 717 Run qpdf --help=page-ranges for help with page ranges.
715 )"); 718 )");
716 } 719 }
manual/cli.rst
@@ -2805,6 +2805,9 @@ Overlay and Underlay @@ -2805,6 +2805,9 @@ Overlay and Underlay
2805 ignored. You can also give a page range with --repeat to cause 2805 ignored. You can also give a page range with --repeat to cause
2806 those pages to be repeated after the original pages are exhausted. 2806 those pages to be repeated after the original pages are exhausted.
2807 2807
  2808 + This options are repeatable. Pages will be stacked in order of
  2809 + appearance: first underlays, then the original page, then overlays.
  2810 +
2808 Run qpdf --help=page-ranges for help with page ranges. 2811 Run qpdf --help=page-ranges for help with page ranges.
2809 2812
2810 You can use :command:`qpdf` to overlay or underlay pages from other 2813 You can use :command:`qpdf` to overlay or underlay pages from other
@@ -2823,8 +2826,10 @@ are applied, possibly obscured by the original page, and overlay files @@ -2823,8 +2826,10 @@ are applied, possibly obscured by the original page, and overlay files
2823 are drawn on top of the page to which they are applied, possibly 2826 are drawn on top of the page to which they are applied, possibly
2824 obscuring the page. The ability to specify the file using the 2827 obscuring the page. The ability to specify the file using the
2825 :qpdf:ref:`--file` option was added in qpdf 11.9.0. You can combine 2828 :qpdf:ref:`--file` option was added in qpdf 11.9.0. You can combine
2826 -overlay and underlay, but you can only specify each option at most one  
2827 -time. 2829 +overlay and underlay. Starting in qpdf 11.9.0, you can specify these
  2830 +options multiple times. The final page will be a stack containing the
  2831 +underlays in order of appearance, then the original page, then the
  2832 +overlays in order of appearance.
2828 2833
2829 The default behavior of overlay and underlay is that pages are taken 2834 The default behavior of overlay and underlay is that pages are taken
2830 from the overlay/underlay file in sequence and applied to 2835 from the overlay/underlay file in sequence and applied to
manual/qpdf.1
@@ -849,6 +849,9 @@ of the primary output until it runs out of pages, and any extra pages are @@ -849,6 +849,9 @@ of the primary output until it runs out of pages, and any extra pages are
849 ignored. You can also give a page range with --repeat to cause 849 ignored. You can also give a page range with --repeat to cause
850 those pages to be repeated after the original pages are exhausted. 850 those pages to be repeated after the original pages are exhausted.
851 851
  852 +This options are repeatable. Pages will be stacked in order of
  853 +appearance: first underlays, then the original page, then overlays.
  854 +
852 Run qpdf --help=page-ranges for help with page ranges. 855 Run qpdf --help=page-ranges for help with page ranges.
853 .PP 856 .PP
854 Related Options: 857 Related Options:
manual/release-notes.rst
@@ -48,6 +48,13 @@ Planned changes for future 12.x (subject to change): @@ -48,6 +48,13 @@ Planned changes for future 12.x (subject to change):
48 as well. These new options can be freely intermixed with 48 as well. These new options can be freely intermixed with
49 positional arguments. 49 positional arguments.
50 50
  51 + - Allow :qpdf:ref:`--overlay` and :qpdf:ref:`--underlay` to be
  52 + repeated. They may appear multiple times on the command-line and
  53 + will be stacked in the order in which they appear. In QPDFJob
  54 + JSON (see :ref:`qpdf-job`), the `overlay` and `underlay` keys
  55 + may contain arrays. For compatibility, they may also contain a
  56 + single dictionary.
  57 +
51 - Library Enhancements 58 - Library Enhancements
52 59
53 - Add ``file()``, ``range()``, and ``password()`` to 60 - Add ``file()``, ``range()``, and ``password()`` to