Skip to content

Commit

Permalink
Support for start & type attributes of <ol> tags when using FPDF.…
Browse files Browse the repository at this point in the history
…write_html()
  • Loading branch information
Lucas-C committed Feb 28, 2024
1 parent 88fea56 commit 35812d1
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 31 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<ol>` 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: `<a>`, `<blockquote>`, `<li>`...
* [`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 `<blockquote>` elements
### Changed
Expand Down
5 changes: 3 additions & 2 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,9 @@ def write_html(self, text, *args, **kwargs):
li_tag_indent (int): [**DEPRECATED since v2.7.8**] numeric indentation of <li> elements - Set tag_indents instead
dd_tag_indent (int): [**DEPRECATED since v2.7.8**] numeric indentation of <dd> elements - Set tag_indents instead
table_line_separators (bool): enable horizontal line separators in <table>
ul_bullet_char (str): bullet character for <ul> elements
ul_bullet_color (tuple | str | drawing.Device* instance): color of the <ul> bullets
ul_bullet_char (str): bullet character preceding <li> items in <ul> lists.
li_prefix_color (tuple | str | drawing.Device* instance): color for bullets or numbers preceding <li> tags.
This applies to both <ul> & <ol> 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 <pre> & <code> blocks - Set tag_styles instead
warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags
Expand Down
42 changes: 32 additions & 10 deletions fpdf/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -265,8 +267,9 @@ def __init__(
li_tag_indent (int): [**DEPRECATED since v2.7.9**] numeric indentation of <li> elements - Set tag_indents instead
dd_tag_indent (int): [**DEPRECATED since v2.7.9**] numeric indentation of <dd> elements - Set tag_indents instead
table_line_separators (bool): enable horizontal line separators in <table>
ul_bullet_char (str): bullet character for <ul> elements
ul_bullet_color (tuple | str | drawing.Device* instance): color of the <ul> bullets
ul_bullet_char (str): bullet character preceding <li> items in <ul> lists.
li_prefix_color (tuple | str | drawing.Device* instance): color for bullets or numbers preceding <li> tags.
This applies to both <ul> & <ol> lists.
heading_sizes (dict): [**DEPRECATED since v2.7.9**] font size per heading level names ("h1", "h2"...) - Set tag_styles instead
pre_code_font (str): [**DEPRECATED since v2.7.9**] font to use for <pre> & <code> blocks - Set tag_styles instead
warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags
Expand All @@ -277,10 +280,10 @@ def __init__(
self.pdf = pdf
self.image_map = image_map or (lambda src: src)
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.li_prefix_color = (
color_as_decimal(li_prefix_color)
if isinstance(li_prefix_color, str)
else convert_to_device_color(li_prefix_color).colors255
)
self.warn_on_tags_not_matching = warn_on_tags_not_matching

Expand Down Expand Up @@ -308,6 +311,7 @@ def __init__(
self.align = ""
self.style_stack = [] # list of FontFace
self.indent = 0
self.ol_type = [] # when inside a <ol> tag, can be "a", "A", "i", "I" or "1"
self.bullet = []
self.font_color = pdf.text_color.colors255
self.heading_level = None
Expand Down Expand Up @@ -640,11 +644,13 @@ def handle_starttag(self, tag, attrs):
self._new_paragraph()
if tag == "ol":
self.indent += 1
self.bullet.append(0)
start = int(attrs["start"]) if "start" in attrs else 1
self.bullet.append(start - 1)
self.ol_type.append(attrs.get("type", "1"))
self._new_paragraph()
if tag == "li":
self._ln(2)
self.set_text_color(*self.ul_bullet_color)
self.set_text_color(*self.li_prefix_color)
if self.bullet:
bullet = self.bullet[self.indent - 1]
else:
Expand All @@ -653,7 +659,8 @@ def handle_starttag(self, tag, attrs):
if not isinstance(bullet, str):
bullet += 1
self.bullet[self.indent - 1] = bullet
bullet = f"{bullet}. "
ol_type = self.ol_type[self.indent - 1]
bullet = f"{ol_prefix(ol_type, bullet)}. "
indent = "\u00a0" * self.tag_indents["li"] * self.indent
self._write_paragraph(f"{indent}{bullet} ")
self.set_text_color(*self.font_color)
Expand Down Expand Up @@ -852,6 +859,7 @@ def handle_endtag(self, tag):
if tag in ("ul", "ol"):
self._end_paragraph()
self.indent -= 1
self.ol_type.pop()
self.bullet.pop()
if tag == "table":
self.table.render()
Expand Down Expand Up @@ -964,6 +972,20 @@ def error(self, message):
raise RuntimeError(message)


def ol_prefix(ol_type, index):
if ol_type == "1":
return index
if ol_type == "a":
return ascii_lowercase[index - 1]
if ol_type == "A":
return ascii_uppercase[index - 1]
if ol_type == "I":
return int2roman(index)
if ol_type == "i":
return int2roman(index).lower()
raise NotImplementedError(f"Unsupported type: {ol_type}")


class HTMLMixin:
"""
[**DEPRECATED since v2.6.0**]
Expand Down
27 changes: 27 additions & 0 deletions fpdf/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,33 @@ def convert_unit(
return to_convert / unit_conversion_factor


ROMAN_NUMERAL_MAP = (
("M", 1000),
("CM", 900),
("D", 500),
("CD", 400),
("C", 100),
("XC", 90),
("L", 50),
("XL", 40),
("X", 10),
("IX", 9),
("V", 5),
("IV", 4),
("I", 1),
)


def int2roman(n):
"Convert an integer to Roman numeral"
result = ""
for numeral, integer in ROMAN_NUMERAL_MAP:
while n >= integer:
result += numeral
n -= integer
return result


################################################################################
################### Utility functions to track memory usage ####################
################################################################################
Expand Down
File renamed without changes.
Binary file added test/html/html_ol_start_and_type.pdf
Binary file not shown.
51 changes: 33 additions & 18 deletions test/html/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ def test_html_bold_italic_underline(tmp_path):

def test_html_customize_ul(tmp_path):
html = """<ul>
<li><b>term1</b>: definition1</li>
<li><b>term2</b>: definition2</li>
</ul>"""
<li><b>term1</b>: definition1</li>
<li><b>term2</b>: definition2</li>
</ul>"""

# 1. Customizing through class attributes:
class CustomPDF(FPDF):
Expand All @@ -203,21 +203,36 @@ class CustomPDF(FPDF):
pdf = CustomPDF()
pdf.set_font_size(30)
pdf.add_page()
pdf.write_html(html)
pdf.ln()
# 2. Customizing through instance attributes:
pdf.li_tag_indent = 10
pdf.ul_bullet_char = "\x9b"
pdf.write_html(html)
pdf.ln()
# 3. Customizing through optional method arguments:
for indent, bullet in ((15, "\xac"), (20, "\xb7")):
pdf.write_html(html, li_tag_indent=indent, ul_bullet_char=bullet)
with pytest.warns(DeprecationWarning): # li_tag_indent
pdf.write_html(html)
pdf.ln()
# 2. Customizing through instance attributes:
pdf.li_tag_indent = 10
pdf.ul_bullet_char = "\x9b"
pdf.write_html(html)
pdf.ln()
# 3. Customizing through optional method arguments:
for indent, bullet in ((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)


def test_html_ul_bullet_color(tmp_path):
def test_html_ol_start_and_type(tmp_path):
pdf = FPDF()
pdf.set_font_size(30)
pdf.add_page()
pdf.write_html(
"""<ol start="2" type="i">
<li>item</li>
<li>item</li>
<li>item</li>
</ol>"""
)
assert_pdf_equal(pdf, HERE / "html_ol_start_and_type.pdf", tmp_path)


def test_html_li_prefix_color(tmp_path):
html = """<ul>
<li>item1</li>
<li>item2</li>
Expand All @@ -227,13 +242,13 @@ def test_html_ul_bullet_color(tmp_path):
pdf = FPDF()
pdf.set_font_size(30)
pdf.add_page()
pdf.write_html(html, ul_bullet_color=0) # black
pdf.write_html(html, li_prefix_color=0) # black
pdf.ln()
pdf.write_html(html, ul_bullet_color="green")
pdf.write_html(html, li_prefix_color="green")
pdf.ln()
pdf.write_html(html, ul_bullet_color=DeviceRGB(r=0.5, g=1, b=0))
pdf.write_html(html, li_prefix_color=DeviceRGB(r=0.5, g=1, b=0))
pdf.ln()
assert_pdf_equal(pdf, HERE / "html_ul_bullet_color.pdf", tmp_path)
assert_pdf_equal(pdf, HERE / "html_li_prefix_color.pdf", tmp_path)


def test_html_align_paragraph(tmp_path):
Expand Down

0 comments on commit 35812d1

Please sign in to comment.