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 ed19d6962..0f888791e 100644 Binary files a/test/html/html_blockquote_color.pdf and b/test/html/html_blockquote_color.pdf differ diff --git a/test/html/html_blockquote_color_using_FontFace.pdf b/test/html/html_blockquote_color_using_FontFace.pdf index b0dbc49be..509300f21 100644 Binary files a/test/html/html_blockquote_color_using_FontFace.pdf and b/test/html/html_blockquote_color_using_FontFace.pdf differ diff --git a/test/html/html_custom_heading_sizes.pdf b/test/html/html_custom_heading_sizes.pdf index 298a62dc7..90271ffcc 100644 Binary files a/test/html/html_custom_heading_sizes.pdf and b/test/html/html_custom_heading_sizes.pdf differ diff --git a/test/html/html_customize_ul.pdf b/test/html/html_customize_ul.pdf index d85f8d525..e7c998509 100644 Binary files a/test/html/html_customize_ul.pdf and b/test/html/html_customize_ul.pdf differ diff --git a/test/html/html_customize_ul_deprecated.pdf b/test/html/html_customize_ul_deprecated.pdf new file mode 100644 index 000000000..d85f8d525 Binary files /dev/null and b/test/html/html_customize_ul_deprecated.pdf differ diff --git a/test/html/html_link_color.pdf b/test/html/html_dd_tag_indent_deprecated.pdf similarity index 58% rename from test/html/html_link_color.pdf rename to test/html/html_dd_tag_indent_deprecated.pdf index d11bf1bb2..457134b31 100644 Binary files a/test/html/html_link_color.pdf and b/test/html/html_dd_tag_indent_deprecated.pdf differ diff --git a/test/html/html_description.pdf b/test/html/html_description.pdf index 516d99de7..9a32ba41f 100644 Binary files a/test/html/html_description.pdf and b/test/html/html_description.pdf differ diff --git a/test/html/html_font_family.pdf b/test/html/html_font_family.pdf new file mode 100644 index 000000000..067781b56 Binary files /dev/null and b/test/html/html_font_family.pdf differ diff --git a/test/html/html_heading_above_below.pdf b/test/html/html_heading_above_below.pdf index 647a8702d..cdcd7c8cc 100644 Binary files a/test/html/html_heading_above_below.pdf and b/test/html/html_heading_above_below.pdf differ diff --git a/test/html/html_headings_line_height.pdf b/test/html/html_headings_line_height.pdf index d99aa7ea4..914e30ce6 100644 Binary files a/test/html/html_headings_line_height.pdf and b/test/html/html_headings_line_height.pdf differ diff --git a/test/html/html_link_style.pdf b/test/html/html_link_style.pdf new file mode 100644 index 000000000..8d5f0b44a Binary files /dev/null and b/test/html/html_link_style.pdf differ diff --git a/test/html/html_measurement_units.pdf b/test/html/html_measurement_units.pdf index dfa0b74aa..f1dee4707 100644 Binary files a/test/html/html_measurement_units.pdf and b/test/html/html_measurement_units.pdf differ diff --git a/test/html/html_sections.pdf b/test/html/html_sections.pdf new file mode 100644 index 000000000..a6e9ac7bf Binary files /dev/null and b/test/html/html_sections.pdf differ diff --git a/test/html/html_table_with_bgcolor.pdf b/test/html/html_table_with_bgcolor.pdf index 9f9793207..977b6ba90 100644 Binary files a/test/html/html_table_with_bgcolor.pdf and b/test/html/html_table_with_bgcolor.pdf differ diff --git a/test/html/html_whitespace_handling.pdf b/test/html/html_whitespace_handling.pdf index f676cd5b1..2027872d9 100644 Binary files a/test/html/html_whitespace_handling.pdf and b/test/html/html_whitespace_handling.pdf differ diff --git a/test/html/test_html.py b/test/html/test_html.py index 73a487035..a26141f78 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -4,7 +4,6 @@ from fpdf import FPDF, FontFace, HTMLMixin, TextStyle, TitleStyle from fpdf.drawing import DeviceRGB -from fpdf.html import color_as_decimal from fpdf.errors import FPDFException from test.conftest import assert_pdf_equal, LOREM_IPSUM @@ -189,7 +188,7 @@ def test_html_bold_italic_underline(tmp_path): """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 f06a6167e..81c6c599d 100644 Binary files a/test/outline/html_toc.pdf and b/test/outline/html_toc.pdf differ diff --git a/test/outline/html_toc_with_custom_rendering.pdf b/test/outline/html_toc_with_custom_rendering.pdf index ecbc5de94..9220ee1e1 100644 Binary files a/test/outline/html_toc_with_custom_rendering.pdf and b/test/outline/html_toc_with_custom_rendering.pdf differ 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.