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

Refactor template.py to create FlexTemplate() #228

Merged
merged 30 commits into from
Oct 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
71bd53d
Fix parsing of csv template files
gmischler Sep 18, 2021
d70bc37
fixes suggested by static code check
gmischler Sep 18, 2021
6ed9686
Update template.py
gmischler Sep 19, 2021
fa62a8d
now it's dark.
gmischler Sep 20, 2021
f1d7802
do some hardcoded template tests without multiline
gmischler Sep 21, 2021
ec69b8f
first round Splitting Template() into FlexTemplate()
gmischler Sep 25, 2021
6771592
offset and rotate for render(), first test
gmischler Sep 25, 2021
536e819
small fixes and cleanup
gmischler Sep 25, 2021
42e0d27
removing mistaken checkin
gmischler Sep 25, 2021
5b1d889
test for multipage Template(); Template.code39 with standard template…
gmischler Sep 26, 2021
92c9e28
refer defaults to type handlers, x2 optional for barcodes
gmischler Sep 26, 2021
0195db4
more template and flextemplate tests
gmischler Sep 26, 2021
bb97a63
Merge remote-tracking branch 'upstream/master'
gmischler Sep 26, 2021
e5ab09c
static check fixes
gmischler Sep 26, 2021
fdb03de
more pylint
gmischler Sep 26, 2021
56f639e
blackity-black
gmischler Sep 26, 2021
bef03d1
even blacker
gmischler Sep 26, 2021
cad0264
Expand docstrings, update help, hide private methods.
gmischler Sep 29, 2021
0f99984
Issues from PR review
gmischler Sep 29, 2021
1af4365
Merge remote-tracking branch 'upstream/master'
gmischler Sep 29, 2021
bba1ca1
Issue #226 solved: Rotate anything anywhere
gmischler Sep 30, 2021
b89147a
Issue #238 solved - split_multicell doesn't modify target document
gmischler Sep 30, 2021
13e9739
Documentation details and corrections
gmischler Sep 30, 2021
058f7ea
breaking up long line
gmischler Sep 30, 2021
5807548
rotation fix slightly changed barcode output
gmischler Sep 30, 2021
557148a
Update CHANGELOG.md
gmischler Sep 30, 2021
ddba2ce
Include _write() in template rotation test
gmischler Sep 30, 2021
4847792
FlexTemplate.render() with scaling
gmischler Oct 1, 2021
6f2c98f
empty text field - consistency between T and W
gmischler Oct 2, 2021
2b2d82e
Enforce user input types as early as possible
gmischler Oct 2, 2021
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ nosetests.xml

# Idea
.idea

# Vim backup and swap files
*.*~
*.swp
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/).

## [2.4.4] - not released yet
### Added
- `Template()` has gained a more flexible cousin `FlexTemplate()`, thanks to @gmischler
- markdown support in `multi_cell()`, thanks to Yeshi Namkhai
- base 64 images can now be provided to `FPDF.image`, thanks to @MWhatsUp
- documentation on how to generate datamatrix barcodes using the `pystrich` lib: [documentation section](https://pyfpdf.github.io/fpdf2/Barcodes.html#datamatrix),
thanks to @MWhatsUp
- `write_html`: headings (`<h1>`, `<h2>`...) relative sizes can now be configured through an optional `heading_sizes` parameter
### Fixed
- `Template`: `split_multicell()` will not write spurious font data to the target document anymore, thanks to @gmischler
- `Template`: rotation now should work correctly in all situations, thanks to @gmischler
- `write_html`: headings (`<h1>`, `<h2>`...) can now contain non-ASCII characters without triggering a `UnicodeEncodeError`
- `Template`: CSV column types are now safely parsed, thanks to @gmischler
### Changed
- `Template`: Incompatible change: the Code39 barcode type has changed the input field names, making it possible to use it in CSV files.
- `write_html`: the line height of headings (`<h1>`, `<h2>`...) is now properly scaled with its font size
- some `FPDF` methods should not be used inside a `rotation` context, or things can get broken.
This is now forbidden: an exception is now raised in those cases.
Expand Down
240 changes: 212 additions & 28 deletions docs/Templates.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,224 @@
# Introduction #

Templates are predefined documents (like invoices, tax forms, etc.), where each element (text, lines, barcodes, etc.) has a fixed position (x1, y1, x2, y2), style (font, size, etc.) and a default text.
Templates are predefined documents (like invoices, tax forms, etc.), or parts of such documents, where each element (text, lines, barcodes, etc.) has a fixed position (x1, y1, x2, y2), style (font, size, etc.) and a default text.

This elements can act as placeholders, so the program can change the default text "filling" the document.
These elements can act as placeholders, so the program can change the default text "filling in" the document.

Also, the elements can be defined in a CSV file or in a database, so the user can easily adapt the form to his printing needs.
Besides being defined in code, the elements can also be defined in a CSV file or in a database, so the user can easily adapt the form to his printing needs.
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved

A template is used like a dict, setting its items' values.


# How to use Templates #

There are two approaches to using templates.


## Using Template() ##

The traditional approach is to use the Template() class, This class accepts one template definition, and can apply it to each page of a document. The usage pattern here is:

```python
tmpl = Template(elements=elements)
# first page and content
tmpl.add_page()
tmpl[item_key_01] = "Text 01"
tmpl[item_key_02] = "Text 02"
...

# second page and content
tmpl.add_page()
tmpl[item_key_01] = "Text 11"
tmpl[item_key_02] = "Text 12"
...

# possibly more pages
...

# finalize document and write to file
tmpl.render(outfile="example.pdf")
```

The Template() class will create and manage its own FPDF() instance, so you don't need to worry about how it all works together. It also allows to set the page format, title of the document, measuring unit, and other metadata for the PDF file.

For the method signatures, see [pyfpdf.github.io: class Template](https://pyfpdf.github.io/fpdf2/fpdf/template.html#fpdf.template.Template).

Setting text values for specific template items is done by treating the class as a dict, with the name of the item as the key:

```python
Template["company_name"] = "Sample Company"
```


## Using FlexTemplate() ##

When more flexibility is desired, then the FlexTemplate() class comes into play.
In this case, you first need to create your own FPDF() instance. You can then pass this to the constructor of one or several FlexTemplate() instances, and have each of them load a template definition. For any page of the document, you can set text values on a template, and then render it on that page. After rendering, the template will be reset to its default values.

```python
pdf = FPDF()
pdf.add_page()
# One template for the first page
fp_tmpl = FlexTemplate(pdf, elements=fp_elements)
fp_tmpl["item_key_01"] = "Text 01"
fp_tmpl["item_key_02"] = "Text 02"
...
fp_tmpl.render() # add template items to first page

# add some more non-template content to the first page
pdf.polyline(point_list, fill=False, polygon=False)

# second page
pdf.add_page()
# header for the second page
h_tmpl = FlexTemplate(pdf, elements=h_elements)
h_tmpl["item_key_HA"] = "Text 2A"
h_tmpl["item_key_HB"] = "Text 2B"
...
h_tmpl.render() # add header items to second page

# footer for the second page
f_tmpl = FlexTemplate(pdf, elements=f_elements)
f_tmpl["item_key_FC"] = "Text 2C"
f_tmpl["item_key_FD"] = "Text 2D"
...
f_tmpl.render() # add footer items to second page

# other content on the second page
pdf.dashed_line(x1, y1, x2, y2, dash_length=1, space_length=1):

# third page
pdf.add_page()
# header for the third page, just reuse the same template instance after render()
h_tmpl["item_key_HA"] = "Text 3A"
h_tmpl["item_key_HB"] = "Text 3B"
...
h_tmpl.render() # add header items to third page

# footer for the third page
f_tmpl["item_key_FC"] = "Text 3C"
f_tmpl["item_key_FD"] = "Text 3D"
...
f_tmpl.render() # add footer items to third page

# other content on the third page
pdf.rect(x, y, w, h, style=None)

# possibly more pages
pdf.next_page()
...
...

# finally write everything to a file
pdf.output("example.pdf")
```

Evidently, this can end up quite a bit more involved, but there are hardly any limits on how you can combine templated and non-templated content on each page. Just think of the different templates as of building blocks, like configurable rubber stamps, which you can apply in any combination on any page you like.
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved

Of course, you can just as well use a set of full-page templates, possibly differentiating between cover page, table of contents, normal content pages, and an index page, or something along those lines.

And here's how you can use a template several times on one page (and by extension, several times on several pages). When rendering with an `offsetx` and/or `offsety` argument, the contents of the template will end up in a different place on the page. A `rotate` argument will change its orientation, rotated around the origin of the template. The pivot of the rotation is the offset location. And finally, a `scale` argument allows you to insert the template larger or smaller than it was defined.

```python
elements = [
{"name":"box", "type":"B", "x1":0, "y1":0, "x2":50, "y2":50,},
{"name":"d1", "type":"L", "x1":0, "y1":0, "x2":50, "y2":50,},
{"name":"d2", "type":"L", "x1":0, "y1":50, "x2":50, "y2":0,},
{"name":"label", "type":"T", "x1":0, "y1":52, "x2":50, "y2":57, "text":"Label",},
]
pdf = FPDF()
pdf.add_page()
templ = FlexTemplate(pdf, elements)
templ["label"] = "Offset: 50 / 50 mm"
templ.render(offsetx=50, offsety=50)
templ["label"] = "Offset: 50 / 120 mm"
templ.render(offsetx=50, offsety=120)
templ["label"] = "Offset: 120 / 50 mm, Scale: 0.5"
templ.render(offsetx=120, offsety=50, scale=0.5)
templ["label"] = "Offset: 120 / 120 mm, Rotate: 30°, Scale=0.5"
templ.render(offsetx=120, offsety=120, rotate=30.0, scale=0.5)
pdf.output("example.pdf")
```

For the method signatures, see [pyfpdf.github.io: class FlexTemplate](https://pyfpdf.github.io/fpdf2/fpdf/template.html#fpdf.template.FlexTemplate).

Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
The dict syntax for setting text values is the same as above:

```python
FlexTemplate["company_name"] = "Sample Company"
```


# Details - Template definition #

A template is composed of a header and a list of elements.
A template definition consists of a number of elements, which have the following properties (columns in a CSV, items in a dict, fields in a database).
Dimensions (except font size, which always uses points) are given in user defined units (default: mm). Those are the units that can be specified when creating a `Template()` or a `FPDF()` instance.

The header contains the page format, title of the document and other metadata.
* __name__: placeholder identification (unique text string)
* _mandatory_
* __type__:
* '__T__': Text - places one or several lines of text on the page
* '__L__': Line - draws a line from x1/y1 to x2/y2
* '__I__': Image - positions and scales an image into the bounding box
* '__B__': Box - draws a rectangle around the bounding box
* '__BC__': Barcode - inserts an "Interleaved 2 of 5" type barcode
* '__C39__': Code 39 - inserts a "Code 39" type barcode
* Incompatible change: The first implementation of this type used the non-standard template keys "x", "y", "w", and "h", which are no longer valid.
* '__W__': "Write" - uses the `FPDF.write()` method to add text to the page
* _mandatory_
* __x1, y1, x2, y2__: top-left, bottom-right coordinates, defining a bounding box in most cases
* for multiline text, this is the bounding box for just the first line, not the complete box
* for the barcodes types, the height of the barcode is `y2 - y1`, x2 is ignored.
* _mandatory_ ("x2" _optional_ for the barcode types)
* __font__: the name of a font type for the text types
* _optional_
* default: "helvetica"
* __size__: the size property of the element (float value)
* for text, the font size (in points!)
* for line and rect, the line width
* for the barcode types, the width of one bar
* _optional_
* default: 10 for text, 2 for 'BC', 1.5 for 'C39'
* __bold, italic, underline__: text style properties
* in elements dict, enabled with True or equivalent value
* in csv, only int values, 0 as false, non-0 as true
* _optional_
* default: false
* __foreground, background__: text and fill colors (int value, commonly given in hex as 0xRRGGBB)
* _optional_
* default: 0x000000/0xFFFFFF
* __align__: text alignment, '__L__': left, '__R__': right, '__C__': center
* _optional_
* default: 'L'
* __text__: default string, can be replaced at runtime
* displayed text for 'T' and 'W'
* data to encode for barcode types
* _optional_ (if missing for text/write, the element is ignored)
* default: empty
* __priority__: Z-order (int value)
* _optional_
* default: 0
* __multiline__: configure text wrapping
* in dicts, None for single line, True for multicells (multiple lines), False trims to exactly fit the space defined
* in csv, 0 for single line, >0 for multiple lines, <0 for exact fit
* _optional_
* default: single line
* __rotation__: rotate the element in degrees around the top left corner x1/y1 (float)
* _optional_
* default: 0.0 - no rotation

Elements have the following properties (columns in a CSV, fields in a database):
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).

* name: placeholder identification
* type: 'T': texts, 'L': lines, 'I': images, 'B': boxes, 'BC': barcodes
* x1, y1, x2, y2: top-left, bottom-right coordinates (in mm)
* font: e.g. "helvetica"
* size: text size in points, e.g. 10
* bold, italic, underline: text style (non-empty to enable)
* foreground, background: text and fill colors, e.g. 0xFFFFFF
* align: text alignment, 'L': left, 'R': right, 'C': center
* text: default string, can be replaced at runtime
* priority: Z-order
* multiline: None for single line (default), True to for multicells (multiple lines), False trims to exactly fit the space defined

# How to create a template #

A template can be created in 3 ways:

* By defining everything manually in a hardcoded way
* By defining everything manually in a hardcoded way as a Python dictionary
* By using a template definition in a CSV document and parsing the CSV with Template.parse\_dict()
* By defining the template in a database (this applies to [Web2Py] (Web2Py.md) integration)


Note the following, the definition of a template will contain the elements. The header will be given during instantiation (except for the database method).

# Example - Hardcoded #

```python
Expand All @@ -56,7 +236,7 @@ elements = [
{ 'name': 'barcode', 'type': 'BC', 'x1': 20.0, 'y1': 246.5, 'x2': 140.0, 'y2': 254.0, 'font': 'Interleaved 2of5 NT', 'size': 0.75, 'bold': 0, 'italic': 0, 'underline': 0, 'foreground': 0, 'background': 0, 'align': 'I', 'text': '200000000001000159053338016581200810081', 'priority': 3, 'multiline': 0},
]

#here we instantiate the template and define the HEADER
#here we instantiate the template
f = Template(format="A4", elements=elements,
title="Sample Invoice")
f.add_page()
Expand All @@ -72,20 +252,24 @@ f.render("./template.pdf")

See template.py or [Web2Py] (Web2Py.md) for a complete example.


# Example - Elements defined in CSV file #
You define your elements in a CSV file "mycsvfile.csv"
that will look like:
```
line0;L;20.0;12.0;190.0;12.0;times;0.5;0;0;0;0;16777215;C;;0;0
line1;L;20.0;36.0;190.0;36.0;times;0.5;0;0;0;0;16777215;C;;0;0
name0;T;21.0;14.0;104.0;25.0;times;16.0;0;0;0;0;16777215;L;name;2;0
title0;T;21.0;26.0;104.0;30.0;times;10.0;0;0;0;0;16777215;L;title;2;0
multiline;T;21.0;50.0;28.0;54.0;times;10.5;0;0;0;0;16777215;L;multi line;0;1
numeric_text;T;21.0;80.0;100.0;84.0;times;10.5;0;0;0;0;16777215;R;007;0;0
line0;L;20.0;12.0;190.0;12.0;times;0.5;0;0;0;0;16777215;C;;0;0;0.0
line1;L;20.0;36.0;190.0;36.0;times;0.5;0;0;0;0;16777215;C;;0;0;0.0
name0;T;21.0;14.0;104.0;25.0;times;16.0;0;0;0;0;16777215;L;name;2;0;0.0
title0;T;21.0;26.0;104.0;30.0;times;10.0;0;0;0;0;16777215;L;title;2;0;0.0
multiline;T;21.0;50.0;28.0;54.0;times;10.5;0;0;0;0;0xffff00;L;multi line;0;1;0.0
numeric_text;T;21.0;80.0;100.0;84.0;times;10.5;0;0;0;0;;R;007;0;0;0.0
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.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')
('name','type','x1','y1','x2','y2','font','size','bold','italic','underline','foreground','background','align','text','priority', 'multiline', 'rotate')
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
7 changes: 4 additions & 3 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3276,8 +3276,8 @@ def interleaved2of5(self, txt, x, y, w=1, h=10):
"A": "nn",
"Z": "wn",
}

self.set_fill_color(0)
# The caller should do this, or we can't rotate the thing.
# self.set_fill_color(0)
code = txt
# add leading zero if code-length is odd
if len(code) % 2 != 0:
Expand Down Expand Up @@ -3366,7 +3366,8 @@ def code39(self, txt, x, y, w=1.5, h=5):
"+": "nwnnnwnwn",
"%": "nnnwnwnwn",
}
self.set_fill_color(0)
# The caller should do this, or we can't rotate the thing.
# self.set_fill_color(0)
for c in txt.upper():
if c not in chars:
raise RuntimeError(f'Invalid char "{c}" for Code39')
Expand Down
Loading