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 2023-12-28 23:02:03 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pikepdf (Old) and /work/SRC/openSUSE:Factory/.python-pikepdf.new.28375 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pikepdf" Thu Dec 28 23:02:03 2023 rev:21 rq:1135318 version:8.10.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pikepdf/python-pikepdf.changes 2023-12-14 22:03:30.796873948 +0100 +++ /work/SRC/openSUSE:Factory/.python-pikepdf.new.28375/python-pikepdf.changes 2023-12-28 23:03:47.335541986 +0100 @@ -1,0 +2,11 @@ +Wed Dec 27 14:00:51 UTC 2023 - Dirk Müller <dmuel...@suse.com> + +- update to 8.10.1: + * Rebuilt with QPDF 11.6.4. + * Replaced use of a custom C++ logger with sharing QPDF's. + It is still relayed to the Python logger. + * Added a simpler API for adding attachments from bytes data. + * Deprecated use of Object.parse(str) in favor of + Object.parse(bytes). + +------------------------------------------------------------------- Old: ---- pikepdf-8.9.0.tar.gz New: ---- pikepdf-8.10.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pikepdf.spec ++++++ --- /var/tmp/diff_new_pack.F5kIK0/_old 2023-12-28 23:03:48.007566547 +0100 +++ /var/tmp/diff_new_pack.F5kIK0/_new 2023-12-28 23:03:48.007566547 +0100 @@ -19,7 +19,7 @@ %{?sle15_python_module_pythons} Name: python-pikepdf -Version: 8.9.0 +Version: 8.10.1 Release: 0 Summary: Read and write PDFs with Python, powered by qpdf License: MPL-2.0 ++++++ pikepdf-8.9.0.tar.gz -> pikepdf-8.10.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/PKG-INFO new/pikepdf-8.10.1/PKG-INFO --- old/pikepdf-8.9.0/PKG-INFO 2023-12-11 00:26:49.663727000 +0100 +++ new/pikepdf-8.10.1/PKG-INFO 2023-12-17 10:33:05.458814400 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pikepdf -Version: 8.9.0 +Version: 8.10.1 Summary: Read and write PDFs with Python, powered by qpdf Author-email: "James R. Barlow" <ja...@purplerock.ca> License: MPL-2.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/docs/conf.py new/pikepdf-8.10.1/docs/conf.py --- old/pikepdf-8.9.0/docs/conf.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/docs/conf.py 2023-12-17 10:31:00.000000000 +0100 @@ -92,7 +92,7 @@ # |version| and |release|, also used in various other places throughout the # built documents. -release = "8.9.0" +release = "8.10.1" version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/docs/releasenotes/version8.rst new/pikepdf-8.10.1/docs/releasenotes/version8.rst --- old/pikepdf-8.9.0/docs/releasenotes/version8.rst 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/docs/releasenotes/version8.rst 2023-12-17 10:31:00.000000000 +0100 @@ -1,3 +1,25 @@ +v8.10.1 +======= + +- Rebuilt with QPDF 11.6.4. +- Replaced use of a custom C++ logger with sharing QPDF's. It is still relayed to + the Python logger. +- Added a simpler API for adding attachments from bytes data. +- Deprecated use of Object.parse(str) in favor of Object.parse(bytes). + +v8.10.0 +======= + +- Fixed a performance regression when appending thousands of pages from one PDF to + another. +- Fixed some obscure issues with iterators over ``Pdf.pages`` that would have led + to incorrect or unintuitive behavior, like partial iteration not being accounted + for. +- Using the ``Pdf.pages`` API to insert objects other ``pikepdf.Pdf`` is now + deprecated. Previously, we accepted ``pikepdf.Dictionary`` that had its ``/Type`` + set to ``/Page``. Now, one must wrap these dictionaries in ``pikepdf.Page()``. +- Added type hints that ``pikepdf.Object`` can be implicitly converted to float + and int. v8.9.0 ====== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/pyproject.toml new/pikepdf-8.10.1/pyproject.toml --- old/pikepdf-8.9.0/pyproject.toml 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/pyproject.toml 2023-12-17 10:31:00.000000000 +0100 @@ -7,7 +7,7 @@ [project] name = "pikepdf" -version = "8.9.0" +version = "8.10.1" description = "Read and write PDFs with Python, powered by qpdf" readme = "README.md" requires-python = ">=3.8" @@ -114,7 +114,7 @@ [tool.cibuildwheel.environment] QPDF_MIN_VERSION = "11.5.0" -QPDF_VERSION = "11.6.3" +QPDF_VERSION = "11.6.4" QPDF_PATTERN = "https://github.com/qpdf/qpdf/releases/download/vVERSION/qpdf-VERSION.tar.gz" [tool.cibuildwheel.linux] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/embeddedfiles.cpp new/pikepdf-8.10.1/src/core/embeddedfiles.cpp --- old/pikepdf-8.9.0/src/core/embeddedfiles.cpp 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/embeddedfiles.cpp 2023-12-17 10:31:00.000000000 +0100 @@ -15,6 +15,33 @@ #include "pikepdf.h" #include "pipeline.h" +QPDFFileSpecObjectHelper create_filespec(QPDF &q, + py::bytes data, + std::string description, + std::string filename, + std::string mime_type, + std::string creation_date, + std::string mod_date, + QPDFObjectHandle relationship) +{ + auto efstream = QPDFEFStreamObjectHelper::createEFStream(q, std::string(data)); + auto filespec = QPDFFileSpecObjectHelper::createFileSpec(q, filename, efstream); + + if (!description.empty()) + filespec.setDescription(description); + if (!mime_type.empty()) + efstream.setSubtype(mime_type); + if (!creation_date.empty()) + efstream.setCreationDate(creation_date); + if (!mod_date.empty()) + efstream.setModDate(mod_date); + + if (relationship.isName()) { + filespec.getObjectHandle().replaceKey("/AFRelationship", relationship); + } + return filespec; +} + void init_embeddedfiles(py::module_ &m) { py::class_<QPDFFileSpecObjectHelper, @@ -28,24 +55,14 @@ std::string creation_date, std::string mod_date, QPDFObjectHandle &relationship) { - auto efstream = - QPDFEFStreamObjectHelper::createEFStream(q, std::string(data)); - auto filespec = - QPDFFileSpecObjectHelper::createFileSpec(q, filename, efstream); - - if (!description.empty()) - filespec.setDescription(description); - if (!mime_type.empty()) - efstream.setSubtype(mime_type); - if (!creation_date.empty()) - efstream.setCreationDate(creation_date); - if (!mod_date.empty()) - efstream.setModDate(mod_date); - - if (relationship.isName()) { - filespec.getObjectHandle().replaceKey("/AFRelationship", relationship); - } - return filespec; + return create_filespec(q, + data, + description, + filename, + mime_type, + creation_date, + mod_date, + relationship); }), py::keep_alive<0, 1>(), // LCOV_EXCL_LINE py::arg("q"), @@ -119,6 +136,18 @@ py::class_<QPDFEmbeddedFileDocumentHelper>(m, "Attachments") .def_property_readonly( "_has_embedded_files", &QPDFEmbeddedFileDocumentHelper::hasEmbeddedFiles) + .def("_attach_data", + [](QPDFEmbeddedFileDocumentHelper &efdh, py::str key, py::bytes data) { + auto ef = create_filespec(efdh.getQPDF(), + std::string(data), + std::string(""), + std::string(key), + std::string(""), + std::string(""), + std::string(""), + QPDFObjectHandle::newName("/Unspecified")); + efdh.replaceEmbeddedFile(key, ef); + }) .def("_get_all_filespecs", &QPDFEmbeddedFileDocumentHelper::getEmbeddedFiles, py::return_value_policy::reference_internal) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/logger.cpp new/pikepdf-8.10.1/src/core/logger.cpp --- old/pikepdf-8.9.0/src/core/logger.cpp 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/logger.cpp 2023-12-17 10:31:00.000000000 +0100 @@ -4,8 +4,6 @@ #include "pikepdf.h" #include <qpdf/QPDFLogger.hh> -static auto pikepdf_logger = QPDFLogger::create(); - // Pipeline to relay QPDF log messages to Python logging module // This is a sink - cannot pass to other pipeline objects class Pl_PythonLogger : public Pipeline { @@ -47,7 +45,7 @@ std::shared_ptr<QPDFLogger> get_pikepdf_logger() { // All QPDFs can use the same logger - return pikepdf_logger; + return QPDFLogger::defaultLogger(); } void init_logger(py::module_ &m) @@ -61,6 +59,7 @@ std::shared_ptr<Pipeline> pl_log_error = std::make_shared<Pl_PythonLogger>( "QPDF to Python logging pipeline", py_logger, "error"); + auto pikepdf_logger = get_pikepdf_logger(); pikepdf_logger->setInfo(pl_log_info); pikepdf_logger->setWarn(pl_log_warn); pikepdf_logger->setError(pl_log_error); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/object.cpp new/pikepdf-8.10.1/src/core/object.cpp --- old/pikepdf-8.9.0/src/core/object.cpp 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/object.cpp 2023-12-17 10:31:00.000000000 +0100 @@ -758,7 +758,17 @@ .def_property_readonly("objgen", &object_get_objgen) .def_static( "parse", - [](std::string const &stream, std::string const &description) { + [](py::bytes stream, py::str description) { + return QPDFObjectHandle::parse( + std::string(stream), std::string(description)); + }, + py::arg("stream"), + py::arg("description") = "") + .def_static( + "parse", + [](py::str stream, std::string const &description) { + python_warning("pikepdf.Object.parse(str) is deprecated; use bytes.", + PyExc_DeprecationWarning); return QPDFObjectHandle::parse(stream, description); }, py::arg("stream"), @@ -863,7 +873,7 @@ .def( "__eq__", [](QPDFObjectHelper &self, QPDFObjectHelper &other) { - // Pages that are copies + // Object helpers are equal if their object handles are equal return objecthandle_equal( self.getObjectHandle(), other.getObjectHandle()); }, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/object_convert.cpp new/pikepdf-8.10.1/src/core/object_convert.cpp --- old/pikepdf-8.9.0/src/core/object_convert.cpp 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/object_convert.cpp 2023-12-17 10:31:00.000000000 +0100 @@ -43,13 +43,9 @@ { StackGuard sg(" array_builder"); std::vector<QPDFObjectHandle> result; - int narg = 0; for (const auto &item : iter) { - narg++; - - auto value = objecthandle_encode(item); - result.push_back(value); + result.emplace_back(objecthandle_encode(item)); } return result; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/parsers.cpp new/pikepdf-8.10.1/src/core/parsers.cpp --- old/pikepdf-8.9.0/src/core/parsers.cpp 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/parsers.cpp 2023-12-17 10:31:00.000000000 +0100 @@ -58,8 +58,6 @@ } break; } - case qpdf_object_type_e::ot_stream: - throw py::type_error("Streams are not allowed in content stream instructions"); default: { throw py::type_error("Only scalar types, arrays, and dictionaries are allowed " "in content streams."); @@ -271,13 +269,11 @@ { py::class_<ContentStreamInstruction>(m, "ContentStreamInstruction") .def(py::init<const ContentStreamInstruction &>()) - .def(py::init([](ObjectList operands, QPDFObjectHandle operator_) { - return ContentStreamInstruction(operands, operator_); - })) + .def(py::init<ObjectList, QPDFObjectHandle>()) .def(py::init([](py::iterable operands, QPDFObjectHandle operator_) { ObjectList newlist; for (auto &item : operands) { - newlist.push_back(objecthandle_encode(item)); + newlist.emplace_back(objecthandle_encode(item)); } return ContentStreamInstruction(newlist, operator_); })) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/pikepdf.cpp new/pikepdf-8.10.1/src/core/pikepdf.cpp --- old/pikepdf-8.9.0/src/core/pikepdf.cpp 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/pikepdf.cpp 2023-12-17 10:31:00.000000000 +0100 @@ -136,6 +136,17 @@ init_rectangle(m); init_tokenfilter(m); + auto m_test = m.def_submodule("_test", "pikepdf._core test functions"); + m_test + .def( + "fopen_nonexistent_file", + []() -> void { (void)QUtil::safe_fopen("does_not_exist__42", "rb"); }, + "Used to test that C++ system error -> Python exception propagation works.") + .def( + "log_info", + [](std::string s) { return get_pikepdf_logger()->info(s); }, + "Used to test routing of QPDF's logger to Python logging."); + // -- Module level functions -- m.def("utf8_to_pdf_doc", [](py::str utf8, char unknown) { @@ -148,17 +159,9 @@ return py::str(QUtil::pdf_doc_to_utf8(pdfdoc)); }) .def( - "_test_file_not_found", - []() -> void { (void)QUtil::safe_fopen("does_not_exist__42", "rb"); }, - "Used to test that C++ system error -> Python exception propagation works.") - .def( "_translate_qpdf_logic_error", [](std::string s) { return translate_qpdf_logic_error(s).first; }, "Used to test interpretation of QPDF errors.") - .def( - "_log_info", - [](std::string s) { return get_pikepdf_logger()->info(s); }, - "Used to test routing of QPDF's logger to Python logging.") .def("set_decimal_precision", [](uint prec) { DECIMAL_PRECISION = prec; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/qpdf_pagelist.cpp new/pikepdf-8.10.1/src/core/qpdf_pagelist.cpp --- old/pikepdf-8.9.0/src/core/qpdf_pagelist.cpp 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/qpdf_pagelist.cpp 2023-12-17 10:31:00.000000000 +0100 @@ -44,6 +44,7 @@ if (!slice.compute(this->count(), &start, &stop, &step, &slicelength)) throw py::error_already_set(); // LCOV_EXCL_LINE std::vector<QPDFPageObjectHelper> result; + result.reserve(slicelength); for (py::size_t i = 0; i < slicelength; ++i) { auto oh = this->get_page(start); result.push_back(oh); @@ -134,8 +135,15 @@ py::size_t PageList::count() { return this->doc.getAllPages().size(); } -void PageList::try_insert_qpdfobject_as_page(py::size_t index, py::handle obj) +QPDFPageObjectHelper PageList::page_from_object(py::handle obj) { + try { + auto page = obj.cast<QPDFPageObjectHelper>(); + return page; + } catch (py::cast_error &) { + // Perhaps obj is a dictionary with Type=Name.Page + } + QPDFObjectHandle oh, indirect_oh; try { oh = obj.cast<QPDFObjectHandle>(); @@ -144,6 +152,10 @@ "nor pikepdf.Dictionary with Type=Name.Page"); } + python_warning("Implicit conversion of pikepdf.Dictionary to pikepdf.Page is " + "deprecated. Use pikepdf.Page(dictionary) instead.", + PyExc_DeprecationWarning); + bool copied = false; try { if (!oh.getOwningQPDF()) { @@ -161,8 +173,7 @@ "to insert this as a page: ") + objecthandle_repr(oh)); } - auto page = QPDFPageObjectHelper(indirect_oh); - this->insert_page(index, page); + return QPDFPageObjectHelper(indirect_oh); } catch (std::runtime_error &) { // If we created a new temporary indirect object to hold the page, and // failed to insert, delete the object we created as best we can. @@ -181,22 +192,33 @@ this->insert_page(index, poh); return; } catch (py::cast_error &) { - this->try_insert_qpdfobject_as_page(index, obj); + auto page = this->page_from_object(obj); + this->insert_page(index, page); return; } } void PageList::insert_page(py::size_t index, QPDFPageObjectHelper page) { - auto doc = QPDFPageDocumentHelper(*this->qpdf); if (index != this->count()) { auto refpage = this->get_page(index); - doc.addPageAt(page, true, refpage); + this->doc.addPageAt(page, true, refpage); } else { - doc.addPage(page, false); + this->doc.addPage(page, false); } } +void PageList::append_page(py::handle obj) +{ + auto page = this->page_from_object(obj); + this->doc.addPage(page, false); +} + +void PageList::append_page(QPDFPageObjectHelper page) +{ + this->doc.addPage(page, false); +} + QPDFPageObjectHelper from_objgen(QPDF &q, QPDFObjGen og) { auto h = q.getObjectByObjGen(og); @@ -205,8 +227,22 @@ return QPDFPageObjectHelper(h); } +QPDFPageObjectHelper PageListIterator::next() +{ + if (this->index >= this->pages.size()) { + throw py::stop_iteration(); + } + auto page = this->pages.at(this->index); + this->index++; + return page; +} + void init_pagelist(py::module_ &m) { + py::class_<PageListIterator>(m, "_PageListIterator") + .def("__iter__", [](PageListIterator &it) { return it; }) + .def("__next__", &PageListIterator::next); + py::class_<PageList>(m, "PageList") .def( "__getitem__", @@ -238,13 +274,12 @@ return pl.get_page(pnum - 1); }, py::arg("pnum")) - .def("__iter__", [](PageList &pl) { return PageList(pl.qpdf, 0); }) - .def("__next__", + .def( + "__iter__", [](PageList &pl) { - if (pl.iterpos < pl.count()) - return pl.get_page(pl.iterpos++); - throw py::stop_iteration(); - }) + return PageListIterator{pl, 0}; + }, + py::keep_alive<0, 1>()) .def( "insert", [](PageList &pl, py::ssize_t index, py::object obj) { @@ -256,31 +291,24 @@ .def("reverse", [](PageList &pl) { py::slice ordinary_indices(0, pl.count(), 1); - py::int_ step(-1); - py::slice reversed = py::reinterpret_steal<py::slice>( - PySlice_New(Py_None, Py_None, step.ptr())); + py::slice reversed{{}, {}, -1}; py::list reversed_pages = pl.get_pages(reversed); pl.set_pages_from_iterable(ordinary_indices, reversed_pages); }) .def( "append", - [](PageList &pl, QPDFPageObjectHelper &page) { - pl.insert_page(pl.count(), page); - }, + [](PageList &pl, QPDFPageObjectHelper &page) { pl.append_page(page); }, py::arg("page")) .def( "append", - [](PageList &pl, py::handle page) { pl.insert_page(pl.count(), page); }, + [](PageList &pl, py::handle page) { pl.append_page(page); }, py::arg("page")) .def( "extend", [](PageList &pl, PageList &other) { - auto other_count = other.count(); - for (decltype(other_count) i = 0; i < other_count; i++) { - if (other_count != other.count()) - throw py::value_error( - "source page list modified during iteration"); - pl.insert_page(pl.count(), other.get_page(i)); + auto other_pages = other.doc.getAllPages(); + for (auto &page : other_pages) { + pl.append_page(page); } }, py::arg("other")) @@ -289,21 +317,30 @@ [](PageList &pl, py::iterable iterable) { py::iterator it = iterable.attr("__iter__")(); while (it != py::iterator::sentinel()) { - // assert_pyobject_is_page_obj(*it); assert_pyobject_is_page_helper(*it); - pl.insert_page(pl.count(), *it); + pl.append_page(*it); ++it; } }, py::arg("iterable")) .def("remove", - [](PageList &pl, py::kwargs kwargs) { - auto pnum = kwargs["p"].cast<py::ssize_t>(); + [](PageList &pl, QPDFPageObjectHelper &page) { + try { + pl.doc.removePage(page); + } catch (const QPDFExc &e) { + throw py::value_error("Page is not referenced in the PDF"); + } + }) + .def( + "remove", + [](PageList &pl, py::ssize_t pnum) { if (pnum <= 0) // Indexing past end is checked in .get_page throw py::index_error( "page access out of range in 1-based indexing"); pl.delete_page(pnum - 1); - }) + }, + py::kw_only(), + py::arg("p")) .def("index", [](PageList &pl, const QPDFObjectHandle &h) { return page_index(*pl.qpdf, h); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/core/qpdf_pagelist.h new/pikepdf-8.10.1/src/core/qpdf_pagelist.h --- old/pikepdf-8.9.0/src/core/qpdf_pagelist.h 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/core/qpdf_pagelist.h 2023-12-17 10:31:00.000000000 +0100 @@ -14,8 +14,7 @@ class PageList { // LCOV_EXCL_LINE public: - PageList(std::shared_ptr<QPDF> q, py::size_t iterpos = 0) - : iterpos(iterpos), qpdf(q), doc(*qpdf){}; + PageList(std::shared_ptr<QPDF> q) : qpdf(q), doc(*qpdf){}; QPDFPageObjectHelper get_page(py::size_t index); py::list get_pages(py::slice slice); @@ -26,13 +25,26 @@ py::size_t count(); void insert_page(py::size_t index, py::handle obj); void insert_page(py::size_t index, QPDFPageObjectHelper page); + void append_page(py::handle obj); + void append_page(QPDFPageObjectHelper page); public: - py::size_t iterpos; std::shared_ptr<QPDF> qpdf; QPDFPageDocumentHelper doc; private: std::vector<QPDFPageObjectHelper> get_page_objs_impl(py::slice slice); - void try_insert_qpdfobject_as_page(py::size_t index, py::handle obj); + QPDFPageObjectHelper page_from_object(py::handle obj); }; + +class PageListIterator { // LCOV_EXCL_LINE +public: + PageListIterator(PageList &pl, size_t index) + : pl(pl), index(index), pages(pl.doc.getAllPages()){}; + QPDFPageObjectHelper next(); + +private: + PageList &pl; + size_t index; + std::vector<QPDFPageObjectHelper> pages; +}; \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/pikepdf/_core.pyi new/pikepdf-8.10.1/src/pikepdf/_core.pyi --- old/pikepdf-8.9.0/src/pikepdf/_core.pyi 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/pikepdf/_core.pyi 2023-12-17 10:31:00.000000000 +0100 @@ -467,9 +467,11 @@ def __delitem__(self, name: str | Name | int) -> None: ... def __dir__(self) -> list: ... def __eq__(self, other: Any) -> bool: ... + def __float__(self) -> float: ... def __getattr__(self, name: str) -> Object: ... def __getitem__(self, name: str | Name | int) -> Object: ... def __hash__(self) -> int: ... + def __int__(self) -> int: ... def __iter__(self) -> Iterable[Object]: ... def __len__(self) -> int: ... def __setattr__(self, name: str, value: Any) -> None: ... @@ -817,15 +819,36 @@ class Attachments(MutableMapping[str, AttachedFileSpec]): """Exposes files attached to a PDF. + If a file is attached to a PDF, it is exposed through this interface. + For example ``p.attachments['readme.txt']`` would return a + :class:`pikepdf._core.AttachedFileSpec` that describes the attached file, + if a file were attached under that name. + ``p.attachments['readme.txt'].get_file()`` would return a + :class:`pikepdf._core.AttachedFile`, an archaic intermediate object to support + different versions of the file for different platforms. Typically one + just calls ``p.attachments['readme.txt'].read_bytes()`` to get the + contents of the file. + This interface provides access to any files that are attached to this PDF, exposed as a Python :class:`collections.abc.MutableMapping` interface. The keys (virtual filenames) are always ``str``, and values are always :class:`pikepdf.AttachedFileSpec`. + To create a new attached file, use + :meth:`pikepdf._core.AttachedFileSpec.from_filepath` + to create a :class:`pikepdf._core.AttachedFileSpec` and then assign it to the + :attr:`pikepdf.Pdf.attachments` mapping. If the file is in memory, use + ``p.attachments['test.pdf'] = b'binary data'``. + Use this interface through :attr:`pikepdf.Pdf.attachments`. .. versionadded:: 3.0 + + .. versionchanged:: 8.10.1 + Added convenience interface for directly loading attached files, e.g. + ``pdf.attachments['/test.pdf'] = b'binary data'``. Prior to this release, + there was no way to attach data in memory as a file. """ def __contains__(self, k: object) -> bool: ... @@ -834,7 +857,7 @@ def __getitem__(self, k: str) -> AttachedFileSpec: ... def __iter__(self) -> Iterator[str]: ... def __len__(self) -> int: ... - def __setitem__(self, k: str, v: AttachedFileSpec): ... + def __setitem__(self, k: str, v: AttachedFileSpec | bytes): ... def __init__(self, *args, **kwargs) -> None: ... def _add_replace_filespec(self, arg0: str, arg1: AttachedFileSpec) -> None: ... def _get_all_filespecs(self) -> dict[str, AttachedFileSpec]: ... @@ -1341,10 +1364,11 @@ """For accessing pages in a PDF. A ``list``-like object enumerating a range of pages in a :class:`pikepdf.Pdf`. - It may be all of the pages or a subset. + It may be all of the pages or a subset. Obtain using :attr:`pikepdf.Pdf.pages`. + + See :class:`pikepdf.Page` for accessing individual pages. """ - def __init__(self, *args, **kwargs) -> None: ... def append(self, page: Page) -> None: """Add another page to the end. @@ -1372,7 +1396,7 @@ Raises an exception if no page matches. """ - def index(self, page: Page | Object) -> int: + def index(self, page: Page) -> int: """Given a page, find the index. That is, returns ``n`` such that ``pdf.pages[n] == this_page``. @@ -1398,29 +1422,33 @@ function does not account for that. Use :attr:`pikepdf.Page.label` to get the page label for a page. """ - def remove(self, *, p: int) -> None: - """Remove a page (using 1-based numbering). + def remove(self, page: Page | None = None, *, p: int) -> None: + """Remove a page. Args: - p: 1-based page number + page: If page is not None, remove that page. + p: 1-based page number to remove, if page is None. """ def reverse(self) -> None: """Reverse the order of pages.""" @overload - def __delitem__(self, arg0: int) -> None: ... + def __delitem__(self, idx: int) -> None: ... @overload - def __delitem__(self, arg0: slice) -> None: ... + def __delitem__(self, sl: slice) -> None: ... @overload - def __getitem__(self, arg0: int) -> Page: ... + def __getitem__(self, idx: int) -> Page: ... @overload - def __getitem__(self, arg0: slice) -> list[Page]: ... - def __iter__(self) -> PageList: ... + def __getitem__(self, sl: slice) -> list[Page]: ... + def __iter__(self) -> _PageListIterator: ... def __len__(self) -> int: ... - def __next__(self) -> Page: ... @overload - def __setitem__(self, arg0: int, arg1: Page) -> None: ... + def __setitem__(self, idx: int, page: Page) -> None: ... @overload - def __setitem__(self, arg0: slice, arg1: Iterable[Page]) -> None: ... + def __setitem__(self, sl: slice, pages: Iterable[Page]) -> None: ... + +class _PageListIterator: + def __iter__(self) -> _PageListIterator: ... + def __next__(self) -> Page: ... class Pdf: def _repr_mimebundle_(include: Any = ..., exclude: Any = ...) -> Any: @@ -2771,7 +2799,6 @@ def _new_string_utf8(s: str) -> String: """Low-level function to construct a PDF String object from UTF-8 bytes.""" -def _test_file_not_found(*args, **kwargs) -> Any: ... def _translate_qpdf_logic_error(arg0: str) -> str: ... def get_decimal_precision() -> int: """Set the number of decimal digits to use when converting floats.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/pikepdf/_methods.py new/pikepdf-8.10.1/src/pikepdf/_methods.py --- old/pikepdf-8.9.0/src/pikepdf/_methods.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/pikepdf/_methods.py 2023-12-17 10:31:00.000000000 +0100 @@ -55,7 +55,7 @@ Numeric = TypeVar('Numeric', int, float, Decimal) -def _single_page_pdf(page) -> bytes: +def _single_page_pdf(page: Page) -> bytes: """Construct a single page PDF from the provided page in memory.""" pdf = Pdf.new() pdf.pages.append(page) @@ -666,7 +666,7 @@ bundle = {k for k in bundle if k in include} if exclude: bundle = {k for k in bundle if k not in exclude} - pagedata = _single_page_pdf(self.obj) + pagedata = _single_page_pdf(self) if 'application/pdf' in bundle: data['application/pdf'] = pagedata if 'image/png' in bundle: @@ -700,7 +700,9 @@ raise KeyError(k) return filespec - def __setitem__(self, k: str, v: AttachedFileSpec) -> None: + def __setitem__(self, k: str, v: AttachedFileSpec | bytes) -> None: + if isinstance(v, bytes): + return self._attach_data(k, v) if not v.filename: v.filename = k return self._add_replace_filespec(k, v) @@ -715,7 +717,9 @@ yield from self._get_all_filespecs() def __repr__(self): - return f"<pikepdf._core.Attachments with {len(self)} attached files>" + if len(self) == 0: + return "<pikepdf._core.Attachments no attached files>" + return f"<pikepdf._core.Attachments: {list(self)}>" @augments(AttachedFileSpec) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/pikepdf/_version.py new/pikepdf-8.10.1/src/pikepdf/_version.py --- old/pikepdf-8.9.0/src/pikepdf/_version.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/src/pikepdf/_version.py 2023-12-17 10:31:00.000000000 +0100 @@ -3,4 +3,4 @@ from __future__ import annotations -__version__ = "8.9.0" +__version__ = "8.10.1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/src/pikepdf.egg-info/PKG-INFO new/pikepdf-8.10.1/src/pikepdf.egg-info/PKG-INFO --- old/pikepdf-8.9.0/src/pikepdf.egg-info/PKG-INFO 2023-12-11 00:26:49.000000000 +0100 +++ new/pikepdf-8.10.1/src/pikepdf.egg-info/PKG-INFO 2023-12-17 10:33:05.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pikepdf -Version: 8.9.0 +Version: 8.10.1 Summary: Read and write PDFs with Python, powered by qpdf Author-email: "James R. Barlow" <ja...@purplerock.ca> License: MPL-2.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_attachments.py new/pikepdf-8.10.1/tests/test_attachments.py --- old/pikepdf-8.9.0/tests/test_attachments.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_attachments.py 2023-12-17 10:31:00.000000000 +0100 @@ -33,7 +33,7 @@ assert len(pal.attachments) == 1, "attachment count not incremented" assert 'rle.pdf' in pal.attachments, "attachment filename not registered" - assert 'attached' in repr(pal.attachments) + assert 'rle.pdf' in repr(pal.attachments), "attachment filename not enumerated" pal.save(outpdf) @@ -157,3 +157,9 @@ assert 'foo' not in repr(fs) assert 'AttachedFile' in repr(fs.get_file()) assert fs.relationship == Name.Data + + +def test_attach_direct(pal): + data = b'some data' + pal.attachments['direct.txt'] = data + assert pal.attachments['direct.txt'].get_file().read_bytes() == data diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_errors.py new/pikepdf-8.10.1/tests/test_errors.py --- old/pikepdf-8.9.0/tests/test_errors.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_errors.py 2023-12-17 10:31:00.000000000 +0100 @@ -46,7 +46,7 @@ def test_system_error(): with pytest.raises(FileNotFoundError): - pikepdf._core._test_file_not_found() + pikepdf._core._test.fopen_nonexistent_file() @skip_if_pypy diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_formxobject.py new/pikepdf-8.10.1/tests/test_formxobject.py --- old/pikepdf-8.9.0/tests/test_formxobject.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_formxobject.py 2023-12-17 10:31:00.000000000 +0100 @@ -3,7 +3,7 @@ from __future__ import annotations -from pikepdf import Dictionary, Name, Object, Pdf, Stream +from pikepdf import Dictionary, Name, Object, Page, Pdf, Stream # pylint: disable=e1137 @@ -30,7 +30,7 @@ image = Stream(pdf, image_data) image.stream_dict = Object.parse( - """ + b""" << /Type /XObject /Subtype /Image @@ -71,13 +71,15 @@ contents = Stream(pdf, stream) - page = pdf.make_indirect( - { - '/Type': Name('/Page'), - '/MediaBox': mediabox, - '/Contents': contents, - '/Resources': resources, - } + page = Page( + pdf.make_indirect( + { + '/Type': Name('/Page'), + '/MediaBox': mediabox, + '/Contents': contents, + '/Resources': resources, + } + ) ) pdf.pages.append(page) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_io.py new/pikepdf-8.10.1/tests/test_io.py --- old/pikepdf-8.9.0/tests/test_io.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_io.py 2023-12-17 10:31:00.000000000 +0100 @@ -197,7 +197,7 @@ def test_logging(caplog): caplog.set_level(logging.INFO) - pikepdf._core._log_info("test log message") + pikepdf._core._test.log_info("test log message") assert [("pikepdf._core", logging.INFO)] == [ (rec[0], rec[1]) for rec in caplog.record_tuples ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_matrix.py new/pikepdf-8.10.1/tests/test_matrix.py --- old/pikepdf-8.9.0/tests/test_matrix.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_matrix.py 2023-12-17 10:31:00.000000000 +0100 @@ -11,6 +11,7 @@ from pikepdf import Array from pikepdf._core import Matrix, Rectangle from pikepdf.models import PdfMatrix +from pikepdf.objects import Dictionary def allclose(m1, m2, abs_tol=1e-6): @@ -69,6 +70,21 @@ def test_default_is_identity(self): assert Matrix() == Matrix(1, 0, 0, 1, 0, 0) + def test_not_enough_args(self): + with pytest.raises(TypeError): + Matrix(1, 2, 3, 4, 5) + + def test_tuple(self): + assert Matrix() == Matrix((1, 0, 0, 1, 0, 0)) + with pytest.raises(ValueError): + Matrix((1, 2, 3, 4, 5)) + + def test_failed_object_conversion(self): + with pytest.raises(ValueError): + assert Matrix(Array([1, 2, 3])) + with pytest.raises(ValueError): + assert Matrix(Dictionary(Foo=1)) + def test_accessors(self): m = Matrix(1, 2, 3, 4, 5, 6) assert m.a == 1 @@ -145,3 +161,6 @@ assert (0, 0) < m.transform((1, 0)) < (1, 1) m = Matrix().rotated(-45) assert (0, 0) < m.transform((1, 0)) < (1, -1) + + def test_latex(self): + assert '\\begin' in Matrix(1, 0, 0, 1, 0, 0)._repr_latex_() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_object.py new/pikepdf-8.10.1/tests/test_object.py --- old/pikepdf-8.9.0/tests/test_object.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_object.py 2023-12-17 10:31:00.000000000 +0100 @@ -112,14 +112,14 @@ except InvalidOperation: return # PDF doesn't support exponential notation try: - py_d = Object.parse(decstr) + py_d = Object.parse(decstr.encode('pdfdoc')) except RuntimeError as e: if 'overflow' in str(e) or 'underflow' in str(e): - py_d = Object.parse(str(f)) + py_d = Object.parse(f.encode('pdfdoc')) assert isclose(py_d, d, abs_tol=1e-5), (d, f.hex()) else: - with pytest.raises(PdfError): + with pytest.raises(PdfError), pytest.deprecated_call(): Object.parse(str(d)) @@ -325,11 +325,11 @@ assert Name('/Foo') != String('/Foo') def test_numbers(self): - self.check(Object.parse('1.0'), 1) - self.check(Object.parse('42'), 42) + self.check(Object.parse(b'1.0'), 1) + self.check(Object.parse(b'42'), 42) def test_bool_comparison(self): - self.check(Object.parse('0.0'), False) + self.check(Object.parse(b'0.0'), False) self.check(True, 1) def test_string(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_page.py new/pikepdf-8.10.1/tests/test_page.py --- old/pikepdf-8.9.0/tests/test_page.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_page.py 2023-12-17 10:31:00.000000000 +0100 @@ -130,14 +130,16 @@ def test_add_unowned_page(): # issue 174 pdf = Pdf.new() d = Dictionary(Type=Name.Page) - pdf.pages.append(d) + pdf.pages.append(Page(d)) def test_failed_add_page_cleanup(): pdf = Pdf.new() d = Dictionary(Type=Name.NotAPage) num_objects = len(pdf.objects) - with pytest.raises(TypeError, match="only pages can be inserted"): + with pytest.raises( + TypeError, match="only pages can be inserted" + ), pytest.deprecated_call(): pdf.pages.append(d) assert len(pdf.pages) == 0 @@ -148,7 +150,9 @@ # But we'd better not delete an existing object... d2 = pdf.make_indirect(Dictionary(Type=Name.StillNotAPage)) - with pytest.raises(TypeError, match="only pages can be inserted"): + with pytest.raises( + TypeError, match="only pages can be inserted" + ), pytest.deprecated_call(): pdf.pages.append(d2) assert len(pdf.pages) == 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_pages.py new/pikepdf-8.10.1/tests/test_pages.py --- old/pikepdf-8.9.0/tests/test_pages.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_pages.py 2023-12-17 10:31:00.000000000 +0100 @@ -202,11 +202,10 @@ pdf.pages[0::2] = pdf2.pages[0:1] -@pytest.mark.timeout(1) def test_self_extend(fourpages): pdf = fourpages - with pytest.raises(ValueError, match="source page list modified during iteration"): - pdf.pages.extend(pdf.pages) + pdf.pages.extend(pdf.pages) + assert len(pdf.pages) == 8 def test_one_based_pages(fourpages): @@ -233,7 +232,7 @@ pdf = fourpages with pytest.raises(TypeError): pdf.pages.insert(0, 'this is a string not a page') - with pytest.raises(TypeError): + with pytest.raises(TypeError), pytest.deprecated_call(): pdf.pages.insert(0, Dictionary(Type=Name.NotAPage, Value="Not a page")) @@ -361,10 +360,20 @@ fourpages.pages.remove(p=-1) +def test_remove_by_ref(fourpages): + second_page = fourpages.pages[1] + assert second_page == fourpages.pages[1] + fourpages.pages.remove(second_page) + assert second_page not in fourpages.pages + assert len(fourpages.pages) == 3 + with pytest.raises(ValueError): + fourpages.pages.remove(second_page) + + def test_pages_wrong_type(fourpages): - with pytest.raises(TypeError): + with pytest.raises(TypeError), pytest.deprecated_call(): fourpages.pages.insert(3, {}) - with pytest.raises(TypeError): + with pytest.raises(TypeError), pytest.deprecated_call(): fourpages.pages.insert(3, Array([42])) @@ -478,7 +487,7 @@ p = Pdf.new() d = Dictionary(Type=Name.Page, MediaBox=[0, 0, 612, 792], Resources=Dictionary()) for n in range(5): - p.pages.append(d) + p.pages.append(Page(d)) p.pages[n].Contents = Stream(p, b"BT (Page %s) Tj ET" % str(n).encode()) p.Root.PageLabels = p.make_indirect( @@ -529,3 +538,11 @@ ) with pytest.raises(ValueError): graph.pages.from_objgen(graph.pages[0].Contents.objgen) + + +def test_page_iteration(graph, fourpages): + fourpages_iter = iter(fourpages.pages) + next(fourpages_iter) # Discard + next(fourpages_iter) # Discard + graph.pages.extend(fourpages_iter) # Append remaining two + assert len(graph.pages) == 3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pikepdf-8.9.0/tests/test_sanity.py new/pikepdf-8.10.1/tests/test_sanity.py --- old/pikepdf-8.9.0/tests/test_sanity.py 2023-12-11 00:24:59.000000000 +0100 +++ new/pikepdf-8.10.1/tests/test_sanity.py 2023-12-17 10:31:00.000000000 +0100 @@ -23,7 +23,7 @@ from packaging.version import Version import pikepdf -from pikepdf import Name, Object, Pdf, Stream +from pikepdf import Name, Object, Page, Pdf, Stream def test_minimum_qpdf_version(): @@ -106,7 +106,7 @@ '/Resources': resources, } qpdf_page_dict = page_dict - page = pdf.make_indirect(qpdf_page_dict) + page = Page(pdf.make_indirect(qpdf_page_dict)) pdf.pages.append(page) pdf.save(outdir / 'hi.pdf')