From 3b6cd42f9c76c8ef021996ebb3c6b5001535d8d3 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Fri, 28 Jun 2024 23:15:53 +0200 Subject: [PATCH] Split handling of HTML attributes & style CSS properties (#1211) --- CHANGELOG.md | 11 +- docs/HTML.md | 24 +- fpdf/__init__.py | 5 +- fpdf/enums.py | 8 + fpdf/fonts.py | 78 +++- fpdf/fpdf.py | 93 ++-- fpdf/html.py | 338 ++++++++------ fpdf/line_break.py | 2 +- fpdf/svg.py | 6 +- fpdf/text_region.py | 7 +- test/errors/test_deprecation_warnings.py | 14 + test/fonts/test_add_font.py | 2 +- test/html/html_blockquote_color.pdf | Bin 1032 -> 1174 bytes .../html_blockquote_color_using_FontFace.pdf | Bin 0 -> 1170 bytes test/html/html_heading_above_below.pdf | Bin 0 -> 1590 bytes test/html/html_list_vertical_margin.pdf | Bin 2783 -> 2769 bytes test/html/html_ln_outside_p.pdf | Bin 1056 -> 1055 bytes test/html/html_measurement_units.pdf | Bin 1306 -> 1307 bytes test/html/test_html.py | 427 +++++++++++++----- test/outline/test_outline.py | 142 +++++- test/test_enums.py | 36 ++ test/{errors => }/test_page_format.py | 6 + 22 files changed, 858 insertions(+), 341 deletions(-) create mode 100644 test/errors/test_deprecation_warnings.py create mode 100644 test/html/html_blockquote_color_using_FontFace.pdf create mode 100644 test/html/html_heading_above_below.pdf create mode 100644 test/test_enums.py rename test/{errors => }/test_page_format.py (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b53a865f..5e5fad12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,9 @@ This can also be enabled programmatically with `warnings.simplefilter('default', * 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): 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 +* [`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 * [`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). @@ -38,10 +38,13 @@ This can also be enabled programmatically with `warnings.simplefilter('default', * default values for `top_margin` and `bottom_margin` in `HTML2FPDF._new_paragraph()` calls are now correctly converted into chosen document units. * In [text_columns()](https://py-pdf.github.io/fpdf2/extColumns.html), paragraph top/bottom margins didn't correctly trigger column breaks; [issue #1214](https://github.com/py-pdf/fpdf2/issues/1214) ### Removed -* an obscure and undocumented [feature](https://github.com/py-pdf/fpdf2/issues/1198) of [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html), which used to magically pass local variables as arguments. +* an obscure and undocumented [feature](https://github.com/py-pdf/fpdf2/issues/1198) of [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html), which used to magically pass instance attributes as arguments. +### Deprecated +* `fpdf.TitleStyle` has been renamed into `fpdf.TextStyle` +* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): `tag_indents` introduced in the last version - Now the indentation can be provided through the `tag_styles` parameter, using the `.l_margin` of `TextStyle` instances ### Changed * [`FPDF.table()`](https://py-pdf.github.io/fpdf2/Tables.html) now raises an error when a single row is too high to be rendered on a single page -* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): `tag_indents` can now be non-integer. Indentation of HTML elements is now independent of font size and bullet strings. +* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): indentation of HTML elements can now be non-integer (float), and is now independent of font size and bullet strings. * improved performance of font glyph selection by using functools cache ## [2.7.9] - 2024-05-17 diff --git a/docs/HTML.md b/docs/HTML.md index f7f432ef2..156dd0bdf 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -97,16 +97,16 @@ pdf.write_html("""

Hello world!

""", tag_styles={ - "h1": FontFace(color=(148, 139, 139), size_pt=32), - "h2": FontFace(color=(148, 139, 139), size_pt=24), + "h1": FontFace(color="#948b8b", size_pt=32), + "h2": FontFace(color="#948b8b", size_pt=24), }) pdf.output("html_styled.pdf") ``` -Similarly, the indentation of several HTML tags (`
`, `
`, `
  • `) can be set globally, for the whole HTML document, by passing `tag_indents` to `FPDF.write_html()`: +Similarly, the indentation of several HTML tags (`
    `, `
    `, `
  • `) can be set globally, for the whole HTML document, by passing `tag_styles` to `FPDF.write_html()`: ```python -from fpdf import FPDF +from fpdf import FPDF, TextStyle pdf = FPDF() pdf.add_page() @@ -115,10 +115,23 @@ pdf.write_html("""
    Term
    Definition
    -""", tag_indents={"dd": 5}) +
    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. + Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. + Cras elementum ultrices diam. +
    +""", tag_styles={ + "dd": TextStyle(l_margin=5), + "blockquote": TextStyle(color="#ccc", font_style="I", + t_margin=5, b_margin=5, l_margin=10), + }) pdf.output("html_dd_indented.pdf") ``` +⚠️ Note that this styling is currently only supported for a subset of all HTML tags, +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! + ## Supported HTML features @@ -143,6 +156,7 @@ pdf.output("html_dd_indented.pdf") * ``: cells (with `align`, `bgcolor`, `width`, `rowspan`, `colspan` attributes) ### Page breaks + _New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ Page breaks can be triggered explicitly using the [break-before](https://developer.mozilla.org/en-US/docs/Web/CSS/break-before) or [break-after](https://developer.mozilla.org/en-US/docs/Web/CSS/break-after) CSS properties. diff --git a/fpdf/__init__.py b/fpdf/__init__.py index 2a384f1c7..494f2f346 100644 --- a/fpdf/__init__.py +++ b/fpdf/__init__.py @@ -9,7 +9,7 @@ * `fpdf.enums.YPos` * `fpdf.errors.FPDFException` * `fpdf.fonts.FontFace` -* `fpdf.fpdf.TitleStyle` +* `fpdf.fonts.TextStyle` * `fpdf.prefs.ViewerPreferences` * `fpdf.template.Template` * `fpdf.template.FlexTemplate` @@ -25,7 +25,7 @@ FPDF_FONT_DIR as _FPDF_FONT_DIR, FPDF_VERSION as _FPDF_VERSION, ) -from .fonts import FontFace +from .fonts import FontFace, TextStyle from .html import HTMLMixin, HTML2FPDF from .prefs import ViewerPreferences from .template import Template, FlexTemplate @@ -74,6 +74,7 @@ "Template", "FlexTemplate", "TitleStyle", + "TextStyle", "ViewerPreferences", # Deprecated classes: "HTMLMixin", diff --git a/fpdf/enums.py b/fpdf/enums.py index 08c62d85b..22cb84cb5 100644 --- a/fpdf/enums.py +++ b/fpdf/enums.py @@ -245,6 +245,14 @@ def style(self): name for name, value in self.__class__.__members__.items() if value & self ) + def add(self, value: "TextEmphasis"): + return self | value + + def remove(self, value: "TextEmphasis"): + return TextEmphasis.coerce( + "".join(s for s in self.style if s not in value.style) + ) + @classmethod def coerce(cls, value): if isinstance(value, str): diff --git a/fpdf/fonts.py b/fpdf/fonts.py index d41e2c088..f62cc84a1 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -7,7 +7,7 @@ in non-backward-compatible ways. """ -import re +import re, warnings from bisect import bisect_left from collections import defaultdict @@ -31,6 +31,7 @@ def __deepcopy__(self, _memo): except ImportError: hb = None +from .deprecation import get_stack_level from .drawing import convert_to_device_color, DeviceGray, DeviceRGB from .enums import FontDescriptorFlags, TextEmphasis from .syntax import Name, PDFObject @@ -109,6 +110,81 @@ def combine(default_style, override_style): ) +class TextStyle(FontFace): + """ + Subclass of `FontFace` that allows to specify vertical & horizontal spacing + """ + + def __init__( + self, + font_family: Optional[str] = None, # None means "no override" + # Whereas "" means "no emphasis" + font_style: Optional[str] = None, + font_size_pt: Optional[int] = None, + color: Union[int, tuple] = None, # grey scale or (red, green, blue), + fill_color: Union[int, tuple] = None, # grey scale or (red, green, blue), + underline: bool = False, + t_margin: Optional[int] = None, + l_margin: Optional[int] = None, + b_margin: Optional[int] = None, + ): + super().__init__( + font_family, + ((font_style or "") + "U") if underline else font_style, + font_size_pt, + color, + fill_color, + ) + self.t_margin = t_margin or 0 + self.l_margin = l_margin or 0 + self.b_margin = b_margin or 0 + + def __repr__(self): + return ( + super().__repr__()[:-1] + + f", t_margin={self.t_margin}, l_margin={self.l_margin}, b_margin={self.b_margin})" + ) + + def replace( + self, + /, + font_family=None, + emphasis=None, + font_size_pt=None, + color=None, + fill_color=None, + t_margin=None, + l_margin=None, + b_margin=None, + ): + return TextStyle( + font_family=font_family or self.family, + font_style=self.emphasis if emphasis is None else emphasis.style, + font_size_pt=font_size_pt or self.size_pt, + color=color or self.color, + fill_color=fill_color or self.fill_color, + t_margin=self.t_margin if t_margin is None else t_margin, + l_margin=self.l_margin if l_margin is None else l_margin, + b_margin=self.b_margin if b_margin is None else b_margin, + ) + + +class TitleStyle(TextStyle): + def __init__(self, *args, **kwargs): + warnings.warn( + ( + "fpdf.TitleStyle is deprecated since 2.7.10." + " It has been replaced by fpdf.TextStyle." + ), + DeprecationWarning, + stacklevel=get_stack_level(), + ) + super().__init__(*args, **kwargs) + + +__pdoc__ = {"TitleStyle": False} # Replaced by TextStyle + + class CoreFont: # RAM usage optimization: __slots__ = ("i", "type", "name", "up", "ut", "cw", "fontkey", "emphasis") diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 321855623..3753d9e60 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -85,7 +85,7 @@ class Image: YPos, ) from .errors import FPDFException, FPDFPageFormatException, FPDFUnicodeEncodingException -from .fonts import CoreFont, CORE_FONTS, FontFace, TTFFont +from .fonts import CoreFont, CORE_FONTS, FontFace, TextStyle, TitleStyle, TTFFont from .graphics_state import GraphicsStateMixin from .html import HTML2FPDF from .image_datastructures import ( @@ -142,36 +142,6 @@ class Image: } -class TitleStyle(FontFace): - def __init__( - self, - font_family: Optional[str] = None, # None means "no override" - # Whereas "" means "no emphasis" - font_style: Optional[str] = None, - font_size_pt: Optional[int] = None, - color: Union[int, tuple] = None, # grey scale or (red, green, blue), - underline: bool = False, - t_margin: Optional[int] = None, - l_margin: Optional[int] = None, - b_margin: Optional[int] = None, - ): - super().__init__( - font_family, - ((font_style or "") + "U") if underline else font_style, - font_size_pt, - color, - ) - self.t_margin = t_margin - self.l_margin = l_margin - self.b_margin = b_margin - - def __repr__(self): - return ( - super().__repr__()[:-1] - + f", t_margin={self.t_margin}, l_margin={self.l_margin}, b_margin={self.b_margin})" - ) - - class ToCPlaceholder(NamedTuple): render_function: Callable start_page: int @@ -307,7 +277,7 @@ def __init__( self._toc_placeholder = None # optional ToCPlaceholder instance self._outline = [] # list of OutlineSection self._sign_key = None - self.section_title_styles = {} # level -> TitleStyle + self.section_title_styles = {} # level -> TextStyle self.core_fonts_encoding = "latin-1" "Font encoding, Latin-1 by default" @@ -413,25 +383,24 @@ def write_html(self, text, *args, **kwargs): Args: text (str): HTML content to render - image_map (function): an optional one-argument function that map "src" - to new image URLs - li_tag_indent (int): [**DEPRECATED since v2.7.8**] - numeric indentation of
  • elements - Set tag_indents instead - dd_tag_indent (int): [**DEPRECATED since v2.7.8**] - numeric indentation of
    elements - Set tag_indents instead - table_line_separators (bool): enable horizontal line separators in - ul_bullet_char (str): bullet character preceding
  • items in
      lists. - li_prefix_color (tuple | str | drawing.Device* instance): - color for bullets or numbers preceding
    • tags. - This applies to both
        &
          lists. - heading_sizes (dict): [**DEPRECATED since v2.7.8**] - font size per heading level names ("h1", "h2"...) - Set tag_styles instead - pre_code_font (str): [**DEPRECATED since v2.7.8**] - font to use for
           &  blocks - Set tag_styles instead
          -            warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags
          -            tag_indents (dict):
          -                mapping of HTML tag names to numeric values representing their horizontal left identation
          -            tag_styles (dict): mapping of HTML tag names to colors
          +            image_map (function): an optional one-argument function that map `` "src" to new image URLs
          +            li_tag_indent (int): [**DEPRECATED since v2.7.9**]
          +                numeric indentation of `
        1. ` elements - Set `tag_styles` instead + dd_tag_indent (int): [**DEPRECATED since v2.7.9**] + numeric indentation of `
          ` elements - Set `tag_styles` instead + table_line_separators (bool): enable horizontal line separators in `
  • `. Defaults to `False`. + ul_bullet_char (str): bullet character preceding `
  • ` items in `
      ` lists. + Can also be configured using the HTML `type` attribute of `
        ` tags. + li_prefix_color (tuple, str, fpdf.drawing.DeviceCMYK, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): color for bullets + or numbers preceding `
      • ` tags. This applies to both `
          ` & `
            ` lists. + heading_sizes (dict): [**DEPRECATED since v2.7.9**] + font size per heading level names ("h1", "h2"...) - Set `tag_styles` instead + pre_code_font (str): [**DEPRECATED since v2.7.9**] + font to use for `
            ` & `` blocks - Set `tag_styles` instead
            +            warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags. Defaults to `True`.
            +            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
                     """
                     html2pdf = self.HTML2FPDF_CLASS(self, *args, **kwargs)
                     with self.local_context():
            @@ -5033,18 +5002,18 @@ def set_section_title_styles(
                     After calling this method, calls to `FPDF.start_section` will render section names visually.
             
                     Args:
            -            level0 (TitleStyle): style for the top level section titles
            -            level1 (TitleStyle): optional style for the level 1 section titles
            -            level2 (TitleStyle): optional style for the level 2 section titles
            -            level3 (TitleStyle): optional style for the level 3 section titles
            -            level4 (TitleStyle): optional style for the level 4 section titles
            -            level5 (TitleStyle): optional style for the level 5 section titles
            -            level6 (TitleStyle): optional style for the level 6 section titles
            +            level0 (TextStyle): style for the top level section titles
            +            level1 (TextStyle): optional style for the level 1 section titles
            +            level2 (TextStyle): optional style for the level 2 section titles
            +            level3 (TextStyle): optional style for the level 3 section titles
            +            level4 (TextStyle): optional style for the level 4 section titles
            +            level5 (TextStyle): optional style for the level 5 section titles
            +            level6 (TextStyle): optional style for the level 6 section titles
                     """
                     for level in (level0, level1, level2, level3, level4, level5, level6):
            -            if level and not isinstance(level, TitleStyle):
            +            if level and not isinstance(level, TextStyle):
                             raise TypeError(
            -                    f"Arguments must all be TitleStyle instances, got: {type(level)}"
            +                    f"Arguments must all be TextStyle instances, got: {type(level)}"
                             )
                     self.section_title_styles = {
                         0: level0,
            @@ -5115,7 +5084,7 @@ def start_section(self, name, level=0, strict=True):
                     )
             
                 @contextmanager
            -    def _use_title_style(self, title_style: TitleStyle):
            +    def _use_title_style(self, title_style: TextStyle):
                     if title_style:
                         if title_style.t_margin:
                             self.ln(title_style.t_margin)
            @@ -5177,7 +5146,7 @@ def table(self, *args, **kwargs):
                             relative to the page, when it's not using the full page width.
                         borders_layout (str, fpdf.enums.TableBordersLayout): optional, default to ALL. Control what cell
                             borders are drawn.
            -            cell_fill_color (int, tuple, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): optional.
            +            cell_fill_color (int, tuple, fpdf.drawing.DeviceCMYK, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): optional.
                             Defines the cells background color.
                         cell_fill_mode (str, fpdf.enums.TableCellFillMode): optional. Defines which cells are filled
                             with color in the background.
            diff --git a/fpdf/html.py b/fpdf/html.py
            index fa47bd098..d44c09e28 100644
            --- a/fpdf/html.py
            +++ b/fpdf/html.py
            @@ -14,30 +14,29 @@
             from .drawing import color_from_hex_string, convert_to_device_color
             from .enums import Align, TextEmphasis, XPos, YPos
             from .errors import FPDFException
            -from .fonts import FontFace
            +from .fonts import FontFace, TextStyle
             from .table import Table
            -from .util import int2roman, get_scale_factor
            +from .util import get_scale_factor, int2roman
             
             LOGGER = logging.getLogger(__name__)
             BULLET_WIN1252 = "\x95"  # BULLET character in Windows-1252 encoding
             DEGREE_WIN1252 = "\xb0"
             HEADING_TAGS = ("h1", "h2", "h3", "h4", "h5", "h6")
             DEFAULT_TAG_STYLES = {
            -    "a": FontFace(color=(0, 0, 255)),
            -    "blockquote": FontFace(color=(100, 0, 45)),
            -    "code": FontFace(family="Courier"),
            -    "h1": FontFace(color=(150, 0, 0), size_pt=24),
            -    "h2": FontFace(color=(150, 0, 0), size_pt=18),
            -    "h3": FontFace(color=(150, 0, 0), size_pt=14),
            -    "h4": FontFace(color=(150, 0, 0), size_pt=12),
            -    "h5": FontFace(color=(150, 0, 0), size_pt=10),
            -    "h6": FontFace(color=(150, 0, 0), size_pt=8),
            -    "pre": FontFace(family="Courier"),
            -}
            -DEFAULT_TAG_INDENTS_MM = {
            -    "blockquote": 0,
            -    "dd": 10,
            -    "li": 5,
            +    "a": TextStyle(color="#00f"),
            +    "blockquote": TextStyle(color="#64002d", t_margin=3, b_margin=3),
            +    "code": TextStyle(font_family="Courier"),
            +    "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),
            +    "li": TextStyle(l_margin=5, t_margin=2),
            +    "pre": TextStyle(font_family="Courier"),
            +    "ol": TextStyle(t_margin=2),
            +    "ul": TextStyle(t_margin=2),
             }
             
             # Pattern to substitute whitespace sequences with a single space character each.
            @@ -242,22 +241,17 @@ def color_as_decimal(color="#000000"):
                 return color_from_hex_string(hexcolor).colors255
             
             
            -def parse_style(elem_attrs):
            -    """Parse `style="..."` making it's key-value pairs element's attributes"""
            -    try:
            -        style = elem_attrs["style"]
            -    except KeyError:
            -        pass
            -    else:
            -        for element in style.split(";"):
            -            if not element:
            -                continue
            -
            -            pair = element.split(":")
            -            if len(pair) == 2 and pair[0] and pair[1]:
            -                attr, value = pair
            -
            -                elem_attrs[attr.strip()] = value.strip()
            +def parse_css_style(style_attr):
            +    """Parse `style="..."` HTML attributes, and return a dict of key-value"""
            +    style = {}
            +    for element in style_attr.split(";"):
            +        if not element:
            +            continue
            +        pair = element.split(":")
            +        if len(pair) == 2 and pair[0] and pair[1]:
            +            attr, value = pair
            +            style[attr.strip()] = value.strip()
            +    return style
             
             
             class HTML2FPDF(HTMLParser):
            @@ -276,32 +270,32 @@ def __init__(
                     ul_bullet_char=BULLET_WIN1252,
                     li_prefix_color=(190, 0, 0),
                     heading_sizes=None,
            -        pre_code_font=DEFAULT_TAG_STYLES["pre"].family,
            +        pre_code_font=None,
                     warn_on_tags_not_matching=True,
                     tag_indents=None,
                     tag_styles=None,
            -        list_vertical_margin=None,
            -        **_,
                 ):
                     """
                     Args:
                         pdf (FPDF): an instance of `fpdf.FPDF`
            -            image_map (function): an optional one-argument function that map  "src"
            -                to new image URLs
            -            li_tag_indent (int): [**DEPRECATED since v2.7.9**] numeric indentation of 
          1. elements - Set tag_indents instead - dd_tag_indent (int): [**DEPRECATED since v2.7.9**] numeric indentation of
            elements - Set tag_indents instead - table_line_separators (bool): enable horizontal line separators in
  • - ul_bullet_char (str): bullet character preceding
  • items in
      lists. - li_prefix_color (tuple | str | drawing.Device* instance): color for bullets or numbers preceding
    • tags. - This applies to both
        &
          lists. - heading_sizes (dict): [**DEPRECATED since v2.7.9**] font size per heading level names ("h1", "h2"...) - Set tag_styles instead - pre_code_font (str): [**DEPRECATED since v2.7.9**] font to use for
           &  blocks - Set tag_styles instead
          -            warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags
          -            tag_indents (dict): mapping of HTML tag names to numeric values representing their horizontal left identation.
          -                The indent values are in the chosen pdf document units.
          -            tag_styles (dict): mapping of HTML tag names to colors
          -            list_vertical_margin (float): size of margins that precede lists.
          -                The margin value is in the chosen pdf document units.
          +            image_map (function): an optional one-argument function that map `` "src" to new image URLs
          +            li_tag_indent (int): [**DEPRECATED since v2.7.9**]
          +                numeric indentation of `
        1. ` elements - Set `tag_styles` instead + dd_tag_indent (int): [**DEPRECATED since v2.7.9**] + numeric indentation of `
          ` elements - Set `tag_styles` instead + table_line_separators (bool): enable horizontal line separators in `
  • `. Defaults to `False`. + ul_bullet_char (str): bullet character preceding `
  • ` items in `
      ` lists. + Can also be configured using the HTML `type` attribute of `
        ` tags. + li_prefix_color (tuple, str, fpdf.drawing.DeviceCMYK, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): color for bullets + or numbers preceding `
      • ` tags. This applies to both `
          ` & `
            ` lists. + heading_sizes (dict): [**DEPRECATED since v2.7.9**] + font size per heading level names ("h1", "h2"...) - Set `tag_styles` instead + pre_code_font (str): [**DEPRECATED since v2.7.9**] + font to use for `
            ` & `` blocks - Set `tag_styles` instead
            +            warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags. Defaults to `True`.
            +            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
                     """
                     super().__init__()
                     self.pdf = pdf
            @@ -322,7 +316,7 @@ 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 = dict(b=False, i=False, u=False)
            +        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._page_break_after_paragraph = False
            @@ -338,17 +332,8 @@ def __init__(
                     self.line_height_stack = []
                     self.ol_type = []  # when inside a 
              tag, can be "a", "A", "i", "I" or "1" self.bullet = [] - # 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 - list_vertical_margin = 2 * self.default_conversion_factor - self.list_vertical_margin = list_vertical_margin self.font_color = pdf.text_color.colors255 self.heading_level = None - self.heading_above = 0.2 # extra space above heading, relative to font size - self.heading_below = 0.4 # extra space below heading, relative to font size self._tags_stack = [] self._column = self.pdf.text_columns(skip_leading_spaces=True) self._paragraph = self._column.paragraph() @@ -360,76 +345,98 @@ def __init__( self.td_th = None # becomes a dict of attributes when processing
  • / tags # "inserted" is a special attribute indicating that a cell has be inserted in self.table_row - if not tag_indents: - tag_indents = { - k: v * self.default_conversion_factor - for k, v in DEFAULT_TAG_INDENTS_MM.items() - } - if dd_tag_indent is not None: + self.tag_styles = _scale_units(pdf, DEFAULT_TAG_STYLES) + for tag, tag_style in (tag_styles or {}).items(): + if tag not in DEFAULT_TAG_STYLES: + 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): + # pylint: disable=redefined-loop-name + tag_style = TextStyle( + font_family=tag_style.family, + font_style=( + "" if not tag_style.emphasis else tag_style.emphasis.style + ), + font_size_pt=tag_style.size_pt, + color=tag_style.color, + fill_color=tag_style.fill_color, + # Using default tag margins: + t_margin=self.tag_styles[tag].t_margin, + l_margin=self.tag_styles[tag].l_margin, + b_margin=self.tag_styles[tag].b_margin, + ) + self.tag_styles[tag] = tag_style + if heading_sizes is not None: warnings.warn( ( - "The dd_tag_indent parameter is deprecated since v2.7.9 " + "The heading_sizes parameter is deprecated since v2.7.9 " "and will be removed in a future release. " - "Set the `tag_indents` parameter instead." + "Set the `tag_styles` parameter instead." ), DeprecationWarning, stacklevel=get_stack_level(), ) - tag_indents["dd"] = dd_tag_indent - if li_tag_indent is not None: + for tag, size in heading_sizes.items(): + self.tag_styles[tag] = self.tag_styles[tag].replace(font_size_pt=size) + if pre_code_font is not None: warnings.warn( ( - "The li_tag_indent parameter is deprecated since v2.7.9 " + "The pre_code_font parameter is deprecated since v2.7.9 " "and will be removed in a future release. " - "Set the `tag_indents` parameter instead." + "Set the `tag_styles` parameter instead." ), DeprecationWarning, stacklevel=get_stack_level(), ) - tag_indents["li"] = li_tag_indent - for tag in tag_indents: - if tag not in DEFAULT_TAG_INDENTS_MM: - raise NotImplementedError( - f"Cannot set indent for HTML tag <{tag}> (contributions are welcome to add support for this)" - ) - self.tag_indents = {**DEFAULT_TAG_INDENTS_MM, **tag_indents} - - if not tag_styles: - tag_styles = {} - for tag in tag_styles: - if tag not in DEFAULT_TAG_STYLES: - raise NotImplementedError( - f"Cannot set style for HTML tag <{tag}> (contributions are welcome to add support for this)" - ) - self.tag_styles = {**DEFAULT_TAG_STYLES, **tag_styles} - if heading_sizes is not None: + self.tag_styles["code"] = self.tag_styles["code"].replace( + font_family=pre_code_font + ) + self.tag_styles["pre"] = self.tag_styles["pre"].replace( + font_family=pre_code_font + ) + if dd_tag_indent is not None: warnings.warn( ( - "The heading_sizes parameter is deprecated since v2.7.9 " + "The dd_tag_indent parameter is deprecated since v2.7.9 " "and will be removed in a future release. " "Set the `tag_styles` parameter instead." ), DeprecationWarning, stacklevel=get_stack_level(), ) - for tag, size in heading_sizes.items(): - self.tag_styles[tag] = self.tag_styles[tag].replace(size_pt=size) - if pre_code_font != DEFAULT_TAG_STYLES["pre"].family: + self.tag_styles["dd"] = self.tag_styles["pre"].replace( + l_margin=dd_tag_indent + ) + if li_tag_indent is not None: warnings.warn( ( - "The pre_code_font parameter is deprecated since v2.7.9 " + "The li_tag_indent parameter is deprecated since v2.7.9 " "and will be removed in a future release. " "Set the `tag_styles` parameter instead." ), DeprecationWarning, stacklevel=get_stack_level(), ) - self.tag_styles["code"] = self.tag_styles["code"].replace( - family=pre_code_font + self.tag_styles["li"] = self.tag_styles["li"].replace( + l_margin=li_tag_indent ) - self.tag_styles["pre"] = self.tag_styles["pre"].replace( - family=pre_code_font + if tag_indents: + warnings.warn( + ( + "The tag_indents parameter is deprecated since v2.7.10 " + "and will be removed in a future release. " + "Set the `tag_styles` parameter instead." + ), + DeprecationWarning, + stacklevel=get_stack_level(), ) + for tag, indent in tag_indents.items(): + if tag not in self.tag_styles: + raise NotImplementedError( + f"Cannot set style for HTML tag <{tag}> (contributions are welcome to add support for this)" + ) + self.tag_styles[tag] = self.tag_styles[tag].replace(l_margin=indent) def _new_paragraph( self, @@ -511,13 +518,17 @@ def handle_data(self, data): emphasis |= TextEmphasis.I if self.td_th.get("U"): emphasis |= TextEmphasis.U - style = None + font_style = None if bgcolor or emphasis: - style = FontFace( + font_style = FontFace( emphasis=emphasis, fill_color=bgcolor, color=self.pdf.text_color ) self.table_row.cell( - text=data, align=align, style=style, colspan=colspan, rowspan=rowspan + text=data, + align=align, + style=font_style, + colspan=colspan, + rowspan=rowspan, ) self.td_th["inserted"] = True elif self.table is not None: @@ -561,9 +572,9 @@ def handle_starttag(self, tag, attrs): self._pre_started = False attrs = dict(attrs) LOGGER.debug("STARTTAG %s %s", tag, attrs) - parse_style(attrs) + css_style = parse_css_style(attrs.get("style", "")) self._tags_stack.append(tag) - if attrs.get("break-before") == "page": + if css_style.get("break-before") == "page": self._end_paragraph() # pylint: disable=protected-access self.pdf._perform_page_break() @@ -580,7 +591,7 @@ def handle_starttag(self, tag, attrs): line_height=( self.line_height_stack[-1] if self.line_height_stack else None ), - indent=self.tag_indents["dd"] * (self.indent + 1), + indent=self.tag_styles["dd"].l_margin * (self.indent + 1), ) if tag == "strong": tag = "b" @@ -606,19 +617,23 @@ def handle_starttag(self, tag, attrs): align = attrs.get("align")[0].upper() if not align in ["L", "R", "J", "C"]: align = None - line_height = None - if "line-height" in attrs: + line_height = css_style.get("line-height", attrs.get("line-height")) + # "line-height" attributes are not valid in HTML, + # but we support it for backward compatibility, + # because fpdf2 honors it since 2.6.1 and PR #629 + if line_height: try: # YYY parse and convert non-float line_height values - line_height = float(attrs.get("line-height")) + line_height = float(line_height) except ValueError: - pass + line_height = None self._new_paragraph(align=align, line_height=line_height) 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, color=self.font_color, ) @@ -634,11 +649,15 @@ def handle_starttag(self, tag, attrs): align = None self._new_paragraph( align=align, - top_margin=prev_font_height + self.heading_above * hsize, - bottom_margin=self.heading_below * hsize, + top_margin=prev_font_height + tag_style.t_margin * hsize, + bottom_margin=tag_style.b_margin * hsize, ) color = None - if "color" in attrs: + if "color" in css_style: + color = color_as_decimal(css_style["color"]) + elif "color" in attrs: + # "color" attributes are not valid in HTML, + # but we support it for backward compatibility: color = color_as_decimal(attrs["color"]) elif tag_style.color: color = tag_style.color.colors255 @@ -650,7 +669,7 @@ def handle_starttag(self, tag, attrs): ) if tag == "hr": self._end_paragraph() - width = attrs.get("width") + width = css_style.get("width", attrs.get("width")) if width: if width[-1] == "%": width = self.pdf.epw * int(width[:-1]) / 100 @@ -671,6 +690,7 @@ def handle_starttag(self, tag, attrs): self.style_stack.append( FontFace( family=self.font_family, + emphasis=self.emphasis, size_pt=self.font_size, color=self.font_color, ) @@ -687,6 +707,7 @@ def handle_starttag(self, tag, attrs): self.style_stack.append( FontFace( family=self.font_family, + emphasis=self.emphasis, size_pt=self.font_size, color=self.font_color, ) @@ -702,9 +723,19 @@ def handle_starttag(self, tag, attrs): 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, + ) + ) 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, @@ -713,9 +744,9 @@ def handle_starttag(self, tag, attrs): 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=3 * self.default_conversion_factor, - bottom_margin=3 * self.default_conversion_factor, - indent=self.tag_indents["blockquote"] * self.indent, + 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 @@ -723,16 +754,22 @@ def handle_starttag(self, tag, attrs): ul_prefix(attrs["type"]) if "type" in attrs else self.ul_bullet_char ) self.bullet.append(bullet_char) - if "line-height" in attrs: + line_height = css_style.get("line-height", attrs.get("line-height")) + # "line-height" attributes are not valid in HTML, + # but we support it for backward compatibility, + # because fpdf2 honors it since 2.6.1 and PR #629 + if line_height: try: # YYY parse and convert non-float line_height values - self.line_height_stack.append(float(attrs.get("line-height"))) + self.line_height_stack.append(float(line_height)) except ValueError: pass else: self.line_height_stack.append(None) if self.indent == 1: - self._new_paragraph(top_margin=self.list_vertical_margin, line_height=0) + self._new_paragraph( + top_margin=self.tag_styles["ul"].t_margin, line_height=0 + ) self._write_paragraph("\u00a0") self._end_paragraph() if tag == "ol": @@ -740,22 +777,28 @@ def handle_starttag(self, tag, attrs): start = int(attrs["start"]) if "start" in attrs else 1 self.bullet.append(start - 1) self.ol_type.append(attrs.get("type", "1")) - if "line-height" in attrs: + line_height = css_style.get("line-height", attrs.get("line-height")) + # "line-height" attributes are not valid in HTML, + # but we support it for backward compatibility, + # because fpdf2 honors it since 2.6.1 and PR #629 + if line_height: try: # YYY parse and convert non-float line_height values - self.line_height_stack.append(float(attrs.get("line-height"))) + self.line_height_stack.append(float(line_height)) except ValueError: pass else: self.line_height_stack.append(None) if self.indent == 1: - self._new_paragraph(top_margin=self.list_vertical_margin, line_height=0) + self._new_paragraph( + top_margin=self.tag_styles["ol"].t_margin, line_height=0 + ) 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(2 * self.default_conversion_factor) + 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] @@ -771,7 +814,7 @@ def handle_starttag(self, tag, attrs): line_height=( self.line_height_stack[-1] if self.line_height_stack else None ), - indent=self.tag_indents["li"] * self.indent, + indent=self.tag_styles["li"].l_margin * self.indent, bullet=bullet, ) self.set_text_color(*self.font_color) @@ -780,6 +823,7 @@ def handle_starttag(self, tag, attrs): self.style_stack.append( FontFace( family=self.font_family, + emphasis=self.emphasis, size_pt=self.font_size, color=self.font_color, ) @@ -792,12 +836,14 @@ def handle_starttag(self, tag, attrs): # This may result in a FPDFException "font not found". self.set_font(face) self.font_family = face - if "size" in attrs: + if "font-size" in css_style: + self.font_size = int(css_style.get("font-size")) + elif "size" in attrs: self.font_size = int(attrs.get("size")) self.set_font() self.set_text_color(*self.font_color) if tag == "table": - width = attrs.get("width") + width = css_style.get("width", attrs.get("width")) if width: if width[-1] == "%": width = self.pdf.epw * int(width[:-1]) / 100 @@ -908,7 +954,7 @@ 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 css_style.get("break-after") == "page": if tag in ("br", "hr", "img"): self._end_paragraph() # pylint: disable=protected-access @@ -940,24 +986,30 @@ def handle_endtag(self, tag): 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_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": + 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._end_paragraph() - self.set_text_color(*self.font_color) self.indent -= 1 if tag in ("strong", "dt"): tag = "b" @@ -997,6 +1049,7 @@ 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_text_color(*font_face.color.colors255) @@ -1021,7 +1074,7 @@ def set_font(self, family=None, size=None, set_default=False): if size: self.font_size = size self.h = size / self.pdf.k - style = "".join(s for s in ("b", "i", "u") if self.emphasis.get(s)).upper() + 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 if not set_default: # make sure there's at least one font defined in the PDF. @@ -1032,11 +1085,14 @@ def set_font(self, family=None, size=None, set_default=False): self.pdf.set_font_size(self.font_size) self.pdf.page = prev_page - def set_style(self, tag=None, enable=False): - # Modify style and select corresponding font - if tag: - self.emphasis[tag.lower()] = enable - style = "".join(s for s in ("b", "i", "u") if self.emphasis.get(s)) + 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 @@ -1085,6 +1141,18 @@ def error(self, message): raise RuntimeError(message) +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, + ) + return out_tag_styles + + def ul_prefix(ul_type): if ul_type == "circle": return DEGREE_WIN1252 diff --git a/fpdf/line_break.py b/fpdf/line_break.py index 123754a71..ab4b88636 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -7,8 +7,8 @@ in non-backward-compatible ways. """ -from typing import NamedTuple, Any, List, Optional, Union, Sequence from numbers import Number +from typing import NamedTuple, Any, List, Optional, Union, Sequence from .enums import Align, CharVPos, TextDirection, WrapMode from .errors import FPDFException diff --git a/fpdf/svg.py b/fpdf/svg.py index 9d2b20bdd..503354ca2 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -303,17 +303,17 @@ def optional(value, converter=lambda noop: noop): @force_nodocument def apply_styles(stylable, svg_element): """Apply the known styles from `svg_element` to the pdf path/group `stylable`.""" - html.parse_style(svg_element.attrib) + style = html.parse_css_style(svg_element.attrib.get("style", "")) stylable.style.auto_close = False for attr_name, converter in svg_attr_map.items(): - value = svg_element.attrib.get(attr_name) + value = style.get(attr_name, svg_element.attrib.get(attr_name)) if value: setattr(stylable.style, *converter(value)) # handle this separately for now - opacity = svg_element.attrib.get("opacity") + opacity = style.get("opacity", svg_element.attrib.get("opacity")) if opacity: opacity = float(opacity) stylable.style.fill_opacity = opacity diff --git a/fpdf/text_region.py b/fpdf/text_region.py index 085779bba..658ba6d4c 100644 --- a/fpdf/text_region.py +++ b/fpdf/text_region.py @@ -193,7 +193,7 @@ def build_lines(self, print_sh) -> List[LineWrapper]: self._text_fragments = [] text_line = multi_line_break.get_line() first_line = True - while (text_line) is not None: + while text_line is not None: text_lines.append(LineWrapper(text_line, self, first_line=first_line)) first_line = False text_line = multi_line_break.get_line() @@ -569,11 +569,6 @@ def _render_column_lines(self, text_lines, top, bottom): del text_lines[:rendered_lines] return last_line_height - def _render_lines(self, text_lines, top, bottom): - """Default page rendering a set of lines in one column""" - if text_lines: - self._render_column_lines(text_lines, top, bottom) - def collect_lines(self): text_lines = [] for paragraph in self._paragraphs: diff --git a/test/errors/test_deprecation_warnings.py b/test/errors/test_deprecation_warnings.py new file mode 100644 index 000000000..3c21691e8 --- /dev/null +++ b/test/errors/test_deprecation_warnings.py @@ -0,0 +1,14 @@ +import pytest + + +def test_TitleStyle_deprecation(): + # pylint: disable=import-outside-toplevel + with pytest.warns(DeprecationWarning): + from fpdf import TitleStyle + + TitleStyle() + + with pytest.warns(DeprecationWarning): + from fpdf.fonts import TitleStyle + + TitleStyle() diff --git a/test/fonts/test_add_font.py b/test/fonts/test_add_font.py index 83ef9b914..26e8cb0d7 100644 --- a/test/fonts/test_add_font.py +++ b/test/fonts/test_add_font.py @@ -26,7 +26,7 @@ def test_add_font_pkl(): ) -def test_deprecation_warning_for_FPDF_CACHE_DIR(): +def test_deprecation_warning_for_FPDF_CACHE_DIR_and_FPDF_CACHE_MODE(): # pylint: disable=import-outside-toplevel,pointless-statement,reimported from fpdf import fpdf diff --git a/test/html/html_blockquote_color.pdf b/test/html/html_blockquote_color.pdf index 03d4149e2220045951a41addd77e62d56f575c87..ed19d6962e7431cf24a914cb0c37a58727751ca5 100644 GIT binary patch delta 402 zcmeC+n8rEbH>3H)KQ8sgW?Xi5T*W0tsfoE<6>~y+_i`OF5OMo%bJYA*(cYiRC$(NY z2s^m@627fI^-E7nosSU}|B8A!dd}%-q6wav`&HjU|_UP=0=i zf{}uOLJ*g}XI@&qf(4ZAq7ZH4WM=4OU|{CtX5{2z>SF3_?qp)4g~ cM_5HHB&-vQN-By{)3_{5Ot@54UH#p-08O)p8~^|S delta 346 zcmbQn*}*a4H>26aKQ8q~CR}!QT*W0tsfoE<6>~y+cXKs6h`9Xz+okDST>M>e_clh| zOPt@?Mar2or{%Ex{Ucz=wz~1*8Oi3|NvmwdN`+&rd2>4?Ctc{Os`Xy&$mJ2P5L4q?vKGB;H)00D(O1uihdz`)ep1VhZs%y9C5 zX6YIWF8!eV{1OEt1p|d3E`86uw0s3~DBDFL+Q!Y?+04Sg*wD$v(Z$))$i&6O)x^!f n(!$cv$<5r+&D4&tidaaPB^H%b6s4wdnHw2#sj9mAyKw;kE8K7t diff --git a/test/html/html_blockquote_color_using_FontFace.pdf b/test/html/html_blockquote_color_using_FontFace.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b0dbc49becaa42f8ee256db685ebd08bd7e043df GIT binary patch literal 1170 zcmbtTO=uHA6h0_Q2M^VP2P<_hwH9qAvztwJYb++&EC#EMX(iN9>^7OkEy+yXonR`c z7Zp*&;z|6$_Nd@R=s{7Ww$PI&!GqU&EEGWyEdI>o$1bht!MV)tH}k&tzW2T9Oq*5@ z4yXXbAY@|`AQ}ZI!77wP4A60>;K7_Kjxr+vogf9rjM|L5T`>^ zlLVr)T_hgp>jQ)qB%R`;9hf9Emgf$m4NizGhaZEqQYr}o8QO18u?mN1fRY3&Rm!vu zdr+2_X(mb7+%B=AFO4j5y?V+CKdE6y-8B`yhzWo}!J=%J8?b6Nex#6#uf)svKH`DC^J|^_(UE&|qbZ*PNC9 zijk<>EIEAb3jZZVN_geu|38Pc>$fFRLNtmj45d0T7?uKM5gB0yO1msSNI1+PG2B8a zsS3TRvT!)w*b|LCHR2PY`w}FCpK?%5VL3ct_F^M!U`55)2#R$E8(qO*SGO;?>XLB~ zga(l3^##M4rfP5;u44wQ9YMN|8KP{td>A%1-%}!+v5+ptenYILix;;63;zL&`1!bN zJ0;@Q-u|F-kq8+m!x)G1^@`@E=s1J2J@VUxxu_DtIDwU<5l+UGL{g1~buD6<@tA5F cF)gn4{rg6(^B%WdUTd8m3WCngCJUD2J6=`;7lg(x&t(c^X@s*I&M^jYRWLj4?Gj(?&RXp_K zQ9-3hd$6K+|JiKpHZAB}cK6@;zwdv1|KC;2Sv@2c zM+B>wA3287X{2O%g9%KKveT*gcqk6PN(Yg$o7NmV!^iND3KreeV_FKQ5+s%~;qSo7 zQHqtKJwkod*M}&pNjNP6TNtHmTU9umg_u~e9I+js$+G0{!wK^-Irs51#iA79_Fi1|ZqQw_G}c|czkj-C&Fk@H zcW!tWK5W0XipDk`|efaQq|bkae8a- z^=~tat7m_1T|Kex-j6T8&W{epCXQcyyZK?c@x8KY=5l_+q5MZr3{J`(g^`Bc(j#eR zY@b>j3Z)D?Z# zl3hlFLA307MuMKWj0z>LUckb^P8-MKBqOqxWC6Jn}xD#YBVsvDyP8uREdQZdnx zywE&2sq1ka593)I1S1b-vo;e{*JjfQNUq&x&bR5>eBY!t|DHi$)wSUnDcD18Hd6&L z%(W3!1G%)>NE~)f%O*VAaj6$9=dyE(${Hvo&IOkH6?B$o!yE_Il^;1gluj5q5UH8V sSXMTnWh_0RC*u98k%*gC)-bYt|Gp8-moIEj1eJ%zBvDsaf5Ae30NDP*>Hq)$ literal 0 HcmV?d00001 diff --git a/test/html/html_list_vertical_margin.pdf b/test/html/html_list_vertical_margin.pdf index 380f5f1cd83dc28c085eefffbef80a42c6cbb0b6..a7834cfa84b09a813f96822c12a5e3ffeb5cfc27 100644 GIT binary patch delta 1336 zcmcaFdQo&k2xGl5mz^C~aY<2XVlG$3oT(SidL4EUVSP|L?~%^qXI0PjCwshg3b@c3 zFk>(8jAf#~r!V=TvTwcQ;>A;TYew=sk=w`Y{#Y})NKXD{?18V%O_z@JY&cYVqVMDK zz$IQ&a#n2OSbySh)#b+l+j;$JFDF~>$$7o*?wh>%-*=~VUfp;2&6j$+;{n_M@~#uO zw`i(L{H}OS+5V1n_iatzr)}v^SIeKv)E|Fc>Hi7wD_xD2J`LP=a*AQaGvbV4uLm2TQwt= zA2@z}tH%Cwi8}75t&HA<@2HWOr8`BVsy>! zu*lTEdt=cjSmKbSdPA#n`9u$A!KEgiM<;*Sm(axHv_0fr&HjCp{~7PlpE2inb)j_O zPN5FPv?IFHHd-w6*H+<`+9slP#C^)gO!G%;-&)T(mUkqr>c)2G{ndLj&2GrC#mtWr zn{lgd&mq=F#Z5+!_bNZ$KhwrWsaH0Xao@I(VkzUFry@9iyymq%)j!|h+R0Tik>Ay0 z?)l!8Th!%uPN29#tXJz+LEDVOn=__*wP;GL6c%%K=9*~D zyj-PeJoDYEiD9b6zv{JLKN4QFXzHWML9G4gN&Vt^U{V)g`%pdak@d0DJ2wBHBzn8~ zLeo92YEU$1uJsM75xReU-Neumz0`bb`TdRgCURV^>kd^HiWlxp*`T&mz&zr}qZxL* z!q-#^Kj7H&w+-8uWfw}_VtoCv@cx&(-%5|w^`2dL{B5TB@f-Cehk@xle$te= zr!tEko<4b4`O=Mvyerl$T5vfcFjdxhZv6d)e}3>KPt^gY^!SIL>TSh?$rg zVu+cWnnT3m5EdI+zyy)REKSVNH5(ZiV_0ltXoTTtBO^oO$*x?sv1X2@PR`~AriQN0 z#!jx5md0+T#*U^2hR){ZMkeNlPIfi~Rm4IHgv6qfilWpsE+ZpzQ!Z6iSARDy0O%|- ADF6Tf delta 1369 zcmca8dS7%y2xGkkmz^C~aY<2XVlG$3oT(Si`ZYUN|bt^cFnr#$9L6q)nx-l=|{)BT~UaEk1{P3--@)j3ysaQx7ft1av5kD8ScwKO|+3{jOGe~k2)tLwH24G&!12qe>lSRbi}1^@fP)Rhvg=Tc7I|s zY)uc`xJWloAh<%z)VR~IUReLtviq}NZP}-{ZN=mxUzeKfKk1`&a&nhy@GSXMHP@q) zXQ>8P?L4LRn{j2_tWd7hyp+klOda*chG>DX*YB_aBoK~mi23ur!j*}0o7cVD9Cu7K zmgY{Hrv9)tbJi(^5=pK6eeU-s_8sz8JW*96#xMBYUnppzKu5OCu}22AS)QAmR83f$ zf3z9p-D3Q`%hQ4{^S~-#7*u)z!{FN8qUi6wzc(IQ`L4c?bGIqaJAwP^ddd5Wo*ZF4 zeVlpOW>w(_dv}&`emdsW@%gp%l=$lgVv|#KGJk8w*!5>}pX~CxtMKfDj#k)~M{I`e z>4C%CU@Bk6{&nK~ff zL*jk%1?J{@Q`8{1xDglx0&Ku^@UcJ9@?4qBacv=w4WXazxjk6ssF^!?iw);{ZnYB% zCEicZet&KK(!O_j(@Cp6;^mCn`MJD=jSq=^Uudz^f3b&Y#*GQ<1>yqQV(lN^C=-0H z>34(YugQA-!a2X6mqon4z47&ZvmILw*IPc?;My$Ra{Wl^8B6b5waJwgPEygUY+`a( zJTplvni~3Gy?L32echo5)zzC)-pnufX>r_kibhmv>x{$QD_k%B&^{s)=b{|4XvUst zT8XP{v)+3~C(cz$yq_QSE=5sVeR7maag<)F;^kP?OC2eG|MOErYnUSHX7WOVVX_}< zN4*)oFsSK!djvTQE~x1GANn06oFZ^1sPgmQUw*&*A1@3@$=)Mw&Un3?=h_mHLt@(( zS}Zx=)hTVVN-1Ap9ykovzE$cAp0h#nS7zM&M;71v-)^|KJ^A|noPw%Vd}1@l_WSylQlCTn=5M)k(n)HXr|u+6-RYYq z+&Z12I?3|y_Nk#CIo}1%w1Nb}=8J5dO!XED1|Xo2r@#ef7#NsYSYn7-S{k8?85)=y zV~81B7+{E*m|^NQH#dZ+i$hp!XaN&M60TGG~YGCYQ=4@u}=wjw-;Am$^xsU#MaR1~GAaTys|SaPYV Jy863u0RVRjGh+Y% diff --git a/test/html/html_ln_outside_p.pdf b/test/html/html_ln_outside_p.pdf index a552e647db4349a7c936dd203736948832253d9b..2754e081fc5ccc8fc67abfe6deb69d3c2e4aa49e 100644 GIT binary patch delta 298 zcmZ3$F`r|DBV)Zemz^C~aY<2XVlG$3oYdaaT!#!qT)zM8id;53JI~j1vuMe~brVt@ zo7S2x%6!Yb|H%KXqJoZT=4UJ?C+Ul%vhi;$jCd0<6?VQS=TWNGZ?=w@te>1<+P;pA%RX6a&TXG2g$EF>Tji%KerQq#E1 NO-#8|RbBnvxB$^|ZB_sP delta 313 zcmbQwv4CTPBV)YFhr(&dOfYjj6wZFF0(FiN%M*(SfsS8kl0{F+fJ>~@&Z%xcAEue2a5 zMWN0+QntHJ*WK7BzrRgFp>4iJ^rYv#Q(qkWc{J+Je)n$@=WYj=cijEJxv{%eB0W8S zTI~HvWh+!A4y)&~y>Y5}`K$P3{w(`=$IWh=W0+nu+8QbtfPg}t0vDKJU|?=!fFWjK zXn`SSX=pmRg~d9?)X>n_*vP=#%*E8m(%jI%($U1(*}~P(*vZn-)X>G$&W50hSV)8< W7L`;KrKWLNnwxQ{s=E5SaRC5_?x;2Z diff --git a/test/html/test_html.py b/test/html/test_html.py index 752052554..73a487035 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -2,7 +2,7 @@ import pytest -from fpdf import FPDF, FontFace, HTMLMixin, TitleStyle +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 @@ -195,6 +195,25 @@ def test_html_bold_italic_underline(tmp_path): def test_html_customize_ul(tmp_path): + html = """
      +
    • term1: definition1
    • +
    • term2: definition2
    • +
    """ + pdf = FPDF() + pdf.set_font_size(30) + pdf.add_page() + # Customizing through optional method arguments: + 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)}, + ul_bullet_char=bullet, + ) + pdf.ln() + assert_pdf_equal(pdf, HERE / "html_customize_ul.pdf", tmp_path) + + +def test_html_customize_ul_deprecated(tmp_path): html = """
    • term1: definition1
    • term2: definition2
    • @@ -210,6 +229,16 @@ def test_html_customize_ul(tmp_path): assert_pdf_equal(pdf, HERE / "html_customize_ul.pdf", tmp_path) +def test_html_deprecated_li_tag_indent_deprecated(tmp_path): + pdf = FPDF() + pdf.add_page() + with pytest.warns(DeprecationWarning): + pdf.write_html("
      • item 1
      ", li_tag_indent=40) + pdf.write_html("
      • item 2
      ", li_tag_indent=50) + pdf.write_html("
      • item 3
      ", li_tag_indent=60) + assert_pdf_equal(pdf, HERE / "html_li_tag_indent.pdf", tmp_path) + + def test_html_ol_start_and_type(tmp_path): pdf = FPDF() pdf.set_font_size(30) @@ -235,8 +264,7 @@ def test_html_ul_type(tmp_path):
    • another list item
    • -
    - """ + """ ) pdf.ln() pdf.add_font(fname=HERE / "../fonts/DejaVuSans.ttf") @@ -246,8 +274,7 @@ def test_html_ul_type(tmp_path):
    • a list item
    • another list item
    • -
    - """ + """ ) assert_pdf_equal(pdf, HERE / "html_ul_type.pdf", tmp_path) @@ -289,8 +316,7 @@ def test_html_align_paragraph(tmp_path):

    {LOREM_IPSUM[600:800]}"

    align=invalid, ignore and default left: -

    {LOREM_IPSUM[800:1000]}"

    - """ +

    {LOREM_IPSUM[800:1000]}"

    """ ) assert_pdf_equal(pdf, HERE / "html_align_paragraph.pdf", tmp_path) @@ -340,18 +366,52 @@ def test_html_headings_line_height(tmp_path): # issue-223 long_title = "The Quick Brown Fox Jumped Over The Lazy Dog " pdf.write_html( f""" -

    H1 {long_title*2}

    -

    H2 {long_title*2}

    -

    H3 {long_title*2}

    -

    H4 {long_title*3}

    -
    H5 {long_title*3}
    -
    H6 {long_title*4}
    -

    P {long_title*5}

    """ +

    H1 {long_title*2}

    +

    H2 {long_title*2}

    +

    H3 {long_title*2}

    +

    H4 {long_title*3}

    +
    H5 {long_title*3}
    +
    H6 {long_title*4}
    +

    P {long_title*5}

    """ ) assert_pdf_equal(pdf, HERE / "html_headings_line_height.pdf", tmp_path) def test_html_custom_heading_sizes(tmp_path): # issue-223 + pdf = FPDF() + pdf.add_page() + pdf.write_html( + """

    This is a H1

    +

    This is a H2

    +

    This is a H3

    +

    This is a H4

    +
    This is a H5
    +
    This is a H6
    """, + tag_styles={ + "h1": TextStyle( + color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=6 + ), + "h2": TextStyle( + color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=12 + ), + "h3": TextStyle( + color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=18 + ), + "h4": TextStyle( + color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=24 + ), + "h5": TextStyle( + color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=30 + ), + "h6": TextStyle( + color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=36 + ), + }, + ) + assert_pdf_equal(pdf, HERE / "html_custom_heading_sizes.pdf", tmp_path) + + +def test_html_custom_heading_sizes_deprecated(tmp_path): # issue-223 pdf = FPDF() pdf.add_page() with pytest.warns(DeprecationWarning): @@ -382,13 +442,12 @@ def test_html_description(tmp_path): pdf.add_page() pdf.write_html( """ -
    description title
    -
    description details
    -
    -
    description title
    -
    description details
    -
    - """ +
    description title
    +
    description details
    +
    +
    description title
    +
    description details
    +
    """ ) assert_pdf_equal(pdf, HERE / "html_description.pdf", tmp_path) @@ -412,8 +471,7 @@ class PDF(FPDF, HTMLMixin):
    description title
    description details
    -
    - """ + """ ) assert_pdf_equal(pdf, HERE / "html_description.pdf", tmp_path) @@ -529,6 +587,17 @@ def test_html_unorthodox_headings_hierarchy(tmp_path): # issue 631 def test_html_custom_pre_code_font(tmp_path): # issue 770 + pdf = FPDF() + pdf.add_font(fname=HERE / "../fonts/DejaVuSansMono.ttf") + pdf.add_page() + pdf.write_html( + " Cześć! ", + tag_styles={"code": TextStyle(font_family="DejaVuSansMono")}, + ) + assert_pdf_equal(pdf, HERE / "html_custom_pre_code_font.pdf", tmp_path) + + +def test_html_custom_pre_code_font_deprecated(tmp_path): # issue 770 pdf = FPDF() pdf.add_font(fname=HERE / "../fonts/DejaVuSansMono.ttf") pdf.add_page() @@ -551,11 +620,11 @@ def test_html_heading_color_attribute(tmp_path): # discussion 880 pdf.add_page() pdf.write_html( """ -

    Title

    - Content -

    Subtitle in green

    - Content - """ +

    Title

    + Content +

    Subtitle in green

    + Content + """ ) assert_pdf_equal(pdf, HERE / "html_heading_color_attribute.pdf", tmp_path) @@ -571,7 +640,7 @@ def test_html_format_within_p(tmp_path): # discussion 880 in the PDF. This is a sample text that will be justified in the PDF. This is a sample text that will be justified in the PDF. This is a sample text that will be justified in the PDF.

    - """ + """ ) assert_pdf_equal(pdf, HERE / "html_format_within_p.pdf", tmp_path) @@ -581,7 +650,7 @@ def test_html_bad_font(): pdf.add_page() pdf.set_font("times", size=18) with pytest.raises(FPDFException): - pdf.write_html("""

    hello helvetica

    """) + pdf.write_html('

    hello helvetica

    ') def test_html_ln_outside_p(tmp_path): @@ -591,9 +660,7 @@ def test_html_ln_outside_p(tmp_path): pdf.add_page() pdf.set_font("times", size=18) pdf.write_html( - """ -

    someting in paragraph

  • causing _ln() outside paragraph
  • - """ + "

    someting in paragraph

  • causing _ln() outside paragraph
  • " ) assert_pdf_equal(pdf, HERE / "html_ln_outside_p.pdf", tmp_path) @@ -602,15 +669,32 @@ def test_html_and_section_title_styles(): # issue 1080 pdf = FPDF() pdf.add_page() pdf.set_font("Helvetica", size=10) - pdf.set_section_title_styles(TitleStyle("Helvetica", "B", 20, (0, 0, 0))) + pdf.set_section_title_styles(TextStyle("Helvetica", "B", 20, (0, 0, 0))) with pytest.raises(NotImplementedError): pdf.write_html( """ -

    Heading One

    -

    Just enough text to show how bad the situation really is

    -

    Heading Two

    -

    This will not overflow

    - """ +

    Heading One

    +

    Just enough text to show how bad the situation really is

    +

    Heading Two

    +

    This will not overflow

    + """ + ) + + +def test_html_and_section_title_styles_with_deprecated_TitleStyle(): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica", size=10) + with pytest.warns(DeprecationWarning): + pdf.set_section_title_styles(TitleStyle("Helvetica", "B", 20, (0, 0, 0))) + with pytest.raises(NotImplementedError): + pdf.write_html( + """ +

    Heading One

    +

    Just enough text to show how bad the situation really is

    +

    Heading Two

    +

    This will not overflow

    + """ ) @@ -618,7 +702,7 @@ def test_html_link_color(tmp_path): pdf = FPDF() pdf.add_page() html = 'foo' - pdf.write_html(html, tag_styles={"a": FontFace(color=color_as_decimal("red"))}) + pdf.write_html(html, tag_styles={"a": TextStyle(color=color_as_decimal("red"))}) assert_pdf_equal(pdf, HERE / "html_link_color.pdf", tmp_path) @@ -626,11 +710,58 @@ def test_html_blockquote_color(tmp_path): pdf = FPDF() pdf.add_page() html = "Text before
    foo
    Text afterwards" - pdf.write_html(html, tag_styles={"blockquote": FontFace(color=(125, 125, 0))}) + blockquote_style = TextStyle( + color=(125, 125, 0), font_style="ITALICS", t_margin=3, b_margin=3, l_margin=10 + ) + pdf.write_html(html, tag_styles={"blockquote": blockquote_style}) assert_pdf_equal(pdf, HERE / "html_blockquote_color.pdf", tmp_path) def test_html_headings_color(tmp_path): + pdf = FPDF() + pdf.add_page() + html = "

    foo

    bar

    " + pdf.write_html( + html, + tag_styles={ + "h1": TextStyle( + color=(148, 139, 139), font_size_pt=24, t_margin=0.2, b_margin=0.4 + ), + "h2": TextStyle( + color=(148, 139, 139), font_size_pt=18, t_margin=0.2, b_margin=0.4 + ), + }, + ) + assert_pdf_equal(pdf, HERE / "html_headings_color.pdf", tmp_path) + + +def test_html_unsupported_tag_color(): + pdf = FPDF() + pdf.add_page() + with pytest.raises(NotImplementedError): + pdf.write_html("

    foo

    ", tag_styles={"p": TextStyle()}) + + +def test_html_link_color_using_FontFace(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) + + +def test_html_blockquote_color_using_FontFace(tmp_path): + pdf = FPDF() + pdf.add_page() + html = "Text before
    foo
    Text afterwards" + pdf.write_html( + html, + tag_styles={"blockquote": FontFace(color=(125, 125, 0), emphasis="ITALICS")}, + ) + assert_pdf_equal(pdf, HERE / "html_blockquote_color_using_FontFace.pdf", tmp_path) + + +def test_html_headings_color_using_FontFace(tmp_path): pdf = FPDF() pdf.add_page() html = "

    foo

    bar

    " @@ -644,7 +775,7 @@ def test_html_headings_color(tmp_path): assert_pdf_equal(pdf, HERE / "html_headings_color.pdf", tmp_path) -def test_html_unsupported_tag_color(): +def test_html_unsupported_tag_color_using_FontFace(): pdf = FPDF() pdf.add_page() with pytest.raises(NotImplementedError): @@ -655,7 +786,14 @@ def test_html_blockquote_indent(tmp_path): # issue-1074 pdf = FPDF() pdf.add_page() html = "Text before
    foo
    Text afterwards" - pdf.write_html(html, tag_indents={"blockquote": 20}) + pdf.write_html( + html, + tag_styles={ + "blockquote": TextStyle( + color=(100, 0, 45), t_margin=3, b_margin=3, l_margin=20 + ) + }, + ) html = ( "
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," @@ -664,55 +802,70 @@ def test_html_blockquote_indent(tmp_path): # issue-1074 "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident," "sunt in culpa qui officia deserunt mollit anim id est laborum.
    " ) - pdf.write_html(html, tag_indents={"blockquote": 40}) + pdf.write_html( + html, + tag_styles={ + "blockquote": TextStyle( + color=(100, 0, 45), t_margin=3, b_margin=3, l_margin=40 + ) + }, + ) assert_pdf_equal(pdf, HERE / "html_blockquote_indent.pdf", tmp_path) -def test_html_li_tag_indent(tmp_path): +def test_html_blockquote_indent_using_deprecated_tag_indents(tmp_path): # issue-1074 pdf = FPDF() pdf.add_page() + html = "Text before
    foo
    Text afterwards" with pytest.warns(DeprecationWarning): - pdf.write_html("
    • item 1
    ", li_tag_indent=40) - pdf.write_html("
    • item 2
    ", li_tag_indent=50) - pdf.write_html("
    • item 3
    ", li_tag_indent=60) - assert_pdf_equal(pdf, HERE / "html_li_tag_indent.pdf", tmp_path) + pdf.write_html(html, tag_indents={"blockquote": 20}) + html = ( + "
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu" + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident," + "sunt in culpa qui officia deserunt mollit anim id est laborum.
    " + ) + with pytest.warns(DeprecationWarning): + pdf.write_html(html, tag_indents={"blockquote": 40}) + assert_pdf_equal(pdf, HERE / "html_blockquote_indent.pdf", tmp_path) def test_html_ol_ul_line_height(tmp_path): pdf = FPDF() pdf.add_page() pdf.write_html( - """ -

    Default line-height:

    -
      -
    • item
    • -
    • item
    • -
    • item
    • -
    -

    1.5 line-height:

    -
      -
    1. item
    2. -
    3. item
    4. -
    5. item
    6. -
    -

    Double line-height:

    -
      -
    • item
    • -
    • item
    • -
    • item
    • -
    -

    1.5 line-height as "style":

    -
      -
    1. item
    2. -
    3. item
    4. -
    5. item
    6. -
    -

    Double line-height as "style":

    -
      -
    • item
    • -
    • item
    • -
    • item
    • -
    """ + """

    Default line-height:

    +
      +
    • item
    • +
    • item
    • +
    • item
    • +
    +

    1.5 line-height:

    +
      +
    1. item
    2. +
    3. item
    4. +
    5. item
    6. +
    +

    Double line-height:

    +
      +
    • item
    • +
    • item
    • +
    • item
    • +
    +

    1.5 line-height as "style":

    +
      +
    1. item
    2. +
    3. item
    4. +
    5. item
    6. +
    +

    Double line-height as "style":

    +
      +
    • item
    • +
    • item
    • +
    • item
    • +
    """ ) assert_pdf_equal(pdf, HERE / "html_ol_ul_line_height.pdf", tmp_path) @@ -729,23 +882,46 @@ def test_html_long_ol_bullets(tmp_path): pdf = FPDF() pdf.add_page() html_arabic_indian = f""" -
      -
    1. Item 1
    2. -
    3. Item 2
    4. -
    5. Item 3
    6. -
    - """ +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    5. Item 3
    6. +
    """ + pdf.write_html(html_arabic_indian) html_roman = f""" -
      -
    1. Item 1
    2. -
    3. Item 2
    4. -
    5. Item 3
    6. -
    - """ +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    5. Item 3
    6. +
    """ + pdf.write_html(html_roman) + pdf.write_html( + html_arabic_indian, tag_styles={"li": TextStyle(l_margin=50, t_margin=2)} + ) + pdf.write_html(html_roman, tag_styles={"li": TextStyle(l_margin=100, t_margin=2)}) + assert_pdf_equal(pdf, HERE / "html_long_ol_bullets.pdf", tmp_path) + + +def test_html_long_ol_bullets_deprecated(tmp_path): + pdf = FPDF() + pdf.add_page() + html_arabic_indian = f""" +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    5. Item 3
    6. +
    """ pdf.write_html(html_arabic_indian) - pdf.write_html(html_roman, type="i") - pdf.write_html(html_arabic_indian, tag_indents={"li": 50}) - pdf.write_html(html_roman, tag_indents={"li": 100}) + html_roman = f""" +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    5. Item 3
    6. +
    """ + pdf.write_html(html_roman) + with pytest.warns(DeprecationWarning): + pdf.write_html(html_arabic_indian, tag_indents={"li": 50}) + pdf.write_html(html_roman, tag_indents={"li": 100}) assert_pdf_equal(pdf, HERE / "html_long_ol_bullets.pdf", tmp_path) @@ -754,20 +930,19 @@ def test_html_measurement_units(tmp_path): pdf = FPDF(unit=unit) pdf.add_page() html = """ -
      -
    • Item 1
    • -
    • Item 2
    • -
    • Item 3
    • -
    -
      -
    1. Item 1
    2. -
    3. Item 2
    4. -
    5. Item 3
    6. -
    -
    Blockquote text
    -
    Description title
    -
    Description details
    - """ +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    5. Item 3
    6. +
    +
    Blockquote text
    +
    Description title
    +
    Description details
    """ pdf.write_html(html) assert_pdf_equal(pdf, HERE / "html_measurement_units.pdf", tmp_path) @@ -828,8 +1003,8 @@ def test_html_list_vertical_margin(tmp_path): for margin_value in (None, 4, 8, 16): pdf.add_page() html = f""" - This page uses `list_vertical_margin` value of {margin_value} -
      + This page uses `t_margin={margin_value}` for <ul> tags: +
      • Item 1
      • Item 2
      • Item 3
      • @@ -838,9 +1013,14 @@ def test_html_list_vertical_margin(tmp_path):
      • Item 1
      • Item 2
      • Item 3
      • - - """ - pdf.write_html(html, list_vertical_margin=margin_value) + """ + pdf.write_html( + html, + tag_styles={ + "ol": TextStyle(t_margin=margin_value), + "ul": TextStyle(t_margin=margin_value), + }, + ) assert_pdf_equal(pdf, HERE / "html_list_vertical_margin.pdf", tmp_path) @@ -871,3 +1051,22 @@ def test_html_page_break_after(tmp_path): Content on third page.""" ) assert_pdf_equal(pdf, HERE / "html_page_break_after.pdf", tmp_path) + + +def test_html_heading_above_below(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.write_html( + """ +

        Top heading

        +

        Lorem ipsum

        +

        First heading

        +

        Lorem ipsum

        +

        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), + }, + ) + assert_pdf_equal(pdf, HERE / "html_heading_above_below.pdf", tmp_path) diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index aa07bb535..6755ba33b 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -2,7 +2,7 @@ import pytest -from fpdf import FPDF, TitleStyle, errors +from fpdf import FPDF, TextStyle, TitleStyle, errors from test.conftest import assert_pdf_equal @@ -77,7 +77,7 @@ def test_2_pages_outline(tmp_path): pdf.set_font("Helvetica") pdf.set_section_title_styles( # Level 0 titles: - TitleStyle( + TextStyle( font_family="Times", font_style="B", font_size_pt=24, @@ -107,6 +107,42 @@ def test_2_pages_outline(tmp_path): assert_pdf_equal(pdf, HERE / "2_pages_outline.pdf", tmp_path) +def test_2_pages_outline_with_deprecated_TitleStyle(tmp_path): + pdf = FPDF() + pdf.set_font("Helvetica") + with pytest.warns(DeprecationWarning): + pdf.set_section_title_styles( + # Level 0 titles: + TitleStyle( + font_family="Times", + font_style="B", + font_size_pt=24, + color=128, + underline=True, + t_margin=10, + l_margin=10, + b_margin=0, + ), + ) + + pdf.add_page() + pdf.set_y(50) + pdf.set_font(size=40) + p(pdf, "Doc Title", align="C") + pdf.set_font(size=12) + pdf.insert_toc_placeholder(render_toc, pages=2) + for i in range(40): + pdf.start_section(f"Title {i}") + p( + pdf, + ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit," + " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + ), + ) + assert_pdf_equal(pdf, HERE / "2_pages_outline.pdf", tmp_path) + + def test_toc_with_nb_and_footer(tmp_path): # issue-548 class TestPDF(FPDF): def render_toc(self, outline): @@ -161,7 +197,7 @@ def test_toc_without_font_style(tmp_path): # issue-676 pdf = FPDF() pdf.set_font("helvetica") pdf.set_section_title_styles( - level0=TitleStyle(font_size_pt=28, l_margin=10), level1=TitleStyle() + level0=TextStyle(font_size_pt=28, l_margin=10), level1=TextStyle() ) pdf.add_page() pdf.start_section("Title") @@ -174,7 +210,7 @@ def test_toc_with_font_style_override_bold(tmp_path): # issue-1072 pdf.add_page() pdf.set_font("Helvetica", "B") pdf.set_section_title_styles( - TitleStyle("Helvetica", font_size_pt=20, color=(0, 0, 0)) + TextStyle("Helvetica", font_size_pt=20, color=(0, 0, 0)) ) pdf.start_section("foo") assert_pdf_equal(pdf, HERE / "toc_with_font_style_override_bold1.pdf", tmp_path) @@ -183,12 +219,47 @@ def test_toc_with_font_style_override_bold(tmp_path): # issue-1072 pdf.add_page() pdf.set_font("Helvetica", "B") pdf.set_section_title_styles( - TitleStyle("Helvetica", font_style="", font_size_pt=20, color=(0, 0, 0)) + TextStyle("Helvetica", font_style="", font_size_pt=20, color=(0, 0, 0)) ) pdf.start_section("foo") assert_pdf_equal(pdf, HERE / "toc_with_font_style_override_bold2.pdf", tmp_path) +def test_toc_without_font_style_with_deprecated_TitleStyle(tmp_path): + pdf = FPDF() + pdf.set_font("helvetica") + with pytest.warns(DeprecationWarning): + pdf.set_section_title_styles( + level0=TitleStyle(font_size_pt=28, l_margin=10), level1=TitleStyle() + ) + pdf.add_page() + pdf.start_section("Title") + pdf.start_section("Subtitle", level=1) + assert_pdf_equal(pdf, HERE / "toc_without_font_style.pdf", tmp_path) + + +def test_toc_with_font_style_override_bold_with_deprecated_TitleStyle(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica", "B") + with pytest.warns(DeprecationWarning): + pdf.set_section_title_styles( + TitleStyle("Helvetica", font_size_pt=20, color=(0, 0, 0)) + ) + pdf.start_section("foo") + assert_pdf_equal(pdf, HERE / "toc_with_font_style_override_bold1.pdf", tmp_path) + + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica", "B") + with pytest.warns(DeprecationWarning): + pdf.set_section_title_styles( + TitleStyle("Helvetica", font_style="", font_size_pt=20, color=(0, 0, 0)) + ) + pdf.start_section("foo") + assert_pdf_equal(pdf, HERE / "toc_with_font_style_override_bold2.pdf", tmp_path) + + def test_toc_with_table(tmp_path): # issue-1079 def render_toc_with_table(pdf: FPDF, outline: list): pdf.set_font(size=20) @@ -242,7 +313,7 @@ def p(pdf, text, **kwargs): def insert_test_content(pdf): pdf.set_section_title_styles( # Level 0 titles: - TitleStyle( + TextStyle( font_family="Times", font_style="B", font_size_pt=24, @@ -253,7 +324,7 @@ def insert_test_content(pdf): b_margin=0, ), # Level 1 subtitles: - TitleStyle( + TextStyle( font_family="Times", font_style="B", font_size_pt=20, @@ -293,3 +364,60 @@ def insert_test_content(pdf): pdf, "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", ) + + +def insert_test_content_with_deprecated_TitleStyle(pdf): + with pytest.warns(DeprecationWarning): + pdf.set_section_title_styles( + # Level 0 titles: + TitleStyle( + font_family="Times", + font_style="B", + font_size_pt=24, + color=128, + underline=True, + t_margin=10, + l_margin=10, + b_margin=0, + ), + # Level 1 subtitles: + TitleStyle( + font_family="Times", + font_style="B", + font_size_pt=20, + color=128, + underline=True, + t_margin=10, + l_margin=20, + b_margin=5, + ), + ) + + pdf.start_section("Title 1") + pdf.start_section("Subtitle 1.1", level=1) + p( + pdf, + ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit," + " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + ), + ) + pdf.add_page() + pdf.start_section("Subtitle 1.2", level=1) + p( + pdf, + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + ) + pdf.add_page() + pdf.start_section("Title 2") + pdf.start_section("Subtitle 2.1", level=1) + p( + pdf, + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + ) + pdf.add_page() + pdf.start_section("Subtitle 2.2", level=1) + p( + pdf, + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + ) diff --git a/test/test_enums.py b/test/test_enums.py new file mode 100644 index 000000000..b5650e55b --- /dev/null +++ b/test/test_enums.py @@ -0,0 +1,36 @@ +import pytest + +from fpdf.enums import TextEmphasis + + +def test_text_emphasis_coerce(): + assert TextEmphasis.coerce("B") == TextEmphasis.B + assert TextEmphasis.coerce("BOLD") == TextEmphasis.B + assert TextEmphasis.coerce("I") == TextEmphasis.I + assert TextEmphasis.coerce("italics") == TextEmphasis.I + assert TextEmphasis.coerce("U") == TextEmphasis.U + assert TextEmphasis.coerce("Underline") == TextEmphasis.U + with pytest.raises(ValueError): + assert TextEmphasis.coerce("BXXX") + assert ( + TextEmphasis.coerce("BIU") == TextEmphasis.B | TextEmphasis.I | TextEmphasis.U + ) + + +def test_text_emphasis_style(): + assert TextEmphasis.coerce("B").style == "B" + assert TextEmphasis.coerce("IB").style == "BI" + assert TextEmphasis.coerce("BIU").style == "BIU" + + +def test_text_emphasis_add(): + assert TextEmphasis.B.add(TextEmphasis.I).add( + TextEmphasis.U + ) == TextEmphasis.coerce("BIU") + + +def test_text_emphasis_remove(): + assert ( + TextEmphasis.coerce("BIU").remove(TextEmphasis.B).remove(TextEmphasis.I) + == TextEmphasis.U + ) diff --git a/test/errors/test_page_format.py b/test/test_page_format.py similarity index 80% rename from test/errors/test_page_format.py rename to test/test_page_format.py index 06e560b2d..593605c32 100644 --- a/test/errors/test_page_format.py +++ b/test/test_page_format.py @@ -4,6 +4,12 @@ from fpdf.fpdf import get_page_format +def test_page_format_ok(): + assert get_page_format("a4") == (595.28, 841.89) + assert get_page_format("letter") == (612, 792) + assert get_page_format((297, 210), k=2) == (594, 420) + + def test_page_format_error(): with pytest.raises(FPDFPageFormatException) as error: get_page_format("letter1")