From ddc72ff5db3de291d66a497883cbef2375ca068d Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:22:16 +0200 Subject: [PATCH] Added support for CSS page break properties + bugfixes (#1209) --- CHANGELOG.md | 19 +++--- README.md | 2 +- docs/Development.md | 8 +++ docs/HTML.md | 19 +++++- docs/Shapes.md | 2 + docs/Tables.md | 2 + docs/Templates.md | 1 + docs/Text.md | 3 +- docs/TextStyling.md | 14 +++-- docs/index.md | 2 +- fpdf/fpdf.py | 32 +++++++--- fpdf/html.py | 54 +++++++++++----- scripts/changed_pdfs_comparison.html | 6 +- scripts/compare-changed-pdfs.py | 6 +- scripts/pdfchecker.py | 3 + test/html/html_features.pdf | Bin 6323 -> 6742 bytes ...ght_at_page_bottom_triggers_page_break.pdf | Bin 0 -> 2650 bytes test/html/html_page_break_after.pdf | Bin 0 -> 1810 bytes test/html/html_page_break_before.pdf | Bin 0 -> 1819 bytes ...th_multiline_cells_and_split_over_page.pdf | Bin 3183 -> 3167 bytes test/html/test_html.py | 59 +++++++++++++++--- ...l_context_font_size_and_header_footer.pdf} | Bin 1546 -> 1534 bytes test/outline/html_toc.pdf | Bin 4312 -> 4304 bytes test/outline/html_toc_2_pages.pdf | Bin 20920 -> 20885 bytes .../html_toc_with_custom_rendering.pdf | Bin 2476 -> 2471 bytes .../html_toc_with_h1_as_2nd_heading.pdf | Bin 2850 -> 2845 bytes test/test_graphics_context.py | 23 ++++++- test/text/test_cell.py | 21 ------- 28 files changed, 200 insertions(+), 76 deletions(-) create mode 100644 test/html/html_img_without_height_at_page_bottom_triggers_page_break.pdf create mode 100644 test/html/html_page_break_after.pdf create mode 100644 test/html/html_page_break_before.pdf rename test/{text/header_footer_and_local_context_font_size.pdf => local_context_font_size_and_header_footer.pdf} (81%) diff --git a/CHANGELOG.md b/CHANGELOG.md index de0f49290..5f8717543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,19 +19,21 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.7.10] - Not released yet ### Added * [`Templates`](https://py-pdf.github.io/fpdf2/fpdf/Templates.html) can now be also defined in JSON files. -* support to optionally set `wrapmode` in templates (default `"WORD"` can optionally be set to `"CHAR"` to support wrapping on characters for scripts like Chinese or Japanese) - _cf._ [#1159](https://github.com/py-pdf/fpdf2/issues/1159) -* support for quadratic and cubic Bézier curves with [`FPDF.bezier()`](https://py-pdf.github.io/fpdf2/fpdf/Shapes.html#fpdf.fpdf.FPDF.bezier) -* feature to identify the Unicode script of the input text and break it into fragments when different scripts are used, improving text shaping results +* support to optionally set `wrapmode` in templates (default `"WORD"` can optionally be set to `"CHAR"` to support wrapping on characters for scripts like Chinese or Japanese) - _cf._ [#1159](https://github.com/py-pdf/fpdf2/issues/1159) - thanks to @carlhiggs +* support for quadratic and cubic Bézier curves with [`FPDF.bezier()`](https://py-pdf.github.io/fpdf2/fpdf/Shapes.html#fpdf.fpdf.FPDF.bezier) - thanks to @awmc000 +* feature to identify the Unicode script of the input text and break it into fragments when different scripts are used, improving [text shaping](https://py-pdf.github.io/fpdf2/TextShaping.html) results * [`FPDF.image()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image): now handles `keep_aspect_ratio` in combination with an enum value provided to `x` * file names are mentioned in errors when `fpdf2` fails to parse a SVG image -* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): spacing before lists can now be adjusted via the `HTML2FPDF.list_vertical_margin` attribute +* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): now supports CSS page breaks properties : [documentation](https://py-pdf.github.io/fpdf2/HTML.html#page-breaks) +* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): spacing before lists can now be adjusted via the `HTML2FPDF.list_vertical_margin` attribute - thanks to @lcgeneralprojects ### Fixed * [`FPDF.local_context()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.local_context) used to leak styling during page breaks, when rendering `footer()` & `header()` * [`fpdf.drawing.DeviceCMYK`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceCMYK) objects can now be passed to [`FPDF.set_draw_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_draw_color), [`FPDF.set_fill_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_fill_color) and [`FPDF.set_text_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_text_color) without raising a `ValueError`: [documentation](https://py-pdf.github.io/fpdf2/Text.html#text-formatting). * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): fixing rendering of `
Hello world. I am tired.
right aligned text
-i am a paragraph
in two parts.
i am a paragraph
in two parts.
hello in green
hello small
hello helvetica
@@ -79,7 +79,7 @@ pdf.output("html.pdf") ``` -## Styling HTML tags globally +### Styling HTML tags globally _New in [:octicons-tag-24: 2.7.9](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ @@ -124,6 +124,7 @@ pdf.output("html_dd_indented.pdf") * ``: paragraphs (and `align`, `line-height` attributes)
+* `
` & `
+Top of a new page. +
+``` ## Known limitations diff --git a/docs/Shapes.md b/docs/Shapes.md index 72cc00b51..8437d2256 100644 --- a/docs/Shapes.md +++ b/docs/Shapes.md @@ -162,6 +162,8 @@ pdf.output("solid_arc.pdf") ![](solid_arc.png) ## Bezier Curve ## +_New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ + Using [`bezier()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.bezier) to create a cubic Bezier curve: ```python from fpdf import FPDF diff --git a/docs/Tables.md b/docs/Tables.md index 9b8a1847d..464e42eae 100644 --- a/docs/Tables.md +++ b/docs/Tables.md @@ -224,6 +224,8 @@ The cell color is set following those settings, ordered by priority: 4. The table setting `cell_fill_color`, if `cell_fill_mode` indicates to fill a cell 5. The document `.fill_color` set before rendering the table +_New in [:octicons-tag-24: 2.7.9](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ + Finally, it is possible to define your own cell-filling logic: ```python diff --git a/docs/Templates.md b/docs/Templates.md index 7c902a381..e9f53d907 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -265,6 +265,7 @@ f.render("./template.pdf") See template.py or [Web2Py] (Web2Py.md) for a complete example. # Example - Elements defined in JSON file # +_New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ The JSON file must consist of an array of objects. Each object with its name/value pairs define a template element: diff --git a/docs/Text.md b/docs/Text.md index 9237038d5..565c09786 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -49,7 +49,8 @@ For all text insertion methods, the relevant font related properties (eg. font/s All three `set_*_colors()` methods accept either a single greyscale value, 3 values as RGB components, a single `#abc` or `#abcdef` hexadecimal color string, or an instance of [`fpdf.drawing.DeviceCMYK`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceCMYK), [`fpdf.drawing.DeviceRGB`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceRGB) or [`fpdf.drawing.DeviceGray`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceGray). You can even use [named web colors](https://en.wikipedia.org/wiki/Web_colors#HTML_color_names) by using [`html.color_as_decimal()`](fpdf/html.html#fpdf.html.color_as_decimal). -In addition, some of the methods can optionally use [markdown](TextStyling.md#markdowntrue) or [HTML](HTML.md) markup in the supplied text in order to change the font style (bold/italic/underline) of parts of the output. +More text styling options can be found on the page [Text styling](TextStyling.md), +including [Markdown syntax](TextStyling.md#markdowntrue) and [HTML markup](HTML.md). ## Change in current position `.cell()` and `.multi_cell()` let you specify where the current position (`.x`/`.y`) should go after the call. diff --git a/docs/TextStyling.md b/docs/TextStyling.md index cdcd2c97b..899ef6044 100644 --- a/docs/TextStyling.md +++ b/docs/TextStyling.md @@ -1,8 +1,8 @@ # Text styling # -## set_font() ## +## .set_font() ## -Setting emphasis on text can be controlled by using `set_font(style=...)`: +Setting emphasis on text can be controlled by using `.set_font(style=...)`: * `style="B"` indicates **bold** * `style="I"` indicates _italics_ @@ -66,6 +66,10 @@ pdf.multi_cell(w=150, text=LOREM_IPSUM[:200], new_x="LEFT", fill=True) ``` ![](char_spacing.png) +For a more complete support of **Markdown** syntax, +check out this guide to combine `fpdf2` with the `mistletoe` library: +[Combine with mistletoe to use Markdown](CombineWithMistletoeoToUseMarkdown.md). + ## Subscript, Superscript, and Fractional Numbers @@ -175,7 +179,7 @@ More examples from [`test_text_mode.py`](https://github.com/py-pdf/fpdf2/blob/ma An optional `markdown=True` parameter can be passed to the [`cell()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.cell) & [`multi_cell()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell) methods -in order to enable basic Markdown-like styling: `**bold**, __italics__, --underlined--` +in order to enable basic Markdown-like styling: `**bold**, __italics__, --underlined--`. Bold & italics require using dedicated fonts for each style. @@ -199,9 +203,9 @@ Several unit tests in `test/text/` demonstrate that: * [test_multi_cell_markdown_with_ttf_fonts](https://github.com/py-pdf/fpdf2/blob/2.6.1/test/text/test_multi_cell_markdown.py#L27) -## write_html ## +## .write_html() ## -[`write_html`](HTML.md) allows to set emphasis on text through the ``, `` and `` tags: +[`.write_html()`](HTML.md) allows to set emphasis on text through the ``, `` and `` tags: ```python pdf.write_html("""bold diff --git a/docs/index.md b/docs/index.md index acfeb9ada..fd9db7618 100644 --- a/docs/index.md +++ b/docs/index.md @@ -41,7 +41,7 @@ Go try it **now** online in a Jupyter notebook: [![Open In Colab](https://colab. * It has very few dependencies: [Pillow](https://pillow.readthedocs.io/en/stable/), [defusedxml](https://pypi.org/project/defusedxml/), & [fonttools](https://pypi.org/project/fonttools/) * Can render [mathematical equations & charts](https://py-pdf.github.io/fpdf2/Maths.html) * Many example scripts available throughout this documentation, including usage examples with [Django](https://www.djangoproject.com/), [Flask](https://flask.palletsprojects.com), [FastAPI](https://fastapi.tiangolo.com/), [streamlit](https://streamlit.io/), AWS lambdas... : [Usage in web APIs](UsageInWebAPI.md) -* Unit tests with `qpdf`-based PDF diffing, and PDF samples validation using 3 different checkers: +* more than 1300 unit tests with `qpdf`-based PDF diffing, and PDF samples validation using 3 different checkers: [![QPDF logo](qpdf-logo.svg)](https://github.com/qpdf/qpdf) [![PDF Checker logo](pdfchecker-logo.png)](https://www.datalogics.com/products/pdf-tools/pdf-checker/) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 43baeafb6..e60f23404 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -2770,6 +2770,7 @@ def local_context(self, **kwargs): fill_opacity font_family font_size + font_size_pt font_style font_stretching intersection_rule @@ -2807,7 +2808,7 @@ def _start_local_context( self, font_family=None, font_style=None, - font_size=None, + font_size_pt=None, line_width=None, draw_color=None, fill_color=None, @@ -2819,11 +2820,16 @@ def _start_local_context( This method starts a "q/Q" context in the page content stream, and inserts operators in it to initialize all the PDF settings specified. """ - if "font_size_pt" in kwargs: - if font_size is not None: + if "font_size" in kwargs: + # At some point we may want to deprecate font_size here in favour of font_size_pt, + # and raise a warning if font_size is provided: + # * font_size_pt is more consistent with the size parameter of .set_font(), provided in points. + # * font_size can be misused, as users may not be aware of the difference between the 2 properties, + # and may erroneously provide a value in points as font_size. + if font_size_pt is not None: raise ValueError("font_size & font_size_pt cannot be both provided") - font_size = kwargs["font_size_pt"] / self.k - del kwargs["font_size_pt"] + font_size_pt = kwargs["font_size"] * self.k + del kwargs["font_size"] gs = None for key, value in kwargs.items(): if key in ( @@ -2868,11 +2874,15 @@ def _start_local_context( else: self._out("q") # All the following calls to .set*() methods invoke .out() and write to the stream buffer: - if font_family is not None or font_style is not None or font_size is not None: + if ( + font_family is not None + or font_style is not None + or font_size_pt is not None + ): self.set_font( font_family or self.font_family, font_style or self.font_style, - font_size or self.font_size_pt, + font_size_pt or self.font_size_pt, ) if line_width is not None: self.set_line_width(line_width) @@ -3586,15 +3596,19 @@ def _perform_page_break(self): # by popping out every GraphicsState: gs_stack = [] while self._is_current_graphics_state_nested(): + gs_stack.append(self._get_current_graphics_state()) + self._pop_local_stack() # This code assumes that every Graphics State in the stack # has been pushed in it while adding a "q" in the PDF stream # (which is what FPDF.local_context() does): self._end_local_context() - gs_stack.append(self._pop_local_stack()) + # Using a temporary GS to render header & footer: + self._push_local_stack() self.add_page(same=True) for prev_gs in reversed(gs_stack): - self._push_local_stack(prev_gs) self._start_local_context(**prev_gs) + self._push_local_stack() + self._pop_local_stack() self.x = x # restore x but not y after drawing header def _has_next_page(self): diff --git a/fpdf/html.py b/fpdf/html.py index 601a7c323..fa47bd098 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -12,7 +12,7 @@ from .deprecation import get_stack_level from .drawing import color_from_hex_string, convert_to_device_color -from .enums import TextEmphasis, XPos, YPos +from .enums import Align, TextEmphasis, XPos, YPos from .errors import FPDFException from .fonts import FontFace from .table import Table @@ -325,7 +325,7 @@ def __init__( self.emphasis = dict(b=False, i=False, u=False) self.font_size = pdf.font_size_pt self.set_font(pdf.font_family or "times", size=self.font_size, set_default=True) - + self._page_break_after_paragraph = False self._pre_formatted = False # preserve whitespace while True. # nothing written yet to, remove one initial nl: self._pre_started = False @@ -338,9 +338,8 @@ def __init__( self.line_height_stack = [] self.ol_type = [] # when inside atag, can be "a", "A", "i", "I" or "1" self.bullet = [] - self.default_conversion_factor = ( - get_scale_factor("mm") / self.pdf.k - ) # factor for converting default values from mm to document units + # factor for converting default values from mm to document units: + self.default_conversion_factor = get_scale_factor("mm") / self.pdf.k if list_vertical_margin is None: # Default value of 2 to be multiplied by the conversion factor # for list_vertical_margin is given in mm @@ -464,6 +463,10 @@ def _end_paragraph(self): self._column.render() self._paragraph = None self.follows_trailing_space = True + if self._page_break_after_paragraph: + # pylint: disable=protected-access + self.pdf._perform_page_break() + self._page_break_after_paragraph = False def _write_paragraph(self, text, link=None): if not self._paragraph: @@ -537,6 +540,8 @@ def handle_data(self, data): else: self._write_data(data) self.follows_trailing_space = data[-1] == " " + if self._page_break_after_paragraph: + self._end_paragraph() def _write_data(self, data): if self.href: @@ -558,6 +563,10 @@ def handle_starttag(self, tag, attrs): LOGGER.debug("STARTTAG %s %s", tag, attrs) parse_style(attrs) self._tags_stack.append(tag) + if attrs.get("break-before") == "page": + self._end_paragraph() + # pylint: disable=protected-access + self.pdf._perform_page_break() if tag == "dt": self._new_paragraph( line_height=( @@ -640,10 +649,21 @@ def handle_starttag(self, tag, attrs): size=tag_style.size_pt or self.font_size, ) if tag == "hr": + self._end_paragraph() + width = attrs.get("width") + if width: + if width[-1] == "%": + width = self.pdf.epw * int(width[:-1]) / 100 + else: + width = int(width) / self.pdf.k + else: + width = self.pdf.epw + # Centering: + x_start = self.pdf.l_margin + (self.pdf.epw - width) / 2 self.pdf.line( - x1=self.pdf.l_margin, + x1=x_start, y1=self.pdf.y, - x2=self.pdf.l_margin + self.pdf.epw, + x2=x_start + width, y2=self.pdf.y, ) self._write_paragraph("\n") @@ -863,23 +883,20 @@ def handle_starttag(self, tag, attrs): self.table_row.cell(img=attrs["src"], img_fill_width=True) self.td_th["inserted"] = True return - if self.pdf.y + height > self.pdf.page_break_trigger: - self.pdf.add_page(same=True) - x, y = self.pdf.get_x(), self.pdf.get_y() + x = self.pdf.get_x() if self.align and self.align[0].upper() == "C": - x = self.pdf.w / 2 - width / 2 + x = Align.C LOGGER.debug( 'image "%s" x=%d y=%d width=%d height=%d', attrs["src"], x, - y, + self.pdf.get_y(), width, height, ) - info = self.pdf.image( - self.image_map(attrs["src"]), x, y, width, height, link=self.href + self.pdf.image( + self.image_map(attrs["src"]), x=x, w=width, h=height, link=self.href ) - self.pdf.set_y(y + info.rendered_height) if tag == "center": self._new_paragraph(align="C") if tag == "toc": @@ -891,6 +908,13 @@ def handle_starttag(self, tag, attrs): self.pdf.char_vpos = "SUP" if tag == "sub": self.pdf.char_vpos = "SUB" + if attrs.get("break-after") == "page": + if tag in ("br", "hr", "img"): + self._end_paragraph() + # pylint: disable=protected-access + self.pdf._perform_page_break() + else: + self._page_break_after_paragraph = True def handle_endtag(self, tag): LOGGER.debug("ENDTAG %s", tag) diff --git a/scripts/changed_pdfs_comparison.html b/scripts/changed_pdfs_comparison.html index e5b9ef57b..df1616c22 100644 --- a/scripts/changed_pdfs_comparison.html +++ b/scripts/changed_pdfs_comparison.html @@ -7,11 +7,15 @@ + LEFT: Version from
master
branch + - + RIGHT: Version from current branch +
{% for changed_pdf_file in changed_pdf_files %} {{ changed_pdf_file }}- +{% endfor %} diff --git a/scripts/compare-changed-pdfs.py b/scripts/compare-changed-pdfs.py index ebd48462b..48e08fc44 100755 --- a/scripts/compare-changed-pdfs.py +++ b/scripts/compare-changed-pdfs.py @@ -30,14 +30,14 @@ def scantree_dirs(path): yield from scantree_dirs(entry.path) -target_dir = sys.argv[1] if len(sys.argv) > 1 else "test" -print(f"Processing all PDF reference files in {target_dir}") +target_dir = sys.argv[1] if len(sys.argv) > 1 else "test/" +print(f"Processing all PDF reference files in directory {target_dir}") stdout = check_output("git diff --name-status master", shell=True) changed_pdf_files = [ line[1:].strip() for line in stdout.decode("utf-8").splitlines() - if line.startswith(f"M\t{target_dir}") + if line.startswith(f"M\t{target_dir}") and line.endswith(".pdf") ] TMP_DIR.mkdir(exist_ok=True) diff --git a/scripts/pdfchecker.py b/scripts/pdfchecker.py index c86d665a2..15ccbc0e1 100755 --- a/scripts/pdfchecker.py +++ b/scripts/pdfchecker.py @@ -7,6 +7,9 @@ # * parallelize the execution of this analysis on all PDF files # * allow to ignore some errors considered harmless, listed in pdfchecker-ignore.json +# Note: among the 3 checkers we use for fpdf2, PDF Checker is the only one that report errors +# for unbalanced q/Q contexts in content streams, even if it does not provide a clear message. + # USAGE: ./pdfchecker.py [$pdf_filepath|--process-all-test-pdf-files|--print-aggregated-report] import sys diff --git a/test/html/html_features.pdf b/test/html/html_features.pdf index c19c4e2a16198fa1035f9e936fa68cdcd649e3d5..f2ea382450000d1338d4be9e519bbd2d59b842d3 100644 GIT binary patch delta 2881 zcmah~2{=^i8*j!wGc?Rh#Ke@P5oOMqIkU7#hRGDymMHvXD`d%1l$a|eTbcSo{``fc zL>q22mbxx2mToEBP|>wiT2xAf>mDsnX?mXj_nhZB-}^hi_kDlw`@QF!a;SqP*1{l% zs5&w@2s*^shC%XAVL*+-JkW?S6tNMUsVOczBFxt_2p5&pxUM<4n?bA@H+px9TL0SS zv|`)_>ksy!Nh=1*A7u^&bj=)@yxfav&-@78%z7@ zc=y(N#i|af|2<*lXR}{ljudms+|SGEMpxru+C-LE7IT|~u~FM*sJd3wvxRh?g-IE9 zC;1?@W4-ew;{{P_6d{&f_qtlPd~24ADQR?&Hy%*kCMhaOC|z;)g=$Ul5}nNP1+b64 z!qxrMoIT#jk0Tz|S7Iy?FRX^6uGQY~)(tgjd6y$6F*s_>O;9N#02`I&0Zm!Dh{NN} zIPUa}ZU^+Z6JEX{ORPioRxI6+IUt_1`B%6Vf3!glyHMWSZ>P8}B)wszno_>()7< zw`XdnOpMHy2vQgEV-FlW+3#1cq|l_1_25wXlgIuMcSN~~nQ|v343Wp>T%~89IMXOjTj@O%q zoSY>ec_CFF%r0CqIZ>G=Sx_?)YgTpk#-H~E@xKe+tPGW3r&&Na9@aTL!OA;yhSzZR zqDRg1=x)Wur^uq6*(G64F>Q4HwT8l!`~TJ0_xyjZd4i$Won5V`YYG( yld=%y0q8+a{Bs^^P(Ynp1BuR|yx47RcG1 zAMaKajad%{sW1|@TH^aE_oxJ` $?gwQ-0ja$@s@<8>#z6Bkj4p#zq{}crY}?hgn27!BXmk=0{!roOJ#5ob zRo}7jL+N%A-RxT8R)V;{+_^ KMnjJ{0IUPiDtBj{yZw_Yi-Z0-NY_ulLj@1&N|l*|uW0=wlc$ >b5*3TyBKir%t1D+DrkU{xd7Sgy#>d9*wx1~A+YvF~yny5|aajNa{zb@G346+ZbVYW#2J3zAtz7d(8P~7e;6|_p()L&OyuHI| z>>FV8jwI*}o!T9``vEJbe&gNa=Ap?=j+v3q$V&@!C=G^-Z0~!<^pky>LEtWq z3Jh^pfQO$0_Ty|oHhO=GvjyXLWgx&iVK^WM?}6ch3A_b{SH6l6hQ;uvoy)sbuvje2 z0Q1$<)tNK~%>id3fN2ox%a%!l5YVgY^0Q|M4dS5B%+O;+y$RoYPs5;FjR1_*2xx)5 zst5>HBa*q(QPKY-die(Xgm?wu&`MC>Zxn?u6i`i?4<<=k;CD4mu!2knG9(JvMK;1f zpoolsov1ZJHUgJOD!`DU25iadV1z_g<9(KG?HL|HgFhL-oiZ1gkX6ufP07oaF&W=6 zz8ZF(VQ6=0@Tc53lXd 3Mb2X^3Oq_{BNC-OSpQl$Cyg7u%{AyUx zT$giM77PKzx8QJBF)i3!3%)sn!RMN@5OajL%F=>m`u`d3pTFv!VG&VbzMF9f59Z?Z K^(}1$xPJkaz%$kW delta 2779 zcmah~3piA37dAsNV?@j##2{rdW$&3iH_2r%ZX=FMB@{`;?-s&I$53vC!WSt@N}ZBR zE}^R;mnhQdL~iMHL4;f?YWPQ&=P&a=|G%I8JbSHoziYkg`_}sQeu|XADat4$RYfh8 z?Te!17NgKG6O^D#c?g0vFu(?f1bKP*VZ*cQ9Gm^mLHM42osqP)Z)g0d`PUV_uMezU zV`!8p=@?UfWY^=?;^@rdfniilvEQot_Z%Iy(bgY^WU(zba+Y;>%MtM&xMw lI-oQz>iX7~w=wK0t!}aO)acERkt0Qp zGqyeEF3EWDrc}qk i`^0nMr_klB~S<&~r zPBp)+Lm`O#!LIi(ALS4@2a7e#YPeflG<7;v=WIGf>Aw6T{h2|wW;=c<4tqIq>-M42 zpcKa5XD89w)4Y%@ }j_xo}DQ{)jo&Y`lx-j@i)CIJ~s@?zRlq!UEVfsQ}t*&CT!|%wgzAFd9~t< zIs0Fo()Z&BGMaR4HrtnSVTCc?$`u+$liEh-(5_bZwxb=m;^eh=YQm4BE$d#YbQh0Q zkW_8V9HXQRr){E1SCeo#n0@!N^-oqn75>&$g~L5Zr!y~dxFRq4lhVX5#^=fV1NttB z^Lsu^j#dYkgTF*MXqYAFt1_UAY4ey6{g>vZXEfV)LHM|jzhl?@mHf! nul7d+Yfe`O=awXqp~P2RZrj!tqtOJZ@7|Z# za^YM !MYR_NYva*dDDu zGw4Rl9ST#)E$Il9coPSk)+pqsoA#iSj~5lWgg8`|eQxNl2)mJq^H8GRic4y dVfy$?#oujH|>V3#|^o Zy0y88<8)JsZ=R;l#5 zM!PV?!_23D#QW)Vd~c=Blh3z9*R9XHo2F^@7~eF0ZK$v~rz!4zZT?i5<<;qHQfHmU zV(NyJ_?;t2MH# N494@Jnqit_0nDUki(z7X`+t+4$I9R z>+08@$Mikj4HY<%c!m52=~|*uSxoE2sM7u2y&}!d9ZoJit+`Xq+X&U988)G|K4zeC z%9EaeIkJ~fYzWtD7c7%K>>d;a-{@G3X@RJpyT-;QO^^94f#ULwUR%T@6Ge7q+HEee zkL>5Jh~Yii7)47>dChowZ|a!&Xy~m&Q3cggJI7*m$|4(IH3!~mS11BQ@t@K 63(-gY)<9YLX~ zlnwEsF>Hguf^9}It6y(5iXrE863VxpPqN d{ z$XWDB%i883Mfq4+=K+P*iA@PIEomMb@m4!>w{?yMl!>Zy@9p_b`&ew`;4bT-GY`#b z)$|gWcG@RmBVWH;ZY1%ii^{5F56E*D`H7Vx#zV*k?JVcn_ej}t5j(_2&T3}{?|Pj^ z^7y_<>G+lPE+A3d99%)EfDv&Lj}HBEicY23*w#Y0xKTdApr&p`s6p{=;$0L*zGTAP zxbw-9i+Z{Ih>K6tmb_QrpL9-t)U!dXJ)J%DqEOE^F!YkK-0DSxn6>s(dh$}T(zJIH zo?>e2IigZ29+MvW?U3QkT+K)3mge!NiP~&GVqxDapHLpwo9fE5WL==UXQuq~oWxCP z;yU N&hZ;>D|4@1KdSNe$+`Hr-pp3BqN@6O2harIstRGOu;=no_7W7Fj25OQP zC<=gZgs v#P93 z8jW59Gl>u}V7m`iUmvT>3Gfdg>g(%rU?L0)aC-qnA;OgJCKa5LV**JzEVwAWMD2fb zX=$=1;K@=&+|PNy89ACDUJ{tdDxzS4XNITG=N9}J1-F;~j`>0;6fiBPtV{hS%Jm2i zAyU3Oj=u+b5p`|%hWL8>dj gq4^RKZK2>KSw`$<@!D`?mPA~L`K3osf(Fck|h`hUR~ z4B>ptUwORwG!zy?sA?)i7mUZRB2)%lNEo5QjD !lVcl zgRl^xHqjvA0% w8Ny{?m6F0t<=IXb=P3T&5BUVRh{T0t93HZE7w3cLoqC zg4*Ygsii5=MW1QLU?Ut;Hf(CjWYHiF0vVf_7;_kOV=Bvpfxrg;pW*iH3+xdT5+3BW Q6HBAfSy(MCGg}V!-;@{&pa1{> diff --git a/test/html/html_img_without_height_at_page_bottom_triggers_page_break.pdf b/test/html/html_img_without_height_at_page_bottom_triggers_page_break.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a174a39e1263b9506ff6382c88136a9f128b2cd2 GIT binary patch literal 2650 zcmbtW3se*76$V#82dz{AMMZzA5e?#GW)hN-NF@m&2x + z!8*Wt5tK+s$ue99WMXWYjD%kkw^}HGYV{_{hD1al6l3H;aToBl7K({9a5T$|AOloQ za~i Bwz;G19*?@Rh zN2|BO*HHu=Wn=AD0|g=ZC|+Xn)?EO3-BCa_ZRRK|bk?lrC>3R3jTC~$QH+U817e(4 zvT;^QpNTk12wl$uyp@owTl}pRiEqs!5=G)$gh)FnCc!fG&%!RGYl7RByPkb~dPbB+ z62&KZ)S!|bi6HDpB6uw;V 7o??mq-A~ryVkKc&{OUGHpOEN3~XX01O;!1(*a31M$S qViD&sB6)a{qs`r&|3C{8PyT4ip!=giA4Elk4C4gBHbqR$3= zAFX?O?#TNcf_Yu;p0;<7U251`J%iR 8ZV`4y_NjJf(8g~(a(iYx`ye{& zw$JK{rlogo75SYv3EHyr185A2r? zmV~IR)xFF6HCOK%9|o4s`Mjd}_`;Jjzaf=#A5E*Y90;FM1_JlqIbAOMp!WKn>s_*% zocWD0bPJw6{rtkpe%!mZ;ggQki8uCMlwVQdz0$poybY>UTvR->{l0NrQ*|Sgdc5#Q-+jCy{j6WDW Nc; zb3szzwa4G*wyhK;f0ef=RZ@c%7qw-#X~e6 pl1|F0SItDJ;Hs5*qpgy<%f?qEzupdSI6q79m@@=4377Gdi|nInkx<|^Urv* zv$DeIJRP~UJpcD!w>SILA?dyB w31$J(^hZ K zll_BdR{@2i?As&a23?oe>^n&lx9^(qkC|kf@UPTWy9agItwFuL=2w2Q4Fa;`2+o zp8rlBu&-)k^BK;#MN?#1V1{*vqu9O3o*r8gY7`ZD) z?lBu&>L%to1`O5Z{|6TxsaL#nF&N;BGVdJxjkG(=buduE?#Yh@D%fAaV%2ht3?ng| zz%Wt-uVjo27KwtFxM(}9R4O6@7=iE$hA$8%kq{D)3SP U>!ih*FMoJY* mg%l@Au{4Slhecr#KfYp!JT^{m<%V_x$B8gRAc$J4M*am6fSTO^ literal 0 HcmV?d00001 diff --git a/test/html/html_page_break_after.pdf b/test/html/html_page_break_after.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5799ea35dc8abaa9605f0261e966374d278c3bb0 GIT binary patch literal 1810 zcmb_d&rcIU6vhM+okTDE2 kr z=x#m-@i>r@Zj~~Kf%M3+eOSQ!R}N^+XG}adfb^Kyj _d@iE(nsem{oR5p)exdAWYJwj8Nu7wdIqwJpLIV Q#kp|BUS~}-s9Zw@5&L@)*=lLJUM!SS7r4Z zmb2G0`JJ1`vx7Ho-#M9Bk31b8|Ml!`WUn6CHX4on`p^Byk4Cgk#zqe!Ba 9gd;`GC2qRhVALEh3=| nxXU-EuW-$okMBWsStlI=T AEp0zV#?(g=#sIbP>d)^m$_!eBw-GlT~xHJ(DI!h2`2 z{Z6*unT;@Rc+xcU?h|f!yz~?I+&tmEErwQ#cR!4z?jut-k%AF2Wxm#tIU~z6(Gc63 zN49_rkqX3y)`GgGVGVA k^VODOiY`ODR2;j$y1O3?-ISm4vBlDU1@jf#rmW7OvjWtVo|(9&4Tu!Z968O)agY F!9Q1Y1a<%b literal 0 HcmV?d00001 diff --git a/test/html/html_page_break_before.pdf b/test/html/html_page_break_before.pdf new file mode 100644 index 0000000000000000000000000000000000000000..be48a0adfb4ce70e6e19e90345a62bb371330d13 GIT binary patch literal 1819 zcmb_dPe>F|7*9yafh;mZ?eY#utE4k;X2+e8&0J?(+A? EQimW5$*7ARyY=3>vyHo?7vEuq{muJ+-}n8#Kid#d zgUzr-U?AXtIdGow`x#a-GX@3%!yeZYHs}|~rzj)lo<90K%djU M5BFbV5 zF9FF5TO^r$?Y%gR02|Q;k _5r|{mH1hm_4eIzPFfyqWA)uz+ z+C?*i0UI`uVx}@_V-2 TEPlq9jY%^oU z5uv166ga7O7a_HE2!nbGBZ~x^(lAm{+)N;b?MB8R9s-g~jcjZoEzM-_2+`GV{GuTC zc~&f@XYqp2%XvQtTx3TmHoWs|)Aq{x{Xgr@cuv*U{yIJB8J+SBNh>R3Q{Trt%PWmj zXkX2^XYYaO2R|E!nnn{>yyaHewvO_eDynAQM_yN287IC%!mXPmyiUZTgsH7VxKY9o zde>!K^Y(Go2)DapX(9ak=H$ea>-CATA4^G#?U=f~XKrL+^rLU)%j(H&TX?+w;e6$d zT@`)HhnKI;KSeL9tIzfLy5H4(h<{AZ-n%yA`FeNu_2-wv&nHtIZ(<#Prls0L? pu2LdG+Y>+>x;nu&q=I+MZ721u3LWq@aEIzuHq Pp?0o$h=0mIfEfMH{q0h}9EI+K&vPU zB1nRN7!j2oU=%ROksd6 #$=Lr5#|+3F;O6S6y5W|Qoj<3qWGTNT7p4NEU_hf zfehk$A4D9or;0Fvcg2D&O-~^!_YV})FCnTG8#PS~@&g(&k|uz(3S?SCD(LrjDLgN7 uQb2{OA_Ri6s(QV=B=Z5q$4gvLY2W-tt|M)%SvXfAC`cmH(9jhQGJgRQMh_+c literal 0 HcmV?d00001 diff --git a/test/html/html_table_with_multiline_cells_and_split_over_page.pdf b/test/html/html_table_with_multiline_cells_and_split_over_page.pdf index 317979244821c98f5fe52c4dee50614ca5dba1e1..f78c8b3328a4d2c76ad64c21936c5e4e16abcad6 100644 GIT binary patch delta 1089 zcmaDaabIFXKWn|2DVLocS8+*EYGN)|#hljA)05^{2<-VDer03r#8wue7;~HI8?z7Z zm0!SmHbQz2+l3wW_m_%a*jDx3$=>PIsZ(dPB>t2xTr%my<4?zV?fyqPC~_< TUO)Z!wiRW+S+>_d zo*Cb5o5UZ$=_vK@|JI7bo}TBo=Kb2|xc>O|x4-MJ?B3J7J9fe%Hci!)f1b_?_*46v z@%p`-1)^(qHTSRF{&|@h?^_d|VDm<)l2@$z>=Z%|U369Pk_xL-$$itc{Xoy!mp NkA Ri^-Rjf2i;nt$(uJzZGmy1tXzjgCV z5w7PMl^-H?-EyC&@mri+%pSgEf%%UMi#`ASOrA0+s3|4X>bS;T{yCkYZ$3D9`TOv` zsK2}8RoUf;dk6W`Oq^@_*7=JV-&e0=;g@*0rEgJd-M?>Z=T2|Fn&ZgnTFcxVleK ze}nXr8;QS?pIGgE#rn}$O;*>=c%|0sjG%*_&dpr)2iueH1f5NM_1EH&@_qZs7nNet z=2YtBbEQe7zMdp&;FoM-y~1GD)hB{0dw2^DMLZN_|7y01ee+z!gDZ}d)E?Tf# tomhb&isg(mipy^ zl(rnB_3lWs%=(qK#hX)Xqyhv4#Wl{k$*`#`dS8{vyUFN !y-FWrEYJp^KXCp>c+D*4^(}Hj=wCrc&Avp%HNu){%2Z@RaMP1A^lmk zJ{fmX;w*MYe?7YK%hJ0We!6{gNS&JCyIJ2jl0DV%Oh#Qz>x`qju1~)0{BPsyKRF>Y zeDaH8lu|{^8+$W %zr$bzhAd`^SU*DvtLDVOyjHB z`-i1><_uo0)VvgEj@vB3navbrXsKWT0t$HwTwsQQk%^%NhM1|L8HSjdp)rP-xuN0Y ztK7D+mPW>IrY7dD7N! %@me>W}wk6P*A delta 1095 zcmcaF@m^v>KWn{(A(x#US8+*EYGN)|#hj(nP8U5k6L9_huPZLgjDuqW!$#lU%_s7- zPO&Sfym9Q(pWNa;fBrG=DQX42H}74TloMO>eP?YVTfJStC66B;Km3-g`@OiyOTg>l zAC~^-?bH7xI=jrZOV)kJ>v6PWO(M&(CI4fq><`3$)PCc~Zog_ry-LsH`wy=Nu4=#h zI5oa>dwt{Q{V((D>|Xj+wYYIC`mw(MQP<>I_vA{=rpvM!+rGbl-)@}+!-p!-l~EId zCaIm++Vbf4XMPLbGWNxZ#
y;;giLFe=GWYdGgyeY@1t|epk*27k;(n zAJ2<720J$DgqtqmDvDUzQ)C{x*J5|$&I(!E8{*3k@;1xu-l?G$GFPHNDMaR0*0ifN z=E3=4GV#yFZfDEP(bw+Kui9F*Klj^&$u@=-iJWQe;l?+=3x187GAXJl#aAqON?rX8 z@kNR3zM9V6RW9N_vtO1q_x^Blx4vm;t8Q<2Bxs(*zYY`0hg*Cf_fP(5&GRzMvZzPp zO_11{RYx`}`E)4EaDS7!_)FTvx3-eKTg61BU4=tJa;}``ll)@;xyzOH^3vts6DvX+ zZgEVtwAq=yhWifR%>z5!Pwv@!^Gw$-`J=UG>K_L0n7zw)hqj+c@8=$uevT7Y`#%dP zG08vmG@6-^%vLP6g(>ciHp_+d4F+d}H~G$ao?;#yw#2CPBxiDHfXvs=OWx-^zUcO3 z=cTv3T(84at~Fh0V&8kW=ky$x3H}e>Iy0UxINWruELC&jci!YZoo9djJ(~LQV2L=3 zqxG(Z4`%GDUnkrgZ1r)&tKGGMtLGe>|H<`ig;S*9#C7a bHJ zq)GnoSHC`O(b(TKB{S;Q!tK*ax9vSy{p@+)?x{u=RZCVJkDlgWbg65{mg4J~mmeNk zymj5FcGit~;bv^&p^G{d7bMj#R1!YZRre{}CF_1`v__olzFR^e^>LTaDCX`l|5?hM z^xks&hI{>=?AShP-rEy&{pwQ9<;+awt^clmm!9>3QNXRpw6buS&&uqO(`H78Se>Pp zuD-VJT>N9Z)WeUx-mLwaYV@DK !^a+EOz8Jj8?fPg}t0vDKJU}R!!f+1#V zY=j|ZW@L#WW^QCY`6#z-th2GRnSq&wv!RipiL<4Vg_)6wrJIYRlbNHNxud0{nVk(m c6|s;qBC)8Xq9`?u%gEfql1o+9)!&T^08n4{mjD0& diff --git a/test/html/test_html.py b/test/html/test_html.py index cdbc13af1..752052554 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -53,18 +53,18 @@ def test_html_features(tmp_path): pdf.write_html(" h4
") pdf.write_html("h5
") pdf.write_html("h6
") - pdf.write_html("Rendering <hr>:
") + pdf.write_html("Rendering two <hr> tags:
") + pdf.write_html('
') pdf.write_html("
") # Now inserting
tags until a page jump is triggered: - for _ in range(25): + for _ in range(24): pdf.write_html("
") pdf.write_html("i am preformatted text.") pdf.write_html("hello blockquote") pdf.write_html("") pdf.write_html("
- li1
- another
- l item
") pdf.write_html("
- li1
- another
- l item
") - pdf.write_html('
- description title
- description details
') - pdf.write_html("") + pdf.write_html("
") pdf.write_html( "" " " @@ -91,7 +91,7 @@ def test_html_features(tmp_path): " " "
" ) - pdf.write_html('') + pdf.write_html("
") pdf.write_html( '' " " @@ -173,6 +173,10 @@ def getrow(i): pdf.add_page() img_path = HERE.parent / "image/png_images/c636287a4d7cb1a36362f7f236564cef.png" pdf.write_html(f"") + # With an (incorrect) trailing slash: + pdf.write_html(f"") + # With an (incorrect) end tag: + pdf.write_html(f"") assert_pdf_equal(pdf, HERE / "html_features.pdf", tmp_path) @@ -481,8 +485,7 @@ def test_html_img_not_overlapping(tmp_path): pdf.add_page() pdf.write_html( """ -
text
-""" +text
""" ) assert_pdf_equal( pdf, @@ -491,6 +494,19 @@ def test_html_img_not_overlapping(tmp_path): ) +def test_html_img_without_height_at_page_bottom_triggers_page_break(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.y = 200 + img_path = HERE.parent / "image/png_images/c636287a4d7cb1a36362f7f236564cef.png" + pdf.write_html(f'') + assert_pdf_equal( + pdf, + HERE / "html_img_without_height_at_page_bottom_triggers_page_break.pdf", + tmp_path, + ) + + def test_warn_on_tags_not_matching(caplog): pdf = FPDF() pdf.add_page() @@ -826,3 +842,32 @@ def test_html_list_vertical_margin(tmp_path): """ pdf.write_html(html, list_vertical_margin=margin_value) assert_pdf_equal(pdf, HERE / "html_list_vertical_margin.pdf", tmp_path) + + +def test_html_page_break_before(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.write_html( + """Content on first page. +
+ Content on second page, with some slight top margin. ++ Content on third page. +
""" + ) + assert_pdf_equal(pdf, HERE / "html_page_break_before.pdf", tmp_path) + + +def test_html_page_break_after(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.write_html( + """Content on first page. +
+ Content on second page. ++ Other content on second page. +
+ Content on third page.""" + ) + assert_pdf_equal(pdf, HERE / "html_page_break_after.pdf", tmp_path) diff --git a/test/text/header_footer_and_local_context_font_size.pdf b/test/local_context_font_size_and_header_footer.pdf similarity index 81% rename from test/text/header_footer_and_local_context_font_size.pdf rename to test/local_context_font_size_and_header_footer.pdf index b4651ce63583e7ae92ab51146c25d1937da6e12a..1ba512fac347bc8c5f952780686d97246f84b229 100644 GIT binary patch delta 225 zcmeC;`NzFsJrk3G<>ZY_K01d?q-G~x%*e}IlFoPW#pIdOS0$uyTo&&LNXc1}p2-sS zc-s8J{C%uCheIzAr0B4=ImX~Tbo2AXFK{{|8Vk*cc_6;i_~I) z6QUg(&fV}he~BY!c641G=cZhSG$r+en>CsLGTIqiDj0x(LY@K_m| 5irW@un! zj3H)dXfQd2)i&15z}3aX)yd4w+|<(4+{oC-!r0Qq$jHLg#KhUc#KpwUhM L;xw(JB!NW}*+7 z?^LW{J6a~x-+2DGYOe;9*a_)so9`LtrtCOhv~}mP=(5X?KCN>ITxH|_yXUr<^k%`8 zF`}z73ijSzwa6`d0+VmBl7G6@nT5|pqHK@t-SDO55z|}mriAaj^_xzHeidKxk25fR zhQ3GWJcZTl_7hq86{95X|K@CDy)|jg#Y*uN!jmi m>%e`*>%+U {+&aTXxSxW9B*hMy||N%U32}JakX-+um(+=kM+FmHF}Fr== ethrp+v~9KqCs z>y+#tSZ#l*blgw&{-NiOFHCD{VEuF|`TWH>Lj95JUa7`y-YsU%&)OCA)bf^YSnjs- z-#iLKS1dZ16uG*pZ=180iEpRP+zr;#W(8+{Q<;6@qR=VrE&7~IU(G%0cLto^ukd-k zKxmA?{0YCl@yxu--G4!dQ$|I=D!-P&nnjAqcJgbDf;mmi+l^f2*2$e%)^TiN!i}hx z{klw?+cqW3K0FfD>)2j0aY2F9$E! !-}7g;XXqTo%~xkHRGh@p^G0cp;Vhq7 zNhuHfj%Q9OmuPz47qjP>#=FC(J7iT&z8(5-TUYwpi;N`(d%xZDTXw6E|J|g*maEe8 zOZH2w-CVvWYVxYhik!@B^@e5&1|Xo2r@#ef7#Lbwnxl&u8Jl2=nV4YcH8M3e!O&}F zWP~ARZfF1&t4Fff$lL-`v!x-1n6ZJWF}i`qhGrNJG&Zs@#878q1hE)uuMEh~##mf! zY-VCH*-pSV*3{I|&Dq4n)X~Mzz}N!FG<0+`Hg$D!akMlub+$CLvmvM=7Lpnhi%Ker SQq#DMfu845Rdw}u;{pIx)_0Wv delta 1009 zcmcbhctdf6C0o5Ymz^C~aY<2XVlG$3oT(E|`yDnAaQ*(XYd23&>L;xw(JB!NW}*+7 z?^LW{J6a~y-+2DGYOe;9*a_+1RnIcch3q)bwRPXI=(5WXKdp0#T)D{Md(Uk>Yn{Z! ztEx^;*t9Dowfps1x2e2V%`F+Xxm>L-*B#rU?( g5aB?OfJw2$?t2Jbde|Kgaj{ zI=W}2xW_*gh2v&34}SXiY1TaRTk8rsWFFK^(3HAh?Q7dD_KmMlV$0jVZ%TC@9dJK? zer2W4QH8nJ(k{FG*|RT2Icww1G {1d~{Cw{;#%aA1C6@N{e`9(s6z|(*Bzm;o=Tp2-S5D&^v;3}O4sQ%Ir5{Y0v?NLN z=;C)NGrTRkFIwN7$a{UpjXKQ=O|!*E8ck#FXD?N?nsws*?SnIKhD6yqlwY(saANLf zo0y($C#_cqPw(0+$;r%CZ)Big00Ihm3S3}@fsuiMA%>W-sVRn-iKz*On5l^ohM1W# zrXA*n7GQPt$QGMhVrsTD!VohyFvD=5v7tGJ1C5O=EYTffY+?kl7;3K!$j`=DTy1P- zYBt$Sz&6&=%+1Z*+|Ag@#o5u)+}y><$iU3W+| Dyb++ RP2(~KdY(&F)z#mP3jiKAcVPek diff --git a/test/outline/html_toc_2_pages.pdf b/test/outline/html_toc_2_pages.pdf index 61d5e607d783bf8952c90fdd589dddcc2480b4aa..30e02e65927b01fa1396a131a55c42cd6daca78f 100644 GIT binary patch delta 4229 zcmai0dpK128#nGIqH&4DN>MD+oH=LCIg?v%ql>Vi8=*wG%wUvC+M!UXp>;y&VpJ+b zcF?#~lq`uTOl6X+OSMU5L%RKrYIn=&d4A`g?|DAw{l4$}c|Y&_K1Xzlly-`AF`kmD zzECKn2;{IyA|EAUs45@#%RQhb`aFD7Vc5+KwK<^~%&K?>4g2#6Uut)wjiB4xUH=TXr z*$Jth%yn9Ow|ZAnci*%Rx|{Ru50C>mn OicKbnOz^bkAAH{dcmg9 zbDOsa{5JR%ZVt*-OU=y@ TEYFizHAjK zt*am%TVTK^ftbG0Y#zV6TkC=2Q{#4 !f5D3N@@^T6PV` zgv43fz0u36DH;zGsI>9S^{o@zqIz8JN^HT>>x$Tj+xAYI+6}xX^LM@Mygj#_Z&C5I zXJYQX&V;5$SRhrHeU`JOrt5`9(t2JvKV+c$`g6gBF@e<@UJyHD!D`zhNw$>}_rz|O zURa7{dz(TyRK?qa^%@9wC1+3eMrAJMbw0HrhLhg2$}suX@d`~b(7@)Hrr&Sq9Z=!~ z^)^T{y@Iv&O8R)|)!u<~4|<&z0m-*6SDa6_<#4B&i+8Lr7A0tOz06WG>k5&6I;;4+ zEQ0jI)%9g9qqAmm`Qoif*^TTS{Cz(yFA^UWgj5GoB+hL(po9%E67lahWs!8Qu8;k7 z;pIe%c!!GnG+l0`O39q8&=Q@pVBh8Dq072ELaKV@W*Sh!_F`)clX*0NY;8@RRC@!P zyyIAN`c=+en%chYCwl8W^1a`M`U_VD@5^8A;S(FHThZ$(&D#B;QVBZfc}t@%Hvab2 zD%VB3r5~FM98AYuPDJjA4yZZoMt(cLy07u=*xb$WL9ItH=fys?MJCB?k|X=Jzikic zb@6iZ_Z_A;_E)!0Y~fv)agI0fC&S0}RaMHXp27nDFd9(vvD0 xC>@&N%>MA$&y+_xq_;1xpeA#mZ(&z?Y1)PQ z&e|tA)almxhpJfFI)kv{?E%~L+Ms3a weGcfL;GcB&Ivzu zX`s6Y%2rq_o3U&~*9--!OwY;|ugJ~eCUuq#bkUfsFX@DQjUyb^e7rW{W^TW6{5m5< zQ`)Iar9#R0RDV6AC^dU}?1;vtIS4hVRD!w}^rg$y0PbCfb%S-xks~pD$>GErCdZz$ zA6mz8{)@r2*OHpE`TT=K$ l1fgNi+mbLcg@!QB5-75n6SsSyXO7>qwJg-3&@ zsMNHqj;4_~wmV7aarh{Pd0pB=t>fRDdG0Ri5S^KsH$EYMJm+8yRABP3?5*l2g?+^{ zUV2C5DaUxKsz_|+ VZAAXT0}=|X!aHPmxW?|EKCR8`M4?w;}y z1)JgbA_eig@rbDP%CRF&md<*z$n1^xQY;?5<><(mktRm1k`-qD>Qoh(#&axnaZHS| zDQ_M9C@NLH)A*@po{na0(`=MdfRI`M%?ib$ZOtAe(M?`#NM`xK&7Lf0Q?Aajrje*Z z8L>`yz}-%79dDiAlDuVCdz`%FuP17Z^M8;PvNsaLp$wL0V{>55nbfYs*+({(N#?ya ztmx1Q|NRZz$Eq((*Invqi1aeU1?6*)q^lWaT=n#f20l%O3O!1c#FrNI8@r`7@Ezo5 zhv51e3(j vDren#Pj+z!(lHVdk2M OHGJ<^OKQ$t^+n WPpV!h&5(T5)GR#q5}35Yug$c$ckcSN!2&6TloAw%SD)9zO8*=0Ly*;n=^ z#jSKj*tD<@t+qau*k9AcvujsTLr2S%6>2I4#a)!@3$BkZhLBfRa<^D2>*v2$>#dy# zDLo#};fI3^x~j{j{8h0{ilTOUzn4@7`7A4tS8H@9Vo`crfi{|*hYwVpGym3}X=m!6 zu8}XA+hgbKjt^8EieJ^YtX& EzG;@Oo4al Mei?iQwM!0DQ zE^z{GaC6+W;)W0B$V+xZbL*#v<*vU< Ep zspSb#zf>x{QAJB;dAwf|n)&fpNB=f62X^tsQXzERDx5j+GX0l5 Z3 zOgt>Xw_I1Oh3eNW*J6Kv%_qe_iu9hhdD)D*t$Oi1@l=SbU&u_Vf>#3RB4>71k#tyg zq;}L$n`hB1FV$1~DlOyR_ >Y(#>NE`v~R3XI)DpiDAy99%T<3o z_PYni`!2rYGNlHY4{-(CaRqgXQgw}MGHcll%KV^LS0$tORW~kqYA4kAIZag#hNvKf z6)oC{NfxSOi!9jKIvNG5z)vxLqLysT1E64+EZK4pZPL=jDjIX4Q?N2CHr9lnkTn}y z0#Y!cbtqN`s$+B;Hf9e|uo4?zEP1htJW2zo3 DJjQ34oCjLw@1ZWU4)geG*Fs3>L02(kg832ENfPEi>hjRu1Iy#jd0DuTF2n2u0 z`oE+BfWcr)O&Wk9Lec;NPOT~cpfnya2ooeEL#HtaOy~eY2uufYAwR45ubcoKq7&^H z|M=s5V+jAiA^HP{Ork^Z2MK~dNDu^Rz;_Aa^#?$RPN)LNU=Yd$!U&;1K$J#c2QtC$ zgWxeB8nI^}00nv9Wix3A(g;By2qXl77%(vij1q`IC_o?r{U tgt3#9$O6FoBr}q0 lTdzyPA7~a0y6$3!uz`Q00cq^Oc0pZTnGXZ zm>?*Fc$bM1 M_&&V+!W#4U<4Czl@f(=Qf2 zj*fN!%(Qi6A&xARWe+h~fE^36V<2|64u~xawzmAg7VE$K@@(L4-N6kABm+!*A&`xX J99>!D{{hte{{{d6 delta 4314 zcmai0c|25Y8#c0MsY#-Iwj#uI&YW}RoP)?R3N6wq)QglFrfDppWh7>-$vd=2y-6u5 zQduS~q)?XByd+IEm82EDA= EX GbJ3sUADgyc-r@9jWv;C9kKnsO&)!Y4zO+D=<`#xNw$I(_{m^bl z@{yp0e|@ls$wHcHPIfP8gL{q~Lx-HcE3K7t7N`ZW*M)_1gD%W6NT7@Z0fz9rp_N;_ zYIe7tp7Tw4QEKmNikjZE*LthXSLy1$@y<4nKW-juZf>ry-z;n0zRu&82eQq(GnBL& zk;zWyJsTP2@QqDn7DMaD$qlBVRH2bk8V^;ow^x&a_=l=R !v8I zi}yO^@KjW!$rHG=hb#4^PInyFu{uuvwN5}hs2;q1Po+2>uajK4V^}R{rd$yW^JckL z 9c3v zucdHy9vLo4kPh$AAE;?7zj|087ZzJIl-MV|Zqaf0w6FFO%gL_}J!+XN9;%T{n7?)n zH_R~#N4qLACRM7jlMCvXvrkxMypZs$YR J~%CGR?*UoH UCAJzl=&vW+vm zKa{o5+|k@`l)4qWlMR!s#ZQXooBSSo>fQ3xDN0GAtIGV{6YZ()$K#%KB7*%}mDE*F z?B&(?9MP}IHL9=C`vf(sh>u##JQ#cVlA#2?dDZ&psdrnfRO}qTJy-VMW?QH|{R8~^ zS`i}OzxbMi@~a6Sk@q 3kJ2LT4N`6A9rDOJ zWMVg-d8~V@6gJ&N!O~+-+L&(FkQPvL>t{q+wg-84?Ml(b_-CVGtjQ_EjmIhYGsw1U zSA$ALra#L-otB`&%&)#O@soTnEKfUs4$)GYVmY2^R-NFP(wQG1J=JE6R(FPowxkiS z)1`JdBkI#UY<$=`1x#*(iS$50M7>67jy0OOJxQ{+whAwDW+#T1Ne;Ambn>m_o4rak zMWDrohBV`~CTQwc5vX |Esn&n) z5dZO8FNIgwmZ03;|415HozSPS{GHa$_93=bQD5In>FX%D9791_v2n$~1g+2Pm#nqv zezD-(rJ;d#6`VbhEh|13#TE9|m6&ig4x}tnyWYOtCL_;zM)p?+zw08Mr&f~>Dy*!f z`pVHDsk>gleYv9J?u6 71XbF >iVQ0q1PGZ;W1yDU*2KGUYmFQ|1{HX1Un=u;;&` zii{g#!<>ol2F3eD+~?-|>ouY^b8ZptEhXH_Ckp)EN7v5q6G(KkA8-SgS!Yb6cCBH% z7S)7K{l4O1{wJj@`4|;pp; uBe+_&V_W`OzcgK7wFW4IDHwn$Y-PW zmu_}LV^2E&q6%f=!$TMQic)EVbGOV;y*8^kbJw-jJ5vn|6jg3vg5k+KTU&9hB)3Lr zKBT>170?VidY11sIg?c--ilwXYiBh+`s)kT`7O`x=q16hbx_sP{tW*l{Hw$B_jjWD z=ERHP!InLbqIOxh1jiWis{HlMc!~DSQ-V7 kk}%ckpC5 zrY>mVM;e$kPQ27*dUJPr&s~GzE(~r7e`<%S7d6@6wFq!Jb5d+- Wu;EXuT1fF_HAoDqO7Ds4=@m2b8L {&Tu2X-d_t&a)yA+jua=YWMUfCMeaE^M1e>4PRd>@jPwKo$f6+@7Rud zjU`3 2q``NeF%1t$ZwvzPG;w`n>$%U0CN?SBf zc$`{WRnmG?>Ji}avHvtR#dAXG6}a|L>dt0m!CLLd^i&tkwGYB5i;ERhn7Yn=s *Fqigf)Ui^t10oH-Nzbs Tetp zmTc=$7RSv=znXAq^DxEr&C!swZSjRYu1oRfU*v*9??j#n*{UFJ?+t?^D&%U1OWv_K z`EvvfWv4?zSx@j@&Fl{oo>f<{F77I`i!{wlyYh)z4L_KDSem%^g-zBykk9?_{ZhS1 z!^us5oenL_HAK;uH&g0mdAwY1zf-Ma*VD}DCHgdu+Mefa5 TY!{ EwK(E!rnO=iYO>+`%dIX(09)Lf&O*wDEfBCQS{G_(Mre& ze2mj5{16e1z(+YpD?uapgU+M)kuE>*`-u=7ArQt45x#X8CGdesz>YB3;`)O@FA*|E z1lT-pl*0RYKPZIGk5+<5BzQZ2lmp8xT786r5j!Fz#7}s4w$245cX &>4wIT;5F+IefJ6pF@Ud!vAOj<91VkA)X*~?)kwI{jM1~Gv zq}|c!APJZbLF77^L9Rna*W>;09i*ebSP=Wgf;f3W=obW`Ul4>KXvFtX+X?+a2xgE} zfDnu%7leX8c_*_D!eG+wAe`|t2vG+EI0*x$gE()jColw)Kwuapfxrk(20`g$5DX@5 z1pe1z-srId5fC7O5W_$A=zs`>kO(3$MjA|n0g!+ZbgZ2`03>FC#2UeI(gZU=khEh4 z9VQJcgYaprstjVwq#V&~Bs+iqbNPS_6d^ZbC`knrAm0HfagN7=pmcfX>d2Oa=}x>5i}~17Wz(>CTRbD-1f>+x_1PzmZ=h Xe{OgbH)y>Yhyxg+W@^e@?5g%(V_Xbv diff --git a/test/outline/html_toc_with_custom_rendering.pdf b/test/outline/html_toc_with_custom_rendering.pdf index 1d6dcf7fe45651f81ff6d554a509a91fd2ace244..ecbc5de94ecca2cdce13f36af8952164371699f3 100644 GIT binary patch delta 495 zcmZ1@yj*xg15>@38JC?MS8+*EYGN)|#hlXfd$|r7h_pSl4gc6%YWeKRa^EmV_Cn1k zY+Tp6EVf8~`Nk_WWm-#);``zvk#kOy-hL2kZ}{7Juxp9Sj)xLQl*E=~pFP4i*L(@@ zAyLf&KgVUl%k!7`^vyc`Lu&p5*+a^lFT$HXKG9~{y>-DUrtC+vOg8*%*5g_``HE *cnK`*@V*0nHI^JBh;pU#d%=_ -}TGdC!jHzkAH(M|Zu+|%! zD;R))LY@K_m| 5irW@un!j3H)dXn-kZVS*uMY=B{hp@}g>uQ!s#hNh;NmYA7i zI>y}662lS;OVi0hoVKw}#;!)@#?BT-&gLeD7KTQ~Mh2!XmgbhO#!ePyrp5+#HUw3~ aLgF&9sHCDOHI2*A($t(wRn^tsjSBz);FuZ! delta 491 zcmZ23yheCK15>@ZA(x#US8+*EYGN)|#hlXfd$|r7h_F7e^)EcOE2jAKWwq4{ns ld5n)M+g_?Q`DWTU{VFDW`e61 (1!dA>( zqNcoL(#h#MlXZKizTRl2uMsU`HvM}2gYfP5mhWD@arW-ScgL!KsYQKcO#YvBd$TdK z0IQvek%9pTDC8+{ff)t{mL`ViVul7r78qiNhNhDnIfUyiF=UJlF-$cyF*e0eXKIRR zs+k3*?G^?m7 wTn~}4DiJ_~JsezlTlYya&tA(Ydg^{_F gft?LO6|s ^a}05`Xir2qf` diff --git a/test/outline/html_toc_with_h1_as_2nd_heading.pdf b/test/outline/html_toc_with_h1_as_2nd_heading.pdf index 732358e33e9a176a9cc16690f92cab65f3785baa..6ee0b65d446b548684330e311e9b2aad45d0fb23 100644 GIT binary patch delta 439 zcmZ1^Hdk!Jb(VSqE;~D};*z4 Xe5Ij!e5avgHuIrh=k|6^~NL0r+>r+GqLRy#yL zaV&ZeCLX_^y({8_#!V*sefu9dHL@S>c02x1N8qWz1M@j^(q;wT4~#HcBQi~{&Xv_t z+iCCfzrCN>-}Y6%e9*tjaBWFYpYGlNFQ$n delta 444 zcmbO$wn%Klb(VTlE;~D};*z4 Xe5Ij!e5axppbuw1CypX8<6Gdm@Jb2Mj@SQ-0I zmcR{{kKHeK^tUOnyu`cbB!{2krG$#My~))n8v=ShaP!H%3!dRoa_WLn>LaGrQ!Ngj z&k$Y8k+l2x*Vte?)75v|u78N$ERk--o$0?^{_f|T5bL;o|Ms61S!SZ6_rCVrtdnBX zKiB+}i2L!umw$6AYdTB4iGhiN0SGAMDR6-q28N~v#+YK31{h*y=H?h;=B9=iVix9@ zdM(Ys_F5oYY-C`8q1ng?r~=J_Mn;w 6m#V6(zZ(|-F)V{S diff --git a/test/test_graphics_context.py b/test/test_graphics_context.py index 0bf072af3..147e6c478 100644 --- a/test/test_graphics_context.py +++ b/test/test_graphics_context.py @@ -322,7 +322,7 @@ def test_local_context_init(tmp_path): pdf.add_page() pdf.set_font("Helvetica", "", 12) with pdf.local_context( - font_family="Courier", font_style="B", font_size=24, text_color=(255, 128, 0) + font_family="Courier", font_style="B", font_size_pt=24, text_color=(255, 128, 0) ): pdf.cell(text="Local context") pdf.ln() @@ -372,3 +372,24 @@ def test_invalid_local_context_init(): with pytest.raises(ValueError): with pdf.local_context(stroke_width=2): pass + + +def test_local_context_font_size_and_header_footer(tmp_path): # issue 1204 + class PDF(FPDF): + def header(self): + self.cell(text=f"Header {self.page_no()}") + self.ln() + + def footer(self): + self.set_y(-15) + self.cell(text=f"Footer {self.page_no()}") + + pdf = PDF() + pdf.set_font(family="helvetica", size=12) + pdf.add_page() + with pdf.local_context(font_size_pt=36): # LABEL C + pdf.multi_cell(w=0, text="\n".join(f"Line {i + 1}" for i in range(21))) + assert pdf.font_size_pt == 12 + assert_pdf_equal( + pdf, HERE / "local_context_font_size_and_header_footer.pdf", tmp_path + ) diff --git a/test/text/test_cell.py b/test/text/test_cell.py index d7401572d..d2b130016 100644 --- a/test/text/test_cell.py +++ b/test/text/test_cell.py @@ -325,27 +325,6 @@ def test_cell_deprecated_txt_arg(): pdf.cell(txt="Lorem ipsum Ut nostrud irure") -def test_header_footer_and_local_context_font_size(tmp_path): # issue 1204 - class PDF(FPDF): - def header(self): - self.cell(text=f"Header {self.page_no()}") - self.ln() - - def footer(self): - self.set_y(-15) - self.cell(text=f"Footer {self.page_no()}") - - pdf = PDF() - pdf.set_font(family="helvetica", size=12) - pdf.add_page() - with pdf.local_context(font_size=36): # LABEL C - pdf.multi_cell(w=0, text="\n".join(f"Line {i + 1}" for i in range(21))) - assert pdf.font_size_pt == 12 - assert_pdf_equal( - pdf, HERE / "header_footer_and_local_context_font_size.pdf", tmp_path - ) - - @ensure_exec_time_below(seconds=24) @ensure_rss_memory_below(mib=1) def test_cell_speed_with_long_text(): # issue #907