From 5b2e543089e24aae0557835234ef7f733446dc5b Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Wed, 10 Jan 2024 16:07:47 -0500 Subject: [PATCH] Honor repeated overlay/underlay --- ChangeLog | 8 ++ include/qpdf/QPDFJob.hh | 12 +-- job.sums | 6 +- libqpdf/QPDFJob.cc | 147 ++++++++++++++++++++++------------ libqpdf/QPDFJob_config.cc | 6 +- libqpdf/qpdf/auto_job_help.hh | 3 + manual/cli.rst | 9 ++- manual/qpdf.1 | 3 + manual/release-notes.rst | 7 ++ 9 files changed, 136 insertions(+), 65 deletions(-) diff --git a/ChangeLog b/ChangeLog index f6fac9a1..b36ebaa6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2024-01-10 Jay Berkenbilt + + * Allow --overlay and --underlay to be repeated. They may appear + multiple times on the command-line and will be stacked in the + order in which they appear. In QPDFJob JSON, the overlay and + underlay keys may contain arrays. For compatibility, they may also + contain a single dictionary. + 2024-01-09 Jay Berkenbilt * Add new command-line arguments --file and --range which can be diff --git a/include/qpdf/QPDFJob.hh b/include/qpdf/QPDFJob.hh index 315423cd..c291d1e8 100644 --- a/include/qpdf/QPDFJob.hh +++ b/include/qpdf/QPDFJob.hh @@ -514,14 +514,16 @@ class QPDFJob void handlePageSpecs(QPDF& pdf, std::vector>& page_heap); bool shouldRemoveUnreferencedResources(QPDF& pdf); void handleRotations(QPDF& pdf); - void getUOPagenos(UnderOverlay& uo, std::map>& pagenos); + void getUOPagenos( + std::vector& uo, std::map>>& pagenos); void handleUnderOverlay(QPDF& pdf); std::string doUnderOverlayForPage( QPDF& pdf, UnderOverlay& uo, - std::map>& pagenos, + std::map>>& pagenos, size_t page_idx, - std::map& fo, + size_t uo_idx, + std::map>& fo, std::vector& pages, QPDFPageObjectHelper& dest_page); void validateUnderOverlay(QPDF& pdf, UnderOverlay* uo); @@ -696,8 +698,8 @@ class QPDFJob size_t oi_min_height{DEFAULT_OI_MIN_HEIGHT}; size_t oi_min_area{DEFAULT_OI_MIN_AREA}; size_t ii_min_bytes{DEFAULT_II_MIN_BYTES}; - UnderOverlay underlay{"underlay"}; - UnderOverlay overlay{"overlay"}; + std::vector underlay; + std::vector overlay; UnderOverlay* under_overlay{nullptr}; std::vector page_specs; std::map rotations; diff --git a/job.sums b/job.sums index 4e1feeb1..1da9f07b 100644 --- a/job.sums +++ b/job.sums @@ -9,12 +9,12 @@ include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa8 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 job.yml 53cad86659db6722e8f415aacb19fc51ab81bb1589c3cb8f65ec893bb4bf5566 libqpdf/qpdf/auto_job_decl.hh 20d6affe1e260f5a1af4f1d82a820b933835440ff03020e877382da2e8dac6c6 -libqpdf/qpdf/auto_job_help.hh 5808d936f6cd41af278ca298ed0c0762ce0a16956cbe1757a40e4443485cf31e +libqpdf/qpdf/auto_job_help.hh e4bb9e097516f35b4dbc676e1de99f294d8f42912541c8e3844ea401e44336ef libqpdf/qpdf/auto_job_init.hh 19d1da7c4c0c635bd1c5db8d5f17df8edad3442f8eba006adb075cec295fa158 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd libqpdf/qpdf/auto_job_json_init.hh a87256c082427ec0318223762472970b2eced535c0c8b0288d45c8cdaaf62f74 libqpdf/qpdf/auto_job_schema.hh 5dac568dff39614e161a0af59a0f328f1e28edf69b96f08bb76fd592d51bb053 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 -manual/cli.rst 0e6a957defa4839abb9a69414de6a5ec5524fd6ff56fe9abf8f241bee54813e2 -manual/qpdf.1 7250b4e26033fca6b6b9cb23a51e1f46c26f8033663901d4af06b451e287e814 +manual/cli.rst 98219ac9942824b78119cca7cd75691f7c98a31ed3c8b4f108d60a699087c418 +manual/qpdf.1 2544e085c5f0f92e242944eea3bc5736e1036f67595a7a7c988f4ea8d75da901 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc index d82d6fc9..6aba2561 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -1834,9 +1834,6 @@ QPDFJob::processInputSource( void QPDFJob::validateUnderOverlay(QPDF& pdf, UnderOverlay* uo) { - if (uo->filename.empty()) { - return; - } QPDFPageDocumentHelper main_pdh(pdf); int main_npages = QIntC::to_int(main_pdh.getAllPages().size()); processFile(uo->pdf, uo->filename.c_str(), uo->password.get(), true, false); @@ -1878,14 +1875,15 @@ std::string QPDFJob::doUnderOverlayForPage( QPDF& pdf, UnderOverlay& uo, - std::map>& pagenos, + std::map>>& pagenos, size_t page_idx, - std::map& fo, + size_t uo_idx, + std::map>& fo, std::vector& pages, QPDFPageObjectHelper& dest_page) { int pageno = 1 + QIntC::to_int(page_idx); - if (!pagenos.count(pageno)) { + if (!(pagenos.count(pageno) && pagenos[pageno].count(uo_idx))) { return ""; } @@ -1899,13 +1897,13 @@ QPDFJob::doUnderOverlayForPage( std::string content; int min_suffix = 1; QPDFObjectHandle resources = dest_page.getAttribute("/Resources", true); - for (int from_pageno: pagenos[pageno]) { + for (int from_pageno: pagenos[pageno][uo_idx]) { doIfVerbose([&](Pipeline& v, std::string const& prefix) { v << " " << uo.which << " " << from_pageno << "\n"; }); auto from_page = pages.at(QIntC::to_size(from_pageno - 1)); - if (0 == fo.count(from_pageno)) { - fo[from_pageno] = pdf.copyForeignObject(from_page.getFormXObjectForPage()); + if (fo[from_pageno].count(uo_idx) == 0) { + fo[from_pageno][uo_idx] = pdf.copyForeignObject(from_page.getFormXObjectForPage()); } // If the same page is overlaid or underlaid multiple times, we'll generate multiple names @@ -1913,13 +1911,13 @@ QPDFJob::doUnderOverlayForPage( std::string name = resources.getUniqueResourceName("/Fx", min_suffix); QPDFMatrix cm; std::string new_content = dest_page.placeFormXObject( - fo[from_pageno], name, dest_page.getTrimBox().getArrayAsRectangle(), cm); + fo[from_pageno][uo_idx], name, dest_page.getTrimBox().getArrayAsRectangle(), cm); dest_page.copyAnnotations(from_page, cm, dest_afdh, make_afdh(from_page)); if (!new_content.empty()) { resources.mergeResources("<< /XObject << >> >>"_qpdf); auto xobject = resources.getKey("/XObject"); if (xobject.isDictionary()) { - xobject.replaceKey(name, fo[from_pageno]); + xobject.replaceKey(name, fo[from_pageno][uo_idx]); } ++min_suffix; content += new_content; @@ -1929,73 +1927,104 @@ QPDFJob::doUnderOverlayForPage( } void -QPDFJob::getUOPagenos(QPDFJob::UnderOverlay& uo, std::map>& pagenos) +QPDFJob::getUOPagenos( + std::vector& uos, + std::map>>& pagenos) { - size_t idx = 0; - size_t from_size = uo.from_pagenos.size(); - size_t repeat_size = uo.repeat_pagenos.size(); - for (int to_pageno: uo.to_pagenos) { - if (idx < from_size) { - pagenos[to_pageno].push_back(uo.from_pagenos.at(idx)); - } else if (repeat_size) { - pagenos[to_pageno].push_back(uo.repeat_pagenos.at((idx - from_size) % repeat_size)); + size_t uo_idx = 0; + for (auto const& uo: uos) { + size_t page_idx = 0; + size_t from_size = uo.from_pagenos.size(); + size_t repeat_size = uo.repeat_pagenos.size(); + for (int to_pageno: uo.to_pagenos) { + if (page_idx < from_size) { + pagenos[to_pageno][uo_idx].push_back(uo.from_pagenos.at(page_idx)); + } else if (repeat_size) { + pagenos[to_pageno][uo_idx].push_back( + uo.repeat_pagenos.at((page_idx - from_size) % repeat_size)); + } + ++page_idx; } - ++idx; + ++uo_idx; } } void QPDFJob::handleUnderOverlay(QPDF& pdf) { - validateUnderOverlay(pdf, &m->underlay); - validateUnderOverlay(pdf, &m->overlay); - if ((nullptr == m->underlay.pdf) && (nullptr == m->overlay.pdf)) { + if (m->underlay.empty() && m->overlay.empty()) { return; } - std::map> underlay_pagenos; - getUOPagenos(m->underlay, underlay_pagenos); - std::map> overlay_pagenos; - getUOPagenos(m->overlay, overlay_pagenos); - std::map underlay_fo; - std::map overlay_fo; - std::vector upages; - if (m->underlay.pdf.get()) { - upages = QPDFPageDocumentHelper(*(m->underlay.pdf)).getAllPages(); + for (auto& uo: m->underlay) { + validateUnderOverlay(pdf, &uo); } - std::vector opages; - if (m->overlay.pdf.get()) { - opages = QPDFPageDocumentHelper(*(m->overlay.pdf)).getAllPages(); + for (auto& uo: m->overlay) { + validateUnderOverlay(pdf, &uo); } - QPDFPageDocumentHelper main_pdh(pdf); - std::vector main_pages = main_pdh.getAllPages(); - size_t main_npages = main_pages.size(); + // First map key is 1-based page number. Second is index into the overlay/underlay vector. Watch + // out to not reverse the keys or be off by one. + std::map>> underlay_pagenos; + std::map>> overlay_pagenos; + getUOPagenos(m->underlay, underlay_pagenos); + getUOPagenos(m->overlay, overlay_pagenos); doIfVerbose([&](Pipeline& v, std::string const& prefix) { v << prefix << ": processing underlay/overlay\n"; }); - for (size_t i = 0; i < main_npages; ++i) { + + auto get_pages = [](std::vector& v, + std::vector>& v_out) { + for (auto const& uo: v) { + if (uo.pdf) { + v_out.push_back(QPDFPageDocumentHelper(*(uo.pdf)).getAllPages()); + } + } + }; + std::vector> upages; + get_pages(m->underlay, upages); + std::vector> opages; + get_pages(m->overlay, opages); + + std::map> underlay_fo; + std::map> overlay_fo; + QPDFPageDocumentHelper main_pdh(pdf); + auto main_pages = main_pdh.getAllPages(); + size_t main_npages = main_pages.size(); + for (size_t page_idx = 0; page_idx < main_npages; ++page_idx) { + auto pageno = QIntC::to_int(page_idx) + 1; doIfVerbose( - [&](Pipeline& v, std::string const& prefix) { v << " page " << 1 + i << "\n"; }); - auto pageno = QIntC::to_int(i) + 1; - if (!(underlay_pagenos.count(pageno) || overlay_pagenos.count(pageno))) { + [&](Pipeline& v, std::string const& prefix) { v << " page " << pageno << "\n"; }); + if (underlay_pagenos[pageno].empty() && overlay_pagenos[pageno].empty()) { continue; } // This code converts the original page, any underlays, and any overlays to form XObjects. // Then it concatenates display of all underlays, the original page, and all overlays. Prior // to 11.3.0, the original page contents were wrapped in q/Q, but this didn't work if the - // original page had unbalanced q/Q operators. See github issue #904. - auto& dest_page = main_pages.at(i); + // original page had unbalanced q/Q operators. See GitHub issue #904. + auto& dest_page = main_pages.at(page_idx); auto dest_page_oh = dest_page.getObjectHandle(); auto this_page_fo = dest_page.getFormXObjectForPage(); // The resulting form xobject lazily reads the content from the original page, which we are - // going to replace. Therefore we have to explicitly copy it. + // going to replace. Therefore, we have to explicitly copy it. auto content_data = this_page_fo.getRawStreamData(); this_page_fo.replaceStreamData(content_data, QPDFObjectHandle(), QPDFObjectHandle()); auto resources = dest_page_oh.replaceKeyAndGetNew("/Resources", "<< /XObject << >> >>"_qpdf); resources.getKey("/XObject").replaceKeyAndGetNew("/Fx0", this_page_fo); - auto content = doUnderOverlayForPage( - pdf, m->underlay, underlay_pagenos, i, underlay_fo, upages, dest_page); + size_t uo_idx{0}; + std::string content; + for (auto& underlay: m->underlay) { + content += doUnderOverlayForPage( + pdf, + underlay, + underlay_pagenos, + page_idx, + uo_idx, + underlay_fo, + upages[uo_idx], + dest_page); + ++uo_idx; + } content += dest_page.placeFormXObject( this_page_fo, "/Fx0", @@ -2003,8 +2032,19 @@ QPDFJob::handleUnderOverlay(QPDF& pdf) true, false, false); - content += doUnderOverlayForPage( - pdf, m->overlay, overlay_pagenos, i, overlay_fo, opages, dest_page); + uo_idx = 0; + for (auto& overlay: m->overlay) { + content += doUnderOverlayForPage( + pdf, + overlay, + overlay_pagenos, + page_idx, + uo_idx, + overlay_fo, + opages[uo_idx], + dest_page); + ++uo_idx; + } dest_page_oh.replaceKey("/Contents", pdf.newStream(content)); } } @@ -3057,9 +3097,10 @@ QPDFJob::writeOutfile(QPDF& pdf) try { QUtil::remove_file(backup.c_str()); } catch (QPDFSystemError& e) { - *m->log->getError() << m->message_prefix << ": unable to delete original file (" - << e.what() << ");" << " original file left in " << backup - << ", but the input was successfully replaced\n"; + *m->log->getError() + << m->message_prefix << ": unable to delete original file (" << e.what() << ");" + << " original file left in " << backup + << ", but the input was successfully replaced\n"; } } } diff --git a/libqpdf/QPDFJob_config.cc b/libqpdf/QPDFJob_config.cc index c43cc82f..e9084e85 100644 --- a/libqpdf/QPDFJob_config.cc +++ b/libqpdf/QPDFJob_config.cc @@ -1010,14 +1010,16 @@ QPDFJob::PagesConfig::password(std::string const& arg) std::shared_ptr QPDFJob::Config::overlay() { - o.m->under_overlay = &o.m->overlay; + o.m->overlay.emplace_back("overlay"); + o.m->under_overlay = &o.m->overlay.back(); return std::shared_ptr(new UOConfig(this)); } std::shared_ptr QPDFJob::Config::underlay() { - o.m->under_overlay = &o.m->underlay; + o.m->underlay.emplace_back("underlay"); + o.m->under_overlay = &o.m->underlay.back(); return std::shared_ptr(new UOConfig(this)); } diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh index 903bdd56..cd594e9c 100644 --- a/libqpdf/qpdf/auto_job_help.hh +++ b/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 ignored. You can also give a page range with --repeat to cause those pages to be repeated after the original pages are exhausted. +This options are repeatable. Pages will be stacked in order of +appearance: first underlays, then the original page, then overlays. + Run qpdf --help=page-ranges for help with page ranges. )"); } diff --git a/manual/cli.rst b/manual/cli.rst index 0ccdee8f..dc82bf88 100644 --- a/manual/cli.rst +++ b/manual/cli.rst @@ -2805,6 +2805,9 @@ Overlay and Underlay ignored. You can also give a page range with --repeat to cause those pages to be repeated after the original pages are exhausted. + This options are repeatable. Pages will be stacked in order of + appearance: first underlays, then the original page, then overlays. + Run qpdf --help=page-ranges for help with page ranges. 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 are drawn on top of the page to which they are applied, possibly obscuring the page. The ability to specify the file using the :qpdf:ref:`--file` option was added in qpdf 11.9.0. You can combine -overlay and underlay, but you can only specify each option at most one -time. +overlay and underlay. Starting in qpdf 11.9.0, you can specify these +options multiple times. The final page will be a stack containing the +underlays in order of appearance, then the original page, then the +overlays in order of appearance. The default behavior of overlay and underlay is that pages are taken from the overlay/underlay file in sequence and applied to diff --git a/manual/qpdf.1 b/manual/qpdf.1 index 4832d97b..8aa19d06 100644 --- a/manual/qpdf.1 +++ b/manual/qpdf.1 @@ -849,6 +849,9 @@ of the primary output until it runs out of pages, and any extra pages are ignored. You can also give a page range with --repeat to cause those pages to be repeated after the original pages are exhausted. +This options are repeatable. Pages will be stacked in order of +appearance: first underlays, then the original page, then overlays. + Run qpdf --help=page-ranges for help with page ranges. .PP Related Options: diff --git a/manual/release-notes.rst b/manual/release-notes.rst index 938d2705..b9f7d14b 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -48,6 +48,13 @@ Planned changes for future 12.x (subject to change): as well. These new options can be freely intermixed with positional arguments. + - Allow :qpdf:ref:`--overlay` and :qpdf:ref:`--underlay` to be + repeated. They may appear multiple times on the command-line and + will be stacked in the order in which they appear. In QPDFJob + JSON (see :ref:`qpdf-job`), the `overlay` and `underlay` keys + may contain arrays. For compatibility, they may also contain a + single dictionary. + - Library Enhancements - Add ``file()``, ``range()``, and ``password()`` to