lists.
heading_sizes (dict): [**DEPRECATED since v2.7.8**] font size per heading level names ("h1", "h2"...) - Set tag_styles instead
pre_code_font (str): [**DEPRECATED since v2.7.8**] font to use for & blocks - Set tag_styles instead
warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags
diff --git a/fpdf/html.py b/fpdf/html.py
index be17c635d..3893acd9e 100644
--- a/fpdf/html.py
+++ b/fpdf/html.py
@@ -7,6 +7,7 @@
"""
from html.parser import HTMLParser
+from string import ascii_lowercase, ascii_uppercase
import logging, re, warnings
from .deprecation import get_stack_level
@@ -15,6 +16,7 @@
from .errors import FPDFException
from .fonts import FontFace
from .table import Table
+from .util import int2roman
LOGGER = logging.getLogger(__name__)
BULLET_WIN1252 = "\x95" # BULLET character in Windows-1252 encoding
@@ -249,7 +251,7 @@ def __init__(
dd_tag_indent=10,
table_line_separators=False,
ul_bullet_char=BULLET_WIN1252,
- ul_bullet_color=(190, 0, 0),
+ li_prefix_color=(190, 0, 0),
heading_sizes=None,
pre_code_font=DEFAULT_TAG_STYLES["pre"].family,
warn_on_tags_not_matching=True,
@@ -265,8 +267,9 @@ def __init__(
li_tag_indent (int): [**DEPRECATED since v2.7.9**] numeric indentation of - elements - Set tag_indents instead
dd_tag_indent (int): [**DEPRECATED since v2.7.9**] numeric indentation of
- elements - Set tag_indents instead
table_line_separators (bool): enable horizontal line separators in
- ul_bullet_char (str): bullet character for elements
- ul_bullet_color (tuple | str | drawing.Device* instance): color of the bullets
+ ul_bullet_char (str): bullet character preceding - items in
lists.
+ li_prefix_color (tuple | str | drawing.Device* instance): color for bullets or numbers preceding - tags.
+ This applies to both
& lists.
heading_sizes (dict): [**DEPRECATED since v2.7.9**] font size per heading level names ("h1", "h2"...) - Set tag_styles instead
pre_code_font (str): [**DEPRECATED since v2.7.9**] font to use for & blocks - Set tag_styles instead
warn_on_tags_not_matching (bool): control warnings production for unmatched HTML tags
@@ -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
@@ -308,6 +311,7 @@ def __init__(
self.align = ""
self.style_stack = [] # list of FontFace
self.indent = 0
+ self.ol_type = [] # when inside a tag, can be "a", "A", "i", "I" or "1"
self.bullet = []
self.font_color = pdf.text_color.colors255
self.heading_level = None
@@ -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:
@@ -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)
@@ -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()
@@ -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**]
diff --git a/fpdf/util.py b/fpdf/util.py
index 9734cbd4c..5bfc109d7 100644
--- a/fpdf/util.py
+++ b/fpdf/util.py
@@ -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 ####################
################################################################################
diff --git a/test/html/html_ul_bullet_color.pdf b/test/html/html_html_li_prefix_color.pdf
similarity index 100%
rename from test/html/html_ul_bullet_color.pdf
rename to test/html/html_html_li_prefix_color.pdf
diff --git a/test/html/html_ol_start_and_type.pdf b/test/html/html_ol_start_and_type.pdf
new file mode 100644
index 000000000..bf956f440
Binary files /dev/null and b/test/html/html_ol_start_and_type.pdf differ
diff --git a/test/html/test_html.py b/test/html/test_html.py
index efda78936..8d5a12b48 100644
--- a/test/html/test_html.py
+++ b/test/html/test_html.py
@@ -191,9 +191,9 @@ def test_html_bold_italic_underline(tmp_path):
def test_html_customize_ul(tmp_path):
html = """
- - term1: definition1
- - term2: definition2
-
"""
+ - term1: definition1
+ - term2: definition2
+
"""
# 1. Customizing through class attributes:
class CustomPDF(FPDF):
@@ -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(
+ """
+ - item
+ - item
+ - item
+
"""
+ )
+ assert_pdf_equal(pdf, HERE / "html_ol_start_and_type.pdf", tmp_path)
+
+
+def test_html_li_prefix_color(tmp_path):
html = """
- item1
- item2
@@ -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):