Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling of text overflow in FPDF.write() & FPDF.write_html() - fix #847 #850

Merged
merged 1 commit into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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 @@ -3593,7 +3593,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.