github py-pdf/fpdf2 2.9.0
Tiling patterns, HTML and SVG improvements, and other bug fixes

3 hours ago

[2.9.0] - 2026-10-29

Added

  • support for transform, style, opacity, and preserveAspectRatio="none" on SVG <image> tags - cf. issue #537 - thanks to @agustin18
  • support for colored and uncolored tiling patterns with FPDF.create_pattern() and FPDF.use_pattern(), including SVG <pattern> elements and pattern fills and strokes on referenced <use> shapes - cf. issue #790 - thanks to @andersonhc
  • support for unordered lists in multi_cell(markdown=True), using *, - or + as bullet markers - cf. issue #654 - thanks to @chaosreload
  • documentation on generating rMQR Codes - thanks to @Lucas-C
  • Python 3.15 is now officially supported - thanks to @andersonhc
  • Unicode font detection and enhanced error handling: when text cannot be encoded with the current font, the FPDFUnicodeEncodingException message now identifies the Unicode script (Cyrillic, Arabic, Chinese, etc.) and suggests a suitable font available on the system, via the new internal UnicodeFontManager - cf. PR #1563 - thanks to @otreci4sgelt0nas

Fixed

  • None values in PDF dictionaries are serialized as null instead of an invalid Python literal - cf. PR #1980 - thanks to @agustin18
  • Homogeneous arrays of PDF string and name objects use their PDF serialization, including encryption when applicable - cf. PR #2004 - thanks to @xiehuanyi and @andersonhc
  • PDF dates pad years below 1000 to four digits - cf. PR #2005 - thanks to @xiehuanyi
  • Corrected the text_columns API documentation link in the English tutorial and 10 translations - cf. PR #2039 - thanks to @ryanlcruz
  • FPDF.set_display_mode() rejects infinite numeric zoom values instead of writing invalid PDF numbers.
  • Page-count aliases honor center and right alignment with core fonts when text shaping is enabled.
  • 16-bit grayscale images, like 16-bit grayscale PNG files, are scaled down to 8 bits instead of being converted to RGBA, which made every value above 255 white, and keep the transparency of their tRNS color key - thanks to @wleeaf
  • Docstrings of PaintedPath.line_relative(), horizontal_line_relative(), vertical_line_relative() and Action name their arguments as the signatures do (dx, dy, next_action) - thanks to @mmporong
  • Tables inside FPDF.unbreakable() are replayed with their rows and cells intact when the block is moved to a new page - cf. issue #1230 - thanks to @alphaZytx
  • FPDF.table() raises a clear ValueError naming the column when text_align is a tuple with fewer values than the table has columns, like it does for col_widths, instead of an IndexError - thanks to @RavSinghChandan
  • FPDF.table(): a cell with a colspan below 1 spans one column, like a rowspan below 1 already spans one row, instead of raising ZeroDivisionError or FPDFException or breaking the column layout - thanks to @RavSinghChandan
  • Synchronize font_style and emphasis setters with font selection and state tracking - cf. issue #1223 - thanks to @agustin18
  • FPDF.table() raises ValueError: ... is not a valid TableBordersLayout for an unknown borders_layout name, like it does for cell_fill_mode and v_align, instead of a bare AttributeError - thanks to @RavSinghChandan
  • FPDF.set_font() and FPDF.add_font() treat a repeated style letter as the same style: style="BB" is bold, instead of raising FPDFException: Undefined font: helveticaBB - thanks to @RavSinghChandan
  • Document outlines count all visible descendants and link siblings by parent, retaining bookmarks when the hierarchy starts below the top level. - thanks to @xiehuanyi
  • Annotation colors and border widths are serialized as PDF numbers without exponent notation. - thanks to @xiehuanyi
  • Checksums for compressed embedded files are calculated from the original file bytes, as required by the PDF specification. - thanks to @xiehuanyi
  • Repeated annotation flags no longer change unrelated flag bits; annotation flags are combined with bitwise OR. - thanks to @xiehuanyi
  • FPDF.ink_annotation() raises a clear ValueError for an empty coords sequence, instead of ValueError: min() iterable argument is empty - cf. issue #2072 - thanks to @agustin18
  • SVG images with a zero width or height disable rendering instead of using viewport dimensions. - thanks to @xiehuanyi
  • FPDF.write_html() uses the first available font of a <font face> list, like browsers, as in face="Arial, Helvetica, sans-serif", with the generic serif, sans-serif and monospace families rendered by the core fonts, instead of raising FPDFException: Undefined font - thanks to @RavSinghChandan
  • SVG gradient stop-opacity accepts percentages, clamps out-of-range values to 0-1, and combines with the color's alpha channel instead of replacing it. - thanks to @xiehuanyi
  • SVG basic-shape coordinates and corner radii now accept absolute length units. - thanks to @xiehuanyi
  • SVG transforms accept numbers written in scientific notation, such as scale(1e-3), instead of silently ignoring the transform. - thanks to @xiehuanyi
  • SVG gradient stops honor inline style declarations of stop-opacity over the corresponding presentation attribute. - thanks to @xiehuanyi
  • SVG length and angle attributes accept scientific notation, such as width="1e2pt", instead of raising ValueError. - thanks to @xiehuanyi
  • Floating-point numbers in PDF arrays, dictionaries and destinations are written without exponent notation, preserving their values and allowing very small numeric zoom factors to be read correctly. - thanks to @xiehuanyi
  • SVG byte strings and BytesIO inputs accept a UTF-8 BOM and XML whitespace after <svg or <?xml, instead of being sent to the raster image loader. - thanks to @xiehuanyi
  • SVG import: a <polygon> or <polyline> whose points has an error (an invalid coordinate or an odd number of them) is drawn up to the last complete point before it, and one with empty points draws nothing, as the SVG specification says, instead of raising IndexError - thanks to @RavSinghChandan
  • FPDF.write_html() ignores an unsupported align value on <td>, <th> and <tr>, such as middle, like it already does for <p>, instead of raising ValueError - thanks to @RavSinghChandan
  • FPDF.regular_polygon() raises ValueError: 'numSides' parameter must be equal or greater than 1 for numSides below 1, instead of silently writing a path with no points (a bare h S, which closes a path that was never started) - thanks to @RavSinghChandan
  • Bulleted text-column paragraphs now honor their top margins, indented paragraphs retain the column's right boundary, and HTML list items honor their TextStyle.t_margin - based on PR #1218 - thanks to @andersonhc and @gmischler
  • FPDF.text_columns() raises a clear ValueError for ncols below 1, like insert_toc_placeholder() does for pages, instead of a ZeroDivisionError or an unrelated Not enough horizontal space error when rendering - thanks to @RavSinghChandan
  • FPDF() and FPDF.add_page() raise FPDFPageFormatException: Width and height must be positive numbers for a page format whose width or height is zero or negative, such as (0, 0), instead of writing a page with an empty or inverted /MediaBox - thanks to @RavSinghChandan
  • FPDF.write_html() now supports CSS length units (px, pt) and surrounding whitespace in dimension attributes for <table>, <td>, <th>, <hr>, and <img> elements without raising ValueError - thanks to @agustin18
  • FPDF.write_html() reads a CSS font-size on <font> with its unit, like browsers, as in style="font-size: 14pt" or "12px", instead of silently ignoring it; a number without a unit is still in points - thanks to @RavSinghChandan
  • FPDF.write_html() supports <ul type="square">, the third standard HTML bullet type, instead of raising NotImplementedError: Unicode fonts use ▪ and core fonts fall back to the bullet - thanks to @RavSinghChandan
  • FPDF.write_html() accepts the <ul type> keywords in any case, like browsers: <ul type="CIRCLE"> and <ul type="Square"> no longer raise NotImplementedError - thanks to @RavSinghChandan
  • FPDF.write_html() supports <ul type="none">, rendering the list items without a marker like browsers do, instead of raising NotImplementedError - thanks to @RavSinghChandan
  • FPDF.write_html() keeps the default bullet for an empty or unknown <ul type>, such as type="" or a misspelled type="disk", like browsers and like <ol> already does, instead of raising NotImplementedError - thanks to @RavSinghChandan
  • SVG import: color keywords are case-insensitive (fill="RED"), and fill / stroke accept transparent, painting nothing like none, instead of raising ValueError - thanks to @RavSinghChandan
  • FPDF.write_html() accepts css rgb(R, G, B) colors, and colors with surrounding spaces, in color and bgcolor attributes instead of raising ValueError - thanks to @RavSinghChandan
  • FPDF.write_html() accepts a legacy HTML color of six hex digits without the leading #, like browsers do, as in bgcolor="eeeeee" or <font color="ff0000">, instead of raising ValueError - thanks to @RavSinghChandan
  • FPDF.write_html() ignores color="transparent" and bgcolor="transparent" like browsers, keeping the inherited text or background color, instead of raising ValueError - thanks to @RavSinghChandan
  • FPDF.write_html() ignores an invalid CSS color on a heading, like browsers, as in <h1 style="color: nope">, keeping the color attribute or the heading's default color, instead of raising ValueError - thanks to @RavSinghChandan
  • FPDF.write_html() no longer raises IndexError on an <ol type="a"> or <ol type="A"> list with more than 26 items: like browsers, letters continue with aa, ab... and a start below 1 shows as a number - thanks to @RavSinghChandan
  • FPDF.write_html() numbers an <ol> with an unknown type, such as type="disc", as 1, 2, 3, like browsers, instead of raising NotImplementedError - thanks to @RavSinghChandan
  • FPDF.write_html() supports <li value="N"> in ordered lists: like browsers, the item is numbered N and the following ones continue from it - thanks to @RavSinghChandan
  • FPDF.write_html() no longer raises KeyError: 'href' on an <a> element without an href attribute, such as a named anchor (<a name="intro">); its text is rendered as plain text - thanks to @RavSinghChandan
  • FPDF.set_display_mode() raises FPDFException: Incorrect zoom display mode for a negative or non-numeric zoom, such as -50 or None, like it does for an unknown zoom name, instead of writing an invalid /OpenAction zoom or failing later in output() - thanks to @RavSinghChandan
  • FPDF.write_html() no longer warns about a missing end tag for the HTML void elements <meta>, <link>, <col>, <wbr>, <input>, <base>, <area>, <embed>, <source> and <track>, which never have one, as in the <head> of an HTML email - thanks to @RavSinghChandan
  • FPDF.write_html() treats an invalid colspan or rowspan on <td> / <th>, or one below 1, as 1 like browsers do, instead of raising ValueError or ZeroDivisionError - thanks to @RavSinghChandan
  • FPDF.write_html() stops a rowspan that goes past the last row of a <table> at the last row, like browsers, instead of raising FPDFException: Rowspan extends beyond end of table - thanks to @RavSinghChandan
  • FPDF.write_html() renders a <table> whose width is wider than the page, such as width="150%", at the page width with a warning, and ignores a negative width like browsers, instead of raising ValueError - thanks to @RavSinghChandan
  • FPDF.write_html() no longer raises ValueError: Invalid .col_widths specified on a table whose first row gives a width to only some of its cells, such as <td width="50%">a</td><td>b</td>: like browsers, the other cells share the remaining space, after the gaps of cellspacing; a cell with a colspan gives each column it spans an equal part of its width - thanks to @RavSinghChandan
  • warning detection during FPDFRecorder.replay() now correctly identifies context managers (contextlib.AbstractContextManager, @contextmanager, custom context managers) within unbreakable() sections - thanks to @agustin18
  • SVG import: opacity, fill-opacity and stroke-opacity accept percentages (50%), and an out-of-range opacity is clamped to 0-1 like the other two instead of raising ValueError - thanks to @RavSinghChandan
  • css rgb() colors accept percentages, as in rgb(100%, 0%, 50%) (clamped to 0%-100%), in SVG import and FPDF.write_html() instead of raising ValueError - thanks to @RavSinghChandan
  • hex color strings, such as in set_fill_color("#..."), must contain only hexadecimal digits: "#1 2345" and "#+12345" were silently read as #012345, and "#xyz" raised an unclear invalid literal for int() error; all now raise ValueError: ... could not be interpreted as a RGB(A) hex string - thanks to @RavSinghChandan
  • FPDF.ink_annotation() computes the horizontal position of the annotation rectangle (/Rect) from the x coordinates of the path instead of the y coordinates, so the rectangle encloses the drawn path; it could previously lie outside the page - thanks to @RavSinghChandan
  • FPDF.set_auto_page_break() and FPDF.set_margin() raise ValueError: Bottom margin must be zero or a positive number for a negative margin, instead of placing the page break below the bottom edge of the page, so that the last lines before each break were written off the page and lost - thanks to @RavSinghChandan
  • FPDF.embed_file() accepts empty byte content and rejects simultaneous path and byte sources even when the bytes are empty. - thanks to @xiehuanyi
  • FPDF.set_line_width() raises ValueError: Line width must be zero or a positive number for a negative width, like set_dash_pattern() does for a negative dash length, instead of writing a negative line width that the PDF specification does not allow - thanks to @RavSinghChandan
  • FPDF.star() raises ValueError: 'corners' parameter must be equal or greater than 1 for corners below 1, instead of a bare ZeroDivisionError for corners=0 and silently drawing nothing for a negative value - thanks to @RavSinghChandan
  • set_text_color(), set_fill_color() and set_draw_color() report a component outside 0-255 as given: set_text_color(300) now raises ValueError: color component 300 not in range [0, 255] instead of 1.1764705882352942 not in range [0.0, 1.0] - thanks to @RavSinghChandan
  • An internal link with an index that FPDF.add_link() did not return now raises a clear ValueError, like a link without a page number, instead of a bare AssertionError (or a KeyError with python -O) - thanks to @RavSinghChandan
  • FPDF.write_html() no longer writes the content of <style> and <script> elements into the document as text, like browsers, so an HTML page or email with a <style> block no longer shows its CSS - thanks to @RavSinghChandan
  • FPDF.add_page() and FPDF.set_page_label() raise ValueError: 'label_start' parameter must be equal or greater than 1 for a label_start below 1, instead of writing an invalid /St value that the PDF specification requires to be 1 or greater - thanks to @RavSinghChandan
  • FPDF.set_stretching() raises ValueError: Stretching must be a positive number for a stretching of zero or below, instead of laying out text with no width: justified text was all put on a single line, and other text raised an unrelated Not enough horizontal space to render a single character error - thanks to @RavSinghChandan
  • FPDF.write_html() raises NotImplementedError: Unsupported nested HTML tags inside <td> element: <table> for a <table> nested in a table cell, like for the other unsupported nested tags, instead of a bare AssertionError with no message - thanks to @RavSinghChandan

Changed

  • Updated the Portuguese tutorial, including tutorials 4 and 5 and the new PDF/A tutorial 7 - cf. PR #2038 - thanks to @ryanlcruz
  • Modernized type annotations for Python 3.11+, using union syntax, standard collection types, and typing.Self - thanks to @andersonhc
  • Removed redundant trailing zeros from numbers in PDF dictionaries and arrays outside content streams, preserving existing numeric precision - thanks to @andersonhc

Removed

  • Removed the defusedxml dependency: SVG parsing now relies on the standard-library ElementTree parser and Expat amplification limits; bounded internal entities are accepted - thanks to @agustin18, cf. issue #1793
  • support for Python 3.10, that reached end-of-life - thanks to @andersonhc

Don't miss a new fpdf2 release

NewReleases is sending notifications on new releases.