From 78f39e20954b69c58ca49878533dbb408f0f7621 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:57:16 +0200 Subject: [PATCH 1/9] write_html(): homogenization in handling of tag_styles --- docs/HTML.md | 3 +- fpdf/html.py | 111 +++++++++++++++------ test/html/html_features.pdf | Bin 6742 -> 6740 bytes test/html/html_heading_color_attribute.pdf | Bin 1424 -> 1426 bytes test/html/html_list_vertical_margin.pdf | Bin 2769 -> 2768 bytes test/html/html_superscript.pdf | Bin 1317 -> 1317 bytes test/html/test_html.py | 4 +- test/outline/html_toc.pdf | Bin 4304 -> 4303 bytes test/outline/html_toc_2_pages.pdf | Bin 20885 -> 21413 bytes 9 files changed, 81 insertions(+), 37 deletions(-) diff --git a/docs/HTML.md b/docs/HTML.md index 156dd0bdf..f882bf24b 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -78,7 +78,6 @@ pdf.write_html(""" pdf.output("html.pdf") ``` - ### Styling HTML tags globally _New in [:octicons-tag-24: 2.7.9](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ @@ -135,7 +134,7 @@ However, **Pull Request are welcome** to implement missing features! ## Supported HTML features -* `

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

` to `

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

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


` tags * ``, ``, ``: bold, italic, underline diff --git a/fpdf/html.py b/fpdf/html.py index d44c09e28..429221b5d 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -23,10 +23,14 @@ DEGREE_WIN1252 = "\xb0" HEADING_TAGS = ("h1", "h2", "h3", "h4", "h5", "h6") DEFAULT_TAG_STYLES = { - "a": TextStyle(color="#00f"), + # inline tags: + "a": FontFace(color="#00f"), + "code": FontFace(family="Courier"), + # block tags: "blockquote": TextStyle(color="#64002d", t_margin=3, b_margin=3), - "code": TextStyle(font_family="Courier"), + "center": TextStyle(), "dd": TextStyle(l_margin=10), + "dt": TextStyle(), "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), @@ -34,6 +38,7 @@ "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), + "p": TextStyle(t_margin=4 + 7 / 30), "pre": TextStyle(font_family="Courier"), "ol": TextStyle(t_margin=2), "ul": TextStyle(t_margin=2), @@ -324,7 +329,6 @@ def __init__( # nothing written yet to
, remove one initial nl:
         self._pre_started = False
         self.follows_trailing_space = False  # The last write has ended with a space.
-        self.follows_heading = False  # We don't want extra space below a heading.
         self.href = ""
         self.align = ""
         self.style_stack = []  # list of FontFace
@@ -351,7 +355,11 @@ def __init__(
                 raise NotImplementedError(
                     f"Cannot set style for HTML tag <{tag}> (contributions are welcome to add support for this)"
                 )
-            if isinstance(tag_style, FontFace) and not isinstance(tag_style, TextStyle):
+            default_tag_style = self.tag_styles[tag]
+            is_base_fontFace = isinstance(tag_style, FontFace) and not isinstance(
+                tag_style, TextStyle
+            )
+            if is_base_fontFace and isinstance(default_tag_style, TextStyle):
                 # pylint: disable=redefined-loop-name
                 tag_style = TextStyle(
                     font_family=tag_style.family,
@@ -362,9 +370,9 @@ def __init__(
                     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,
+                    t_margin=default_tag_style.t_margin,
+                    l_margin=default_tag_style.l_margin,
+                    b_margin=default_tag_style.b_margin,
                 )
             self.tag_styles[tag] = tag_style
         if heading_sizes is not None:
@@ -390,7 +398,7 @@ def __init__(
                 stacklevel=get_stack_level(),
             )
             self.tag_styles["code"] = self.tag_styles["code"].replace(
-                font_family=pre_code_font
+                family=pre_code_font
             )
             self.tag_styles["pre"] = self.tag_styles["pre"].replace(
                 font_family=pre_code_font
@@ -405,7 +413,7 @@ def __init__(
                 DeprecationWarning,
                 stacklevel=get_stack_level(),
             )
-            self.tag_styles["dd"] = self.tag_styles["pre"].replace(
+            self.tag_styles["dd"] = self.tag_styles["dd"].replace(
                 l_margin=dd_tag_indent
             )
         if li_tag_indent is not None:
@@ -449,8 +457,6 @@ def _new_paragraph(
     ):
         self._end_paragraph()
         self.align = align or ""
-        if not top_margin and not self.follows_heading:
-            top_margin = self.font_size / self.pdf.k
         self._paragraph = self._column.paragraph(
             text_align=align,
             line_height=line_height,
@@ -461,7 +467,6 @@ def _new_paragraph(
             bullet_string=bullet,
         )
         self.follows_trailing_space = True
-        self.follows_heading = False
 
     def _end_paragraph(self):
         self.align = ""
@@ -477,7 +482,7 @@ def _end_paragraph(self):
 
     def _write_paragraph(self, text, link=None):
         if not self._paragraph:
-            self._new_paragraph()
+            self._new_paragraph(top_margin=self.font_size / self.pdf.k)
         self._paragraph.write(text, link=link)
 
     def _ln(self, h=None):
@@ -579,19 +584,26 @@ def handle_starttag(self, tag, attrs):
             # pylint: disable=protected-access
             self.pdf._perform_page_break()
         if tag == "dt":
+            tag_style = self.tag_styles[tag]
             self._new_paragraph(
                 line_height=(
                     self.line_height_stack[-1] if self.line_height_stack else None
                 ),
+                # TODO: use top_margin=tag_style.t_margin
+                top_margin=self.font_size / self.pdf.k,
+                bottom_margin=tag_style.b_margin,
+                indent=tag_style.l_margin,
             )
             tag = "b"
         if tag == "dd":
-            self.follows_heading = True
+            tag_style = self.tag_styles[tag]
             self._new_paragraph(
                 line_height=(
                     self.line_height_stack[-1] if self.line_height_stack else None
                 ),
-                indent=self.tag_styles["dd"].l_margin * (self.indent + 1),
+                top_margin=tag_style.t_margin,
+                bottom_margin=tag_style.b_margin,
+                indent=tag_style.l_margin * (self.indent + 1),
             )
         if tag == "strong":
             tag = "b"
@@ -627,7 +639,14 @@ def handle_starttag(self, tag, attrs):
                     line_height = float(line_height)
                 except ValueError:
                     line_height = None
-            self._new_paragraph(align=align, line_height=line_height)
+            tag_style = self.tag_styles[tag]
+            self._new_paragraph(
+                align=align,
+                line_height=line_height,
+                top_margin=tag_style.t_margin,
+                bottom_margin=tag_style.b_margin,
+                indent=tag_style.l_margin,
+            )
         if tag in HEADING_TAGS:
             prev_font_height = self.font_size / self.pdf.k
             self.style_stack.append(
@@ -649,8 +668,10 @@ def handle_starttag(self, tag, attrs):
                 align = None
             self._new_paragraph(
                 align=align,
+                # TODO: rm prev_font_height & hsize
                 top_margin=prev_font_height + tag_style.t_margin * hsize,
                 bottom_margin=tag_style.b_margin * hsize,
+                indent=tag_style.l_margin,
             )
             color = None
             if "color" in css_style:
@@ -721,7 +742,12 @@ def handle_starttag(self, tag, attrs):
             )
             self._pre_formatted = True
             self._pre_started = True
-            self._new_paragraph()
+            self._new_paragraph(
+                # TODO: top_margin=tag_style.t_margin,
+                top_margin=self.font_size / self.pdf.k,
+                bottom_margin=tag_style.b_margin,
+                indent=tag_style.l_margin,
+            )
         if tag == "blockquote":
             self.style_stack.append(
                 FontFace(
@@ -744,9 +770,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=self.tag_styles["blockquote"].t_margin,
-                bottom_margin=self.tag_styles["blockquote"].b_margin,
-                indent=self.tag_styles["blockquote"].l_margin * self.indent,
+                top_margin=tag_style.t_margin,
+                bottom_margin=tag_style.b_margin,
+                indent=tag_style.l_margin * self.indent,
             )
         if tag == "ul":
             self.indent += 1
@@ -767,8 +793,12 @@ def handle_starttag(self, tag, attrs):
             else:
                 self.line_height_stack.append(None)
             if self.indent == 1:
+                tag_style = self.tag_styles[tag]
                 self._new_paragraph(
-                    top_margin=self.tag_styles["ul"].t_margin, line_height=0
+                    line_height=0,
+                    top_margin=tag_style.t_margin,
+                    bottom_margin=tag_style.b_margin,
+                    indent=tag_style.l_margin,
                 )
                 self._write_paragraph("\u00a0")
             self._end_paragraph()
@@ -790,15 +820,16 @@ def handle_starttag(self, tag, attrs):
             else:
                 self.line_height_stack.append(None)
             if self.indent == 1:
+                tag_style = self.tag_styles[tag]
                 self._new_paragraph(
-                    top_margin=self.tag_styles["ol"].t_margin, line_height=0
+                    line_height=0,
+                    top_margin=tag_style.t_margin,
+                    bottom_margin=tag_style.b_margin,
+                    indent=tag_style.l_margin,
                 )
                 self._write_paragraph("\u00a0")
             self._end_paragraph()
         if tag == "li":
-            # Default value of 2 for h to be multiplied by the conversion factor
-            # in self._ln(h) here is given in mm
-            self._ln(self.tag_styles["li"].t_margin)
             self.set_text_color(*self.li_prefix_color)
             if self.bullet:
                 bullet = self.bullet[self.indent - 1]
@@ -810,11 +841,16 @@ def handle_starttag(self, tag, attrs):
                 self.bullet[self.indent - 1] = bullet
                 ol_type = self.ol_type[self.indent - 1]
                 bullet = f"{ol_prefix(ol_type, bullet)}."
+            tag_style = self.tag_styles[tag]
+            self._ln(tag_style.t_margin)
             self._new_paragraph(
                 line_height=(
                     self.line_height_stack[-1] if self.line_height_stack else None
                 ),
-                indent=self.tag_styles["li"].l_margin * self.indent,
+                indent=tag_style.l_margin * self.indent,
+                # TODO: merge this top_margin with _ln() call above
+                top_margin=self.font_size / self.pdf.k,
+                bottom_margin=tag_style.b_margin,
                 bullet=bullet,
             )
             self.set_text_color(*self.font_color)
@@ -944,7 +980,14 @@ def handle_starttag(self, tag, attrs):
                 self.image_map(attrs["src"]), x=x, w=width, h=height, link=self.href
             )
         if tag == "center":
-            self._new_paragraph(align="C")
+            tag_style = self.tag_styles[tag]
+            self._new_paragraph(
+                align="C",
+                # TODO: use tag_style.t_margin
+                top_margin=self.font_size / self.pdf.k,
+                bottom_margin=tag_style.b_margin,
+                indent=tag_style.l_margin,
+            )
         if tag == "toc":
             self._end_paragraph()
             self.pdf.insert_toc_placeholder(
@@ -990,7 +1033,6 @@ def handle_endtag(self, tag):
             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
@@ -1145,11 +1187,14 @@ def _scale_units(pdf, in_tag_styles):
     conversion_factor = get_scale_factor("mm") / pdf.k
     out_tag_styles = {}
     for tag_name, tag_style in in_tag_styles.items():
-        out_tag_styles[tag_name] = tag_style.replace(
-            t_margin=tag_style.t_margin * conversion_factor,
-            l_margin=tag_style.l_margin * conversion_factor,
-            b_margin=tag_style.b_margin * conversion_factor,
-        )
+        if isinstance(tag_style, TextStyle):
+            out_tag_styles[tag_name] = tag_style.replace(
+                t_margin=tag_style.t_margin * conversion_factor,
+                l_margin=tag_style.l_margin * conversion_factor,
+                b_margin=tag_style.b_margin * conversion_factor,
+            )
+        else:
+            out_tag_styles[tag_name] = tag_style
     return out_tag_styles
 
 
diff --git a/test/html/html_features.pdf b/test/html/html_features.pdf
index f2ea382450000d1338d4be9e519bbd2d59b842d3..8b54a246aba9f6729cf231231c20209912f46d1d 100644
GIT binary patch
delta 1174
zcmca+a>ZoBH^zEnE;~D};*z4	Xe5IY)y|7tJvc*z-R8jG+9Q7rGOReok2RW5R8<
zKMg`D2aayq7IyO4XXS*Wt0&}pb}?}++npYDS!AD#fe_cac7N8I3MB`PLlRx-KVoF=
z{hHsSu*fmFt%%Q6(xHScb!+bJ&$1%=*+p#EH)ngZ@Xipv^C~B>{(JA{r+oap|DE1H
z-n2XDr~O;Ityet?7sz5$)=gV-TZFKovz+?Kfz2cZgHWE9@Dy}HCZ)(KmSe(YGXIulUdgD{Di41
zOY@;u%ck#Kw#s(Kwv|@VP22a^|IK@}q_6UP+hvmvS#ws-sa&fll(s=hcn5R6?e~QG
z$kfV*enqA`l^?#l%d6dbVBMUpu_cvvcwE(`mH!nP~DLNnA8GUFIm
zJ?08yiaa}6X4>=G8{cL>TDj7LTVtBYjM&phK1F(*ePQ`KYxkvH%T~9ovAyz6aL-CT
z`RTuvCM~kzU!%yb{-8(6s&@N_T!+@=1g3};yb6k{ry~FKE!bcFpOKfX_W0(@%rgX-
z%q=G8ij*)JSxkNmq%6!QM~ap*8cjYABn>CKij^`NPu?x|q~27)00b2B6u7_)149FI
zGYm0H6LWMiBLgD~3^8LY>P(C*G1Qq_m_Wqp5f&SnnPFODVTj=vBTGy%V*^7Bj~E+b
zQD
z!DVWQso50hWemM0hM0OyjWI&i)XWqXT8i*^H#IlMaJ8w0naSimlD4rXCQgo)j&5dd
zMuv{AE*6%KCYH{|7Di64j&3e41_rKnHUw3~Ldu-PqLPZD)HE(rOCvKbRaIAiH!c9z
C<9I{>

delta 1176
zcmca&a?NDJH^zEXE;~D};*z4	Xe5IY&cI=iN3C*z;Ta+X|UOpL|zw6lq+LsVZJA
zpP=m)q;Q(^?6tptf+npLKXT{Rp$WX1>FH%Vl!Jd19q`cDA!jH0;bAAkLXKpOc@K*V
zD_+YRHFdD{N}jS5NoBYwIpylM-1FrDd)CZ0-Z3vWS}EheoCjg)_1(Mj=jfLIy6{0K
zLw@=zuc!Y*YA0RwC|n@p$@*QwljqzaV~)lx9ig7T-nmW{*r^e`D^pF!z3G@yw0V9G
z`@;W;(I39&^j}@_HoExKw}^Y2|FUiV_`z7}_(SIdp%X%T*VotG_c`_9K%05e@ru5f
zU>5_8HF-5Nvsc|}&YZn!vuZ{C@7wRy{9euYo@3$5y#CnCQ!}qiB^~AoOJOR1u>QH+
z8~xQf`gc6nsgg(pLU-qSzr;fw6v&4S7>Tz
zh!I?-hM1a7jSVmyV`^fAsn^sPBUDYzOkts=2#MNw)Rm#L+Z1(&L-tG^o;
E08vqR*8l(j

diff --git a/test/html/html_heading_color_attribute.pdf b/test/html/html_heading_color_attribute.pdf
index 3652b93086dc2c7c5a836c0ecd5dc473a20e79e8..9d11c0c37b7a92d8a39e187de5b473349b7c1d8a 100644
GIT binary patch
delta 404
zcmbQhJ&AimAY;8Lmz^C~aY<2XVlG$3oZPb;xegf!9Q*ip-XrPPLdBlRlS3~aTsuQu
zAbI5!5AP$_&9jzpJl*!V?t8e!B#~tYCvTH`&D=b7g3!ONy~`)MJ=vOeP^@=#>$->f
zCk{lh&P;!0=W&%abf3R-#KHTU+`TuOZF`Yb-miJ2rReMR0Jk0a%yKsS(|=djSIxP+
zyTNYJY^M(HmA#h_=`4?`V*IITS-SZR<3>g%Q`5=wnLpJVDHwo&LY@K_m|Br%chD`9cV~jS$)%1x+t%DTymjj)(H24)79m|~V@
z7-HsT<``lY7A6>CmKGM1f3Vob8d;hfxEdL`7&#g^o4Go>8kjj60-1*9#zv-2ZWb1H
eHUw3~LP9mMsHCDOHI2*Az|??CRn^tsjSB$Xpn!(}

diff --git a/test/html/html_list_vertical_margin.pdf b/test/html/html_list_vertical_margin.pdf
index a7834cfa84b09a813f96822c12a5e3ffeb5cfc27..2ddb6c0b57f45c26045651b1104af48d46e2e168 100644
GIT binary patch
delta 589
zcmca8dO>tU2xGkwmz^C~aY<2XVlG$3oT(SiW*u@6VSP|L?~%^svv23__X)bC$HKZ>
zpylRuhvcl4QIk9TAN{_jSf){AKI!hA!{sL~R-B$O=eXu<7MpV-Q`FL0OqVHIA8}vo
zF;(McK&qhKg!c2k{quL_7QC4)xB6jibotw~@0)JCpCx+jH{bc=@9V>PN^jdgWPGR{
z<@ssq|H+p=HdK_QX#d!x@b>Y{lhMl$OkbZpasK*5oyDoLlHc{`oNrI#R_(fXPjT4?
zjjTYOFS1Eo{fh+E3SXM%y_(VGJ$Gx!hJfWBue>uWCr>nqQrqgWY>Uk)kF05)%9Au}
zcb_u)%_@6nx(-)rUJAs|n}eCy+3SrI3_w63Pk{@}FfcH)FvbwG#1u0$Ff&0{XJ~9@
zfgxsMYJeeTZfXV*i$hp!XaN&M60OV

delta 557
zcmca0dQo&k2;=0dto)OoFbUM_Pxg506mX$6V8&kF8Oua}PhavwW#4+q#fzux){NwN
zBDas({jp|pk(~U^*aKgin=T#c*>I@#MBm5dflIum_c9w)6VcUQV{$
zlk^a}0LVGGC;$Ke

diff --git a/test/html/html_superscript.pdf b/test/html/html_superscript.pdf
index f29e0f9a579ba727729ed9cb0a59f22148dbeb20..cbb6f0a89b4fd994a942a3d74b30adcf833bc08b 100644
GIT binary patch
delta 95
zcmZ3=wUleac}AvXRg*6=>a#qNuv@U1h3P1hlYxt~o2jvrlZAnSg_EhPxtoQFiKBtJ
Vqp5+hftj(nft?K@6_dGHr2sN77?J=0

delta 95
zcmZ3=wUleac}AwRn#mU#^;sUO+0EL_!gQ3$$=J!!(8AKuz{JtW(b(9~)!EY6%*@Eb
V)y=@w$=Sr*(9VXCipkupQUD+q7?}V7

diff --git a/test/html/test_html.py b/test/html/test_html.py
index 73a487035..328c61948 100644
--- a/test/html/test_html.py
+++ b/test/html/test_html.py
@@ -739,7 +739,7 @@ def test_html_unsupported_tag_color():
     pdf = FPDF()
     pdf.add_page()
     with pytest.raises(NotImplementedError):
-        pdf.write_html("

foo

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

foo


bar

", tag_styles={"hr": TextStyle()}) def test_html_link_color_using_FontFace(tmp_path): @@ -779,7 +779,7 @@ def test_html_unsupported_tag_color_using_FontFace(): pdf = FPDF() pdf.add_page() with pytest.raises(NotImplementedError): - pdf.write_html("

foo

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

foo


bar

", tag_styles={"hr": FontFace()}) def test_html_blockquote_indent(tmp_path): # issue-1074 diff --git a/test/outline/html_toc.pdf b/test/outline/html_toc.pdf index f06a6167e7ddd3607f9f9df2c24ea4880ff1d1eb..4f5be8eb813defbe377bf41b9d41d246961bcffd 100644 GIT binary patch delta 998 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 O1O3o->=-sMT=ySuc}151 literal 20885 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 From b43f2138a22694600e15b9183b72726e9330bcb7 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:27:06 +0200 Subject: [PATCH 2/9] Implementing remaining TODOs --- fpdf/html.py | 49 ++++++++++++--------- test/html/html_custom_heading_sizes.pdf | Bin 2027 -> 2025 bytes test/html/html_customize_ul.pdf | Bin 1299 -> 1303 bytes test/html/html_customize_ul_deprecated.pdf | Bin 0 -> 1299 bytes test/html/html_heading_above_below.pdf | Bin 1590 -> 1596 bytes test/html/test_html.py | 36 +++++++++------ 6 files changed, 52 insertions(+), 33 deletions(-) create mode 100644 test/html/html_customize_ul_deprecated.pdf diff --git a/fpdf/html.py b/fpdf/html.py index 429221b5d..98faa9ef7 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -28,18 +28,30 @@ "code": FontFace(family="Courier"), # block tags: "blockquote": TextStyle(color="#64002d", t_margin=3, b_margin=3), - "center": TextStyle(), + "center": TextStyle(t_margin=4 + 7 / 30), "dd": TextStyle(l_margin=10), - "dt": TextStyle(), - "h1": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=24), - "h2": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=18), - "h3": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=14), - "h4": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=12), - "h5": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=10), - "h6": TextStyle(color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=8), + "dt": TextStyle(t_margin=4 + 7 / 30), + "h1": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=24, t_margin=5 + 834 / 900 + ), + "h2": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=18, t_margin=5 + 453 / 900 + ), + "h3": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=14, t_margin=5 + 199 / 900 + ), + "h4": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=12, t_margin=5 + 72 / 900 + ), + "h5": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=10, t_margin=5 - 55 / 900 + ), + "h6": TextStyle( + color="#960000", b_margin=0.4, font_size_pt=8, t_margin=5 - 182 / 900 + ), "li": TextStyle(l_margin=5, t_margin=2), "p": TextStyle(t_margin=4 + 7 / 30), - "pre": TextStyle(font_family="Courier"), + "pre": TextStyle(t_margin=4 + 7 / 30, font_family="Courier"), "ol": TextStyle(t_margin=2), "ul": TextStyle(t_margin=2), } @@ -455,6 +467,10 @@ def _new_paragraph( indent=0, bullet="", ): + if bullet and top_margin: + raise NotImplementedError( + f"{top_margin=} will be ignored because {bullet=} is provided, due to TextRegion._render_column_lines()" + ) self._end_paragraph() self.align = align or "" self._paragraph = self._column.paragraph( @@ -589,8 +605,7 @@ def handle_starttag(self, tag, attrs): line_height=( self.line_height_stack[-1] if self.line_height_stack else None ), - # TODO: use top_margin=tag_style.t_margin - top_margin=self.font_size / self.pdf.k, + top_margin=tag_style.t_margin, bottom_margin=tag_style.b_margin, indent=tag_style.l_margin, ) @@ -648,7 +663,6 @@ def handle_starttag(self, tag, attrs): indent=tag_style.l_margin, ) if tag in HEADING_TAGS: - prev_font_height = self.font_size / self.pdf.k self.style_stack.append( FontFace( family=self.font_family, @@ -668,8 +682,7 @@ def handle_starttag(self, tag, attrs): align = None self._new_paragraph( align=align, - # TODO: rm prev_font_height & hsize - top_margin=prev_font_height + tag_style.t_margin * hsize, + top_margin=tag_style.t_margin, bottom_margin=tag_style.b_margin * hsize, indent=tag_style.l_margin, ) @@ -743,8 +756,7 @@ def handle_starttag(self, tag, attrs): self._pre_formatted = True self._pre_started = True self._new_paragraph( - # TODO: top_margin=tag_style.t_margin, - top_margin=self.font_size / self.pdf.k, + top_margin=tag_style.t_margin, bottom_margin=tag_style.b_margin, indent=tag_style.l_margin, ) @@ -848,8 +860,6 @@ def handle_starttag(self, tag, attrs): self.line_height_stack[-1] if self.line_height_stack else None ), indent=tag_style.l_margin * self.indent, - # TODO: merge this top_margin with _ln() call above - top_margin=self.font_size / self.pdf.k, bottom_margin=tag_style.b_margin, bullet=bullet, ) @@ -983,8 +993,7 @@ def handle_starttag(self, tag, attrs): tag_style = self.tag_styles[tag] self._new_paragraph( align="C", - # TODO: use tag_style.t_margin - top_margin=self.font_size / self.pdf.k, + top_margin=tag_style.t_margin, bottom_margin=tag_style.b_margin, indent=tag_style.l_margin, ) diff --git a/test/html/html_custom_heading_sizes.pdf b/test/html/html_custom_heading_sizes.pdf index 298a62dc74cc2934140bd390053c7f1b1a47f7e4..90271ffccdbe33bf76679cd0da09920f1e42cb0c 100644 GIT binary patch delta 458 zcmaFO|B`=0AY;8Dmz^C~aY<2XVlG$3oYD(>xtt7n+8*u=|Jb|h$g=$}wKN`;DVQsC z)Ze~%@b>nO2a-XvR(`P4<+QN!-}&h6wyv}D4mR#-kJ}JzzFYFImC;6rJpsNKjH?uc z9{jG1+aLJTRD18;_!q^88n0h)4>!=A8YOM&969ZvUhoo|iup%>s5qF+yd?Fpf%9a- zq_azm?znwno9wIRz0e|Q&Dv@9#yj~rR;QlY{Eg9qiP=cce6k@+4Vd1*Qp;#ES&y}z z(PHvC*0*AY777L+ppd7)1!fo+m>HT+-pD3wYl0zTZf=SpW?^ZGE@o(8WICCVUD(zL zL&nI$07J~g0K*N2rUsUi1KDk3P2F4!4NcrkOw3$eo!rb!j9uLfT`ZhUon72qoh?iZ f>}&|Ch=s&cVo^y&QED2Op{cncm#V6(zZ(|-aHx_S delta 489 zcmaFK|C)b8AY;8Tmz^C~aY<2XVlG$3oYD*XxtI)j*gov_|Jb|h$`aq(ceFGfm0b`Q zI8uLmZu9zl(Sy=KQW`A#;+>fK`f9W)cIF;UJOAK8XMp~VQ1hLfYh`b4cJizhykZ>X zVA^nN_WJt)|5C%gmenu1%E=zL*lt!+;YyL*`3rR>AN2G3;+$@$wtAz3$xIuO;zoYq zBMC>+T6SIUNHI8>5;V~+Io9<5%!_*&zY3=A*!+djf{EE!&vLRpOAU}VoV<>umeFXk z4r@J-T*Lad-pD|~00b2B6u7_)0|PTdOAIk{12ar9b8`$aO9Ml6F+&3*40VQvrY2x@ z=13MB8d(@&Xf`p#aG;^7q2XkIcH3A(GfPVsLq|(XOA`|(Q&ST&LklBE3o}ClQ!^tM iCr39s8-glgA<>vvR8motn#N^lYHrM>s_N?R#svTu<&zZv diff --git a/test/html/html_customize_ul.pdf b/test/html/html_customize_ul.pdf index d85f8d5251d1799b34e5f6ffa73594071da64d81..e7c9985099f034f92e550d0943caf6d00360d157 100644 GIT binary patch delta 453 zcmbQtHJxjNBV)Zemz^C~aY<2XVlG$3oV62uvkn^ww0^fcs&n_Oi@ous6{npxztCHt ztfHxBvQY7Y^cSJMPT%*&=_-p@3toA!VD3HV`;R9#PIptR{*g0JY0WjMHBL(v-aWK5 zlYek88%j+qL13tQPc@|}5Vb2wrd^c9bD*=}UJI{&ivmiV1K^|QZ-8Ai!X4bLuS z-1hwL%UJ%uCntOp+_t;6{KdJu9~Z1H*``saw|7eZ%V+YtG{2mU=;Pj}*UmOYIC%PJ z+nLAridt*wPn-Pm$*ZDOUxN0{mf_RdTch3nJA%3T+{deX=AX35zqmTEa^oDI?Tkmy zE}j1A?vn`-DZvvTYYLw9>|6irucY6zPXGC3njk~2E%*>qrKvypGrLo_VBqEurq_(N z777L+ppd7)1!fo+m>Zg7h*=n#Vu)E98cnWYv5qlvbv7_`GjlXHFmSUpwlp(vHFR=y oc6D?%b}}|Fb~d-OA*do25=V(eB^5=fX~Qe*gdg delta 449 zcmbQvHJNLJBV)ZWmz^C~aY<2XVlG$3oTU>Svkn^wxPG@gs&n^jiptrFCwk0h7wvB7 zo3LcZg_bsl{0rVsxbA+h+S-$mbA6eHoAuxGd(LnE5F^la$bBQ7C~R{UKr^VB0{?Zt56&->Uvi|%ndy!}&>W#>7|l6lLYpZLkx880ZfIfCgmqpgX8 z0SGAMDR6-q1_tJa#u#E2hK3komIfA+8(6GkObyMb2EnS_>%*@;joDD4vjm*uB m4K2(pj7*(f?Q96Dh=s&YVo^y&QED2OrI{s{s;aBM8y5gA2DH=w diff --git a/test/html/html_customize_ul_deprecated.pdf b/test/html/html_customize_ul_deprecated.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d85f8d5251d1799b34e5f6ffa73594071da64d81 GIT binary patch literal 1299 zcmbtUT}TvB6efaIlf#Co1o54w}SN?zXgR=#e-v@PN0ZGa^%}wz*PYA zT@u}`-P3{qU#&DDlPfCXkRE1qHVbW43(`wMM8$fN6Y#Q1%77l#G)lliE0jGrN&sJ_ zBQMsX5q$8dl3?dodm^ALB-+T5lX`)?W1H%7s6N;zQRy@5t=DUwSp8e`IM( zn=dot^ODY~@9)yH7j-Y)zxu=3wzP$P_3069+2!o#g>k6VZy}flu<}-361+&jtJ9+bVm?GwR>G z+dYugbLnT_it)1Wm8V-5Pu@+NjLQ#hPBcgzPbAWP@$IAOc^x~mve?-t6-m0lVUg3L zm`KL-K=@iUg3P=ijwtk){W=v^)tdmnRn<#$Q%wOJUlVO033D-vQCH45jd55`O#Dj< z57xr}pMm-1wDtC`P*tM5x6)$)`8@ z4Afg%a9I*~siD}U_^9VVu5TlhL6iFHCci*qm+q~kjWlLXps3L}Q1YRE`j zJ~e79qHXX&j0s@Z2NgG<8oA%c{>1z}Z|9{{U;r0Jf+0lh7KTG^ypOfgFX3^7YX6Lc{{10!<`F+&rG-pLDD zEn*Fw%uGxzT+EzJj10^z%*@P83{1?OTujW3&48L6P3>$5s)&WeLt;@$MNw)Rm!Xk? M5tpi}tG^o;09PoD$p8QV delta 421 zcmdnPvyEp%0AszWC6}EYS8+*EYGN)|#hl)=d$|r7h_F7e^)EcO>xhc)trXEW3VKKP zG|Y%n{u2N3EALVzzP6(6hi#t=TL`#?_KDw&%;DzK(@5SB{zg~wM1#QcIYqs9JZ5=C zvYG{>yy;J2$kXO$2$6<5l=D+5BW4mU7%6;CYD`uKcN;VJE<*l)|o&EG9dve<+ zdx`7vYd$>w(oqnpcfD}t1??Tdr7xqzKTh#fj`O@%R1*1oGc%Jm6N{0asmWwRmhbgO zW(o!%ppd7)1!fo+m>HR3h?yH2VTxH8V~ANAn4^ms8Wr zG`4hca&vPwF>-PhL&brs;aL3 GZd?G8ABIB! diff --git a/test/html/test_html.py b/test/html/test_html.py index 328c61948..a796f43a6 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -206,7 +206,7 @@ def test_html_customize_ul(tmp_path): for indent, bullet in ((5, "\x86"), (10, "\x9b"), (15, "\xac"), (20, "\xb7")): pdf.write_html( html, - tag_styles={"li": TextStyle(l_margin=indent, t_margin=2)}, + tag_styles={"li": TextStyle(l_margin=indent, b_margin=2)}, ul_bullet_char=bullet, ) pdf.ln() @@ -226,10 +226,10 @@ def test_html_customize_ul_deprecated(tmp_path): for indent, bullet in ((5, "\x86"), (10, "\x9b"), (15, "\xac"), (20, "\xb7")): pdf.write_html(html, li_tag_indent=indent, ul_bullet_char=bullet) pdf.ln() - assert_pdf_equal(pdf, HERE / "html_customize_ul.pdf", tmp_path) + assert_pdf_equal(pdf, HERE / "html_customize_ul_deprecated.pdf", tmp_path) -def test_html_deprecated_li_tag_indent_deprecated(tmp_path): +def test_html_li_tag_indent_deprecated(tmp_path): pdf = FPDF() pdf.add_page() with pytest.warns(DeprecationWarning): @@ -389,22 +389,22 @@ def test_html_custom_heading_sizes(tmp_path): # issue-223
This is a H6
""", tag_styles={ "h1": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=6 + color="#960000", t_margin=5 + 834 / 900, b_margin=0.4, font_size_pt=6 ), "h2": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=12 + color="#960000", t_margin=5 + 453 / 900, b_margin=0.4, font_size_pt=12 ), "h3": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=18 + color="#960000", t_margin=5 + 199 / 900, b_margin=0.4, font_size_pt=18 ), "h4": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=24 + color="#960000", t_margin=5 + 72 / 900, b_margin=0.4, font_size_pt=24 ), "h5": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=30 + color="#960000", t_margin=5 - 55 / 900, b_margin=0.4, font_size_pt=30 ), "h6": TextStyle( - color="#960000", t_margin=0.2, b_margin=0.4, font_size_pt=36 + color="#960000", t_margin=5 - 182 / 900, b_margin=0.4, font_size_pt=36 ), }, ) @@ -725,10 +725,16 @@ def test_html_headings_color(tmp_path): html, tag_styles={ "h1": TextStyle( - color=(148, 139, 139), font_size_pt=24, t_margin=0.2, b_margin=0.4 + color=(148, 139, 139), + font_size_pt=24, + t_margin=5 + 834 / 900, + b_margin=0.4, ), "h2": TextStyle( - color=(148, 139, 139), font_size_pt=18, t_margin=0.2, b_margin=0.4 + color=(148, 139, 139), + font_size_pt=18, + t_margin=5 + 453 / 900, + b_margin=0.4, ), }, ) @@ -1065,8 +1071,12 @@ def test_html_heading_above_below(tmp_path):

Second heading

Lorem ipsum

""", tag_styles={ - "h1": TextStyle(color="#960000", t_margin=1, b_margin=0.5, font_size_pt=24), - "h2": TextStyle(color="#960000", t_margin=1, b_margin=0.5, font_size_pt=18), + "h1": TextStyle( + color="#960000", t_margin=10, b_margin=0.5, font_size_pt=24 + ), + "h2": TextStyle( + color="#960000", t_margin=10, b_margin=0.5, font_size_pt=18 + ), }, ) assert_pdf_equal(pdf, HERE / "html_heading_above_below.pdf", tmp_path) From b7de0f873d1995d7013a4ca7efcaa4df53e3e978 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:40:31 +0200 Subject: [PATCH 3/9] Supporting all FontFace attributes for tags --- fpdf/html.py | 19 +++++++++++----- ...tml_link_color.pdf => html_link_style.pdf} | Bin 1141 -> 1313 bytes test/html/test_html.py | 21 ++++++++++-------- 3 files changed, 26 insertions(+), 14 deletions(-) rename test/html/{html_link_color.pdf => html_link_style.pdf} (55%) diff --git a/fpdf/html.py b/fpdf/html.py index 98faa9ef7..767d298a9 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -24,7 +24,7 @@ HEADING_TAGS = ("h1", "h2", "h3", "h4", "h5", "h6") DEFAULT_TAG_STYLES = { # inline tags: - "a": FontFace(color="#00f"), + "a": FontFace(color="#00f", emphasis="UNDERLINE"), "code": FontFace(family="Courier"), # block tags: "blockquote": TextStyle(color="#64002d", t_margin=3, b_margin=3), @@ -1157,18 +1157,27 @@ def set_text_color(self, r=None, g=0, b=0): self.pdf.page = prev_page def put_link(self, text): - # Put a hyperlink + "Put a hyperlink" + prev_style = FontFace( + family=self.font_family, + emphasis=self.emphasis, + size_pt=self.font_size, + color=self.font_color, + ) tag_style = self.tag_styles["a"] if tag_style.color: self.set_text_color(*tag_style.color.colors255) + if tag_style.emphasis: + self.emphasis = tag_style.emphasis self.set_font( family=tag_style.family or self.font_family, size=tag_style.size_pt or self.font_size, ) - self.set_style("u", True) self._write_paragraph(text, link=self.href) - self.set_style("u", False) - self.set_text_color(*self.font_color) + # Restore previous style: + self.emphasis = prev_style.emphasis + self.set_font(prev_style.family, prev_style.size_pt) + self.set_text_color(*prev_style.color.colors255) # pylint: disable=no-self-use def render_toc(self, pdf, outline): diff --git a/test/html/html_link_color.pdf b/test/html/html_link_style.pdf similarity index 55% rename from test/html/html_link_color.pdf rename to test/html/html_link_style.pdf index d11bf1bb2000d50c3c804a68d28677ff48738bbc..8d5f0b44aa9576fe6367c4a651f861547941f739 100644 GIT binary patch delta 458 zcmey$v5;%R2lkATk^(Dz{fQrJxC~7U^-L`kEDTI1n=<+`non+KbgnlvGUc+f<0>vG zN=?k=s+ePYW;fR%2Z7dyb@LugiLyAiMtbu$M%^Q72KoxS1ob_8I9@-tZawjm&p!UZ zyq48ctx|`JmR~raw1F>rai`jky+PMnR7|s5<}MVt|N2p_!>^~C50~zEY`bX6p`XQX z=J%gjzH$EHyu7~?yUYbw9SYnaz!PUZ<>JDn)As%1P&Aor#?&b5lvteVmY-LmpzoYt zT9lbur0bNQlj5J0lUZ1rIysZc3Fz=OOww#z`ff%FW|I#y*)v*9W@dI~w4ChD92sO} zq+kF73V8}#V1|K#nV}_yn7N?^hM0wsDTbJ(k3%z`6UWK(-nfa^gZ*^ z@)dw4fY~kz(KeQ57M5nlMwU*dP6kfKF3ygQ#;)du2F7lNre>Coh8A{&Rm4K#A+e~W Tq9`?u%hJM_OI6j?-;E0Zmn)Oz delta 375 zcmZ3;^_647hl$^9I8Ds;3@sEa3=AjRGWs%_P3~rNuD7)0va{nVE-6Y)%;l<>V|!*J zpM!%y%fq_)NnTvp-}{`k7hZ_n)PKO!#Od+Zxo*pTe@>MV6Uwu;F_C%|xvP;wYWIofHn+&${CA@8y2Ih}AkOwTW-2aSUznb46<6U(%}asW z24v+=&R{B*%ZfNOdU`JR*EF}06i%KerQq#CBj19R|RbBnvxB!Stbuj<{ diff --git a/test/html/test_html.py b/test/html/test_html.py index a796f43a6..c163d34cf 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -4,7 +4,6 @@ from fpdf import FPDF, FontFace, HTMLMixin, TextStyle, TitleStyle from fpdf.drawing import DeviceRGB -from fpdf.html import color_as_decimal from fpdf.errors import FPDFException from test.conftest import assert_pdf_equal, LOREM_IPSUM @@ -698,12 +697,13 @@ def test_html_and_section_title_styles_with_deprecated_TitleStyle(): ) -def test_html_link_color(tmp_path): +def test_html_link_style(tmp_path): pdf = FPDF() pdf.add_page() - html = 'foo' - pdf.write_html(html, tag_styles={"a": TextStyle(color=color_as_decimal("red"))}) - assert_pdf_equal(pdf, HERE / "html_link_color.pdf", tmp_path) + html = 'Link to www.example.com' + style = FontFace(color="#f00", family="Courier", size_pt=8, emphasis="BIU") + pdf.write_html(html, tag_styles={"a": style}) + assert_pdf_equal(pdf, HERE / "html_link_style.pdf", tmp_path) def test_html_blockquote_color(tmp_path): @@ -748,12 +748,15 @@ def test_html_unsupported_tag_color(): pdf.write_html("

foo


bar

", tag_styles={"hr": TextStyle()}) -def test_html_link_color_using_FontFace(tmp_path): +def test_html_link_style_using_TextStyle(tmp_path): pdf = FPDF() pdf.add_page() - html = 'foo' - pdf.write_html(html, tag_styles={"a": FontFace(color=color_as_decimal("red"))}) - assert_pdf_equal(pdf, HERE / "html_link_color.pdf", tmp_path) + html = 'Link to www.example.com' + style = TextStyle( + color="#f00", font_family="Courier", font_size_pt=8, font_style="BIU" + ) + pdf.write_html(html, tag_styles={"a": style}) + assert_pdf_equal(pdf, HERE / "html_link_style.pdf", tmp_path) def test_html_blockquote_color_using_FontFace(tmp_path): From ea2d38daa7098fd486eadeb02b52b4d972609919 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:48:53 +0200 Subject: [PATCH 4/9] Extra clean-up --- fpdf/html.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/fpdf/html.py b/fpdf/html.py index 767d298a9..9dacd8af7 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -697,6 +697,8 @@ def handle_starttag(self, tag, attrs): color = tag_style.color.colors255 if color: self.set_text_color(*color) + 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, @@ -732,6 +734,8 @@ def handle_starttag(self, tag, attrs): 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, @@ -749,6 +753,8 @@ def handle_starttag(self, tag, attrs): 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, @@ -778,13 +784,10 @@ def handle_starttag(self, tag, attrs): family=tag_style.family or self.font_family, size=tag_style.size_pt or self.font_size, ) - self.indent += 1 self._new_paragraph( - # Default values to be multiplied by the conversion factor - # for top_margin and bottom_margin here are given in mm top_margin=tag_style.t_margin, bottom_margin=tag_style.b_margin, - indent=tag_style.l_margin * self.indent, + indent=tag_style.l_margin, ) if tag == "ul": self.indent += 1 @@ -1061,7 +1064,6 @@ def handle_endtag(self, tag): self.set_font(font_face.family, font_face.size_pt) self.set_text_color(*font_face.color.colors255) self._end_paragraph() - self.indent -= 1 if tag in ("strong", "dt"): tag = "b" if tag == "em": From eebb8bde937ebf1bc7f5f824f4902efaefb25fb2 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:17:02 +0200 Subject: [PATCH 5/9] Handling those HTML tags in the same place: "blockquote", "center", "code", "dd", "dt", "pre" --- fpdf/html.py | 186 ++++++------------ test/html/html_blockquote_color.pdf | Bin 1174 -> 1169 bytes .../html_blockquote_color_using_FontFace.pdf | Bin 1170 -> 1167 bytes test/html/html_description.pdf | Bin 1166 -> 1166 bytes 4 files changed, 60 insertions(+), 126 deletions(-) diff --git a/fpdf/html.py b/fpdf/html.py index 9dacd8af7..5d8807a08 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -25,12 +25,18 @@ DEFAULT_TAG_STYLES = { # inline tags: "a": FontFace(color="#00f", emphasis="UNDERLINE"), + "b": FontFace(emphasis="BOLD"), "code": FontFace(family="Courier"), + "em": FontFace(emphasis="ITALICS"), + "font": FontFace(), + "i": FontFace(emphasis="ITALICS"), + "strong": FontFace(emphasis="BOLD"), + "u": FontFace(emphasis="UNDERLINE"), # block tags: "blockquote": TextStyle(color="#64002d", t_margin=3, b_margin=3), "center": TextStyle(t_margin=4 + 7 / 30), "dd": TextStyle(l_margin=10), - "dt": TextStyle(t_margin=4 + 7 / 30), + "dt": TextStyle(font_style="B", t_margin=4 + 7 / 30), "h1": TextStyle( color="#960000", b_margin=0.4, font_size_pt=24, t_margin=5 + 834 / 900 ), @@ -599,27 +605,6 @@ def handle_starttag(self, tag, attrs): self._end_paragraph() # pylint: disable=protected-access self.pdf._perform_page_break() - if tag == "dt": - tag_style = self.tag_styles[tag] - self._new_paragraph( - line_height=( - self.line_height_stack[-1] if self.line_height_stack else None - ), - top_margin=tag_style.t_margin, - bottom_margin=tag_style.b_margin, - indent=tag_style.l_margin, - ) - tag = "b" - if tag == "dd": - tag_style = self.tag_styles[tag] - self._new_paragraph( - line_height=( - self.line_height_stack[-1] if self.line_height_stack else None - ), - top_margin=tag_style.t_margin, - bottom_margin=tag_style.b_margin, - indent=tag_style.l_margin * (self.indent + 1), - ) if tag == "strong": tag = "b" if tag == "em": @@ -638,6 +623,25 @@ def handle_starttag(self, tag, attrs): pass if tag == "br": self._write_paragraph("\n") + if tag == "hr": + self._end_paragraph() + width = css_style.get("width", attrs.get("width")) + if width: + if width[-1] == "%": + width = self.pdf.epw * int(width[:-1]) / 100 + else: + width = int(width) / self.pdf.k + else: + width = self.pdf.epw + # Centering: + x_start = self.pdf.l_margin + (self.pdf.epw - width) / 2 + self.pdf.line( + x1=x_start, + y1=self.pdf.y, + x2=x_start + width, + y2=self.pdf.y, + ) + self._write_paragraph("\n") if tag == "p": align = None if "align" in attrs: @@ -703,45 +707,10 @@ def handle_starttag(self, tag, attrs): family=tag_style.family or self.font_family, size=tag_style.size_pt or self.font_size, ) - if tag == "hr": - self._end_paragraph() - width = css_style.get("width", attrs.get("width")) - if width: - if width[-1] == "%": - width = self.pdf.epw * int(width[:-1]) / 100 - else: - width = int(width) / self.pdf.k - else: - width = self.pdf.epw - # Centering: - x_start = self.pdf.l_margin + (self.pdf.epw - width) / 2 - self.pdf.line( - x1=x_start, - y1=self.pdf.y, - x2=x_start + width, - y2=self.pdf.y, - ) - self._write_paragraph("\n") - if tag == "code": - self.style_stack.append( - FontFace( - family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, - color=self.font_color, - ) - ) - tag_style = self.tag_styles[tag] - if tag_style.color: - self.set_text_color(*tag_style.color.colors255) - 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, - ) - if tag == "pre": - self._end_paragraph() + if tag in ("blockquote", "center", "code", "dd", "dt", "pre"): + is_block = tag in ("blockquote", "center", "dd", "dt", "pre") + if is_block: + self._end_paragraph() self.style_stack.append( FontFace( family=self.font_family, @@ -759,36 +728,19 @@ def handle_starttag(self, tag, attrs): family=tag_style.family or self.font_family, size=tag_style.size_pt or self.font_size, ) - self._pre_formatted = True - self._pre_started = True - self._new_paragraph( - top_margin=tag_style.t_margin, - bottom_margin=tag_style.b_margin, - indent=tag_style.l_margin, - ) - if tag == "blockquote": - self.style_stack.append( - FontFace( - family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, - color=self.font_color, + if tag == "pre": + self._pre_formatted = True + self._pre_started = True + if is_block: + self._new_paragraph( + align="C" if tag == "center" else None, + line_height=( + self.line_height_stack[-1] if self.line_height_stack else None + ), + top_margin=tag_style.t_margin, + bottom_margin=tag_style.b_margin, + indent=tag_style.l_margin * (self.indent + 1), ) - ) - tag_style = self.tag_styles[tag] - if tag_style.color: - self.set_text_color(*tag_style.color.colors255) - if tag_style.emphasis: - self.emphasis = tag_style.emphasis - self.set_font( - family=tag_style.family or self.font_family, - size=tag_style.size_pt or self.font_size, - ) - self._new_paragraph( - top_margin=tag_style.t_margin, - bottom_margin=tag_style.b_margin, - indent=tag_style.l_margin, - ) if tag == "ul": self.indent += 1 bullet_char = ( @@ -992,14 +944,6 @@ def handle_starttag(self, tag, attrs): self.pdf.image( self.image_map(attrs["src"]), x=x, w=width, h=height, link=self.href ) - if tag == "center": - tag_style = self.tag_styles[tag] - self._new_paragraph( - align="C", - top_margin=tag_style.t_margin, - bottom_margin=tag_style.b_margin, - indent=tag_style.l_margin, - ) if tag == "toc": self._end_paragraph() self.pdf.insert_toc_placeholder( @@ -1038,32 +982,6 @@ def handle_endtag(self, tag): tag, self._tags_stack[-1], ) - 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() - 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() if tag in ("strong", "dt"): tag = "b" if tag == "em": @@ -1076,6 +994,24 @@ def handle_endtag(self, tag): if tag == "p": self._end_paragraph() self.align = "" + if tag in HEADING_TAGS: + self.heading_level = None + font_face = self.style_stack.pop() + self.emphasis = font_face.emphasis + self.set_font(font_face.family, font_face.size_pt) + self.set_text_color(*font_face.color.colors255) + self._end_paragraph() + if tag in ("blockquote", "center", "code", "dd", "dt", "pre"): + is_block = tag in ("blockquote", "center", "dd", "dt", "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) + if tag == "pre": + self._pre_formatted = False + self._pre_started = False + if is_block: + self._end_paragraph() if tag in ("ul", "ol"): self._end_paragraph() self.indent -= 1 @@ -1106,8 +1042,6 @@ def handle_endtag(self, tag): 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": self.pdf.char_vpos = "LINE" if tag == "sub": diff --git a/test/html/html_blockquote_color.pdf b/test/html/html_blockquote_color.pdf index ed19d6962e7431cf24a914cb0c37a58727751ca5..0f888791e99e209e85dd2fc1d2ecc9edffc551d2 100644 GIT binary patch delta 316 zcmbQnIgxXNBV)ZGmz^C~aY<2XVlG$3oY1qUxegf!v_7nz_eke;+MGKcn?y?qY5HEiiepD?GD3cT@`x+P$|l3-W8(ZuqZs)i-DcV_W`i(z)w@Mk$u$3&mcAnvvFW0>P0!tS9>^$o%Eg!!& zT%YHO_MOdF8Q(J68YvinfI^-E7nosSU}|BAA!dd}%-q~!@_%OQ7*}Iw12-c>7b90= zM^jTbQ)5GOOIJ4+Gb3XYM{_e%GdmlCDq0J z;?P@|xzKLk!D*{5x1KIP7r28{?e&I^;v+A$6h#e{(_$7$9PRjaZPJqVbl-yteE&B) z`>dDzKE>PD{Yd)b3v1`_rkKvTo?<66nRCsz=3g0FUl*TRS|eJ0Lp1WgHr=Y-ws|Lr_I5BzzK!N-By{)3_{5Ot@54UH#p- E0Q)0w+5i9m diff --git a/test/html/html_blockquote_color_using_FontFace.pdf b/test/html/html_blockquote_color_using_FontFace.pdf index b0dbc49becaa42f8ee256db685ebd08bd7e043df..509300f218eaf892f01079a5392943c5d843885f 100644 GIT binary patch delta 315 zcmbQl+0VJbfwA7mlFQDHtGJ{nH8Gc~VovDU(_DuP1X>@~&U>WuI&Dsj*QK=y;_tXV zGfh=%nw`LN<@j~6+(J`>KXcAMSTrN5KXNY1@i=Gcq&Af~uC~*bUtPk?6#Ysj#Yy>N zXkoAB^ooW%*9D?qG^=iJ^I4a?S$4{8b_44ROXlonmMA;9{f~Mt|H@-V{i#*|wm9x( z@=o10Dk&1@ax}FtQYH4h2 w;bZ_5Hg$G2Gqo@`bv1M{GcdQaA*do25-^EHB^5=fXz!UXSk`Dm9}3vb8;lhtb#zPOqZ2&6NLqi6h2<~_y3y( zA59<2U7qk*@RyFvE!%MRxQa^>i%KerQq#CBjLo=IRbBnvxB$Y6 BBWeHu delta 128 zcmeC Date: Mon, 1 Jul 2024 15:49:01 +0200 Subject: [PATCH 6/9] Handling those HTML tags in the same place: "b", "em", "i", "strong", "u" --- fpdf/fpdf.py | 16 ++++-- fpdf/html.py | 75 +++++++++++++------------- test/html/html_description.pdf | Bin 1166 -> 1164 bytes test/html/html_measurement_units.pdf | Bin 1307 -> 1302 bytes test/html/html_table_with_bgcolor.pdf | Bin 1657 -> 1657 bytes test/html/test_html.py | 2 +- 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 3753d9e60..5674292c9 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -293,11 +293,12 @@ def __init__( # Graphics state variables defined as properties by GraphicsStateMixin. # We set their default values here. self.font_family = "" # current font family - self.font_style = "" # current font style + # current font style (BOLD/ITALICS - does not handle UNDERLINE): + self.font_style = "" + self.underline = False # underlining flag self.font_size_pt = 12 # current font size in points self.font_stretching = 100 # current font stretching self.char_spacing = 0 # current character spacing - self.underline = False # underlining flag self.current_font = None # None or an instance of CoreFont or TTFFont self.draw_color = self.DEFAULT_DRAW_COLOR self.fill_color = self.DEFAULT_FILL_COLOR @@ -410,11 +411,18 @@ def _set_min_pdf_version(self, version): self.pdf_version = max(self.pdf_version, version) @property - def is_ttf_font(self): + def emphasis(self) -> TextEmphasis: + "The current text emphasis: bold, italics and/or underlined." + return TextEmphasis.coerce( + f"{self.font_style}U" if self.underline else self.font_style + ) + + @property + def is_ttf_font(self) -> bool: return self.current_font and self.current_font.type == "TTF" @property - def page_mode(self): + def page_mode(self) -> PageMode: return self._page_mode @page_mode.setter diff --git a/fpdf/html.py b/fpdf/html.py index 5d8807a08..14c871192 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -592,28 +592,19 @@ def _write_data(self, data): " You can open up an issue on github.com/py-pdf/fpdf2 if this is something you would like to see implemented." ) self.pdf.start_section(data, self.heading_level - 1, strict=False) - LOGGER.debug(f"write: '%s' h={self.h:.2f}", data) self._write_paragraph(data) def handle_starttag(self, tag, attrs): self._pre_started = False attrs = dict(attrs) - LOGGER.debug("STARTTAG %s %s", tag, attrs) css_style = parse_css_style(attrs.get("style", "")) self._tags_stack.append(tag) if css_style.get("break-before") == "page": self._end_paragraph() # pylint: disable=protected-access self.pdf._perform_page_break() - if tag == "strong": - tag = "b" - if tag == "em": - tag = "i" - if tag in ("b", "i", "u"): - if self.td_th is not None: - self.td_th[tag] = True - else: - self.set_style(tag, True) + if tag in ("b", "i", "u") and self.td_th is not None: + self.td_th[tag] = True if tag == "a": self.href = attrs["href"] try: @@ -702,12 +693,24 @@ def handle_starttag(self, tag, attrs): if color: self.set_text_color(*color) if tag_style.emphasis: - self.emphasis = 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, ) - if tag in ("blockquote", "center", "code", "dd", "dt", "pre"): + if tag in ( + "b", + "blockquote", + "center", + "code", + "em", + "i", + "dd", + "dt", + "pre", + "strong", + "u", + ): is_block = tag in ("blockquote", "center", "dd", "dt", "pre") if is_block: self._end_paragraph() @@ -723,7 +726,7 @@ def handle_starttag(self, tag, attrs): if tag_style.color: self.set_text_color(*tag_style.color.colors255) if tag_style.emphasis: - self.emphasis = 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, @@ -739,7 +742,7 @@ def handle_starttag(self, tag, attrs): ), top_margin=tag_style.t_margin, bottom_margin=tag_style.b_margin, - indent=tag_style.l_margin * (self.indent + 1), + indent=tag_style.l_margin, ) if tag == "ul": self.indent += 1 @@ -933,14 +936,6 @@ def handle_starttag(self, tag, attrs): x = self.pdf.get_x() if self.align and self.align[0].upper() == "C": x = Align.C - LOGGER.debug( - 'image "%s" x=%d y=%d width=%d height=%d', - attrs["src"], - x, - self.pdf.get_y(), - width, - height, - ) self.pdf.image( self.image_map(attrs["src"]), x=x, w=width, h=height, link=self.href ) @@ -962,7 +957,6 @@ def handle_starttag(self, tag, attrs): self._page_break_after_paragraph = True def handle_endtag(self, tag): - LOGGER.debug("ENDTAG %s", tag) while ( self._tags_stack and tag != self._tags_stack[-1] @@ -982,13 +976,6 @@ def handle_endtag(self, tag): tag, self._tags_stack[-1], ) - if tag in ("strong", "dt"): - tag = "b" - if tag == "em": - tag = "i" - if tag in ("b", "i", "u"): - if not self.td_th is not None: - self.set_style(tag, False) if tag == "a": self.href = "" if tag == "p": @@ -1001,7 +988,19 @@ def handle_endtag(self, tag): self.set_font(font_face.family, font_face.size_pt) self.set_text_color(*font_face.color.colors255) self._end_paragraph() - if tag in ("blockquote", "center", "code", "dd", "dt", "pre"): + if tag in ( + "b", + "blockquote", + "center", + "code", + "em", + "i", + "dd", + "dt", + "pre", + "strong", + "u", + ): is_block = tag in ("blockquote", "center", "dd", "dt", "pre") font_face = self.style_stack.pop() self.emphasis = font_face.emphasis @@ -1061,13 +1060,14 @@ def set_font(self, family=None, size=None, set_default=False): if size: self.font_size = size self.h = size / self.pdf.k - style = self.emphasis.style - LOGGER.debug(f"set_font: %s style=%s h={self.h:.2f}", self.font_family, style) prev_page = self.pdf.page if not set_default: # make sure there's at least one font defined in the PDF. self.pdf.page = 0 - if (self.font_family, style) != (self.pdf.font_family, self.pdf.font_style): - self.pdf.set_font(self.font_family, style, self.font_size) + if (self.font_family, self.emphasis) != ( + self.pdf.font_family, + self.pdf.emphasis, + ): + self.pdf.set_font(self.font_family, self.emphasis.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 @@ -1080,7 +1080,6 @@ def set_style(self, tag, enable): else: self.emphasis = self.emphasis.remove(emphasis) style = self.emphasis.style - LOGGER.debug("SET_FONT_STYLE %s", style) prev_page = self.pdf.page self.pdf.page = 0 self.pdf.set_font(style=style) @@ -1104,7 +1103,7 @@ def put_link(self, text): if tag_style.color: self.set_text_color(*tag_style.color.colors255) if tag_style.emphasis: - self.emphasis = 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, diff --git a/test/html/html_description.pdf b/test/html/html_description.pdf index 044a8d361b996235b30de495af2761f8858ffed9..9a32ba41f01d8fe5e31a4fb18e33fc86c266a0a2 100644 GIT binary patch delta 261 zcmeCV7Di?+7LIPN20#fr8-glgx$Nw?ic1oUN-By{)3_{* MO}JE5UH#p-0D7`dcK`qY delta 263 zcmeC-?Bm?vz{q55FxiPwCy|9yO8S*#ltzSv#Ouge8ZAp0L?snPGM6k7Qx{Jao09VE zx#pCVX-}49X|<#*lv=WoL5z78yUE!rLFcbT9K3T$AWN!?QFEng=S<6^9;Xzi&7M6o z_;OKju#yh{6D9_qde4QM&oaJZ)Uq&CFaQCCJOwT=!@$7Q!T>|e%-mw~LuO${)5%|% ztz!%foJ>v494#yj42>+DU5pG2U7U;!4a}X~EL>bn%^mG*2&#zXva{nVE=epZsVGWK Q^a}0Pzb>5dZ)H diff --git a/test/html/html_measurement_units.pdf b/test/html/html_measurement_units.pdf index dfa0b74aa9a043d4084ea71a889578fd6ac9bddf..f1dee4707b82f17d939a0029c3560aeb1b43be63 100644 GIT binary patch delta 454 zcmbQuHH~Y717p3J8JC?MS8+*EYGN)|#hj^^PWl}-5MX;yJuj*6q0!fUG1^g_c~k!| ztuWP4ouKFTL1~YEsmvYM;*~Y0|NK1mE0ys)uXFCh-80!|=vo#u_Bd9wx@LU)x%kNm zS&L^EwK=3S9x@&(lW~^I4v||^u8|{Wyv3_HVR>;y^S5%Y!&=exUW>1YKm4Y7l>fn! z7iVOXc*O%JED9~}FpY7EJQ~$$%EY@^D>1|GwaIL$-I2#9-U`25qTV{CPmR^{nn|pe zV(UUr&q=1Wx4jg-_0KGr)H)@u!$)bmTmQQ~^4H$~Ua#ot@lk#5^cap~USAd)M0b7e z&XIpySo@-ErL>)5>B|1_aLFrjTUnBSe0d*QBYfHWy8Qq3yR-f=+-cvyu{nn66{DS@ zxq<-*DC8+{ff)t{=7we%Vitxb7-E)&hLc-ZtYe&wEX*9uoh^)volFc}3>}RP+)Ruu o3@zPUj0{bjom}i}2&#yM#8F~VNkvg=8keQH5tpi}tG^o;0D11IwEzGB delta 459 zcmbQnHJfXL17p3pA(x#US8+*EYGN)|#hj^^y!{Rv2(Ue<^)vL|JhSiq$8B0u6T(l5 zJFInVbogb^)Ljt&%3{W{Ps>i}{d+F`ayzSyS<9_M;hVo~h$w!*)Oo=E@ZNmBm&>1= zh|V~w~10?&H|pE^)6h z{OGki3eyV2Ttm0KV%ylVS))5bYoptOg;A<4&-VCTzH;NLRo~WH)!6qclQ*rMyvS=> z$=)eGT*2z8Q*~ax4l|lrt=R097G$L;)OknBcGu~;8~bGK+9ed)-djXZdfq$r#j&49 zqyFr7-^R84?Z(^3^L{kkIBM4;w|TSqX}x?;>nlP%4=0;3mnqtn{(Y&xFSCwu&Be`X zo5Pu2G1?g!DHwo&LY@K_m|yDs(9GD-(8<)o$d#mvrzpo&;XL?srLR1~GAaao$1bE&Gj`nz!f0H9p8AOHXW diff --git a/test/html/html_table_with_bgcolor.pdf b/test/html/html_table_with_bgcolor.pdf index 9f97932078bae250b8a7cd53d4095aff669f110e..977b6ba901b399ca668b32d0528c4844772f7147 100644 GIT binary patch delta 429 zcmey#^OI-8CdPX2zFj{8r*)dR%NbJrvv zF2%#aD^Ff4{UWa_XWmm-mA5MKoQ;@~ql)lEEf=pPic*J^I09qe-9Av=;WDMTSnJ9` zrtrtVu7w@5JKuBiw7g{6`>T@!eoef;MsBIslTR`GVuW5~FnZrjs`pCCU+`9D*;DPX zUyb*7uufa{HfFPU^5Qc+{pm(~WNvz=%**o6Oj$TTX~XBaWg)x2TJ|kJ$LOeEEO{^@ zO`>|u=i8xa%d$(nljVEX=54r^n?1+Y=iDz{>C7#21(P3WJ^FsE>_OAs+h#T4!Ph5N z99|`RaK#Ds_stUnPp$uv@UQ!-)z^J*?@gQ$_h)gZPa*_3(WWDORN$viAF zlaDdGZ7yJOXJoXPyn{8x$;i^w)x^xv+|1a`!ot$U(%it&*wEF@&CT4@+0@n8&CZ69 Jipg)N$Fweut=Pq#NZb8of!!Y|7EBW=B=o_M;mW~a)9i%ipY9j%{o;-158OP|x> zYrZh=FXYzoy}dI{|CrlMN%`|B6_%T(pRl<)@6rh;`=bfZt>3OFdtEH!KbN^F?wL{3 z#xn-rBA#zwea7edi)qK~B%<#oti65BqI$-huhC|gGOU%49SA-0zW40`*6QuKKh`Z< zr~I)k)Ve8fBL6*h<;7FtKOX!ezS`<_?d`qFhI@aw3mb>+S58W(h{-y3FhzoGXQ}Uu z%<8KPRh?4Hu2coAkb1Of?#GOH&%O}5>C+iy>-;SzzhZJ{iSP1boNUTGak3VR+hjf# znaRhPJvJAxxHB@EP2Rzp;$-gRYUt`@Y-;LkX6R~ZVeIVe>}GCY?&|1hY+>r?=xS#} KNX6thHfaF9<-hF! diff --git a/test/html/test_html.py b/test/html/test_html.py index c163d34cf..0240c09d6 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -188,7 +188,7 @@ def test_html_bold_italic_underline(tmp_path): """bold italic underlined - all at once!""" + all at once!""" ) assert_pdf_equal(pdf, HERE / "html_bold_italic_underline.pdf", tmp_path) From 03fc88f1d240ec48853aa432d2d3df175c40050c Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Tue, 2 Jul 2024 08:58:51 +0200 Subject: [PATCH 7/9] Getting rid of HTML2FPDF.emphasis --- fpdf/html.py | 130 ++++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/fpdf/html.py b/fpdf/html.py index 14c871192..2039d362c 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -61,6 +61,9 @@ "ol": TextStyle(t_margin=2), "ul": TextStyle(t_margin=2), } +INLINE_TAGS = HEADING_TAGS + ("a", "b", "code", "em", "font", "i", "strong", "u") +BLOCK_TAGS = ("blockquote", "center", "dd", "dt", "li", "p", "pre", "ol", "ul") +assert (set(BLOCK_TAGS) | set(INLINE_TAGS)) == set(DEFAULT_TAG_STYLES.keys()) # Pattern to substitute whitespace sequences with a single space character each. # The following are all Unicode characters with White_Space classification plus the newline. @@ -339,9 +342,13 @@ def __init__( # If a font was defined previously, we reinstate that seperately after we're finished here. # In this case the TOC will be rendered with that font and not ours. But adding a TOC tag only # makes sense if the whole document gets converted from HTML, so this should be acceptable. - self.emphasis = TextEmphasis.NONE - self.font_size = pdf.font_size_pt - self.set_font(pdf.font_family or "times", size=self.font_size, set_default=True) + self.font_family = pdf.font_family or "times" + self.font_size_pt = pdf.font_size_pt + self.set_font( + family=self.font_family, emphasis=TextEmphasis.NONE, set_default=True + ) + self.style_stack = [] # list of FontFace + self.h = pdf.font_size_pt / pdf.k self._page_break_after_paragraph = False self._pre_formatted = False # preserve whitespace while True. # nothing written yet to
, remove one initial nl:
@@ -349,7 +356,6 @@ def __init__(
         self.follows_trailing_space = False  # The last write has ended with a space.
         self.href = ""
         self.align = ""
-        self.style_stack = []  # list of FontFace
         self.indent = 0
         self.line_height_stack = []
         self.ol_type = []  # when inside a 
    tag, can be "a", "A", "i", "I" or "1" @@ -504,7 +510,7 @@ def _end_paragraph(self): def _write_paragraph(self, text, link=None): if not self._paragraph: - self._new_paragraph(top_margin=self.font_size / self.pdf.k) + self._new_paragraph(top_margin=self.font_size_pt / self.pdf.k) self._paragraph.write(text, link=link) def _ln(self, h=None): @@ -661,14 +667,14 @@ def handle_starttag(self, tag, attrs): self.style_stack.append( FontFace( family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, color=self.font_color, ) ) self.heading_level = int(tag[1:]) tag_style = self.tag_styles[tag] - hsize = (tag_style.size_pt or self.font_size) / self.pdf.k + hsize = (tag_style.size_pt or self.font_size_pt) / self.pdf.k if attrs: align = attrs.get("align") if not align in ["L", "R", "J", "C"]: @@ -692,11 +698,10 @@ def handle_starttag(self, tag, attrs): color = tag_style.color.colors255 if color: self.set_text_color(*color) - 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, + size=tag_style.size_pt or self.font_size_pt, + extra_emphasis=tag_style.emphasis, ) if tag in ( "b", @@ -711,30 +716,28 @@ def handle_starttag(self, tag, attrs): "strong", "u", ): - is_block = tag in ("blockquote", "center", "dd", "dt", "pre") - if is_block: + if tag in BLOCK_TAGS: self._end_paragraph() self.style_stack.append( FontFace( family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, color=self.font_color, ) ) 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, + size=tag_style.size_pt or self.font_size_pt, + extra_emphasis=tag_style.emphasis, ) if tag == "pre": self._pre_formatted = True self._pre_started = True - if is_block: + if tag in BLOCK_TAGS: self._new_paragraph( align="C" if tag == "center" else None, line_height=( @@ -827,8 +830,8 @@ def handle_starttag(self, tag, attrs): self.style_stack.append( FontFace( family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, color=self.font_color, ) ) @@ -841,9 +844,9 @@ def handle_starttag(self, tag, attrs): self.set_font(face) self.font_family = face if "font-size" in css_style: - self.font_size = int(css_style.get("font-size")) + self.font_size_pt = int(css_style.get("font-size")) elif "size" in attrs: - self.font_size = int(attrs.get("size")) + self.font_size_pt = int(attrs.get("size")) self.set_font() self.set_text_color(*self.font_color) if tag == "table": @@ -984,8 +987,9 @@ 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_font( + font_face.family, font_face.size_pt, emphasis=font_face.emphasis + ) self.set_text_color(*font_face.color.colors255) self._end_paragraph() if tag in ( @@ -1001,15 +1005,15 @@ def handle_endtag(self, tag): "strong", "u", ): - is_block = tag in ("blockquote", "center", "dd", "dt", "pre") font_face = self.style_stack.pop() - self.emphasis = font_face.emphasis - self.set_font(font_face.family, font_face.size_pt) + self.set_font( + font_face.family, font_face.size_pt, emphasis=font_face.emphasis + ) self.set_text_color(*font_face.color.colors255) if tag == "pre": self._pre_formatted = False self._pre_started = False - if is_block: + if tag in BLOCK_TAGS: self._end_paragraph() if tag in ("ul", "ol"): self._end_paragraph() @@ -1037,9 +1041,10 @@ def handle_endtag(self, tag): if tag == "font": # recover last font state font_face = self.style_stack.pop() - self.emphasis = font_face.emphasis self.font_color = font_face.color.colors255 - self.set_font(font_face.family, font_face.size_pt) + self.set_font( + font_face.family, font_face.size_pt, emphasis=font_face.emphasis + ) self.set_text_color(*font_face.color.colors255) if tag == "sup": self.pdf.char_vpos = "LINE" @@ -1054,36 +1059,33 @@ def feed(self, data): if self._tags_stack and self.warn_on_tags_not_matching: LOGGER.warning("Missing HTML end tag for <%s>", self._tags_stack[-1]) - def set_font(self, family=None, size=None, set_default=False): + def set_font( + self, + family=None, + size=None, + emphasis=None, + extra_emphasis=None, + set_default=False, + ): + pdf = self.pdf + if emphasis is None: + emphasis = pdf.emphasis + if extra_emphasis: + emphasis |= extra_emphasis if family: self.font_family = family if size: - self.font_size = size - self.h = size / self.pdf.k - prev_page = self.pdf.page + self.font_size_pt = size + self.h = size / pdf.k + prev_page = pdf.page if not set_default: # make sure there's at least one font defined in the PDF. - self.pdf.page = 0 - if (self.font_family, self.emphasis) != ( - self.pdf.font_family, - self.pdf.emphasis, - ): - self.pdf.set_font(self.font_family, self.emphasis.style, self.font_size) - if self.font_size != self.pdf.font_size: - self.pdf.set_font_size(self.font_size) - self.pdf.page = prev_page - - def set_style(self, tag, enable): - "Modify style and select corresponding font" - emphasis = TextEmphasis.coerce(tag.upper()) - if enable: - self.emphasis = self.emphasis.add(emphasis) - else: - self.emphasis = self.emphasis.remove(emphasis) - style = self.emphasis.style - prev_page = self.pdf.page - self.pdf.page = 0 - self.pdf.set_font(style=style) - self.pdf.page = prev_page + pdf.page = 0 + if (self.font_family, emphasis) != (pdf.font_family, pdf.emphasis): + pdf.set_font(self.font_family, emphasis.style, self.font_size_pt) + assert pdf.emphasis == emphasis + if self.font_size_pt != pdf.font_size: + pdf.set_font_size(self.font_size_pt) + pdf.page = prev_page def set_text_color(self, r=None, g=0, b=0): prev_page = self.pdf.page @@ -1095,23 +1097,23 @@ def put_link(self, text): "Put a hyperlink" prev_style = FontFace( family=self.font_family, - emphasis=self.emphasis, - size_pt=self.font_size, + emphasis=self.pdf.emphasis, + size_pt=self.font_size_pt, color=self.font_color, ) tag_style = self.tag_styles["a"] 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, + size=tag_style.size_pt or self.font_size_pt, + extra_emphasis=tag_style.emphasis, ) self._write_paragraph(text, link=self.href) # Restore previous style: - self.emphasis = prev_style.emphasis - self.set_font(prev_style.family, prev_style.size_pt) + self.set_font( + prev_style.family, prev_style.size_pt, emphasis=prev_style.emphasis + ) self.set_text_color(*prev_style.color.colors255) # pylint: disable=no-self-use From a3792ea4a8efeda2ae5aa042475de1d17fba2dd8 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:43:42 +0200 Subject: [PATCH 8/9] Improvements follow @gmischler code review --- CHANGELOG.md | 1 + docs/HTML.md | 22 ++++++++++ fpdf/html.py | 42 +++++++++++++------- test/html/html_dd_tag_indent_deprecated.pdf | Bin 0 -> 1153 bytes test/html/html_font_family.pdf | Bin 0 -> 1282 bytes test/html/test_html.py | 28 +++++++++++++ 6 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 test/html/html_dd_tag_indent_deprecated.pdf create mode 100644 test/html/html_font_family.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e5fad12a..f1d133a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', * feature to identify the Unicode script of the input text and break it into fragments when different scripts are used, improving [text shaping](https://py-pdf.github.io/fpdf2/TextShaping.html) results * [`FPDF.image()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image): now handles `keep_aspect_ratio` in combination with an enum value provided to `x` * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): now supports CSS page breaks properties : [documentation](https://py-pdf.github.io/fpdf2/HTML.html#page-breaks) +* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): new optional `font_family` parameter to set the default font family * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): spacing before lists can now be adjusted via the `tag_styles` attribute - thanks to @lcgeneralprojects * file names are mentioned in errors when `fpdf2` fails to parse a SVG image ### Fixed diff --git a/docs/HTML.md b/docs/HTML.md index f882bf24b..f6d7ee12a 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -131,6 +131,28 @@ pdf.output("html_dd_indented.pdf") and that some [`FontFace`](https://py-pdf.github.io/fpdf2/fpdf/fonts.html#fpdf.fonts.FontFace) or [`TextStyle`](https://py-pdf.github.io/fpdf2/fpdf/fonts.html#fpdf.fonts.TextStyle) properties may not be honored. However, **Pull Request are welcome** to implement missing features! +### Default font + +_New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ + +The default font used by [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) is **Times**. + +You can change this default font by passing `font_family` to this method: +```python +from fpdf import FPDF + +pdf = FPDF() +pdf.add_page() +pdf.write_html(""" +

    Big title

    +
    +

    Section title

    +

    Hello world!

    +
    +""", font_family="Helvetica") +pdf.output("html_helvetica.pdf") +``` + ## Supported HTML features diff --git a/fpdf/html.py b/fpdf/html.py index 2039d362c..00cf9fe8e 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -23,7 +23,7 @@ DEGREE_WIN1252 = "\xb0" HEADING_TAGS = ("h1", "h2", "h3", "h4", "h5", "h6") DEFAULT_TAG_STYLES = { - # inline tags: + # Inline tags are FontFace instances : "a": FontFace(color="#00f", emphasis="UNDERLINE"), "b": FontFace(emphasis="BOLD"), "code": FontFace(family="Courier"), @@ -32,7 +32,7 @@ "i": FontFace(emphasis="ITALICS"), "strong": FontFace(emphasis="BOLD"), "u": FontFace(emphasis="UNDERLINE"), - # block tags: + # Block tags are TextStyle instances : "blockquote": TextStyle(color="#64002d", t_margin=3, b_margin=3), "center": TextStyle(t_margin=4 + 7 / 30), "dd": TextStyle(l_margin=10), @@ -61,9 +61,20 @@ "ol": TextStyle(t_margin=2), "ul": TextStyle(t_margin=2), } -INLINE_TAGS = HEADING_TAGS + ("a", "b", "code", "em", "font", "i", "strong", "u") -BLOCK_TAGS = ("blockquote", "center", "dd", "dt", "li", "p", "pre", "ol", "ul") -assert (set(BLOCK_TAGS) | set(INLINE_TAGS)) == set(DEFAULT_TAG_STYLES.keys()) +INLINE_TAGS = ("a", "b", "code", "em", "font", "i", "strong", "u") +BLOCK_TAGS = HEADING_TAGS + ( + "blockquote", + "center", + "dd", + "dt", + "li", + "p", + "pre", + "ol", + "ul", +) +# This defensive programming check ensures that we do not forget any tag in the 2 *_TAGS constants above: +assert (set(BLOCK_TAGS) ^ set(INLINE_TAGS)) == set(DEFAULT_TAG_STYLES.keys()) # Pattern to substitute whitespace sequences with a single space character each. # The following are all Unicode characters with White_Space classification plus the newline. @@ -300,6 +311,7 @@ def __init__( warn_on_tags_not_matching=True, tag_indents=None, tag_styles=None, + font_family="times", ): """ Args: @@ -322,6 +334,7 @@ def __init__( tag_indents (dict): [**DEPRECATED since v2.7.10**] mapping of HTML tag names to numeric values representing their horizontal left identation. - Set `tag_styles` instead tag_styles (dict[str, fpdf.fonts.TextStyle]): mapping of HTML tag names to `fpdf.TextStyle` or `fpdf.FontFace` instances + font_family (str): optional font family. Default to Times. """ super().__init__() self.pdf = pdf @@ -342,7 +355,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.font_family = pdf.font_family or "times" + self.font_family = pdf.font_family or font_family self.font_size_pt = pdf.font_size_pt self.set_font( family=self.font_family, emphasis=TextEmphasis.NONE, set_default=True @@ -379,11 +392,12 @@ def __init__( raise NotImplementedError( f"Cannot set style for HTML tag <{tag}> (contributions are welcome to add support for this)" ) - default_tag_style = self.tag_styles[tag] - is_base_fontFace = isinstance(tag_style, FontFace) and not isinstance( - tag_style, TextStyle - ) - if is_base_fontFace and isinstance(default_tag_style, TextStyle): + if not isinstance(tag_style, FontFace): + raise ValueError( + f"tag_styles values must be instances of FontFace or TextStyle - received: {tag_style}" + ) + # We convert FontFace values provided for block tags into TextStyle values: + if tag in BLOCK_TAGS and not isinstance(tag_style, TextStyle): # pylint: disable=redefined-loop-name tag_style = TextStyle( font_family=tag_style.family, @@ -394,9 +408,9 @@ def __init__( color=tag_style.color, fill_color=tag_style.fill_color, # Using default tag margins: - t_margin=default_tag_style.t_margin, - l_margin=default_tag_style.l_margin, - b_margin=default_tag_style.b_margin, + 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: diff --git a/test/html/html_dd_tag_indent_deprecated.pdf b/test/html/html_dd_tag_indent_deprecated.pdf new file mode 100644 index 0000000000000000000000000000000000000000..457134b31b585e52494752004b6261ba56a7168d GIT binary patch literal 1153 zcmbtTO=}ZD7_OiogC4Cdy^KO=T4`s$KO_`3n=A$^O*fHhLl4{RG;Z0gw>qsJoj2ULoMdhsVXlMlPJpae1=MKQw(*BS>5S8$t#7@%cL zI_w#gWm80@EWtq+^ggQoukXsCs;?DK6R>*tTy09)H&v=%uQhgV9?zf5Hz&Z%%-QsM z@yXQG&!_K;d;N*m({~P{et&ZIHl92Pf1J)8gl|sz+w*(jmF(Wtm#;MWNHrcypS8WS z>guQc_5HW+8aIx<-1>TL;=^&_u5fykf8@EI6JHKe>``7mYDQh0F8H(?a8s*p3rs!GuT3WmU*4H$E&S%c|*&i-5u?W%0H=4BwCQU=9i<>uDN7KOqd@m+zfQq^$m ta>Z60!*or{RV__>Ja6!Nt+SV>GG)rLN7veUSAJ5zUOHTB7d z{tEF$DB@44MX>e3ccoZRAN&J?Gx@Mf3Kn!;cK7Vud+t5w&P-K{&McnO0frjntu>I% z0#x8J6A%M*$M+)G)a9?*3P5+M=ev2{g-uN&CNbyIHna^qXA}9ivAIDZs=6H-f%$nr znWxgF6KN|Ol;zsO=WLJ@BF7gCki|hD6R6Ndcaz5gq6(u09>nXc4@XdyR~wQPT;T@1 zlb}&09#zi-Wg>z`Kc?!2@^hG>Mk5}FZ5ly4nNpD_@~O~aKF1FP4P~u?D`=6nxkmw7 zq^u(zL9As|R3t*`u7mC?z4r4<-K0}R84(AyXF1*PGjNy{TI8FH{M-7+3FSzFsvsvt z9f6`8CYUPT#>65xtz%3yxt1}R*0kwciQsNXSAixc4X6$M1ufImO}Gk=Fv+17WJeg0 z9gJX@9N(AD9LEe($}T!;(*_O>S!YX literal 0 HcmV?d00001 diff --git a/test/html/test_html.py b/test/html/test_html.py index 0240c09d6..02c7ded49 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -1083,3 +1083,31 @@ def test_html_heading_above_below(tmp_path): }, ) assert_pdf_equal(pdf, HERE / "html_heading_above_below.pdf", tmp_path) + + +def test_html_dd_tag_indent_deprecated(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.write_html( + "
    description title
    description details
    ", + tag_styles={"dd": TextStyle(l_margin=5)}, + ) + assert_pdf_equal(pdf, HERE / "html_dd_tag_indent_deprecated.pdf", tmp_path) + pdf = FPDF() + pdf.add_page() + with pytest.warns(DeprecationWarning): + pdf.write_html( + "
    description title
    description details
    ", + dd_tag_indent=5, + ) + assert_pdf_equal(pdf, HERE / "html_dd_tag_indent_deprecated.pdf", tmp_path) + + +def test_html_font_family(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.write_html( + "

    hello world. i am sleepy.

    ", + font_family="Helvetica", + ) + assert_pdf_equal(pdf, HERE / "html_font_family.pdf", tmp_path) From 1a76aa78b90b8030a9a7c9330d5d42ceafc65f0e Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:44:36 +0200 Subject: [PATCH 9/9] Adopting @gmischler review feedbacks --- fpdf/html.py | 25 +++++++++++++----- test/html/html_features.pdf | Bin 6740 -> 6742 bytes test/html/html_heading_above_below.pdf | Bin 1596 -> 1595 bytes test/html/html_heading_color_attribute.pdf | Bin 1426 -> 1424 bytes test/html/html_headings_line_height.pdf | Bin 2825 -> 2824 bytes test/html/html_list_vertical_margin.pdf | Bin 2768 -> 2769 bytes test/html/html_sections.pdf | Bin 0 -> 1724 bytes test/html/html_superscript.pdf | Bin 1317 -> 1317 bytes test/html/html_whitespace_handling.pdf | Bin 1864 -> 1864 bytes test/html/test_html.py | 23 ++++++++++++++++ test/outline/html_toc.pdf | Bin 4303 -> 4306 bytes test/outline/html_toc_2_pages.pdf | Bin 21413 -> 20885 bytes .../html_toc_with_custom_rendering.pdf | Bin 2471 -> 2470 bytes test/outline/test_outline_html.py | 8 +++--- 14 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 test/html/html_sections.pdf diff --git a/fpdf/html.py b/fpdf/html.py index 00cf9fe8e..1df85cb4f 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -22,6 +22,10 @@ BULLET_WIN1252 = "\x95" # BULLET character in Windows-1252 encoding DEGREE_WIN1252 = "\xb0" HEADING_TAGS = ("h1", "h2", "h3", "h4", "h5", "h6") +# Some of the margin values below are fractions, in order to be fully backward-compatible, +# and due to the _scale_units() conversion performed in HTML2FPDF constructor below. +# Those constants are formatted as Mixed Fractions, a mathematical representation +# making clear what the closest integer value is. DEFAULT_TAG_STYLES = { # Inline tags are FontFace instances : "a": FontFace(color="#00f", emphasis="UNDERLINE"), @@ -56,7 +60,7 @@ color="#960000", b_margin=0.4, font_size_pt=8, t_margin=5 - 182 / 900 ), "li": TextStyle(l_margin=5, t_margin=2), - "p": TextStyle(t_margin=4 + 7 / 30), + "p": TextStyle(), "pre": TextStyle(t_margin=4 + 7 / 30, font_family="Courier"), "ol": TextStyle(t_margin=2), "ul": TextStyle(t_margin=2), @@ -367,6 +371,7 @@ def __init__( # nothing written yet to
    , remove one initial nl:
             self._pre_started = False
             self.follows_trailing_space = False  # The last write has ended with a space.
    +        self.follows_heading = False  # We don't want extra space below a heading.
             self.href = ""
             self.align = ""
             self.indent = 0
    @@ -493,12 +498,12 @@ def _new_paragraph(
             indent=0,
             bullet="",
         ):
    -        if bullet and top_margin:
    -            raise NotImplementedError(
    -                f"{top_margin=} will be ignored because {bullet=} is provided, due to TextRegion._render_column_lines()"
    -            )
    +        # Note that currently top_margin is ignored if bullet is also provided,
    +        # due to the behaviour of TextRegion._render_column_lines()
             self._end_paragraph()
             self.align = align or ""
    +        if not top_margin and not self.follows_heading:
    +            top_margin = self.font_size_pt / self.pdf.k
             self._paragraph = self._column.paragraph(
                 text_align=align,
                 line_height=line_height,
    @@ -509,6 +514,7 @@ def _new_paragraph(
                 bullet_string=bullet,
             )
             self.follows_trailing_space = True
    +        self.follows_heading = False
     
         def _end_paragraph(self):
             self.align = ""
    @@ -523,8 +529,10 @@ def _end_paragraph(self):
                     self._page_break_after_paragraph = False
     
         def _write_paragraph(self, text, link=None):
    +        if not text:
    +            return
             if not self._paragraph:
    -            self._new_paragraph(top_margin=self.font_size_pt / self.pdf.k)
    +            self._new_paragraph()
             self._paragraph.write(text, link=link)
     
         def _ln(self, h=None):
    @@ -752,6 +760,10 @@ def handle_starttag(self, tag, attrs):
                     self._pre_formatted = True
                     self._pre_started = True
                 if tag in BLOCK_TAGS:
    +                if tag == "dd":
    +                    # Not compliant with the HTML spec, but backward-compatible
    +                    # cf. https://github.com/py-pdf/fpdf2/pull/1217#discussion_r1666643777
    +                    self.follows_heading = True
                     self._new_paragraph(
                         align="C" if tag == "center" else None,
                         line_height=(
    @@ -1006,6 +1018,7 @@ def handle_endtag(self, tag):
                 )
                 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 in (
                 "b",
                 "blockquote",
    diff --git a/test/html/html_features.pdf b/test/html/html_features.pdf
    index 8b54a246aba9f6729cf231231c20209912f46d1d..f2ea382450000d1338d4be9e519bbd2d59b842d3 100644
    GIT binary patch
    delta 1176
    zcmca&a?NDJH^zEXE;~D};*z4	Xe5IY&cI=iN3C*z;Ta+X|UOpL|zw6lq+LsVZJA
    zpP=m)q;Q(^?6tptf+npLKXT{Rp$WX1>FH%Vl!Jd19q`cDA!jH0;bAAkLXKpOc@K*V
    zD_+YRHFdD{N}jS5NoBYwIpylM-1FrDd)CZ0-Z3vWS}EheoCjg)_1(Mj=jfLIy6{0K
    zLw@=zuc!Y*YA0RwC|n@p$@*QwljqzaV~)lx9ig7T-nmW{*r^e`D^pF!z3G@yw0V9G
    z`@;W;(I39&^j}@_HoExKw}^Y2|FUiV_`z7}_(SIdp%X%T*VotG_c`_9K%05e@ru5f
    zU>5_8HF-5Nvsc|}&YZn!vuZ{C@7wRy{9euYo@3$5y#CnCQ!}qiB^~AoOJOR1u>QH+
    z8~xQf`gc6nsgg(pLU-qSzr;fw6v&4S7>Tz
    zh!I?-hM1a7jSVmyV`^fAsn^sPBUDYzOkts=2#MNw)Rm#L+Z1(&L-tG^o;
    E08vqR*8l(j
    
    delta 1174
    zcmca+a>ZoBH^zEnE;~D};*z4	Xe5IY)y|7tJvc*z-R8jG+9Q7rGOReok2RW5R8<
    zKMg`D2aayq7IyO4XXS*Wt0&}pb}?}++npYDS!AD#fe_cac7N8I3MB`PLlRx-KVoF=
    z{hHsSu*fmFt%%Q6(xHScb!+bJ&$1%=*+p#EH)ngZ@Xipv^C~B>{(JA{r+oap|DE1H
    z-n2XDr~O;Ityet?7sz5$)=gV-TZFKovz+?Kfz2cZgHWE9@Dy}HCZ)(KmSe(YGXIulUdgD{Di41
    zOY@;u%ck#Kw#s(Kwv|@VP22a^|IK@}q_6UP+hvmvS#ws-sa&fll(s=hcn5R6?e~QG
    z$kfV*enqA`l^?#l%d6dbVBMUpu_cvvcwE(`mH!nP~DLNnA8GUFIm
    zJ?08yiaa}6X4>=G8{cL>TDj7LTVtBYjM&phK1F(*ePQ`KYxkvH%T~9ovAyz6aL-CT
    z`RTuvCM~kzU!%yb{-8(6s&@N_T!+@=1g3};yb6k{ry~FKE!bcFpOKfX_W0(@%rgX-
    z%q=G8ij*)JSxkNmq%6!QM~ap*8cjYABn>CKij^`NPu?x|q~27)00b2B6u7_)149FI
    zGYm0H6LWMiBLgD~3^8LY>P(C*G1Qq_m_Wqp5f&SnnPFODVTj=vBTGy%V*^7Bj~E+b
    zQD
    z!DVWQso50hWemM0hM0OyjWI&i)XWqXT8i*^H#IlMaJ8w0naSimlD4rXCQgo)j&5dd
    zMuv{AE*6%KCYH{|7Di64j&3e41_rKnHUw3~Ldu-PqLPZD)HE(rOCvKbRaIAiH!c9z
    C<9I{>
    
    diff --git a/test/html/html_heading_above_below.pdf b/test/html/html_heading_above_below.pdf
    index a70b66d19dcc270d80ea552a9c90cc7dbcced7c4..cdcd7c8cc15f1601db946953434a4f7b1ba2bfc8 100644
    GIT binary patch
    delta 417
    zcmdnPvzuo_AY;8Lmz^C~aY<2XVlG$3oZhp$xehr9uwJO!pLFU*&*shjIZ+2il7#Qb
    z3kaJ!-Rs)_UPwtPW6Ddt;`fO+IGltJ$F8x_3Fe$Mk>iixwYZW+%>vq;mmKG0ghJ
    HcjE#8$Kn$a8Ido@0y7K@%#4jN#LNvXF~uwmFvKhkP0+;*4UEh�*WK
    zdM7VuHIFrPGBYu?a4~Z>F)}c>Ff%hVF)%TAaxpPCHUsK*G_|uKs3H~;8;M0F6-B9O
    QT!uylMqH|@uKsRZ0HlJCH2?qr
    
    diff --git a/test/html/html_heading_color_attribute.pdf b/test/html/html_heading_color_attribute.pdf
    index 9d11c0c37b7a92d8a39e187de5b473349b7c1d8a..3652b93086dc2c7c5a836c0ecd5dc473a20e79e8 100644
    GIT binary patch
    delta 376
    zcmbQlJ%M{eAY;8Tmz^C~aY<2XVlG$3oZPbqxegid9Q#;3?~(O6@7!CxW#O*;_u49%
    zHuiOfm}%|X7pmBr%chD`9cV~jS$)%1x+t%DTymjj)(H24)79m|~V@
    z7-HsT<``lY7A6>CmKGM1f3Vob8d;hfxEdL`7&#g^o4Go>8kjj60-1*9#zv-2ZWb1H
    eHUw3~LP9mMsHCDOHI2*Az|??CRn^tsjSB$Xpn!(}
    
    delta 404
    zcmbQhJ&AimAY;8Lmz^C~aY<2XVlG$3oZPb;xegf!9Q*ip-XrPPLdBlRlS3~aTsuQu
    zAbI5!5AP$_&9jzpJl*!V?t8e!B#~tYCvTH`&D=b7g3!ONy~`)MJ=vOeP^@=#>$->f
    zCk{lh&P;!0=W&%abf3R-#KHTU+`TuOZF`Yb-miJ2rReMR0Jk0a%yKsS(|=djSIxP+
    zyTNYJY^M(HmA#h_=`4?`V*IITS-SZR<3>g%Q`5=wnLpJVDHwo&LY@K_m|k!)z$jE3iIf&7y-bq@6uT*30rj99R&dhwgajijGQ}m%GUYovlp`b#=2kQ%8
    z-RwClV3&PzDYIx>HnY8>M}ePX%kr6elc)UjIprqwZ<&+j{=K`7FIXwwFL6y@;=KI4
    zRh;MMuTJ~<|IMejqUvA2aQxGo5KyN)(L>?uL4|XH)5K+EUS4xsHT_~*!5p1>jmA6o
    zHI{#Pwd(dckM}8CBxT+o335{BbW407vT}0onf+cl_or4`NM7_SyWepwGjF$;_nPBr
    z-_vEd`?h;&uKjk!{7U3z+qR#HZ|iycvR@?aJ|=JG#m({k>hYPU^!OfqXVv}6wf8ya
    zAD8Z{bEhrUm)5>%{WA7lIag|4N^wb1YU1W*CUf?BLj?m6P{>o@0y7K@%uNk2#4L=>
    zF~lqlEYQUa4GoPk#EgwF%ri7Kg6cIxSZru+VT7*P$iUbPL(JH~Vsbv0ZLE`viIbb5
    zrKyFRo29Xdxw(b0shgpbxuvUtk(-;DrJ0=#K^3uFc6MCFC5c5P6-B9OTt>#`23)GD
    IuKsRZ07MqPu>b%7
    
    delta 555
    zcmeAW>lE7%$jE3qIf&7yepT)Qiz}H6Pbmf27=M>L?;~=|r@%X6L*wz60Zv+aEPr&*
    z+0Nc5c_>|XXUT#?1-TE@IXKU0c6>0ooI1&9^^zQwhwrtfeEvP#oo80n$DYK$Z6E)<
    zTsrH)r>$Ajo{L|Xe{ZpRx%vlwN6x3_szRJzwJ8GE1E-10T3yQR2+?2k_Q8(&O$%6_
    z{&TTx4G+J&&Ha}GNKDw9(@&_w>9A&yy7Wx@)*H4{D{u6yn0%Q1qSe{l>#3c-x8M91
    zJl^y;O!H`#e~t58)7z$J`n~^W7sxn^?>771yI+Z0KZcU}$FQX6WMV=w{&JWMOD$Lr_I5mz^C~aYn
    F7Xa`Ez5)OM
    
    diff --git a/test/html/html_list_vertical_margin.pdf b/test/html/html_list_vertical_margin.pdf
    index 2ddb6c0b57f45c26045651b1104af48d46e2e168..a7834cfa84b09a813f96822c12a5e3ffeb5cfc27 100644
    GIT binary patch
    delta 557
    zcmca0dQo&k2;=0dto)OoFbUM_Pxg506mX$6V8&kF8Oua}PhavwW#4+q#fzux){NwN
    zBDas({jp|pk(~U^*aKgin=T#c*>I@#MBm5dflIum_c9w)6VcUQV{$
    zlk^a}0LVGGC;$Ke
    
    delta 589
    zcmca8dO>tU2xGkwmz^C~aY<2XVlG$3oT(SiW*u@6VSP|L?~%^svv23__X)bC$HKZ>
    zpylRuhvcl4QIk9TAN{_jSf){AKI!hA!{sL~R-B$O=eXu<7MpV-Q`FL0OqVHIA8}vo
    zF;(McK&qhKg!c2k{quL_7QC4)xB6jibotw~@0)JCpCx+jH{bc=@9V>PN^jdgWPGR{
    z<@ssq|H+p=HdK_QX#d!x@b>Y{lhMl$OkbZpasK*5oyDoLlHc{`oNrI#R_(fXPjT4?
    zjjTYOFS1Eo{fh+E3SXM%y_(VGJ$Gx!hJfWBue>uWCr>nqQrqgWY>Uk)kF05)%9Au}
    zcb_u)%_@6nx(-)rUJAs|n}eCy+3SrI3_w63Pk{@}FfcH)FvbwG#1u0$Ff&0{XJ~9@
    zfgxsMYJeeTZfXV*i$hp!XaN&M60OV
    
    diff --git a/test/html/html_sections.pdf b/test/html/html_sections.pdf
    new file mode 100644
    index 0000000000000000000000000000000000000000..a6e9ac7bf805137947b43bf82028dec96317b532
    GIT binary patch
    literal 1724
    zcma)7Z*1E{6o+XH&Dt~wlR#rq4^T)4CHC2lW0$B})6`p`TcWfY(5z4|xdvCR-PxCt
    zvax@FI&BkV{F6|N5C{sbm=KJCphBuvK2&3aNqnIi6%_;aVPZl^1qFe`os+aqf&}-)
    zzUTLTzkBcf-r1jssrSL4z(BwOYtH}^i7;%Nl`}Db4Evy-aX?BSpOU={y8~r(Eo$XK
    ziX)bg5(@G%kVP1j74o%vWC#H^p=FW7w6rkD%uqPq0~;SgW@{SjmRZIGtg7Sfz|0v2
    z0f^_Yp_|A7Vamt2P@UR{mBWCIo2bn)a)V~c>;QUP;!`4R)39b(Sr08e@=7))MkaQE
    zSi!Z)wt|pEj+L|1gjVuG(mXtPG_-=J>IOzOIlG}@6hmn%gBZ3GnOXce;1z{NIoL+p
    zAd??|x$x!AS?;FxKZ?hu*Co+!17qgNzQf;kKeJ)z@*A6e+~&J9bLH~KlRc}O3gH6_
    zf-`Y8z3b>tn;PbOM=y=~kpI}Kg?q0}8|T%xu70)=9bf9d>)e{xqRdBw&)#`TKggVM
    z8V^1&!GTz~ZXo@>4X=Hk@;r-tXY
    zj8FMae3x1~IdUSoIR2@`4t!92eq`x*ajN&L!e9PFO=Fogv!fUGH9v5A=C=9S=7x!P
    zx4teeZO-&%Z$9(N#1nz{X9K_Q|1~mvq3zFgFX+bIJHGh);-ja&URB)tVTZW8^ON~o
    zgNtuts+V%r%E}3ON*>i5q*|nD>~>@fBdn)2hHW=V>2)&;*oSnp)pYcVfnmFHd$21<
    zst=;$l1wOqr>C^v%RQywkqE0oKzWm-hit;O(p?CH6ia3Vu-zz6_&WxPFrwvWTlv}1
    z>w%@}t-vl1lB(Md1~P>vTG3YCODC&H-vdK^t6_MCjUf_Qnn%Ds{74V+k)aR_iIo{D
    z@hBc1NN3fZCRg?4+3MD@fj}dzllum@c&KT3H8uCI)L~f;3d__xX?<$g$_dx81*`bn
    zp#Me4D?w$MFrm2_l{peN8V2RW>-l(Zez5AUBa=$6q!<}y&=_lmiljhZfKcJc>VZmt
    z;{x93yp8%94nUrv4y6U8P)G=Yez0OAN1+RtD>j8FuExegBDY$bQg4%_`q;2UZn>IR
    zIYioBV^esNZ?z3_BDv9OY%o;E0eOj(x8jFwO*fG3{vx{cClRdyo3tzp=q*n))?xNr
    zz^yLb#6T(%l{rCzvLMT?d{hx7SrkMy%<)`EZ5N|4rRBdp+}U!lW@ESVP!KrA?{AN*
    F%s-r$1z-RG
    
    literal 0
    HcmV?d00001
    
    diff --git a/test/html/html_superscript.pdf b/test/html/html_superscript.pdf
    index cbb6f0a89b4fd994a942a3d74b30adcf833bc08b..f29e0f9a579ba727729ed9cb0a59f22148dbeb20 100644
    GIT binary patch
    delta 95
    zcmZ3=wUleac}AwRn#mU#^;sUO+0EL_!gQ3$$=J!!(8AKuz{JtW(b(9~)!EY6%*@Eb
    V)y=@w$=Sr*(9VXCipkupQUD+q7?}V7
    
    delta 95
    zcmZ3=wUleac}AvXRg*6=>a#qNuv@U1h3P1hlYxt~o2jvrlZAnSg_EhPxtoQFiKBtJ
    Vqp5+hftj(nft?K@6_dGHr2sN77?J=0
    
    diff --git a/test/html/html_whitespace_handling.pdf b/test/html/html_whitespace_handling.pdf
    index f676cd5b19306a621324eae5fec170bf6c73a433..2027872d90e0310bcb10a741f628034f1ad4d911 100644
    GIT binary patch
    delta 416
    zcmX@XcY<$25@Wr0UbBHj%lA1+(?YUjKdasL>b>$#sgO}PW`SZ$!t$MUxn0e-cj<3-
    z*p&9{yLnlMdjBMq!$)`PZ@9(6;pC**m2M+)@1(V3t~(=VQOfHXE{0uPrzSjCT=DBz
    zl~L=*oKNzt7R4%sZB5*j&5vAJ6JP5ZO3qCVn_V#TS$5^^>oauf<@p}*=cIf+(Z-fq
    ze&;P~%Hbt~ZqovI)|CI~y&_j`QNBsms&C!fodq*E&UC71vqr=_9(}*#08gr->d&4h
    z*^!n-+rrmH3ACGat^cu+xxReOj)Qx)Z2D<4x90Tn_osKQZTM%AE^&6o(>|T*UFYV0
    zTUi-vEW=vIc``V?V_D`6?RuZ|VDJ1Dsls(9y$;{6c(>-=9m&q!lm9!qzDxVZvsH!l
    znP!aUxms(-FH0r1NNx}IQRct9>3Kz{rs>-sUI~}1&T0LfKj+xyulE?1T5?8jUc+S1
    u;^boJW@u?@YT;^O?&xOd?Br@;Vr1^-W@_qWVCib==4xj{NX2Abc4+|fgSUtP
    
    delta 416
    zcmX@XcY<$25@Y>Ezh(o8*7ucN#hsmH0jCnStLa_bt-8lSQnaAQ@Yc@j+g+_Lgf4&D
    zw8ZlHpTBR9VnD%t89n6r~IC
    z@qB8h+$R6mdFrW<#%ZK@Rd$oPY)h~C(IYPybAMZ?>sQi<6}(IcGi+VVIDmJ#wy!n
    zCe|pQu0B|PE7a@Jg`lc~>3o0QE))>^9JBh>?~+f-^LMU)xoz6&FV`(&^N;(Kn`~e1
    zy}UN#-W4AA#&AW>;x>s{UHiLz{LS{BX^Sg9w0e!jj-#IqUZ0yQxYPQlyhy3}=kyht
    znj8EdIu&NUW?XbOHT}#E`6R8a`O9pNukH%pCL7P46Qx{{_^
    +           

    Subtitle 1

    +
    +

    Subtitle 1.1

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

    Subtitle 1.2

    + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +
    + + """ + ) + assert_pdf_equal(pdf, HERE / "html_sections.pdf", tmp_path) + + def test_html_and_section_title_styles(): # issue 1080 pdf = FPDF() pdf.add_page() diff --git a/test/outline/html_toc.pdf b/test/outline/html_toc.pdf index 4f5be8eb813defbe377bf41b9d41d246961bcffd..81c6c599d8d0283bc8b7243a16d2c08e2c46ea1f 100644 GIT binary patch delta 986 zcmX@Fcu8@CC0o4-mz^C~aY<2XVlG$3oT(EW{SF%lxPG@h8qc=U$aB`El5Q66Bg>Rz z4jged)z~(m>%e`*>%+Uze#cV z+qhNgMJ6QAK9lgXqSDttH(bI*ncYG@rHk#0U-0oot$Ur{ROedTSIf?}_;B%aah6<= zvB&bQH@67duh+lXVRiG(X8wKq=QmEyWbb9Pocx_#wtkk`U8Pe^v95IoO7l*;_RY20 z*Lwc=!iii=VkflwZQYB#Y*Kfwx?XYmUGyDWF%7S&&olUMY`dvxt#~waVHex#vu;#o&1PP12BwIgZWgjI7CyV)NK zMkzHmvp79p%b(C*@8G;ZyhxU{FSy`aSlf!r=Z$ZQT(l74iPrwR)K%f2W8WI(IT5Q3 zUrf*w5_u*V@4M=&SLlC3&3eO?uhQCdw!Lp_^AIxF)cxUDhEZnM#71k`<$?DSIp$dx zeH2@~!(U&y?1Ba7VR^osh{cO19{O^t+InWzaoZi<#{|RP*G~ACwT1n(9m5g1$vZbo zaQ@J+S#}gQV|Quw~0k16-B9OT*g4pa;d7i`nz!f0GSPG{{R30 delta 1012 zcmcblcwTXXC0o4_mz^C~aY<2XVlG$3oT(EW{SF%lxPG@h8ZWle$aB`Jmtve^9lov> z4buD<7u@iBWLQuWbE!iqp>a>=+@JpceLH{agx{9Ew`n%p^>Ws(pr@9z((jnvoYCuX zezudSY1!|+T3RM-ngQ2UKb0)YRb%x7HcD@fj7^oe<%DF?ZQCSMKWe8{cnjtNUifTliq|^Ym=FO9mdx zx8B?$Xun?nW=B*G-%a*aRr7NuXS4S*T1@`QE?fVLBibc$!|D_KJI?v;Jh3tO`1TJ! zEw(IG0EW(|8^tEek0ei9eUxkc&DiGI?M$vKKgsyEmrj1W*)}Lfbk&NDF=1JT?w6HB zU+r?4=XXQa`|C=R>PfjzGB}@#&YI40@R{_I8LO|xPx$HIp`|y&|HL9Y=d|yKl()Dr zv2(UuipCvyI#Wpt=8D(~jV?@^n)OhR(U)k3MKvTYA`s*0~S z?E3w|D2csyuR+cAjfuU}+_)}hzki#uO7{B$GfDS|yV&(>x5V3lM#YW~9n3^pOF~p1wOpVYDG&VHD zaGra&%%*Nkvg=8kaH9?_8>?uKsRZ0N4F(Jpcdz diff --git a/test/outline/html_toc_2_pages.pdf b/test/outline/html_toc_2_pages.pdf index 2a759cde7cbab95705500ce96b5525e3e952b1ab..30e02e65927b01fa1396a131a55c42cd6daca78f 100644 GIT binary patch literal 20885 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 literal 21413 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 O1O3o->=-sMT=ySuc}151 diff --git a/test/outline/html_toc_with_custom_rendering.pdf b/test/outline/html_toc_with_custom_rendering.pdf index ecbc5de94ecca2cdce13f36af8952164371699f3..9220ee1e1c03cec6611933c3391afede000e8932 100644 GIT binary patch delta 429 zcmZ23yi9mQBNL{!|i+Xl` zIAGZkdB)@VtWH_0d=Kds>!1g7ntakF|4%fs%*s2QUeV0Yv0i1teU4+3*K@qt)}+O~ zrZfA{JN{i7Tl|8;1&UHmO-$Xibz_nBwP~!Ws}JRUE@Qnm$MXGMpTf?^Ld&_*>(r_~ zGN-*xd$ZY?S&-G%Ou+yI6!H|fzzhQeOJh@XF+&3*BMdP^1Ix*c9KyE77&68N7^WJU z7+GSdGc`4ue33&~6VpC(OA8Fu7M3QHc{y!k&790k&752&XpW`y&<@rl|`evQ}AvOPj>>*{&7vW7GpJ+4f-n!rv zQ}&}-8-6zHajl(vMYKg`W>Lz_oZK}r{aaHVZ?4*KbI)Jq{dcP7f6sfS$QLZb{j9o9 zt?DCV#?-Xmn~j(SS?kRe3_w63Pk{@}Ffgz*HbWOPG%zy85HmD1z!bAE!4NYxz_7#6 z#2BjA3t_RLsVSxqXfvJn7 sxuvVIlZBb7v4NcpK^3uFc6MCFC5c5P6-B9OT!xmW=3J_(uKsRZ0PY5eWB>pF diff --git a/test/outline/test_outline_html.py b/test/outline/test_outline_html.py index 951b1d56e..0d4e692ca 100644 --- a/test/outline/test_outline_html.py +++ b/test/outline/test_outline_html.py @@ -18,24 +18,24 @@ def test_html_toc(tmp_path): Table of content:
    -

    Subtitle 1


    +

    Subtitle 1

    Subtitle 1.1

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




















    -

    Subtitle 1.2


    +

    Subtitle 1.2

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








































    Subtitle 2


    -

    Subtitle 2.1


    +

    Subtitle 2.1

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




















    -

    Subtitle 2.2


    +

    Subtitle 2.2

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