From 6f7a207d9c0b7e3f537b6a5f9f25e6ab0b693f8e Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Fri, 5 Jul 2024 23:24:45 +0200 Subject: [PATCH] write_html(): homogenization in handling of tag_styles + new optional arg font_family (#1217) --- CHANGELOG.md | 1 + docs/HTML.md | 25 +- fpdf/fpdf.py | 16 +- fpdf/html.py | 449 ++++++++++-------- test/html/html_blockquote_color.pdf | Bin 1174 -> 1169 bytes .../html_blockquote_color_using_FontFace.pdf | Bin 1170 -> 1167 bytes test/html/html_custom_heading_sizes.pdf | Bin 2027 -> 2025 bytes test/html/html_customize_ul.pdf | Bin 1299 -> 1303 bytes test/html/html_customize_ul_deprecated.pdf | Bin 0 -> 1299 bytes ....pdf => html_dd_tag_indent_deprecated.pdf} | Bin 1141 -> 1153 bytes test/html/html_description.pdf | Bin 1166 -> 1164 bytes test/html/html_font_family.pdf | Bin 0 -> 1282 bytes test/html/html_heading_above_below.pdf | Bin 1590 -> 1595 bytes test/html/html_headings_line_height.pdf | Bin 2825 -> 2824 bytes test/html/html_link_style.pdf | Bin 0 -> 1313 bytes test/html/html_measurement_units.pdf | Bin 1307 -> 1302 bytes test/html/html_sections.pdf | Bin 0 -> 1724 bytes test/html/html_table_with_bgcolor.pdf | Bin 1657 -> 1657 bytes test/html/html_whitespace_handling.pdf | Bin 1864 -> 1864 bytes test/html/test_html.py | 114 ++++- test/outline/html_toc.pdf | Bin 4304 -> 4306 bytes .../html_toc_with_custom_rendering.pdf | Bin 2471 -> 2470 bytes test/outline/test_outline_html.py | 8 +- 23 files changed, 367 insertions(+), 246 deletions(-) create mode 100644 test/html/html_customize_ul_deprecated.pdf rename test/html/{html_link_color.pdf => html_dd_tag_indent_deprecated.pdf} (58%) create mode 100644 test/html/html_font_family.pdf create mode 100644 test/html/html_link_style.pdf create mode 100644 test/html/html_sections.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e5fad12a..f1d133a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', * 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` * [`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): new optional `font_family` parameter to set the default font family * [`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 `tag_styles` attribute - thanks to @lcgeneralprojects * file names are mentioned in errors when `fpdf2` fails to parse a SVG image ### Fixed diff --git a/docs/HTML.md b/docs/HTML.md index 156dd0bdf..f6d7ee12a 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -78,7 +78,6 @@ pdf.write_html(""" pdf.output("html.pdf") ``` - ### Styling HTML tags globally _New in [:octicons-tag-24: 2.7.9](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ @@ -132,10 +131,32 @@ pdf.output("html_dd_indented.pdf") and that some [`FontFace`](https://py-pdf.github.io/fpdf2/fpdf/fonts.html#fpdf.fonts.FontFace) or [`TextStyle`](https://py-pdf.github.io/fpdf2/fpdf/fonts.html#fpdf.fonts.TextStyle) properties may not be honored. However, **Pull Request are welcome** to implement missing features! +### Default font + +_New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ + +The default font used by [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) is **Times**. + +You can change this default font by passing `font_family` to this method: +```python +from fpdf import FPDF + +pdf = FPDF() +pdf.add_page() +pdf.write_html(""" +

Big title

+
+

Section title

+

Hello world!

+
+""", font_family="Helvetica") +pdf.output("html_helvetica.pdf") +``` + ## Supported HTML features -* `

` to ``: headings (and `align` attribute) +* `

` to `

`: headings (and `align` attribute) * `

`: paragraphs (and `align`, `line-height` attributes) * `
` & `


` tags * ``, ``, ``: bold, italic, underline diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 3753d9e60..5674292c9 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -293,11 +293,12 @@ def __init__( # Graphics state variables defined as properties by GraphicsStateMixin. # We set their default values here. self.font_family = "" # current font family - self.font_style = "" # current font style + # current font style (BOLD/ITALICS - does not handle UNDERLINE): + self.font_style = "" + self.underline = False # underlining flag self.font_size_pt = 12 # current font size in points self.font_stretching = 100 # current font stretching self.char_spacing = 0 # current character spacing - self.underline = False # underlining flag self.current_font = None # None or an instance of CoreFont or TTFFont self.draw_color = self.DEFAULT_DRAW_COLOR self.fill_color = self.DEFAULT_FILL_COLOR @@ -410,11 +411,18 @@ def _set_min_pdf_version(self, version): self.pdf_version = max(self.pdf_version, version) @property - def is_ttf_font(self): + def emphasis(self) -> TextEmphasis: + "The current text emphasis: bold, italics and/or underlined." + return TextEmphasis.coerce( + f"{self.font_style}U" if self.underline else self.font_style + ) + + @property + def is_ttf_font(self) -> bool: return self.current_font and self.current_font.type == "TTF" @property - def page_mode(self): + def page_mode(self) -> PageMode: return self._page_mode @page_mode.setter diff --git a/fpdf/html.py b/fpdf/html.py index d44c09e28..1df85cb4f 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -22,22 +22,63 @@ BULLET_WIN1252 = "\x95" # BULLET character in Windows-1252 encoding DEGREE_WIN1252 = "\xb0" HEADING_TAGS = ("h1", "h2", "h3", "h4", "h5", "h6") +# Some of the margin values below are fractions, in order to be fully backward-compatible, +# and due to the _scale_units() conversion performed in HTML2FPDF constructor below. +# Those constants are formatted as Mixed Fractions, a mathematical representation +# making clear what the closest integer value is. DEFAULT_TAG_STYLES = { - "a": TextStyle(color="#00f"), + # Inline tags are FontFace instances : + "a": FontFace(color="#00f", emphasis="UNDERLINE"), + "b": FontFace(emphasis="BOLD"), + "code": FontFace(family="Courier"), + "em": FontFace(emphasis="ITALICS"), + "font": FontFace(), + "i": FontFace(emphasis="ITALICS"), + "strong": FontFace(emphasis="BOLD"), + "u": FontFace(emphasis="UNDERLINE"), + # Block tags are TextStyle instances : "blockquote": TextStyle(color="#64002d", t_margin=3, b_margin=3), - "code": TextStyle(font_family="Courier"), + "center": TextStyle(t_margin=4 + 7 / 30), "dd": TextStyle(l_margin=10), - "h1": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=24), - "h2": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=18), - "h3": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=14), - "h4": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=12), - "h5": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=10), - "h6": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=8), + "dt": TextStyle(font_style="B", t_margin=4 + 7 / 30), + "h1": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=24, t_margin=5 + 834 / 900 + ), + "h2": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=18, t_margin=5 + 453 / 900 + ), + "h3": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=14, t_margin=5 + 199 / 900 + ), + "h4": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=12, t_margin=5 + 72 / 900 + ), + "h5": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=10, t_margin=5 - 55 / 900 + ), + "h6": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=8, t_margin=5 - 182 / 900 + ), "li": TextStyle(l_margin=5, t_margin=2), - "pre": TextStyle(font_family="Courier"), + "p": TextStyle(), + "pre": TextStyle(t_margin=4 + 7 / 30, font_family="Courier"), "ol": TextStyle(t_margin=2), "ul": TextStyle(t_margin=2), } +INLINE_TAGS = ("a", "b", "code", "em", "font", "i", "strong", "u") +BLOCK_TAGS = HEADING_TAGS + ( + "blockquote", + "center", + "dd", + "dt", + "li", + "p", + "pre", + "ol", + "ul", +) +# This defensive programming check ensures that we do not forget any tag in the 2 *_TAGS constants above: +assert (set(BLOCK_TAGS) ^ set(INLINE_TAGS)) == set(DEFAULT_TAG_STYLES.keys()) # Pattern to substitute whitespace sequences with a single space character each. # The following are all Unicode characters with White_Space classification plus the newline. @@ -274,6 +315,7 @@ def __init__( warn_on_tags_not_matching=True, tag_indents=None, tag_styles=None, + font_family="times", ): """ Args: @@ -296,6 +338,7 @@ def __init__( tag_indents (dict): [**DEPRECATED since v2.7.10**] mapping of HTML tag names to numeric values representing their horizontal left identation. - Set `tag_styles` instead tag_styles (dict[str, fpdf.fonts.TextStyle]): mapping of HTML tag names to `fpdf.TextStyle` or `fpdf.FontFace` instances + font_family (str): optional font family. Default to Times. """ super().__init__() self.pdf = pdf @@ -316,9 +359,13 @@ def __init__( # If a font was defined previously, we reinstate that seperately after we're finished here. # In this case the TOC will be rendered with that font and not ours. But adding a TOC tag only # makes sense if the whole document gets converted from HTML, so this should be acceptable. - self.emphasis = TextEmphasis.NONE - self.font_size = pdf.font_size_pt - self.set_font(pdf.font_family or "times", size=self.font_size, set_default=True) + self.font_family = pdf.font_family or font_family + self.font_size_pt = pdf.font_size_pt + self.set_font( + family=self.font_family, emphasis=TextEmphasis.NONE, set_default=True + ) + self.style_stack = [] # list of FontFace + self.h = pdf.font_size_pt / pdf.k self._page_break_after_paragraph = False self._pre_formatted = False # preserve whitespace while True. # nothing written yet to
, remove one initial nl:
@@ -327,7 +374,6 @@ def __init__(
         self.follows_heading = False  # We don't want extra space below a heading.
         self.href = ""
         self.align = ""
-        self.style_stack = []  # list of FontFace
         self.indent = 0
         self.line_height_stack = []
         self.ol_type = []  # when inside a 
    tag, can be "a", "A", "i", "I" or "1" @@ -351,7 +397,12 @@ def __init__( raise NotImplementedError( f"Cannot set style for HTML tag <{tag}> (contributions are welcome to add support for this)" ) - if isinstance(tag_style, FontFace) and not isinstance(tag_style, TextStyle): + if not isinstance(tag_style, FontFace): + raise ValueError( + f"tag_styles values must be instances of FontFace or TextStyle - received: {tag_style}" + ) + # We convert FontFace values provided for block tags into TextStyle values: + if tag in BLOCK_TAGS and not isinstance(tag_style, TextStyle): # pylint: disable=redefined-loop-name tag_style = TextStyle( font_family=tag_style.family, @@ -390,7 +441,7 @@ def __init__( stacklevel=get_stack_level(), ) self.tag_styles["code"] = self.tag_styles["code"].replace( - font_family=pre_code_font + family=pre_code_font ) self.tag_styles["pre"] = self.tag_styles["pre"].replace( font_family=pre_code_font @@ -405,7 +456,7 @@ def __init__( DeprecationWarning, stacklevel=get_stack_level(), ) - self.tag_styles["dd"] = self.tag_styles["pre"].replace( + self.tag_styles["dd"] = self.tag_styles["dd"].replace( l_margin=dd_tag_indent ) if li_tag_indent is not None: @@ -447,10 +498,12 @@ def _new_paragraph( indent=0, bullet="", ): + # Note that currently top_margin is ignored if bullet is also provided, + # due to the behaviour of TextRegion._render_column_lines() self._end_paragraph() self.align = align or "" if not top_margin and not self.follows_heading: - top_margin = self.font_size / self.pdf.k + top_margin = self.font_size_pt / self.pdf.k self._paragraph = self._column.paragraph( text_align=align, line_height=line_height, @@ -476,6 +529,8 @@ def _end_paragraph(self): self._page_break_after_paragraph = False def _write_paragraph(self, text, link=None): + if not text: + return if not self._paragraph: self._new_paragraph() self._paragraph.write(text, link=link) @@ -565,43 +620,19 @@ def _write_data(self, data): " You can open up an issue on github.com/py-pdf/fpdf2 if this is something you would like to see implemented." ) self.pdf.start_section(data, self.heading_level - 1, strict=False) - LOGGER.debug(f"write: '%s' h={self.h:.2f}", data) self._write_paragraph(data) def handle_starttag(self, tag, attrs): self._pre_started = False attrs = dict(attrs) - LOGGER.debug("STARTTAG %s %s", tag, attrs) css_style = parse_css_style(attrs.get("style", "")) self._tags_stack.append(tag) if css_style.get("break-before") == "page": self._end_paragraph() # pylint: disable=protected-access self.pdf._perform_page_break() - if tag == "dt": - self._new_paragraph( - line_height=( - self.line_height_stack[-1] if self.line_height_stack else None - ), - ) - tag = "b" - if tag == "dd": - self.follows_heading = True - self._new_paragraph( - line_height=( - self.line_height_stack[-1] if self.line_height_stack else None - ), - indent=self.tag_styles["dd"].l_margin * (self.indent + 1), - ) - if tag == "strong": - tag = "b" - if tag == "em": - tag = "i" - if tag in ("b", "i", "u"): - if self.td_th is not None: - self.td_th[tag] = True - else: - self.set_style(tag, True) + if tag in ("b", "i", "u") and self.td_th is not None: + self.td_th[tag] = True if tag == "a": self.href = attrs["href"] try: @@ -611,6 +642,25 @@ def handle_starttag(self, tag, attrs): pass if tag == "br": self._write_paragraph("\n") + if tag == "hr": + self._end_paragraph() + width = css_style.get("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=x_start, + y1=self.pdf.y, + x2=x_start + width, + y2=self.pdf.y, + ) + self._write_paragraph("\n") if tag == "p": align = None if "align" in attrs: @@ -627,20 +677,26 @@ def handle_starttag(self, tag, attrs): line_height = float(line_height) except ValueError: line_height = None - self._new_paragraph(align=align, line_height=line_height) + tag_style = self.tag_styles[tag] + self._new_paragraph( + align=align, + line_height=line_height, + top_margin=tag_style.t_margin, + bottom_margin=tag_style.b_margin, + indent=tag_style.l_margin, + ) if tag in HEADING_TAGS: - prev_font_height = self.font_size / self.pdf.k self.style_stack.append( FontFace( family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, color=self.font_color, ) ) self.heading_level = int(tag[1:]) tag_style = self.tag_styles[tag] - hsize = (tag_style.size_pt or self.font_size) / self.pdf.k + hsize = (tag_style.size_pt or self.font_size_pt) / self.pdf.k if attrs: align = attrs.get("align") if not align in ["L", "R", "J", "C"]: @@ -649,8 +705,9 @@ def handle_starttag(self, tag, attrs): align = None self._new_paragraph( align=align, - top_margin=prev_font_height + tag_style.t_margin * hsize, + top_margin=tag_style.t_margin, bottom_margin=tag_style.b_margin * hsize, + indent=tag_style.l_margin, ) color = None if "color" in css_style: @@ -665,50 +722,29 @@ def handle_starttag(self, tag, attrs): self.set_text_color(*color) self.set_font( family=tag_style.family or self.font_family, - size=tag_style.size_pt or self.font_size, - ) - if tag == "hr": - self._end_paragraph() - width = css_style.get("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=x_start, - y1=self.pdf.y, - x2=x_start + width, - y2=self.pdf.y, - ) - self._write_paragraph("\n") - if tag == "code": - self.style_stack.append( - FontFace( - family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, - color=self.font_color, - ) - ) - tag_style = self.tag_styles[tag] - if tag_style.color: - self.set_text_color(*tag_style.color.colors255) - self.set_font( - family=tag_style.family or self.font_family, - size=tag_style.size_pt or self.font_size, + size=tag_style.size_pt or self.font_size_pt, + extra_emphasis=tag_style.emphasis, ) - if tag == "pre": - self._end_paragraph() + if tag in ( + "b", + "blockquote", + "center", + "code", + "em", + "i", + "dd", + "dt", + "pre", + "strong", + "u", + ): + if tag in BLOCK_TAGS: + self._end_paragraph() self.style_stack.append( FontFace( family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, color=self.font_color, ) ) @@ -717,37 +753,26 @@ def handle_starttag(self, tag, attrs): self.set_text_color(*tag_style.color.colors255) self.set_font( family=tag_style.family or self.font_family, - size=tag_style.size_pt or self.font_size, + size=tag_style.size_pt or self.font_size_pt, + extra_emphasis=tag_style.emphasis, ) - self._pre_formatted = True - self._pre_started = True - self._new_paragraph() - if tag == "blockquote": - self.style_stack.append( - FontFace( - family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, - color=self.font_color, + if tag == "pre": + self._pre_formatted = True + self._pre_started = True + if tag in BLOCK_TAGS: + if tag == "dd": + # Not compliant with the HTML spec, but backward-compatible + # cf. https://github.com/py-pdf/fpdf2/pull/1217#discussion_r1666643777 + self.follows_heading = True + self._new_paragraph( + align="C" if tag == "center" else None, + line_height=( + self.line_height_stack[-1] if self.line_height_stack else None + ), + top_margin=tag_style.t_margin, + bottom_margin=tag_style.b_margin, + indent=tag_style.l_margin, ) - ) - tag_style = self.tag_styles[tag] - if tag_style.color: - self.set_text_color(*tag_style.color.colors255) - if tag_style.emphasis: - self.emphasis = tag_style.emphasis - self.set_font( - family=tag_style.family or self.font_family, - size=tag_style.size_pt or self.font_size, - ) - self.indent += 1 - self._new_paragraph( - # Default values to be multiplied by the conversion factor - # for top_margin and bottom_margin here are given in mm - top_margin=self.tag_styles["blockquote"].t_margin, - bottom_margin=self.tag_styles["blockquote"].b_margin, - indent=self.tag_styles["blockquote"].l_margin * self.indent, - ) if tag == "ul": self.indent += 1 bullet_char = ( @@ -767,8 +792,12 @@ def handle_starttag(self, tag, attrs): else: self.line_height_stack.append(None) if self.indent == 1: + tag_style = self.tag_styles[tag] self._new_paragraph( - top_margin=self.tag_styles["ul"].t_margin, line_height=0 + line_height=0, + top_margin=tag_style.t_margin, + bottom_margin=tag_style.b_margin, + indent=tag_style.l_margin, ) self._write_paragraph("\u00a0") self._end_paragraph() @@ -790,15 +819,16 @@ def handle_starttag(self, tag, attrs): else: self.line_height_stack.append(None) if self.indent == 1: + tag_style = self.tag_styles[tag] self._new_paragraph( - top_margin=self.tag_styles["ol"].t_margin, line_height=0 + line_height=0, + top_margin=tag_style.t_margin, + bottom_margin=tag_style.b_margin, + indent=tag_style.l_margin, ) self._write_paragraph("\u00a0") self._end_paragraph() if tag == "li": - # Default value of 2 for h to be multiplied by the conversion factor - # in self._ln(h) here is given in mm - self._ln(self.tag_styles["li"].t_margin) self.set_text_color(*self.li_prefix_color) if self.bullet: bullet = self.bullet[self.indent - 1] @@ -810,11 +840,14 @@ def handle_starttag(self, tag, attrs): self.bullet[self.indent - 1] = bullet ol_type = self.ol_type[self.indent - 1] bullet = f"{ol_prefix(ol_type, bullet)}." + tag_style = self.tag_styles[tag] + self._ln(tag_style.t_margin) self._new_paragraph( line_height=( self.line_height_stack[-1] if self.line_height_stack else None ), - indent=self.tag_styles["li"].l_margin * self.indent, + indent=tag_style.l_margin * self.indent, + bottom_margin=tag_style.b_margin, bullet=bullet, ) self.set_text_color(*self.font_color) @@ -823,8 +856,8 @@ def handle_starttag(self, tag, attrs): self.style_stack.append( FontFace( family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, color=self.font_color, ) ) @@ -837,9 +870,9 @@ def handle_starttag(self, tag, attrs): self.set_font(face) self.font_family = face if "font-size" in css_style: - self.font_size = int(css_style.get("font-size")) + self.font_size_pt = int(css_style.get("font-size")) elif "size" in attrs: - self.font_size = int(attrs.get("size")) + self.font_size_pt = int(attrs.get("size")) self.set_font() self.set_text_color(*self.font_color) if tag == "table": @@ -932,19 +965,9 @@ def handle_starttag(self, tag, attrs): x = self.pdf.get_x() if self.align and self.align[0].upper() == "C": x = Align.C - LOGGER.debug( - 'image "%s" x=%d y=%d width=%d height=%d', - attrs["src"], - x, - self.pdf.get_y(), - width, - height, - ) self.pdf.image( self.image_map(attrs["src"]), x=x, w=width, h=height, link=self.href ) - if tag == "center": - self._new_paragraph(align="C") if tag == "toc": self._end_paragraph() self.pdf.insert_toc_placeholder( @@ -963,7 +986,6 @@ def handle_starttag(self, tag, attrs): self._page_break_after_paragraph = True def handle_endtag(self, tag): - LOGGER.debug("ENDTAG %s", tag) while ( self._tags_stack and tag != self._tags_stack[-1] @@ -983,46 +1005,43 @@ def handle_endtag(self, tag): tag, self._tags_stack[-1], ) + if tag == "a": + self.href = "" + if tag == "p": + self._end_paragraph() + self.align = "" if tag in HEADING_TAGS: self.heading_level = None font_face = self.style_stack.pop() - self.emphasis = font_face.emphasis - self.set_font(font_face.family, font_face.size_pt) + self.set_font( + font_face.family, font_face.size_pt, emphasis=font_face.emphasis + ) self.set_text_color(*font_face.color.colors255) self._end_paragraph() self.follows_heading = True # We don't want extra space below a heading. - if tag == "code": - font_face = self.style_stack.pop() - self.emphasis = font_face.emphasis - self.set_font(font_face.family, font_face.size_pt) - self.set_text_color(*font_face.color.colors255) - if tag == "pre": - font_face = self.style_stack.pop() - self.emphasis = font_face.emphasis - self.set_font(font_face.family, font_face.size_pt) - self.set_text_color(*font_face.color.colors255) - self._pre_formatted = False - self._pre_started = False - self._end_paragraph() - if tag == "blockquote": + if tag in ( + "b", + "blockquote", + "center", + "code", + "em", + "i", + "dd", + "dt", + "pre", + "strong", + "u", + ): font_face = self.style_stack.pop() - self.emphasis = font_face.emphasis - self.set_font(font_face.family, font_face.size_pt) + self.set_font( + font_face.family, font_face.size_pt, emphasis=font_face.emphasis + ) self.set_text_color(*font_face.color.colors255) - self._end_paragraph() - self.indent -= 1 - if tag in ("strong", "dt"): - tag = "b" - if tag == "em": - tag = "i" - if tag in ("b", "i", "u"): - if not self.td_th is not None: - self.set_style(tag, False) - if tag == "a": - self.href = "" - if tag == "p": - self._end_paragraph() - self.align = "" + if tag == "pre": + self._pre_formatted = False + self._pre_started = False + if tag in BLOCK_TAGS: + self._end_paragraph() if tag in ("ul", "ol"): self._end_paragraph() self.indent -= 1 @@ -1049,12 +1068,11 @@ def handle_endtag(self, tag): if tag == "font": # recover last font state font_face = self.style_stack.pop() - self.emphasis = font_face.emphasis self.font_color = font_face.color.colors255 - self.set_font(font_face.family, font_face.size_pt) + self.set_font( + font_face.family, font_face.size_pt, emphasis=font_face.emphasis + ) self.set_text_color(*font_face.color.colors255) - if tag == "center": - self._end_paragraph() if tag == "sup": self.pdf.char_vpos = "LINE" if tag == "sub": @@ -1068,36 +1086,33 @@ def feed(self, data): if self._tags_stack and self.warn_on_tags_not_matching: LOGGER.warning("Missing HTML end tag for <%s>", self._tags_stack[-1]) - def set_font(self, family=None, size=None, set_default=False): + def set_font( + self, + family=None, + size=None, + emphasis=None, + extra_emphasis=None, + set_default=False, + ): + pdf = self.pdf + if emphasis is None: + emphasis = pdf.emphasis + if extra_emphasis: + emphasis |= extra_emphasis if family: self.font_family = family if size: - self.font_size = size - self.h = size / self.pdf.k - style = self.emphasis.style - LOGGER.debug(f"set_font: %s style=%s h={self.h:.2f}", self.font_family, style) - prev_page = self.pdf.page + self.font_size_pt = size + self.h = size / pdf.k + prev_page = pdf.page if not set_default: # make sure there's at least one font defined in the PDF. - self.pdf.page = 0 - if (self.font_family, style) != (self.pdf.font_family, self.pdf.font_style): - self.pdf.set_font(self.font_family, style, self.font_size) - if self.font_size != self.pdf.font_size: - self.pdf.set_font_size(self.font_size) - self.pdf.page = prev_page - - def set_style(self, tag, enable): - "Modify style and select corresponding font" - emphasis = TextEmphasis.coerce(tag.upper()) - if enable: - self.emphasis = self.emphasis.add(emphasis) - else: - self.emphasis = self.emphasis.remove(emphasis) - style = self.emphasis.style - LOGGER.debug("SET_FONT_STYLE %s", style) - prev_page = self.pdf.page - self.pdf.page = 0 - self.pdf.set_font(style=style) - self.pdf.page = prev_page + pdf.page = 0 + if (self.font_family, emphasis) != (pdf.font_family, pdf.emphasis): + pdf.set_font(self.font_family, emphasis.style, self.font_size_pt) + assert pdf.emphasis == emphasis + if self.font_size_pt != pdf.font_size: + pdf.set_font_size(self.font_size_pt) + pdf.page = prev_page def set_text_color(self, r=None, g=0, b=0): prev_page = self.pdf.page @@ -1106,18 +1121,27 @@ def set_text_color(self, r=None, g=0, b=0): self.pdf.page = prev_page def put_link(self, text): - # Put a hyperlink + "Put a hyperlink" + prev_style = FontFace( + family=self.font_family, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, + color=self.font_color, + ) tag_style = self.tag_styles["a"] if tag_style.color: self.set_text_color(*tag_style.color.colors255) self.set_font( family=tag_style.family or self.font_family, - size=tag_style.size_pt or self.font_size, + size=tag_style.size_pt or self.font_size_pt, + extra_emphasis=tag_style.emphasis, ) - self.set_style("u", True) self._write_paragraph(text, link=self.href) - self.set_style("u", False) - self.set_text_color(*self.font_color) + # Restore previous style: + self.set_font( + prev_style.family, prev_style.size_pt, emphasis=prev_style.emphasis + ) + self.set_text_color(*prev_style.color.colors255) # pylint: disable=no-self-use def render_toc(self, pdf, outline): @@ -1145,11 +1169,14 @@ def _scale_units(pdf, in_tag_styles): conversion_factor = get_scale_factor("mm") / pdf.k out_tag_styles = {} for tag_name, tag_style in in_tag_styles.items(): - out_tag_styles[tag_name] = tag_style.replace( - t_margin=tag_style.t_margin * conversion_factor, - l_margin=tag_style.l_margin * conversion_factor, - b_margin=tag_style.b_margin * conversion_factor, - ) + if isinstance(tag_style, TextStyle): + out_tag_styles[tag_name] = tag_style.replace( + t_margin=tag_style.t_margin * conversion_factor, + l_margin=tag_style.l_margin * conversion_factor, + b_margin=tag_style.b_margin * conversion_factor, + ) + else: + out_tag_styles[tag_name] = tag_style return out_tag_styles diff --git a/test/html/html_blockquote_color.pdf b/test/html/html_blockquote_color.pdf index ed19d6962e7431cf24a914cb0c37a58727751ca5..0f888791e99e209e85dd2fc1d2ecc9edffc551d2 100644 GIT binary patch delta 316 zcmbQnIgxXNBV)ZGmz^C~aY<2XVlG$3oY1qUxegf!v_7nz_eke;+MGKcn?y?qY5HEiiepD?GD3cT@`x+P$|l3-W8(ZuqZs)i-DcV_W`i(z)w@Mk$u$3&mcAnvvFW0>P0!tS9>^$o%Eg!!& zT%YHO_MOdF8Q(J68YvinfI^-E7nosSU}|BAA!dd}%-q~!@_%OQ7*}Iw12-c>7b90= zM^jTbQ)5GOOIJ4+Gb3XYM{_e%GdmlCDq0J z;?P@|xzKLk!D*{5x1KIP7r28{?e&I^;v+A$6h#e{(_$7$9PRjaZPJqVbl-yteE&B) z`>dDzKE>PD{Yd)b3v1`_rkKvTo?<66nRCsz=3g0FUl*TRS|eJ0Lp1WgHr=Y-ws|Lr_I5BzzK!N-By{)3_{5Ot@54UH#p- E0Q)0w+5i9m diff --git a/test/html/html_blockquote_color_using_FontFace.pdf b/test/html/html_blockquote_color_using_FontFace.pdf index b0dbc49becaa42f8ee256db685ebd08bd7e043df..509300f218eaf892f01079a5392943c5d843885f 100644 GIT binary patch delta 315 zcmbQl+0VJbfwA7mlFQDHtGJ{nH8Gc~VovDU(_DuP1X>@~&U>WuI&Dsj*QK=y;_tXV zGfh=%nw`LN<@j~6+(J`>KXcAMSTrN5KXNY1@i=Gcq&Af~uC~*bUtPk?6#Ysj#Yy>N zXkoAB^ooW%*9D?qG^=iJ^I4a?S$4{8b_44ROXlonmMA;9{f~Mt|H@-V{i#*|wm9x( z@=o10Dk&1@ax}FtQYH4h2 w;bZ_5Hg$G2Gqo@`bv1M{GcdQaA*do25-^EHB^5=fXz!UXSk`Dm9}3vb8;lhtb#zPOqZ2&6NLqi6h2<~_y3y( zA59<2U7qk*@RyFvE!%MRxtt7n+8*u=|Jb|h$g=$}wKN`;DVQsC z)Ze~%@b>nO2a-XvR(`P4<+QN!-}&h6wyv}D4mR#-kJ}JzzFYFImC;6rJpsNKjH?uc z9{jG1+aLJTRD18;_!q^88n0h)4>!=A8YOM&969ZvUhoo|iup%>s5qF+yd?Fpf%9a- zq_azm?znwno9wIRz0e|Q&Dv@9#yj~rR;QlY{Eg9qiP=cce6k@+4Vd1*Qp;#ES&y}z z(PHvC*0*AY777L+ppd7)1!fo+m>HT+-pD3wYl0zTZf=SpW?^ZGE@o(8WICCVUD(zL zL&nI$07J~g0K*N2rUsUi1KDk3P2F4!4NcrkOw3$eo!rb!j9uLfT`ZhUon72qoh?iZ f>}&|Ch=s&cVo^y&QED2Op{cncm#V6(zZ(|-aHx_S delta 489 zcmaFK|C)b8AY;8Tmz^C~aY<2XVlG$3oYD*XxtI)j*gov_|Jb|h$`aq(ceFGfm0b`Q zI8uLmZu9zl(Sy=KQW`A#;+>fK`f9W)cIF;UJOAK8XMp~VQ1hLfYh`b4cJizhykZ>X zVA^nN_WJt)|5C%gmenu1%E=zL*lt!+;YyL*`3rR>AN2G3;+$@$wtAz3$xIuO;zoYq zBMC>+T6SIUNHI8>5;V~+Io9<5%!_*&zY3=A*!+djf{EE!&vLRpOAU}VoV<>umeFXk z4r@J-T*Lad-pD|~00b2B6u7_)0|PTdOAIk{12ar9b8`$aO9Ml6F+&3*40VQvrY2x@ z=13MB8d(@&Xf`p#aG;^7q2XkIcH3A(GfPVsLq|(XOA`|(Q&ST&LklBE3o}ClQ!^tM iCr39s8-glgA<>vvR8motn#N^lYHrM>s_N?R#svTu<&zZv diff --git a/test/html/html_customize_ul.pdf b/test/html/html_customize_ul.pdf index d85f8d5251d1799b34e5f6ffa73594071da64d81..e7c9985099f034f92e550d0943caf6d00360d157 100644 GIT binary patch delta 453 zcmbQtHJxjNBV)Zemz^C~aY<2XVlG$3oV62uvkn^ww0^fcs&n_Oi@ous6{npxztCHt ztfHxBvQY7Y^cSJMPT%*&=_-p@3toA!VD3HV`;R9#PIptR{*g0JY0WjMHBL(v-aWK5 zlYek88%j+qL13tQPc@|}5Vb2wrd^c9bD*=}UJI{&ivmiV1K^|QZ-8Ai!X4bLuS z-1hwL%UJ%uCntOp+_t;6{KdJu9~Z1H*``saw|7eZ%V+YtG{2mU=;Pj}*UmOYIC%PJ z+nLAridt*wPn-Pm$*ZDOUxN0{mf_RdTch3nJA%3T+{deX=AX35zqmTEa^oDI?Tkmy zE}j1A?vn`-DZvvTYYLw9>|6irucY6zPXGC3njk~2E%*>qrKvypGrLo_VBqEurq_(N z777L+ppd7)1!fo+m>Zg7h*=n#Vu)E98cnWYv5qlvbv7_`GjlXHFmSUpwlp(vHFR=y oc6D?%b}}|Fb~d-OA*do25=V(eB^5=fX~Qe*gdg delta 449 zcmbQvHJNLJBV)ZWmz^C~aY<2XVlG$3oTU>Svkn^wxPG@gs&n^jiptrFCwk0h7wvB7 zo3LcZg_bsl{0rVsxbA+h+S-$mbA6eHoAuxGd(LnE5F^la$bBQ7C~R{UKr^VB0{?Zt56&->Uvi|%ndy!}&>W#>7|l6lLYpZLkx880ZfIfCgmqpgX8 z0SGAMDR6-q1_tJa#u#E2hK3komIfA+8(6GkObyMb2EnS_>%*@;joDD4vjm*uB m4K2(pj7*(f?Q96Dh=s&YVo^y&QED2OrI{s{s;aBM8y5gA2DH=w diff --git a/test/html/html_customize_ul_deprecated.pdf b/test/html/html_customize_ul_deprecated.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d85f8d5251d1799b34e5f6ffa73594071da64d81 GIT binary patch literal 1299 zcmbtUT}TvB6efaIlf#Co1o54w}SN?zXgR=#e-v@PN0ZGa^%}wz*PYA zT@u}`-P3{qU#&DDlPfCXkRE1qHVbW43(`wMM8$fN6Y#Q1%77l#G)lliE0jGrN&sJ_ zBQMsX5q$8dl3?dodm^ALB-+T5lX`)?W1H%7s6N;zQRy@5t=DUwSp8e`IM( zn=dot^ODY~@9)yH7j-Y)zxu=3wzP$P_3069+2!o#g>k6VZy}flu<}-361+&jtJ9+bVm?GwR>G z+dYugbLnT_it)1Wm8V-5Pu@+NjLQ#hPBcgzPbAWP@$IAOc^x~mve?-t6-m0lVUg3L zm`KL-K=@iUg3P=ijwtk){W=v^)tdmnRn<#$Q%wOJUlVO033D-vQCH45jd55`O#Dj< z57xr}pMm-1wDtC`P*tM5x6)$)`8@ z4Afg%a9I*~siD}U_^9VVu5TlhL6iFHCci*qm+q~kjWlLXps3L}Q1YRE`j zJ~e79qHXX&j0s@Z2NgGdo7)jq5eo_H#G;al SqSQ1l3nODLRaIAiH!c9eJaU-; delta 496 zcmZqV{K_%ma=l|-UVcfjLbQ#IzM}$=2v*P!4f2E#8s+8XdZ`tOxdl0?ddc~@ns#y!Wx=IM zC6xuK3i>{odD&e0Az-#+9vjehJGn6VGjKk-hovMB#OZ!{tGo?QhIfT)Mt6J=-d-!j+nr0<{gu z%Ab6Zv1syACV|QOnXD(zWa633#%#f8G1;Cu!rsKtM8N`*SzEG=9ujEoH&-CRsvjhro= r%uUS=O-#((+{_Iv-3;sqtB8e!Ut&>7MNw)RmxZw*m#V6(zZ(|-QF@qR diff --git a/test/html/html_description.pdf b/test/html/html_description.pdf index 516d99de750856323b55dadeb57e57caeb3847d2..9a32ba41f01d8fe5e31a4fb18e33fc86c266a0a2 100644 GIT binary patch delta 261 zcmeCV7Di?+7LIPN20#fr8-glgx$Nw?ic1oUN-By{)3_{* MO}JE5UH#p-0D7`dcK`qY delta 249 zcmeC-?Bm?vz{q55FxiPwCy|9yO8S*#ltzSv#Ouge8ZAp0L?snPGM6k7Qx{Jao09VE zx#pCVX-}49X|<#*lv=WoL5z78yUE!rLFcbT9K3T$AWN!?QFEng=S<6^CtfLTn>~AG z@a3}L;G`V>Crk`yW_v!|e3tPQqn3rCf&mC9}YQ0>S$(ZZfI&`?B-@?LqNskLKa~TGcHwCSARDy E00lZr>Hq)$ diff --git a/test/html/html_font_family.pdf b/test/html/html_font_family.pdf new file mode 100644 index 0000000000000000000000000000000000000000..067781b560ac15dcfc60836cc5cc5f7f8bb2ed92 GIT binary patch literal 1282 zcmbtUU279T6fLNz1HNgk7Gy+AW1*d$>_>Ja6!Nt+SV>GG)rLN7veUSAJ5zUOHTB7d z{tEF$DB@44MX>e3ccoZRAN&J?Gx@Mf3Kn!;cK7Vud+t5w&P-K{&McnO0frjntu>I% z0#x8J6A%M*$M+)G)a9?*3P5+M=ev2{g-uN&CNbyIHna^qXA}9ivAIDZs=6H-f%$nr znWxgF6KN|Ol;zsO=WLJ@BF7gCki|hD6R6Ndcaz5gq6(u09>nXc4@XdyR~wQPT;T@1 zlb}&09#zi-Wg>z`Kc?!2@^hG>Mk5}FZ5ly4nNpD_@~O~aKF1FP4P~u?D`=6nxkmw7 zq^u(zL9As|R3t*`u7mC?z4r4<-K0}R84(AyXF1*PGjNy{TI8FH{M-7+3FSzFsvsvt z9f6`8CYUPT#>65xtz%3yxt1}R*0kwciQsNXSAixc4X6$M1ufImO}Gk=Fv+17WJeg0 z9gJX@9N(AD9LEe($}T!;(*_O>S!YX literal 0 HcmV?d00001 diff --git a/test/html/html_heading_above_below.pdf b/test/html/html_heading_above_below.pdf index 647a8702ddc023de5751314d1db8399d47e62e71..cdcd7c8cc15f1601db946953434a4f7b1ba2bfc8 100644 GIT binary patch delta 434 zcmdnSvzuo_0AszGDVLocS8+*EYGN)|#hl)=ySWZI2(VtL+n;pmM$hKW{W(zwMUsT? z$O{OYI^FBq|6WK*DPzh@z2f(YH#nSx567;t(Fx|9G?C+v;I+7tMa=@*otGTvWQ0i7 z@GX2G!`%AXKe_X3NLS6}LpyIgPpyq=*xw?0;z;)4V>5cBFK&Bj+|6~WY)9zj$I{1o z`s|V~*>2qOrv0^yd&{(c57rp2IIEu*E;Gw&{>L>zC!Woo-pQV|F7m?WKa4vVnaoTl zZ(}ZCv9!=Lnyk(8z24YJ!2kpl@)Wqh3ZbTLB%BQp#! zLt}{E$qQL6V%?mb%?(Wqoy-l)O&l#vj15fPjLn^#%#17zoGnc(UF~cLs)&U|M`BS) UMNw)Rm!Xk?A(yJEtG^o;0O@a#zyJUM delta 410 zcmdnZvyEp%0AszWC6}EYS8+*EYGN)|#hl)=d$|r7h_F7e^)EcO>xhc)trXEW3VKKP zG|Y%n{u2N3EALVzzP6(6hi#t=TL`#?_KDw&%;DzK(@5SB{zg~wM1#QcIYqs9JZ5=C zvYG{>yy;J2$kXO$2$6<5l=D+5BW4mU7%6;CYD`uKcN;VJE<*l)|o&EG9dve<+ zdx`7vYd$>w(oqnpcfD}t1??Tdr7xqzKTh#fj`O@%R1*1oGZWJeMkZ6^$$OX!Sj~;} zOid;muzc4tGE*=B0fjsTE-=Huz|6=LL(JUJXmTK{u&ptMjHQ7&x|pGXkpYI7p|Q#2 zg{-!*Cgv^{hQ^jIPHt|_CPq$frY5E)#;ykDCdQU-&gSOMb~Xf6#6p5Sv8be?C^e1C P(9qJ1OI6j?-;E0ZJ>7+@ diff --git a/test/html/html_headings_line_height.pdf b/test/html/html_headings_line_height.pdf index d99aa7ea4f1d1d6eb548a6b54af3f93b8be7396f..914e30ce6196b0e2d6730c9878ca5a476d5b00f2 100644 GIT binary patch delta 555 zcmeAa>k!)z$jE3iIf&7y-bq@6uT*30rj99R&dhwgajijGQ}m%GUYovlp`b#=2kQ%8 z-RwClV3&PzDYIx>HnY8>M}ePX%kr6elc)UjIprqwZ<&+j{=K`7FIXwwFL6y@;=KI4 zRh;MMuTJ~<|IMejqUvA2aQxGo5KyN)(L>?uL4|XH)5K+EUS4xsHT_~*!5p1>jmA6o zHI{#Pwd(dckM}8CBxT+o335{BbW407vT}0onf+cl_or4`NM7_SyWepwGjF$;_nPBr z-_vEd`?h;&uKjk!{7U3z+qR#HZ|iycvR@?aJ|=JG#m({k>hYPU^!OfqXVv}6wf8ya zAD8Z{bEhrUm)5>%{WA7lIag|4N^wb1YU1W*CUf?BLj?m6P{>o@0y7K@%uNk2#4L=> zF~lqlEYQUa4GoPk#EgwF%ri7Kg6cIxSZru+VT7*P$iUbPL(JH~Vsbv0ZLE`viIbb5 zrKyFRo29Xdxw(b0shgpbxuvUtk(-;DrJ0=#K^3uFc6MCFC5c5P6-B9OTt>#`23)GD IuKsRZ07MqPu>b%7 delta 555 zcmeAW>lE7%$jE3qIf&7yepT)Qiz}H6Pbmf27=M>L?;~=|r@%X6L*wz60Zv+aEPr&* z+0Nc5c_>|XXUT#?1-TE@IXKU0c6>0ooI1&9^^zQwhwrtfeEvP#oo80n$DYK$Z6E)< zTsrH)r>$Ajo{L|Xe{ZpRx%vlwN6x3_szRJzwJ8GE1E-10T3yQR2+?2k_Q8(&O$%6_ z{&TTx4G+J&&Ha}GNKDw9(@&_w>9A&yy7Wx@)*H4{D{u6yn0%Q1qSe{l>#3c-x8M91 zJl^y;O!H`#e~t58)7z$J`n~^W7sxn^?>771yI+Z0KZcU}$FQX6WMV=w{&JWMOD$Lr_I5mz^C~aYn F7Xa`Ez5)OM diff --git a/test/html/html_link_style.pdf b/test/html/html_link_style.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8d5f0b44aa9576fe6367c4a651f861547941f739 GIT binary patch literal 1313 zcmbVM%WD%s7{^=LL#bE2jAE-En%T)_lU*C5d2}&YN!%zxOOL z5kx4Se4rwNdZ-jc6s5EtJQUH3SJC1@^bgR2;5T{b(h7oe*xmV_zu))R{$kD=BoR#^ zNJV_&jFL<$ILjMMAfn*IZY4lt8mR1X1s|am*U9iE8dE`{n|dT_AR|U1h6&nZGj)n^ z(W%lvNu?CZDl%L&6t+;OEL|2ZXPuY`Tdp{SSi|!mfOH*kW_Uv&TwpZIy+)0-#Q~D= z>~4~*BOH%cLun+$F8VZMTmbB35~mS7N(djf^X(PM}JV;yd)4mYqXH6}!h`-sb?x)56+QYlbp zITMrtpjewi(edHhC1lfpH~caUP$JAw)*?htx_0SVt|tH{wme7B94+$-RqzO9RdE`T zs4l$%;ZvukG_UVpeXcAY?3?>|;pV6LQRB^{?Tb&g489srF?zNY=P&nsT^pKv{iXN7 zsim8<54X*2Tk-F|UD~DvA~^UFVi zZ?~?j-hX^+g|VMkFD14m8**;?bW%}~sYreMpt!DUI+{d( zWF~5<5c?xD0k%9L88J88#iFp-8|>m_vt0t#W}}@Eg(7Y6HN)|Q@3;y^S5%Y!&=exUW>1YKm4Y7l>fn! z7iVOXc*O%JED9~}FpY7EJQ~$$%EY@^D>1|GwaIL$-I2#9-U`25qTV{CPmR^{nn|pe zV(UUr&q=1Wx4jg-_0KGr)H)@u!$)bmTmQQ~^4H$~Ua#ot@lk#5^cap~USAd)M0b7e z&XIpySo@-ErL>)5>B|1_aLFrjTUnBSe0d*QBYfHWy8Qq3yR-f=+-cvyu{nn66{DS@ zxq<-*DC8+{ff)t{=7we%Vitxb7-E)&hLc-ZtYe&wEX*9uoh^)volFc}3>}RP+)Ruu o3@zPUj0{bjom}i}2&#yM#8F~VNkvg=8keQH5tpi}tG^o;0D11IwEzGB delta 459 zcmbQnHJfXL17p3pA(x#US8+*EYGN)|#hj^^y!{Rv2(Ue<^)vL|JhSiq$8B0u6T(l5 zJFInVbogb^)Ljt&%3{W{Ps>i}{d+F`ayzSyS<9_M;hVo~h$w!*)Oo=E@ZNmBm&>1= zh|V~w~10?&H|pE^)6h z{OGki3eyV2Ttm0KV%ylVS))5bYoptOg;A<4&-VCTzH;NLRo~WH)!6qclQ*rMyvS=> z$=)eGT*2z8Q*~ax4l|lrt=R097G$L;)OknBcGu~;8~bGK+9ed)-djXZdfq$r#j&49 zqyFr7-^R84?Z(^3^L{kkIBM4;w|TSqX}x?;>nlP%4=0;3mnqtn{(Y&xFSCwu&Be`X zo5Pu2G1?g!DHwo&LY@K_m|yDs(9GD-(8<)o$d#mvrzpo&;XL?srLR1~GAaao$1bE&Gj`nz!f0H9p8AOHXW diff --git a/test/html/html_sections.pdf b/test/html/html_sections.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a6e9ac7bf805137947b43bf82028dec96317b532 GIT binary patch literal 1724 zcma)7Z*1E{6o+XH&Dt~wlR#rq4^T)4CHC2lW0$B})6`p`TcWfY(5z4|xdvCR-PxCt zvax@FI&BkV{F6|N5C{sbm=KJCphBuvK2&3aNqnIi6%_;aVPZl^1qFe`os+aqf&}-) zzUTLTzkBcf-r1jssrSL4z(BwOYtH}^i7;%Nl`}Db4Evy-aX?BSpOU={y8~r(Eo$XK ziX)bg5(@G%kVP1j74o%vWC#H^p=FW7w6rkD%uqPq0~;SgW@{SjmRZIGtg7Sfz|0v2 z0f^_Yp_|A7Vamt2P@UR{mBWCIo2bn)a)V~c>;QUP;!`4R)39b(Sr08e@=7))MkaQE zSi!Z)wt|pEj+L|1gjVuG(mXtPG_-=J>IOzOIlG}@6hmn%gBZ3GnOXce;1z{NIoL+p zAd??|x$x!AS?;FxKZ?hu*Co+!17qgNzQf;kKeJ)z@*A6e+~&J9bLH~KlRc}O3gH6_ zf-`Y8z3b>tn;PbOM=y=~kpI}Kg?q0}8|T%xu70)=9bf9d>)e{xqRdBw&)#`TKggVM z8V^1&!GTz~ZXo@>4X=Hk@;r-tXY zj8FMae3x1~IdUSoIR2@`4t!92eq`x*ajN&L!e9PFO=Fogv!fUGH9v5A=C=9S=7x!P zx4teeZO-&%Z$9(N#1nz{X9K_Q|1~mvq3zFgFX+bIJHGh);-ja&URB)tVTZW8^ON~o zgNtuts+V%r%E}3ON*>i5q*|nD>~>@fBdn)2hHW=V>2)&;*oSnp)pYcVfnmFHd$21< zst=;$l1wOqr>C^v%RQywkqE0oKzWm-hit;O(p?CH6ia3Vu-zz6_&WxPFrwvWTlv}1 z>w%@}t-vl1lB(Md1~P>vTG3YCODC&H-vdK^t6_MCjUf_Qnn%Ds{74V+k)aR_iIo{D z@hBc1NN3fZCRg?4+3MD@fj}dzllum@c&KT3H8uCI)L~f;3d__xX?<$g$_dx81*`bn zp#Me4D?w$MFrm2_l{peN8V2RW>-l(Zez5AUBa=$6q!<}y&=_lmiljhZfKcJc>VZmt z;{x93yp8%94nUrv4y6U8P)G=Yez0OAN1+RtD>j8FuExegBDY$bQg4%_`q;2UZn>IR zIYioBV^esNZ?z3_BDv9OY%o;E0eOj(x8jFwO*fG3{vx{cClRdyo3tzp=q*n))?xNr zz^yLb#6T(%l{rCzvLMT?d{hx7SrkMy%<)`EZ5N|4rRBdp+}U!lW@ESVP!KrA?{AN* F%s-r$1z-RG literal 0 HcmV?d00001 diff --git a/test/html/html_table_with_bgcolor.pdf b/test/html/html_table_with_bgcolor.pdf index 9f97932078bae250b8a7cd53d4095aff669f110e..977b6ba901b399ca668b32d0528c4844772f7147 100644 GIT binary patch delta 429 zcmey#^OI-8CdPX2zFj{8r*)dR%NbJrvv zF2%#aD^Ff4{UWa_XWmm-mA5MKoQ;@~ql)lEEf=pPic*J^I09qe-9Av=;WDMTSnJ9` zrtrtVu7w@5JKuBiw7g{6`>T@!eoef;MsBIslTR`GVuW5~FnZrjs`pCCU+`9D*;DPX zUyb*7uufa{HfFPU^5Qc+{pm(~WNvz=%**o6Oj$TTX~XBaWg)x2TJ|kJ$LOeEEO{^@ zO`>|u=i8xa%d$(nljVEX=54r^n?1+Y=iDz{>C7#21(P3WJ^FsE>_OAs+h#T4!Ph5N z99|`RaK#Ds_stUnPp$uv@UQ!-)z^J*?@gQ$_h)gZPa*_3(WWDORN$viAF zlaDdGZ7yJOXJoXPyn{8x$;i^w)x^xv+|1a`!ot$U(%it&*wEF@&CT4@+0@n8&CZ69 Jipg)N$Fweut=Pq#NZb8of!!Y|7EBW=B=o_M;mW~a)9i%ipY9j%{o;-158OP|x> zYrZh=FXYzoy}dI{|CrlMN%`|B6_%T(pRl<)@6rh;`=bfZt>3OFdtEH!KbN^F?wL{3 z#xn-rBA#zwea7edi)qK~B%<#oti65BqI$-huhC|gGOU%49SA-0zW40`*6QuKKh`Z< zr~I)k)Ve8fBL6*h<;7FtKOX!ezS`<_?d`qFhI@aw3mb>+S58W(h{-y3FhzoGXQ}Uu z%<8KPRh?4Hu2coAkb1Of?#GOH&%O}5>C+iy>-;SzzhZJ{iSP1boNUTGak3VR+hjf# znaRhPJvJAxxHB@EP2Rzp;$-gRYUt`@Y-;LkX6R~ZVeIVe>}GCY?&|1hY+>r?=xS#} KNX6thHfaF9<-hF! diff --git a/test/html/html_whitespace_handling.pdf b/test/html/html_whitespace_handling.pdf index f676cd5b19306a621324eae5fec170bf6c73a433..2027872d90e0310bcb10a741f628034f1ad4d911 100644 GIT binary patch delta 416 zcmX@XcY<$25@Wr0UbBHj%lA1+(?YUjKdasL>b>$#sgO}PW`SZ$!t$MUxn0e-cj<3- z*p&9{yLnlMdjBMq!$)`PZ@9(6;pC**m2M+)@1(V3t~(=VQOfHXE{0uPrzSjCT=DBz zl~L=*oKNzt7R4%sZB5*j&5vAJ6JP5ZO3qCVn_V#TS$5^^>oauf<@p}*=cIf+(Z-fq ze&;P~%Hbt~ZqovI)|CI~y&_j`QNBsms&C!fodq*E&UC71vqr=_9(}*#08gr->d&4h z*^!n-+rrmH3ACGat^cu+xxReOj)Qx)Z2D<4x90Tn_osKQZTM%AE^&6o(>|T*UFYV0 zTUi-vEW=vIc``V?V_D`6?RuZ|VDJ1Dsls(9y$;{6c(>-=9m&q!lm9!qzDxVZvsH!l znP!aUxms(-FH0r1NNx}IQRct9>3Kz{rs>-sUI~}1&T0LfKj+xyulE?1T5?8jUc+S1 u;^boJW@u?@YT;^O?&xOd?Br@;Vr1^-W@_qWVCib==4xj{NX2Abc4+|fgSUtP delta 416 zcmX@XcY<$25@Y>Ezh(o8*7ucN#hsmH0jCnStLa_bt-8lSQnaAQ@Yc@j+g+_Lgf4&D zw8ZlHpTBR9VnD%t89n6r~IC z@qB8h+$R6mdFrW<#%ZK@Rd$oPY)h~C(IYPybAMZ?>sQi<6}(IcGi+VVIDmJ#wy!n zCe|pQu0B|PE7a@Jg`lc~>3o0QE))>^9JBh>?~+f-^LMU)xoz6&FV`(&^N;(Kn`~e1 zy}UN#-W4AA#&AW>;x>s{UHiLz{LS{BX^Sg9w0e!jj-#IqUZ0yQxYPQlyhy3}=kyht znj8EdIu&NUW?XbOHT}#E`6R8a`O9pNukH%pCL7P46Qx{{_^bold italic underlined - all at once!""" + all at once!""" ) assert_pdf_equal(pdf, HERE / "html_bold_italic_underline.pdf", tmp_path) @@ -206,7 +205,7 @@ def test_html_customize_ul(tmp_path): for indent, bullet in ((5, "\x86"), (10, "\x9b"), (15, "\xac"), (20, "\xb7")): pdf.write_html( html, - tag_styles={"li": TextStyle(l_margin=indent, t_margin=2)}, + tag_styles={"li": TextStyle(l_margin=indent, b_margin=2)}, ul_bullet_char=bullet, ) pdf.ln() @@ -226,10 +225,10 @@ def test_html_customize_ul_deprecated(tmp_path): for indent, bullet in ((5, "\x86"), (10, "\x9b"), (15, "\xac"), (20, "\xb7")): pdf.write_html(html, li_tag_indent=indent, ul_bullet_char=bullet) pdf.ln() - assert_pdf_equal(pdf, HERE / "html_customize_ul.pdf", tmp_path) + assert_pdf_equal(pdf, HERE / "html_customize_ul_deprecated.pdf", tmp_path) -def test_html_deprecated_li_tag_indent_deprecated(tmp_path): +def test_html_li_tag_indent_deprecated(tmp_path): pdf = FPDF() pdf.add_page() with pytest.warns(DeprecationWarning): @@ -389,22 +388,22 @@ def test_html_custom_heading_sizes(tmp_path): # issue-223
    This is a H6
    """, tag_styles={ "h1": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=6 + color="#960000", t_margin=5 + 834 / 900, b_margin=0.4, font_size_pt=6 ), "h2": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=12 + color="#960000", t_margin=5 + 453 / 900, b_margin=0.4, font_size_pt=12 ), "h3": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=18 + color="#960000", t_margin=5 + 199 / 900, b_margin=0.4, font_size_pt=18 ), "h4": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=24 + color="#960000", t_margin=5 + 72 / 900, b_margin=0.4, font_size_pt=24 ), "h5": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=30 + color="#960000", t_margin=5 - 55 / 900, b_margin=0.4, font_size_pt=30 ), "h6": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=36 + color="#960000", t_margin=5 - 182 / 900, b_margin=0.4, font_size_pt=36 ), }, ) @@ -665,6 +664,29 @@ def test_html_ln_outside_p(tmp_path): assert_pdf_equal(pdf, HERE / "html_ln_outside_p.pdf", tmp_path) +def test_html_sections(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica", size=12) + pdf.write_html( + """ +
    +

    Subtitle 1

    +
    +

    Subtitle 1.1

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
    +
    +

    Subtitle 1.2

    + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +
    +
    + """ + ) + assert_pdf_equal(pdf, HERE / "html_sections.pdf", tmp_path) + + def test_html_and_section_title_styles(): # issue 1080 pdf = FPDF() pdf.add_page() @@ -698,12 +720,13 @@ def test_html_and_section_title_styles_with_deprecated_TitleStyle(): ) -def test_html_link_color(tmp_path): +def test_html_link_style(tmp_path): pdf = FPDF() pdf.add_page() - html = 'foo' - pdf.write_html(html, tag_styles={"a": TextStyle(color=color_as_decimal("red"))}) - assert_pdf_equal(pdf, HERE / "html_link_color.pdf", tmp_path) + html = 'Link to www.example.com' + style = FontFace(color="#f00", family="Courier", size_pt=8, emphasis="BIU") + pdf.write_html(html, tag_styles={"a": style}) + assert_pdf_equal(pdf, HERE / "html_link_style.pdf", tmp_path) def test_html_blockquote_color(tmp_path): @@ -725,10 +748,16 @@ def test_html_headings_color(tmp_path): html, tag_styles={ "h1": TextStyle( - color=(148, 139, 139), font_size_pt=24, t_margin=0.2, b_margin=0.4 + color=(148, 139, 139), + font_size_pt=24, + t_margin=5 + 834 / 900, + b_margin=0.4, ), "h2": TextStyle( - color=(148, 139, 139), font_size_pt=18, t_margin=0.2, b_margin=0.4 + color=(148, 139, 139), + font_size_pt=18, + t_margin=5 + 453 / 900, + b_margin=0.4, ), }, ) @@ -739,15 +768,18 @@ def test_html_unsupported_tag_color(): pdf = FPDF() pdf.add_page() with pytest.raises(NotImplementedError): - pdf.write_html("

    foo

    ", tag_styles={"p": TextStyle()}) + pdf.write_html("

    foo


    bar

    ", tag_styles={"hr": TextStyle()}) -def test_html_link_color_using_FontFace(tmp_path): +def test_html_link_style_using_TextStyle(tmp_path): pdf = FPDF() pdf.add_page() - html = 'foo' - pdf.write_html(html, tag_styles={"a": FontFace(color=color_as_decimal("red"))}) - assert_pdf_equal(pdf, HERE / "html_link_color.pdf", tmp_path) + html = 'Link to www.example.com' + style = TextStyle( + color="#f00", font_family="Courier", font_size_pt=8, font_style="BIU" + ) + pdf.write_html(html, tag_styles={"a": style}) + assert_pdf_equal(pdf, HERE / "html_link_style.pdf", tmp_path) def test_html_blockquote_color_using_FontFace(tmp_path): @@ -779,7 +811,7 @@ def test_html_unsupported_tag_color_using_FontFace(): pdf = FPDF() pdf.add_page() with pytest.raises(NotImplementedError): - pdf.write_html("

    foo

    ", tag_styles={"p": FontFace()}) + pdf.write_html("

    foo


    bar

    ", tag_styles={"hr": FontFace()}) def test_html_blockquote_indent(tmp_path): # issue-1074 @@ -1065,8 +1097,40 @@ def test_html_heading_above_below(tmp_path):

    Second heading

    Lorem ipsum

    """, tag_styles={ - "h1": TextStyle(color="#960000", t_margin=1, b_margin=0.5, font_size_pt=24), - "h2": TextStyle(color="#960000", t_margin=1, b_margin=0.5, font_size_pt=18), + "h1": TextStyle( + color="#960000", t_margin=10, b_margin=0.5, font_size_pt=24 + ), + "h2": TextStyle( + color="#960000", t_margin=10, b_margin=0.5, font_size_pt=18 + ), }, ) assert_pdf_equal(pdf, HERE / "html_heading_above_below.pdf", tmp_path) + + +def test_html_dd_tag_indent_deprecated(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.write_html( + "
    description title
    description details
    ", + tag_styles={"dd": TextStyle(l_margin=5)}, + ) + assert_pdf_equal(pdf, HERE / "html_dd_tag_indent_deprecated.pdf", tmp_path) + pdf = FPDF() + pdf.add_page() + with pytest.warns(DeprecationWarning): + pdf.write_html( + "
    description title
    description details
    ", + dd_tag_indent=5, + ) + assert_pdf_equal(pdf, HERE / "html_dd_tag_indent_deprecated.pdf", tmp_path) + + +def test_html_font_family(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.write_html( + "

    hello world. i am sleepy.

    ", + font_family="Helvetica", + ) + assert_pdf_equal(pdf, HERE / "html_font_family.pdf", tmp_path) diff --git a/test/outline/html_toc.pdf b/test/outline/html_toc.pdf index f06a6167e7ddd3607f9f9df2c24ea4880ff1d1eb..81c6c599d8d0283bc8b7243a16d2c08e2c46ea1f 100644 GIT binary patch delta 991 zcmcbhcu8@CC0o4-mz^C~aY<2XVlG$3oT(EW{SF%lxPG@h8qc=U$aB`El5Q66Bg>Rz z4jged)z~(dUK1Q{d)bI9acBrZ06s$e|{rZYF-L7K!B|Lq%1CT zg~@g7m5i2?f3VBf-&H!*6zf`dpfvBaYu{X}eXZw@FPzB5Bz8i(-`2g@%O-W_s_PY} z-$mcC71QvV`aFaG#e#h(amp^-MuyP zFYHs}D7&vvy6#%N<1~B4RXdW_O;|NYv77y&V3bm0GmF#nwfqU~4$k!p#EWEE`+^I; zg|)4?eBSt`$VCewo@njAOI;NXI`*wmo)fX!@Wli@A(3Z-@xH6RdWHTs)T}pL`6{hV zXWRR>HV+|#P2C@kWf*04O>DH5T^@KZkz<~9(MPexJN)&P%Pv@O9+v0JiCDaN;-N3M zs;y^c9k<=#eM~UyeeHyQSzAuqu`(Qyo4j+gB_QHH=EqSo6l@wV6^!cuQFr3xq<-*DC8+{ff)vdmX;RiVn)U$mKb6tCYb6>jmL;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|}mriAajN}EoGeidKxk25fR zhQ3GWJcZTl_7hq86{95X|K@CDy)|jg#mTqW)aq*wHK({;&YZH@@zl|@)UO9KU(7qc zLUh_WOEXWBUFX*RL`~+H#)Q=$p2oX3w5w%eQ6sOk|$JZ{*5cwR~mr#Y6WL zzwO;NcmCc!Uzr~-ep;IHuasPJ`Bu)Yj``Q8=eX#a?S8|&R$zWUS884gG(3Q;{G=={ za|MIR4eS++7L$Ln%h%@!rXF0UWdFcw`%|UkezNxuJ%4;*T2lk-r&G!2FU}F_k6iak zHE#26F>`*_uArxuw{*jDx1ImyQ5d>n(ZQt1)m44loV84RJ8kA}u%0$6IP;sz>=PG- zPHAt^=WP0F?y)oA^nQiU^94d<4CYVx^^IrdUGDx1LYy)x0#^C84Av}C^-Q*tUuzW1 zX=>hXZ=V-pi@M7`|SW#ZhnDOvX6k*HqB_L7MU3Zy<>T{7$Ws$cz{Kf66c z=O}KzI(wnwB$l2xN_z}v`OHd6dEj?Eb4s~H)APQVJ;yZO9X{P5Yx3>ThugZ+*Ir~S zG1&XE!=BolK@ile>8HnT^fNHvi#OW~?_;FaQCCJOwT=!@$te5*VVWVn)U$m|`X-7{!|i+Xl` zIAGZkdB)@VtWH_0d=Kds>!1g7ntakF|4%fs%*s2QUeV0Yv0i1teU4+3*K@qt)}+O~ zrZfA{JN{i7Tl|8;1&UHmO-$Xibz_nBwP~!Ws}JRUE@Qnm$MXGMpTf?^Ld&_*>(r_~ zGN-*xd$ZY?S&-G%Ou+yI6!H|fzzhQeOJh@XF+&3*BMdP^1Ix*c9KyE77&68N7^WJU z7+GSdGc`4ue33&~6VpC(OA8Fu7M3QHc{y!k&790k&752&XpW`y&<@rl|`evQ}AvOPj>>*{&7vW7GpJ+4f-n!rv zQ}&}-8-6zHajl(vMYKg`W>Lz_oZK}r{aaHVZ?4*KbI)Jq{dcP7f6sfS$QLZb{j9o9 zt?DCV#?-Xmn~j(SS?kRe3_w63Pk{@}Ffgz*HbWOPG%zy85HmD1z!bAE!4NYxz_7#6 z#2BjA3t_RLsVSxqXfvJn7 sxuvVIlZBb7v4NcpK^3uFc6MCFC5c5P6-B9OT!xmW=3J_(uKsRZ0PY5eWB>pF diff --git a/test/outline/test_outline_html.py b/test/outline/test_outline_html.py index 951b1d56e..0d4e692ca 100644 --- a/test/outline/test_outline_html.py +++ b/test/outline/test_outline_html.py @@ -18,24 +18,24 @@ def test_html_toc(tmp_path): Table of content:
    -

    Subtitle 1


    +

    Subtitle 1

    Subtitle 1.1

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.




















    -

    Subtitle 1.2


    +

    Subtitle 1.2

    Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.








































    Subtitle 2


    -

    Subtitle 2.1


    +

    Subtitle 2.1

    Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.




















    -

    Subtitle 2.2


    +

    Subtitle 2.2

    Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.