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 9 2024-01-09 Jay Berkenbilt <ejb@ql.org>
2 10  
3 11 * Add new command-line arguments --file and --range which can be
... ...
include/qpdf/QPDFJob.hh
... ... @@ -514,14 +514,16 @@ class QPDFJob
514 514 void handlePageSpecs(QPDF& pdf, std::vector<std::unique_ptr<QPDF>>& page_heap);
515 515 bool shouldRemoveUnreferencedResources(QPDF& pdf);
516 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 519 void handleUnderOverlay(QPDF& pdf);
519 520 std::string doUnderOverlayForPage(
520 521 QPDF& pdf,
521 522 UnderOverlay& uo,
522   - std::map<int, std::vector<int>>& pagenos,
  523 + std::map<int, std::map<size_t, std::vector<int>>>& pagenos,
523 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 527 std::vector<QPDFPageObjectHelper>& pages,
526 528 QPDFPageObjectHelper& dest_page);
527 529 void validateUnderOverlay(QPDF& pdf, UnderOverlay* uo);
... ... @@ -696,8 +698,8 @@ class QPDFJob
696 698 size_t oi_min_height{DEFAULT_OI_MIN_HEIGHT};
697 699 size_t oi_min_area{DEFAULT_OI_MIN_AREA};
698 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 703 UnderOverlay* under_overlay{nullptr};
702 704 std::vector<PageSpec> page_specs;
703 705 std::map<std::string, RotationSpec> rotations;
... ...
job.sums
... ... @@ -9,12 +9,12 @@ include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa8
9 9 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62
10 10 job.yml 53cad86659db6722e8f415aacb19fc51ab81bb1589c3cb8f65ec893bb4bf5566
11 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 13 libqpdf/qpdf/auto_job_init.hh 19d1da7c4c0c635bd1c5db8d5f17df8edad3442f8eba006adb075cec295fa158
14 14 libqpdf/qpdf/auto_job_json_decl.hh 843892c8e8652a86b7eb573893ef24050b7f36fe313f7251874be5cd4cdbe3fd
15 15 libqpdf/qpdf/auto_job_json_init.hh a87256c082427ec0318223762472970b2eced535c0c8b0288d45c8cdaaf62f74
16 16 libqpdf/qpdf/auto_job_schema.hh 5dac568dff39614e161a0af59a0f328f1e28edf69b96f08bb76fd592d51bb053
17 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 20 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b
... ...
libqpdf/QPDFJob.cc
... ... @@ -1834,9 +1834,6 @@ QPDFJob::processInputSource(
1834 1834 void
1835 1835 QPDFJob::validateUnderOverlay(QPDF& pdf, UnderOverlay* uo)
1836 1836 {
1837   - if (uo->filename.empty()) {
1838   - return;
1839   - }
1840 1837 QPDFPageDocumentHelper main_pdh(pdf);
1841 1838 int main_npages = QIntC::to_int(main_pdh.getAllPages().size());
1842 1839 processFile(uo->pdf, uo->filename.c_str(), uo->password.get(), true, false);
... ... @@ -1878,14 +1875,15 @@ std::string
1878 1875 QPDFJob::doUnderOverlayForPage(
1879 1876 QPDF& pdf,
1880 1877 UnderOverlay& uo,
1881   - std::map<int, std::vector<int>>& pagenos,
  1878 + std::map<int, std::map<size_t, std::vector<int>>>& pagenos,
1882 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 1882 std::vector<QPDFPageObjectHelper>& pages,
1885 1883 QPDFPageObjectHelper& dest_page)
1886 1884 {
1887 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 1887 return "";
1890 1888 }
1891 1889  
... ... @@ -1899,13 +1897,13 @@ QPDFJob::doUnderOverlayForPage(
1899 1897 std::string content;
1900 1898 int min_suffix = 1;
1901 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 1901 doIfVerbose([&](Pipeline& v, std::string const& prefix) {
1904 1902 v << " " << uo.which << " " << from_pageno << "\n";
1905 1903 });
1906 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 1909 // If the same page is overlaid or underlaid multiple times, we'll generate multiple names
... ... @@ -1913,13 +1911,13 @@ QPDFJob::doUnderOverlayForPage(
1913 1911 std::string name = resources.getUniqueResourceName("/Fx", min_suffix);
1914 1912 QPDFMatrix cm;
1915 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 1915 dest_page.copyAnnotations(from_page, cm, dest_afdh, make_afdh(from_page));
1918 1916 if (!new_content.empty()) {
1919 1917 resources.mergeResources("<< /XObject << >> >>"_qpdf);
1920 1918 auto xobject = resources.getKey("/XObject");
1921 1919 if (xobject.isDictionary()) {
1922   - xobject.replaceKey(name, fo[from_pageno]);
  1920 + xobject.replaceKey(name, fo[from_pageno][uo_idx]);
1923 1921 }
1924 1922 ++min_suffix;
1925 1923 content += new_content;
... ... @@ -1929,73 +1927,104 @@ QPDFJob::doUnderOverlayForPage(
1929 1927 }
1930 1928  
1931 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 1952 void
1948 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 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 1971 doIfVerbose([&](Pipeline& v, std::string const& prefix) {
1974 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 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 1998 continue;
1982 1999 }
1983 2000 // This code converts the original page, any underlays, and any overlays to form XObjects.
1984 2001 // Then it concatenates display of all underlays, the original page, and all overlays. Prior
1985 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 2005 auto dest_page_oh = dest_page.getObjectHandle();
1989 2006 auto this_page_fo = dest_page.getFormXObjectForPage();
1990 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 2009 auto content_data = this_page_fo.getRawStreamData();
1993 2010 this_page_fo.replaceStreamData(content_data, QPDFObjectHandle(), QPDFObjectHandle());
1994 2011 auto resources =
1995 2012 dest_page_oh.replaceKeyAndGetNew("/Resources", "<< /XObject << >> >>"_qpdf);
1996 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 2028 content += dest_page.placeFormXObject(
2000 2029 this_page_fo,
2001 2030 "/Fx0",
... ... @@ -2003,8 +2032,19 @@ QPDFJob::handleUnderOverlay(QPDF&amp; pdf)
2003 2032 true,
2004 2033 false,
2005 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 2048 dest_page_oh.replaceKey("/Contents", pdf.newStream(content));
2009 2049 }
2010 2050 }
... ... @@ -3057,9 +3097,10 @@ QPDFJob::writeOutfile(QPDF&amp; pdf)
3057 3097 try {
3058 3098 QUtil::remove_file(backup.c_str());
3059 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 1010 std::shared_ptr<QPDFJob::UOConfig>
1011 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 1015 return std::shared_ptr<UOConfig>(new UOConfig(this));
1015 1016 }
1016 1017  
1017 1018 std::shared_ptr<QPDFJob::UOConfig>
1018 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 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 711 ignored. You can also give a page range with --repeat to cause
712 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 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 2805 ignored. You can also give a page range with --repeat to cause
2806 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 2811 Run qpdf --help=page-ranges for help with page ranges.
2809 2812  
2810 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 2826 are drawn on top of the page to which they are applied, possibly
2824 2827 obscuring the page. The ability to specify the file using the
2825 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 2834 The default behavior of overlay and underlay is that pages are taken
2830 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 849 ignored. You can also give a page range with --repeat to cause
850 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 855 Run qpdf --help=page-ranges for help with page ranges.
853 856 .PP
854 857 Related Options:
... ...
manual/release-notes.rst
... ... @@ -48,6 +48,13 @@ Planned changes for future 12.x (subject to change):
48 48 as well. These new options can be freely intermixed with
49 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 58 - Library Enhancements
52 59  
53 60 - Add ``file()``, ``range()``, and ``password()`` to
... ...