fpdf2 icon indicating copy to clipboard operation
fpdf2 copied to clipboard

Width calculated by get_string_width is slightly too small for multi_cell

Open andreaswimmer opened this issue 9 months ago • 7 comments

Error details get_string_width returns a (slightly) too small a value, leading to multi_cell splitting the string.

In the snippet below, I calculate the width of a string with get_string_width and then create a multi_cell with exactly that width (127.512) However multi_cell deems that too small and inserts a line break. It is not due to c_margin, which I set to 0/include in the calculation.

This bug is very finicky and hard to hit. Small changes in the text, increasing the value by a tiny amount (length += 1e-12), changing the font or the page unit all make it disappear.

From what I can tell, this is a classic floating point error: MultiLineBreak calculates the width as 127.51200000000001 - which is above the limit of 127.512 - and inserts a line break.

Is this fixable? My current workaround is to round up the values of get_string_width, is there a better way you'd recommend?

Minimal code You will need the font Inter, which you can get for free at https://rsms.me/inter/download/ I tried to reproduce the issue with a default font, but couldn't.

from fpdf import FPDF
from fpdf.enums import MethodReturnValue

doc = FPDF(unit="pt")
doc.add_font("Inter", fname="./Inter.ttc")
doc.set_font("Inter")
doc.add_page()
# the bug occurs even if not setting c_margin to 0, since it is included in the length below
doc.c_margin = 0
text = "2025-03-11 17:42 UTC"
length = doc.get_string_width(text) + doc.c_margin * 2
# length = 127.512
print(f"length = {length}")
lines = doc.multi_cell(
    text=text,
    dry_run=True,
    output=MethodReturnValue.LINES,
    w=length,
)

# this fails
assert len(lines) == 1, lines

Environment

  • Operating System: Windows 11
  • Python version: 3.9.21
  • fpdf2 version used: 2.8.2

andreaswimmer avatar Mar 12 '25 13:03 andreaswimmer

Thank you for the bug report @andreaswimmer 👍

I was able to reproduce the problem.

My current workaround is to round up the values of get_string_width, is there a better way you'd recommend?

Where have you been inserting those rounding operations?

Would you like to submit a PR to fix this? 🙂

Lucas-C avatar Mar 12 '25 14:03 Lucas-C

@allcontributors please add @andreaswimmer for bug

Lucas-C avatar Mar 12 '25 14:03 Lucas-C

@Lucas-C

I've put up a pull request to add @andreaswimmer! :tada:

allcontributors[bot] avatar Mar 12 '25 14:03 allcontributors[bot]

My current workaround is to round up the values of get_string_width, is there a better way you'd recommend?

Where have you been inserting those rounding operations?

just immediately after the call. So in the code example above, I'd insert

length = math.floor(length + 1)

This rounds up if it's a floating point number and adds one if it happens to be an integer. With the my unit being set to pt this is not a big change.

Would you like to submit a PR to fix this? 🙂

I'm not sure I know what the best fix would be in the first place. If the units are different from pt, adding one seems like a lot. Like, if the unit is cm, all text measurements being off by up to 1cm seems unreasonably huge.

andreaswimmer avatar Mar 12 '25 16:03 andreaswimmer

I've ran into this problem as well and worked around it by rounding to a distant decimal place using math.ceil(value * 10000) / 10000, or by adding a fudge factor of 1e-10. This isn't a great solution though, I'm not sure what the best way to fix this is. Maybe something related to doing "close-enough" comparisons rather than strict comparisons with floating point values inside fpdf

jasonfevangsafe avatar Mar 13 '25 19:03 jasonfevangsafe

Hey @andersonhc , I would love to work on this issue. Please assign the issue to me.

Vedant817 avatar Oct 13 '25 21:10 Vedant817

@Vedant817 you have asked to be assigned to 6 issues in the past 24 hours. This is a lot, and it makes me doubt how serious you are about contributing to fpdf2. Please choose only a single issue to work on at a time, and start by working on a first PR, or you behaviour will be considered to be spam.

Lucas-C avatar Oct 14 '25 09:10 Lucas-C