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 keep_aspect_ratio flag in templates. #1119

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
## [2.7.9] - Not released yet
### Added
* support for overriding paragraph direction on bidirectional text
* support for keeping aspect ratio for images in templates
### Fixed

### Changed
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
* __keep\_aspect\_ratio__: keeps the aspect ratio when scaling image to fit inside the bounds. See [Fitting an image inside a rectange](https://py-pdf.github.io/fpdf2/Images.html#fitting-an-image-inside-a-rectangle) for more details on how this works.
* _optional_
* default: false

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 @@ -231,7 +234,7 @@ from fpdf import Template

#this will define the ELEMENTS that will compose the 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_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, 'keep_aspect_ratio': 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': '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},
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', 'keep_aspect_ratio')
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
16 changes: 14 additions & 2 deletions 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),
"keep_aspect_ratio": object, # "bool or equivalent",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not typing this as bool?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was mostly just a little confused here with the types, as other fields also seem to be flags but are typed as objects (eg. bold & italic). Seems that "multiline" is also possibly a bool but it is typed as (bool, type(None)) for some reason.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gmischler just a ping for you on this subject, as you have more experience than me with the templating system 🙂

Copy link
Collaborator

@gmischler gmischler Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like I added this in #244.
The motivation was to allow other values with a True/False meaning, similar to what most library functions in Python allow. The documentation also specifies "enabled with True or equivalent value".
If there is a better/more explicit way to specify this, I haven't found it yet...

The other reason was that bold/italic/underline can also be specified in in a csv file, which uses ints instead of bools (more precisely: strings representing ints). This reason does not apply to keep_aspect_ratio, but the first one still does.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright!
So what do you think is the best there? int? (bool, int)?

IMHO we could stick to bool even if non-boolean "truthy" values are also valid, this would make the intent of this parameter clearer.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO we could stick to bool even if non-boolean "truthy" values are also valid, this would make the intent of this parameter clearer.

If we use bool, then we need an additional conversion (first from str to int, then to bool) for CSV files.

You'd expect there to be a standard way of expressing "bool or True/False equivalent", wouldn't you?
A quick search has not turned up anything useful for me...

Maybe just using bool is really the simplest way, although that is technically a backwards incompatible change.

}

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),
("keep_aspect_ratio", int, False),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
("keep_aspect_ratio", int, False),
("keep_aspect_ratio", bool, False),

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, the converter is int here in order to maintain similarity to the other boolean flags. Would need to test if this behaviour works (though I cannot see why it shouldn't).

Copy link
Collaborator

@gmischler gmischler Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing bold/italic/underline to using bool would require additional conversion magic when reading CSVs. Since we're evaluating strings there, currently int("0") evaluates correctly as False, while bool("0") would incorrectly evaluate to True.
It might be helpful to add some comments to the code explaining this...

Of couse, since keep_aspect_ratio is not supported via CSV, that one can use bool without a problem.

Copy link
Member

@Lucas-C Lucas-C Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed!

)
self.elements = []
if encoding is None:
Expand Down Expand Up @@ -423,9 +425,19 @@ def _ellipse(
pdf.set_line_width(size * scale)
pdf.ellipse(x1, y1, x2 - x1, y2 - y1, style=style)

def _image(self, *_, x1=0, y1=0, x2=0, y2=0, text="", **__):
def _image(
self, *_, x1=0, y1=0, x2=0, y2=0, text="", keep_aspect_ratio=False, **__
):
if text:
self.pdf.image(text, x1, y1, w=x2 - x1, h=y2 - y1, link="")
self.pdf.image(
text,
x1,
y1,
w=x2 - x1,
h=y2 - y1,
link="",
keep_aspect_ratio=keep_aspect_ratio,
)

def _barcode(
self,
Expand Down
Binary file not shown.
2 changes: 2 additions & 0 deletions test/template/keep_aspect_ratio.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
img;I;0;0;50;25;;;;;;;;image;;;;;1
box;B;0;0;50;25;;;;;;;;box;;;;;
42 changes: 42 additions & 0 deletions test/template/test_flextemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,45 @@ 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_keep_aspect_ratio(tmp_path):
"""
Tries to render a square image inside a rectangle with keep_aspect_ratio=True.
The image should be fit the rectangle but should remain a square.
"""

tmpl = [
{
"name": "box",
"type": "B",
"x1": 0,
"y1": 0,
"x2": 50,
"y2": 25,
},
{
"name": "img",
"type": "I",
"x1": 0,
"y1": 0,
"x2": 50,
"y2": 25,
"keep_aspect_ratio": True,
},
]

img = qrcode.make("Test keep_aspect_ratio").get_image()

pdf = FPDF()
pdf.add_page()
templ1 = FlexTemplate(pdf, tmpl)
templ1["img"] = img
templ1.render(offsetx=20, offsety=20)

templ2 = FlexTemplate(pdf)
templ2.parse_csv(HERE / "keep_aspect_ratio.csv", delimiter=";")
templ2["img"] = img
templ2.render(offsetx=20, offsety=50)

assert_pdf_equal(pdf, HERE / "flextemplate_keep_aspect_ratio.pdf", tmp_path)
Loading