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. #118pikepdf.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. #281Both are on by default, which changes what existing callers of
add_pages_fromget: the destination may gain an/OCPropertiesand outline items it did not gain before. Passlayers='ignore'oroutlines='ignore'for the previous behaviour, for example if you build the merged outline yourself.pikepdf.PageCopyResultreportslayers_added,outline_items_addedandoutline_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 raisedKeyError.Extracting an image whose
/CalGrayor/CalRGBcolour space array lacks its parameter dictionary now decodes it as the corresponding device space, without an ICC profile, as a missing/WhitePointalready did. Previously it raisedIndexError.pikepdf.Pdf.check_pdf_syntax()now performs the same checks asqpdf --checkand reports the same problems. It checks the linearization data and hint tables of linearized files, which previously required a separate call topikepdf.Pdf.check_linearization(), and checks the interactive form, embedded files, page labels and outlines. A document with no pages is reported asERROR: file does not contain any pages, and a page whose content stream cannot be read is reported asERROR: page N: ...along with the other problems found, where it previously raisedpikepdf.PdfError. #515Free-threaded Python: fixed crashes when threads used pages of the same
Pdfat once. The page boxes (pikepdf.Page.mediaboxand the others),pikepdf.Page.rotation,pikepdf.Page.rotate(),pikepdf.Page.as_form_xobject(),pikepdf.Page.contents_coalesce()and several otherpikepdf.Pagemethods read or changed thePdfwithout holding its lock.Free-threaded Python: fixed a crash when the last reference to a
Pdfwas 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()orpikepdf.Page.parse_contents(), while another thread used the samePdf.Free-threaded Python: fixed a data race on the source
Pdfwhen a page was appended, inserted or assigned into the page list of anotherPdf. Both documents are now locked, aspages.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 differentPdf, which asetofobjgencannot. See Following references safely.Added
pikepdf.Object.is_same_object_as(), which tests whether two references lead to the same object. Neitheris,==nor comparingobjgenanswers this reliably.Added
pikepdf.Pdf.content_scopes()andpikepdf.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. Eachpikepdf.ContentScopeis reported once for the document;pages=selects pages andkinds=selects what is reported.Added
pikepdf.Page.get_resources(), which returns the resources of a page or form XObject for reading. Unlikepikepdf.Page.resources, it finds resources inherited from the page tree, does not create an empty dictionary when there are none, and returnsNoneinstead of raising when/Resourcesis 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/Nextactions 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
DataDecodingErrorwith the reason, whether the stream’s data was read from a file or set from memory. Previously a stream read from a file raised aPdfErrorthat said only “read_bytes called on unfilterable stream”, and the reason was available only frompikepdf.Pdf.get_warnings().DataDecodingErroris aPdfError, so existing handlers are unaffected. #182An exception raised by a JBIG2 decoder now reaches the caller of
pikepdf.PdfImage.as_pil_image(),pikepdf.PdfImage.extract_to()andpikepdf.PdfImage.read_bytes()unchanged, with its own type, message and traceback. Previously, for an image read from a file, it was replaced by a genericDataDecodingErrororPdfError. #182Added
QpdfRuntimeError, raised for errors from qpdf that pikepdf has no more specific exception for. These were previously raised as a bareRuntimeError, whichexcept pikepdf.PikepdfErrordid not catch. The new exception derives from bothPikepdfErrorandRuntimeError, so handlers written forRuntimeErrorstill work. #240The 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, butpikepdf.Name('/')raisedValueError, and dictionaries refused/as a key. This brokepikepdf.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('/'))isFalse, like an emptypikepdf.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. #745AcroFormField.get_inheritable_field_value_as_name()now returns apikepdf.Name, as documented, instead of astr. When the value is missing or not a name, it returns the empty nameName('/'), which is falsy, instead of''. Comparisons such as== '/Tx'are unaffected. #748Fixed an out-of-bounds read when opening a PDF from a Python file object whose
readinto()reports more bytes than were requested. This now raisesValueError, as writing to such a stream already did. Non-blocking streams, whosereadinto()returnsNone, now raiseValueErrorinstead of hanging. Thanks to @Nayana-Naik73. #747Fixed 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 ofread().pikepdf.open()reads directly from the file descriptor when given a filename, instead of calling theseek()andreadinto()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 topikepdf.open(), including plain file objects, are read through their methods as before.Documented
pikepdf.TokenFilterwith worked examples that shorten numbers and convert RGB colors to grayscale, and explained when to use a token filter instead ofpikepdf.parse_content_stream(). The API reference no longer says that the parser should not be used to edit content streams. TheTokenFilter.handle_tokendocumentation 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 matchingpikepdf.sanitize.Sanitizersteps 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/AcroFormfield tree. Thanks to Stjorn for the report.The same sanitizer steps left an action
/Nextchain untouched past its depth limit of 50, so a targeted action (for example a/JavaScriptaction) 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. #746The sanitizer now also drops anything in a
/Nextchain that is not an action dictionary, such as a nested array, instead of passing it through unexamined. It raisedAttributeErrororValueErroron 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.attachmentsnow 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/AFRelationshippikepdf already writes, a file attached to a PDF/A-3 document now keeps it conformant. qpdf maintains only the/EmbeddedFilesname tree, so this was previously left to the user. #463The PDF/A validator now approves embedded files in PDF/A-3b documents: the
/EmbeddedFilesname tree and the catalog’s/AFarray are checked against clause 6.8 of ISO 19005-3 (MIME type,/Fand/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/AFarray; newpikepdf.pdfa.PrepareResultfields count these repairs. See Attachments in PDF/A-3.
PDF/A named destinations
The PDF/A validator now checks named destinations, in the
/Destsname tree and in the catalog’s/Destsdictionary, 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./AlternatePresentationsis 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/Differencesbut no/BaseEncodingthe 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. Newpikepdf.pdfa.PrepareResultfields 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 inmin_versionorforce_versionfor 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 everyrdf:Descriptionwhatever itsrdf: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
pikepdf.pdfa.repair_annotation_flags(), the step ofpikepdf.pdfa.prepare()that removes hidden annotations and sets the Print flag on the rest, is now public, with its resultpikepdf.pdfa.AnnotationRepairResult. Running it before handing a file to another PDF/A converter such as Ghostscript keeps hyperlinks that producers wrote without the Print flag, which Ghostscript’s PDF/A mode deletes.
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
/Differencesonly 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 asafii10017for Cyrillic text. It now accepts every name in the full list, as ISO 19005 requires and veraPDF checks. Names outside the list, such asuni0410orA.sc, are still rejected.A
Pdfopened or created withconversion_mode='explicit'returned plainintfor some numbers that qpdf writes itself, such as/Pages /Countafter adding or removing a page,/Rotateafterpikepdf.Page.rotate(), or the/BBoxofpikepdf.Page.as_form_xobject(). These values now follow the document’s conversion mode like any other.In explicit conversion mode,
IntegerandRealnow support the rest of the Python number protocol, with the same results as theintorDecimalthat implicit mode produces:round(),math.trunc(),math.floor(),math.ceil(),divmod(), and format specs such asf'{x:.2f}'.int()of aRealnow truncates, asint()of aDecimaldoes, instead of raisingTypeError; for exampleint(page.Rotate)now works on a/Rotate 90.0.The type stubs for
Matrixnow accept apikepdf.Object, such as a/Matrixarray, 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.UTCand the broader ISO 8601 support indatetime.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-abi3wheel; free-threaded CPython 3.15 gets its owncp315twheel, 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()andpikepdf.Pdf.new()now accept a keyword-onlyconversion_modeargument ('implicit'or'explicit'), and the mode can be read or changed afterward through the newpikepdf.Pdf.conversion_modeproperty. APdf’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 toNonereverts to inheriting the context-manager/global mode.Added
pikepdf.implicit_conversion(), a thread-local context manager symmetric withpikepdf.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-
Pdfmode, then the global default set bypikepdf.set_object_conversion_mode(). An object with no owningPdf(e.g. a barepikepdf.Dictionary(...)you have not yet attached to a document) resolves through the context-manager/global scopes; once it is inserted into aPdfit follows that document’s mode.Added
pikepdf.Object.get_raw(), which behaves likeget()(key,Name, orNamePath, plus adefault) but never unboxes the result: it always returns apikepdf.Objectregardless of the current conversion mode. A null stored inside an array comes back as aNull-typed object rather thanNone; in a dictionary, qpdf treats a key whose value is null as absent, soget_rawreturns the default for it, exactly asgetdoes.Added container-level typed getters
get_int(),get_bool(),get_float(),get_decimal(),get_dict(), andget_list(), each(key_or_path, default=None). These composeget_rawwith the matchingas_*accessor, so reading an optional value of uncertain type is a mode-independent one-liner instead of a hand-writtenwith pikepdf.explicit_conversion(): ...wrapper.Added
coerce=Truetoas_int(),as_bool(),as_float(), andas_decimal(), and to the correspondingget_*getters, for reading values that real-world PDFs encode with a “nearby” type:as_int(coerce=True)accepts aReal(truncated toward zero) and a numericString;as_bool(coerce=True)accepts a nonzeroIntegerorReal(fixing the common/Marked 1case that previously required a fallback toas_int() != 0);as_float/as_decimalwithcoerce=Trueaccept anIntegerand a numericString, including exponent notation such as"1e-5".pikepdf.Integerandpikepdf.Realnow support the ordering comparisons<,<=,>and>=against Pythonint,float,bool,Decimal, and each other, sobox[0] < box[2],sorted(),min()andmax()work on explicit-mode values.Realcompares by its exact decimal value.Arithmetic on
pikepdf.Integerandpikepdf.Realnow works between two objects (Real('2.5') + Integer(3)) and withDecimalandbooloperands, and**is supported. Results are the native Python type that implicit mode would have produced:intforIntegerwithint/Integer,Decimalwhenever aRealorDecimalis involved, andfloatwith afloatoperand. In explicit mode,box[2] - box[0] > 100therefore works without unboxing, while raisingTypeErrorif 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 anInteger,BooleanorRealand passes any other value through unchanged. Unlike theas_*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 feedsisinstance,is True,Decimal(),json.dumpsor 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()andas_bytes(), each(value, default=None), with keyword-onlycoercefor the numeric and boolean ones. They are the value-side twins of theget_*typed getters, for an array element, content stream operand or other value already in hand: a pikepdf object converts exactly as itsas_*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()andas_bytes(), which return aString’s decoded text or raw bytes, and raiseTypeError(or return a supplied default) for any other type, unlikestr()andbytes(), which accept almost anything. Added the matching typed gettersget_str()andget_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 objnow tests whether the path can be traversed to a value, anddel obj[path]deletes the final component after traversing to its parent, both for name and array-index components.NamePathis now iterable, yielding its individual components (strfor names,intfor indices) in order, e.g.list(NamePath.A.B[0])gives['/A', '/B', 0].NamePathinstances now compare and hash by their component sequence, soNamePath.A.B == NamePath.A.BisTrueand aNamePathcan be used as a dict key or stored in aset.isinstance(path, pikepdf.NamePath)now works, andpikepdf.NamePathcan 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:titleanddc:descriptionare now read from theirx-defaultitem, as the XMP specification defines, rather than from the first item. Acrobat writesx-defaultlast when a document has several languages, so/Titleand/Subjectwere synced from the wrong language, and PDF/A-1 validators rejected the result.Behavior change: Setting a language alternative now updates its
x-defaultitem 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 onex-defaultitem, instead of writing severalx-defaultitems.XMP properties are now read from every top-level
rdf:Description, not only those withrdf:about="". Properties in Descriptions withrdf:about="uuid:..."(written by Distiller and older Acrobat) or with nordf:aboutcould 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:Descriptiongets the samerdf:aboutvalue, 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.recoveredandXmpDocument.recovered, which areTrueif 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/Countin each node of the page tree to the number of pages beneath it. qpdf repairs a damaged page tree but left/Countas it was, so a wrong/Countin the input was written unchanged, and veraPDF could not validate the file at all.
PDF/A validation and repair
Added the
pikepdf.pdfamodule, 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/CIDSetfor PDF/A-1, and rewrites the XMP metadata with a PDF/A declaration. It returns aPrepareResultdescribing what changed:describe()gives a sentence per change,messages()the same sentences with a suggested log level, andPrepareResult.xmp_problemwhy an XMP packet could not be read. XMP properties are kept from everyrdf:Descriptionwhen all share onerdf:about, including the non-emptyuuid:...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 raisesPdfaErrorwith the report (whosepreparedrecords the repairs made), and the destination is left untouched.pikepdf.pdfa.resolve_save_kwargs()returns the completepikepdf.Pdf.save()settings for a flavour. Settings that PDF/A forbids or that would change the bytes after the check (such asencryption,normalize_contentandfix_metadata_version) are pinned, and a conflicting value raisesValueError.
The validator is an allowlist: it approves only constructs it recognizes and knows to conform. A
Reporthas 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).saveaccepts only'pass'. EachFindingnames a rule: a veraPDF rule id such asISO_19005_2:6.2.8-3, or apikepdf: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
prepareand 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 fromcheckmatched 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.pdfaneeds jsonschema, referencing and fontTools, available as the optional extrapip install 'pikepdf[pdfa]'.import pikepdfdoes not import them;import pikepdf.pdfaraisesImportErrornaming 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.saveuses it.
qpdf limits
Added
pikepdf.settings.get_qpdf_limits()andpikepdf.settings.set_qpdf_limits(), which read and change qpdf’s process-wide limits for hardening against malicious or damaged PDFs: parser nesting depth, error count and container size, the number of stream filters, memory for decoding Flate, DCT, PNG, TIFF and RunLength data, and whether corrupt JPEG data is an error. Previously only some of these limits were reachable, throughpikepdf.JobBuilder.limits().set_qpdf_limitsreturns the previous values so they can be restored.Added
pikepdf.settings.disable_qpdf_default_limits(), which lifts qpdf’s optional default limits for the rest of the process, andpikepdf.settings.qpdf_limit_errors(), which counts how many times any limit has been exceeded.
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. Usepikepdf.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.PdfErrorwhen the pages are accessed./Rotatevalues 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
prognamekeyword argument ofpikepdf.Jobis deprecated and ignored, and passing it issues aDeprecationWarning. It was passed to qpdf’sQPDFJob::initializeFromArgvas 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 ofargs, as it always was.Behavior change: Reading
PdfImagemetadata such as.width,.heightor.colorspacewhose value has the wrong PDF type (for example a/Widthwritten as a string or name) now raisesTypeError, with a message naming the key and the value found, instead ofNotImplementedError: Metadata access for Width. The exception is also aNotImplementedError, so existing handlers keep working.Behavior change: A
pikepdf.Stringno longer compares equal tobytes:pikepdf.String('abc') == b'abc'is nowFalse. It still compares equal to thestrit decodes to. AStringcompared equal to both astrand abytesthat 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']raisedKeyError. AStringnow hashes like itsstr. To compare the raw data, usebytes(s) == b'...'oras_bytes().Behavior change: A
pikepdf.Nameno longer compares equal tobytes:pikepdf.Name('/Foo') == b'/Foo'is nowFalse. It still compares equal to thestrof its UTF-8 bytes, soobj.Type == '/Page'works as before, and it now hashes like thatstr, so{pikepdf.Name('/héllo'): 1}['/héllo']no longer raisesKeyError. As PDF 2.0 (ISO 32000-2, 7.3.5) requires, names are compared byte for byte, with no Unicode normalization. Usebytes(name) == b'...'to compare the raw bytes.Behavior change: A
pikepdf.Realcombined with anint,Integer, orDecimal, or negated with unary-/+/abs(), now yields aDecimal(previously afloat, orTypeErrorforintoperands other than/). ARealwith afloatoperand still yields afloat. Division of anIntegerorRealby zero now raisesZeroDivisionError, as for Python numbers, instead ofValueError.Behavior change:
pikepdf.Matrixnow raisesTypeError, notValueError, for apikepdf.Objectthat is not a matrix or anObjectListwith a non-numeric element, matching what a native argument of the wrong type raises, so anexcept TypeErroraroundMatrix(*operands)behaves the same in either conversion mode. Size errors (must have 6 elements) remainValueError.Behavior change:
bool()on apikepdf.Integerorpikepdf.Realis now by value (bool(pikepdf.Integer(0))isFalse), instead of raisingNotImplementedError: code is unreachable.Behavior change:
as_dict()andas_list()now accept adefaultargument and raiseTypeErroron a type mismatch, instead of raisingpikepdf.PdfErrorwith no way to supply a default.Behavior change: Objects created in Python are now adopted by the
Pdfthey are inserted into, sopdf.Root.X = 42followed bypdf.Root.get_raw('/X').is_owned_by(pdf)isTrue, 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. ADictionaryorArrayis adopted in place and keeps its aliasing with the document; inserting the same container into a secondPdfnow raisesForeignObjectError, as it already did for containers parsed from a file. Construct a new object, or usepikepdf.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 anotherPdf. 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 intoPdf.pages, and values inserted throughNameTree/NumberTree__setitem__, now report the destination document as their owner and follow itsconversion_mode.Behavior change:
pikepdf.Object.with_same_owner_as()andpikepdf.Pdf.make_indirect()now raiseForeignObjectErrorfor a direct object that already belongs to a differentPdf, instead of silently retagging an object that the other document still references. Remove it from that document first, or make it indirect there and usepikepdf.Pdf.copy_foreign().Behavior change:
as_int()withcoerce=Truenow returns a supplied default for a value that does not fit in a 64-bit PDF integer, instead of raisingOverflowError. Called without a default it still raisesOverflowError.Behavior change:
repr()of apikepdf.Objectnow honors the effective conversion mode of the object being displayed (context manager, then its owningPdf, then the global setting), rather than only the global setting.Behavior change:
pikepdf.set_object_conversion_mode()now raisesValueErrorfor 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 fromopen(), instead of calling the stream’swrite()method for every chunk of output. Saving a large document is about twice as fast. Other streams, such asBytesIO, pipes and subclasses of theioclasses, are written throughwrite()as before.isinstance()checks againstpikepdf.Dictionary,pikepdf.Stream,pikepdf.Integerand 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()andpikepdf.as_decimal()answer for a nativeintorDecimalwithout constructing a PDF object, and converting a Real to aDecimalno longer imports thedecimalmodule 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, andD:2020010112(no minutes) and offsets in hours only, such as-08', raisedValueError, sopikepdf.Pdf.docinfodates in these forms were not synchronized to XMP.pikepdf.Pdf.save()now raisesValueErrorwhen the destination is the stream thePdfwas 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 openPdf, since qpdf reads its input lazily. Open withallow_overwriting_input=Trueto 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_decodeparmsandpikepdf.PdfImage.decode_parmsno longer raiseAttributeErrorwhen an image’s/DecodeParmsis a bare number or boolean, in either conversion mode. A/DecodeParmsthat is neither a dictionary nor an array is now ignored, as qpdf does, so each filter gets empty parameters.An image
/Decodethat is not an array (a bare number, boolean, name or dictionary) is now ignored in favour of the default decode array, instead of raisingAttributeErrorwhen the image is read or converted.pikepdf.Stream.write()now raisesTypeErrorrather thanAttributeErrorwhenfilterordecode_parmsis a native Python value such as anint,strordict.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/nullreplaced the device with a regular file.pikepdf.Matrix.inverse()now returns the correct translation for matrices whose determinant is not 1. Previously theeandfterms 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
intoutside the signed 64-bit range of a PDF integer, by assignment or inArray(...),Dictionary(...)orInteger(...), now raisesOverflowErrorinstead ofRuntimeError: std::bad_cast(or a misleadingTypeError).PdfImage,PdfImageBase,PdfJpxImage,PdfInlineImageandPaletteDataonce again report their module aspikepdf.models.image, the path they are imported from, instead of the privatepikepdf.models.image._classesintroduced when that module became a package in v10.10.The examples for
pikepdf.set_object_conversion_mode()andpikepdf.explicit_conversion()no longer open a nonexistenttest.pdf, and the global-mode example restores the default mode when it is done.str()of apikepdf.Integer,BooleanorRealnow gives the value –'42','True','1.50'– rather than the object’s repr. In implicit mode a scalar arrives asint/bool/Decimalandstr()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 onpikepdf.Integer,BooleanandReal(and on a null object), instead of raisingRuntimeError: don't know how to hash this. A scalar hashes like the Python value it compares equal to, soReal('1.0'),Decimal('1.0')and1agree, and a scalar can be a dict key or a set member as it could in implicit mode.pikepdf.Pdf.make_indirect()andpikepdf.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 thatPdf, after whichhash()raised and any dict or set already holding it broke; a module-level constant such asFOO = Name.Foobecame 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 anArray,DictionaryorStreamnow names the scalars nested inside it –pikepdf.Real('42.42')rather than'42.42', which was indistinguishable from a PDF string and did not surviveeval(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 (
PdfImageraisedNotImplementedErrorfor any indexed or /DeviceN colorspace), page labels (page.labelrestarted numbering at 1 and warned about a valid/St), outlines (a closed item read as open,flagscame back empty, and saving wrote the wrong/Count), form appearance generation (TypeErrorlaying out multiline and combed text fields),get_objects_with_ctm(a malformedcmoperator raised instead of being skipped),SimpleFontmetrics, andAction.new_window.PdfImagemetadata now reads the same in either conversion mode. A real/Widthor/Heightor a boolean/BitsPerComponentraisedNotImplementedErrorin explicit mode, and a real inside a/ColorSpacearray raised it in implicit mode.SimpleFont.ascent,descentandunscaled_char_width()now return aDecimalas documented, in either conversion mode. Font metrics stored as PDF integers were previously returned asint.Iterating a
NamePath(e.g.list(path)) no longer falls back to the legacy__getitem__(0), (1), ...protocol, which never raisedIndexErrorand so iterated forever, exhausting memory.NamePath in objno longer silently answersFalsefor 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 anint). The correct form, matching the compiled docstring and the Accessing nested objects with NamePath documentation, isNamePath['/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 bydel page.obj['/MediaBox']) and used after the document was closed, kept a dangling reference to the freed document and could crash the interpreter onrepr()or when inserted elsewhere. Removing an object from a document now releases its association with it, and aPdfdisconnects 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 packagesrc/pikepdf/_core/split into one stub per translation unit:_core/_matrix.pyicoverssrc/core/matrix.cpp,_core/_page.pyicoverssrc/core/page.cpp, and so on, with_core/__init__.pyire-exporting the lot and documenting the layout. Nothing changes at runtime or in the public API –pikepdf._coreis still one extension module andimport pikepdf._corestill resolves to it. Type checkers now name a type by its defining stub in messages (pikepdf._core._object.Objectrather thanpikepdf._core.Object);pikepdf.Objectremains 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.Streamandpikepdf.Dictionarydocumentation, 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=)andstatic_id=, and the matchingJobBuilder.deterministic_id()andJobBuilder.static_id().deterministic_idgives reproducible, production-safe/IDvalues;static_idsets the same dummy/IDin every file and is for testing only.static_idnow appears last in thePdf.save()signature. Allsave()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.DataDecodingErrornow derives frompikepdf.PdfError. A stream that will not decode is a defect in the document, and the same call that raises it –Object.read_bytes()– already raisedPdfErrorfor other kinds of damage, soexcept PdfErrorwas a handler that looked correct, passed on healthy files, and let a traceback escape on damaged ones. Code catchingDataDecodingErrorby name is unaffected. Code that ordersexcept PdfErrorbeforeexcept DataDecodingErrorwill now take the first branch; if the distinction matters, reverse the order.Behavior change:
pikepdf.PdfParsingErrornow derives frompikepdf.PdfError, for the same reason.pikepdf.PasswordErrorremains a sibling ofPdfError, not a subclass. A wrong password does not mean the document is defective, and handlers that report the two separately depend onexcept PdfErrornot catching it.pikepdf.NotExtractableErroris now exported. It was already the base class of the exportedHifiPrintImageNotTranscodableErrorbut could not be caught by name.Fixed the exceptions documentation, which referenced a nonexistent
FormCopyWarning(the class isPageCopyWarning) and omittedReferenceCycleError,PageCopyWarningandNotExtractableError.
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
listor asetwas assigned. Reading such a property back returns alistfor an ordered property and asetfor an unordered one, as before.Behavior change: assigning a plain string to a property that holds several values now issues the new
pikepdf.XmpTypeWarningand stores a single element array, rather than writing a bare string that other software discards. Thelog.errormessage pikepdf previously produced fordc:creatoralone is replaced by this warning, which covers every such property.Behavior change: assigning a
listorsetto 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.datetimeanddatetime.datemay now be assigned to date valued properties such asxmp: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:mmand-hh:mm.intmay be assigned topdfaid:partandbooltoxmpRights:Marked.Assigning a
setto an ordered property such asdc:creatornow 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 raisesTypeErrorfor 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
KeyErrornamingpikepdf.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_SCHEMAmaps a property’s qualified name to itsXmpPropertytype, 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:MetadataDaterather 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 asdict(meta.items())) raisedKeyErroror, 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. #634key in metadatanow 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 byinwhilemetadata[key]returns its value. Iterating the metadata likewise now includes a property with an empty value that is stored as an attribute ofrdf: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 as2024-06-01T12:00:00.5Zin XMP caused/ModDateto be dropped from DocumentInfo with a warning. A date-only XMP value such as2024-06-01now becomesD:20240601in 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 raisesKeyError(andmetadata.get()returns the default) instead ofValueErrorfrom 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 acceptedpikepdf.Object, but the elements of an operand list are usually numbers, which pikepdf decodes toint/bool/Decimalon 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 waypikepdf.Arraydoes, soinstruction.operands == [0]is True and0 in instruction.operandsworks. #742Comparisons on
_ObjectListnow 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()andremove()gave wrong answers even for operand lists made entirely ofpikepdf.Object.pikepdf._core._ObjectMapping, returned byObject.as_dict()andPage.get_images(), had all of the same problems and received the same treatment:__setitem__andupdate()now encode their value,==and!=compare values rather than object identity, and comparing a mapping to adictworks instead of printing a nanobind conversion warning._ObjectMapping.__setitem__and__delitem__now accept apikepdf.Namekey, which__getitem__and__contains__already did.Comparing an
_ObjectListor_ObjectMappingto a list or dict no longer printsnanobind: implicit conversion from type 'list' to type 'pikepdf._core._ObjectList' failed!to stderr.A warning raised from pikepdf’s C++ layer –
PageCopyWarning, thePage.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 underpython -W errororwarnings.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 andunparse_content_stream()no longer copy their partial result repeatedly while building it. Output is unchanged.pikepdf.StreamParseris now exported from the top-level package and included in__all__. It was always the required argument type of the publicPage.parse_contents(), but previously could only be imported from the privatepikepdf._coremodule. #738
Internals
Moved
Page’s box properties (mediabox,cropbox,artbox,bleedbox,trimbox), therotationproperty androtate(), 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 privatePage._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_cppdecorator frompikepdf._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 asPage._cpp__repr__no longer exist.Moved a second group of Python augmentations to C++ for the same reason: the
Attachmentsmapping methods,AttachedFileSpec.relationshipand its__repr__,AttachedFile.read_bytes(),Page.form_xobjects,Rectangle’s__repr__,__hash__andto_bbox(),Token.__repr__, andObject’sas_int(),as_bool(),as_float(),as_decimal()and_ipython_key_completions_().len(pdf.attachments)and iterating it no longer build apikepdf.AttachedFileSpecfor 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_xobjectsandObject._get_real_value()– are gone.A support class passed to
augmentsmay 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.Attachmentsuses this to get theMutableMappingmixins.
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 intoqpdf30.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 inpikepdf-<version>.dist-info/licenses/and are enumerated in the wheel’sLicense-Filemetadata.License-ExpressionremainsMPL-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
./qpdfto 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.iccnow returnsNoneinstead of raisingInvalidPdfImageError. An inline image’s colour space cannot be/ICCBased, so “no ICC profile” is the correct answer rather than an error – and raising madepikepdf.PdfImageBase.paletteunusable 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']frompikepdf.PdfInlineImage.filters, and an/Indexedcolour space written as/CS [/I /RGB 1 <...>]is now recognized as indexed instead of raisingNotImplementedError. Correspondingly,pikepdf.PdfInlineImage.unparse()now abbreviates names inside arrays, not only at the top level. Contents of/DecodeParmsdictionaries 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:
/Iis/Interpolateas a key but/Indexedas a value (ISO 32000-2 Tables 91 and 92). Previously/Iwas always expanded to/Indexed, so/I truebecame/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
DataDecodingErrorwith the decoder’s error message, instead of a bareRuntimeError("qpdf will consume this exception")plus an unraisable traceback on stderr. (#735)Exceptions other than
DataDecodingErrorraised by a JBIG2 decoder now keep their own type. Previously a failure unrelated to the image data – aDependencyErrorfrom a custom decoder, orKeyboardInterrupt– was reported as if the JBIG2 data were corrupt. Implementations ofpikepdf.jbig2.JBIG2DecoderInterface.decode_jbig2should raiseDataDecodingErrorto 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.OutlineItemnow supports the outline item dictionary’s/C(color),/F(italic/bold flags viapikepdf.OutlineItemFlagand the.italic/.boldproperties), 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-onlypikepdf.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/Destsdictionary) – to aDestination.Added a typed action model (
pikepdf.Actionand subclassesGoToAction,GoToRAction,GoToEAction,GoToDpAction,LaunchAction,URIAction,NamedAction,SetOCGStateAction,JavaScriptAction) for reading and constructing/Aaction dictionaries.pikepdf.OutlineItem.parsed_actiongives a typed view of an item’s action.Fixed the outline dictionary’s root
/Countbeing written as0instead of omitted when the outline has no entries at all, matching spec Table 150.
Fixed 1-bit
/Indexedimages losing their palette when extracted.pikepdf.PdfImage.as_pil_image()andpikepdf.PdfImage.extract_to()previously ignored the palette of a 1-bit indexed image over a/DeviceCMYKbase (returning a bilevel grayscale image) and of a single-colour palette (hival0). Both now decode correctly, matching the existing behavior for 2/4/8-bit indexed images.A 1-bit
/Indexedimage whose base colour space is unsupported (for example/DeviceNor/Separation) now raisesNotImplementedErrorinstead 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
/Widthand/Heightbefore allocating, raisingImageDecompressionErrorwhen it does not. This closes the remaining decompression-bomb gaps left by thepikepdf.PdfImage.MAX_IMAGE_PIXELSlimit 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 toNonerestored 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 ofMAX_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
ImageDecompressionErrorrather 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
/Widthor/HeightraisesInvalidPdfImageErrorinstead of failing further down with an obscure error from Pillow.
v10.10.0
Behavior change
pikepdf.PdfImage.as_pil_image()andpikepdf.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 (LAorRGBA) 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. Passapply_mask=Falseto recover the old behavior and obtain only the opaque base image.
New features
Image masking is now honored when extracting images.
/SMasksoft masks,/Maskstencil (explicit) masks, and/Maskcolour-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,/CalGrayand/CalCMYKimages, which previously raisedNotImplementedError. The samples are decoded as their device equivalents, and for CalRGB/CalGray an ICC profile synthesized from the colour space’sWhitePoint/Gamma/Matrixis attached to the extracted image so the calibration is preserved for colour-managed consumers.Added support for the
/Labcolour space, extracted as a PillowLABimage (saved as TIFF) with the PDFL*/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/Decodearray, and inline images that name their colour space now resolve it from the in-scope/Resourceswhen obtained viapikepdf.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 tomax(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 toNoneto disable the check.pikepdf.Arraynow implements the standard Pythonlistinterface: slicing (includingdelon slices and slice assignment), and theclear(),count(),index(),insert(),pop(),remove(), andreverse()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/Matteentry on a soft mask is not undone (a warning is emitted). When an image has both an/SMaskand a/Mask, the soft mask takes precedence. Colour-key masking is applied only to 8-bitL/RGB/CMYKimages.A filter chain containing two or more terminal image codecs (for example
[/DCTDecode /CCITTFaxDecode]) cannot be decoded by any reader and now raisesUnsupportedImageTypeErrorrather thanNotImplementedError.
Security
Hardened image extraction against decompression-bomb (memory exhaustion) attacks. A malicious PDF could declare an image with enormous
/Widthand/Heightso thatpikepdf.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 toPIL.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 raisepikepdf.DecompressionBombErrorand borderline images emitpikepdf.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/CCITTFaxDecodefilter’s own/DecodeParmsrather than the leading filter’s, which previously produced a corrupt extraction.Corrected the documentation of
pikepdf.StreamDecodeLevel: thespecializedandalllevels were each described with the other’s behavior.
v10.9.1
Fixed a crash (
SIGABRTviastd::terminate) that could occur when a file-backedpikepdf.Pdfwas deallocated while a Python exception was already propagating – for example whenpikepdf.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 existingpikepdf.Job, without hand-writing qpdf’s camelCase job JSON. Encryption permissions are expressed with the familiarpikepdf.Permissions/pikepdf.Encryptionmodels, 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:
Whole-document qpdf JSON:
pikepdf.Pdf.write_qpdf_json(),pikepdf.Pdf.from_qpdf_json()andpikepdf.Pdf.update_from_qpdf_json()serialize and reconstruct an entire PDF as qpdf JSON (theqpdf --json-output/--json-inputformat, version 2). This complements the existing object-levelpikepdf.Object.to_json(). Addedpikepdf.JSONStreamDatato control how stream data is represented.pikepdf.Pdf.get_xref_table()returns the cross-reference table as structured data (pikepdf.XrefEntry), complementing the print-onlypikepdf.Pdf.show_xref_table().pikepdf.Pdf.fix_dangling_references()repairs references to objects that are not present in the file.pikepdf.Page.flatten_rotation()bakes a page’s/Rotatevalue into its content stream.pikepdf.Page.copy_annotations()copies annotations (and associated form fields) from another page, applying a transformation matrix.pikepdf.Page.get_matrix_for_transformations()andpikepdf.Page.get_matrix_for_form_xobject_placement()expose qpdf’s page/form-XObject placement matrices.pikepdf.AcroForm.validate(),pikepdf.AcroForm.invalidate_cache()andpikepdf.AcroForm.transform_annotations()for working with interactive forms after manual structural edits.
Added
pikepdf.Page.get_images(), which by default recurses into nested form XObjects to find images. Thepikepdf.Page.imagesproperty 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. Useget_images()instead, orget_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 rawpage.Rotateattribute, it resolves a/Rotatevalue inherited from the page tree and reports0when no rotation is set, instead of raising. Assigning to it sets the absolute rotation. This addresses the long-standing confusion between thepage.Rotateattribute and thepage.rotate()method (#467).pikepdf.Page.rotate()now defaultsrelativetoFalse, sopage.rotate(90)sets an absolute rotation. Passingrelativeas a positional argument is deprecated and emits aDeprecationWarning; 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 apikepdf.PageCopyResult. Naivepages.extend()across documents andsave()of documents with orphaned form widgets now emitpikepdf.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.Destsname tree and the legacy PDF 1.1Root.Destsdictionary — so internal links keep working regardless of merge order. Name collisions are renamed and reported viapikepdf.PageCopyResult(named_dests_added,renamed_dests,dropped_dests). Naivepages.extend()now also warns when copied pages reference named destinations. (#148)
Fixes
Fixed image extraction ignoring the
/Decodearray, which caused colors to be inverted (or otherwise mismapped) when a PDF specified a non-default/Decodesuch as[1, 0].pikepdf.PdfImage.as_pil_image()andpikepdf.PdfImage.extract_to()now apply/Decodeas a linear per-channel mapping for grayscale, RGB and CMYK raster images, matching how a PDF viewer renders the image. Previously/Decodewas honored only for CCITTFax-encoded images. Thanks to Mark-Joy for the report. #650 Both methods gained anapply_decode_arrayparameter (defaultTrue). Passapply_decode_array=Falseto 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/Decoderemaps palette indices rather than colors – a non-identity/Decodethere 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/Decodewould double-invert them.Fixed
pikepdf.Pdf.save()decompressing streams when called withcompress_streams=Falseand no explicitstream_decode_level. qpdf 11.10 changed its default stream decode level togeneralized, which caused such saves to decompress (without recompressing) streams and balloon the output file. pikepdf now pins the decode level tononein this case, restoring the documented behavior thatcompress_streams=Falsealone does not trigger decompression. Fixes #676.The minimum required qpdf version is now 12.3.2. The new
pikepdf.AcroForm.validate()binding calls qpdf’sQPDFAcroFormDocumentHelper::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 ofpikepdf.PdfError), raised when an operation would create a cycle of direct (non-indirect) objects – a direct object may not contain itself, directly or indirectly. Usepikepdf.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.sanitizemodule 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), andremove_collection(PDF portfolio view), plus apikepdf.sanitize.Sanitizerbuilder for chaining these operations. The action-based removals now also traverse the document outline (bookmarks) and treat embedded go-to (/GoToE) as external access, andremove_attachmentsnow sweeps/AFassociated-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 andgetaccessors 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, theName.Attrshorthand, the scalar typesInteger/Boolean/Real, andNamePath) 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 alongsideqpdf30.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 fatalPyInterpreterState_Get ... the GIL is released (the current Python thread state is NULL)error or anImportErrorforpikepdf._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;delvewheelvendors a name-mangled copy of the C++ standard library runtime and uses CPython’s ownvcruntime140, 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._corefails 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)
DictionaryorArrayobjects that form a cyclic reference graph, for examplea['/Kids'] = [b]; b['/Kids'] = [a]; a == b. The cycle detector previously keyed its bookkeeping onunparseBinary(), 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
AttributeErrorwhen reading a document outline (“bookmarks”) whose items are missing the required/Titlefield. By default,Pdf.open_outline()now quietly treats a missing/Titleas an empty string; passingstrict=TrueraisesOutlineStructureErrorinstead. Fixes :issue:730.
v10.7.2
Fixed a segmentation fault when an object that is not an
Encryption,dict,bool, orNone(for example alistorunittest.mock.MagicMock) was passed to theencryptionargument ofPdf.save(). ATypeErroris 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 privatePdf._token_filter_refsattribute. The attribute is now reset before use.Suppressed nanobind’s
leaked instances/types/functionsreport at interpreter shutdown. Module-scope Python state (e.g.pytest.mark.parametrizearguments) commonly holds pikepdf objects until the interpreter exits; nanobind reports these as leaks even though they are not bugs. Set the environment variablePIKEPDF_NANOBIND_LEAK_WARNINGS=1before importing pikepdf to re-enable the report for debugging. Fixes :issue:728.Fixed
Array.append(None)raisingTypeErrorinstead 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) raisingTypeErrorinstead of the documentedValueErroradvising to usedelto remove the key. Same nanobind migration regression asArray.append(None).Fixed macro redefinition warnings on Fedora rawhide (Python 3.14 + glibc 2.42) by ensuring
Python.his 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
-Wpsabinote about C++17 ABI change forstd::pairin pybind11 headers.
v10.5.0
Fixed logger in
ctmmodule using__file__instead of__name__, which produced unhelpful log names. :issue:712Modernized README.
Test all README code blocks instead of just one.
v10.4.0
Enums are now proper Python
enum.Enum/enum.IntFlagtypes (PEP 435 compliant), migrated from pybind11’s deprecatedpy::enum_topy::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:708Fixed 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:
696Fixed 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:
697Added
.values()accessor toObjectfor iterating over dictionary values. Thanks @qooxzuub.:issue:699,697Added
.copy()and.update()methods toDictionary. Thanks @qooxzuub.:issue:700Improved
Object.copyimplementation and added type stubs. Thanks @qooxzuub.:issue:702Fixed missing return in
SimpleFont._encode_diffmap(). Thanks @lachlan.charlick :issue:706Improved error messages for invalid dictionary access. Thanks @qooxzuub.:issue:
701Lazy load lxml and Pillow to improve import time. Thanks @qooxzuub. :issue:
704Improved
atomic_overwriterobustness 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:689The
pikepdf.explicit_conversion()context manager is now thread-local and takes precedence over the global setting frompikepdf.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
Added
pikepdf.NamePathfor ergonomic access to deeply nested PDF structures. NamePath provides a single-operation traversal with helpful error messages showing exactly where traversal failed. See Accessing nested objects with NamePath for details.Added explicit scalar types:
pikepdf.Integer,pikepdf.Boolean, andpikepdf.Real. When explicit conversion mode is enabled, these types are returned instead of Python native types (int,bool,Decimal), enabling better type safety and static type checking.Added
pikepdf.set_object_conversion_mode()andpikepdf.get_object_conversion_mode()to control conversion behavior globally.Added
pikepdf.explicit_conversion()context manager for temporarily enabling explicit conversion mode.Added safe accessor methods to
pikepdf.Object:as_int(),as_bool(),as_float(), andas_decimal()with optional default parameters for type-safe access to scalar values.pikepdf.Integerandpikepdf.Realnow support full arithmetic operations with bothintandfloatoperands, including true division (/).
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:682Reformatted 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.