Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pikepdf for openSUSE:Factory checked in at 2021-03-15 10:54:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pikepdf (Old) and /work/SRC/openSUSE:Factory/.python-pikepdf.new.2401 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pikepdf" Mon Mar 15 10:54:39 2021 rev:8 rq:878602 version:2.8.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pikepdf/python-pikepdf.changes 2021-02-15 23:16:36.823423894 +0100 +++ /work/SRC/openSUSE:Factory/.python-pikepdf.new.2401/python-pikepdf.changes 2021-03-15 10:54:41.565198646 +0100 @@ -1,0 +2,21 @@ +Thu Mar 4 19:17:02 UTC 2021 - Martin Hauke <mar...@gmx.de> + +- Update to version 2.8.0 + * Fixed an issue with extracting data from images that had their + DecodeParms structured as a list of dictionaries. + * Fixed an issue where a dangling stream object is created if we + fail to create the requested stream dictionary. + * Calling Dictionary() and Array() on objects which are already + of that type returns a shallow copy rather than throwing an + exception, in keeping with Python semantics. +- Update to version 2.7.0 + * Added an option to tell Pdf.save to recompress flate streams, + and a global option to set the flate compression level. This + option can be use to force the recompression of flate streams + if they are not well compressed. + * Fixed "TypeError: only pages can be inserted" when attempting + to an insert an unowned page using QPDF 10.2.0 or later. +- Update to version 2.6.0 + * Rebuild wheels against QPDF 10.2.0. + +------------------------------------------------------------------- Old: ---- pikepdf-2.5.2.tar.gz New: ---- pikepdf-2.8.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pikepdf.spec ++++++ --- /var/tmp/diff_new_pack.TIY151/_old 2021-03-15 10:54:43.049200924 +0100 +++ /var/tmp/diff_new_pack.TIY151/_new 2021-03-15 10:54:43.053200930 +0100 @@ -2,7 +2,7 @@ # spec file for package python-pikepdf # # Copyright (c) 2021 SUSE LLC -# Copyright (c) 2020, Martin Hauke <mar...@gmx.de> +# Copyright (c) 2020-2021, Martin Hauke <mar...@gmx.de> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -20,7 +20,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-pikepdf -Version: 2.5.2 +Version: 2.8.0 Release: 0 Summary: Read and write PDFs with Python, powered by qpdf License: MPL-2.0 ++++++ pikepdf-2.5.2.tar.gz -> pikepdf-2.8.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/PKG-INFO new/pikepdf-2.8.0/PKG-INFO --- old/pikepdf-2.5.2/PKG-INFO 2021-02-01 02:07:18.598896700 +0100 +++ new/pikepdf-2.8.0/PKG-INFO 2021-03-01 01:23:55.900248800 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pikepdf -Version: 2.5.2 +Version: 2.8.0 Summary: Read and write PDFs with Python, powered by qpdf Home-page: https://github.com/pikepdf/pikepdf Author: James R. Barlow diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/azure-pipelines.yml new/pikepdf-2.8.0/azure-pipelines.yml --- old/pikepdf-2.5.2/azure-pipelines.yml 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/azure-pipelines.yml 2021-03-01 01:21:28.000000000 +0100 @@ -1,5 +1,5 @@ variables: - qpdf_version: "10.1.0" + qpdf_version: "10.2.0" qpdf_release: ${{ format('https://github.com/qpdf/qpdf/releases/download/release-qpdf-{0}/qpdf-{0}.tar.gz', variables.qpdf_version) }} qpdf_min_version: "10.0.3" qpdf_min_release: ${{ format('https://github.com/qpdf/qpdf/releases/download/release-qpdf-{0}/qpdf-{0}.tar.gz', variables.qpdf_min_version) }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/docs/release_notes.rst new/pikepdf-2.8.0/docs/release_notes.rst --- old/pikepdf-2.5.2/docs/release_notes.rst 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/docs/release_notes.rst 2021-03-01 01:21:28.000000000 +0100 @@ -18,6 +18,31 @@ ``pikepdf._qpdf`` is a private interface within pikepdf that applications should not access directly, along with any modules with a prefixed underscore. +v2.8.0 +====== + +- Fixed an issue with extracting data from images that had their DecodeParms + structured as a list of dictionaries. +- Fixed an issue where a dangling stream object is created if we fail to create + the requested stream dictionary. +- Calling ``Dictionary()`` and ``Array()`` on objects which are already of that + type returns a shallow copy rather than throwing an exception, in keeping with + Python semantics. + +v2.7.0 +====== + +- Added an option to tell ``Pdf.save`` to recompress flate streams, and a global + option to set the flate compression level. This option can be use to force + the recompression of flate streams if they are not well compressed. +- Fixed "TypeError: only pages can be inserted" when attempting to an insert an + unowned page using QPDF 10.2.0 or later. + +v2.6.0 +====== + +- Rebuild wheels against QPDF 10.2.0. + v2.5.2 ====== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/pikepdf/_methods.py new/pikepdf-2.8.0/src/pikepdf/_methods.py --- old/pikepdf-2.5.2/src/pikepdf/_methods.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/src/pikepdf/_methods.py 2021-03-01 01:21:28.000000000 +0100 @@ -418,14 +418,8 @@ """ Create a new pikepdf.Stream object that is attached to this PDF. - Args: - data (bytes): Binary data for the stream object - d: Dictionary portion of the stream object - kwargs: Keyword arguments to define the dictionary. Do not set - /Filter or /Length here as pikepdf will manage these. - - Example: - pdf.make_stream(b"Binary data here", Type=Name.XObject, Subtype=Name.Image) + See: + :meth:`pikepdf.Stream.__new__` """ return Stream(self, data, d, **kwargs) @@ -656,7 +650,7 @@ self.Root.PageMode = Name.UseAttachments def save( - self, + self, # TODO mandatory kwargs filename_or_stream: Union[Path, str, BinaryIO, None] = None, static_id: bool = False, preserve_pdfa: bool = True, @@ -671,6 +665,7 @@ qdf: bool = False, progress: Callable[[int], None] = None, encryption: Optional[Union[Encryption, bool]] = None, + recompress_flate: bool = False, ) -> None: """ Save all modifications to this :class:`pikepdf.Pdf`. @@ -730,6 +725,12 @@ to encode stream objects. See documentation for ``StreamDecodeLevel``. + recompress_flate: When disabled (the default), qpdf does not + uncompress and recompress streams compressed with the Flate + compression algorithm. If True, pikepdf will instruct qpdf to + do this, which may be useful if recompressing streams to a + higher compression level. + normalize_content: Enables parsing and reformatting the content stream within PDFs. This may debugging PDFs easier. @@ -797,10 +798,11 @@ progress=progress, encryption=encryption, samefile_check=getattr(self, '_tmp_stream', None) is None, + recompress_flate=recompress_flate, ) @staticmethod - def open( + def open( # TODO mandatory kwargs filename_or_stream: Union[Path, str, BinaryIO], password: Union[str, bytes] = "", hex_password: bool = False, @@ -1021,7 +1023,7 @@ The name of the object. Example: - resource_name = Page(pdf.pages[0]).add_resource(formxobj, Name.XObject) + >>> resource_name = Page(pdf.pages[0]).add_resource(formxobj, Name.XObject) """ if not Name.Resources in self.obj: self.obj.Resources = Dictionary() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/pikepdf/models/image.py new/pikepdf-2.8.0/src/pikepdf/models/image.py --- old/pikepdf-2.5.2/src/pikepdf/models/image.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/src/pikepdf/models/image.py 2021-03-01 01:21:28.000000000 +0100 @@ -69,7 +69,7 @@ if isinstance(value, Dictionary): return [value.as_dict()] if isinstance(value, Array): - return [v.as_list() for v in value] + return [v for v in value.as_list()] raise NotImplementedError(value) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/pikepdf/objects.py new/pikepdf-2.8.0/src/pikepdf/objects.py --- old/pikepdf-2.5.2/src/pikepdf/objects.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/src/pikepdf/objects.py 2021-03-01 01:21:28.000000000 +0100 @@ -169,6 +169,8 @@ raise TypeError('Strings cannot be converted to arrays of chars') if a is None: a = [] + if isinstance(a, Array): + return a.__copy__() return _qpdf._new_array(a) @@ -198,11 +200,14 @@ pikepdf.Object """ if kwargs and d is not None: - raise ValueError('Unsupported parameters') + raise ValueError('Cannot use both a mapping object and keyword args') if kwargs: # Add leading slash # Allows Dictionary(MediaBox=(0,0,1,1), Type=Name('/Page')... return _qpdf._new_dictionary({('/' + k): v for k, v in kwargs.items()}) + if isinstance(d, Dictionary): + # Already a dictionary + return d.__copy__() if not d: d = {} if d and any(not key.startswith('/') for key in d.keys()): @@ -217,14 +222,44 @@ def __new__(cls, owner: 'Pdf', data: bytes = None, d=None, **kwargs): """ + Create a new stream object, which stores arbitrary binary data and may + or may not be compressed. + + A stream dictionary is like a pikepdf.Dictionary or Python dict, except + it has a binary payload of data attached. The dictionary describes + how the data is compressed or encoded. + + The dictionary may be initialized just like pikepdf.Dictionary is initialized, + using a mapping object or keyword arguments. + Args: owner: The Pdf to which this stream shall be attached. - obj: The data bytes for the stream. - d: A mapping object that will be used to construct a ``Dictionary``. - kwargs: Keyword arguments that will define the dictionary. Do not set - /Filter or /Length here as pikepdf will manage these. + data: The data bytes for the stream. + d: An optional mapping object that will be used to construct the stream's + dictionary. + kwargs: Keyword arguments that will define the stream dictionary. Do not set + /Length here as pikepdf will manage this value. Set /Filter + if the data is already encoded in some format. + obj: Deprecated alias for *data*. Returns: pikepdf.Object + + Examples: + Using kwargs: + >>> s1 = pikepdf.Stream( + pdf, + b"uncompressed image data", + BitsPerComponent=8, + ColorSpace=Name.DeviceRGB, + ... + ) + Using dict: + >>> d = pikepdf.Dictionary(...) + >>> s2 = pikepdf.Stream( + pdf, + b"data", + d + ) """ # Support __new__(...obj=bytes) which should have been data=bytes, @@ -238,8 +273,11 @@ if data is None: raise TypeError("Must make Stream from binary data") - stream = _qpdf._new_stream(owner, data) + stream_dict = None if d or kwargs: stream_dict = Dictionary(d, **kwargs) + + stream = _qpdf._new_stream(owner, data) + if stream_dict: stream.stream_dict = stream_dict return stream diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/pikepdf.egg-info/PKG-INFO new/pikepdf-2.8.0/src/pikepdf.egg-info/PKG-INFO --- old/pikepdf-2.5.2/src/pikepdf.egg-info/PKG-INFO 2021-02-01 02:07:18.000000000 +0100 +++ new/pikepdf-2.8.0/src/pikepdf.egg-info/PKG-INFO 2021-03-01 01:23:55.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pikepdf -Version: 2.5.2 +Version: 2.8.0 Summary: Read and write PDFs with Python, powered by qpdf Home-page: https://github.com/pikepdf/pikepdf Author: James R. Barlow diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/qpdf/object_repr.cpp new/pikepdf-2.8.0/src/qpdf/object_repr.cpp --- old/pikepdf-2.5.2/src/qpdf/object_repr.cpp 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/src/qpdf/object_repr.cpp 2021-03-01 01:21:28.000000000 +0100 @@ -36,7 +36,7 @@ std::string objecthandle_scalar_value(QPDFObjectHandle h, bool escaped) { - std::stringstream ss; + std::ostringstream ss; switch (h.getTypeCode()) { case QPDFObject::object_type_e::ot_null: ss << "None"; @@ -67,53 +67,53 @@ std::string objecthandle_pythonic_typename(QPDFObjectHandle h, std::string prefix) { - std::string s; + std::ostringstream ss; - s += prefix; + ss << prefix; switch (h.getTypeCode()) { case QPDFObject::object_type_e::ot_null: - s += "NoneType"; + ss << "NoneType"; break; case QPDFObject::object_type_e::ot_boolean: - s += "Boolean"; + ss << "Boolean"; break; case QPDFObject::object_type_e::ot_integer: - s += "Integer"; + ss << "Integer"; break; case QPDFObject::object_type_e::ot_real: - s += "Real"; + ss << "Real"; break; case QPDFObject::object_type_e::ot_name: - s += "Name"; + ss << "Name"; break; case QPDFObject::object_type_e::ot_string: - s += "String"; + ss << "String"; break; case QPDFObject::object_type_e::ot_operator: - s += "Operator"; + ss << "Operator"; break; case QPDFObject::object_type_e::ot_inlineimage: - s += "InlineImage"; + ss << "InlineImage"; break; case QPDFObject::object_type_e::ot_array: - s += "Array"; + ss << "Array"; break; case QPDFObject::object_type_e::ot_dictionary: if (h.hasKey("/Type")) { - s += std::string("Dictionary(type_=\"") + h.getKey("/Type").getName() + "\")"; + ss << "Dictionary(Type=\"" << h.getKey("/Type").getName() << "\")"; } else { - s += "Dictionary"; + ss << "Dictionary"; } break; case QPDFObject::object_type_e::ot_stream: - s += "Stream"; + ss << "Stream"; break; default: - s += "<Error>"; + ss << "<Error>"; break; } - return s; + return ss.str(); } @@ -130,13 +130,13 @@ std::string objecthandle_repr_inner(QPDFObjectHandle h, uint depth, std::set<QPDFObjGen>* visited, bool* pure_expr) { StackGuard sg(" objecthandle_repr_inner"); - std::ostringstream oss; + std::ostringstream ss; if (!h.isScalar()) { if (visited->count(h.getObjGen()) > 0) { *pure_expr = false; - oss << "<.get_object(" << h.getObjGen().getObj() << ", " << h.getObjGen().getGen() << ")>"; - return oss.str(); + ss << "<.get_object(" << h.getObjGen().getObj() << ", " << h.getObjGen().getGen() << ")>"; + return ss.str(); } if (!(h.getObjGen() == QPDFObjGen(0, 0))) @@ -150,68 +150,68 @@ case QPDFObject::object_type_e::ot_real: case QPDFObject::object_type_e::ot_name: case QPDFObject::object_type_e::ot_string: - oss << objecthandle_scalar_value(h); + ss << objecthandle_scalar_value(h); break; case QPDFObject::object_type_e::ot_operator: - oss << objecthandle_repr_typename_and_value(h); + ss << objecthandle_repr_typename_and_value(h); break; case QPDFObject::object_type_e::ot_inlineimage: - oss << objecthandle_pythonic_typename(h); - oss << "("; - oss << "data=<...>"; - oss << ")"; + ss << objecthandle_pythonic_typename(h); + ss << "("; + ss << "data=<...>"; + ss << ")"; break; case QPDFObject::object_type_e::ot_array: - oss << "["; + ss << "["; { bool first = true; - oss << " "; + ss << " "; for (auto item: h.getArrayAsVector()) { - if (!first) oss << ", "; + if (!first) ss << ", "; first = false; - oss << objecthandle_repr_inner(item, depth, visited, pure_expr); + ss << objecthandle_repr_inner(item, depth, visited, pure_expr); } - oss << " "; + ss << " "; } - oss << "]"; + ss << "]"; break; case QPDFObject::object_type_e::ot_dictionary: - oss << "{"; // This will end the line + ss << "{"; // This will end the line { bool first = true; - oss << "\n"; + ss << "\n"; for (auto item: h.getDictAsMap()) { - if (!first) oss << ",\n"; + if (!first) ss << ",\n"; first = false; - oss << std::string((depth + 1) * 2, ' '); // Indent each line + ss << std::string((depth + 1) * 2, ' '); // Indent each line if (item.first == "/Parent" && item.second.isPagesObject()) { // Don't visit /Parent keys since that just puts every page on the repr() of a single page - oss << std::quoted(item.first) << ": <reference to /Pages>"; + ss << std::quoted(item.first) << ": <reference to /Pages>"; } else { - oss << std::quoted(item.first) << ": " << objecthandle_repr_inner(item.second, depth + 1, visited, pure_expr); + ss << std::quoted(item.first) << ": " << objecthandle_repr_inner(item.second, depth + 1, visited, pure_expr); } } - oss << "\n"; + ss << "\n"; } - oss << std::string(depth * 2, ' '); // Restore previous indent level - oss << "}"; + ss << std::string(depth * 2, ' '); // Restore previous indent level + ss << "}"; break; case QPDFObject::object_type_e::ot_stream: *pure_expr = false; - oss << objecthandle_pythonic_typename(h); - oss << "("; - oss << "stream_dict="; - oss << objecthandle_repr_inner(h.getDict(), depth + 1, visited, pure_expr); - oss << ", "; - oss << "data=<...>"; - oss << ")"; + ss << objecthandle_pythonic_typename(h); + ss << "("; + ss << "stream_dict="; + ss << objecthandle_repr_inner(h.getDict(), depth + 1, visited, pure_expr); + ss << ", "; + ss << "data=<...>"; + ss << ")"; break; default: - oss << "???"; + ss << "???"; break; } - return oss.str(); + return ss.str(); } std::string objecthandle_repr(QPDFObjectHandle h) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/qpdf/pikepdf.cpp new/pikepdf-2.8.0/src/qpdf/pikepdf.cpp --- old/pikepdf-2.5.2/src/qpdf/pikepdf.cpp 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/src/qpdf/pikepdf.cpp 2021-03-01 01:21:28.000000000 +0100 @@ -20,6 +20,7 @@ #include <qpdf/QPDFExc.hh> #include <qpdf/QPDFSystemError.hh> #include <qpdf/QUtil.hh> +#include <qpdf/Pl_Flate.hh> #include <pybind11/stl.h> #include <pybind11/iostream.h> @@ -139,6 +140,15 @@ }, "If set to true, ``pikepdf.open(...access_mode=access_default)`` will use mmap." ); + m.def("set_flate_compression_level", + [](int level) { + if (0 <= level && level <= 9) + Pl_Flate::setCompressionLevel(level); + else + throw py::value_error("Flate compression level must be between 0 and 9"); + }, + "Set the compression level whenever the Flate compression algorithm is used." + ); static py::exception<QPDFExc> exc_main(m, "PdfError"); static py::exception<QPDFExc> exc_password(m, "PasswordError"); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/qpdf/qpdf.cpp new/pikepdf-2.8.0/src/qpdf/qpdf.cpp --- old/pikepdf-2.5.2/src/qpdf/qpdf.cpp 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/src/qpdf/qpdf.cpp 2021-03-01 01:21:28.000000000 +0100 @@ -359,7 +359,8 @@ bool qdf=false, py::object progress=py::none(), py::object encryption=py::none(), - bool samefile_check=true) + bool samefile_check=true, + bool recompress_flate=false) { std::string description; QPDFWriter w(q); @@ -380,6 +381,7 @@ w.setDecodeLevel(stream_decode_level.cast<qpdf_stream_decode_level_e>()); } w.setObjectStreamMode(object_stream_mode); + w.setRecompressFlate(recompress_flate); py::object stream; bool should_close_stream = false; @@ -661,7 +663,8 @@ py::arg("qdf")=false, py::arg("progress")=py::none(), py::arg("encryption")=py::none(), - py::arg("samefile_check")=true + py::arg("samefile_check")=true, + py::arg("recompress_flate")=false ) .def("_get_object_id", &QPDF::getObjectByID) .def("get_object", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/src/qpdf/qpdf_pagelist.cpp new/pikepdf-2.8.0/src/qpdf/qpdf_pagelist.cpp --- old/pikepdf-2.5.2/src/qpdf/qpdf_pagelist.cpp 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/src/qpdf/qpdf_pagelist.cpp 2021-03-01 01:21:28.000000000 +0100 @@ -140,33 +140,51 @@ void PageList::insert_page(size_t index, py::handle obj) { - QPDFObjectHandle page; + QPDFObjectHandle h; try { - page = obj.cast<QPDFObjectHandle>(); + h = obj.cast<QPDFObjectHandle>(); } catch (const py::cast_error&) { throw py::type_error("only pages can be inserted"); } - if (!page.isPageObject()) - throw py::type_error("only pages can be inserted"); - this->insert_page(index, page); + this->insert_page(index, h); } -void PageList::insert_page(size_t index, QPDFObjectHandle page) +void PageList::insert_page(size_t index, QPDFObjectHandle h) { // Find out who owns us - QPDF *page_owner = page.getOwningQPDF(); + QPDF *handle_owner = h.getOwningQPDF(); + QPDFObjectHandle owned, page; + bool copied = false; - if (page_owner == this->qpdf.get()) { + if (handle_owner == this->qpdf.get() || !handle_owner) { // qpdf does not accept duplicating pages within the same file, // so manually create a copy - page = this->qpdf->makeIndirectObject(page.shallowCopy()); - } - if (index != this->count()) { - QPDFObjectHandle refpage = this->get_page(index); - this->qpdf->addPageAt(page, true, refpage); + owned = this->qpdf->makeIndirectObject(h.shallowCopy()); + copied = true; } else { - this->qpdf->addPage(page, false); + owned = h; + } + + try { + if (!owned.isPageObject()) { + throw py::type_error("only pages can be inserted"); + } + + page = owned; + if (index != this->count()) { + QPDFObjectHandle refpage = this->get_page(index); + this->qpdf->addPageAt(page, true, refpage); + } else { + this->qpdf->addPage(page, false); + } + } catch (const std::runtime_error &e) { + if (copied) { + // If we created a new object to hold the page, and failed, delete + // the object we created. + this->qpdf->replaceObject(owned.getObjGen(), QPDFObjectHandle::newNull()); + } + throw; } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/tests/test_image_access.py new/pikepdf-2.8.0/tests/test_image_access.py --- old/pikepdf-2.5.2/tests/test_image_access.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/tests/test_image_access.py 2021-03-01 01:21:28.000000000 +0100 @@ -10,6 +10,7 @@ import pikepdf from pikepdf import ( + Array, Dictionary, Name, Pdf, @@ -485,3 +486,29 @@ ): pim = PdfImage(xobj) _icc = pim.icc + + +def test_dict_or_array_dict(): + pdf = pikepdf.new() + imobj = Stream( + pdf, + b'dummy', + BitsPerComponent=1, + ColorSpace=Name.DeviceGray, + DecodeParms=Array( + [ + Dictionary( + BlackIs1=False, + Columns=16, + K=-1, + ) + ] + ), + Filter=Array([Name.CCITTFaxDecode]), + Height=16, + Width=16, + Type=Name.XObject, + Subtype=Name.Image, + ) + pim = pikepdf.PdfImage(imobj) + assert pim.decode_parms[0].K == -1 # Check that array of dict is unpacked properly diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/tests/test_object.py new/pikepdf-2.8.0/tests/test_object.py --- old/pikepdf-2.5.2/tests/test_object.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/tests/test_object.py 2021-03-01 01:21:28.000000000 +0100 @@ -2,6 +2,7 @@ import sys from copy import copy from decimal import Decimal, InvalidOperation +from distutils.version import LooseVersion from math import isclose, isfinite from zlib import compress @@ -285,9 +286,14 @@ '/Dictionary': Dictionary({'/Color': 'Red'}), } ) - expected = """\ + if LooseVersion(pikepdf.__libqpdf_version__) >= LooseVersion('10.2.0'): + short_pi = '3.14' + else: + short_pi = '3.140000' + expected = ( + """\ pikepdf.Dictionary({ - "/Array": [ 1, 2, Decimal('3.140000') ], + "/Array": [ 1, 2, Decimal('%s') ], "/Boolean": True, "/Dictionary": { "/Color": "Red" @@ -298,6 +304,8 @@ "/String": "hi" }) """ + % short_pi + ) def strip_all_whitespace(s): return ''.join(s.split()) @@ -420,7 +428,7 @@ json_bytes = d.to_json(False) as_dict = json.loads(json_bytes) assert as_dict == { - "/Array": [1, 2, 3.140000], + "/Array": [1, 2, 3.14], "/Boolean": True, "/Dictionary": {"/Color": "Red"}, "/Integer": 42, @@ -605,3 +613,25 @@ p = pikepdf.new() with pytest.raises(TypeError, match='data'): Stream(p) + + +def test_stream_no_dangling_stream_on_failure(): + p = pikepdf.new() + num_objects = len(p.objects) + with pytest.raises(AttributeError): + Stream(p, b'3.14159', ['Not a mapping object']) + assert len(p.objects) == num_objects, "A dangling object was created" + + +def test_dict_of_dict(): + d = Dictionary(One=1, Two=2) + d2 = Dictionary(d) + assert d == d2 + assert d is not d2 + + +def test_array_of_array(): + a = Array([1, 2]) + a2 = Array(a) + assert a == a2 + assert a is not a2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/tests/test_page.py new/pikepdf-2.8.0/tests/test_page.py --- old/pikepdf-2.5.2/tests/test_page.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/tests/test_page.py 2021-03-01 01:21:28.000000000 +0100 @@ -56,3 +56,31 @@ res2 = graph_page.add_resource(d, Name.XObject, prefix='Im') assert str(res2).startswith("/Im") assert graph_page.resources.XObject[res2].Height == 1 + + +def test_add_unowned_page(): # issue 174 + pdf = pikepdf.new() + d = pikepdf.Dictionary(Type=pikepdf.Name.Page) + pdf.pages.append(d) + + +def test_failed_add_page_cleanup(): + pdf = pikepdf.new() + d = pikepdf.Dictionary(Type=pikepdf.Name.NotAPage) + num_objects = len(pdf.objects) + with pytest.raises(TypeError, match="only pages can be inserted"): + pdf.pages.append(d) + assert len(pdf.pages) == 0 + + # If we fail to add a new page, we expect one new null object handle to be + # be added (since QPDF does not remove the object outright) + assert len(pdf.objects) == num_objects + 1, "QPDF semantics changed" + assert pdf.objects[-1] is None, "Left a stale object behind without deleting" + + # But we'd better not delete an existing object... + d2 = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.StillNotAPage)) + with pytest.raises(TypeError, match="only pages can be inserted"): + pdf.pages.append(d2) + assert len(pdf.pages) == 0 + + assert d2.same_owner_as(pdf.Root) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/tests/test_pdf.py new/pikepdf-2.8.0/tests/test_pdf.py --- old/pikepdf-2.5.2/tests/test_pdf.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/tests/test_pdf.py 2021-03-01 01:21:28.000000000 +0100 @@ -5,6 +5,7 @@ import locale import shutil import sys +import zlib from io import BytesIO, StringIO from os import fspath from pathlib import Path @@ -284,3 +285,28 @@ def test_repr(trivial): assert repr(trivial).startswith('<') + + +def test_recompress(resources, outdir): + with pikepdf.open(resources / 'image-mono-inline.pdf') as pdf: + obj = pdf.get_object((7, 0)) + assert isinstance(obj, pikepdf.Stream) + + data = obj.read_bytes() + data_z1 = zlib.compress(data, level=0) # No compression but zlib wrapper + + obj.write(data_z1, filter=pikepdf.Name.FlateDecode) + + bigger = outdir / 'a.pdf' + smaller = outdir / 'b.pdf' + pdf.save(bigger, recompress_flate=False) + pdf.save(smaller, recompress_flate=True) + assert smaller.stat().st_size < bigger.stat().st_size + + +def test_flate_compression_level(): + # We don't want to change the compression level because it's global state + # and will change subsequent test results, so just ping it with an invalid + # value to get partial code coverage. + with pytest.raises(ValueError): + pikepdf._qpdf.set_flate_compression_level(99) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-2.5.2/tests/test_sanity.py new/pikepdf-2.8.0/tests/test_sanity.py --- old/pikepdf-2.5.2/tests/test_sanity.py 2021-02-01 02:05:12.000000000 +0100 +++ new/pikepdf-2.8.0/tests/test_sanity.py 2021-03-01 01:21:28.000000000 +0100 @@ -16,7 +16,7 @@ def test_minimum_qpdf_version(): from pikepdf import _qpdf - assert LooseVersion(_qpdf.qpdf_version()) >= LooseVersion('10.0.0') + assert LooseVersion(_qpdf.qpdf_version()) >= LooseVersion('10.0.3') def test_open_pdf(resources):