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

Allow for using wrapmode='CHAR' in templates #1176

Merged
merged 10 commits into from
May 27, 2024
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ This can also be enabled programmatically with `warnings.simplefilter('default',

### Deprecated

## [2.7.10] - 2024-05-18
### Added
* support to optionally set `wrapmode` in templates (default `"WORD"` can optionally be set to `"CHAR"` to support wrapping on characters for scripts like Chinese or Japanese) - _cf._ [#1159](https://github.com/py-pdf/fpdf2/issues/1159)

## [2.7.9] - 2024-05-17
### Added
Expand Down
7 changes: 5 additions & 2 deletions docs/Templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ Dimensions (except font size, which always uses points) are given in user define
* __rotation__: rotate the element in degrees around the top left corner x1/y1 (float)
* _optional_
* default: 0.0 - no rotation
* __wrapmode__: optionally set wrapmode to "CHAR" to support multiline line wrapping on characters instead of words
* _optional_
* default: 'WORD'

Fields that are not relevant to a specific element type will be ignored there, but if not left empty, they must still adhere to the specified data type (in dicts, string fields may be None).

Expand All @@ -233,7 +236,7 @@ from fpdf import Template
elements = [
{ 'name': 'company_logo', 'type': 'I', 'x1': 20.0, 'y1': 17.0, 'x2': 78.0, 'y2': 30.0, 'font': None, 'size': 0.0, 'bold': 0, 'italic': 0, 'underline': 0, 'align': 'C', 'text': 'logo', 'priority': 2, 'multiline': False},
{ 'name': 'company_name', 'type': 'T', 'x1': 17.0, 'y1': 32.5, 'x2': 115.0, 'y2': 37.5, 'font': 'helvetica', 'size': 12.0, 'bold': 1, 'italic': 0, 'underline': 0,'align': 'C', 'text': '', 'priority': 2, 'multiline': False},
{ 'name': 'multline_text', 'type': 'T', 'x1': 20, 'y1': 100, 'x2': 40, 'y2': 105, 'font': 'helvetica', 'size': 12, 'bold': 0, 'italic': 0, 'underline': 0, 'background': 0x88ff00, 'align': 'C', 'text': 'Lorem ipsum dolor sit amet, consectetur adipisici elit', 'priority': 2, 'multiline': True},
{ 'name': 'multline_text', 'type': 'T', 'x1': 20, 'y1': 100, 'x2': 40, 'y2': 105, 'font': 'helvetica', 'size': 12, 'bold': 0, 'italic': 0, 'underline': 0, 'background': 0x88ff00, 'align': 'C', 'text': 'Lorem ipsum dolor sit amet, consectetur adipisici elit', 'priority': 2, 'multiline': True, 'wrapmode': 'WORD'},
{ 'name': 'box', 'type': 'B', 'x1': 15.0, 'y1': 15.0, 'x2': 185.0, 'y2': 260.0, 'font': 'helvetica', 'size': 0.0, 'bold': 0, 'italic': 0, 'underline': 0, 'align': 'C', 'text': None, 'priority': 0, 'multiline': False},
{ 'name': 'box_x', 'type': 'B', 'x1': 95.0, 'y1': 15.0, 'x2': 105.0, 'y2': 25.0, 'font': 'helvetica', 'size': 0.0, 'bold': 1, 'italic': 0, 'underline': 0, 'align': 'C', 'text': None, 'priority': 2, 'multiline': False},
{ 'name': 'line1', 'type': 'L', 'x1': 100.0, 'y1': 25.0, 'x2': 100.0, 'y2': 57.0, 'font': 'helvetica', 'size': 0, 'bold': 0, 'italic': 0, 'underline': 0, 'align': 'C', 'text': None, 'priority': 3, 'multiline': False},
Expand Down Expand Up @@ -272,7 +275,7 @@ rotated;T;21.0;80.0;100.0;84.0;times;10.5;0;0;0;0;;R;ROTATED;0;0;30.0
```

Remember that each line represents an element and each field represents one of the properties of the element in the following order:
('name','type','x1','y1','x2','y2','font','size','bold','italic','underline','foreground','background','align','text','priority', 'multiline', 'rotate')
('name','type','x1','y1','x2','y2','font','size','bold','italic','underline','foreground','background','align','text','priority', 'multiline', 'rotate', 'wrapmode')
gmischler marked this conversation as resolved.
Show resolved Hide resolved
As noted above, most fields may be left empty, so a line is valid with only 6 items. The "empty_fields" line of the example demonstrates all that can be left away. In addition, for the barcode types "x2" may be empty.

Then you can use the file like this:
Expand Down
13 changes: 12 additions & 1 deletion fpdf/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def load_elements(self, elements):
"priority": int,
"multiline": (bool, type(None)),
"rotate": (int, float),
"wrapmode": (str, type(None)),
}

self.elements = elements
Expand Down Expand Up @@ -190,6 +191,7 @@ def _varsep_float(s, default="0"):
("priority", int, False),
("multiline", self._parse_multiline, False),
("rotate", _varsep_float, False),
("wrapmode", str, False),
)
self.elements = []
if encoding is None:
Expand Down Expand Up @@ -290,6 +292,7 @@ def split_multicell(self, text, element_name):
align=element.get("align", ""),
dry_run=True,
output="LINES",
wrapmode=element.get("wrapmode", "WORD"),
)

def _text(
Expand All @@ -310,6 +313,7 @@ def _text(
foreground=0,
background=None,
multiline=None,
wrapmode="WORD",
**__,
):
if not text:
Expand Down Expand Up @@ -343,14 +347,21 @@ def _text(
pdf.cell(w=width, h=height, text=text, border=0, align=align, fill=fill)
elif multiline: # automatic word - warp
pdf.multi_cell(
w=width, h=height, text=text, border=0, align=align, fill=fill
w=width,
h=height,
text=text,
border=0,
align=align,
fill=fill,
wrapmode=wrapmode,
)
else: # trim to fit exactly the space defined
text = pdf.multi_cell(
w=width,
h=height,
text=text,
align=align,
wrapmode=wrapmode,
dry_run=True,
output="LINES",
)[0]
Expand Down
54 changes: 54 additions & 0 deletions test/template/charwrap_test_elements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
elements = [
{
"name": "multi",
"type": "T",
"x1": 80,
"y1": 10,
"x2": 170,
"y2": 15,
"text": "If multiline is False, the text should still not wrap even if wrapmode is specified.",
"background": 0xEEFFFF,
"multiline": False,
"wrapmode": "WORD",
},
{
"name": "multi",
"type": "T",
"x1": 80,
"y1": 40,
"x2": 170,
"y2": 45,
"text": "If multiline is True, and wrapmode is omitted, the text should wrap by word and not "
+ "cause an error due to omission of the wrapmode argument.",
"background": 0xEEFFFF,
"multiline": True,
},
{
"name": "multi",
"type": "T",
"x1": 80,
"y1": 70,
"x2": 170,
"y2": 75,
"text": "If multiline is True and the wrapmode argument is provided, it should not cause a "
+ "problem even if using the default wrapmode of 'WORD'.",
"background": 0xEEFFFF,
"multiline": True,
"wrapmode": "WORD",
},
{
"name": "multi",
"type": "T",
"x1": 80,
"y1": 100,
"x2": 170,
"y2": 105,
"text": "If multiline is True and the wrapmode is 'CHAR', it should result in "
+ "wrapping based on characters instead of words, regardless of language (i.e. even though "
+ "this is designed to support scripts like Chinese and Japanese, wrapping long sentences "
+ "like this in English still demonstrates functionality.)",
"background": 0xEEFFFF,
"multiline": True,
"wrapmode": "CHAR",
},
]
Binary file modified test/template/flextemplate_multipage.pdf
Binary file not shown.
Binary file added test/template/flextemplate_wrapmode.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions test/template/mycsvfile.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ empty_fields;T;21.0;100.0;100.0;104.0
rotated;T;21.0;80.0;100.0;84.0;times;10.5;0;0;0;0;;R;ROTATED;0;0;30
cropped;T;31.0;50.0;40.0;54.0;times;10.5;0;0;0;0;;L;cropped;0;-1
barcode;BC;31.0;70.0;;80.0;;1;;;;;;;987654321;0;0
character_wrapped;T;41.0;100.0;55.0;104.0;times;10.5;0;0;0;0;0xffffff;L;wrap on characters not words;0;1;0;CHAR
Binary file modified test/template/template_multipage.pdf
Binary file not shown.
Binary file modified test/template/template_nominal_csv.pdf
Binary file not shown.
Binary file added test/template/template_wrapmode.pdf
Binary file not shown.
12 changes: 12 additions & 0 deletions test/template/test_flextemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from fpdf.fpdf import FPDF
from fpdf.template import FlexTemplate
from ..conftest import assert_pdf_equal
from test.template.charwrap_test_elements import elements as charwrap_elements

HERE = Path(__file__).resolve().parent

Expand Down Expand Up @@ -508,3 +509,14 @@ def test_flextemplate_leak(tmp_path): # issue #570
pdf.ln()
pdf.cell(text="after", new_x="LEFT", new_y="NEXT")
assert_pdf_equal(pdf, HERE / "flextemplate_leak.pdf", tmp_path)


def test_flextemplate_wrapmode(tmp_path):
gmischler marked this conversation as resolved.
Show resolved Hide resolved
"""Test that wrap mode can optionally be used to set wrapping using characters instead of words"""

pdf = FPDF()
pdf.add_page()
pdf.set_font("courier", "", 10)
templ = FlexTemplate(pdf, charwrap_elements)
templ.render()
assert_pdf_equal(pdf, HERE / "flextemplate_wrapmode.pdf", tmp_path)
11 changes: 8 additions & 3 deletions test/template/test_template.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from pathlib import Path
from pytest import raises, warns

import qrcode

from fpdf.template import Template, FPDFException

from ..conftest import assert_pdf_equal
from test.template.charwrap_test_elements import elements as charwrap_elements

HERE = Path(__file__).resolve().parent

Expand Down Expand Up @@ -589,3 +587,10 @@ def test_template_split_multicell():
tmpl = Template(format="A4", unit="pt", elements=elements)
res = tmpl.split_multicell(text, "multline_text")
assert res == expected


def test_template_wrapmode(tmp_path):
# Test that wrap mode can optionally be used to set wrapping using characters instead of words.
tmpl = Template(elements=charwrap_elements)
tmpl.add_page()
assert_pdf_equal(tmpl, HERE / "template_wrapmode.pdf", tmp_path)
Loading