From 5cbdeca27737e74ed7dc4f5bc1de0e9b9372b047 Mon Sep 17 00:00:00 2001 From: dmail00 Date: Wed, 20 Dec 2023 21:23:16 +0000 Subject: [PATCH 1/6] Allow users to set HTML links colors --- fpdf/fpdf.py | 7 +++++ fpdf/html.py | 39 ++++++++++++++++++++++--- test/html/html_blockquote.pdf | Bin 0 -> 1030 bytes test/html/html_headings_color.pdf | Bin 0 -> 1371 bytes test/html/html_link_color.pdf | Bin 0 -> 1139 bytes test/html/html_ordered_li_color.pdf | Bin 0 -> 995 bytes test/html/html_unordered_li_color.pdf | Bin 0 -> 995 bytes test/html/test_html.py | 40 ++++++++++++++++++++++++++ 8 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 test/html/html_blockquote.pdf create mode 100644 test/html/html_headings_color.pdf create mode 100644 test/html/html_link_color.pdf create mode 100644 test/html/html_ordered_li_color.pdf create mode 100644 test/html/html_unordered_li_color.pdf diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index c6af49f3a..2ea1e53da 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -409,6 +409,13 @@ def write_html(self, text, *args, **kwargs): heading_sizes (dict): font size per heading level names ("h1", "h2"...) pre_code_font (str): font to use for
 &  blocks
             warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags
+            element_colors (dict): dictionary of colors for elements. Possible keys:
+            * "link" sets the link color for a href tag
+            * "li" sets the bullet or number for 
  • tags + * "blockquote" sets the color for
    tags + * "headings" sets the color is not specified in a h1, h2 etc. heading tag + + The values for the dictionary are a tuple of three ints, being the r,g and b values """ kwargs2 = vars(self) # Method arguments must override class & instance attributes: diff --git a/fpdf/html.py b/fpdf/html.py index 9f6cb475e..592a65dae 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -5,7 +5,7 @@ They may change at any time without prior warning or any deprecation period, in non-backward-compatible ways. """ - +from __future__ import annotations from html.parser import HTMLParser import logging, re, warnings @@ -236,6 +236,7 @@ def __init__( heading_sizes=None, pre_code_font="courier", warn_on_tags_not_matching=True, + element_colors: dict[str, tuple[int, int, int]] | None = None, **_, ): """ @@ -309,6 +310,34 @@ 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 + self.link_color: tuple[int, int, int] + self.li_color: tuple[int, int, int] + self.blockquote_color: tuple[int, int, int] + self.headings_color: tuple[int, int, int] + self._set_color_scheme(element_colors) + + def _set_color_scheme( + self, element_colors: dict[str, tuple[int, int, int]] | None + ) -> None: + _old_default_colors: dict[str, tuple[int, int, int]] = { + "link": (0, 0, 255), + "li": (190, 0, 0), + "blockquote": (100, 0, 45), + "headings": (150, 0, 0), + } + + if element_colors is None: + element_colors = {} + + self.link_color = element_colors.get("link", _old_default_colors["link"]) + self.li_color = element_colors.get("li", _old_default_colors["li"]) + self.blockquote_color = element_colors.get( + "blockquote", _old_default_colors["blockquote"] + ) + self.headings_color = element_colors.get( + "headings", _old_default_colors["headings"] + ) + def _new_paragraph( self, align=None, line_height=1.0, top_margin=0, bottom_margin=0 ): @@ -488,7 +517,9 @@ def handle_starttag(self, tag, attrs): bottom_margin=self.heading_below * hsize, ) color = ( - color_as_decimal(attrs["color"]) if "color" in attrs else (150, 0, 0) + color_as_decimal(attrs["color"]) + if "color" in attrs + else self.headings_color ) self.set_text_color(*color) self.set_font(size=hsize_pt) @@ -504,7 +535,7 @@ def handle_starttag(self, tag, attrs): self._new_paragraph() self._pre_started = True if tag == "blockquote": - self.set_text_color(100, 0, 45) + self.set_text_color(*self.blockquote_color) self.indent += 1 self._new_paragraph(top_margin=3, bottom_margin=3) if tag == "ul": @@ -797,7 +828,7 @@ def set_text_color(self, r=None, g=0, b=0): def put_link(self, text): # Put a hyperlink - self.set_text_color(0, 0, 255) + self.set_text_color(*self.link_color) self.set_style("u", True) self._write_paragraph(text, link=self.href) self.set_style("u", False) diff --git a/test/html/html_blockquote.pdf b/test/html/html_blockquote.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a2b22f5547a9c5b4756f1642f41e22d9a6b3e41a GIT binary patch literal 1030 zcmah|J!lj`6efi!#@0f#nIbKt?rw6f?9GJTS-2=^ zlAtz1#7+Y?Dz=K)T%i_rqF5Oe#6nP;An8Pc&fJe#lEP_b_j~hw@4au{jFdfpLY>S2 z6)KR`u7G?VAeSYSLlvOupb^7rMuaMh0G%U^06VM=tBOE$OP|zCXlB(((-P6Sm1P1^ z88=A`3I#xDL&|9>*_mZR?K%$_?Q25h2mB1ANf-(NNiE?OmT-t>C~;Ysv}g~Gp)9X5 zNaA9SL)J{CktOa|PdTAHhS?scj3e4KXJk zsj~(Fs6=R!UxuotNmk4wf?J@yHGiuE)=wYvuDz(8ZPA@VS1&jhf9KzPdo|3Gv1w^{zlgr^da45l$HZsC12h-3 zNIX$tEi9ZZQqfC5n-DDo)TVJT005dzYP@@&@>azl^e$Iwq)d_q{xeC+=kv%{q0C$n zrl4}f>a&EyDiT8`a+9{GV5%jYcKW;9-=`K+vbmJ3D}0p$;|c5Gn0HFG3`^BAs%j}> zomZ_d24<50dBG3irui9u5~()yuuUd$WiwNDCzyH? z@gGoBiVD4W5d?3Zy?FCb1jVZ!R6O}7_)W4&)>LpWyZiQ=_r7`azD-pecZp;)M6ina z_HC5UBc;fDOkjePHNO?%h6bOiTS&P|TfSG|ecVt1F>E7~wXmg=jAg^u`tAtkd-Y@cEbc^BEtHJH-y&_LzPCn7dAoGOj62yB>3dg5ufV^ zf`-t0;0fx`CT~%stW(wzw=hw2a#bWk>UB~7g;xFjEqkCD8|fb*x^{3!tE>7)O|2X+ z)pkyNJzt+er%wM`*i7#=p8Z^So__E$J@ce7zjrS^S1Zn6e|++D&^q$z^~0z8Z(hwl zI;O9loqd01Ztc?A{qOUe?>>~STqx`vo!S4AdMo{oRb%IkM9T#)qArI`D|Nq1qopeE zdSK-$gXVqK!ODiuR#@bZ0HoA<+hTB_(qTx#&}hk#a78*cc16s`HyJFQFQ$<64;W|;I=n;9(FbgPl86)9-?t5 ztQ;m9D>rU#LXAbVjG>QGIMlHdI`MPG8^k6KXWbV8#pyQZ=Ms{#^lwQODgThny~WJiUYKMia^%L;kS;k!4g?UVor}aaZIaapu~oP>iM^<*i^T8m6ugNC@#J6NO}wb!xBr0$5y4|Uco*Mn61&ucbJ(4EZ|1$<@AuwIR4sdoB-08( zD&i}bm3&^oCEjHM5e1*|8X;OrgUeY~@LAgM+#>IxB^4~1p(Rr~(laEf8{l17?@)xR zZi|LWp`cLKkl|9Xu=5?t%)0P68^lD|_QWh=UEhZQ@@d6g=UsvDJfkJ092*&W+8{?>8^KT_Kg_2k*8vuZ`SG zG*@3g{ZYI>H#;^qeKap_Z+zO`xA^h-ohLKD-#b4?zr9#}RJ=NUI<$rv#yq)dOM zkl~@SHbFA8Ba2uKc)doUWEeUca?>6lD7Qh+;^1Ez{L9O+u(?>+638dJ(*l9fgmsb_ zS%aj~gcvHUi^LdL)$z&L<6c0Uii&ha`uhuIH7%{7CfczXC~_gTV>5t_TrnFl{=JtW z&;YycX*!J5E?ZWI@z`a{W%V7K2wcynK{VVo?>Ys@*x_6tIi4_9W!B_KN+I_aj1D1M t$<)nk#?n<)Cpp73P1vN1X+t$E+baD3Mx=5m+(1M!=_x}=BxdGqLHz--Rx#oLLk}AYG|b)DGG)jw#hVE-OQBTiKL!9 z=%F_c9u*{@9*R`|3lD;L7QuhOiuO`(&g6$(?4fg+-S5r&-uJ$FvrzYg99gmu0RzOw zeN-$W?1=%FKoDMzIw`a)6>6*_e4BNmup)-gG8AIF_L50~=E)LuRdj88ivg~OU6!I! z2{GQ$bY_Njb&K(GTSkIUIROWeyajv^$BICwu7ulSAOWv(=81UF=My*uEw3>v;)ODd zMK@DMOFUA~CF3%M{3NFyChB*_&|s+;ByE<0lcm(fGkiLA#ur4fWQo!mhm!fMEjkS0 z6~?==2bAihR3R$Y{Lc@c;{DOa-e`wjJ#+T!uilSR@9@oB?%|!?=gWUS$MOEDC%@0_YA>fH zrmorxpPLnS>Tr6QpXhp;A!mD;q1*-J9;U^$cwxL+dHm^fdU& zSGeZiAg<#Q(;~z*)VfF9MZ;LUo=F}iY!ewkk&aImIJRxsun9+)3*!**Bg|FZ=#{~U zd;FYPIF98ldMwwrR2%>CIi{MK877l3idiy#OwH&KQ<8B*2nqT*s4=ebO##{p{p~}m x=-8A7)qJH)y{en{e9N$?;T4R6VG+ml^QDtdj9pG;n8>kB4z=7Zk{X=5VqZkc zh=75=03=5Kif$d*qAtv^fS3^c1Ki~YvBjEW91ARY*WwKj*Pa?PATAKFr*18l@Mip zMQ5aF8>f_&dm`j)!3o$4#RFjDD3Sy!bw4=cV*$9qsLP}Ah|S>ylstb~#0^9c@nNcr zlDJaO1Z5%tbDq--V)?sd@M*%wagQdDPgAPmDL$3D^$NLoFeaeR7 z1Z-O+B_gK52u(f^|L0d@O02`|w-_BvpOdy`UJ|XjRd1bM{c@+hfo|OVvD3-E>vq2H zyubVEV|F{cz4`3m{NG%LZ{f9MNRY=c7Q{R%23V$cBIqLsn)KQ*Nu^Mz rnVw@CR@t;G2FcsCvQ}^_hFdjB>HjBYE+-<0#muI>ZJ|u2*6`3DGIbBf literal 0 HcmV?d00001 diff --git a/test/html/test_html.py b/test/html/test_html.py index 7d8a0355c..1cbb593b2 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -561,3 +561,43 @@ def test_html_and_section_title_styles(): # issue 1080

    This will not overflow

    """ ) + + +def test_html_link_color(tmp_path): + pdf = FPDF() + pdf.add_page() + text = 'foo' + pdf.write_html(text, element_colors={"link": (255, 0, 0)}) + assert_pdf_equal(pdf, HERE / "html_link_color.pdf", tmp_path) + + +def test_html_unordered_li_color(tmp_path): + pdf = FPDF() + pdf.add_page() + text = "
    • foo
    " + pdf.write_html(text, element_colors={"li": (0, 255, 0)}) + assert_pdf_equal(pdf, HERE / "html_unordered_li_color.pdf", tmp_path) + + +def test_html_ordered_li_color(tmp_path): + pdf = FPDF() + pdf.add_page() + text = "
    1. foo
    " + pdf.write_html(text, element_colors={"li": (0, 255, 0)}) + assert_pdf_equal(pdf, HERE / "html_ordered_li_color.pdf", tmp_path) + + +def test_html_blockquote_color(tmp_path): + pdf = FPDF() + pdf.add_page() + text = "Text before
    foo
    Text afterwards" + pdf.write_html(text, element_colors={"blockquote": (125, 125, 0)}) + assert_pdf_equal(pdf, HERE / "html_blockquote.pdf", tmp_path) + + +def test_html_headings_color(tmp_path): + pdf = FPDF() + pdf.add_page() + text = "

    foo

    bar

    " + pdf.write_html(text, element_colors={"headings": (148, 139, 139)}) + assert_pdf_equal(pdf, HERE / "html_headings_color.pdf", tmp_path) From 5a142ce930a2f5916e08380182a50d1bcbf96d24 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:28:22 +0100 Subject: [PATCH 2/6] [HTML] Introducing tag_indents & tag_styles --- CHANGELOG.md | 8 +- docs/HTML.md | 39 +++ fpdf/drawing.py | 16 +- fpdf/fpdf.py | 27 +- fpdf/html.py | 291 ++++++++++++------ ...ockquote.pdf => html_blockquote_color.pdf} | Bin test/html/html_blockquote_indent.pdf | Bin 0 -> 1036 bytes test/html/html_li_tag_indent.pdf | Bin 0 -> 1001 bytes test/html/test_html.py | 76 +++-- 9 files changed, 321 insertions(+), 136 deletions(-) rename test/html/{html_blockquote.pdf => html_blockquote_color.pdf} (100%) create mode 100644 test/html/html_blockquote_indent.pdf create mode 100644 test/html/html_li_tag_indent.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index cada1c04e..429a8d425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,11 +20,13 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ### Added * support for overriding paragraph direction on bidirectional text * new optional `ul_bullet_color` parameter for `FPDF.write_html()` -### Fixed - +* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now accepts a `tag_styles` parameter to control the font, color & size of HTML elements: ``, `
    `, `
  • `... +* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now accepts a `tag_indents` parameter to control, for example, the indent of `
    ` elements ### Changed * improved the performance of `FPDF.start_section()` - _cf._ [issue #1092](https://github.com/py-pdf/fpdf2/issues/1092) - +### Deprecated +* The `dd_tag_indent` & `li_tag_indent` parameters of `FPDF.write_html()` are replaced by the new `tag_indents` generic parameter. +* The `heading_sizes` & `pre_code_font` parameters of `FPDF.write_html()` are replaced by the new `tag_styles` generic parameter. ## [2.7.8] - 2024-02-09 ### Added diff --git a/docs/HTML.md b/docs/HTML.md index f68a94ab7..3c076e28a 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -71,6 +71,45 @@ pdf.output("html.pdf") ``` +## Styling HTML tags globally + +The style 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, FontFace + +pdf = FPDF() +pdf.add_page() +pdf.write_html(""" +

    Big title

    +
    +

    Section title

    +

    Hello world!

    +
    +""", tag_styles={ + "h1": FontFace(color=(148, 139, 139), size_pt=32), + "h2": FontFace(color=(148, 139, 139), 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()`: + +```python +from fpdf import FPDF + +pdf = FPDF() +pdf.add_page() +pdf.write_html(""" +
    +
    Term
    +
    Definition
    +
    +""", tag_indents={"dd": 5}) +pdf.output("html_dd_indented.pdf") +``` + + ## Supported HTML features * `

    ` to ``: headings (and `align` attribute) diff --git a/fpdf/drawing.py b/fpdf/drawing.py index 26d0a8d3d..c470004b0 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -214,7 +214,7 @@ def __new__(cls, r, g, b, a=None): @property def colors(self): - """The color components as a tuple in order `(r, g, b)` with alpha omitted.""" + "The color components as a tuple in order `(r, g, b)` with alpha omitted, in range 0-1." return self[:-1] @property @@ -262,8 +262,13 @@ def __new__(cls, g, a=None): @property def colors(self): - """The color components as a tuple in order (g,) with alpha omitted.""" - return self[:-1] + "The color components as a tuple in order (r, g, b) with alpha omitted, in range 0-1." + return self.g, self.g, self.g + + @property + def colors255(self): + "The color components as a tuple in order `(r, g, b)` with alpha omitted, in range 0-255." + return tuple(255 * v for v in self.colors) @property def colors255(self): @@ -271,7 +276,7 @@ def colors255(self): return tuple(255 * v for v in self.colors) def serialize(self) -> str: - return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}" + return f"{0 if self.g == 0 else self.g} {self.OPERATOR}" __pdoc__["DeviceGray.OPERATOR"] = False @@ -322,8 +327,7 @@ def __new__(cls, c, m, y, k, a=None): @property def colors(self): - """The color components as a tuple in order (c, m, y, k) with alpha omitted.""" - + "The color components as a tuple in order (c, m, y, k) with alpha omitted, in range 0-1." return self[:-1] def serialize(self) -> str: diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 2ea1e53da..6e9251417 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -401,21 +401,16 @@ def write_html(self, text, *args, **kwargs): 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): numeric indentation of
  • elements - dd_tag_indent (int): numeric indentation of
    elements + 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 for
      elements ul_bullet_color (tuple | str | drawing.Device* instance): color of the
        bullets - heading_sizes (dict): font size per heading level names ("h1", "h2"...) - pre_code_font (str): font to use for
         &  blocks
        +            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
        -            element_colors (dict): dictionary of colors for elements. Possible keys:
        -            * "link" sets the link color for a href tag
        -            * "li" sets the bullet or number for 
      • tags - * "blockquote" sets the color for
        tags - * "headings" sets the color is not specified in a h1, h2 etc. heading tag - - The values for the dictionary are a tuple of three ints, being the r,g and b values + 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 """ kwargs2 = vars(self) # Method arguments must override class & instance attributes: @@ -901,7 +896,7 @@ def add_page( if isinstance(self.page_background, tuple): self.set_fill_color(*self.page_background) self.rect(0, 0, self.w, self.h, style="F") - self.set_fill_color(*(255 * v for v in fc.colors)) + self.set_fill_color(*fc.colors255) else: self.image(self.page_background, 0, 0, self.w, self.h) @@ -1111,7 +1106,7 @@ def set_page_background(self, background): background, (str, io.BytesIO, Image, DeviceRGB, tuple, type(None)) ): if isinstance(background, DeviceRGB): - self.page_background = tuple(255 * v for v in background.colors) + self.page_background = background.colors255 else: self.page_background = background else: @@ -4943,7 +4938,11 @@ def use_font_face(self, font_face: FontFace): prev_font = (self.font_family, self.font_style, self.font_size_pt) self.set_font( font_face.family or self.font_family, - font_face.emphasis.style if font_face.emphasis is not None else "", + ( + font_face.emphasis.style + if font_face.emphasis is not None + else self.font_style + ), font_face.size_pt or self.font_size_pt, ) prev_text_color = self.text_color diff --git a/fpdf/html.py b/fpdf/html.py index 592a65dae..8cb84c84f 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -5,7 +5,7 @@ They may change at any time without prior warning or any deprecation period, in non-backward-compatible ways. """ -from __future__ import annotations + from html.parser import HTMLParser import logging, re, warnings @@ -18,7 +18,25 @@ LOGGER = logging.getLogger(__name__) BULLET_WIN1252 = "\x95" # BULLET character in Windows-1252 encoding -DEFAULT_HEADING_SIZES = dict(h1=24, h2=18, h3=14, h4=12, h5=10, h6=8) +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), + "li": FontFace(color=(190, 0, 0)), + "pre": FontFace(family="Courier"), +} +DEFAULT_TAG_INDENTS = { + "blockquote": 0, + "dd": 10, + "li": 5, +} # Pattern to substitute whitespace sequences with a single space character each. # The following are all Unicode characters with White_Space classification plus the newline. @@ -234,9 +252,10 @@ def __init__( ul_bullet_char=BULLET_WIN1252, ul_bullet_color=(190, 0, 0), heading_sizes=None, - pre_code_font="courier", + pre_code_font=DEFAULT_TAG_STYLES["pre"].family, warn_on_tags_not_matching=True, - element_colors: dict[str, tuple[int, int, int]] | None = None, + tag_indents=None, + tag_styles=None, **_, ): """ @@ -244,30 +263,26 @@ def __init__( 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): numeric indentation of
      • elements - dd_tag_indent (int): numeric indentation of
        elements + li_tag_indent (int): [**DEPRECATED since v2.7.9**] numeric indentation of
      • 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 for
      elements ul_bullet_color (tuple | str | drawing.Device* instance): color of the
        bullets - heading_sizes (dict): font size per heading level names ("h1", "h2"...) - pre_code_font (str): font to use for
         &  blocks
        +            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
        +            tag_styles (dict): mapping of HTML tag names to colors
                 """
                 super().__init__()
                 self.pdf = pdf
                 self.image_map = image_map or (lambda src: src)
        -        self.li_tag_indent = li_tag_indent
        -        self.dd_tag_indent = dd_tag_indent
                 self.ul_bullet_char = ul_bullet_char
                 self.ul_bullet_color = (
                     color_as_decimal(ul_bullet_color)
                     if isinstance(ul_bullet_color, str)
                     else convert_to_device_color(ul_bullet_color).colors255
                 )
        -        self.heading_sizes = dict(**DEFAULT_HEADING_SIZES)
        -        if heading_sizes:
        -            self.heading_sizes.update(heading_sizes)
        -        self.pre_code_font = pre_code_font
                 self.warn_on_tags_not_matching = warn_on_tags_not_matching
         
                 # We operate in a local context and will only temporarily switch to the outer one for rendering.
        @@ -278,10 +293,10 @@ 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.style = dict(b=False, i=False, u=False)
        +        self.emphasis = dict(b=False, i=False, u=False)
                 self.font_size = pdf.font_size_pt
                 self.set_font(pdf.font_family or "times", size=self.font_size, set_default=True)
        -        self._prev_font = (pdf.font_family, self.font_size, self.style)
        +        self._prev_font = (pdf.font_family, self.font_size, self.emphasis)
                 self.pdf._push_local_stack()  # xpylint: disable=protected-access
         
                 self._pre_formatted = False  # preserve whitespace while True.
        @@ -292,10 +307,10 @@ def __init__(
                 self.follows_heading = False  # We don't want extra space below a heading.
                 self.href = ""
                 self.align = ""
        -        self.font_stack = []
        +        self.style_stack = []  # list of FontFace
                 self.indent = 0
                 self.bullet = []
        -        self.font_color = tuple((255 * v for v in pdf.text_color.colors))
        +        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
        @@ -308,35 +323,75 @@ def __init__(
                 self.table_row = None  # becomes a Row instance when processing 
    tags self.tr = None # becomes a dict of attributes when processing tags 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 - - self.link_color: tuple[int, int, int] - self.li_color: tuple[int, int, int] - self.blockquote_color: tuple[int, int, int] - self.headings_color: tuple[int, int, int] - self._set_color_scheme(element_colors) - - def _set_color_scheme( - self, element_colors: dict[str, tuple[int, int, int]] | None - ) -> None: - _old_default_colors: dict[str, tuple[int, int, int]] = { - "link": (0, 0, 255), - "li": (190, 0, 0), - "blockquote": (100, 0, 45), - "headings": (150, 0, 0), - } - - if element_colors is None: - element_colors = {} - - self.link_color = element_colors.get("link", _old_default_colors["link"]) - self.li_color = element_colors.get("li", _old_default_colors["li"]) - self.blockquote_color = element_colors.get( - "blockquote", _old_default_colors["blockquote"] - ) - self.headings_color = element_colors.get( - "headings", _old_default_colors["headings"] - ) + # "inserted" is a special attribute indicating that a cell has be inserted in self.table_row + + if not tag_indents: + tag_indents = {} + if dd_tag_indent != DEFAULT_TAG_INDENTS["dd"]: + warnings.warn( + ( + "The dd_tag_indent parameter is deprecated since v2.7.9 " + "and will be removed in a future release. " + "Set the `tag_indents` parameter instead." + ), + DeprecationWarning, + stacklevel=get_stack_level(), + ) + tag_indents["dd"] = dd_tag_indent + if li_tag_indent != DEFAULT_TAG_INDENTS["li"]: + warnings.warn( + ( + "The li_tag_indent parameter is deprecated since v2.7.9 " + "and will be removed in a future release. " + "Set the `tag_indents` 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: + raise NotImplementedError( + f"Cannot set indent for HTML tag <{tag}> (contributions are welcome to add support for this)" + ) + self.tag_indents = {**DEFAULT_TAG_INDENTS, **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: + warnings.warn( + ( + "The heading_sizes 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: + warnings.warn( + ( + "The pre_code_font 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["pre"] = self.tag_styles["pre"].replace( + family=pre_code_font + ) def _new_paragraph( self, align=None, line_height=1.0, top_margin=0, bottom_margin=0 @@ -466,7 +521,7 @@ def handle_starttag(self, tag, attrs): self._write_paragraph("\n") tag = "b" if tag == "dd": - self._write_paragraph("\n" + "\u00a0" * self.dd_tag_indent) + self._write_paragraph("\n" + "\u00a0" * self.tag_indents["dd"]) if tag == "strong": tag = "b" if tag == "em": @@ -499,12 +554,18 @@ def handle_starttag(self, tag, attrs): except ValueError: pass self._new_paragraph(align=align, line_height=line_height) - if tag in self.heading_sizes: + if tag in HEADING_TAGS: prev_font_height = self.font_size / self.pdf.k - self.font_stack.append((self.font_face, self.font_size, self.font_color)) + self.style_stack.append( + FontFace( + family=self.font_family, + size_pt=self.font_size, + color=self.font_color, + ) + ) self.heading_level = int(tag[1:]) - hsize_pt = self.heading_sizes[tag] - hsize = hsize_pt / self.pdf.k + tag_style = self.tag_styles[tag] + hsize = (tag_style.size_pt or self.font_size) / self.pdf.k if attrs: align = attrs.get("align") if not align in ["L", "R", "J", "C"]: @@ -516,28 +577,64 @@ def handle_starttag(self, tag, attrs): top_margin=prev_font_height + self.heading_above * hsize, bottom_margin=self.heading_below * hsize, ) - color = ( - color_as_decimal(attrs["color"]) - if "color" in attrs - else self.headings_color + color = None + if "color" in attrs: + color = color_as_decimal(attrs["color"]) + elif tag_style.color: + color = tag_style.color.colors255 + if color: + 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, ) - self.set_text_color(*color) - self.set_font(size=hsize_pt) if tag == "hr": self.pdf.add_page(same=True) if tag == "code": - self.font_stack.append((self.font_face, self.font_size, self.font_color)) - self.set_font(self.pre_code_font, self.font_size) + self.style_stack.append( + FontFace( + family=self.font_family, + 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, + ) if tag == "pre": - self.font_stack.append((self.font_face, self.font_size, self.font_color)) - self.set_font(self.pre_code_font, self.font_size) + self.style_stack.append( + FontFace( + family=self.font_family, + 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, + ) self._pre_formatted = True self._new_paragraph() self._pre_started = True if tag == "blockquote": - self.set_text_color(*self.blockquote_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, + ) self.indent += 1 self._new_paragraph(top_margin=3, bottom_margin=3) + if self.tag_indents["blockquote"]: + self._write_paragraph("\u00a0" * self.tag_indents["blockquote"]) if tag == "ul": self.indent += 1 self.bullet.append(self.ul_bullet_char) @@ -558,12 +655,18 @@ def handle_starttag(self, tag, attrs): bullet += 1 self.bullet[self.indent - 1] = bullet bullet = f"{bullet}. " - indent = "\u00a0" * self.li_tag_indent * self.indent + indent = "\u00a0" * self.tag_indents["li"] * self.indent self._write_paragraph(f"{indent}{bullet} ") self.set_text_color(*self.font_color) if tag == "font": # save previous font state: - self.font_stack.append((self.font_face, self.font_size, self.font_color)) + self.style_stack.append( + FontFace( + family=self.font_family, + size_pt=self.font_size, + color=self.font_color, + ) + ) if "color" in attrs: color = color_as_decimal(attrs["color"]) self.font_color = color @@ -571,7 +674,7 @@ def handle_starttag(self, tag, attrs): face = attrs.get("face").lower() # This may result in a FPDFException "font not found". self.set_font(face) - self.font_face = face + self.font_family = face if "size" in attrs: self.font_size = int(attrs.get("size")) self.set_font() @@ -713,22 +816,22 @@ def handle_endtag(self, tag): tag, self._tags_stack[-1], ) - if tag in self.heading_sizes: + if tag in HEADING_TAGS: self.heading_level = None - face, size, color = self.font_stack.pop() - self.set_font(face, size) - self.set_text_color(*color) + font_face = self.style_stack.pop() + 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": - face, size, color = self.font_stack.pop() - self.set_font(face, size) - self.set_text_color(*color) + font_face = self.style_stack.pop() + self.set_font(font_face.family, font_face.size_pt) + self.set_text_color(*font_face.color.colors255) if tag == "pre": self._end_paragraph() - face, size, color = self.font_stack.pop() - self.set_font(face, size) - self.set_text_color(*color) + font_face = self.style_stack.pop() + 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 if tag == "blockquote": @@ -769,10 +872,10 @@ def handle_endtag(self, tag): self.td_th = None if tag == "font": # recover last font state - face, size, color = self.font_stack.pop() - self.font_color = color - self.set_font(face, size) - self.set_text_color(*self.font_color) + font_face = self.style_stack.pop() + 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) if tag == "center": self._end_paragraph() if tag == "sup": @@ -787,24 +890,24 @@ def feed(self, data): self._end_paragraph() # render the final chunk of text and clean up our local context. self.pdf._pop_local_stack() # pylint: disable=protected-access if self._prev_font[0]: # restore previously defined font settings - self.style = self._prev_font[2] + self.emphasis = self._prev_font[2] self.set_font(self._prev_font[0], size=self._prev_font[1], set_default=True) 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, face=None, size=None, set_default=False): - if face: - self.font_face = face + def set_font(self, family=None, size=None, set_default=False): + if family: + self.font_family = family if size: self.font_size = size self.h = size / self.pdf.k - style = "".join(s for s in ("b", "i", "u") if self.style.get(s)).upper() - LOGGER.debug(f"set_font: %s style=%s h={self.h:.2f}", self.font_face, style) + style = "".join(s for s in ("b", "i", "u") if self.emphasis.get(s)).upper() + 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. self.pdf.page = 0 - if (self.font_face, style) != (self.pdf.font_family, self.pdf.font_style): - self.pdf.set_font(self.font_face, style, self.font_size) + 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 @@ -812,8 +915,8 @@ def set_font(self, face=None, size=None, set_default=False): def set_style(self, tag=None, enable=False): # Modify style and select corresponding font if tag: - self.style[tag.lower()] = enable - style = "".join(s for s in ("b", "i", "u") if self.style.get(s)) + self.emphasis[tag.lower()] = enable + style = "".join(s for s in ("b", "i", "u") if self.emphasis.get(s)) LOGGER.debug("SET_FONT_STYLE %s", style) prev_page = self.pdf.page self.pdf.page = 0 @@ -828,7 +931,13 @@ def set_text_color(self, r=None, g=0, b=0): def put_link(self, text): # Put a hyperlink - self.set_text_color(*self.link_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, + ) self.set_style("u", True) self._write_paragraph(text, link=self.href) self.set_style("u", False) diff --git a/test/html/html_blockquote.pdf b/test/html/html_blockquote_color.pdf similarity index 100% rename from test/html/html_blockquote.pdf rename to test/html/html_blockquote_color.pdf diff --git a/test/html/html_blockquote_indent.pdf b/test/html/html_blockquote_indent.pdf new file mode 100644 index 0000000000000000000000000000000000000000..91089b579549a43202d535e2c9627ac607c73b3c GIT binary patch literal 1036 zcmah|&1(}u6vvy475uz-dV>A%(Cp4`cC#yCrAaneY;8y@ilM?LnWQVpOxc}4TB(QD zFO>FD5N-7$cvtYEP!B~fiqMn)ffqjuUec4!>G;2@}7%AudatH-oxbo9T87F1mOR**tRDl?_5z{QN;y7m6!aFrLOF+qa zRT3h{L4=kiofe^;m?dA zB}seStFdY%jVy7udddmqA;dc|IWG{uJ%&69StBTs5X>m0OdjErp*_B=U*jYYwbndN zGNi=H1Sw;LR{0EI&5}_e4~SPs&Bqs)KcY?b(9NF<_v}v}Ua!XoHqiX`z_$ZaYx}P* zzSweZEVq`z6SrBb@4~^=Uk}$eir0_bJz1IA9Qj(lbqbz8d$Y7MW5kZe@aH3mt-`w> ztwrbZm#eV)eR<#6`>iMGS5MvX_d5%3cdFYfzx&c_#YcBE$9Zu4@XMiXDSEe*uKY%% zkCYdZEE7s7Q+}O{xTGv|dgN-kifNy4F^h*cB0NmEoXS{Hh!x@Wq3j!s8t6A{|9P*<)^MhpOk zGH0-D+E~*uwpC$Qusx`%gQp|G&45&p3Ko+7j)7?yx&al~#cXK1fbL?p=tf!u!}i{L zT4FC2{|9CytUY|DDWvJ;;DP7YNYH+Dlm0vrB`bNxILLP+_I;FA7)UAPcLoYcRZp01 rT-Oc5mF?RJ1M9k*O~STRBI?CitdMJ8hXSYgE|&e|xFtrQb`;w)OvM7v|* zP$J+P5<$sV&{0Aa(NIG}M8zjC;|EzK1=Gy#xpU7wbMBp7s}^jLf`teeAP!%kav9;O z7;^~(;hkuZLeEm6@BrawY!K0km_W}^h~qg0(*@Ti1=my2&glsQ+@eF4qU~+O_(0Q{ z8QR?w#{IsG1fO#P4kGy!_&AOgflh7GQ!$o+cR8zycs$}WI0Y>)TokENNnefz}elCB%^Y%$$>G9UxuiDXhp}DtC zMr4JiED-9NxD$<7x)q8MRp!;Xk{j_M;QfgEJdG9r!tL>)oVH9iPW5`$Kh2TxL3{Mi z2Tflt?zL58-Jp{*+K^i0&0yO6Iv zB3{uWrbUQnsMRIjhGA?x$Rtk^c7zOYk&e$66dlKM;0UfT52hjDSD2@6qgMta-pzAn z=_Y2|Y9g-XI<}g+Ygo}zPh)|}ghnw-rcbIJy<$o-4uz1QAB7s`20s#@tThis is a H1 -

    This is a H2

    -

    This is a H3

    -

    This is a H4

    -
    This is a H5
    -
    This is a H6
    """, - heading_sizes=dict(h1=6, h2=12, h3=18, h4=24, h5=30, h6=36), - ) + with pytest.warns(DeprecationWarning): + 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
    """, + heading_sizes=dict(h1=6, h2=12, h3=18, h4=24, h5=30, h6=36), + ) assert_pdf_equal(pdf, HERE / "html_custom_heading_sizes.pdf", tmp_path) @@ -482,7 +484,8 @@ 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ść! ", pre_code_font="DejaVuSansMono") + with pytest.warns(DeprecationWarning): + pdf.write_html(" Cześć! ", pre_code_font="DejaVuSansMono") assert_pdf_equal(pdf, HERE / "html_custom_pre_code_font.pdf", tmp_path) @@ -566,38 +569,67 @@ def test_html_and_section_title_styles(): # issue 1080 def test_html_link_color(tmp_path): pdf = FPDF() pdf.add_page() - text = 'foo' - pdf.write_html(text, element_colors={"link": (255, 0, 0)}) + 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_unordered_li_color(tmp_path): pdf = FPDF() pdf.add_page() - text = "
    • foo
    " - pdf.write_html(text, element_colors={"li": (0, 255, 0)}) + html = "
    • foo
    " + pdf.write_html(html, tag_styles={"li": FontFace(color=color_as_decimal("lime"))}) assert_pdf_equal(pdf, HERE / "html_unordered_li_color.pdf", tmp_path) def test_html_ordered_li_color(tmp_path): pdf = FPDF() pdf.add_page() - text = "
    1. foo
    " - pdf.write_html(text, element_colors={"li": (0, 255, 0)}) + html = "
    1. foo
    " + pdf.write_html(html, tag_styles={"li": FontFace(color=DeviceRGB(r=0, g=1, b=0))}) assert_pdf_equal(pdf, HERE / "html_ordered_li_color.pdf", tmp_path) def test_html_blockquote_color(tmp_path): pdf = FPDF() pdf.add_page() - text = "Text before
    foo
    Text afterwards" - pdf.write_html(text, element_colors={"blockquote": (125, 125, 0)}) - assert_pdf_equal(pdf, HERE / "html_blockquote.pdf", tmp_path) + html = "Text before
    foo
    Text afterwards" + pdf.write_html(html, tag_styles={"blockquote": FontFace(color=(125, 125, 0))}) + assert_pdf_equal(pdf, HERE / "html_blockquote_color.pdf", tmp_path) def test_html_headings_color(tmp_path): pdf = FPDF() pdf.add_page() - text = "

    foo

    bar

    " - pdf.write_html(text, element_colors={"headings": (148, 139, 139)}) + html = "

    foo

    bar

    " + pdf.write_html( + html, + tag_styles={ + "h1": FontFace(color=(148, 139, 139), size_pt=24), + "h2": FontFace(color=(148, 139, 139), size_pt=18), + }, + ) 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": FontFace()}) + + +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": 5}) + assert_pdf_equal(pdf, HERE / "html_blockquote_indent.pdf", tmp_path) + + +def test_html_li_tag_indent(tmp_path): + pdf = FPDF() + pdf.add_page() + with pytest.warns(DeprecationWarning): + pdf.write_html("
    • item
    ", li_tag_indent=10) + assert_pdf_equal(pdf, HERE / "html_li_tag_indent.pdf", tmp_path) From 8bab81248df0aa457fd741ab04ab35a99faf6187 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 28 Feb 2024 08:44:25 +0100 Subject: [PATCH 3/6] Fixing some unit tests --- fpdf/drawing.py | 2 +- fpdf/enums.py | 5 ++++- fpdf/fonts.py | 6 ++++-- fpdf/fpdf.py | 5 +++-- test/drawing/test_drawing.py | 4 ++-- test/outline/test_outline.py | 15 +++++++++++++-- .../toc_with_font_style_override_bold1.pdf | Bin 0 -> 1726 bytes ...df => toc_with_font_style_override_bold2.pdf} | Bin 8 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 test/outline/toc_with_font_style_override_bold1.pdf rename test/outline/{toc_with_font_style_override_bold.pdf => toc_with_font_style_override_bold2.pdf} (100%) diff --git a/fpdf/drawing.py b/fpdf/drawing.py index c470004b0..20ee90f7e 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -276,7 +276,7 @@ def colors255(self): return tuple(255 * v for v in self.colors) def serialize(self) -> str: - return f"{0 if self.g == 0 else self.g} {self.OPERATOR}" + return f"{number_to_str(self.g)} {self.OPERATOR}" __pdoc__["DeviceGray.OPERATOR"] = False diff --git a/fpdf/enums.py b/fpdf/enums.py index 0002669c3..1db0abdf1 100644 --- a/fpdf/enums.py +++ b/fpdf/enums.py @@ -227,6 +227,9 @@ class TextEmphasis(CoerciveIntFlag): style = B | I """ + NONE = 0 + "No emphasis" + B = 1 "Bold" @@ -246,7 +249,7 @@ def style(self): def coerce(cls, value): if isinstance(value, str): if value == "": - return 0 + return cls.NONE if value.upper() == "BOLD": return cls.B if value.upper() == "ITALICS": diff --git a/fpdf/fonts.py b/fpdf/fonts.py index 87379f48e..6d93a796d 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -51,7 +51,9 @@ class FontFace: "fill_color", ) family: Optional[str] - emphasis: Optional[TextEmphasis] # can be a combination: B | U + emphasis: Optional[TextEmphasis] # None means "no override" + # Whereas "" means "no emphasis" + # This can be a combination: B | U size_pt: Optional[int] # Colors are single number grey scales or (red, green, blue) tuples: color: Optional[Union[DeviceGray, DeviceRGB]] @@ -61,7 +63,7 @@ def __init__( self, family=None, emphasis=None, size_pt=None, color=None, fill_color=None ): self.family = family - self.emphasis = TextEmphasis.coerce(emphasis) if emphasis else None + self.emphasis = None if emphasis is None else TextEmphasis.coerce(emphasis) self.size_pt = size_pt self.color = None if color is None else convert_to_device_color(color) self.fill_color = ( diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 6e9251417..98711934a 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -144,7 +144,8 @@ class Image: class TitleStyle(FontFace): def __init__( self, - font_family: Optional[str] = None, + 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), @@ -155,7 +156,7 @@ def __init__( ): super().__init__( font_family, - (font_style or "") + ("U" if underline else ""), + ((font_style or "") + "U") if underline else font_style, font_size_pt, color, ) diff --git a/test/drawing/test_drawing.py b/test/drawing/test_drawing.py index 57d92ba6e..a3155f9a6 100644 --- a/test/drawing/test_drawing.py +++ b/test/drawing/test_drawing.py @@ -148,8 +148,8 @@ def test_device_gray(self): gray = fpdf.drawing.DeviceGray(g=0.5) gray_a = fpdf.drawing.DeviceGray(g=0.5, a=0.75) - assert gray.colors == (0.5,) - assert gray_a.colors == (0.5,) + assert gray.colors == (0.5, 0.5, 0.5) + assert gray_a.colors == (0.5, 0.5, 0.5) with pytest.raises(ValueError): fpdf.drawing.DeviceGray(g=2) diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index 837c9e8ff..aa07bb535 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -173,9 +173,20 @@ def test_toc_with_font_style_override_bold(tmp_path): # issue-1072 pdf = FPDF() pdf.add_page() pdf.set_font("Helvetica", "B") - pdf.set_section_title_styles(TitleStyle("Helvetica", "", 20, (0, 0, 0))) + 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") + 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_bold.pdf", tmp_path) + assert_pdf_equal(pdf, HERE / "toc_with_font_style_override_bold2.pdf", tmp_path) def test_toc_with_table(tmp_path): # issue-1079 diff --git a/test/outline/toc_with_font_style_override_bold1.pdf b/test/outline/toc_with_font_style_override_bold1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e3fab61ef1d589cd0c6df83756006de816a5f14f GIT binary patch literal 1726 zcmah~&yU+g6h=Lvkr3z7%e$=574r6^2WS%65eVpqgmLdaY zXii8`<|sT@xdwVH#Xu$bN|s?vDs!d?9_d*s#6&3#oibX|9E3Vd)UrX;Z1Q6NQVXdw zaLY9}QkAa_>vD1VN~b_)X(+RGm zKuNGn%D`$$*dS&&)Wn8yqD6|@B%v08h}1$5dL-mhKLFk~i!z;xa7E6(aVCHMV!w2} zyRA1Vx%2X|Gqc>+jx~OFG+n><^XvD=FMfFSPyV;QW4-usx=z3N>U-P%_`6Hw@|E8& zpR~T7Kl|h9v(}U6tq0G4ee>|cCyU;?WSoQ+8~5<#f}vX6Bls;yw-D-*JfDaJ`KCrW^`0xA*EmUZ=7gX~! Date: Wed, 28 Feb 2024 10:36:17 +0100 Subject: [PATCH 4/6] Not allowing to style
  • tags --- docs/HTML.md | 2 +- fpdf/drawing.py | 5 ----- fpdf/html.py | 1 - test/html/html_ordered_li_color.pdf | Bin 995 -> 0 bytes test/html/html_unordered_li_color.pdf | Bin 995 -> 0 bytes test/html/test_html.py | 16 ---------------- 6 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 test/html/html_ordered_li_color.pdf delete mode 100644 test/html/html_unordered_li_color.pdf diff --git a/docs/HTML.md b/docs/HTML.md index 3c076e28a..e653ce8f6 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -73,7 +73,7 @@ pdf.output("html.pdf") ## Styling HTML tags globally -The style of several HTML tags (``, `
    `, ``, `
    `, `
  • `, `

    `, `

    `, `

    `...) can be set globally, for the whole HTML document, by passing `tag_styles` to `FPDF.write_html()`: +The style 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, FontFace diff --git a/fpdf/drawing.py b/fpdf/drawing.py index 20ee90f7e..221dd4d1f 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -270,11 +270,6 @@ def colors255(self): "The color components as a tuple in order `(r, g, b)` with alpha omitted, in range 0-255." return tuple(255 * v for v in self.colors) - @property - def colors255(self): - "The color components as a tuple in order `(r, g, b)` with alpha omitted, in range 0-255." - return tuple(255 * v for v in self.colors) - def serialize(self) -> str: return f"{number_to_str(self.g)} {self.OPERATOR}" diff --git a/fpdf/html.py b/fpdf/html.py index 8cb84c84f..be17c635d 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -29,7 +29,6 @@ "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), - "li": FontFace(color=(190, 0, 0)), "pre": FontFace(family="Courier"), } DEFAULT_TAG_INDENTS = { diff --git a/test/html/html_ordered_li_color.pdf b/test/html/html_ordered_li_color.pdf deleted file mode 100644 index 7a6d40a794975dffa6d8ea9a5961a2dfd939f88b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 995 zcmah|&ubGw6o!JJbELHz--Rx#oLLk}AYG|b)DGG)jw#hVE-OQBTiKL!9 z=%F_c9u*{@9*R`|3lD;L7QuhOiuO`(&g6$(?4fg+-S5r&-uJ$FvrzYg99gmu0RzOw zeN-$W?1=%FKoDMzIw`a)6>6*_e4BNmup)-gG8AIF_L50~=E)LuRdj88ivg~OU6!I! z2{GQ$bY_Njb&K(GTSkIUIROWeyajv^$BICwu7ulSAOWv(=81UF=My*uEw3>v;)ODd zMK@DMOFUA~CF3%M{3NFyChB*_&|s+;ByE<0lcm(fGkiLA#ur4fWQo!mhm!fMEjkS0 z6~?==2bAihR3R$Y{Lc@c;{DOa-e`wjJ#+T!uilSR@9@oB?%|!?=gWUS$MOEDC%@0_YA>fH zrmorxpPLnS>Tr6QpXhp;A!mD;q1*-J9;U^$cwxL+dHm^fdU& zSGeZiAg<#Q(;~z*)VfF9MZ;LUo=F}iY!ewkk&aImIJRxsun9+)3*!**Bg|FZ=#{~U zd;FYPIF98ldMwwrR2%>CIi{MK877l3idiy#OwH&KQ<8B*2nqT*s4=ebO##{p{p~}m x=-8A7)qJH)y{en{e9N$?;T4R6VG+ml^QDtdj9pG;n8>kB4z=7Zk{X=5VqZkc zh=75=03=5Kif$d*qAtv^fS3^c1Ki~YvBjEW91ARY*WwKj*Pa?PATAKFr*18l@Mip zMQ5aF8>f_&dm`j)!3o$4#RFjDD3Sy!bw4=cV*$9qsLP}Ah|S>ylstb~#0^9c@nNcr zlDJaO1Z5%tbDq--V)?sd@M*%wagQdDPgAPmDL$3D^$NLoFeaeR7 z1Z-O+B_gK52u(f^|L0d@O02`|w-_BvpOdy`UJ|XjRd1bM{c@+hfo|OVvD3-E>vq2H zyubVEV|F{cz4`3m{NG%LZ{f9MNRY=c7Q{R%23V$cBIqLsn)KQ*Nu^Mz rnVw@CR@t;G2FcsCvQ}^_hFdjB>HjBYE+-<0#muI>ZJ|u2*6`3DGIbBf diff --git a/test/html/test_html.py b/test/html/test_html.py index cc8ad8f8a..efda78936 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -574,22 +574,6 @@ def test_html_link_color(tmp_path): assert_pdf_equal(pdf, HERE / "html_link_color.pdf", tmp_path) -def test_html_unordered_li_color(tmp_path): - pdf = FPDF() - pdf.add_page() - html = "
    • foo
    " - pdf.write_html(html, tag_styles={"li": FontFace(color=color_as_decimal("lime"))}) - assert_pdf_equal(pdf, HERE / "html_unordered_li_color.pdf", tmp_path) - - -def test_html_ordered_li_color(tmp_path): - pdf = FPDF() - pdf.add_page() - html = "
    1. foo
    " - pdf.write_html(html, tag_styles={"li": FontFace(color=DeviceRGB(r=0, g=1, b=0))}) - assert_pdf_equal(pdf, HERE / "html_ordered_li_color.pdf", tmp_path) - - def test_html_blockquote_color(tmp_path): pdf = FPDF() pdf.add_page() From 790093a4f9a24edad590fc8873a4657a0407db15 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:13:36 +0100 Subject: [PATCH 5/6] Support for `start` & `type` attributes of
      tags when using FPDF.write_html() --- CHANGELOG.md | 3 +- fpdf/fpdf.py | 5 +- fpdf/html.py | 43 +++++++++++---- fpdf/util.py | 27 ++++++++++ ...let_color.pdf => html_li_prefix_color.pdf} | Bin test/html/html_ol_start_and_type.pdf | Bin 0 -> 1019 bytes test/html/test_html.py | 51 +++++++++++------- 7 files changed, 98 insertions(+), 31 deletions(-) rename test/html/{html_ul_bullet_color.pdf => html_li_prefix_color.pdf} (100%) create mode 100644 test/html/html_ol_start_and_type.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 429a8d425..5438ab6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.7.9] - Not released yet ### Added * support for overriding paragraph direction on bidirectional text -* new optional `ul_bullet_color` parameter for `FPDF.write_html()` +* new optional `li_prefix_color` parameter for `FPDF.write_html()` +* support for `start` & `type` attributes of `
        ` tags when using `FPDF.write_html()` * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now accepts a `tag_styles` parameter to control the font, color & size of HTML elements: ``, `
        `, `
      1. `... * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now accepts a `tag_indents` parameter to control, for example, the indent of `
        ` elements ### Changed diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 98711934a..ec75d94d5 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -405,8 +405,9 @@ def write_html(self, text, *args, **kwargs): li_tag_indent (int): [**DEPRECATED since v2.7.8**] numeric indentation of
      2. 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 for
          elements - ul_bullet_color (tuple | str | drawing.Device* instance): color of the
            bullets + 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
                  diff --git a/fpdf/html.py b/fpdf/html.py
                  index be17c635d..082e7c2e1 100644
                  --- a/fpdf/html.py
                  +++ b/fpdf/html.py
                  @@ -7,6 +7,7 @@
                   """
                   
                   from html.parser import HTMLParser
                  +from string import ascii_lowercase, ascii_uppercase
                   import logging, re, warnings
                   
                   from .deprecation import get_stack_level
                  @@ -15,6 +16,7 @@
                   from .errors import FPDFException
                   from .fonts import FontFace
                   from .table import Table
                  +from .util import int2roman
                   
                   LOGGER = logging.getLogger(__name__)
                   BULLET_WIN1252 = "\x95"  # BULLET character in Windows-1252 encoding
                  @@ -249,7 +251,7 @@ def __init__(
                           dd_tag_indent=10,
                           table_line_separators=False,
                           ul_bullet_char=BULLET_WIN1252,
                  -        ul_bullet_color=(190, 0, 0),
                  +        li_prefix_color=(190, 0, 0),
                           heading_sizes=None,
                           pre_code_font=DEFAULT_TAG_STYLES["pre"].family,
                           warn_on_tags_not_matching=True,
                  @@ -265,8 +267,9 @@ def __init__(
                               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 for