Skip to content

Commit

Permalink
Better handling of text overflow in FPDF.write() & FPDF.write_html() -
Browse files Browse the repository at this point in the history
…fix #847 (#850)
  • Loading branch information
Lucas-C authored Jul 12, 2023
1 parent 199d419 commit e9abc00
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
### Fixed
- [`FPDF.table()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): the `colspan` setting has been fixed - [documentation](https://pyfpdf.github.io/fpdf2/Tables.html#column-span)
- [`FPDF.image()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image): allowing images path starting with `data` to be passed as input
- text overflow is better handled by `FPDF.write()` & `FPDF.write_html()` - _cf._ [issue #847](https://github.com/PyFPDF/fpdf2/issues/847)
- the initial text color is preserved when using `FPDF.write_html()` - _cf._ [issue #846](https://github.com/PyFPDF/fpdf2/issues/846)
### Deprecated
- the `center` optional parameter of [`FPDF.cell()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.cell) is **no more** deprecated, as it allows for horizontal positioning, which is different from text alignment control with `align="C"`
Expand Down
3 changes: 3 additions & 0 deletions fpdf/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def __init__(self, fpdf, fontkey, style):
self.fontkey = fontkey
self.emphasis = TextEmphasis.coerce(style)

def __repr__(self):
return f"CoreFont(i={self.i}, fontkey={self.fontkey})"


class TTFFont:
__slots__ = (
Expand Down
2 changes: 1 addition & 1 deletion fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3605,7 +3605,7 @@ def write(
# first line from current x position to right margin
first_width = self.w - self.x - self.r_margin
txt_line = multi_line_break.get_line_of_given_width(
first_width - 2 * self.c_margin, wordsplit=False
first_width - 2 * self.c_margin,
)
# remaining lines fill between margins
full_width = self.w - self.l_margin - self.r_margin
Expand Down
21 changes: 3 additions & 18 deletions fpdf/line_break.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,10 @@ def __init__(
self.link = link

def __repr__(self):
gstate = self.graphics_state.copy()
if "current_font" in gstate:
del gstate["current_font"] # TMI
return (
f"Fragment(characters={self.characters},"
f" graphics_state={gstate}, k={self.k}, link={self.link})"
f" graphics_state={self.graphics_state},"
f" k={self.k}, link={self.link})"
)

@property
Expand Down Expand Up @@ -394,18 +392,14 @@ def __init__(
self.idx_last_forced_break = None

# pylint: disable=too-many-return-statements
def get_line_of_given_width(self, maximum_width: float, wordsplit: bool = True):
def get_line_of_given_width(self, maximum_width: float):
first_char = True # "Tw" ignores the first character in a text object.
idx_last_forced_break = self.idx_last_forced_break
self.idx_last_forced_break = None

if self.fragment_index == len(self.styled_text_fragments):
return None

last_fragment_index = self.fragment_index
last_character_index = self.character_index
line_full = False

current_line = CurrentLine(print_sh=self.print_sh)
while self.fragment_index < len(self.styled_text_fragments):
current_fragment = self.styled_text_fragments[self.fragment_index]
Expand Down Expand Up @@ -442,9 +436,6 @@ def get_line_of_given_width(self, maximum_width: float, wordsplit: bool = True):
) = current_line.automatic_break(self.justify)
self.character_index += 1
return line
if not wordsplit:
line_full = True
break
if idx_last_forced_break == self.character_index:
raise FPDFException(
"Not enough horizontal space to render a single character"
Expand All @@ -464,12 +455,6 @@ def get_line_of_given_width(self, maximum_width: float, wordsplit: bool = True):

self.character_index += 1

if line_full and not wordsplit:
# roll back and return empty line to trigger continuation
# on the next line.
self.fragment_index = last_fragment_index
self.character_index = last_character_index
return CurrentLine().manual_break(self.justify)
if current_line.width:
return current_line.manual_break()
return None
10 changes: 10 additions & 0 deletions test/text/test_line_break.py
Original file line number Diff line number Diff line change
Expand Up @@ -1129,3 +1129,13 @@ def test_trim_trailing_spaces():
cl.fragments = [frag]
res = cl.trim_trailing_spaces()
assert res is None


def test_line_break_no_initial_newline(): # issue-847
text = "X" * 50
alphabet = {"normal": {}}
alphabet["normal"]["X"] = 4.7
fragments = [FxFragment(alphabet, text, _gs_normal, 1)]
multi_line_break = MultiLineBreak(fragments)
text_line = multi_line_break.get_line_of_given_width(188)
assert text_line.fragments
78 changes: 39 additions & 39 deletions test/text/test_unbreakable.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,12 @@ def test_multi_cell_table_unbreakable_with_split_only(tmp_path): # issue 359

pdf.ln()

with pdf.unbreakable() as doc:
for _ in range(4):
for row in data:
max_no_of_lines_in_cell = 1
for cell in row:
with pytest.warns(DeprecationWarning, match=expected_warn):
with pytest.warns(DeprecationWarning, match=expected_warn):
with pdf.unbreakable() as doc:
for _ in range(4):
for row in data:
max_no_of_lines_in_cell = 1
for cell in row:
result = doc.multi_cell(
cell_width,
l_height,
Expand All @@ -184,39 +184,39 @@ def test_multi_cell_table_unbreakable_with_split_only(tmp_path): # issue 359
max_line_height=l_height,
split_only=True,
)
no_of_lines_in_cell = len(result)
if no_of_lines_in_cell > max_no_of_lines_in_cell:
max_no_of_lines_in_cell = no_of_lines_in_cell
no_of_lines_list.append(max_no_of_lines_in_cell)

for j, row in enumerate(data):
cell_height = no_of_lines_list[j] * l_height
for cell in row:
if j == 0:
doc.multi_cell(
cell_width,
cell_height,
"**" + cell + "**",
border=1,
fill=False,
align="L",
new_x="RIGHT",
new_y="TOP",
max_line_height=l_height,
markdown=False,
)
else:
doc.multi_cell(
cell_width,
cell_height,
cell,
border=1,
align="L",
new_x="RIGHT",
new_y="TOP",
max_line_height=l_height,
)
doc.ln(cell_height)
no_of_lines_in_cell = len(result)
if no_of_lines_in_cell > max_no_of_lines_in_cell:
max_no_of_lines_in_cell = no_of_lines_in_cell
no_of_lines_list.append(max_no_of_lines_in_cell)

for j, row in enumerate(data):
cell_height = no_of_lines_list[j] * l_height
for cell in row:
if j == 0:
doc.multi_cell(
cell_width,
cell_height,
"**" + cell + "**",
border=1,
fill=False,
align="L",
new_x="RIGHT",
new_y="TOP",
max_line_height=l_height,
markdown=False,
)
else:
doc.multi_cell(
cell_width,
cell_height,
cell,
border=1,
align="L",
new_x="RIGHT",
new_y="TOP",
max_line_height=l_height,
)
doc.ln(cell_height)

assert_pdf_equal(
pdf, HERE / "multi_cell_table_unbreakable_with_split_only.pdf", tmp_path
Expand Down
2 changes: 1 addition & 1 deletion test/text/test_varied_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def write_fragments(self, frags, align=Align.L):
# first line from current x position to right margin
first_width = self.w - self.x - self.r_margin
text_line = multi_line_break.get_line_of_given_width(
first_width - 2 * self.c_margin, wordsplit=False
first_width - 2 * self.c_margin
)
# remaining lines fill between margins
full_width = self.w - self.l_margin - self.r_margin
Expand Down
29 changes: 22 additions & 7 deletions test/text/test_write.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from pathlib import Path

import fpdf
from fpdf import FPDF
from test.conftest import assert_pdf_equal, LOREM_IPSUM

HERE = Path(__file__).resolve().parent
FONTS_DIR = HERE.parent / "fonts"


def test_write_page_break(tmp_path):
doc = fpdf.FPDF()
doc = FPDF()
doc.add_page()
doc.set_font("helvetica", size=24)
doc.y = 20
Expand All @@ -18,8 +18,15 @@ def test_write_page_break(tmp_path):


def test_write_soft_hyphen(tmp_path):
"""
The current behaviour is close to CSS word-break: break-all
cf. https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-wrap#comparing_overflow-wrap_word-break_and_hyphens
We used to prefer a line break over a word split without regards to soft hyphens:
https://github.com/PyFPDF/fpdf2/blob/2.7.4/test/text/write_soft_hyphen.pdf
But that caused issue with write_html(), cf. issue #847
"""
s = "Donau\u00addamp\u00adfschiff\u00adfahrts\u00adgesellschafts\u00adkapitäns\u00admützen\u00adstreifen. "
doc = fpdf.FPDF()
doc = FPDF()
doc.add_page()
doc.set_font("helvetica", size=24)
doc.y = 20
Expand All @@ -41,7 +48,7 @@ def test_write_soft_hyphen(tmp_path):

def test_write_trailing_nl(tmp_path): # issue #455
"""Each item in lines triggers a line break at the end."""
pdf = fpdf.FPDF()
pdf = FPDF()
pdf.add_page()
pdf.set_font("Times", size=16)
lines = ["Hello\n", "Sweet\n", "World\n"]
Expand All @@ -53,7 +60,7 @@ def test_write_trailing_nl(tmp_path): # issue #455

def test_write_font_stretching(tmp_path): # issue #478
right_boundary = 60
pdf = fpdf.FPDF()
pdf = FPDF()
pdf.add_page()
# built-in font
pdf.set_font("Helvetica", "", 8)
Expand Down Expand Up @@ -81,7 +88,7 @@ def test_write_font_stretching(tmp_path): # issue #478


def test_write_superscript(tmp_path):
pdf = fpdf.FPDF()
pdf = FPDF()
pdf.add_page()
pdf.set_font("Helvetica", "", 20)

Expand Down Expand Up @@ -131,7 +138,7 @@ def write_this():

def test_write_char_wrap(tmp_path): # issue #649
right_boundary = 50
pdf = fpdf.FPDF()
pdf = FPDF()
pdf.add_page()
pdf.set_right_margin(pdf.w - right_boundary)
pdf.set_font("Helvetica", "", 10)
Expand All @@ -150,3 +157,11 @@ def test_write_char_wrap(tmp_path): # issue #649
pdf.line(pdf.l_margin, 10, pdf.l_margin, 130)
pdf.line(right_boundary, 10, right_boundary, 130)
assert_pdf_equal(pdf, HERE / "write_char_wrap.pdf", tmp_path)


def test_write_overflow_no_initial_newline(tmp_path): # issue-847
pdf = FPDF()
pdf.add_page()
pdf.set_font(family="Helvetica", size=20)
pdf.write(7, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
assert_pdf_equal(pdf, HERE / "write_overflow_no_initial_newline.pdf", tmp_path)
Binary file added test/text/write_overflow_no_initial_newline.pdf
Binary file not shown.
Binary file modified test/text/write_soft_hyphen.pdf
Binary file not shown.

0 comments on commit e9abc00

Please sign in to comment.