py-pdf/fpdf2

Combination of set_text_shaping and offset_rendering causing error

jsid72 opened this issue ยท 5 comments

Describe the bug

Use of set_text_shaping(True) [needed for Arabic support] and offset_rendering() [used to handle page break] in combination are causing an error. If we remove one of these two from the code then things work fine.

Error details

Traceback (most recent call last)
Cell In[54], line 21
     19 col_width = pdf.epw / 4  # distribute content evenly
     20 for i in range(5):  # repeat table 4 times
---> 21     with pdf.offset_rendering() as dummy:
     22         for row in data:  # data comes from snippets on the Tables documentation page
     23             for datum in row:

File C:\anaconda3\lib\contextlib.py:119, in _GeneratorContextManager.__enter__(self)
    117 del self.args, self.kwds, self.func
    118 try:
--> 119     return next(self.gen)
    120 except StopIteration:
    121     raise RuntimeError("generator didn't yield") from None

File C:\PythonPDFMaking\lib\site-packages\fpdf\fpdf.py:4678, in FPDF.offset_rendering(self)
   4672 """
   4673 All rendering performed in this context is made on a dummy FPDF object.
   4674 This allows to test the results of some operations on the global layout
   4675 before performing them "for real".
   4676 """
   4677 prev_page, prev_y = self.page, self.y
-> 4678 recorder = FPDFRecorder(self, accept_page_break=False)
   4679 recorder.page_break_triggered = False
   4680 yield recorder

File C:\PythonPDFMaking\lib\site-packages\fpdf\recorder.py:37, in FPDFRecorder.__init__(self, pdf, accept_page_break)
     35 def __init__(self, pdf, accept_page_break=True):
     36     self.pdf = pdf
---> 37     self._initial = deepcopy(self.pdf.__dict__)
     38     self._calls = []
     39     if not accept_page_break:

File C:\anaconda3\lib\copy.py:146, in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File C:\anaconda3\lib\copy.py:230, in _deepcopy_dict(x, memo, deepcopy)
    228 memo[id(x)] = y
    229 for key, value in x.items():
--> 230     y[deepcopy(key, memo)] = deepcopy(value, memo)
    231 return y

File C:\anaconda3\lib\copy.py:146, in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File C:\anaconda3\lib\copy.py:205, in _deepcopy_list(x, memo, deepcopy)
    203 append = y.append
    204 for a in x:
--> 205     append(deepcopy(a, memo))
    206 return y

File C:\anaconda3\lib\copy.py:146, in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File C:\anaconda3\lib\copy.py:230, in _deepcopy_dict(x, memo, deepcopy)
    228 memo[id(x)] = y
    229 for key, value in x.items():
--> 230     y[deepcopy(key, memo)] = deepcopy(value, memo)
    231 return y

File C:\anaconda3\lib\copy.py:172, in deepcopy(x, memo, _nil)
    170                 y = x
    171             else:
--> 172                 y = _reconstruct(x, memo, *rv)
    174 # If is its own copy, don't memoize.
    175 if y is not x:

File C:\anaconda3\lib\copy.py:270, in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    268 if state is not None:
    269     if deep:
--> 270         state = deepcopy(state, memo)
    271     if hasattr(y, '__setstate__'):
    272         y.__setstate__(state)

File C:\anaconda3\lib\copy.py:146, in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File C:\anaconda3\lib\copy.py:210, in _deepcopy_tuple(x, memo, deepcopy)
    209 def _deepcopy_tuple(x, memo, deepcopy=deepcopy):
--> 210     y = [deepcopy(a, memo) for a in x]
    211     # We're not going to put the tuple in the memo, but it's still important we
    212     # check for it, in case the tuple contains recursive mutable structures.
    213     try:

File C:\anaconda3\lib\copy.py:210, in <listcomp>(.0)
    209 def _deepcopy_tuple(x, memo, deepcopy=deepcopy):
--> 210     y = [deepcopy(a, memo) for a in x]
    211     # We're not going to put the tuple in the memo, but it's still important we
    212     # check for it, in case the tuple contains recursive mutable structures.
    213     try:

File C:\anaconda3\lib\copy.py:146, in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File C:\anaconda3\lib\copy.py:230, in _deepcopy_dict(x, memo, deepcopy)
    228 memo[id(x)] = y
    229 for key, value in x.items():
--> 230     y[deepcopy(key, memo)] = deepcopy(value, memo)
    231 return y

File C:\anaconda3\lib\copy.py:161, in deepcopy(x, memo, _nil)
    159 reductor = getattr(x, "__reduce_ex__", None)
    160 if reductor is not None:
--> 161     rv = reductor(4)
    162 else:
    163     reductor = getattr(x, "_ _reduce_ _", None)

File <stringsource>:2, in uharfbuzz._harfbuzz.Font.__reduce_cython__()

TypeError: no default __reduce__ due to non-trivial __cinit__

Minimal code

data = (
    ("01", "A", "6"),
    ("02", "B", "6"),
    ("03", "C", "1"),
    ("04", "D", "1"),
    ("05", "E", "4"),
)

from fpdf import FPDF
pdf = FPDF()
pdf.add_page()

font_folder = r'C:\WINDOWS\FONTS\\'
pdf.add_font('Calibri','',font_folder + 'calibri.ttf')
pdf.set_text_shaping(True)

pdf.set_font("Calibri", size=16)
line_height = pdf.font_size * 2
col_width = pdf.epw / 4  # distribute content evenly
for i in range(5):  # repeat table 4 times
    with pdf.offset_rendering() as dummy:
        for row in data:  # data comes from snippets on the Tables documentation page
            for datum in row:
                dummy.cell(col_width, line_height, f"{datum} ({i})", border=1)
            dummy.ln(line_height)
        dummy.ln(line_height * 2)
    if dummy.page_break_triggered:
        pdf.add_page()
        pdf.cell(text="Appendix C")
        pdf.ln(line_height)
    for row in data:  # data comes from snippets on the Tables documentation page
        for datum in row:
            pdf.cell(col_width, line_height, f"{datum} ({i})", border=1)
        pdf.ln(line_height)
    pdf.ln(line_height * 2)
pdf.output("unbreakable_tables.pdf")

Environment

  • Operating System: Windows 10
  • Python version: 3.9.13
  • fpdf2 version used: 2.7.6

Thank you for the clear bug report @jsid72!

Seems like uharfbuzz.Font does not like being deepcopied:
https://github.com/py-pdf/fpdf2/blob/2.7.7/fpdf/fonts.py#L275

There is some really minimal code reproducing this error:

from copy import deepcopy
import uharfbuzz as hb

font = hb.Font(hb.Face(hb.Blob.from_file_path('calibri.ttf')))
deepcopy(font)

I opened #1084 to fix this

Could you please review it @andersonhc?

@allcontributors please add @jsid72 for bug

@Lucas-C

I've put up a pull request to add @jsid72! ๐ŸŽ‰

The fix has been merged into the master branch of this repo, but not released yet.

You can install this unreleased latest version this way, if you want to test that this fix solves your initial problem:

pip install git+https://github.com/py-pdf/fpdf2.git@master