v10

Breaking changes for v10.x

  • Dropped Python 3.9 compatibility, since it is end of life. Python 3.10 through 3.14 are supported. (Python 3.10 was later dropped in v10.14.0.)

  • Dropped macOS 13 support, since it is end of life.

  • Dropped macOS 14 Intel wheels, because GitHub doesn’t provide a way to build them - macOS 15 Intel works fine.

  • Dropped macOS Intel wheels and macOS 14 wheels in v10.14.0. Binary wheels now require macOS 15 or newer on Apple silicon.

  • Dropped deprecated method Pdf.check() (use .check_pdf_syntax()).

pikepdf supports free-threaded (no-GIL) CPython. Starting with v10.8.0, pikepdf publishes free-threaded CPython 3.14 (cp314t) wheels to PyPI; before v10.8.0, free-threaded use required building from source. As always, coordinating concurrent modification of the same object across threads requires a lock – see the architecture notes on thread safety.

v10.18.0

  • pikepdf.Pdf.add_pages_from() now carries layers (optional content) with the pages it copies. The optional content groups that the copied pages depend on are listed in the destination’s /OCProperties, with the default visibility, place in the layer list, lock, radio button groups and automatic states that the source’s default configuration gave them. Previously the groups were copied but not listed, so viewers ignored them: the layer list was empty and content hidden in the source was shown. #118

  • pikepdf.Pdf.add_pages_from() now carries bookmarks. Outline items of the source that lead to a copied page are appended to the destination’s outline, keeping their hierarchy, titles and style; items that lead to pages that were not copied are dropped. #281

  • Both are on by default, which changes what existing callers of add_pages_from get: the destination may gain an /OCProperties and outline items it did not gain before. Pass layers='ignore' or outlines='ignore' for the previous behaviour, for example if you build the merged outline yourself. pikepdf.PageCopyResult reports layers_added, outline_items_added and outline_items_dropped.

  • Reading the label of a page whose page label style is an unknown name, such as /S /Z, now warns and keeps the label prefix, as an invalid non-name style already did. Previously it raised KeyError.

  • Extracting an image whose /CalGray or /CalRGB colour space array lacks its parameter dictionary now decodes it as the corresponding device space, without an ICC profile, as a missing /WhitePoint already did. Previously it raised IndexError.

  • pikepdf.Pdf.check_pdf_syntax() now performs the same checks as qpdf --check and reports the same problems. It checks the linearization data and hint tables of linearized files, which previously required a separate call to pikepdf.Pdf.check_linearization(), and checks the interactive form, embedded files, page labels and outlines. A document with no pages is reported as ERROR: file does not contain any pages, and a page whose content stream cannot be read is reported as ERROR: page N: ... along with the other problems found, where it previously raised pikepdf.PdfError. #515

  • Free-threaded Python: fixed crashes when threads used pages of the same Pdf at once. The page boxes (pikepdf.Page.mediabox and the others), pikepdf.Page.rotation, pikepdf.Page.rotate(), pikepdf.Page.as_form_xobject(), pikepdf.Page.contents_coalesce() and several other pikepdf.Page methods read or changed the Pdf without holding its lock.

  • Free-threaded Python: fixed a crash when the last reference to a Pdf was dropped while another thread was using one of its objects.

  • Free-threaded Python: fixed a data race when a content stream was parsed, with pikepdf.parse_content_stream() or pikepdf.Page.parse_contents(), while another thread used the same Pdf.

  • Free-threaded Python: fixed a data race on the source Pdf when a page was appended, inserted or assigned into the page list of another Pdf. Both documents are now locked, as pages.extend() already did.

  • The test suite now runs under ThreadSanitizer on free-threaded Python in continuous integration, which is how the fixes above were found.

  • Fixed building from source on Android (Termux), where importing pikepdf failed with cannot locate symbol "_Py_TrueStruct" since v10.6.0. The extension module is now linked against libpython when the build interpreter is an Android Python, as that platform requires. Android is not a tested platform. #752

v10.17.0

  • Added pikepdf.ObjectSet, which records the objects that a walk of a PDF’s object graph has already visited. Objects in a PDF are shared and may refer back to their ancestors, so a search that only limits its depth can take exponential time on quite ordinary files. ObjectSet.add() reports whether an object is new, and tells apart direct objects and objects of different Pdf, which a set of objgen cannot. See Following references safely.

  • Added pikepdf.Object.is_same_object_as(), which tests whether two references lead to the same object. Neither is, == nor comparing objgen answers this reliably.

  • Added pikepdf.Pdf.content_scopes() and pikepdf.Page.content_scopes(), which iterate over every content stream that pages can draw, each with the resource dictionary it uses: the page itself, form XObjects, annotation appearance streams, tiling patterns, Type 3 glyph procedures and soft masks, however they are nested or shared. This is the way to find every font, image or colour space a document uses. Each pikepdf.ContentScope is reported once for the document; pages= selects pages and kinds= selects what is reported.

  • Added pikepdf.Page.get_resources(), which returns the resources of a page or form XObject for reading. Unlike pikepdf.Page.resources, it finds resources inherited from the page tree, does not create an empty dictionary when there are none, and returns None instead of raising when /Resources is malformed.

  • Fixed comparing two objects with == taking exponential time when the objects being compared share children, such as the same file opened twice. Each pair of objects is now compared once.

  • Fixed pikepdf.Pdf.add_pages_from() hanging on a page with an action whose /Next actions are shared or form a loop.

  • Fixed pikepdf.Pdf.open_outline() reading only the first item at each level of an outline whose items are direct objects. Such items are now made indirect when the outline is saved, as the PDF specification requires.

  • Reading a stream that will not decode now raises DataDecodingError with the reason, whether the stream’s data was read from a file or set from memory. Previously a stream read from a file raised a PdfError that said only “read_bytes called on unfilterable stream”, and the reason was available only from pikepdf.Pdf.get_warnings(). DataDecodingError is a PdfError, so existing handlers are unaffected. #182

  • An exception raised by a JBIG2 decoder now reaches the caller of pikepdf.PdfImage.as_pil_image(), pikepdf.PdfImage.extract_to() and pikepdf.PdfImage.read_bytes() unchanged, with its own type, message and traceback. Previously, for an image read from a file, it was replaced by a generic DataDecodingError or PdfError. #182

  • Added QpdfRuntimeError, raised for errors from qpdf that pikepdf has no more specific exception for. These were previously raised as a bare RuntimeError, which except pikepdf.PikepdfError did not catch. The new exception derives from both PikepdfError and RuntimeError, so handlers written for RuntimeError still work. #240

  • The empty name / is now supported. The PDF 1.7 and 2.0 specifications define a lone slash as a valid name made of zero characters, but pikepdf.Name('/') raised ValueError, and dictionaries refused / as a key. This broke pikepdf.Pdf.add_pages_from() on files with an empty-named destination such as /Dests << / 3 0 R >>. Such names can now be created, used as dictionary keys, and migrated during page copying. bool(Name('/')) is False, like an empty pikepdf.String. Name(''), which lacks the leading slash, is still rejected. PDF/A does not forbid empty names, and the PDF/A validator accepts them. Thanks to @sjudd for the report and @DSeaStar for the analysis. #745

  • AcroFormField.get_inheritable_field_value_as_name() now returns a pikepdf.Name, as documented, instead of a str. When the value is missing or not a name, it returns the empty name Name('/'), which is falsy, instead of ''. Comparisons such as == '/Tx' are unaffected. #748

  • Fixed an out-of-bounds read when opening a PDF from a Python file object whose readinto() reports more bytes than were requested. This now raises ValueError, as writing to such a stream already did. Non-blocking streams, whose readinto() returns None, now raise ValueError instead of hanging. Thanks to @Nayana-Naik73. #747

  • Fixed building from source on PyPy, which failed to compile since v10.0. PyPy now reads Python file objects through readinto(), like CPython, instead of copying the result of read().

  • pikepdf.open() reads directly from the file descriptor when given a filename, instead of calling the seek() and readinto() methods of a Python file object for every read. Opening and reading a large file with many objects is about four to five times faster, matching the speed of memory mapping (access_mode=AccessMode.mmap). Streams passed to pikepdf.open(), including plain file objects, are read through their methods as before.

  • Documented pikepdf.TokenFilter with worked examples that shorten numbers and convert RGB colors to grayscale, and explained when to use a token filter instead of pikepdf.parse_content_stream(). The API reference no longer says that the parser should not be used to edit content streams. The TokenFilter.handle_token documentation now says correctly that exceptions it raises propagate to the caller. #62

v10.16.0

  • Added binary wheels for Windows on ARM64. Thanks to @ndabas. #744

  • Binary wheels now bundle qpdf 12.4.2, which is also the new minimum version of qpdf, since it is the first release with Windows ARM64 binaries.

  • pikepdf.sanitize.remove_javascript(), pikepdf.sanitize.remove_external_access(), pikepdf.sanitize.remove_multimedia() and the matching pikepdf.sanitize.Sanitizer steps missed actions attached to form field dictionaries. Actions survived on a field stored separately from its widget annotation, on a hidden field with no widget (such as a calculation helper), on nested fields under /Kids, and on widgets that appear on no page. The sanitizer now walks the whole /AcroForm field tree. Thanks to Stjorn for the report.

  • The same sanitizer steps left an action /Next chain untouched past its depth limit of 50, so a targeted action (for example a /JavaScript action) buried deeper survived, and the step that preserves benign downstream actions could promote it to a shallower position. The chain is now severed at the limit, which also drops any benign actions past it. Thanks to @Nayana-Naik73. #746

  • The sanitizer now also drops anything in a /Next chain that is not an action dictionary, such as a nested array, instead of passing it through unexamined. It raised AttributeError or ValueError on some such values (for example /Next [42]), and now runs in explicit conversion mode regardless of the caller’s settings, so malformed values of the wrong type are skipped rather than crashing it.

v10.15.0

PDF/A-3 attachments

  • Attaching a file through Pdf.attachments now lists its file specification in the document catalog’s /AF (associated files) array, and deleting or replacing an attachment removes its entry. Together with the /AFRelationship pikepdf already writes, a file attached to a PDF/A-3 document now keeps it conformant. qpdf maintains only the /EmbeddedFiles name tree, so this was previously left to the user. #463

  • The PDF/A validator now approves embedded files in PDF/A-3b documents: the /EmbeddedFiles name tree and the catalog’s /AF array are checked against clause 6.8 of ISO 19005-3 (MIME type, /F and /UF, /AFRelationship, and that every embedded file is an associated file). Embedded files anywhere else, such as in file attachment annotations, are still reported as unsupported; they remain violations in PDF/A-1b and unsupported in PDF/A-2b. pikepdf.pdfa.prepare() gives PDF/A-3 embedded files a missing MIME type (application/octet-stream) and /AFRelationship (/Unspecified), and lists unassociated ones in the catalog’s /AF array; new pikepdf.pdfa.PrepareResult fields count these repairs. See Attachments in PDF/A-3.

PDF/A named destinations

  • The PDF/A validator now checks named destinations, in the /Dests name tree and in the catalog’s /Dests dictionary, instead of reporting them as unsupported. Named destinations are common: pdfTeX (hyperref), Acrobat Distiller and JasperReports write them. It also checks named appearance streams (/Names /AP) as Form XObjects.

  • /AlternatePresentations is now reported as a violation of PDF/A-2 and PDF/A-3 (clause 6.10), and document-level JavaScript (/Names /JavaScript) as a violation of the rule forbidding JavaScript actions; both were reported as unsupported. pikepdf.pdfa.prepare() removes them.

  • pikepdf.pdfa.prepare() gives the encoding of a non-symbolic TrueType font that has /Differences but no /BaseEncoding the base encoding /WinAnsiEncoding, as PDF/A-2 and PDF/A-3 require, when no glyph the document draws would change. Older producers such as OpenPDF write such encodings. New pikepdf.pdfa.PrepareResult fields report these repairs.

PDF/A developer extensions

  • The PDF/A validator now checks the Adobe developer extensions dictionary (/Extensions << /ADBE ... >>) that Acrobat writes in the document catalog, instead of reporting every such file as unsupported. qpdf rewrites this entry to match the written version and extension level, and removes it for PDF/A-1, and the validator checks the entry as it will be written. Other developer extensions remain unsupported.

  • pikepdf.pdfa.resolve_save_kwargs() accepts an extension level in min_version or force_version for PDF/A-2 and PDF/A-3, such as ('1.7', 8).

Metadata

  • New pikepdf.models.PdfMetadata.copy_properties() copies named XMP properties from other metadata with their whole value: every language of a language alternative, array order, structures and qualifiers, which assigning a value read from the source would lose. Properties are read from every rdf:Description whatever its rdf:about, and the structure of known properties is checked as for assignment. This is useful to restore metadata another tool dropped while rewriting a document.

PDF/A annotations

Fixes

  • The PDF/A validator reported the standard stream key /DL (decoded length) as unsupported on content streams, images, metadata streams and output intent profiles, so a file whose producer writes it could not be converted. It is now accepted on every stream.

  • The PDF/A validator accepted glyph names in a non-symbolic TrueType font’s /Differences only from the Adobe Glyph List for New Fonts, so it rejected valid PDF/A-2 and PDF/A-3 files that used other names from the full Adobe Glyph List, such as afii10017 for Cyrillic text. It now accepts every name in the full list, as ISO 19005 requires and veraPDF checks. Names outside the list, such as uni0410 or A.sc, are still rejected.

  • A Pdf opened or created with conversion_mode='explicit' returned plain int for some numbers that qpdf writes itself, such as /Pages /Count after adding or removing a page, /Rotate after pikepdf.Page.rotate(), or the /BBox of pikepdf.Page.as_form_xobject(). These values now follow the document’s conversion mode like any other.

  • In explicit conversion mode, Integer and Real now support the rest of the Python number protocol, with the same results as the int or Decimal that implicit mode produces: round(), math.trunc(), math.floor(), math.ceil(), divmod(), and format specs such as f'{x:.2f}'. int() of a Real now truncates, as int() of a Decimal does, instead of raising TypeError; for example int(page.Rotate) now works on a /Rotate 90.0.

  • The type stubs for Matrix now accept a pikepdf.Object, such as a /Matrix array, and a list of objects, as the runtime always has.

v10.14.0

Several improvements to explicit conversion mode and NamePath, prompted by the OCRmyPDF project’s migration to these APIs (pikepdf.explicit_conversion(), the as_* safe accessors, and NamePath) in a production codebase that reads untrusted, often malformed, PDFs.

Python support

  • Dropped support for Python 3.10, which reaches end of life on October 31, 2026. Python 3.11 through 3.14 are supported, and wheels are no longer built for CPython 3.10. pip will keep installing pikepdf 10.13 on Python 3.10.

  • Internal code now uses Python 3.11 features: tomllib, enum.StrEnum, typing.Self, datetime.UTC and the broader ISO 8601 support in datetime.fromisoformat. The compatibility shims for Python 3.10 are gone.

  • CI now tests against CPython 3.15 (release candidate). CPython 3.15 uses the existing cp314-abi3 wheel; free-threaded CPython 3.15 gets its own cp315t wheel, since the stable ABI does not cover free-threaded builds.

Platform support

  • macOS wheels are now built on GitHub’s macos-15 runner, since GitHub is deprecating the macos-14 runner. Binary wheels now require macOS 15 or newer.

  • Dropped macOS Intel (x86_64) wheels. We no longer test macOS on Intel, since the platform is end of life. Intel Mac users can build from source, or keep using pikepdf 10.13.

Conversion mode

  • pikepdf.Pdf.open() and pikepdf.Pdf.new() now accept a keyword-only conversion_mode argument ('implicit' or 'explicit'), and the mode can be read or changed afterward through the new pikepdf.Pdf.conversion_mode property. A Pdf’s mode travels with it across threads and does not affect any other document, which makes it the right scope for a library embedded inside a host application. Setting the property to None reverts to inheriting the context-manager/global mode.

  • Added pikepdf.implicit_conversion(), a thread-local context manager symmetric with pikepdf.explicit_conversion(), so implicit mode can be forced from inside code that runs under explicit mode.

  • Precedence when several scopes are in play: context manager, then per-Pdf mode, then the global default set by pikepdf.set_object_conversion_mode(). An object with no owning Pdf (e.g. a bare pikepdf.Dictionary(...) you have not yet attached to a document) resolves through the context-manager/global scopes; once it is inserted into a Pdf it follows that document’s mode.

  • Added pikepdf.Object.get_raw(), which behaves like get() (key, Name, or NamePath, plus a default) but never unboxes the result: it always returns a pikepdf.Object regardless of the current conversion mode. A null stored inside an array comes back as a Null-typed object rather than None; in a dictionary, qpdf treats a key whose value is null as absent, so get_raw returns the default for it, exactly as get does.

  • Added container-level typed getters get_int(), get_bool(), get_float(), get_decimal(), get_dict(), and get_list(), each (key_or_path, default=None). These compose get_raw with the matching as_* accessor, so reading an optional value of uncertain type is a mode-independent one-liner instead of a hand-written with pikepdf.explicit_conversion(): ... wrapper.

  • Added coerce=True to as_int(), as_bool(), as_float(), and as_decimal(), and to the corresponding get_* getters, for reading values that real-world PDFs encode with a “nearby” type: as_int(coerce=True) accepts a Real (truncated toward zero) and a numeric String; as_bool(coerce=True) accepts a nonzero Integer or Real (fixing the common /Marked 1 case that previously required a fallback to as_int() != 0); as_float/as_decimal with coerce=True accept an Integer and a numeric String, including exponent notation such as "1e-5".

  • pikepdf.Integer and pikepdf.Real now support the ordering comparisons <, <=, > and >= against Python int, float, bool, Decimal, and each other, so box[0] < box[2], sorted(), min() and max() work on explicit-mode values. Real compares by its exact decimal value.

  • Arithmetic on pikepdf.Integer and pikepdf.Real now works between two objects (Real('2.5') + Integer(3)) and with Decimal and bool operands, and ** is supported. Results are the native Python type that implicit mode would have produced: int for Integer with int/Integer, Decimal whenever a Real or Decimal is involved, and float with a float operand. In explicit mode, box[2] - box[0] > 100 therefore works without unboxing, while raising TypeError if the PDF stored a non-number. The result is never a pikepdf object: a number computed from a document is not in the document, and returning an object would let one unmigrated read propagate into code far from the cause.

  • Added pikepdf.unbox(), which returns the native Python value of an Integer, Boolean or Real and passes any other value through unchanged. Unlike the as_* accessors it does not require knowing which numeric type a value has, and it accepts values read in either mode, so it is the one-token migration for a read that feeds isinstance, is True, Decimal(), json.dumps or a function documented to return a native type.

  • Added module-level typed conversions pikepdf.as_int(), as_bool(), as_float(), as_decimal(), as_dict(), as_list(), as_str() and as_bytes(), each (value, default=None), with keyword-only coerce for the numeric and boolean ones. They are the value-side twins of the get_* typed getters, for an array element, content stream operand or other value already in hand: a pikepdf object converts exactly as its as_* method does, and a native value is accepted only if it is the type implicit mode would have produced for that PDF type, so the result is the same in either conversion mode. pikepdf.as_int(True) gives the default, because a Boolean is not an Integer.

  • Added as_str() and as_bytes(), which return a String’s decoded text or raw bytes, and raise TypeError (or return a supplied default) for any other type, unlike str() and bytes(), which accept almost anything. Added the matching typed getters get_str() and get_bytes().

  • See Object model for the full description of scopes and precedence, plus a “Migrating to explicit mode” checklist.

  • Added a new topic page, PDF type safety with explicit mode, explaining why explicit mode exists and setting out the deprecation schedule: v11 warns on implicit conversions, v12 makes explicit mode the default, and v13 removes implicit mode. New code that reads values of uncertain type should prefer the mode-independent getters described above.

NamePath

  • path in obj now tests whether the path can be traversed to a value, and del obj[path] deletes the final component after traversing to its parent, both for name and array-index components.

  • NamePath is now iterable, yielding its individual components (str for names, int for indices) in order, e.g. list(NamePath.A.B[0]) gives ['/A', '/B', 0].

  • NamePath instances now compare and hash by their component sequence, so NamePath.A.B == NamePath.A.B is True and a NamePath can be used as a dict key or stored in a set.

  • isinstance(path, pikepdf.NamePath) now works, and pikepdf.NamePath can be used directly in type annotations (e.g. def f(key: pikepdf.Name | pikepdf.NamePath)) without importing a private name.

Metadata and PDF/A

OCRmyPDF tested its PDF/A output against veraPDF, and found files that pikepdf’s metadata handling made invalid.

  • XMP language alternatives such as dc:title and dc:description are now read from their x-default item, as the XMP specification defines, rather than from the first item. Acrobat writes x-default last when a document has several languages, so /Title and /Subject were synced from the wrong language, and PDF/A-1 validators rejected the result.

  • Behavior change: Setting a language alternative now updates its x-default item and keeps the other languages, instead of replacing all of them with the new value. An item that held the same text as the old default is updated too, as Adobe’s XMP toolkit does. Assigning several values to a language alternative that pikepdf does not know joins them into one x-default item, instead of writing several x-default items.

  • XMP properties are now read from every top-level rdf:Description, not only those with rdf:about="". Properties in Descriptions with rdf:about="uuid:..." (written by Distiller and older Acrobat) or with no rdf:about could not be read or deleted, and setting one added a duplicate, which made veraPDF reject the whole packet. Setting a property now removes any other occurrences of it, and deleting it removes all of them.

  • Behavior change: When XMP is written, every top-level rdf:Description gets the same rdf:about value, as the XMP specification requires, and empty Descriptions are removed. The value is kept if the non-empty values agree, and made empty if they conflict.

  • Added pikepdf.models.PdfMetadata.recovered and XmpDocument.recovered, which are True if the XMP was not well-formed and had to be repaired or replaced as it was read.

  • Added pikepdf.PdfInlineImage.read_raw_bytes(), which returns the still-encoded data of an inline image exactly as it appears in the content stream.

  • pikepdf.Pdf.save() now corrects /Count in each node of the page tree to the number of pages beneath it. qpdf repairs a damaged page tree but left /Count as it was, so a wrong /Count in the input was written unchanged, and veraPDF could not validate the file at all.

PDF/A validation and repair

  • Added the pikepdf.pdfa module, which prepares, checks and saves PDF/A-1b, PDF/A-2b and PDF/A-3b documents. See PDF/A validation and repair.

    • pikepdf.pdfa.prepare() repairs a document in memory: it installs a PDF/A output intent (sRGB by default, or a supplied ICC profile), removes image interpolation and hidden annotations, adds /CIDSet for PDF/A-1, and rewrites the XMP metadata with a PDF/A declaration. It returns a PrepareResult describing what changed: describe() gives a sentence per change, messages() the same sentences with a suggested log level, and PrepareResult.xmp_problem why an XMP packet could not be read. XMP properties are kept from every rdf:Description when all share one rdf:about, including the non-empty uuid:... value that Acrobat and pikepdf’s own metadata editor write.

    • pikepdf.pdfa.check() predicts the verdict for the file that would be written, without writing it, by modelling what qpdf’s writer changes (stream filters, trailer, version, encryption and which objects are written).

    • pikepdf.pdfa.save() prepares the document, writes it to a temporary file, reopens and validates the bytes written, and only then moves the file into place. If the written file does not pass, it raises PdfaError with the report (whose prepared records the repairs made), and the destination is left untouched.

    • pikepdf.pdfa.resolve_save_kwargs() returns the complete pikepdf.Pdf.save() settings for a flavour. Settings that PDF/A forbids or that would change the bytes after the check (such as encryption, normalize_content and fix_metadata_version) are pinned, and a conflicting value raises ValueError.

  • The validator is an allowlist: it approves only constructs it recognizes and knows to conform. A Report has a verdict of 'pass', 'fail' (at least one violation) or 'not_checked' (only constructs the validator does not check, which may be valid PDF/A). save accepts only 'pass'. Each Finding names a rule: a veraPDF rule id such as ISO_19005_2:6.2.8-3, or a pikepdf: id for local policies and unsupported constructs. The validator is not a certification; veraPDF remains the reference.

  • On the 2,906 files of the veraPDF test corpus, converted with prepare and checked against veraPDF 1.30 for each of PDF/A-1b, 2b and 3b, the validator approved no file that veraPDF rejects. For PDF/A-2b it approved 1,363 files, rejected 887 and left 656 not checked; veraPDF accepted 28 of the rejected files (the validator is conservative by design). The prediction from check matched the validation of the written file in every case.

  • The validator and the repairs give the same results whether a document is read in implicit or explicit conversion mode: they work in explicit mode internally, whatever mode the caller uses.

  • pikepdf.pdfa needs jsonschema, referencing and fontTools, available as the optional extra pip install 'pikepdf[pdfa]'. import pikepdf does not import them; import pikepdf.pdfa raises ImportError naming the extra if they are missing.

  • The PDF/A rule catalogue is derived from the veraPDF validation profiles (CC BY 4.0, veraPDF Consortium); see third-party-licenses/README.md.

  • Added the internal helper pikepdf._io.atomic_write_verified, which writes a file to a temporary location, calls a verification function on it, and moves it into place only if verification succeeds. pikepdf.pdfa.save uses it.

qpdf limits

Behavior changes

  • The minimum required qpdf version is now 12.4.1, and wheels bundle qpdf 12.4.1. Changes inherited from qpdf 12.4:

    • Content stream parsing (pikepdf.parse_content_stream(), pikepdf.Page.parse_contents()) now stops after 15 syntax errors, issuing a warning. Instructions after that point are dropped, so parsing and rewriting a badly damaged content stream can lose content. Use pikepdf.settings.set_qpdf_limits(parser_max_errors=0) to parse as much as possible.

    • A page tree nested more than 100 levels deep now raises pikepdf.PdfError when the pages are accessed.

    • /Rotate values outside [0, 360), such as -90, are now normalized when a page is converted to a form XObject, overlaid or rotation-flattened.

    • JSON output of real numbers no longer includes leading zeroes.

  • The progname keyword argument of pikepdf.Job is deprecated and ignored, and passing it issues a DeprecationWarning. It was passed to qpdf’s QPDFJob::initializeFromArgv as the name of an environment variable, not the program name, and qpdf 12.4 no longer uses it. The program name is taken from the first item of args, as it always was.

  • Behavior change: Reading PdfImage metadata such as .width, .height or .colorspace whose value has the wrong PDF type (for example a /Width written as a string or name) now raises TypeError, with a message naming the key and the value found, instead of NotImplementedError: Metadata access for Width. The exception is also a NotImplementedError, so existing handlers keep working.

  • Behavior change: A pikepdf.String no longer compares equal to bytes: pikepdf.String('abc') == b'abc' is now False. It still compares equal to the str it decodes to. A String compared equal to both a str and a bytes that are unequal to each other, and so could not hash like both of them; for non-ASCII text it hashed like neither, so {pikepdf.String('héllo'): 1}['héllo'] raised KeyError. A String now hashes like its str. To compare the raw data, use bytes(s) == b'...' or as_bytes().

  • Behavior change: A pikepdf.Name no longer compares equal to bytes: pikepdf.Name('/Foo') == b'/Foo' is now False. It still compares equal to the str of its UTF-8 bytes, so obj.Type == '/Page' works as before, and it now hashes like that str, so {pikepdf.Name('/héllo'): 1}['/héllo'] no longer raises KeyError. As PDF 2.0 (ISO 32000-2, 7.3.5) requires, names are compared byte for byte, with no Unicode normalization. Use bytes(name) == b'...' to compare the raw bytes.

  • Behavior change: A pikepdf.Real combined with an int, Integer, or Decimal, or negated with unary -/+/abs(), now yields a Decimal (previously a float, or TypeError for int operands other than /). A Real with a float operand still yields a float. Division of an Integer or Real by zero now raises ZeroDivisionError, as for Python numbers, instead of ValueError.

  • Behavior change: pikepdf.Matrix now raises TypeError, not ValueError, for a pikepdf.Object that is not a matrix or an ObjectList with a non-numeric element, matching what a native argument of the wrong type raises, so an except TypeError around Matrix(*operands) behaves the same in either conversion mode. Size errors (must have 6 elements) remain ValueError.

  • Behavior change: bool() on a pikepdf.Integer or pikepdf.Real is now by value (bool(pikepdf.Integer(0)) is False), instead of raising NotImplementedError: code is unreachable.

  • Behavior change: as_dict() and as_list() now accept a default argument and raise TypeError on a type mismatch, instead of raising pikepdf.PdfError with no way to supply a default.

  • Behavior change: Objects created in Python are now adopted by the Pdf they are inserted into, so pdf.Root.X = 42 followed by pdf.Root.get_raw('/X').is_owned_by(pdf) is True, matching objects parsed from a file. Scalars (Name, String, Integer, …) are adopted as a copy, so a handle held in a constant can still be inserted into several documents. A Dictionary or Array is adopted in place and keeps its aliasing with the document; inserting the same container into a second Pdf now raises ForeignObjectError, as it already did for containers parsed from a file. Construct a new object, or use pikepdf.Pdf.copy_foreign(). Adoption is not permanent: removing the object from the document – deleting or replacing the dictionary key or array element that held it, or destroying the document – releases the claim, so the object can then be inserted into another Pdf. If the same object was reachable under two keys, removing either one releases it; its value is unaffected and still readable through the remaining reference.

  • Behavior change: Objects copied into a document are adopted by it too. The direct children of an object produced by pikepdf.Pdf.copy_foreign(), pikepdf.Object.with_same_owner_as(), pikepdf.Pdf.make_indirect(), or by appending/inserting a page into Pdf.pages, and values inserted through NameTree/NumberTree __setitem__, now report the destination document as their owner and follow its conversion_mode.

  • Behavior change: pikepdf.Object.with_same_owner_as() and pikepdf.Pdf.make_indirect() now raise ForeignObjectError for a direct object that already belongs to a different Pdf, instead of silently retagging an object that the other document still references. Remove it from that document first, or make it indirect there and use pikepdf.Pdf.copy_foreign().

  • Behavior change: as_int() with coerce=True now returns a supplied default for a value that does not fit in a 64-bit PDF integer, instead of raising OverflowError. Called without a default it still raises OverflowError.

  • Behavior change: repr() of a pikepdf.Object now honors the effective conversion mode of the object being displayed (context manager, then its owning Pdf, then the global setting), rather than only the global setting.

  • Behavior change: pikepdf.set_object_conversion_mode() now raises ValueError for a value other than 'implicit' or 'explicit', instead of silently accepting it.

Performance

  • pikepdf.Pdf.save() writes directly to the file descriptor when saving to a filename or to a plain binary file object from open(), instead of calling the stream’s write() method for every chunk of output. Saving a large document is about twice as fast. Other streams, such as BytesIO, pipes and subclasses of the io classes, are written through write() as before.

  • isinstance() checks against pikepdf.Dictionary, pikepdf.Stream, pikepdf.Integer and the other object classes are about five times faster.

  • pikepdf.unbox() is now implemented in C++ and is about 40 times faster. pikepdf.as_int(), pikepdf.as_float() and pikepdf.as_decimal() answer for a native int or Decimal without constructing a PDF object, and converting a Real to a Decimal no longer imports the decimal module each time.

Fixes

  • pikepdf.models.metadata.decode_pdf_date() now accepts every PDF date form the specification allows, in which all fields after the year are optional. D:202001011230 (no seconds) was misread as 12:03, and D:2020010112 (no minutes) and offsets in hours only, such as -08', raised ValueError, so pikepdf.Pdf.docinfo dates in these forms were not synchronized to XMP.

  • pikepdf.Pdf.save() now raises ValueError when the destination is the stream the Pdf was opened from, or another stream on the same file, as it already did for the input file’s path. Previously the new file was written into the input at the stream’s current position, which produced a corrupt file and could corrupt the open Pdf, since qpdf reads its input lazily. Open with allow_overwriting_input=True to save over the input.

  • Setting an XMP property that a file stored as plain text, such as <dc:title>Old</dc:title>, now replaces the text. Previously the new value was added alongside the old text, which was still returned when the property was read back.

  • pikepdf.PdfImage.filter_decodeparms and pikepdf.PdfImage.decode_parms no longer raise AttributeError when an image’s /DecodeParms is a bare number or boolean, in either conversion mode. A /DecodeParms that is neither a dictionary nor an array is now ignored, as qpdf does, so each filter gets empty parameters.

  • An image /Decode that is not an array (a bare number, boolean, name or dictionary) is now ignored in favour of the default decode array, instead of raising AttributeError when the image is read or converted.

  • pikepdf.Stream.write() now raises TypeError rather than AttributeError when filter or decode_parms is a native Python value such as an int, str or dict.

  • pikepdf.Pdf.save() to an existing file that is not a regular file, such as /dev/null, a FIFO or a character device, now writes into it directly. Previously the temporary file was renamed over it, so when run as root, saving to /dev/null replaced the device with a regular file.

  • pikepdf.Matrix.inverse() now returns the correct translation for matrices whose determinant is not 1. Previously the e and f terms were not divided by the determinant, so the result was not a true inverse for any matrix that both scaled and translated. Matrices built only from rotations and translations were unaffected. (#743)

  • Storing a Python int outside the signed 64-bit range of a PDF integer, by assignment or in Array(...), Dictionary(...) or Integer(...), now raises OverflowError instead of RuntimeError: std::bad_cast (or a misleading TypeError).

  • PdfImage, PdfImageBase, PdfJpxImage, PdfInlineImage and PaletteData once again report their module as pikepdf.models.image, the path they are imported from, instead of the private pikepdf.models.image._classes introduced when that module became a package in v10.10.

  • The examples for pikepdf.set_object_conversion_mode() and pikepdf.explicit_conversion() no longer open a nonexistent test.pdf, and the global-mode example restores the default mode when it is done.

  • str() of a pikepdf.Integer, Boolean or Real now gives the value – '42', 'True', '1.50' – rather than the object’s repr. In implicit mode a scalar arrives as int/bool/Decimal and str() never reached the object, so every f-string, log line and string concatenation in code reading PDF values silently changed meaning under explicit mode.

  • hash() now works on pikepdf.Integer, Boolean and Real (and on a null object), instead of raising RuntimeError: don't know how to hash this. A scalar hashes like the Python value it compares equal to, so Real('1.0'), Decimal('1.0') and 1 agree, and a scalar can be a dict key or a set member as it could in implicit mode.

  • pikepdf.Pdf.make_indirect() and pikepdf.Object.with_same_owner_as() no longer turn a direct scalar (Name, String, Integer, Real, Boolean, Operator) that you pass them into an indirect object in place. They make a copy indirect and return it, leaving your object direct and unowned. Previously the object you held silently became an indirect object of that Pdf, after which hash() raised and any dict or set already holding it broke; a module-level constant such as FOO = Name.Foo became tied to one document. Arrays, dictionaries and streams are still made indirect in place, so the object you passed stays aliased with the one in the document.

  • In explicit mode, repr() of an Array, Dictionary or Stream now names the scalars nested inside it – pikepdf.Real('42.42') rather than '42.42', which was indistinguishable from a PDF string and did not survive eval(repr(obj)).

  • pikepdf’s own higher-level APIs now work under explicit conversion mode. They read values out of PDF objects and compute with them, and every such read assumed the implicit-mode native type. Fixed in image extraction (PdfImage raised NotImplementedError for any indexed or /DeviceN colorspace), page labels (page.label restarted numbering at 1 and warned about a valid /St), outlines (a closed item read as open, flags came back empty, and saving wrote the wrong /Count), form appearance generation (TypeError laying out multiline and combed text fields), get_objects_with_ctm (a malformed cm operator raised instead of being skipped), SimpleFont metrics, and Action.new_window.

  • PdfImage metadata now reads the same in either conversion mode. A real /Width or /Height or a boolean /BitsPerComponent raised NotImplementedError in explicit mode, and a real inside a /ColorSpace array raised it in implicit mode.

  • SimpleFont.ascent, descent and unscaled_char_width() now return a Decimal as documented, in either conversion mode. Font metrics stored as PDF integers were previously returned as int.

  • Iterating a NamePath (e.g. list(path)) no longer falls back to the legacy __getitem__(0), (1), ... protocol, which never raised IndexError and so iterated forever, exhausting memory.

  • NamePath in obj no longer silently answers False for every path; see above.

  • Fixed a stale NamePath['/A']['/B'].C[0] example in the type stub, which did not type-check against the runtime behavior (an instance’s __getitem__ only accepts an int). The correct form, matching the compiled docstring and the Accessing nested objects with NamePath documentation, is NamePath['/A']('/B').C[0].

  • A direct object parsed from a file, then removed from the document (for example box = page.obj.get_raw('/MediaBox') followed by del page.obj['/MediaBox']) and used after the document was closed, kept a dangling reference to the freed document and could crash the interpreter on repr() or when inserted elsewhere. Removing an object from a document now releases its association with it, and a Pdf disconnects every object it adopted as it is destroyed.

Internals

  • Linux wheel jobs now cache the compiled qpdf per build image with actions/cache, so jobs that share an image no longer each download and compile the same qpdf. See the build process notes.

  • Fixed the Linux wheel build script’s AlmaLinux detection, which was a malformed shell test that always evaluated false.

  • The type stubs for the C++ extension module, until now a single 4,300-line src/pikepdf/_core.pyi, are a stub-only package src/pikepdf/_core/ split into one stub per translation unit: _core/_matrix.pyi covers src/core/matrix.cpp, _core/_page.pyi covers src/core/page.cpp, and so on, with _core/__init__.pyi re-exporting the lot and documenting the layout. Nothing changes at runtime or in the public API – pikepdf._core is still one extension module and import pikepdf._core still resolves to it. Type checkers now name a type by its defining stub in messages (pikepdf._core._object.Object rather than pikepdf._core.Object); pikepdf.Object remains the name to write in annotations.

  • API documentation for the C++ extension now lives only in the src/pikepdf/_core/ stubs, which are what Sphinx, type checkers and IDEs read. Docstrings attached to the C++ bindings were never published and had drifted from the stubs; they are merged into the stubs and removed from C++, and a test now fails if a binding gains a docstring or a public C++ name is missing from the stubs. help() on a C++ method now shows only its signature, as it already did for most of them.

Documentation

  • Restored the full pikepdf.Stream and pikepdf.Dictionary documentation, including constructor arguments and examples, which had been reduced to a single line when those classes moved to C++.

  • Clarified guidance on Pdf.save(..., deterministic_id=) and static_id=, and the matching JobBuilder.deterministic_id() and JobBuilder.static_id(). deterministic_id gives reproducible, production-safe /ID values; static_id sets the same dummy /ID in every file and is for testing only. static_id now appears last in the Pdf.save() signature. All save() options are keyword-only, so existing code is unaffected.

v10.13.0

Exception hierarchy

pikepdf’s exceptions now form a documented hierarchy rooted at the new pikepdf.PikepdfError, with pikepdf.PikepdfWarning playing the same role for warnings. See Exceptions for the full tree. #739

  • Behavior change: pikepdf.DataDecodingError now derives from pikepdf.PdfError. A stream that will not decode is a defect in the document, and the same call that raises it – Object.read_bytes() – already raised PdfError for other kinds of damage, so except PdfError was a handler that looked correct, passed on healthy files, and let a traceback escape on damaged ones. Code catching DataDecodingError by name is unaffected. Code that orders except PdfError before except DataDecodingError will now take the first branch; if the distinction matters, reverse the order.

  • Behavior change: pikepdf.PdfParsingError now derives from pikepdf.PdfError, for the same reason.

  • pikepdf.PasswordError remains a sibling of PdfError, not a subclass. A wrong password does not mean the document is defective, and handlers that report the two separately depend on except PdfError not catching it.

  • pikepdf.NotExtractableError is now exported. It was already the base class of the exported HifiPrintImageNotTranscodableError but could not be caught by name.

  • Fixed the exceptions documentation, which referenced a nonexistent FormCopyWarning (the class is PageCopyWarning) and omitted ReferenceCycleError, PageCopyWarning and NotExtractableError.

XMP metadata value types

XMP assigns a type to every standard property, and software that reads XMP discards a property whose type is wrong – Ghostscript strips these silently, and PDF/A validators reject them. pikepdf used to choose the RDF container from the Python type of the value it was handed, so meta['dc:subject'] = ['a', 'b'] wrote an rdf:Seq where the specification requires an rdf:Bag, and meta['dc:creator'] = 'Author' wrote a bare string where an rdf:Seq belongs. pikepdf now knows the type of the properties in the standard schemas and converts values to it. See Metadata value types. #555

  • Behavior change: a property that holds several values is now written in the container the specification requires, regardless of whether a list or a set was assigned. Reading such a property back returns a list for an ordered property and a set for an unordered one, as before.

  • Behavior change: assigning a plain string to a property that holds several values now issues the new pikepdf.XmpTypeWarning and stores a single element array, rather than writing a bare string that other software discards. The log.error message pikepdf previously produced for dc:creator alone is replaced by this warning, which covers every such property.

  • Behavior change: assigning a list or set to a property that holds one value now joins the values with ; (warning that it did so) instead of writing an array the specification does not allow there.

  • datetime.datetime and datetime.date may now be assigned to date valued properties such as xmp:CreateDate, and are encoded as ISO 8601. A time zone offset that is not a whole number of minutes is rounded to the minute, since XMP allows only +hh:mm and -hh:mm. int may be assigned to pdfaid:part and bool to xmpRights:Marked.

  • Assigning a set to an ordered property such as dc:creator now sorts the values, so the order written is the same on every run rather than whatever order the set happened to iterate in.

  • Pdf.open_metadata(strict=True) now also raises TypeError for a value that does not match the type of the property, in addition to its existing effect of refusing to repair invalid XMP.

  • Assigning to a property whose namespace prefix has not been registered now raises KeyError naming pikepdf.models.metadata.XmpDocument.register_xml_namespace(). Previously the prefix was silently dropped and the property written into no namespace.

  • Properties pikepdf does not know about are unaffected: the container is still inferred from the Python type of the value.

  • The new pikepdf.models.metadata.XMP_SCHEMA maps a property’s qualified name to its XmpProperty type, for callers that want to check types themselves.

Fixes

  • Iterating XMP metadata no longer produces keys that cannot be looked up. When XMP fails to parse and is recovered, an element whose namespace prefix was never declared survives under its literal name – xmp:MetadataDate rather than {http://ns.adobe.com/xap/1.0/}MetadataDate – so iteration yielded a name that __getitem__ resolved to a different one, and any code that walked the metadata (such as dict(meta.items())) raised KeyError or, where the name was truncated, ValueError: Invalid tag name. pikepdf now rebinds such names to the namespace their prefix refers to when it parses XMP, and discards the ones it cannot resolve. This also means the XMP pikepdf writes back for these documents is well-formed XML; previously it contained undeclared prefixes that no strict parser would read. #634

  • key in metadata now reports whether the key is present rather than whether its value is truthy, so a property with an empty value is no longer reported as missing by in while metadata[key] returns its value. Iterating the metadata likewise now includes a property with an empty value that is stored as an attribute of rdf:Description.

  • XMP dates are now parsed according to the XMP specification when updating DocumentInfo, rather than with datetime.fromisoformat. On Python 3.10 the latter rejects a fraction of a second that is not exactly 3 or 6 digits and a time zone offset without a colon, so a valid date such as 2024-06-01T12:00:00.5Z in XMP caused /ModDate to be dropped from DocumentInfo with a warning. A date-only XMP value such as 2024-06-01 now becomes D:20240601 in DocumentInfo, and back, without a spurious midnight time, matching the existing handling of year and year-month values.

  • Looking up a malformed key such as 'xmp:' now raises KeyError (and metadata.get() returns the default) instead of ValueError from lxml.

  • pikepdf._core._ObjectList, the list of operands attached to a content stream instruction, now behaves like a list of pikepdf objects. Previously its methods only accepted pikepdf.Object, but the elements of an operand list are usually numbers, which pikepdf decodes to int/bool/Decimal on the way out – so there was no value a caller could pass back in. ==, !=, in, count(), remove(), append(), insert(), extend() and __setitem__ now encode their argument the same way pikepdf.Array does, so instruction.operands == [0] is True and 0 in instruction.operands works. #742

  • Comparisons on _ObjectList now compare objects by value, as the rest of pikepdf does. They previously used qpdf’s C++ operator==, which reports only whether two handles refer to the same underlying object, so ==, in, count() and remove() gave wrong answers even for operand lists made entirely of pikepdf.Object.

  • pikepdf._core._ObjectMapping, returned by Object.as_dict() and Page.get_images(), had all of the same problems and received the same treatment: __setitem__ and update() now encode their value, == and != compare values rather than object identity, and comparing a mapping to a dict works instead of printing a nanobind conversion warning.

  • _ObjectMapping.__setitem__ and __delitem__ now accept a pikepdf.Name key, which __getitem__ and __contains__ already did.

  • Comparing an _ObjectList or _ObjectMapping to a list or dict no longer prints nanobind: implicit conversion from type 'list' to type 'pikepdf._core._ObjectList' failed! to stderr.

  • A warning raised from pikepdf’s C++ layer – PageCopyWarning, the Page.rotate() deprecation warning, and the several warnings issued while opening and saving – is now raised as an exception when a warning filter asks for that, such as under python -W error or warnings.simplefilter('error'). Previously the exception was created and then discarded, and the call carried on as if the warning had been ignored.

  • Decoding a JBIG2 image no longer holds a second copy of the compressed data after the decoder has run, and no longer copies the whole image out of an internal buffer to hand it to the decoder.

  • repr() of a deeply nested object and unparse_content_stream() no longer copy their partial result repeatedly while building it. Output is unchanged.

  • pikepdf.StreamParser is now exported from the top-level package and included in __all__. It was always the required argument type of the public Page.parse_contents(), but previously could only be imported from the private pikepdf._core module. #738

Internals

  • Moved Page’s box properties (mediabox, cropbox, artbox, bleedbox, trimbox), the rotation property and rotate(), and _ObjectMapping’s key-based methods (get, __getitem__, __setitem__, __delitem__, __contains__) from Python augmentations to C++. Each was a thin Python wrapper around a private C++ binding, so its implementation was split across two files for no benefit; they are now defined once, in C++, and the private Page._get_mediabox(), _get_artbox(), _get_bleedbox(), _get_cropbox(), _get_trimbox() and _get_rotation() bindings they delegated to are gone. Behavior is unchanged.

  • Removed the augment_override_cpp decorator from pikepdf._augments. A Python augmentation may no longer replace a method that C++ already defines; where C++ behavior needs to change, change it in C++, so that each method has exactly one implementation. With it goes the _cpp<name> copy the decorator left behind on the augmented class, so private attributes such as Page._cpp__repr__ no longer exist.

  • Moved a second group of Python augmentations to C++ for the same reason: the Attachments mapping methods, AttachedFileSpec.relationship and its __repr__, AttachedFile.read_bytes(), Page.form_xobjects, Rectangle’s __repr__, __hash__ and to_bbox(), Token.__repr__, and Object’s as_int(), as_bool(), as_float(), as_decimal() and _ipython_key_completions_(). len(pdf.attachments) and iterating it no longer build a pikepdf.AttachedFileSpec for every attached file merely to count or name them. The private bindings these delegated to – Attachments._get_all_filespecs(), _get_filespec(), _attach_data(), _add_replace_filespec(), _remove_filespec(), Page._form_xobjects and Object._get_real_value() – are gone.

  • A support class passed to augments may now subclass an abstract base class to pick up its mixin methods and leave the abstract methods to C++; previously the abstract stubs were installed over the C++ implementations. pikepdf.Attachments uses this to get the MutableMapping mixins.

v10.12.0

Packaging and licensing

  • Binary wheels now redistribute the licenses of the compiled third-party libraries they bundle, along with an attribution manifest mapping each component to its license. #736

    • The new third-party-licenses/ directory documents every vendored binary: qpdf and libjpeg-turbo on all platforms; OpenSSL and zlib (both statically linked into qpdf30.dll) plus the Microsoft Visual C++ runtime on Windows; the GnuTLS/Nettle/GMP stack on macOS; and the GCC runtime libraries on musllinux. It also records what is deliberately not bundled – notably that Linux wheels use qpdf’s built-in crypto and link no TLS library at all.

    • These files are declared via PEP 639 project.license-files, so they ship in pikepdf-<version>.dist-info/licenses/ and are enumerated in the wheel’s License-File metadata. License-Expression remains MPL-2.0: pikepdf’s own license is unchanged, and the bundled libraries are separate works redistributed in unmodified binary form.

    • This replaces licenses-for-wheels.txt, which has been removed. It had grown stale – it never mentioned OpenSSL, zlib, or the GnuTLS stack – and it was reaching only Windows wheels, because the build backend’s default license glob happened to match its name on case-insensitive filesystems and nowhere else.

  • Source distributions no longer contain qpdf’s source tree. CI unpacks qpdf into ./qpdf to build libqpdf, and that build prerequisite was being swept into the sdist, adding roughly 2900 Apache-2.0 files to a distribution that declares itself MPL-2.0. sdists now contain only pikepdf’s own code.

  • macOS wheels now use qpdf’s native crypto provider instead of GnuTLS, and all POSIX builds select it explicitly rather than relying on qpdf’s implicit fallback. macOS wheels lose nine bundled libraries as a result – GnuTLS, Nettle, Hogweed, GMP, libidn2, libunistring, Libtasn1, p11-kit and libintl – along with roughly 10 MB and the LGPL obligations that came with them.

    macOS moved to GnuTLS in v8.11.1 to fix legacy encrypted files failing to open (#520), because Homebrew’s OpenSSL had retired the legacy provider that supplies the RC4 and MD5 those files need. The Linux builds were assumed to be doing the same thing, but were not: their images carry no GnuTLS or OpenSSL headers, so they had silently been using native crypto all along, and have been opening legacy encrypted files without trouble ever since. Native crypto implements MD5, RC4, SHA2 and AES itself, so no upstream deprecation policy can withdraw the weak algorithms older PDFs require. There is no change to which files pikepdf can open.

  • Documentation updates with additional guidance about crypto provider selection and the realities of PDF encryption security.

Behaviour change

  • pikepdf.PdfInlineImage.icc now returns None instead of raising InvalidPdfImageError. An inline image’s colour space cannot be /ICCBased, so “no ICC profile” is the correct answer rather than an error – and raising made pikepdf.PdfImageBase.palette unusable on indexed inline images, which consults it.

Fixes

  • Fixed inline image name abbreviation handling, which was incomplete in both directions. #206

    • Abbreviations nested inside arrays are now expanded when an inline image is parsed, so /F [/AHx /Fl] reports ['/ASCIIHexDecode', '/FlateDecode'] from pikepdf.PdfInlineImage.filters, and an /Indexed colour space written as /CS [/I /RGB 1 <...>] is now recognized as indexed instead of raising NotImplementedError. Correspondingly, pikepdf.PdfInlineImage.unparse() now abbreviates names inside arrays, not only at the top level. Contents of /DecodeParms dictionaries are left alone, since filter parameter names have no abbreviations.

    • Abbreviations are now resolved by position, because the same abbreviation can mean different things as a key and as a value: /I is /Interpolate as a key but /Indexed as a value (ISO 32000-2 Tables 91 and 92). Previously /I was always expanded to /Indexed, so /I true became /Indexed true.

    • Added the missing abbreviations /Fl (/FlateDecode), /D (/Decode) and /L (/Length); none of the three were expanded before.

  • A JBIG2 decode failure (jbig2dec present but the payload is invalid) now raises DataDecodingError with the decoder’s error message, instead of a bare RuntimeError("qpdf will consume this exception") plus an unraisable traceback on stderr. (#735)

  • Exceptions other than DataDecodingError raised by a JBIG2 decoder now keep their own type. Previously a failure unrelated to the image data – a DependencyError from a custom decoder, or KeyboardInterrupt – was reported as if the JBIG2 data were corrupt. Implementations of pikepdf.jbig2.JBIG2DecoderInterface.decode_jbig2 should raise DataDecodingError to report undecodable data.

  • When the JBIG2 image comes from a file on disk, qpdf traps the decoder’s exception and records it as a warning, so the decoder’s message is available from pikepdf.Pdf.get_warnings() rather than in the exception.

v10.11.0

  • Extended PDF outline (bookmark) support to cover more of the spec:

    • pikepdf.OutlineItem now supports the outline item dictionary’s /C (color), /F (italic/bold flags via pikepdf.OutlineItemFlag and the .italic/.bold properties), and /SE (structure element backlink) entries.

    • Added pikepdf.Destination, which parses an existing destination array into named accessors (page, fit type, viewport parameters), complementing the existing write-only pikepdf.make_page_destination(). pikepdf.OutlineItem.resolved_destination() resolves any form of an item’s destination – explicit array, page number, or named destination (via the document’s name tree or legacy /Dests dictionary) – to a Destination.

    • Added a typed action model (pikepdf.Action and subclasses GoToAction, GoToRAction, GoToEAction, GoToDpAction, LaunchAction, URIAction, NamedAction, SetOCGStateAction, JavaScriptAction) for reading and constructing /A action dictionaries. pikepdf.OutlineItem.parsed_action gives a typed view of an item’s action.

    • Fixed the outline dictionary’s root /Count being written as 0 instead of omitted when the outline has no entries at all, matching spec Table 150.

  • Fixed 1-bit /Indexed images losing their palette when extracted. pikepdf.PdfImage.as_pil_image() and pikepdf.PdfImage.extract_to() previously ignored the palette of a 1-bit indexed image over a /DeviceCMYK base (returning a bilevel grayscale image) and of a single-colour palette (hival 0). Both now decode correctly, matching the existing behavior for 2/4/8-bit indexed images.

  • A 1-bit /Indexed image whose base colour space is unsupported (for example /DeviceN or /Separation) now raises NotImplementedError instead of silently returning an unpalettized image, matching the 2/4/8-bit path.

Security

  • Image extraction now verifies that the image stream holds enough data for the declared /Width and /Height before allocating, raising ImageDecompressionError when it does not. This closes the remaining decompression-bomb gaps left by the pikepdf.PdfImage.MAX_IMAGE_PIXELS limit added in v10.10.0: that limit bounds the declared size, but said nothing about whether the declared size was consistent with the data present, so a 4-bit image declaring 12000x12000 – well under the default 500M pixel budget – still allocated ~300 MB from a single byte of stream data, and setting the limit to None restored the unbounded case entirely. The 2-bit and 4-bit unpack path and the 1-bit path (where Pillow allocates a byte per pixel before it notices the data is short) are both covered; 8-bit and 16-bit images already failed cleanly. The check applies regardless of MAX_IMAGE_PIXELS. (#733, #734)

  • The 2-bit and 4-bit unpack buffer is no longer over-allocated by a factor of the packing ratio, reducing peak memory for those images by 4x and 2x respectively.

Behavior change

  • Extracting a 1-, 2-, or 4-bit image whose stream is shorter than its declared dimensions require now raises ImageDecompressionError rather than returning a partially decoded image with a black tail. This matches what 8-bit and 16-bit images have always done for the same defect.

  • An image with a non-positive /Width or /Height raises InvalidPdfImageError instead of failing further down with an obscure error from Pillow.

v10.10.0

Behavior change

  • pikepdf.PdfImage.as_pil_image() and pikepdf.PdfImage.extract_to() now apply an image’s soft mask (/SMask) or explicit/colour-key mask (/Mask) by default, returning an image with an alpha channel (LA or RGBA) and writing a transparency-capable format (.png). Previously the mask was silently ignored and the opaque base image was returned. Images without a mask are unaffected. Pass apply_mask=False to recover the old behavior and obtain only the opaque base image.

New features

  • Image masking is now honored when extracting images. /SMask soft masks, /Mask stencil (explicit) masks, and /Mask colour-key masks are composited into an alpha channel. Soft-mask images whose resolution differs from the base image are resampled to match (ISO 32000-2 §8.9.6).

  • Added support for /CalRGB, /CalGray and /CalCMYK images, which previously raised NotImplementedError. The samples are decoded as their device equivalents, and for CalRGB/CalGray an ICC profile synthesized from the colour space’s WhitePoint/Gamma/Matrix is attached to the extracted image so the calibration is preserved for colour-managed consumers.

  • Added support for the /Lab colour space, extracted as a Pillow LAB image (saved as TIFF) with the PDF L*/a*/b* ranges remapped to Pillow’s conventions.

  • Added support for 16-bit-per-component images. 16-bit grayscale is extracted losslessly as Pillow I;16; 16-bit RGB and CMYK are reduced to 8-bit (with a warning) because Pillow has no higher-bit-depth raw mode for them.

  • Extended colour-space handling to several cases that previously raised NotImplementedError: ICCBased CMYK images and indexed images now produce the correct default /Decode array, and inline images that name their colour space now resolve it from the in-scope /Resources when obtained via pikepdf.parse_content_stream().

  • JPEG (/DCTDecode) images with a non-default /ColorTransform – a YCCK CMYK or a non-YCbCr RGB JPEG – are now decoded via Pillow (which honours the JPEG’s own markers) and transcoded, instead of failing to extract.

  • Filter chains that wrap a single terminal image codec (/DCTDecode, /CCITTFaxDecode, /JPXDecode, /JBIG2Decode) in any number of generalized/specialized filters (Flate, LZW, ASCII85/Hex, RunLength) are now peeled and extracted seamlessly.

  • Added pikepdf.PdfImage.MAX_IMAGE_PIXELS, a settable class-level limit on the number of pixels pikepdf will decode from a single image. Until set, it defaults to max(500_000_000, PIL.Image.MAX_IMAGE_PIXELS) – a floor suited to high-DPI scanned PDFs – and tracks Pillow’s setting; once assigned it becomes independent of Pillow. Set it to None to disable the check.

  • pikepdf.Array now implements the standard Python list interface: slicing (including del on slices and slice assignment), and the clear(), count(), index(), insert(), pop(), remove(), and reverse() methods.

Limitations

  • /SMaskInData (alpha encoded inside a JPEG 2000 stream) is applied only when Pillow’s JPEG 2000 decoder surfaces the alpha channel itself; a pre-multiplied (SMaskInData 2) result is not un-premultiplied. A /Matte entry on a soft mask is not undone (a warning is emitted). When an image has both an /SMask and a /Mask, the soft mask takes precedence. Colour-key masking is applied only to 8-bit L/RGB/CMYK images.

  • A filter chain containing two or more terminal image codecs (for example [/DCTDecode /CCITTFaxDecode]) cannot be decoded by any reader and now raises UnsupportedImageTypeError rather than NotImplementedError.

Security

  • Hardened image extraction against decompression-bomb (memory exhaustion) attacks. A malicious PDF could declare an image with enormous /Width and /Height so that pikepdf.PdfImage.as_pil_image() attempted to allocate many gigabytes before reading the (tiny) image stream. pikepdf now enforces a configurable pixel limit, pikepdf.PdfImage.MAX_IMAGE_PIXELS (analogous to PIL.Image.MAX_IMAGE_PIXELS), across every image-decode path – including the 2/4-bit transcoding path, 1-bit and 8-bit images, and the Pillow-decoded JPEG/JPEG2000/CCITT path. Oversized images raise pikepdf.DecompressionBombError and borderline images emit pikepdf.DecompressionBombWarning (both subclass Pillow’s equivalents). (#733)

Fixed

  • A CCITT fax image preceded by a stripped simple filter (e.g. [/FlateDecode /CCITTFaxDecode]) now builds its TIFF header from the /CCITTFaxDecode filter’s own /DecodeParms rather than the leading filter’s, which previously produced a corrupt extraction.

  • Corrected the documentation of pikepdf.StreamDecodeLevel: the specialized and all levels were each described with the other’s behavior.

v10.9.1

  • Fixed a crash (SIGABRT via std::terminate) that could occur when a file-backed pikepdf.Pdf was deallocated while a Python exception was already propagating – for example when pikepdf.open(filename) appears as a transient element of a list/tuple literal whose later element raises. Opening from a filename closes the file in the input source destructor, which calls back into Python; with an exception already in flight that call raised an error that escaped the destructor. The in-flight exception is now preserved and propagates normally. (#732) Added a guard for a likely non-reproducible related case with DecimalPrecision.

v10.9.0

New features

  • Added pikepdf.JobBuilder, a fluent, Pythonic builder for qpdf jobs. It assembles a job specification with chained, snake_case methods (input, output, encrypt, add_pages, split_pages, linearize, compress, add_attachment, add_overlay, limits, …) and runs it via the existing pikepdf.Job, without hand-writing qpdf’s camelCase job JSON. Encryption permissions are expressed with the familiar pikepdf.Permissions/pikepdf.Encryption models, and a .set(**kwargs) escape hatch reaches any other job option. Additional methods cover image optimization (optimize_images, externalize_inline_images), page/content transforms (flatten_annotations, flatten_rotation, generate_appearances, coalesce_contents, normalize_content), content removal (remove_metadata, remove_info, remove_acroform, remove_structure, remove_page_labels), page labels (set_page_labels), version control (min_version, force_version), and reproducible/inspection helpers (deterministic_id, static_id, check).

  • Exposed several pieces of qpdf functionality that pikepdf had not previously bound:

  • Added pikepdf.Page.get_images(), which by default recurses into nested form XObjects to find images. The pikepdf.Page.images property is now deprecated: it only reports images referenced directly by the page and silently omits images drawn through form XObjects, which made it appear as if a page “has no images” when it clearly did. Use get_images() instead, or get_images(recursive=False) for the old behavior.

  • Added pikepdf.Page.rotation, a property that reports a page’s effective clockwise rotation normalized to [0, 360). Unlike the raw page.Rotate attribute, it resolves a /Rotate value inherited from the page tree and reports 0 when no rotation is set, instead of raising. Assigning to it sets the absolute rotation. This addresses the long-standing confusion between the page.Rotate attribute and the page.rotate() method (#467).

  • pikepdf.Page.rotate() now defaults relative to False, so page.rotate(90) sets an absolute rotation. Passing relative as a positional argument is deprecated and emits a DeprecationWarning; pass it as a keyword argument instead, e.g. page.rotate(90, relative=True). Positional support will be removed in pikepdf 11.

  • Added pikepdf.Pdf.add_pages_from() to copy pages between documents while preserving interactive AcroForm form fields, returning a pikepdf.PageCopyResult. Naive pages.extend() across documents and save() of documents with orphaned form widgets now emit pikepdf.PageCopyWarning. (#670, #207)

    When copying pages, named destinations referenced by the copied pages’ annotations (e.g. table-of-contents links) are now carried into the destination document — both the PDF 1.2 Names.Dests name tree and the legacy PDF 1.1 Root.Dests dictionary — so internal links keep working regardless of merge order. Name collisions are renamed and reported via pikepdf.PageCopyResult (named_dests_added, renamed_dests, dropped_dests). Naive pages.extend() now also warns when copied pages reference named destinations. (#148)

Fixes

  • Fixed image extraction ignoring the /Decode array, which caused colors to be inverted (or otherwise mismapped) when a PDF specified a non-default /Decode such as [1, 0]. pikepdf.PdfImage.as_pil_image() and pikepdf.PdfImage.extract_to() now apply /Decode as a linear per-channel mapping for grayscale, RGB and CMYK raster images, matching how a PDF viewer renders the image. Previously /Decode was honored only for CCITTFax-encoded images. Thanks to Mark-Joy for the report. #650 Both methods gained an apply_decode_array parameter (default True). Pass apply_decode_array=False to retrieve the raw stored sample values with the least processing – useful for forensic inspection of the underlying image data. Some image types are intentionally not affected: Indexed-colorspace images (where /Decode remaps palette indices rather than colors – a non-identity /Decode there now emits a warning), and DCT (JPEG) / JPX (JPEG 2000) images, whose codecs carry their own color semantics (such as the Adobe APP14 marker for inverted CMYK) that Pillow already honors; re-applying /Decode would double-invert them.

  • Fixed pikepdf.Pdf.save() decompressing streams when called with compress_streams=False and no explicit stream_decode_level. qpdf 11.10 changed its default stream decode level to generalized, which caused such saves to decompress (without recompressing) streams and balloon the output file. pikepdf now pins the decode level to none in this case, restoring the documented behavior that compress_streams=False alone does not trigger decompression. Fixes #676.

  • The minimum required qpdf version is now 12.3.2. The new pikepdf.AcroForm.validate() binding calls qpdf’s QPDFAcroFormDocumentHelper::validate, which was added in qpdf 12.3.0, so pikepdf no longer builds against older qpdf releases.

Documentation

  • Documented a long-standing page-deletion pitfall: deleting a page unlinks it from the page tree, but a page that is still referenced by an outline (bookmark), link annotation, or named destination remains in the saved file. The Deleting pages topic now explains the behavior and gives workarounds. Thanks to m-holger. Closes #196.

  • Documented how to copy metadata between documents, in a new Copying metadata between documents topic, including why blindly copying all fields (or the raw XMP stream) can import false conformance claims and identifiers. Closes #188.

v10.8.0

  • Added pikepdf.ReferenceCycleError (a subclass of pikepdf.PdfError), raised when an operation would create a cycle of direct (non-indirect) objects – a direct object may not contain itself, directly or indirectly. Use pikepdf.Pdf.make_indirect() to create a reference cycle instead. This requires a build of qpdf that prevents direct-object cycle construction; on older qpdf the offending operation is permitted as before.

  • Added a new pikepdf.sanitize module with curated, low-risk helpers for removing active or auxiliary content from untrusted PDFs: remove_javascript, remove_attachments, remove_external_access, remove_thumbnails, remove_search_index, remove_multimedia (Rendition/Movie/Sound/RichMedia/3D content), remove_web_capture (/SpiderInfo), remove_private_app_data (page-piece dictionaries), and remove_collection (PDF portfolio view), plus a pikepdf.sanitize.Sanitizer builder for chaining these operations. The action-based removals now also traverse the document outline (bookmarks) and treat embedded go-to (/GoToE) as external access, and remove_attachments now sweeps /AF associated-file references from every object (XObjects, structure elements, DParts, etc.). Also added a new Sanitizing PDFs topic discussing PDF sanitization, threat models, and the limits of programmatic redaction. Fixes #673.

  • pikepdf now publishes free-threaded CPython 3.14 (cp314t) binary wheels to PyPI for Linux (manylinux and musllinux, x86-64 and aarch64), macOS (x86-64 and Apple Silicon) and Windows (x86-64). Previously these wheels were not published and free-threaded users had to build pikepdf from source.

  • Updated the PyPI “Free Threading” trove classifier from “1 - Unstable” to “3 - Supported”.

  • Some of pikepdf’s dependencies (such as lxml and Pillow) publish their own free-threaded wheels; on less common platforms or when older versions are involved, free-threading might require source builds of those dependencies.

  • Reimplemented Page’s attribute, item and get accessors in C++ instead of Python. These delegate to the underlying page dictionary and were previously implemented as Python augmentations; moving them to C++ removes extra Python call frames on these hot paths. Behavior is unchanged.

  • Object construction (Name, Array, Dictionary, the Name.Attr shorthand, the scalar types Integer/Boolean/Real, and NamePath) is now implemented in C++ for improved performance. Behavior is unchanged.

v10.7.3

  • Upgraded to cibuildwheel 3.4.1 and refreshed pinned GitHub Actions (actions/checkout@v6, actions/upload-artifact@v7, actions/download-artifact@v8, codecov/codecov-action@v6). Dropped the CPython 3.15 prerelease test job, since cibuildwheel has not yet shipped a stable release with 3.15 support.

  • Fixed Windows wheels bundling a fixed-version, un-mangled copy of the Microsoft Visual C++ runtime (msvcp140*.dll, vcruntime140*.dll, concrt140.dll) inside the package directory. These were inadvertently copied from qpdf’s prebuilt release alongside qpdf30.dll. Shipping them caused a second, conflicting copy of the C++ runtime to load in the same process, which could corrupt CPython’s per-thread state and produce a fatal PyInterpreterState_Get ... the GIL is released (the current Python thread state is NULL) error or an ImportError for pikepdf._core, typically only in some launch environments (e.g. a terminal but not IDLE) or when another extension was imported first. The Windows wheel now copies only qpdf’s own DLLs; delvewheel vendors a name-mangled copy of the C++ standard library runtime and uses CPython’s own vcruntime140, so the wheel remains self-contained without an un-mangled runtime in the package directory that could collide with the system copy. Fixes :issue:718.

  • Improved the error message raised when pikepdf._core fails to import: it now reports the active interpreter, version, free-threading status, and (on Windows) a hint about the Visual C++ Redistributable and interpreter mismatches.

  • Fixed a segmentation fault when comparing two direct (non-indirect) Dictionary or Array objects that form a cyclic reference graph, for example a['/Kids'] = [b]; b['/Kids'] = [a]; a == b. The cycle detector previously keyed its bookkeeping on unparseBinary(), which itself recurses through the whole graph and overflowed the C stack for direct cyclic objects. Equality now detects cycles by object identity instead, so such comparisons terminate. Fixes :issue:731.

  • Fixed an AttributeError when reading a document outline (“bookmarks”) whose items are missing the required /Title field. By default, Pdf.open_outline() now quietly treats a missing /Title as an empty string; passing strict=True raises OutlineStructureError instead. Fixes :issue:730.

v10.7.2

  • Fixed a segmentation fault when an object that is not an Encryption, dict, bool, or None (for example a list or unittest.mock.MagicMock) was passed to the encryption argument of Pdf.save(). A TypeError is now raised instead. Fixes :issue:727.

  • Fixed a possible segmentation fault in Page.add_content_token_filter() if the user had previously assigned a non-list value to the private Pdf._token_filter_refs attribute. The attribute is now reset before use.

  • Suppressed nanobind’s leaked instances/types/functions report at interpreter shutdown. Module-scope Python state (e.g. pytest.mark.parametrize arguments) commonly holds pikepdf objects until the interpreter exits; nanobind reports these as leaks even though they are not bugs. Set the environment variable PIKEPDF_NANOBIND_LEAK_WARNINGS=1 before importing pikepdf to re-enable the report for debugging. Fixes :issue:728.

  • Fixed Array.append(None) raising TypeError instead of inserting a PDF null object. This was a nanobind migration regression vs. v10.5. Fixes :issue:725.

  • Fixed Dictionary.__setattr__(name, None) (i.e. d.Key = None) raising TypeError instead of the documented ValueError advising to use del to remove the key. Same nanobind migration regression as Array.append(None).

  • Fixed macro redefinition warnings on Fedora rawhide (Python 3.14 + glibc 2.42) by ensuring Python.h is included before any standard library headers in all translation units. Fixes :issue:724.

  • Moved the 2-bit and 4-bit subbyte pixel unpack inner loops from Python into C++, eliminating per-byte interpreter overhead when decoding low-bit-depth images.

v10.7.1

  • Fixed build to continue generating Python version specific wheels for 3.12 and 3.13 due to open issue in nanobind. Fixes :issue:723. Thanks @mgorny for reporting.

  • Improved CI build to perform more detailed tests using python3-dbg (debug build) which has more assertions and would have uncovered this issue.

v10.7.0

  • Yanked release from PyPI due to segfaults on Python 3.12 and 3.13; fixed in 10.7.1.

  • Python 3.12+ are now built with abi3 (the Stable ABI). Earlier versions and freethreading builds continue to be built against the specific Python versions.

  • Remove manual hack to generate docs/requirements.txt for the readthedocs.org.

v10.6.0

  • Released v10.6.0 with version bump only.

v10.6.0rc2

  • Fixed a regression during nanobind migration (exception hierarchy unintentionally changed).

v10.6.0rc1

  • Replaced pybind11 with nanobind and added full freethreading support. pikepdf binary size is now both ~20% smaller and about 10% faster thanks to nanobind.

v10.5.1

  • Updated lockfile to avoid a PyJWT CVE. We only depend use PyJWT via pygithub for developer release tooling not in pikepdf itself, so this is inconsequential for pikepdf users but does silence automated security advisories.

  • Suppressed GCC -Wpsabi note about C++17 ABI change for std::pair in pybind11 headers.

v10.5.0

  • Fixed logger in ctm module using __file__ instead of __name__, which produced unhelpful log names. :issue:712

  • Modernized README.

  • Test all README code blocks instead of just one.

v10.4.0

  • Enums are now proper Python enum.Enum/enum.IntFlag types (PEP 435 compliant), migrated from pybind11’s deprecated py::enum_ to py::native_enum.

  • Reimplemented the PDFDocEncoding codec in pure Python using the standard library charmap pattern, removing the C++ dependency on qpdf for encoding.

  • Upgraded to qpdf 12.3.2.

  • Fixed incorrect docstrings for StreamDecodeLevel. :issue:708

  • Fixed type stubs: added PEP 570 positional-only markers, and corrected index() signature.

v10.3.0

  • Fixed UnicodeDecodeError when listing keys of a dictionary containing invalid UTF-8. Thanks @qooxzuub. :issue:696

  • Fixed an issue where opening a PDF with duplicate form field names would cause a crash. Accessing a duplicate field by name now returns a proxy list of all matching fields. Thanks @qooxzuub. :issue:697

  • Added .values() accessor to Object for iterating over dictionary values. Thanks @qooxzuub.:issue:699,697

  • Added .copy() and .update() methods to Dictionary. Thanks @qooxzuub.:issue:700

  • Improved Object.copy implementation and added type stubs. Thanks @qooxzuub.:issue:702

  • Fixed missing return in SimpleFont._encode_diffmap(). Thanks @lachlan.charlick :issue:706

  • Improved error messages for invalid dictionary access. Thanks @qooxzuub.:issue:701

  • Lazy load lxml and Pillow to improve import time. Thanks @qooxzuub. :issue:704

  • Improved atomic_overwrite robustness for restricted directories and special files. :issue:695

v10.2.0

  • Fixed unparse_content_stream() not preserving literal strings when given raw Python tuples. :issue:689

  • The pikepdf.explicit_conversion() context manager is now thread-local and takes precedence over the global setting from pikepdf.set_object_conversion_mode(). Nested context managers are supported via a depth counter.

  • Moved explicit conversion functions to their own module for better code organization.

  • Improved C++ test coverage to 97.5% (from 96.4% line coverage, 94.9% to 95.1% function coverage).

v10.1.0

v10.0.3

  • Fixed an issue where PdfImage.as_pil_image() would create additional unused objects in the PDF that called it.

  • Fixed a shutdown segfault in the alpha release of Python 3.15.

  • Fixed Pdf.show_xref_table() not actually showing its output.

  • Pin test dependencies python-xmp-toolkit to < 2.1.0. python-xmp-toolkit 2.1.0 is effectively a breaking change, requiring a new version of libexempi to be installed that is not available on some cibuildwheel builders. As a workaround, we have pinned the older version. We only use python-xmp-toolkit for testing to confirm correctness–pikepdf has its own XML-based implementation of XMP.

v10.0.2

  • Fixed presentation of strings using unparse_content_stream - if the stream can be represented using PdfDocEncoding, it is rendered in that way for ease of reading. :issue:682

  • Reformatted C++ source.

v10.0.1

  • Fixed issue with performing equality test on dictionaries with cyclic subgraphs. :issue:677

v10.0.0

See breaking changes for v10.0.0 above.