From 71bd53d7d1b0e3a66e4866d797267834779f32ca Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 18 Sep 2021 16:33:43 +0200 Subject: [PATCH 01/28] Fix parsing of csv template files *Values of csv files are converted by position, instead of content * Updated tests to check for regression * Updated documentation and tests to include multiline text. --- docs/Templates.md | 25 +++++---- fpdf/template.py | 51 +++++++++++-------- test/template/mycsvfile.csv | 10 ++-- test/template/template_nominal_csv.pdf | Bin 1170 -> 1271 bytes test/template/template_nominal_hardcoded.pdf | Bin 22367 -> 22662 bytes test/template/test_template.py | 28 +++++++++- 6 files changed, 77 insertions(+), 37 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index f02500516..f8145553f 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -47,12 +47,13 @@ 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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': 'logo', 'priority': 2, }, - { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': '', 'priority': 2, }, - { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': None, 'priority': 0, }, - { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': None, 'priority': 2, }, - { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': None, 'priority': 3, }, - { '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, }, + { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': 'logo', 'priority': 2, 'multiline': 0}, + { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': '', 'priority': 2, 'multiline': 0}, + { 'name': 'multline_text', 'type': 'T', 'x1': 20, 'y1': 100, 'x2': 40, 'y2': 105, 'font': 'helvetica', 'size': 12, 'bold': 0, 'italic': 0, 'underline': 0, 'foreground': 0, 'background': 0x88ff00, 'align': 'I', 'text': 'Lorem ipsum dolor sit amet, consectetur adipisici elit', 'priority': 2, 'multiline': 1} + { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': None, 'priority': 0, 'multiline': 0}, + { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': None, 'priority': 2, 'multiline': 0}, + { '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, 'foreground': 0, 'background': 0, 'align': 'I', 'text': None, 'priority': 3, 'multiline': 0}, + { '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 @@ -75,10 +76,12 @@ See template.py or [Web2Py] (Web2Py.md) for a complete example. You define your elements in a CSV file "mycsvfile.csv" that will look like: ``` -line0;T;20.0;13.0;190.0;13.0;times;10.0;0;0;0;0;65535;C;;0 -line1;T;20.0;67.0;190.0;67.0;times;10.0;0;0;0;0;65535;C;;0 -name0;T;21;14;104;25;times;16.0;0;0;0;0;0;C;;2 -title0;T;64;26;104;30;times;10.0;0;0;0;0;0;C;;2 +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 ``` Remember that each line represents an element and each field represents one of the properties of the element in the following order: @@ -92,7 +95,7 @@ def test_template(): title="Sample Invoice") f.parse_csv("mycsvfile.csv", delimiter=";") f.add_page() - f["company_name"] = "Sample Company" + f["name0"] = "Joe Doe" return f.render("./template.pdf") ``` diff --git a/fpdf/template.py b/fpdf/template.py index b46b45e3d..b39341282 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -74,26 +74,37 @@ def load_elements(self, elements): self.elements = elements self.keys = [v["name"].lower() for v in self.elements] + def _parse_colorcode(self, s): + """ Allow hex and oct values for colors """ + s = s.strip() + if not s: + raise ValueError('Foreground and Background must be numeric') + if s[:2] in ['0x', '0X']: + return int(s, 16) + elif s[0] == '0': + return int(s, 8) + return int(s) + def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): """Parse template format csv file and create elements dict""" - keys = ( - "name", - "type", - "x1", - "y1", - "x2", - "y2", - "font", - "size", - "bold", - "italic", - "underline", - "foreground", - "background", - "align", - "text", - "priority", - "multiline", + handlers = ( + ("name", str.strip), + ("type", str.strip), + ("x1", float), + ("y1", float), + ("x2", float), + ("y2", float), + ("font", str.strip), + ("size", float), + ("bold", int), + ("italic", int), + ("underline", int), + ("foreground", self._parse_colorcode), + ("background", self._parse_colorcode), + ("align", str.strip), + ("text", str.strip), + ("priority", int), + ("multiline", int), ) self.elements = [] self.pg_no = 0 @@ -105,9 +116,7 @@ def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): for i, v in enumerate(row): if not v.startswith("'") and decimal_sep != ".": v = v.replace(decimal_sep, ".") - stripped_value = v.strip() - typed_value = try_to_type(stripped_value) - kargs[keys[i]] = typed_value + kargs[handlers[i][0]] = handlers[i][1](v) self.elements.append(kargs) self.keys = [v["name"].lower() for v in self.elements] diff --git a/test/template/mycsvfile.csv b/test/template/mycsvfile.csv index 27eb161e8..62e0a037d 100644 --- a/test/template/mycsvfile.csv +++ b/test/template/mycsvfile.csv @@ -1,4 +1,6 @@ -line0;L;20.0;12.0;190.0;12.0;times;0.5;0;0;0;0;16777215;C;;0 -line1;L;20.0;36.0;190.0;36.0;times;0.5;0;0;0;0;16777215;C;;0 -name0;T;21;14;104;25;times;16.0;0;0;0;0;16777215;L;name;2 -title0;T;21;26;104;30;times;10.0;0;0;0;0;16777215;L;title;2 +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;0xffff00;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 diff --git a/test/template/template_nominal_csv.pdf b/test/template/template_nominal_csv.pdf index d7937544c4712f67e7f7a7f217d78dae5b156d7e..8eb356c44e7f48f56de890b49519757889b72bfb 100644 GIT binary patch delta 409 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 Bzij{j diff --git a/test/template/test_template.py b/test/template/test_template.py index 48f2aa27c..a3fcbc8eb 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -28,6 +28,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": "logo", "priority": 2, + "multiline": 0, }, { "name": "company_name", @@ -45,6 +46,26 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": "", "priority": 2, + "multiline": 0, + }, + { + "name": "multline_text", + "type": "T", + "x1": 20, + "y1": 100, + "x2": 40, + "y2": 105, + "font": "helvetica", + "size": 12, + "bold": 0, + "italic": 0, + "underline": 0, + "foreground": 0, + "background": 0x88ff00, + "align": "I", + "text": "Lorem ipsum dolor sit amet, consectetur adipisici elit", + "priority": 2, + "multiline": 1, }, { "name": "box", @@ -62,6 +83,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": None, "priority": 0, + "multiline": 0, }, { "name": "box_x", @@ -79,6 +101,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": None, "priority": 2, + "multiline": 0, }, { "name": "line1", @@ -96,6 +119,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": None, "priority": 3, + "multiline": 0, }, { "name": "barcode", @@ -113,6 +137,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": "200000000001000159053338016581200810081", "priority": 3, + "multiline": 0, }, ] tmpl = Template(format="A4", elements=elements, title="Sample Invoice") @@ -124,7 +149,8 @@ def test_template_nominal_hardcoded(tmp_path): def test_template_nominal_csv(tmp_path): - """Taken from docs/Templates.md""" + """Same data as in docs/Templates.md + The numeric_text tests for a regression.""" tmpl = Template(format="A4", title="Sample Invoice") tmpl.parse_csv(HERE / "mycsvfile.csv", delimiter=";") tmpl.add_page() From d70bc37eaa57708f0baa13cf409264f6f85e2316 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 18 Sep 2021 21:38:48 +0200 Subject: [PATCH 02/28] fixes suggested by static code check --- fpdf/template.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index b39341282..03a6871cb 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -10,7 +10,6 @@ from .errors import FPDFException from .fpdf import FPDF -from .util import try_to_type def rgb(col): @@ -74,14 +73,15 @@ def load_elements(self, elements): self.elements = elements self.keys = [v["name"].lower() for v in self.elements] - def _parse_colorcode(self, s): + @staticmethod + def _parse_colorcode(s): """ Allow hex and oct values for colors """ s = s.strip() if not s: raise ValueError('Foreground and Background must be numeric') if s[:2] in ['0x', '0X']: return int(s, 16) - elif s[0] == '0': + if s[0] == '0': return int(s, 8) return int(s) From 6ed96863c09c4bf0b01b325c8996cf0f7575564c Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 19 Sep 2021 18:13:44 +0200 Subject: [PATCH 03/28] Update template.py restrict decimal seperator replacement to float fields --- fpdf/template.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index 03a6871cb..d2e3b2581 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -87,15 +87,19 @@ def _parse_colorcode(s): def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): """Parse template format csv file and create elements dict""" + def varsep_float(s): + """Convert to float with given decimal seperator""" + # glad to have nonlocal scoping... + return float(s.replace(decimal_sep, '.')) handlers = ( ("name", str.strip), ("type", str.strip), - ("x1", float), - ("y1", float), - ("x2", float), - ("y2", float), + ("x1", varsep_float), + ("y1", varsep_float), + ("x2", varsep_float), + ("y2", varsep_float), ("font", str.strip), - ("size", float), + ("size", varsep_float), ("bold", int), ("italic", int), ("underline", int), @@ -114,8 +118,6 @@ def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): for row in csv.reader(f, delimiter=delimiter): kargs = {} for i, v in enumerate(row): - if not v.startswith("'") and decimal_sep != ".": - v = v.replace(decimal_sep, ".") kargs[handlers[i][0]] = handlers[i][1](v) self.elements.append(kargs) self.keys = [v["name"].lower() for v in self.elements] From fa62a8d3de7db47d0408891d3a23e9d8c101dfb4 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Mon, 20 Sep 2021 20:23:01 +0200 Subject: [PATCH 04/28] now it's dark. --- fpdf/template.py | 12 +++++++----- test/template/test_template.py | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index d2e3b2581..f3f44cc31 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -75,22 +75,24 @@ def load_elements(self, elements): @staticmethod def _parse_colorcode(s): - """ Allow hex and oct values for colors """ + """Allow hex and oct values for colors""" s = s.strip() if not s: - raise ValueError('Foreground and Background must be numeric') - if s[:2] in ['0x', '0X']: + raise ValueError("Foreground and Background must be numeric") + if s[:2] in ["0x", "0X"]: return int(s, 16) - if s[0] == '0': + if s[0] == "0": return int(s, 8) return int(s) def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): """Parse template format csv file and create elements dict""" + def varsep_float(s): """Convert to float with given decimal seperator""" # glad to have nonlocal scoping... - return float(s.replace(decimal_sep, '.')) + return float(s.replace(decimal_sep, ".")) + handlers = ( ("name", str.strip), ("type", str.strip), diff --git a/test/template/test_template.py b/test/template/test_template.py index a3fcbc8eb..e043d100c 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -61,7 +61,7 @@ def test_template_nominal_hardcoded(tmp_path): "italic": 0, "underline": 0, "foreground": 0, - "background": 0x88ff00, + "background": 0x88FF00, "align": "I", "text": "Lorem ipsum dolor sit amet, consectetur adipisici elit", "priority": 2, @@ -150,7 +150,7 @@ def test_template_nominal_hardcoded(tmp_path): def test_template_nominal_csv(tmp_path): """Same data as in docs/Templates.md - The numeric_text tests for a regression.""" + The numeric_text tests for a regression.""" tmpl = Template(format="A4", title="Sample Invoice") tmpl.parse_csv(HERE / "mycsvfile.csv", delimiter=";") tmpl.add_page() From f1d7802e05f513008b9d5e82d6a58699c0141df6 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Tue, 21 Sep 2021 22:06:17 +0200 Subject: [PATCH 05/28] do some hardcoded template tests without multiline --- test/template/test_template.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/template/test_template.py b/test/template/test_template.py index e043d100c..dc819e065 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -28,7 +28,6 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": "logo", "priority": 2, - "multiline": 0, }, { "name": "company_name", @@ -46,7 +45,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": "", "priority": 2, - "multiline": 0, + # multiline is optional, so we test some items without it. }, { "name": "multline_text", From ec69b8fc88868f0e8b42c68a0ce46f71cd825e3f Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 25 Sep 2021 08:56:59 +0200 Subject: [PATCH 06/28] first round Splitting Template() into FlexTemplate() --- docs/Templates.md | 172 +++++++++++++++++--- fpdf/template.py | 209 +++++++++++++------------ test/template/mycsvfile.csv | 6 +- test/template/template_nominal_csv.pdf | Bin 1271 -> 1475 bytes test/template/test_template.py | 2 + 5 files changed, 270 insertions(+), 119 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index f8145553f..0290cb6d6 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -8,6 +8,106 @@ Also, the elements can be defined in a CSV file or in a database, so the user ca 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 useage 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 anything else. + + +## 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. + +```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") +``` + +As you see, this can be 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. + +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. + + # Details - Template definition # A template is composed of a header and a list of elements. @@ -16,23 +116,54 @@ The header contains the page format, title of the document and other metadata. Elements have the following properties (columns in a CSV, fields in a database): - * 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 + * __name__: placeholder identification + * _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 + * '__W__': "Write" - uses the FPDF.write() method to add text to the page + * _mandatory_ + * __x1, y1, x2, y2__: top-left, bottom-right coordinates (in mm), defining a bounding box in most cases + * for multiline text, this is the bounding box for just the first line, not the complete box + * _mandatory_ + * __font__: e.g. "helvetica" + * _optional_, default: "helvetica" + * ignored for non-text elements + * __size__: text size in points (int value) + * _optional_, default: 10 + * ignored for non-text elements + * __bold, italic, underline__: text style, enabled with True or equivalent value + * in csv, only int values, 0 as false, non-0 as true + * _optional_, default: false + * ignored for non-text elements + * __foreground, background__: text and fill colors, e.g. 0xFFFFFF + * _optional_, default: 0x000000/0xFFFFFF + * __align__: text alignment, '__L__': left, '__R__': right, '__C__': center + * _optional_, default: 'L' + * ignored for non-text elements + * __text__: default string, can be replaced at runtime + * _optional_, default: empty + * ignored for purely graphical element types (lines, boxes, and images) + * __priority__: Z-order (int value) + * _optional_, default: 0 + * __multiline__: configure text wrapping + * in dicts, None for single line, True to 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 + * ignored for non-text elements + * __rotation__: rotate the element in degrees around the top left corner x1/y1 (float) + * _optional_, default: 0.0 - no rotation + # 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) @@ -76,16 +207,19 @@ See template.py or [Web2Py] (Web2Py.md) for a complete example. 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. Then you can use the file like this: diff --git a/fpdf/template.py b/fpdf/template.py index f3f44cc31..acb4afc89 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -23,33 +23,11 @@ def rgb_as_str(col): return f"{r / 255:.3f} {g / 255:.3f} {b / 255:.3f} rg" -class Template: - # Disabling this check due to the "format" parameter below: - # pylint: disable=redefined-builtin - def __init__( - self, - infile=None, - elements=None, - format="A4", - orientation="portrait", - unit="mm", - title="", - author="", - subject="", - creator="", - keywords="", - ): - """ - Args: - infile (str): [**DEPRECATED**] unused, will be removed in a later version - """ - if infile: - warnings.warn( - '"infile" is unused and will soon be deprecated', - PendingDeprecationWarning, - ) +class FlexTemplate: + def __init__(self, pdf, elements=None): if elements: self.load_elements(elements) + self.pdf = pdf self.handlers = { "T": self.text, "L": self.line, @@ -60,80 +38,85 @@ def __init__( "W": self.write, } self.texts = {} - pdf = self.pdf = FPDF(format=format, orientation=orientation, unit=unit) - pdf.set_title(title) - pdf.set_author(author) - pdf.set_creator(creator) - pdf.set_subject(subject) - pdf.set_keywords(keywords) def load_elements(self, elements): """Initialize the internal element structures""" - self.pg_no = 0 self.elements = elements self.keys = [v["name"].lower() for v in self.elements] @staticmethod def _parse_colorcode(s): """Allow hex and oct values for colors""" - s = s.strip() - if not s: - raise ValueError("Foreground and Background must be numeric") if s[:2] in ["0x", "0X"]: return int(s, 16) - if s[0] == "0": + if s[:2] in ["0o", "0O"]: return int(s, 8) return int(s) + @staticmethod + def _parse_multiline(s): + i = int(s) + if i > 0: + return True + if i < 0: + return False + def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): """Parse template format csv file and create elements dict""" - def varsep_float(s): + def varsep_float(s, default="0"): """Convert to float with given decimal seperator""" # glad to have nonlocal scoping... - return float(s.replace(decimal_sep, ".")) + return float((s.strip() or default).replace(decimal_sep, ".")) handlers = ( - ("name", str.strip), - ("type", str.strip), + ("name", str), + ("type", str), ("x1", varsep_float), ("y1", varsep_float), ("x2", varsep_float), ("y2", varsep_float), - ("font", str.strip), - ("size", varsep_float), - ("bold", int), - ("italic", int), - ("underline", int), - ("foreground", self._parse_colorcode), - ("background", self._parse_colorcode), - ("align", str.strip), - ("text", str.strip), - ("priority", int), - ("multiline", int), + ("font", str, "helvetica"), + ("size", varsep_float, 10.0), + ("bold", int, 0), + ("italic", int, 0), + ("underline", int, 0), + ("foreground", self._parse_colorcode, 0x0), + ("background", self._parse_colorcode, 0xFFFFFF), + ("align", str, "L"), + ("text", str, ""), + ("priority", int, 0), + ("multiline", self._parse_multiline, None), + ("rotate", varsep_float, 0.0), ) self.elements = [] - self.pg_no = 0 if encoding is None: encoding = locale.getpreferredencoding() + hlen = len(handlers) with open(infile, encoding=encoding) as f: for row in csv.reader(f, delimiter=delimiter): + rlen = len(row) + # fill in any missing items + row[rlen + 1 :] = [""] * (hlen - rlen) kargs = {} for i, v in enumerate(row): - kargs[handlers[i][0]] = handlers[i][1](v) + handler = handlers[i] + vs = v.strip() + if not vs: + if len(handler) < 3: + raise FPDFException( + "Mandatory value '%s' missing in csv data" % handler[0] + ) + kargs[handler[0]] = handler[2] # default + else: + kargs[handler[0]] = handler[1](v) self.elements.append(kargs) self.keys = [v["name"].lower() for v in self.elements] - def add_page(self): - self.pg_no += 1 - self.texts[self.pg_no] = {} - def __setitem__(self, name, value): if name.lower() not in self.keys: raise FPDFException(f"Element not loaded, cannot set item: {name}") - if not self.pg_no: - raise FPDFException("No page open, you need to call add_page() first") - self.texts[self.pg_no][name.lower()] = value + self.texts[name.lower()] = value # setitem shortcut (may be further extended) set = __setitem__ @@ -142,14 +125,12 @@ def __contains__(self, name): return name.lower() in self.keys def __getitem__(self, name): - if not self.pg_no: - raise FPDFException("No page open, you need to call add_page() first") if name not in self.keys: return None key = name.lower() - if key in self.texts[self.pg_no]: + if key in self.texts: # text for this page: - return self.texts[self.pg_no][key] + return self.texts[key] # find first element for default text: return next( (x["text"] for x in self.elements if x["name"].lower() == key), None @@ -178,40 +159,6 @@ def split_multicell(self, text, element_name): split_only=True, ) - def render(self, outfile=None, dest=None): - """ - Args: - outfile (str): optional output PDF file path. If ommited, the - `.pdf.output(...)` method can be manuallyy called afterwise. - dest (str): [**DEPRECATED**] unused, will be removed in a later version - """ - if dest: - warnings.warn( - '"dest" is unused and will soon be deprecated', - PendingDeprecationWarning, - ) - pdf = self.pdf - for pg in range(1, self.pg_no + 1): - pdf.add_page() - pdf.set_font("helvetica", "B", 16) - pdf.set_auto_page_break(False, margin=0) - - sorted_elements = sorted(self.elements, key=lambda x: x["priority"]) - - for element in sorted_elements: - element = element.copy() - element["text"] = self.texts[pg].get( - element["name"].lower(), element["text"] - ) - handler_name = element["type"].upper() - if "rotate" in element: - with pdf.rotation(element["rotate"], element["x1"], element["y1"]): - self.handlers[handler_name](pdf, **element) - else: - self.handlers[handler_name](pdf, **element) - if outfile: - pdf.output(outfile) - @staticmethod def text( pdf, @@ -366,3 +313,69 @@ def write( pdf.set_font(font, style, size) pdf.set_xy(x1, y1) pdf.write(5, text, link) + + def render(self): + sorted_elements = sorted(self.elements, key=lambda x: x["priority"]) + for element in sorted_elements: + element = element.copy() + element["text"] = self.texts.get(element["name"].lower(), element["text"]) + handler_name = element["type"].upper() + # if 'rotate' in element: + if element.get("rotate"): # don't rotate by 0.0 degrees + with self.pdf.rotation(element["rotate"], element["x1"], element["y1"]): + self.handlers[handler_name](self.pdf, **element) + else: + self.handlers[handler_name](self.pdf, **element) + self.texts = {} # reset modified entries for the next page + + +class Template(FlexTemplate): + # Disabling this check due to the "format" parameter below: + # pylint: disable=redefined-builtin + def __init__( + self, + infile=None, + elements=None, + format="A4", + orientation="portrait", + unit="mm", + title="", + author="", + subject="", + creator="", + keywords="", + ): + """ + Args: + infile (str): [**DEPRECATED**] unused, will be removed in a later version + """ + pdf = FPDF(format=format, orientation=orientation, unit=unit) + pdf.set_title(title) + pdf.set_author(author) + pdf.set_creator(creator) + pdf.set_subject(subject) + pdf.set_keywords(keywords) + super().__init__(pdf=pdf, elements=elements) + + def add_page(self): + if self.pdf.page: + self.render() + self.pdf.add_page() + + def render(self, outfile=None, dest=None): + """ + Args: + outfile (str): optional output PDF file path. If ommited, the + `.pdf.output(...)` method can be manuallyy called afterwise. + dest (str): [**DEPRECATED**] unused, will be removed in a later version + """ + if dest: + warnings.warn( + '"dest" is unused and will soon be deprecated', + PendingDeprecationWarning, + ) + self.pdf.set_font("helvetica", "B", 16) + self.pdf.set_auto_page_break(False, margin=0) + super().render() + if outfile: + pdf.output(outfile) diff --git a/test/template/mycsvfile.csv b/test/template/mycsvfile.csv index 62e0a037d..0f27eadfc 100644 --- a/test/template/mycsvfile.csv +++ b/test/template/mycsvfile.csv @@ -1,6 +1,8 @@ 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 +title0;T;21.0;26.0;104.0;30.0;times;10.0;0;0;0;0;0xFFFFFF;L;title;2;0 multiline;T;21.0;50.0;28.0;54.0;times;10.5;0;0;0;0;0xffff00;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 +numeric_text;T;21.0;80.0;100.0;84.0;times;10.5;0;0;0;0;;R;007;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 diff --git a/test/template/template_nominal_csv.pdf b/test/template/template_nominal_csv.pdf index 8eb356c44e7f48f56de890b49519757889b72bfb..929dc33e6e78709ab4bebd133c1b2c3b6ade7232 100644 GIT binary patch delta 611 zcmey)d6;`bUA?iXiJcu+aY<2XVlG$3oZd;N{SG_uxPJf3rJpF6eE#rb9Y+xrvlSEM z9VUdi`m%-!JbFJfWzsVKs7;b*Pkh@sFI&1|hQKd|`;%ugt$QaCF@dAY(x+QuVa5LW z23Eqxdlo!dAQHeF@?h;LvPJ^#BWp*Y0+N$LA3*9$JsG%@S0Uy|RDY`u5i`HRsU zE+?4oOiMqLA)Dhh<8XTTo`wi*wzUmUa(7fTcfWcq)b+iiF_K*@YUaNA97`Spx7lG6h3tdsQbKU!+IS5|5suU65E&Pk5W6CYjFND(|%b!qZ~o>Oa2 z{93BTuVY>xA@|6raNp7S+igrtGvi;S?!EQ!KhtXd>oHuZc`4A~2C_DKsnp2iql9`-1c@|R-tG=7Dg85`cW<^Ge$!|u qSSlDO1aawm=B4E;0F4E*!3ij_sHCDOHI2*A(9)bsRn^tsjSBz`zU#37 delta 527 zcmX@i{hf0{UA>XHg`FK&aY<2XVlG$3oZd?Z{SF)Ousx`r|H%GO;;}NR<9n4Dh0I;z z89MV`ZShT#di3?=tZBSj%VJl#ZM0XfUtO_oM$)11()*9z1uD3m$(UY|@tN=W_vKzI zuEhxUu4yb*^1q?Pw*1BAs^;tWOEpcNd|jCrGkw?M=y|Ef>O!B@Ypcko&ytt!V(grG zM8WyDQ;Sn?ZM&-7v1g)1>or-MO%Ip-?wkGgx$dpfv=2s3&-~VfJDqdple{JKYvvT8 zt946L{L)-sl^dVzS+XyvtFwtOsJ!Uh;VGJD1gCo}3gEpGbKz=MQTxU~jqo(T<&2lx zEq@zwyzvk3I&(DYB&#B2)dzrQW+hF;5 zKjW;sbKN$-Wz=V!T*DkT`3|$pWEp0z$xJLRlYcPtP7Y>CG_m9|fCB|HQ&VG8g){}2 zm Date: Sat, 25 Sep 2021 18:09:04 +0200 Subject: [PATCH 07/28] offset and rotate for render(), first test --- docs/Templates.md | 80 +++++++++++++++++++++----- fpdf/template.py | 34 ++++++++--- test/template/flextemplate_offset.pdf | Bin 0 -> 1211 bytes test/template/test_flextemplate.py | 60 +++++++++++++++++++ 4 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 test/template/flextemplate_offset.pdf create mode 100644 test/template/test_flextemplate.py diff --git a/docs/Templates.md b/docs/Templates.md index 0290cb6d6..6d2fdcdb8 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -37,7 +37,37 @@ tmpl[item_key_02] = "Text 12" tmpl.render(outfile="example.pdf") ``` -The Template() class will create and manage its own FPDF() instance, so you don't need to worry about anything else. +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 and other metadata for the PDF file. + +The constructor signature is as follows: + +```python +fpdf.template.Template( + elements=None, + format="A4", + orientation="portrait", + unit="mm", + title="", + author="", + subject="", + creator="", + keywords="", + ) +``` + +Its important methods are: +* Template.load_elements(elements) + * An alternative to supplying the elements dict to the constructor. +* Template.parse_csv(infile, delimiter=",", decimal_sep=".", encoding=None) + * Load a template CSV file instead of supplying a dict. +* Template.add_page() + * Renders the elements to the current page, and proceeds to the next page. +* Template.render(outfile=None) + * Renders the content to the last page, and writes the PDF to a file if its name is given. + +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: + +`tmpl["company_name"] = "Sample Company"` ## Using FlexTemplate() ## @@ -105,20 +135,47 @@ pdf.output("example.pdf") As you see, this can be 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. -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. +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): -# Details - Template definition # +```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" +templ.render(offsetx=120, offsety=50) +templ["label"] = "Offset: 120 / 120 mm" +templ.render(offsetx=120, offsety=120) +pdf.output("example.pdf") +``` + +Since we're handling the properties of the FPDF() instance directly, the constructor signature of this class is much simpler: -A template is composed of a header and a list of elements. +```python +fpdf.template.FlexTemplate(self, pdf, elements=None) +``` + +It supports the same method as Template(), except for `add_page()`, which you will instead execute directly on the FPDF() instance. The dict syntax for setting text values is also supported. -The header contains the page format, title of the document and other metadata. -Elements have the following properties (columns in a CSV, fields in a database): +# Details - Template definition # + +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): * __name__: placeholder identification * _mandatory_ - * type: + * __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 @@ -132,32 +189,27 @@ Elements have the following properties (columns in a CSV, fields in a database): * _mandatory_ * __font__: e.g. "helvetica" * _optional_, default: "helvetica" - * ignored for non-text elements - * __size__: text size in points (int value) + * __size__: text size, or line width for line and rect, in points (float value) * _optional_, default: 10 - * ignored for non-text elements * __bold, italic, underline__: text style, enabled with True or equivalent value * in csv, only int values, 0 as false, non-0 as true * _optional_, default: false - * ignored for non-text elements * __foreground, background__: text and fill colors, e.g. 0xFFFFFF * _optional_, default: 0x000000/0xFFFFFF * __align__: text alignment, '__L__': left, '__R__': right, '__C__': center * _optional_, default: 'L' - * ignored for non-text elements * __text__: default string, can be replaced at runtime * _optional_, default: empty - * ignored for purely graphical element types (lines, boxes, and images) * __priority__: Z-order (int value) * _optional_, default: 0 * __multiline__: configure text wrapping * in dicts, None for single line, True to 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 - * ignored for non-text elements * __rotation__: rotate the element in degrees around the top left corner x1/y1 (float) * _optional_, default: 0.0 - no rotation +Fields that are not relevant to a specific element type will be ignored there. # How to create a template # diff --git a/fpdf/template.py b/fpdf/template.py index acb4afc89..285712301 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -42,7 +42,12 @@ def __init__(self, pdf, elements=None): def load_elements(self, elements): """Initialize the internal element structures""" self.elements = elements - self.keys = [v["name"].lower() for v in self.elements] + self.keys = [] + for e in elements: + if not "priority" in e: + e["priority"] = 0 + self.keys.append(e["name"].lower()) + #self.keys = [v["name"].lower() for v in self.elements] @staticmethod def _parse_colorcode(s): @@ -314,18 +319,29 @@ def write( pdf.set_xy(x1, y1) pdf.write(5, text, link) - def render(self): + def _render_element(self, element): + handler_name = element["type"].upper() + if element.get("rotate"): # don't rotate by 0.0 degrees + with self.pdf.rotation(element["rotate"], element["x1"], element["y1"]): + self.handlers[handler_name](self.pdf, **element) + else: + self.handlers[handler_name](self.pdf, **element) + + def render(self, offsetx=0.0, offsety=0.0, rotate=0.0): sorted_elements = sorted(self.elements, key=lambda x: x["priority"]) for element in sorted_elements: element = element.copy() - element["text"] = self.texts.get(element["name"].lower(), element["text"]) - handler_name = element["type"].upper() - # if 'rotate' in element: - if element.get("rotate"): # don't rotate by 0.0 degrees - with self.pdf.rotation(element["rotate"], element["x1"], element["y1"]): - self.handlers[handler_name](self.pdf, **element) + element["text"] = self.texts.get(element["name"].lower(), element.get("text", "")) + element["x1"] = element["x1"] + offsetx + element["y1"] = element["y1"] + offsety + element["x2"] = element["x2"] + offsetx + element["y2"] = element["y2"] + offsety + if rotate: # don't rotate by 0.0 degrees + with self.pdf.rotation(rotate, offsetx, offsety): + self._render_element(element) else: - self.handlers[handler_name](self.pdf, **element) + self._render_element(element) + self.texts = {} # reset modified entries for the next page diff --git a/test/template/flextemplate_offset.pdf b/test/template/flextemplate_offset.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8870c5e0c5a7c026d90faad3fb3845298dc717ed GIT binary patch literal 1211 zcmY!laB@i4DM>9-(09v8EJ<}qP0mjN8t#*t zmtK;gU~Fy%)Kgqil$w~!RWWC8@M*up1_FD2Yd_-{aOvJR4#yvMT@G55(o@v@ zA+%*jT*5ThqSQ2vtzXw- zMs-EjS`Pum%~F%K`RyHTO&DMS@df6)=cv0P3 zbj*S?+B)NJeUtgxy}HM8=H$DpH|fcU=?hF&yHxUHUj^fhRd>|qsm;66&j*=eX-=N z&fkMKk5}s%&)aw}!RGiAW!veJ=kFE&t%-Y(x3%(y&H8)GCcMdh;`qIedF^zkiJ&-x zMgcVTK(URH7K*v_y)#pa6{0~YB`B6l-#Nb&lrp&VeN$616P@xa6rv4)Ql^%sdPWus z7AA&z7M8JGh%{k}ZmJtF@o?!oB^IZGSPJ?csX1k-C7H>IT>8PKNhRP^2uh1UD_rw{ z=_WHTT|qxQGtV)vI1?s-WIiZKLR<|@wnh2L!Koz*(fYvjt)L&0S^+fQGZ&boouITc zl=cMr1Z=&H4Ul1|0CHsz$W;9Z|D>$cn`BM?tfge?e-VV=~B0Xh;KdPG*UR zLSAW34$!F}wLXcJ`K3Vr_~)fM=jW8><{?>AQIwj-WuRce1+pIk6wFLbjZGEO6kuYe z#=s~90fjucn7JX)<7i?A29}s&7C?`nsWUUgRA+%r%pA$ylA^@SoYW#|2?%&JtN rll6o0^Gg&!0R=2-JoD1>6+l4^j*Q}x#G(?g-z?3|xKveL{oS|#9cG#h literal 0 HcmV?d00001 diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py new file mode 100644 index 000000000..0d614894f --- /dev/null +++ b/test/template/test_flextemplate.py @@ -0,0 +1,60 @@ + +from pathlib import Path +from fpdf.fpdf import FPDF +from fpdf.template import FlexTemplate +from ..conftest import assert_pdf_equal + +HERE = Path(__file__).resolve().parent + + +def test_flextemplate_offset(tmp_path): + 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" + templ.render(offsetx=120, offsety=50) + templ["label"] = "Offset: 120 / 120 mm, Rotate: 30°" + templ.render(offsetx=120, offsety=120, rotate=30.0) + assert_pdf_equal(pdf, HERE / "flextemplate_offset.pdf", tmp_path) + + + From 536e8198305d1fd78a8f76f4d9349528503b2158 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 25 Sep 2021 20:37:54 +0200 Subject: [PATCH 08/28] small fixes and cleanup --- badrot3.py | 31 +++++++++++++++++++++++++++++++ docs/Templates.md | 6 +++++- fpdf/template.py | 11 +++++------ 3 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 badrot3.py diff --git a/badrot3.py b/badrot3.py new file mode 100644 index 000000000..25d999a9b --- /dev/null +++ b/badrot3.py @@ -0,0 +1,31 @@ + + +import fpdf + +fix = True + +def stamp(pdf, x, y): + pdf.set_text_color(0x0) + pdf.set_fill_color(0xFFFFFF) + pdf.set_draw_color(0x0) + pdf.set_line_width(0) + + pdf.set_font("times", "", 20) + + pdf.rect(0+x, 0+y, 50, 50, style="FD") + pdf.line(0+x, 0+y, 50+x, 50+y) + pdf.line(0+x, 50+y, 50+x, 0+y) + pdf.set_xy(0+x, 52+y) + pdf.cell(50,5,"this is a label", border=0, ln=0, align="L", fill=True) + +pdf = fpdf.FPDF() +pdf.add_page() + +if fix: + pdf.set_font("helvetica", "", 10) + +for r,x,y in ((5, 50,50),(10,50,120), (15,120,50),(20,120,120)): + with pdf.rotation(r, x, y): + stamp(pdf, x,y) + +pdf.output("badrot.pdf") diff --git a/docs/Templates.md b/docs/Templates.md index 6d2fdcdb8..0d77abaea 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -166,7 +166,11 @@ Since we're handling the properties of the FPDF() instance directly, the constru fpdf.template.FlexTemplate(self, pdf, elements=None) ``` -It supports the same method as Template(), except for `add_page()`, which you will instead execute directly on the FPDF() instance. The dict syntax for setting text values is also supported. +It supports the same method as Template(), except for `add_page()`, which you will instead execute directly on the FPDF() instance. However, the render() method has a few more parameters: + +`FlexTemplate.render(offsetx=0.0, offsety=0.0, rotate=0.0)` + +The dict syntax for setting text values is also supported. # Details - Template definition # diff --git a/fpdf/template.py b/fpdf/template.py index 285712301..afc61420c 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -223,17 +223,16 @@ def text( ) @staticmethod - def line(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, **__): + def line(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__): if pdf.draw_color.lower() != rgb_as_str(foreground): pdf.set_draw_color(*rgb(foreground)) - # print "SetLineWidth", size + if pdf.fill_color != rgb_as_str(background): + pdf.set_fill_color(*rgb(background)) pdf.set_line_width(size) pdf.line(x1, y1, x2, y2) @staticmethod - def rect( - pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__ - ): + def rect(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__): if pdf.draw_color.lower() != rgb_as_str(foreground): pdf.set_draw_color(*rgb(foreground)) if pdf.fill_color != rgb_as_str(background): @@ -321,7 +320,7 @@ def write( def _render_element(self, element): handler_name = element["type"].upper() - if element.get("rotate"): # don't rotate by 0.0 degrees + if element.get("rotate"): with self.pdf.rotation(element["rotate"], element["x1"], element["y1"]): self.handlers[handler_name](self.pdf, **element) else: From 42e0d27b623fd9e4519724f131aa7c64a7df1f44 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 25 Sep 2021 21:33:16 +0200 Subject: [PATCH 09/28] removing mistaken checkin --- badrot3.py | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 badrot3.py diff --git a/badrot3.py b/badrot3.py deleted file mode 100644 index 25d999a9b..000000000 --- a/badrot3.py +++ /dev/null @@ -1,31 +0,0 @@ - - -import fpdf - -fix = True - -def stamp(pdf, x, y): - pdf.set_text_color(0x0) - pdf.set_fill_color(0xFFFFFF) - pdf.set_draw_color(0x0) - pdf.set_line_width(0) - - pdf.set_font("times", "", 20) - - pdf.rect(0+x, 0+y, 50, 50, style="FD") - pdf.line(0+x, 0+y, 50+x, 50+y) - pdf.line(0+x, 50+y, 50+x, 0+y) - pdf.set_xy(0+x, 52+y) - pdf.cell(50,5,"this is a label", border=0, ln=0, align="L", fill=True) - -pdf = fpdf.FPDF() -pdf.add_page() - -if fix: - pdf.set_font("helvetica", "", 10) - -for r,x,y in ((5, 50,50),(10,50,120), (15,120,50),(20,120,120)): - with pdf.rotation(r, x, y): - stamp(pdf, x,y) - -pdf.output("badrot.pdf") From 5b1d8893c8208bdaa273db22546eedaffd713786 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 26 Sep 2021 15:04:57 +0200 Subject: [PATCH 10/28] test for multipage Template(); Template.code39 with standard template fields. --- .gitignore | 5 ++ docs/Templates.md | 109 +++++++++++++++----------- fpdf/template.py | 41 +++++++--- test/template/flextemplate_offset.pdf | Bin 1211 -> 1158 bytes test/template/template_multipage.pdf | Bin 0 -> 2407 bytes test/template/test_flextemplate.py | 72 ++++++++--------- test/template/test_template.py | 28 +++++-- 7 files changed, 153 insertions(+), 102 deletions(-) create mode 100644 test/template/template_multipage.pdf diff --git a/.gitignore b/.gitignore index 0effa4d07..cf137fd7e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,8 @@ nosetests.xml # Idea .idea +*.un~ +*.swp +*.md~ +*.py~ +*.csv~ diff --git a/docs/Templates.md b/docs/Templates.md index 0d77abaea..be41f4b1b 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -43,37 +43,39 @@ The constructor signature is as follows: ```python fpdf.template.Template( - elements=None, - format="A4", - orientation="portrait", - unit="mm", - title="", - author="", - subject="", - creator="", - keywords="", - ) + elements=None, + format="A4", + orientation="portrait", + unit="mm", + title="", + author="", + subject="", + creator="", + keywords="", + ) ``` -Its important methods are: -* Template.load_elements(elements) +Its public methods are: +* `Template.load_elements(elements)` * An alternative to supplying the elements dict to the constructor. -* Template.parse_csv(infile, delimiter=",", decimal_sep=".", encoding=None) +* `Template.parse_csv(infile, delimiter=",", decimal_sep=".", encoding=None)` * Load a template CSV file instead of supplying a dict. -* Template.add_page() +* `Template.add_page()` * Renders the elements to the current page, and proceeds to the next page. -* Template.render(outfile=None) - * Renders the content to the last page, and writes the PDF to a file if its name is given. +* `Template.render(outfile=None)` + * Renders the contents to the last page, and writes the PDF to a file if its name is given. 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: -`tmpl["company_name"] = "Sample Company"` +```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. +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() @@ -137,14 +139,14 @@ As you see, this can be quite a bit more involved, but there are hardly any limi 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): +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.: ```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",}, + {"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() @@ -156,61 +158,74 @@ templ.render(offsetx=50, offsety=120) templ["label"] = "Offset: 120 / 50 mm" templ.render(offsetx=120, offsety=50) templ["label"] = "Offset: 120 / 120 mm" -templ.render(offsetx=120, offsety=120) +templ.render(offsetx=120, offsety=120, rotate=30.0) pdf.output("example.pdf") ``` Since we're handling the properties of the FPDF() instance directly, the constructor signature of this class is much simpler: ```python -fpdf.template.FlexTemplate(self, pdf, elements=None) +fpdf.template.FlexTemplate(pdf, elements=None) ``` -It supports the same method as Template(), except for `add_page()`, which you will instead execute directly on the FPDF() instance. However, the render() method has a few more parameters: +It supports the same public methods as Template(), except for `add_page()`, which you will instead execute directly on the FPDF() instance. However, the render() method has a few more parameters and a bit different semantics: -`FlexTemplate.render(offsetx=0.0, offsety=0.0, rotate=0.0)` +* `FlexTemplate.load_elements(elements)` + * An alternative to supplying the elements dict to the constructor. +* `FlexTemplate.parse_csv(infile, delimiter=",", decimal_sep=".", encoding=None)` + * Load a template CSV file instead of supplying a dict. +* `FlexFlexTemplate.render(offsetx=0.0, offsety=0.0, rotate=0.0)` + * Renders the contents to the current page. + +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: -The dict syntax for setting text values is also supported. +The dict syntax for setting text values is also supported: +```python +FlexTemplate["company_name"] = "Sample Company" +``` # Details - Template definition # 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): - * __name__: placeholder identification +* __name__: placeholder identification * _mandatory_ - * __type__: +* __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 - * '__W__': "Write" - uses the FPDF.write() method to add text to 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 (in mm), defining a bounding box in most cases +* __x1, y1, x2, y2__: top-left, bottom-right coordinates (in mm), defining a bounding box in most cases * for multiline text, this is the bounding box for just the first line, not the complete box - * _mandatory_ - * __font__: e.g. "helvetica" + * for the barcodes types, the height of the barcode is `y2 - y1`. + * _mandatory_ (x2 is not used in the barcode types, but must still be present as integer value) +* __font__: e.g. "helvetica" * _optional_, default: "helvetica" - * __size__: text size, or line width for line and rect, in points (float value) +* __size__: text size, or line width for line and rect, in points (float value) + * for the barcode types, the width of one bar in mm. * _optional_, default: 10 - * __bold, italic, underline__: text style, enabled with True or equivalent value - * in csv, only int values, 0 as false, non-0 as true +* __bold, italic, underline__: text style, 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, e.g. 0xFFFFFF +* __foreground, background__: text and fill colors, e.g. 0xFFFFFF * _optional_, default: 0x000000/0xFFFFFF - * __align__: text alignment, '__L__': left, '__R__': right, '__C__': center +* __align__: text alignment, '__L__': left, '__R__': right, '__C__': center * _optional_, default: 'L' - * __text__: default string, can be replaced at runtime +* __text__: default string, can be replaced at runtime * _optional_, default: empty - * __priority__: Z-order (int value) +* __priority__: Z-order (int value) * _optional_, default: 0 - * __multiline__: configure text wrapping +* __multiline__: configure text wrapping * in dicts, None for single line, True to 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 + * 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) +* __rotation__: rotate the element in degrees around the top left corner x1/y1 (float) * _optional_, default: 0.0 - no rotation Fields that are not relevant to a specific element type will be ignored there. diff --git a/fpdf/template.py b/fpdf/template.py index afc61420c..656e2b4ba 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -44,10 +44,10 @@ def load_elements(self, elements): self.elements = elements self.keys = [] for e in elements: + # priority is optional, but we need a default for sorting. if not "priority" in e: e["priority"] = 0 self.keys.append(e["name"].lower()) - #self.keys = [v["name"].lower() for v in self.elements] @staticmethod def _parse_colorcode(s): @@ -223,16 +223,16 @@ def text( ) @staticmethod - def line(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__): + def line(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, **__): if pdf.draw_color.lower() != rgb_as_str(foreground): pdf.set_draw_color(*rgb(foreground)) - if pdf.fill_color != rgb_as_str(background): - pdf.set_fill_color(*rgb(background)) pdf.set_line_width(size) pdf.line(x1, y1, x2, y2) @staticmethod - def rect(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__): + def rect( + pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__ + ): if pdf.draw_color.lower() != rgb_as_str(foreground): pdf.set_draw_color(*rgb(foreground)) if pdf.fill_color != rgb_as_str(background): @@ -269,15 +269,30 @@ def barcode( @staticmethod def code39( pdf, - text, - x, - y, *_, - w=1.5, - h=5, + x1=0, + y1=0, + x2=0, + y2=0, + text="", + size=1.5, + x=None, + y=None, + w=None, + h=None, **__, ): - pdf.code39(text, x, y, w, h) + if x is not None or y is not None or w is not None or h is not None: + raise FPDFException( + "Arguments x,y,w,h are invalid. Use x1,y1,x2,y2 instead." + ) + w = x2 - x1 + if w <= 0: + w = 1.5 + h = y2 - y1 + if h <= 0: + h = 5 + pdf.code39(text, x1, y1, size, h) # Added by Derek Schwalenberg Schwalenberg1013@gmail.com to allow (url) links in # templates (using write method) 2014-02-22 @@ -330,7 +345,9 @@ def render(self, offsetx=0.0, offsety=0.0, rotate=0.0): sorted_elements = sorted(self.elements, key=lambda x: x["priority"]) for element in sorted_elements: element = element.copy() - element["text"] = self.texts.get(element["name"].lower(), element.get("text", "")) + element["text"] = self.texts.get( + element["name"].lower(), element.get("text", "") + ) element["x1"] = element["x1"] + offsetx element["y1"] = element["y1"] + offsety element["x2"] = element["x2"] + offsetx diff --git a/test/template/flextemplate_offset.pdf b/test/template/flextemplate_offset.pdf index 8870c5e0c5a7c026d90faad3fb3845298dc717ed..6f7f9b41119b5f9d7ac19f376453a21d09b0eedd 100644 GIT binary patch delta 435 zcmdnZ*~U4czTU{#&W@|Nq$o8pm#bpV-k`I7Ed~;MK8qAzaoP1(|4pH=>Bpq7O2)+- zIwwkm3&o#y-=drActB_R*Gv3HpTGWIW-L=Kse0pRMxNTz;1w$Bw-!_#JX(9X=GtAcuB69(+xEJA&B{Mkw@du%(=C@Ae3)9k z{fNnAXj6YFEo*2rMa5~&RMnh^MS9&IU1ObowcEaZke|dIX%IApn~yE2$UwlIPwkl2 z?v}4Bb*8^5nv_fg8e`G3#9 z`LbxS$K2X7ugopA9xo-+jxT)OB+s--;E$a`?Wgr0KHRe{{VLW}$6vVd_J0ONE5;q0 znVCK^ikny%C>Vf%LY@K_m|*o5fPw)XWS+&K!%Fg^|JJd=_U;BV#UARabvE FE&xLiwlDwy delta 489 zcmZqU+|4FlIfu?YeQ9NT=bhoYBj8rr=`SvkyhCkI_ue~duf>IZ3UF!8n^t<&D29+d_FGtD^q9p5MJ;htMg)Qiq&9OwO zX&PI@A}nj``FhX5z?Ej{Z#a_S3vNh zxEvt7mfB{9mKbst*u>0@CtI;NbDEoR Lsj9mAyKw;kM$gpm diff --git a/test/template/template_multipage.pdf b/test/template/template_multipage.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a67796e3de951b44344fc8d3a4e7adb255aed359 GIT binary patch literal 2407 zcmbW3Yg7|Q6oA#r!vz#YQ9*GuU_mOIC2vI}2}G!X5uyc=YDkt4O?GLr!AC{$S?~>^ z_S9k#JW{lZD71=?qJk)B5u|DXAD|*Apm@XwMcd9oEFe8SJSh3fz+(a0&>7gvxh%z+Go?5lr4;p@Kz_LdI)&h$lv!$qEz6+F$h=(lNL6&+u>Yk-Wn# z4F#vs#=N5^@wi)Qr4J{Mc3Hn^(uI9 z+QRP_#ptiSi8h2D9@-h5|9art%)%m?gUPKXBAgx~AHtX0moGj1KYZDJ`5Jrkwd>|< zoIH`;WF@Nz5=RUlm3Z@GZO#5FbBAx($us}*z8K8;geCg1e)=i*`iGx?t!vuAIAeCha+s%^jI z_^8ghZ!GqSUm3iiL=;iv;JxXA&4z_HV#b_kDr#_F*jkvAvC3*pm<_NOR*qih{L(!k zF1^++u|2kD{>%2TJG-8TA0)mW|9Sa? z3zE@u9ezSaezgD$W2P)R)x1%f%?qC&WRv$Qz+2wjdqS5?Q;u9zgXO6wI=!&;A^v(-vHh@kKE5I|>s2%w`PBoaZzqygUNECdb0m1?<&Tn-|baEc?~ zczE&wpN)C&1(8V4O)h|weJn`em5-nzxgMv#0P2rx;&4i>kWUqn8YO~;8ltGK0zz*H zLr^~gUioT51<=pbgpkmy-wApd!RRggrxlr6i|eO`kXkwMVJ7H;)olS5H65u4#VHVh z!s-W58NM8zBR~rap2*aTO??3LSl2PVyx<20Xm7weDDebcTMnVmfQHS9T7oMm0CnK7 z$8}f_9R!uBDGd%>L*?37*ar}Cq*{TyK_0O7QEHO#h1C(b`cA_HTmk0E!Y~07Hn-l( z^*%y3ZLyq^Ye*Gcrd6;go2vAjSey_l=tbX!04h;a{(vxOH1IsMZlHVxX@G~GL*Qak zW6%=4>@3&e(FhY@EQH=ccLQ7whs6QWz$C-iJZOw=fcQto<3jiJmBG=!$oMcA-_v9B z`|EMI{q+RAetH;_(@ziM^xq4{<-suPYZv45;Zx;3nScuu=benw$<-QM2j4|7?$j%B p+AnAbNy0ag=?xS>M3bP4byqm(DY=g7@}Vb_%R^jT{N_lIzX77SaVY=* literal 0 HcmV?d00001 diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 0d614894f..5ffe5f17b 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -1,4 +1,3 @@ - from pathlib import Path from fpdf.fpdf import FPDF from fpdf.template import FlexTemplate @@ -9,40 +8,40 @@ def test_flextemplate_offset(tmp_path): 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", - }, - ] + { + "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) @@ -55,6 +54,3 @@ def test_flextemplate_offset(tmp_path): templ["label"] = "Offset: 120 / 120 mm, Rotate: 30°" templ.render(offsetx=120, offsety=120, rotate=30.0) assert_pdf_equal(pdf, HERE / "flextemplate_offset.pdf", tmp_path) - - - diff --git a/test/template/test_template.py b/test/template/test_template.py index dc56dc2ad..88139e4fa 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -153,23 +153,41 @@ def test_template_nominal_csv(tmp_path): tmpl = Template(format="A4", title="Sample Invoice") tmpl.parse_csv(HERE / "mycsvfile.csv", delimiter=";") tmpl.add_page() - tmpl['empty_fields'] = 'empty' + tmpl["empty_fields"] = "empty" assert_pdf_equal(tmpl, HERE / "template_nominal_csv.pdf", tmp_path) tmpl = Template(format="A4", title="Sample Invoice") tmpl.parse_csv(HERE / "mycsvfile.csv", delimiter=";", encoding="utf-8") tmpl.add_page() - tmpl['empty_fields'] = 'empty' + tmpl["empty_fields"] = "empty" assert_pdf_equal(tmpl, HERE / "template_nominal_csv.pdf", tmp_path) +def test_template_multipage(tmp_path): + """Testing a Template() populating several pages.""" + tmpl = Template(format="A4", title="Sample Invoice") + tmpl.parse_csv(HERE / "mycsvfile.csv", delimiter=";") + tmpl.add_page() + tmpl["name0"] = "Joe Doe" + tmpl["title0"] = "Director" + tmpl.add_page() + tmpl["name0"] = "Jane Doe" + tmpl["title0"] = "General Manager" + tmpl.add_page() + tmpl["name0"] = "Heinz Mustermann" + tmpl["title0"] = "Worker" + assert_pdf_equal(tmpl, HERE / "template_multipage.pdf", tmp_path) + + def test_template_code39(tmp_path): # issue-161 elements = [ { "name": "code39", "type": "C39", - "x": 40, - "y": 50, - "h": 20, + "x1": 40, + "y1": 50, + "x2": 0, # dummy value + "y2": 70, + "size": 1.5, "text": "Code 39 barcode", "priority": 1, }, From 92c9e281e6b11004bc973dcfda641650ff7e6aad Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 26 Sep 2021 16:05:00 +0200 Subject: [PATCH 11/28] refer defaults to type handlers, x2 optional for barcodes --- docs/Templates.md | 4 +- fpdf/template.py | 73 ++++++++++++++++++---------------- test/template/test_template.py | 1 - 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index be41f4b1b..01e7e5558 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -204,7 +204,7 @@ A template definition consists of a number of elements, which have the following * __x1, y1, x2, y2__: top-left, bottom-right coordinates (in mm), 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`. - * _mandatory_ (x2 is not used in the barcode types, but must still be present as integer value) + * _mandatory_ (_optional_ for the barcode types) * __font__: e.g. "helvetica" * _optional_, default: "helvetica" * __size__: text size, or line width for line and rect, in points (float value) @@ -290,7 +290,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') -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. +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: diff --git a/fpdf/template.py b/fpdf/template.py index 656e2b4ba..cf3178009 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -47,6 +47,9 @@ def load_elements(self, elements): # priority is optional, but we need a default for sorting. if not "priority" in e: e["priority"] = 0 + # x2 is optional for barcode types, but needed for offset rendering + if e["type"] in ["B", "C39"] and "x2" not in e: + e["x2"] = 0 self.keys.append(e["name"].lower()) @staticmethod @@ -74,49 +77,53 @@ def varsep_float(s, default="0"): # glad to have nonlocal scoping... return float((s.strip() or default).replace(decimal_sep, ".")) - handlers = ( - ("name", str), - ("type", str), - ("x1", varsep_float), - ("y1", varsep_float), - ("x2", varsep_float), - ("y2", varsep_float), - ("font", str, "helvetica"), - ("size", varsep_float, 10.0), - ("bold", int, 0), - ("italic", int, 0), - ("underline", int, 0), - ("foreground", self._parse_colorcode, 0x0), - ("background", self._parse_colorcode, 0xFFFFFF), - ("align", str, "L"), - ("text", str, ""), - ("priority", int, 0), - ("multiline", self._parse_multiline, None), - ("rotate", varsep_float, 0.0), + key_config = ( + # key, converter, mandatory + ("name", str, True), + ("type", str, True), + ("x1", varsep_float, True), + ("y1", varsep_float, True), + ("x2", varsep_float, True), + ("y2", varsep_float, True), + ("font", str, False), + ("size", varsep_float, False), + ("bold", int, False), + ("italic", int, False), + ("underline", int, False), + ("foreground", self._parse_colorcode, False), + ("background", self._parse_colorcode, False), + ("align", str, False), + ("text", str, False), + ("priority", int, False), + ("multiline", self._parse_multiline, False), + ("rotate", varsep_float, False), ) self.elements = [] if encoding is None: encoding = locale.getpreferredencoding() - hlen = len(handlers) with open(infile, encoding=encoding) as f: for row in csv.reader(f, delimiter=delimiter): - rlen = len(row) - # fill in any missing items - row[rlen + 1 :] = [""] * (hlen - rlen) + # fill in blanks for any missing items + row.extend([""] * (len(key_config) - len(row))) kargs = {} - for i, v in enumerate(row): - handler = handlers[i] - vs = v.strip() + for i, (val, cfg) in enumerate(zip(row, key_config)): + vs = val.strip() if not vs: - if len(handler) < 3: + if cfg[2]: # mandatory + if cfg[0] == "x2" and row["type"] in ["B", "C39"]: + # two types don't need x2, but offset rendering does + continue raise FPDFException( - "Mandatory value '%s' missing in csv data" % handler[0] + "Mandatory value '%s' missing in csv data" % cfg[0] ) - kargs[handler[0]] = handler[2] # default + elif cfg[0] == "priority": + # formally optional, but we need some value for sorting + kargs["priority"] = 0 + # otherwise, let the type handlers use their own defaults else: - kargs[handler[0]] = handler[1](v) + kargs[cfg[0]] = cfg[1](vs) self.elements.append(kargs) - self.keys = [v["name"].lower() for v in self.elements] + self.keys = [val["name"].lower() for val in self.elements] def __setitem__(self, name, value): if name.lower() not in self.keys: @@ -272,7 +279,6 @@ def code39( *_, x1=0, y1=0, - x2=0, y2=0, text="", size=1.5, @@ -286,9 +292,6 @@ def code39( raise FPDFException( "Arguments x,y,w,h are invalid. Use x1,y1,x2,y2 instead." ) - w = x2 - x1 - if w <= 0: - w = 1.5 h = y2 - y1 if h <= 0: h = 5 diff --git a/test/template/test_template.py b/test/template/test_template.py index 88139e4fa..d2d3cd8c7 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -185,7 +185,6 @@ def test_template_code39(tmp_path): # issue-161 "type": "C39", "x1": 40, "y1": 50, - "x2": 0, # dummy value "y2": 70, "size": 1.5, "text": "Code 39 barcode", From 0195db4b8b6c8efa69b44d5fb3d8c6c5cea705c8 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 26 Sep 2021 19:19:18 +0200 Subject: [PATCH 12/28] more template and flextemplate tests --- docs/Templates.md | 73 ++++++++++++++--------- fpdf/template.py | 16 +++-- test/template/badfloat.csv | 1 + test/template/badint.csv | 1 + test/template/badtype.csv | 1 + test/template/flextemplate_multipage.pdf | Bin 0 -> 1937 bytes test/template/mandmissing.csv | 1 + test/template/test_flextemplate.py | 31 ++++++++++ test/template/test_template.py | 28 ++++++++- 9 files changed, 118 insertions(+), 34 deletions(-) create mode 100644 test/template/badfloat.csv create mode 100644 test/template/badint.csv create mode 100644 test/template/badtype.csv create mode 100644 test/template/flextemplate_multipage.pdf create mode 100644 test/template/mandmissing.csv diff --git a/docs/Templates.md b/docs/Templates.md index 01e7e5558..3fd8ff8a8 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -1,17 +1,19 @@ # 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" 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. 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 useage pattern here is: @@ -61,7 +63,7 @@ Its public methods are: * `Template.parse_csv(infile, delimiter=",", decimal_sep=".", encoding=None)` * Load a template CSV file instead of supplying a dict. * `Template.add_page()` - * Renders the elements to the current page, and proceeds to the next page. + * Renders the elements to the current page (except at first call), and proceeds to the next page. * `Template.render(outfile=None)` * Renders the contents to the last page, and writes the PDF to a file if its name is given. @@ -135,7 +137,7 @@ pdf.next_page() pdf.output("example.pdf") ``` -As you see, this can be 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. +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. 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. @@ -162,7 +164,7 @@ templ.render(offsetx=120, offsety=120, rotate=30.0) pdf.output("example.pdf") ``` -Since we're handling the properties of the FPDF() instance directly, the constructor signature of this class is much simpler: +Since we're handling the properties of the FPDF() instance separately, the constructor signature of this class is much simpler: ```python fpdf.template.FlexTemplate(pdf, elements=None) @@ -179,17 +181,18 @@ It supports the same public methods as Template(), except for `add_page()`, whic 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: -The dict syntax for setting text values is also supported: +The dict syntax for setting text values is the same: ```python FlexTemplate["company_name"] = "Sample Company" ``` + # Details - Template definition # 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): -* __name__: placeholder identification +* __name__: placeholder identification (unique text string) * _mandatory_ * __type__: * '__T__': Text - places one or several lines of text on the page @@ -203,32 +206,47 @@ A template definition consists of a number of elements, which have the following * _mandatory_ * __x1, y1, x2, y2__: top-left, bottom-right coordinates (in mm), 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`. - * _mandatory_ (_optional_ for the barcode types) -* __font__: e.g. "helvetica" - * _optional_, default: "helvetica" -* __size__: text size, or line width for line and rect, in points (float value) + * 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 in points * for the barcode types, the width of one bar in mm. - * _optional_, default: 10 -* __bold, italic, underline__: text style, enabled with True or equivalent value + * _optional_ + * default: 10 for text, 2 mm for 'BC', 1.5 mm 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, e.g. 0xFFFFFF - * _optional_, default: 0x000000/0xFFFFFF + * _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' + * _optional_ + * default: 'L' * __text__: default string, can be replaced at runtime - * _optional_, default: empty + * displayed text for 'T' and 'W' + * data to encode for barcode types + * _optional_ + * default: empty * __priority__: Z-order (int value) - * _optional_, default: 0 + * _optional_ + * default: 0 * __multiline__: configure text wrapping - * in dicts, None for single line, True to for multicells (multiple lines), False trims to exactly fit the space defined + * 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 + * _optional_ + * default: single line * __rotation__: rotate the element in degrees around the top left corner x1/y1 (float) - * _optional_, default: 0.0 - no rotation + * _optional_ + * default: 0.0 - no rotation + +Fields that are not relevant to a specific element type will be ignored there, but if present must still adhere to the specified data type. -Fields that are not relevant to a specific element type will be ignored there. # How to create a template # @@ -239,8 +257,6 @@ A template can be created in 3 ways: * 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 @@ -258,7 +274,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() @@ -274,6 +290,7 @@ 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: diff --git a/fpdf/template.py b/fpdf/template.py index cf3178009..4992baf1a 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -47,9 +47,15 @@ def load_elements(self, elements): # priority is optional, but we need a default for sorting. if not "priority" in e: e["priority"] = 0 + for k in ("name", "type", "x1", "y1", "y2"): + if k not in e: + raise KeyError(f"Mandatory key '{k}' missing in input data") # x2 is optional for barcode types, but needed for offset rendering - if e["type"] in ["B", "C39"] and "x2" not in e: - e["x2"] = 0 + if "x2" not in e: + if e["type"] in ["B", "C39"]: + e["x2"] = 0 + else: + raise KeyError("Mandatory key 'x2' missing in input data") self.keys.append(e["name"].lower()) @staticmethod @@ -114,7 +120,7 @@ def varsep_float(s, default="0"): # two types don't need x2, but offset rendering does continue raise FPDFException( - "Mandatory value '%s' missing in csv data" % cfg[0] + f"Mandatory value '{cfg[0]}' missing in csv data" ) elif cfg[0] == "priority": # formally optional, but we need some value for sorting @@ -289,8 +295,8 @@ def code39( **__, ): if x is not None or y is not None or w is not None or h is not None: - raise FPDFException( - "Arguments x,y,w,h are invalid. Use x1,y1,x2,y2 instead." + raise ValueError( + "Arguments x/y/w/h are invalid. Use x1/y1/y2/size instead." ) h = y2 - y1 if h <= 0: diff --git a/test/template/badfloat.csv b/test/template/badfloat.csv new file mode 100644 index 000000000..ebad2ddcf --- /dev/null +++ b/test/template/badfloat.csv @@ -0,0 +1 @@ +name;T;21.0;14.0;x104.0;25.0;times;16.0;0;0;0;0;16777215;L;name;2;0 diff --git a/test/template/badint.csv b/test/template/badint.csv new file mode 100644 index 000000000..e3909679a --- /dev/null +++ b/test/template/badint.csv @@ -0,0 +1 @@ +name;T;21.0;14.0;104.0;25.0;times;16.0;x;0;0;0;16777215;L;name;2;0 diff --git a/test/template/badtype.csv b/test/template/badtype.csv new file mode 100644 index 000000000..a9defc438 --- /dev/null +++ b/test/template/badtype.csv @@ -0,0 +1 @@ +name;X;21.0;14.0;104.0;25.0;times;16.0;0;0;0;0;16777215;L;name;2;0 diff --git a/test/template/flextemplate_multipage.pdf b/test/template/flextemplate_multipage.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5b117932ebdd131624685bc71146278999bcfd51 GIT binary patch literal 1937 zcmbVNYfuwc7*+5A3xgFDQITF4pkjp0l81>z5E5c2PXmbJh=#C0JmFuruv=f9&o(_wIMU@0@SW zb&HTlJuxp1!U1eR#wQ{o5gL`U1_x+_LX9Kvsly2xU~pBApmLld^*SX^0q;RTOcFF) zrvR6k`}!g{k-$V@rlx38jh4oB0F`PLG%mrFWC9Exf)i?bHQ=xX@E?lS;ff?Ax!`-b zA*_o1UU>_v*&?$v&Tp3!Ypm_GsGWZ4Me;zK64`6tvq~Rl%S5lHhoRil_h((R5KSx4 zd``v)T9W6?uIoziolv`0Hucr#$pQ z#|*8L<&+uu_LHvEy6%~E^5W00JYzQ$#!#Eu+>ew86p!QBU-9XPYv|VXGz3uE*Vpwg zR)=+{j7QVDB2KWj{V;c7e)_d9q^pv1E3VtwiCOMVvCD2uJ=lCoHurJv9Fb8!aig8j zH(QrRqXLI5^%rH1!adWE8*{H7EsHy42+n`ml*Uq)($Un$;Jv=KIn6K7zFQd&7Ou~m z?7@i&iSBFfujn~Y|1xmV&a#)rf*T3FIX7lr4in!V?>8aAx##1BMK$ZK`o~O8>C0gF zHqmEdp7)0NchQkQQb%U`KXl~s=Z(-Y%0S#@TbK8;lC?G}$>PG+`?g%q+Tfg9ei4Vy zU$k!<6MC+-GiH*Zo4C7*lA%SP9aM3rcvu~n`Du0Kf#$MI!;W#gzwEOoUS&J<)J1GJ zbp9&7SA7*&j@xA}yi{4cy-Q<$yKRfPz^%@vJchPT>q*TOm_ zKWY};nDBe=SPK`23+-7Yc69}1b1GxM>d5auv>T`Ld8?Q!d*$Wh>a?e}e)UzW4<{y?C1%|hu~LiM9<|R#*JM6!E?>LiX>+}|{?b!P z19xXd*x}CFKe^hJwBSv~CE6{4_BmcvH8-E6bw^}Lrrz8ibhru6lJr(}Opml^eHP-r zc#22J`JdDwF0zgg_xE#>R1KF!$FJR#g*nxHawhwpm2rvVx1Tupd*yAjJelgU{ZZb% z*z|P)`7!&mEr0Y3?`}GK;W6p!ct+jsxVQ0XVPSQ=QDtx{uDfZ9njJ&XKmrS1Z&1TH4E`Xpl5 zImtR)g|GpJAxs8?1Mqn~4iBh+3B%X|m<=-k@ejt~LkAj(!5J)eG$w##jl#Hm*np!j z9v8OeNDRZ~LAgd@-h4>;D2)3r`SCdLrW@Kv>l7L-u7g)7>>5of&eV&_NfKV449`%? dAVNg~MydfZDO#bU2dKgL90B6y78ou?{sv$6yu<(i literal 0 HcmV?d00001 diff --git a/test/template/mandmissing.csv b/test/template/mandmissing.csv new file mode 100644 index 000000000..c078de184 --- /dev/null +++ b/test/template/mandmissing.csv @@ -0,0 +1 @@ +name;X;21.0;14.0;104.0 diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 5ffe5f17b..21d7b9779 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -54,3 +54,34 @@ def test_flextemplate_offset(tmp_path): templ["label"] = "Offset: 120 / 120 mm, Rotate: 30°" templ.render(offsetx=120, offsety=120, rotate=30.0) assert_pdf_equal(pdf, HERE / "flextemplate_offset.pdf", tmp_path) + + +def test_flextemplate_multipage(tmp_path): + + 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() + tmpl_0 = FlexTemplate(pdf, elements) + tmpl_0["label"] = "Offset: 50 / 50 mm" + tmpl_0.render(offsetx=50, offsety=50) + tmpl_0["label"] = "Offset: 50 / 120 mm" + tmpl_0.render(offsetx=50, offsety=120) + tmpl_0["label"] = "Offset: 120 / 50 mm" + tmpl_0.render(offsetx=120, offsety=50) + tmpl_0["label"] = "Offset: 120 / 120 mm" + tmpl_0.render(offsetx=120, offsety=120, rotate=30.0) + pdf.add_page() + tmpl_0["label"] = "Offset: 120 / 50 mm" + tmpl_0.render(offsetx=120, offsety=50) + tmpl_0["label"] = "Offset: 120 / 120 mm" + tmpl_0.render(offsetx=120, offsety=120, rotate=30.0) + tmpl_1 = FlexTemplate(pdf) + tmpl_1.parse_csv(HERE / "mycsvfile.csv", delimiter=";") + tmpl_1.render() + assert_pdf_equal(pdf, HERE / "flextemplate_multipage.pdf", tmp_path) + diff --git a/test/template/test_template.py b/test/template/test_template.py index d2d3cd8c7..7ca7c7014 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -1,8 +1,9 @@ from pathlib import Path +from pytest import raises import qrcode -from fpdf.template import Template +from fpdf.template import Template, FPDFException from ..conftest import assert_pdf_equal @@ -178,6 +179,31 @@ def test_template_multipage(tmp_path): assert_pdf_equal(tmpl, HERE / "template_multipage.pdf", tmp_path) +def test_template_badinput(tmp_path): + """Testing Template() with non-conforming definitions.""" + elements = [{ }] + with raises(KeyError): + tmpl = Template(elements=elements) + elements = [{"name":"n", "type":"X"}] + with raises(KeyError): + tmpl = Template(elements=elements) + tmpl.render() + elements = [{"name":"n", "type":"T","x1":0,"y1":0,"x2":0,"y2":"x"}] + with raises(TypeError): + tmpl = Template(elements=elements) + tmpl.render() + tmpl = Template() + with raises(FPDFException): + tmpl.parse_csv(HERE / "mandmissing.csv", delimiter=";") + with raises(ValueError): + tmpl.parse_csv(HERE / "badint.csv", delimiter=";") + with raises(ValueError): + tmpl.parse_csv(HERE / "badfloat.csv", delimiter=";") + with raises(KeyError): + tmpl.parse_csv(HERE / "badtype.csv", delimiter=";") + tmpl.render() + + def test_template_code39(tmp_path): # issue-161 elements = [ { From e5ab09ccfb2af26cea647030c3acdf648f7c6a5b Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 26 Sep 2021 21:00:31 +0200 Subject: [PATCH 13/28] static check fixes --- fpdf/template.py | 2 +- test/template/test_flextemplate.py | 1 - test/template/test_template.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index 4992baf1a..0f98f337e 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -419,4 +419,4 @@ def render(self, outfile=None, dest=None): self.pdf.set_auto_page_break(False, margin=0) super().render() if outfile: - pdf.output(outfile) + self.pdf.output(outfile) diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 21d7b9779..501f83cd7 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -84,4 +84,3 @@ def test_flextemplate_multipage(tmp_path): tmpl_1.parse_csv(HERE / "mycsvfile.csv", delimiter=";") tmpl_1.render() assert_pdf_equal(pdf, HERE / "flextemplate_multipage.pdf", tmp_path) - diff --git a/test/template/test_template.py b/test/template/test_template.py index 7ca7c7014..5463e55f4 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -179,6 +179,7 @@ def test_template_multipage(tmp_path): assert_pdf_equal(tmpl, HERE / "template_multipage.pdf", tmp_path) +# pylint: disable=unused-argument def test_template_badinput(tmp_path): """Testing Template() with non-conforming definitions.""" elements = [{ }] From fdb03dec18e28509f642a94436d64d4821ef481b Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 26 Sep 2021 21:13:57 +0200 Subject: [PATCH 14/28] more pylint --- fpdf/template.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index 0f98f337e..22113ee83 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -74,6 +74,7 @@ def _parse_multiline(s): return True if i < 0: return False + return None def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): """Parse template format csv file and create elements dict""" @@ -112,16 +113,17 @@ def varsep_float(s, default="0"): # fill in blanks for any missing items row.extend([""] * (len(key_config) - len(row))) kargs = {} - for i, (val, cfg) in enumerate(zip(row, key_config)): + for val, cfg in zip(row, key_config): vs = val.strip() if not vs: if cfg[2]: # mandatory if cfg[0] == "x2" and row["type"] in ["B", "C39"]: # two types don't need x2, but offset rendering does - continue - raise FPDFException( - f"Mandatory value '{cfg[0]}' missing in csv data" - ) + pass + else: + raise FPDFException( + f"Mandatory value '{cfg[0]}' missing in csv data" + ) elif cfg[0] == "priority": # formally optional, but we need some value for sorting kargs["priority"] = 0 @@ -373,6 +375,7 @@ def render(self, offsetx=0.0, offsety=0.0, rotate=0.0): class Template(FlexTemplate): # Disabling this check due to the "format" parameter below: # pylint: disable=redefined-builtin + # pylint: disable=unused-argument def __init__( self, infile=None, @@ -403,6 +406,7 @@ def add_page(self): self.render() self.pdf.add_page() + # pylint: disable=arguments-differ def render(self, outfile=None, dest=None): """ Args: From 56f639ebaea7554585a8298646b5de90bc7f1481 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 26 Sep 2021 21:27:39 +0200 Subject: [PATCH 15/28] blackity-black --- fpdf/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index 22113ee83..8ee156381 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -122,8 +122,8 @@ def varsep_float(s, default="0"): pass else: raise FPDFException( - f"Mandatory value '{cfg[0]}' missing in csv data" - ) + f"Mandatory value '{cfg[0]}' missing in csv data" + ) elif cfg[0] == "priority": # formally optional, but we need some value for sorting kargs["priority"] = 0 From bef03d1dea445949c7e28fbd30a1767c7b185402 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 26 Sep 2021 21:29:20 +0200 Subject: [PATCH 16/28] even blacker --- test/template/test_flextemplate.py | 39 ++++++++++++++++++++++++++---- test/template/test_template.py | 6 ++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 501f83cd7..107622851 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -59,11 +59,40 @@ def test_flextemplate_offset(tmp_path): def test_flextemplate_multipage(tmp_path): 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",}, - ] + { + "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() tmpl_0 = FlexTemplate(pdf, elements) diff --git a/test/template/test_template.py b/test/template/test_template.py index 5463e55f4..59e79b902 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -182,14 +182,14 @@ def test_template_multipage(tmp_path): # pylint: disable=unused-argument def test_template_badinput(tmp_path): """Testing Template() with non-conforming definitions.""" - elements = [{ }] + elements = [{}] with raises(KeyError): tmpl = Template(elements=elements) - elements = [{"name":"n", "type":"X"}] + elements = [{"name": "n", "type": "X"}] with raises(KeyError): tmpl = Template(elements=elements) tmpl.render() - elements = [{"name":"n", "type":"T","x1":0,"y1":0,"x2":0,"y2":"x"}] + elements = [{"name": "n", "type": "T", "x1": 0, "y1": 0, "x2": 0, "y2": "x"}] with raises(TypeError): tmpl = Template(elements=elements) tmpl.render() From cad0264f29e0af0ee50d8884c92444cc3d8b5143 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 29 Sep 2021 19:17:17 +0200 Subject: [PATCH 17/28] Expand docstrings, update help, hide private methods. --- .gitignore | 7 +- docs/Templates.md | 49 ++---------- fpdf/template.py | 196 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 156 insertions(+), 96 deletions(-) diff --git a/.gitignore b/.gitignore index cf137fd7e..17dd38d39 100644 --- a/.gitignore +++ b/.gitignore @@ -59,8 +59,7 @@ nosetests.xml # Idea .idea -*.un~ + +# Vim backup and swap files +*.*~ *.swp -*.md~ -*.py~ -*.csv~ diff --git a/docs/Templates.md b/docs/Templates.md index 3fd8ff8a8..6900ce5e4 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -16,7 +16,7 @@ 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 useage pattern here is: +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) @@ -41,32 +41,8 @@ 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 and other metadata for the PDF file. -The constructor signature is as follows: +For the method signatures, see [pyfpdf.github.io: class Template](https://pyfpdf.github.io/fpdf2/fpdf/template.html#fpdf.template.Template). -```python -fpdf.template.Template( - elements=None, - format="A4", - orientation="portrait", - unit="mm", - title="", - author="", - subject="", - creator="", - keywords="", - ) -``` - -Its public methods are: -* `Template.load_elements(elements)` - * An alternative to supplying the elements dict to the constructor. -* `Template.parse_csv(infile, delimiter=",", decimal_sep=".", encoding=None)` - * Load a template CSV file instead of supplying a dict. -* `Template.add_page()` - * Renders the elements to the current page (except at first call), and proceeds to the next page. -* `Template.render(outfile=None)` - * Renders the contents to the last page, and writes the PDF to a file if its name is given. - 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 @@ -164,24 +140,9 @@ templ.render(offsetx=120, offsety=120, rotate=30.0) pdf.output("example.pdf") ``` -Since we're handling the properties of the FPDF() instance separately, the constructor signature of this class is much simpler: - -```python -fpdf.template.FlexTemplate(pdf, elements=None) -``` - -It supports the same public methods as Template(), except for `add_page()`, which you will instead execute directly on the FPDF() instance. However, the render() method has a few more parameters and a bit different semantics: - -* `FlexTemplate.load_elements(elements)` - * An alternative to supplying the elements dict to the constructor. -* `FlexTemplate.parse_csv(infile, delimiter=",", decimal_sep=".", encoding=None)` - * Load a template CSV file instead of supplying a dict. -* `FlexFlexTemplate.render(offsetx=0.0, offsety=0.0, rotate=0.0)` - * Renders the contents to the current page. - -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: +For the method signatures, see [pyfpdf.github.io: class FlexTemplate](https://pyfpdf.github.io/fpdf2/fpdf/template.html#fpdf.template.FlexTemplate). -The dict syntax for setting text values is the same: +The dict syntax for setting text values is the same as above: ```python FlexTemplate["company_name"] = "Sample Company" @@ -190,7 +151,7 @@ FlexTemplate["company_name"] = "Sample Company" # Details - Template definition # -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): +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). * __name__: placeholder identification (unique text string) * _mandatory_ diff --git a/fpdf/template.py b/fpdf/template.py index 8ee156381..1e70fd9b9 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -1,4 +1,4 @@ -"""PDF Template Helper for FPDF.py""" +"""PDF Template Helpers for fpdf.py""" __author__ = "Mariano Reingart " __copyright__ = "Copyright (C) 2010 Mariano Reingart" @@ -12,35 +12,60 @@ from .fpdf import FPDF -def rgb(col): +def _rgb(col): return (col // 65536), (col // 256 % 256), (col % 256) -def rgb_as_str(col): - r, g, b = rgb(col) +def _rgb_as_str(col): + r, g, b = _rgb(col) if (r == 0 and g == 0 and b == 0) or g == -1: return f"{r / 255:.3f} g" return f"{r / 255:.3f} {g / 255:.3f} {b / 255:.3f} rg" class FlexTemplate: + """ + A flexible templating class. + + Allows to apply one or several template definitions to any page of + a document in any combination. + """ + def __init__(self, pdf, elements=None): + """ + Arguments: + + pdf (fpdf.FPDF() instance): + All content will be added to this object. + + elements (list of dicts): + A template definition in a list of dicts. + If you omit this, then you need to call either load_elements() + or parse_csv() before doing anything else. + """ if elements: self.load_elements(elements) self.pdf = pdf self.handlers = { - "T": self.text, - "L": self.line, - "I": self.image, - "B": self.rect, - "BC": self.barcode, - "C39": self.code39, - "W": self.write, + "T": self._text, + "L": self._line, + "I": self._image, + "B": self._rect, + "BC": self._barcode, + "C39": self._code39, + "W": self._write, } self.texts = {} def load_elements(self, elements): - """Initialize the internal element structures""" + """ + Load a template definition. + + Arguments: + + elements (list of dicts): + A template definition in a list of dicts + """ self.elements = elements self.keys = [] for e in elements: @@ -77,9 +102,29 @@ def _parse_multiline(s): return None def parse_csv(self, infile, delimiter=",", decimal_sep=".", encoding=None): - """Parse template format csv file and create elements dict""" + """ + Load the template definition from a CSV file. + + Arguments: + + infile (string): + The filename of the CSV file. + + delimiter (single character): + The character that seperates the fields in the CSV file: + Usually a comma, semicolon, or tab. + + decimal_sep (single character): + The decimal separator used in the file. + Usually either a point or a comma. - def varsep_float(s, default="0"): + encoding (string): + The character encoding of the file. + Default is the system default encoding. + + """ + + def _varsep_float(s, default="0"): """Convert to float with given decimal seperator""" # glad to have nonlocal scoping... return float((s.strip() or default).replace(decimal_sep, ".")) @@ -88,12 +133,12 @@ def varsep_float(s, default="0"): # key, converter, mandatory ("name", str, True), ("type", str, True), - ("x1", varsep_float, True), - ("y1", varsep_float, True), - ("x2", varsep_float, True), - ("y2", varsep_float, True), + ("x1", _varsep_float, True), + ("y1", _varsep_float, True), + ("x2", _varsep_float, True), + ("y2", _varsep_float, True), ("font", str, False), - ("size", varsep_float, False), + ("size", _varsep_float, False), ("bold", int, False), ("italic", int, False), ("underline", int, False), @@ -103,7 +148,7 @@ def varsep_float(s, default="0"): ("text", str, False), ("priority", int, False), ("multiline", self._parse_multiline, False), - ("rotate", varsep_float, False), + ("rotate", _varsep_float, False), ) self.elements = [] if encoding is None: @@ -157,7 +202,22 @@ def __getitem__(self, name): ) def split_multicell(self, text, element_name): - """Divide (\n) a string using a given element width""" + """ + Split a string between words, for the parts to fit into a given element + width. Additional splits will be made replacing any '\\n' characters. + + Arguments: + + text (string): + The input text string. + + element_name (string): + The name of the template element to fit the text inside. + + Returns: + A list of substrings, each of which will fit into the element width + when rendered in the element font style and size. + """ element = next( element for element in self.elements @@ -180,7 +240,7 @@ def split_multicell(self, text, element_name): ) @staticmethod - def text( + def _text( pdf, *_, x1=0, @@ -201,10 +261,10 @@ def text( ): if not text: return - if pdf.text_color != rgb_as_str(foreground): - pdf.set_text_color(*rgb(foreground)) - if pdf.fill_color != rgb_as_str(background): - pdf.set_fill_color(*rgb(background)) + if pdf.text_color != _rgb_as_str(foreground): + pdf.set_text_color(*_rgb(foreground)) + if pdf.fill_color != _rgb_as_str(background): + pdf.set_fill_color(*_rgb(background)) font = font.strip().lower() style = "" @@ -238,30 +298,30 @@ def text( ) @staticmethod - def line(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, **__): - if pdf.draw_color.lower() != rgb_as_str(foreground): - pdf.set_draw_color(*rgb(foreground)) + def _line(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, **__): + if pdf.draw_color.lower() != _rgb_as_str(foreground): + pdf.set_draw_color(*_rgb(foreground)) pdf.set_line_width(size) pdf.line(x1, y1, x2, y2) @staticmethod - def rect( + def _rect( pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__ ): - if pdf.draw_color.lower() != rgb_as_str(foreground): - pdf.set_draw_color(*rgb(foreground)) - if pdf.fill_color != rgb_as_str(background): - pdf.set_fill_color(*rgb(background)) + if pdf.draw_color.lower() != _rgb_as_str(foreground): + pdf.set_draw_color(*_rgb(foreground)) + if pdf.fill_color != _rgb_as_str(background): + pdf.set_fill_color(*_rgb(background)) pdf.set_line_width(size) pdf.rect(x1, y1, x2 - x1, y2 - y1, style="FD") @staticmethod - def image(pdf, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): + def _image(pdf, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): if text: pdf.image(text, x1, y1, w=x2 - x1, h=y2 - y1, link="") @staticmethod - def barcode( + def _barcode( pdf, *_, x1=0, @@ -275,14 +335,14 @@ def barcode( **__, ): # pylint: disable=unused-argument - if pdf.draw_color.lower() != rgb_as_str(foreground): - pdf.set_draw_color(*rgb(foreground)) + if pdf.draw_color.lower() != _rgb_as_str(foreground): + pdf.set_draw_color(*_rgb(foreground)) font = font.lower().strip() if font == "interleaved 2of5 nt": pdf.interleaved2of5(text, x1, y1, w=size, h=y2 - y1) @staticmethod - def code39( + def _code39( pdf, *_, x1=0, @@ -308,7 +368,7 @@ def code39( # Added by Derek Schwalenberg Schwalenberg1013@gmail.com to allow (url) links in # templates (using write method) 2014-02-22 @staticmethod - def write( + def _write( pdf, *_, x1=0, @@ -326,8 +386,8 @@ def write( **__, ): # pylint: disable=unused-argument - if pdf.text_color != rgb_as_str(foreground): - pdf.set_text_color(*rgb(foreground)) + if pdf.text_color != _rgb_as_str(foreground): + pdf.set_text_color(*_rgb(foreground)) font = font.strip().lower() style = "" for tag in "B", "I", "U": @@ -373,6 +433,12 @@ def render(self, offsetx=0.0, offsety=0.0, rotate=0.0): class Template(FlexTemplate): + """ + A simple templating class. + + Allows to apply a single template definition to all pages of a document. + """ + # Disabling this check due to the "format" parameter below: # pylint: disable=redefined-builtin # pylint: disable=unused-argument @@ -390,9 +456,36 @@ def __init__( keywords="", ): """ - Args: - infile (str): [**DEPRECATED**] unused, will be removed in a later version + Arguments: + + infile (str): + [**DEPRECATED**] unused, will be removed in a later version + + elements (list of dicts): + A template definition in a list of dicts. + If you omit this, then you need to call either load_elements() + or parse_csv() before doing anything else. + + format (str): + The page format of the document (eg. "A4" or "letter"). + + orientation (str): + The orientation of the document. + Possible values are "portrait"/"P" or "landscape"/"L" + + unit (str): + The units used in the template definition. + One of "mm", "cm", "in", "pt", or a number for points per unit. + + title (str): The title of the document. + + author (str): The author of the document. + + subject (str): The subject matter of the document. + + creator (str): The creator of the document. """ + pdf = FPDF(format=format, orientation=orientation, unit=unit) pdf.set_title(title) pdf.set_author(author) @@ -402,6 +495,7 @@ def __init__( super().__init__(pdf=pdf, elements=elements) def add_page(self): + """Finish the current page, and proceed to the next one.""" if self.pdf.page: self.render() self.pdf.add_page() @@ -409,10 +503,16 @@ def add_page(self): # pylint: disable=arguments-differ def render(self, outfile=None, dest=None): """ - Args: - outfile (str): optional output PDF file path. If ommited, the - `.pdf.output(...)` method can be manuallyy called afterwise. - dest (str): [**DEPRECATED**] unused, will be removed in a later version + Finish the document and process all pending data. + + Arguments: + + outfile (str): + If given, the PDF file will be written to this file name. + Alternatively, the `.pdf.output()` method can be manually called. + + dest (str): + [**DEPRECATED**] unused, will be removed in a later version. """ if dest: warnings.warn( From 0f999846098e15de9a606c608f7a1ffa44b6ce50 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 29 Sep 2021 21:16:26 +0200 Subject: [PATCH 18/28] Issues from PR review --- fpdf/template.py | 6 ++++-- test/template/template_code39.pdf | Bin 1284 -> 1327 bytes .../template_code39_defaultheight.pdf | Bin 0 -> 1326 bytes test/template/test_template.py | 20 +++++++++++++++++- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 test/template/template_code39_defaultheight.pdf diff --git a/fpdf/template.py b/fpdf/template.py index 1e70fd9b9..90da35513 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -43,9 +43,11 @@ def __init__(self, pdf, elements=None): If you omit this, then you need to call either load_elements() or parse_csv() before doing anything else. """ + if not isinstance(pdf, FPDF): + raise TypeError("'pdf' must be an instance of fpdf.FPDF()") + self.pdf = pdf if elements: self.load_elements(elements) - self.pdf = pdf self.handlers = { "T": self._text, "L": self._line, @@ -88,7 +90,7 @@ def _parse_colorcode(s): """Allow hex and oct values for colors""" if s[:2] in ["0x", "0X"]: return int(s, 16) - if s[:2] in ["0o", "0O"]: + if s[0] == "0": return int(s, 8) return int(s) diff --git a/test/template/template_code39.pdf b/test/template/template_code39.pdf index bbf34bdc7ff417ab48d8726c445197a65b41f12f..2516db05714b47addcb0298b6d5fdec637390239 100644 GIT binary patch delta 566 zcmZqSTF*71zTVWt&W@|Nq$o8pm#bnF?ALF9 z;}~aQb)r>vPZ?WM!SgoWce%-LVtity$M%WtiJ_u}&tyN;uG=Z2W-Emy9T(GfPR z`D|lw?zYv+Q_?re9(-Wi_Gx2%nQG00T9I^xGNz-CAJodvwtF`zV4m3YLz?fdIed~U zetYtW^bOw!r{uSP4f%cbt<|+}^L+NO+RA;8iPJcDd+`LB73&-L-)&vUJZa1FTlr7s zbuqtR`++e-y>wNqZB<)-oUJH((%cZ)OGQFQJ6k2h@*hlKoqxx6rp5ag`|Asn7WRi4 zm(D)OzFoBCGoQFf{(~1!6N(pHJbmzyv8mO{LzV9O>__X zb&eY{`o)QFKYld3*S|-x`?Q$H?@6MG_fw~Pe^lRec+bS}+twoWH+Lw@$@||8PB?hC zI4N6Md{d$R-Y<1Z>`p&dFWtJgSV_J7`ts(}oS74@9Lu}2TcGB=ugEqj%U$=>q)%4P zKDTYVSkGs(;vBy-If~NTrImKbToU7V4^y6R`aE}|;N;Vn6OP8`a&Ef$`Dw(Q?Q40n z@2Ic5rEt=PQ$MaR<5Cd!y&d)2lRg`#S?lecf1>kf zm?w6=_YwPz(;H^luJ640!OrvQvD!&me>i^K>o+ZK_px{`3v|K`|De|U_Y3Rzy0nff zo_R2*e9jUnccJIc7yLNUTm0LVf%LY@K_m|SsL)C5Dy!qgH&%+kbk@*@@(P7@0*RaIAiH!cA4Li3vd diff --git a/test/template/template_code39_defaultheight.pdf b/test/template/template_code39_defaultheight.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5fe7741801ef678c6dde37f881e73d13beaa24fb GIT binary patch literal 1326 zcmY!laB@i4DM>9-(09v8EJ<}qP0mjN8t#*t zmtK;gU}90o8X;vp^TyY|Y7wP>_tox%|Y5J^5xQ{ZD4k`O7XIR&+!5*`6KGCx6qn7J14X!}0Lwbmy!F@jku}ZmieO zIqGdTVxaF5mm(qV4ikGI}QxUPMMXN1byk@b*d4$}0-6$zIkba_*gm*7w_I z3X>Xyk3LSw<9IjYXIs&6ohP2Zr^&CN>AJodbcJEf1vixZ3?N1jIAS-&p-`-paCDlfOytSpMiixn_;QmZU59 zY^&PL@7s#9AGca*wdARaS-H-U4HeAoIzNuQJ9*@9-@m`+3V8I(L2c;Ez{>C-T2}v4l(hM&RD5ynR=9OmJ1r z={Q^$cjDfG#z*sAWhyV#F+bnJ^N;c6ZE;~x0)WORG%0|R4o1Q%=F<1hOet1~1|_?o zST23%{8CUd#bu=?9l4m4MSVD0Ktvam@p!#LT>O1^w{MJjcA^Oqc+Y zC7?tOaXK&~6y+xerjQI)f__M91<(@DTwo?~g3``V+7sv> zL1sdu1DIDcOFR_vN^^36UID4~NvzB-1^U-NFV#6er!+SY$(o9y)HE&w1q&{a{ScsF zW@>6|s*t7t6EiaeMkWX-etwApD4>9an`d5Hz5*yJz>!s4l2}v%_M4%Br2&_!s;j>n F7XVv()NlX* literal 0 HcmV?d00001 diff --git a/test/template/test_template.py b/test/template/test_template.py index 59e79b902..cc16f706c 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -214,7 +214,7 @@ def test_template_code39(tmp_path): # issue-161 "y1": 50, "y2": 70, "size": 1.5, - "text": "Code 39 barcode", + "text": "*Code 39 barcode*", "priority": 1, }, ] @@ -223,6 +223,24 @@ def test_template_code39(tmp_path): # issue-161 assert_pdf_equal(tmpl, HERE / "template_code39.pdf", tmp_path) +def test_template_code39_defaultheight(tmp_path): # height <= 0 invokes default + elements = [ + { + "name": "code39", + "type": "C39", + "x1": 40, + "y1": 50, + "y2": 50, + "size": 1.5, + "text": "*Code 39 barcode*", + "priority": 1, + }, + ] + tmpl = Template(format="A4", title="Sample Code 39 barcode", elements=elements) + tmpl.add_page() + assert_pdf_equal(tmpl, HERE / "template_code39_defaultheight.pdf", tmp_path) + + def test_template_qrcode(tmp_path): # issue-175 elements = [ { From bba1ca18b85d7aa4bd9820115ef5344bd9b7f500 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 30 Sep 2021 18:55:24 +0200 Subject: [PATCH 19/28] Issue #226 solved: Rotate anything anywhere --- fpdf/fpdf.py | 7 +- fpdf/template.py | 178 +++++++++++++++++------ test/template/flextemplate_multipage.pdf | Bin 1937 -> 1941 bytes test/template/flextemplate_offset.pdf | Bin 1158 -> 1159 bytes test/template/flextemplate_rotation.pdf | Bin 0 -> 10559 bytes test/template/template_multipage.pdf | Bin 2407 -> 2415 bytes test/template/template_nominal_csv.pdf | Bin 1475 -> 1478 bytes test/template/test_flextemplate.py | 88 +++++++++++ 8 files changed, 228 insertions(+), 45 deletions(-) create mode 100644 test/template/flextemplate_rotation.pdf diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 7d6626550..9ee512b0d 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -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: @@ -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') diff --git a/fpdf/template.py b/fpdf/template.py index 90da35513..a1ec45b91 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -241,9 +241,9 @@ def split_multicell(self, text, element_name): split_only=True, ) - @staticmethod def _text( - pdf, + self, + rotations, *_, x1=0, y1=0, @@ -263,6 +263,7 @@ def _text( ): if not text: return + pdf = self.pdf if pdf.text_color != _rgb_as_str(foreground): pdf.set_text_color(*_rgb(foreground)) if pdf.fill_color != _rgb_as_str(background): @@ -281,77 +282,151 @@ def _text( if underline: style += "U" pdf.set_font(font, style, size) - pdf.set_xy(x1, y1) width, height = x2 - x1, y2 - y1 + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) if multiline is None: # write without wrapping/trimming (default) - pdf.cell( - w=width, h=height, txt=text, border=0, ln=0, align=align, fill=True + self._render_rotated( + (x1, y1), + pdf.cell, + (), + { + "w": width, + "h": height, + "txt": text, + "border": 0, + "ln": 0, + "align": align, + "fill": True, + }, + rotations, ) elif multiline: # automatic word - warp - pdf.multi_cell( - w=width, h=height, txt=text, border=0, align=align, fill=True + self._render_rotated( + (x1, y1), + pdf.multi_cell, + (), + { + "w": width, + "h": height, + "txt": text, + "border": 0, + "align": align, + "fill": True, + }, + rotations, ) else: # trim to fit exactly the space defined text = pdf.multi_cell( w=width, h=height, txt=text, align=align, split_only=True )[0] - pdf.cell( - w=width, h=height, txt=text, border=0, ln=0, align=align, fill=True + self._render_rotated( + (x1, y1), + pdf.cell, + (), + { + "w": width, + "h": height, + "txt": text, + "border": 0, + "ln": 0, + "align": align, + "fill": True, + }, + rotations, ) - @staticmethod - def _line(pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, **__): + def _line(self, rotations, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, **__): + pdf = self.pdf if pdf.draw_color.lower() != _rgb_as_str(foreground): pdf.set_draw_color(*_rgb(foreground)) pdf.set_line_width(size) - pdf.line(x1, y1, x2, y2) + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) + self._render_rotated(None, pdf.line, (x1, y1, x2, y2), {}, rotations) - @staticmethod def _rect( - pdf, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, background=0xFFFFFF, **__ + self, + rotations, + *_, + x1=0, + y1=0, + x2=0, + y2=0, + size=0, + foreground=0, + background=0xFFFFFF, + **__, ): + pdf = self.pdf if pdf.draw_color.lower() != _rgb_as_str(foreground): pdf.set_draw_color(*_rgb(foreground)) if pdf.fill_color != _rgb_as_str(background): pdf.set_fill_color(*_rgb(background)) pdf.set_line_width(size) - pdf.rect(x1, y1, x2 - x1, y2 - y1, style="FD") + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) + self._render_rotated( + None, pdf.rect, (x1, y1, x2 - x1, y2 - y1), {"style": "FD"}, rotations + ) - @staticmethod - def _image(pdf, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): + def _image(self, rotations, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): if text: - pdf.image(text, x1, y1, w=x2 - x1, h=y2 - y1, link="") + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) + self._render_rotated( + None, + self.pdf.image, + (text, x1, y1), + {"w": x2 - x1, "h": y2 - y1, "link": ""}, + rotations, + ) - @staticmethod def _barcode( - pdf, + self, + rotations, *_, x1=0, y1=0, x2=0, y2=0, text="", - font="helvetica", + font="interleaved 2of5 nt", size=1, foreground=0, **__, ): # pylint: disable=unused-argument - if pdf.draw_color.lower() != _rgb_as_str(foreground): - pdf.set_draw_color(*_rgb(foreground)) + pdf = self.pdf + if pdf.fill_color.lower() != _rgb_as_str(foreground): + pdf.set_fill_color(*_rgb(foreground)) font = font.lower().strip() if font == "interleaved 2of5 nt": - pdf.interleaved2of5(text, x1, y1, w=size, h=y2 - y1) + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) + self._render_rotated( + None, + pdf.interleaved2of5, + (text, x1, y1), + {"w": size, "h": y2 - y1}, + rotations, + ) - @staticmethod def _code39( - pdf, + self, + rotations, *_, x1=0, y1=0, y2=0, text="", size=1.5, + foreground=0, x=None, y=None, w=None, @@ -362,16 +437,22 @@ def _code39( raise ValueError( "Arguments x/y/w/h are invalid. Use x1/y1/y2/size instead." ) + pdf = self.pdf + if pdf.fill_color.lower() != _rgb_as_str(foreground): + pdf.set_fill_color(*_rgb(foreground)) h = y2 - y1 if h <= 0: h = 5 - pdf.code39(text, x1, y1, size, h) + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) + self._render_rotated(None, pdf.code39, (text, x1, y1, size, h), {}, rotations) # Added by Derek Schwalenberg Schwalenberg1013@gmail.com to allow (url) links in # templates (using write method) 2014-02-22 - @staticmethod def _write( - pdf, + self, + rotations, *_, x1=0, y1=0, @@ -388,6 +469,7 @@ def _write( **__, ): # pylint: disable=unused-argument + pdf = self.pdf if pdf.text_color != _rgb_as_str(foreground): pdf.set_text_color(*_rgb(foreground)) font = font.strip().lower() @@ -403,16 +485,26 @@ def _write( if underline: style += "U" pdf.set_font(font, style, size) - pdf.set_xy(x1, y1) - pdf.write(5, text, link) - - def _render_element(self, element): - handler_name = element["type"].upper() - if element.get("rotate"): - with self.pdf.rotation(element["rotate"], element["x1"], element["y1"]): - self.handlers[handler_name](self.pdf, **element) + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) + self._render_rotated((x1, y1), pdf.write, (5, text, link), {}, rotations) + + def _render_rotated(self, pos, func, args, kwargs, rotations): + # Solves issue 226 + # Settings operations (fonts, colors, line widths etc.) must not appear + # within a rotation context. + # The solution is to queue up rotations and execute them all in one go + # once everything else has been set up. + # Technically, we could keep rotating until we're dizzy (up to Pythons + # recursion limit), but in practise we take two turns at most. + if rotations: + with self.pdf.rotation(*(rotations[0])): + self._render_rotated(pos, func, args, kwargs, rotations[1:]) else: - self.handlers[handler_name](self.pdf, **element) + if pos: + self.pdf.set_xy(*pos) + func(*args, **kwargs) def render(self, offsetx=0.0, offsety=0.0, rotate=0.0): sorted_elements = sorted(self.elements, key=lambda x: x["priority"]) @@ -425,12 +517,14 @@ def render(self, offsetx=0.0, offsety=0.0, rotate=0.0): element["y1"] = element["y1"] + offsety element["x2"] = element["x2"] + offsetx element["y2"] = element["y2"] + offsety + handler_name = element["type"].upper() if rotate: # don't rotate by 0.0 degrees - with self.pdf.rotation(rotate, offsetx, offsety): - self._render_element(element) + rotations = [ + (rotate, offsetx, offsety), + ] + self.handlers[handler_name](rotations, **element) else: - self._render_element(element) - + self.handlers[handler_name]([], **element) self.texts = {} # reset modified entries for the next page diff --git a/test/template/flextemplate_multipage.pdf b/test/template/flextemplate_multipage.pdf index 5b117932ebdd131624685bc71146278999bcfd51..a466312b7ecf1a16cb1d588cb5b2cdea2c5eb923 100644 GIT binary patch delta 921 zcmbQpKb3#NocjHTHwq~fI*R^e&d^dUTA=Xp_l>^COI8LJYl`PDnz^;S% z7o8)=AtHT?@B7;;Dl_KxA3V{fX;qUHP~7ldZE2I!S{7rmx6*m_^LD?>{$I;}b|a_z z{rih*zndp-pVNK3C^Azijq#GppO{RhWm9>28!e2sBn7RTs#+`fDn_hxk>1KjN&40m zUT+r47+x2V@j3D)LFt^#nV!%x&-cNXr@Y~s^zV~)%)_3Cm%`5;PW}4Bkm<3+;Y0D& z0yZzsTPc>dt$H;j=GNtPzve4%|Lm4Nao4ZgmP;4E4fgtb`+8ML?1x|nu7mPSn|$`k z{}7irKY1@>SG~ERogG(kNl|KIE?32zy&=B&B8EJB&#(U=$oKeI;iHNjTNf!5uH^d6 zF7PHS`fbP}ok#B{um5?YY(<;%&Lf?FKFxc3W8;UFh8aA6^43-D_~GF2^Tzv!yUxoo z%S)ds+i7>N_xcS(vjun$hqziOG+n zJ8@;F$fPH7bq<_=`I5rk=#&L&NO4}MZ#Osp{rCLcm$DN~3mESmFxb=j&u$sd_GFPx zCs!W#*S<>G$IrfyIBXF4|26~5Mak>is&1XUYV!QSR;#;a|J|!sX?S1r7OgFPyUNtQ z+A-wqb)m0(Gq;;@ShrtWYvNHKHC@>F*dlpkx$Ii;IFIMw{^U`-5kX<#q z@M}!L=bV{^hd#O7GMQL6$K(FO=b1M?eVxW}bL|pUu}FtqZ~P*8_dk>2nz&-otzMU? zx;Bpz8`*h=tqp$WMxBd160K}(l7$?Lq-WMRY@3;B{&`JZ_|!QzlUTDQcZ=UxTW=A< zYSupP`YO$o8H$pMC3Z#_zY!KEJUz<=^SgZ)S*Qw@r<#80$Rg&z!X#`rdb4{(pMhTC&{c-pc|jy&wM>++Ia( z=Ss~>fkrZrm7kQwWvF1F0HinDv-mNInkX26fI^-E7nosSXl!mUc@bN(tr>=lk+~6u an280Z38uytlbzVzI8BVWR8?L5-M9b~8=Y(b delta 892 zcmbQrKaqdJoO=7oa@veS;vp058W4A5YT9GqL@9867lg;;J&vV#_&&ll0 z54U(;p`~%y%xkrmsn~t6Qgl-gPF}43 zPAW%^Q$%W(82{M|Dl=r)A3X6(H>ze)cyY^XwWWKU*0LCjowcri7kBGh(T`hB#W{a2 zmVVFhdzbcRdG*JDb=tj0bR1?ioS*8JP%&xWMPXrQZMCOSk(WdlKHKD>^JR0&X8RFX41LkyW%#T_MY|gX|;Cpj*`&gdheNbLKB{S*Z+I1 z@qt76G(k<{5TDS$Z~yQ6F<<%SPgnPeyMDfzb1CKbgiF$A{G6||+Owr7{-||WcCwuN zKZ9W93)_e)BlR){pykU zaV4Pd^!v-scaNP@;AEN1Y<2=78WVRk{Bz&JQ|^0f zwNhL17qyLBzAk49y`S>%HzS9V#QwJ(TZFI7*`;+LYVNyP{}+Kl9v-!@B+2T9bN@UwYOmax*U6%s9vG#h%5K)p5^8yK3$0CvP?M3)){j z+fx|(>Df)A(nFtIZkbHn=eOkjh0i8AmDS!YZ^CjqUBxEo9X@&WL-n_S4j0j@^E{V4 zP3>^`efXVZFvp88K5Bs?nm)Uar~4>*yz^0$pOBk&>x|9n^H)9m6+W+r^Qf->x2N0ozsk3h*_!@VX6F4*ixwSAdXsiq zZtby)iYkB8OP!@cPbu}^Z*y&Ye#~Ye|HrhIvtPvT+cV+Yl+)I4_7eK%h-}_jWA@4%q%d(Oe`=>Ff}%r?8ffKX>7rzs_N?R G#svTl+Lv_z diff --git a/test/template/flextemplate_offset.pdf b/test/template/flextemplate_offset.pdf index 6f7f9b41119b5f9d7ac19f376453a21d09b0eedd..d23f4d309dddf4ad9e24d1c40b4d6cae5e9b3f68 100644 GIT binary patch delta 428 zcmZqUZ0DTNP;X*q$5mWXl$w~!RWWC8khkAq1Ccw=MK-f^?)Yc@a+8q4M#t6|2FD;y z(^VQ1cZj(kDU(sp?VaVj`=htX=Qrm0ZI3kPdyp{+-mnLpR_hBp7s%6W9e=eWX| z9hy=#3&S@zzD*0VYV|5w#Jh_Bj*a=*^1HeVHN@U!+>Q>J|1E7z|MNxr=7dXi1U6id zvk#SOC{(Rotjx!w+9^<+${D@GX6n(3py?CpHa?fNQQ0*nEcZ zE2F5Pf&mC9JcjE#8`AV?q delta 427 zcmZqYY~!5JP;YE!$5mWXl$w~!RWWC8&{@A01BpGKMT)Pu?E0(!rcl`QV^UZp&na8WFDLZM-ATv$Igb4N>utY1VZ#F3&kavFPc2*Z?_2%+zvth4 zS+v+=Zf%)Y=9XHImy&767rt(iXWAt2$4;U4)A|n|?%9@p6>F;FFWh+hKZByx=Ch1n y8AS~g3_w63Pk{@}FfcGPGn#yz*;>;KL&n?;Q_RB1V6ruf3x_e6s;aBM8y5iXBeiG% diff --git a/test/template/flextemplate_rotation.pdf b/test/template/flextemplate_rotation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c8b4dfd1642967881915cd120506b0a13526154d GIT binary patch literal 10559 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 QkU=68`S$G5#_I6>550mtKL7v# literal 0 HcmV?d00001 diff --git a/test/template/template_multipage.pdf b/test/template/template_multipage.pdf index a67796e3de951b44344fc8d3a4e7adb255aed359..b0485b9fc634afa2a0db9f0656f96898072972cd 100644 GIT binary patch delta 1150 zcmaDZ^j>H}1Ea~rM%DUz47)Njk{3xm`sz8mOxMTfv5@MoH}{|4Fx#NPQQNTok=~)R zw#?i@jxh&q)LJk7U6@d!=33#n(m`_p+l|KUU2*N-#pcx?V~t+8%Wz%&^BvP>pMP_4 z&F1}#eouM+?Q_ak6j0y@exb(u64Nd)amAsnaJW-4*tt|H)y87<3RQ;pqLkBjo zcWSNLDX`?qSH;F{?PobPSxTxDJw;rNGC$Wj2|1RDPpWaqwhFQ>T6Xry!kM;nY&S6` z>x8AhaFbcpG-1Kncr~uYj?$}@QWnbk&JN|9?)c*EiKKvvn~ctvE=8s-w9z_huf4hF z(Xy$ww#RN;%02IwrCDt(V!P zrkhNxuiu}%oNXFsa@qsyo!Sqc-D40Ia@^QfIdMyLlWV}1`!8?hESlGtdgZ%{x`w4j zWe9(DLD->o z{+`AO+H7kZp2Y5`XkGpHx~WJ$BlBuGl~wgCo;gi8R>ykbU2(P3CpqK26I8mCCT>~l z@1by)g`l+v9mXI*^B zz9i(-vdIUTI~mPEfmja=#K&@uJOV;a&0cc^OD-qKF3NFw^b(jN?`|>d^`3QN|Ni@( zvCP{c!K(4#%_&f@8a4(_G>L9{y&z-X>}=cKDz<5k_b1L-@MOW0K--W9YZhJpXVZQ> zckXt}EVCE8im&kYZFze8+RLi=>5Df%=$yd1!`FZ2#k7Fw295RS^(wg(LFw}3?ShY7 z>Q`ThbZu{FEMyl74JlUfcwEhTp~mvIVrBnKIZq+ii5|0b%Rs?85frS`LipzF^?maI z60G(P=93m`ad6(MNL8|zFo_aXksHZ01^ zZl1Wb@9odVrOr~j&iuUfszAARvas~eFM<1~e@*znJyrU&TSTmUMc{9^zB delta 1177 zcmaDa^jv5{1EbNzM%DWJ2_?Z7kGYs0c|F8hVqw|#!>9Ptl2-_Og0xNKDt13!lKEi&3D|ay<6|}Q_gV1 z8^=XmS?gFdOKUq8yj^Ig+}hx`UZr!9htgH~bQQsra@CXUTb3z@DyQG_k(+h%@J(fD zgJVgBjd@2FYH@Jh`jWcHKx)fIEhn*C7q>+IERbvw-hGu%Vv*Q+8_u7BC;vsxOcgO- z`{wD+J^If#Z`_qyc4yAXXKLcA&236YjTZ-=Q11D9{vBIt$U7gg-&|?GJ^PQ}{qt1d zQNd0NtC)H58O4|Wq?HD*<@=qs9QqxT)+W)sZ?q{lF?6mr!^=|El&*GRhL%2Y}x#qT3 z1dHqUx$EAlz2fYcT&G~^rspul{o@Aqbe6e2&t+}oU+>Wj$hpM(e1&)4s}tMTU+&$v z&LRDPcIy#`+A6=6V@}bBrR>_C@V3NU%aGXd;_y@Ln78}aEv(^|h~QNat9QI8-lXzP zyP<6UZQ)7ZGoCsLaT~c`+GeR}sC<*#^W5T#wk!SSY<8A;@MMBXM(&0m&FgM1;@FU6 z^6`|{&y)+9t&x&1+%Bg+eVoQ4b93*TMuj7XW*v8^>qz;(=8V9mxnbYFMBJP9IdWCr zRN1(5Pma2Y2TE<_5^0Z8;5V2Qzx#Eoe!vRz$S_~t&EGvIpa0`>_%YArrH>LmerKqi zF3i4T#q3>c%RU5{UR{L@=~IJMAX#Q`Zt?Z1_b{kN^si>BmUD1E*{TITAs{OILxYjgXqH3V~Y zHs1O&g~OMvl+n2U$D2vd1&SgUCqCH2`#aoX<+bGx?lZA1lkIQ~_%czU^S!7;cj!*% zB0giwNk^K_ObpR0_c-HmbE3-Y3ooXH@XXok`{qH)L|~X2*gIUGvk)4lI}a|n$hFz% z!j@$xy?*=3-`G^WvH=vP#~gNbs(`|@$ZXx4hxg|HKI!!|;I7F{pK}>Wi@P;?qgcCK zbtkY%oSeV@bxXhs)18{9p7P&WyFbh|)>hx$_x5MwQfH}MK0lAWDo~D{EG+%=OW=O- zuL&Qx>*t?l-@KKzmr2x6!2kpl@)Wqh3sz5|he3;CXAA33mvy;EzIrQnh)KLs35rxiMI zG0vUmA>=yIC29?yhti36E|d5(eBD>Mo!d6kX2#9KH$ZxiRMj0AGHd3uNha&2~s%R`O z3QzeO5^f*4sh$6$apA^;^4o2;SianE(w(;R=zq3*Pp{{3rRJqTLp(Ka^Do9*Orjt2#y-;iv*ci;Jo(Ht%( znC?tVKa(Mw<2B=Odib7(2yM2t4Nr1+R5W+LdM(uTy`wRbT_`lfSZK+WUjmM+t14Z% z80Yq>bT0Bx(!K7kBA8-6=?QbxU+H)pGTPb(|i*aG!|W7MMeegNW^Y=v|4ZZE_j|$)2qb?z z6Qpq4O>O!0j5U(e0`IJo^zT1fYPeTcY96mv(TmPWj?NPwUDQYsJXUpS@`9dIYft=I zs>QEkULPU%$f$7N(fQkLOiVN5U#0H7_3%H_YX0jnT&Z~}(BKBL@{=|(U1t(CR4@Pm zg**i=FvGyW!qRB+8y3r8Q%iFUIWtQO3^8*{GjuUS14ArgmL`)cSY5abEzP-9RbBnv FxB#67(~|%I diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 107622851..5b4f269a2 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -1,4 +1,5 @@ from pathlib import Path +import qrcode from fpdf.fpdf import FPDF from fpdf.template import FlexTemplate from ..conftest import assert_pdf_equal @@ -113,3 +114,90 @@ def test_flextemplate_multipage(tmp_path): tmpl_1.parse_csv(HERE / "mycsvfile.csv", delimiter=";") tmpl_1.render() assert_pdf_equal(pdf, HERE / "flextemplate_multipage.pdf", tmp_path) + + +def test_flextemplate_rotation(tmp_path): + elements = [ + { + "name": "box", + "type": "B", + "x1": 30, + "y1": 0, + "x2": 80, + "y2": 20, + "rotate": 10.0, + }, + { + "name": "line", + "type": "L", + "x1": 0, + "y1": 0, + "x2": 50, + "y2": 20, + "rotate": 15.0, + }, + { + "name": "rotatapalooza!", + "type": "T", + "x1": 40, + "y1": 10, + "x2": 60, + "y2": 15, + "text": "Label", + "rotate": -15.0, + }, + { + "name": "multi", + "type": "T", + "x1": 80, + "y1": 10, + "x2": 100, + "y2": 15, + "text": "Lorem ipsum dolor sit amet, consectetur adipisici elit", + "rotate": 90.0, + "multiline": True, + }, + { + "name": "barcode", + "type": "BC", + "x1": 60, + "y1": 00, + "x2": 70, + "y2": 10, + "text": "123456", + "size": 1, + "rotate": 30.0, + }, + { + "name": "barcode", + "type": "C39", + "x1": 80, + "y1": 10, + "x2": 70, + "y2": 15, + "text": "*987*", + "size": 1, + "rotate": 60.0, + }, + { + "name": "qrcode", + "type": "I", + "x1": 30, + "y1": 0, + "x2": 40, + "y2": 10, + "rotate": 45, + }, + ] + pdf = FPDF() + pdf.add_page() + pdf.set_font("courier", "", 10) + templ = FlexTemplate(pdf, elements) + templ["qrcode"] = qrcode.make("Test 0").get_image() + templ.render(offsetx=100, offsety=100, rotate=5) + pdf.add_page() + for i in range(0, 360, 6): + templ["qrcode"] = qrcode.make("Test 0").get_image() + templ.render(offsetx=100, offsety=100, rotate=i) + templ.render() + assert_pdf_equal(pdf, HERE / "flextemplate_rotation.pdf", tmp_path) From b89147aabac22fdfa6dde8cbecae10e096a04ac9 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 30 Sep 2021 19:25:45 +0200 Subject: [PATCH 20/28] Issue #238 solved - split_multicell doesn't modify target document --- fpdf/template.py | 8 +++++-- test/template/test_template.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index a1ec45b91..1d122664b 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -46,6 +46,7 @@ def __init__(self, pdf, elements=None): if not isinstance(pdf, FPDF): raise TypeError("'pdf' must be an instance of fpdf.FPDF()") self.pdf = pdf + self.splitting_pdf = None # for split_multicell() if elements: self.load_elements(elements) self.handlers = { @@ -225,6 +226,9 @@ def split_multicell(self, text, element_name): for element in self.elements if element["name"].lower() == element_name.lower() ) + if not self.splitting_pdf: + self.splitting_pdf = FPDF() + self.splitting_pdf.add_page() style = "" if element["bold"]: style += "B" @@ -232,8 +236,8 @@ def split_multicell(self, text, element_name): style += "I" if element["underline"]: style += "U" - self.pdf.set_font(element["font"], style, element["size"]) - return self.pdf.multi_cell( + self.splitting_pdf.set_font(element["font"], style, element["size"]) + return self.splitting_pdf.multi_cell( w=element["x2"] - element["x1"], h=element["y2"] - element["y1"], txt=str(text), diff --git a/test/template/test_template.py b/test/template/test_template.py index cc16f706c..9d9b3b8d8 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -315,3 +315,42 @@ def test_template_justify(tmp_path): # issue-207 tmpl = Template(format="A4", unit="pt", elements=elements) tmpl.add_page() assert_pdf_equal(tmpl, HERE / "template_justify.pdf", tmp_path) + + +def test_template_split_multicell(tmp_path): + elements = [ + { + "name": "multline_text", + "type": "T", + "x1": 20, + "y1": 100, + "x2": 60, + "y2": 105, + "font": "helvetica", + "size": 12, + "bold": 0, + "italic": 0, + "underline": 0, + "foreground": 0, + "background": 0x88FF00, + "align": "I", + "text": "Lorem ipsum", + "priority": 2, + "multiline": 1, + } + ] + text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua." + expected = [ + "Lorem ipsum dolor", + "sit amet, consetetur", + "sadipscing elitr, sed", + "diam nonumy", + "eirmod tempor", + "invidunt ut labore et", + "dolore magna", + "aliquyam erat, sed", + "diam voluptua.", + ] + tmpl = Template(format="A4", unit="pt", elements=elements) + res = tmpl.split_multicell(text, "multline_text") + assert res == expected From 13e9739ab2b7b8de9a87be9314ed8cbdc453b83c Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 30 Sep 2021 19:35:36 +0200 Subject: [PATCH 21/28] Documentation details and corrections --- docs/Templates.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index 6900ce5e4..51612abf6 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -2,7 +2,7 @@ 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. -These 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. 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. @@ -39,7 +39,7 @@ tmpl[item_key_02] = "Text 12" 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 and other metadata for the PDF file. +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). @@ -117,7 +117,7 @@ Evidently, this can end up quite a bit more involved, but there are hardly any l 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.: +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. ```python elements = [ @@ -152,6 +152,7 @@ FlexTemplate["company_name"] = "Sample Company" # Details - Template definition # 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. * __name__: placeholder identification (unique text string) * _mandatory_ @@ -165,7 +166,7 @@ A template definition consists of a number of elements, which have the following * 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 (in mm), defining a bounding box in most cases +* __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) @@ -173,11 +174,11 @@ A template definition consists of a number of elements, which have the following * _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 in points - * for the barcode types, the width of one bar in mm. + * 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 mm for 'BC', 1.5 mm for 'C39' + * 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 @@ -206,7 +207,7 @@ A template definition consists of a number of elements, which have the following * _optional_ * default: 0.0 - no rotation -Fields that are not relevant to a specific element type will be ignored there, but if present must still adhere to the specified data type. +Fields that are not relevant to a specific element type will be ignored there, but if not left empty in a CSV file, they must still adhere to the specified data type. # How to create a template # From 058f7ea2f3629fb56737195a532a08081363a557 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 30 Sep 2021 19:41:00 +0200 Subject: [PATCH 22/28] breaking up long line --- test/template/test_template.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/template/test_template.py b/test/template/test_template.py index 9d9b3b8d8..f318cd7db 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -339,7 +339,11 @@ def test_template_split_multicell(tmp_path): "multiline": 1, } ] - text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua." + text = ( + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr," + " sed diam nonumy eirmod tempor invidunt ut labore et dolore" + " magna aliquyam erat, sed diam voluptua." + ) expected = [ "Lorem ipsum dolor", "sit amet, consetetur", From 580754894f398aeb560b6dd4155a98c26c51948b Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 30 Sep 2021 19:47:19 +0200 Subject: [PATCH 23/28] rotation fix slightly changed barcode output --- test/barcodes/barcodes_code39.pdf | Bin 1109 -> 1094 bytes test/barcodes/barcodes_interleaved2of5.pdf | Bin 1012 -> 996 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/barcodes/barcodes_code39.pdf b/test/barcodes/barcodes_code39.pdf index 9e76441552b27236566d28133ac1d0e6dc8888b8..42e8d02074685366f82ed6b92cb05db9b72f2c6d 100644 GIT binary patch delta 367 zcmcc0ag1X^eZ8rfogG(kNl|KIE?32z+9}q4E`~g=zav|{B|5$T^hc{`thg`K79OA| zwe7q#8yDB|vz`7*@#;?NmoxhRvhqD{aLVw2W7nK74;nRo)hzydWcyb+^`L(S##jCo z3;M(s_)hs=Rc!J7hN!amw;2zjK1j{D`8xa1mZL^8rqyS-w*RXynAq9Dx?MhX_4iIG z+rEacJ3Xb^zEpfFwYatWrGbb1Y&p@%;!lNE?Ay4CE&9l^u#TyAte49DHXHozygqZo zzKv0BQAg6|bll5(G~r&J)`_Wii%d6)HLa3+{_$CDqW!<7h$HXVPsIJ-a`7?uWVT}q za+g1UyX(vEt|nQXEwL}&?WyOBDYLwP`CHxH&%A4--fZ4Hi}5R?n4z(P0SGAMDR6-q p1_q{<7L%7UTWXtFT4KnV8yI1VnVL=JWO3#+wd7J&b@g}S0stWkp4b2Y delta 382 zcmX@cag}32eZ9G%ogG(kNl|KIE?32z+9}q4tcEVLSnmUo$?7YY5eN~^jfrSl?LZ>?$7&9;S_uGugAnj}h+`TTd;T;BL~{(DnC zmj4g$DgK&ooP46mTVd_B)d#D3eJ>;>es7$|J&#v8tCnxYlo|E272SJxAJ}3yr}9AU z!LQMguW}Wld+#nR&6DXmW-9l0wdbkbp1}pOv(Lyb*59^S`SqbLjS|7x8Hd7uB^1P3 zb{<(_9p~oo`jD4ak>Koa4cE(3nXi|Lx~+L5wYo1fJ>{Ckv)kKSYtJ#oAITGTS-b!C z%Wn%O|Cpn*MZRQz{(BR59$DWHzo+eywz^!mZ1v@jWp+PGe1E_GGkM-N|39ZcaBuEr z{K_b1WT9XH0t$HwTwsQQftjK4Ws9bw0$t*2y%wOu_s(^7lZ)rZgKvtKTSTmUh2WhVdt delta 288 zcmaFD{)K%)eZ9G{ogG(kNl|KIE?32z+Kak*&W=1R3Hz2y%xLiYvpMel;)y~6VwwFH zmIXyR7yZ@I)VR^kV|QA)l4rY8&yj3C!Gq-&<1!mX7MfXXeyl8YyY0ESoU3{2b=i9_ zejZ=-<=6SMJ)8FB&ASp)yw)eWSYqw_mhCB1W#1p$AbWq(TD`|su}x~(W((KbKbKpx zkMH{o*R}TLau%_o_tJVL_+#9D9(^M$d`8OnqREQ3kDblCo(n!sU-vNXUfse>)ybNS zri==k;}{PxiWwUy7=VC6o&pz`VPIftXg1k|*-FdM97D>?2vfP4xyj@i%+8$V=3J_( IuKsRZ0Pccs4FCWD From 557148abe837e8fef18c8fc0986a5026044d3710 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 30 Sep 2021 21:00:03 +0200 Subject: [PATCH 24/28] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 194a4819e..a6d217970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 (`

`, `

`...) 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 (`

`, `

`...) 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 (`

`, `

`...) 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. From ddba2ce2095fe032a6f4e8ec772655c4a6926864 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 30 Sep 2021 21:55:07 +0200 Subject: [PATCH 25/28] Include _write() in template rotation test --- test/template/flextemplate_rotation.pdf | Bin 10559 -> 10461 bytes test/template/test_flextemplate.py | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/test/template/flextemplate_rotation.pdf b/test/template/flextemplate_rotation.pdf index c8b4dfd1642967881915cd120506b0a13526154d..8983f30afcab99ca1a93b9cc5e67d92c9f5fa12c 100644 GIT binary patch delta 5276 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

p08gje1!co%&*|w4(gaL`AhMn?Io|r#os9n?= zeO9S;A8s%A-u@fWm>A)F@3*3w<0~zwu0l(y_O(9v+|+=HlyT`~_IwQcY0NNaKkgoa ze-}ecm0=>ULqB^uh$}l?s>!`KDD0&Pif2N@3P95uz8`1A=uatMRARH>HcOVvc2qrF z_e&B2C$)*l6ZRvTQ7ek7A=biuN=%{of8HlPr(tIz#PUEnsi9o1IYK$4^Td#Kc!(`g zDDD4H)^~!TE)PlT4g3*B;{F9*^WRndRS{%hB*EV*r64%|5|cD+4UOa)v;&jkKjmcJs{lHzigCCy0<@$DYG4Y*P@-X_yp zNn%7x>l{TKOt?*=Oww#Ak}JD9;MySf^D>oKP$N!JlqYy;vzH~R{@@+}4fOwbiHBMC zGcI6H*CW3nZ3uNnS$wjC!&c8ER!AwL+ci(*$`38-W77vDN6#n}B0VWl(6Q-1jLVH* zlndRomc#=57FqbcR;t`;Geb(l%(mwmX_xKiU#J~cM8GXIu;!Qnqa0QkQo@V7yLlI#C*A<(+=R6>Ls zBknxik!Br-CdoV4BZT>gBEr5=*)7XEwwjNm2_HV^BiC#@Oks^fC*f?Vyv6(@M&{Us z7kjQ{+fh|_@O*DX75_{M)I1pce&FugL;|8pu{bDHaL4(_Nv~H#Lk~IdUtAGswLbW*;0v?MeR3EPJcfUReY36 zlBszmmVPTda0q)Y?yyBo*$JfjCt7d*!07YMN6zw}$Gpz6*hbz|jV``MWl*z3ia%`S z7piG|l{aH@@v7G$*ogA3t;Z2R-vl{`{rqc&R|7gko}B{v{SaXt&c^5DucO+!_6Z>O zw4dPs&=cpmfnmKQkr$5ba5cg}X~nf6hH_fbc68b8Dl1-aJurV2{?Zt2#tcxneOB1G zC`>v?%^KCYKe2nO+x8ALdiPOaQ*5}N$+mbyO{9sr&UxwuJ)^TS!c0(mbo~3LlyVL! zB#gG_xCv^8ZpZl#yPC1yJK0~$5~m9Ka#8qwm-pOsc;%L?DIr!+fg--`z3l#GHQ3v_<$YLV) zs*KqL7Ar6>K+l}i6f?3YTKwW^#@H=tY&Ak`s)kmk7JKbc?pR9@f#1YBaX43e?ZJoP^vdCPb%@ov(1OuVw zwnMk*$%%}>_?cOS-YW3BOSyScf=&d-FH(nAM0*!O<@>p?>!6Y<`uBf=slJ|jy>&ab zv@ICn)v@KC=Z;(CzUZ_yX-XyG@iuGg)YOhqin-f! zu(@{fb;ok>t{jcLbbXzcmiLwp6T28z1q`dfl0h*47i|0r9srEw5zNbDGOBO%F(xf9 z%%vIT4DFyL@9SgE_?5!RI{)deV?>$kww55J)AGAl%qz6KfmxrirOvjW&-iMyeLHo! z9CV}?hAwsUO(kR}QeC_qo|jKl;WD(r1pR*6omDZH{L)7ylCKNge_nMcLC9BcweU+@ zak?;(hD&nFw&9-TPO};7^DShkvb7!kWh}Dy4Yz>coY%!BGT90@RmqYz8Z~AkbDL>* zq%~^VIcFvcOlE4Ss_azk%u1W+bl&ddnF;Hq#YHBTQe9X!wHt6H)($1LJpw2CgFnqo z*HT%uo>t!gsmqMJw6uVQYKg$!Uzga7z_nU#e;{L_DJ>xR^_`ybvTo@$?s&^YD^YtL zpfq_nSDpkzYYWl&QRL6(gVt+%QW?R(>dF*XJF>_*^5oJ7(UxNFE=CpkvoNWS3G|&W zOU}x+<{K+7vA=jK!frAdob9zv$n*P!&g`I>0kbN)weFH#9=!_=S}kW0-Kor`>T$C9 zbW#OH4x8`c7Chm6b<{sgOSzvN$Gn_0M_oKdThbh<@Q|}5Su4peJG43|O+EUU-RU)= zfM3}IPCB{b%bs(3kGlCx6zxt@1YI07t?|*ehpC*&ch+S-K?HDumN8ekd!pwiS2&?@ z>Q-y*;kd}mDWAq3We*vmM}A{ZiH_BDX|ff8IcU=rrRfBEj4o-i$#=}i3=RC&QJ*Ul z?#8L}IMFF!xtV|w8eFS{&I@0Xu@&)sDo2@jH<*R0b~lhnNbxM6VQ=1Q6-Qvo3Wx)$ zqgYw|xd))O?V{VSzWuRKh|jxT>=_-VuXdz*?+df(?%kt4!MiS0bKa#SaHd2%jsWWiOTcc&6I)Wb@1|B)HK0P|KX*#;q6I`NKok4eq4Z~vwnoXnV$I=n*zQ!0+Q8MsKR-R*s zF^!4`UWLWuF?OG^b|1=&C7GOm%me>lV!pIhoY2&8BxkZT-drsSXmd8ortasf0A3|# zAhIJ3F$2>NfOLcpCj@81ih))#oDe>lGN9UhN$Vjn;4GL;J;+xEv~5J6#Efl3dvYFs ziQdO~%qLR;m`FSR{Cvsyw4{~kAb6FfmvtlqabIef_1zWT%lC3Z$T;Zb?>k`x|Ll{{ zDwhsJ=kFmQ%i!+@)W=)Ip(qGrmVOTRmjEZ`BXV|-=5PU6`o^Kg_A16A8uWt_G zXCLRk*?gGu6`fwY^Q(Ub6*#YG+fAd+h!8lBm3Xv80d0Ml*N1EWHcRC-~A(Dyk1C#VwMGZB12yb4>;5pICjTAg)>`RIvW^5xxe%~)W$ptbR z-%4{CP}wCv(Qq`_CwX=VQqO733_61Da6b%(8RMy^HVxqosRuL_tpM7-$R`G93ya5L z@`abGl&4U4J!~ldis$)=iotK~N@Azkl^84OIgp09uVpaokP!YsduMir^mwF!&Q7lE zdc6*Pt>`KsV7MtZoU{Kit5(0*8v zB!B+`DjHf{kl@$m#rqc?3#vanGx!}Ue2JYi>OLtS?wr2*A9G-Dau$W{1j=HCGNU40*iGQ2r&8!gq7LTCJq zBLnYm+*PU78ap404Ym#lCq8*Vku$oAjiqKJ-60&ro#^vbNh}v&DX%Y@B z?*#It-s#S0y%lo2AKs@sMu$4_#u4ca@}Rv8Z95|m0YXJ5hPKtl_v-dIush7yJR(3( zG;DA_4i$aw2Txed1`#o#0~-5o~Uo>l$Ax!B5J180s2Z zD9N9~WBs8}_%x68r>bI=zp;)|5(6OC>stvsjJj72kox_da=tDR{EHgY<#PE|~!1Zt0>1b-g`0(qQ>^rk@|qlFwVBNyB=RNAKQ77~ce z+f-Dv0SN_18SA?t!+3NYcR+@1uugzUpnvPI3LHfJH=hnSD%QlYWtOG*6SF3nx-j3=4L*7t_IxAA>I}RcTsL3z*_q6>lYes~}$ptztL<(~J+vfu7BIOP3;^)b| z7DIC&{_m#4ABF>7k`SoKhAswusf2$siM^GSwOV(W{n_XOe?N@>Z+11q8)MngshcFy zj|@rYNHwq2TzL@WD7|xafQ*}q34AsZM)a>ZIUN&lx;IQ3vIY&G9Vo}vxKVoFc$+pm zAVijjRm0E`;MZ5;pWKLve1Xz)nxIg}*9PKiH$dPxR)6GxYnQu=S)o}+(hU zJoDu$`YeaGjYUep4PN?Z{=U%nlkExfEaE=Eh7ggK&6koH2b}hVeBdKqb+_oSjq2>I zM9Y7xuE52}vZD#rrTuW^e>78n0b-W7>DwNziX@@0$Grabq*g!TtykV%wry65#D z9jd^}NP}C%3f>?17!0CBXDw$@=j5Wu?4Le0ii=2s!T$75VKhBPRxOEj=#-Yil&_pg zL*T=-r8QPamQj#ggzflZ+6OnU2<~!>ml^GPXK!7)O)3nKM3p%A(@qFXHCa;AiffezC z^mkO_mTYo@SvbxlLJzSP>(x$hFO?T*Wy=nFnfZQua8BC*LJFv*{};z=5)Ph2piXuE^}`&kMG% z{UDB3R75K9env}?^!1*)>Q{Rz>R-jF pE20$s5T~f1psdc@6Z>6JiVBK~==*mMpA Date: Fri, 1 Oct 2021 14:49:21 +0200 Subject: [PATCH 26/28] FlexTemplate.render() with scaling --- docs/Templates.md | 10 ++-- fpdf/template.py | 75 ++++++++++++++++++------ test/template/flextemplate_offset.pdf | Bin 1159 -> 1197 bytes test/template/flextemplate_rotation.pdf | Bin 10461 -> 37173 bytes test/template/test_flextemplate.py | 12 ++-- 5 files changed, 68 insertions(+), 29 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index 51612abf6..2441d5ee8 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -117,7 +117,7 @@ Evidently, this can end up quite a bit more involved, but there are hardly any l 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 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 = [ @@ -133,10 +133,10 @@ 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" -templ.render(offsetx=120, offsety=50) -templ["label"] = "Offset: 120 / 120 mm" -templ.render(offsetx=120, offsety=120, rotate=30.0) +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") ``` diff --git a/fpdf/template.py b/fpdf/template.py index 1d122664b..b9711865f 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -256,6 +256,7 @@ def _text( text="", font="helvetica", size=10, + scale=1.0, bold=False, italic=False, underline=False, @@ -285,7 +286,7 @@ def _text( style += "I" if underline: style += "U" - pdf.set_font(font, style, size) + pdf.set_font(font, style, size * scale) width, height = x2 - x1, y2 - y1 rotate = __.get("rotate") if rotate: @@ -341,11 +342,23 @@ def _text( rotations, ) - def _line(self, rotations, *_, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, **__): + def _line( + self, + rotations, + *_, + x1=0, + y1=0, + x2=0, + y2=0, + size=0, + scale=1.0, + foreground=0, + **__, + ): pdf = self.pdf if pdf.draw_color.lower() != _rgb_as_str(foreground): pdf.set_draw_color(*_rgb(foreground)) - pdf.set_line_width(size) + pdf.set_line_width(size * scale) rotate = __.get("rotate") if rotate: rotations.append((rotate, x1, y2)) @@ -360,6 +373,7 @@ def _rect( x2=0, y2=0, size=0, + scale=1.0, foreground=0, background=0xFFFFFF, **__, @@ -369,7 +383,7 @@ def _rect( pdf.set_draw_color(*_rgb(foreground)) if pdf.fill_color != _rgb_as_str(background): pdf.set_fill_color(*_rgb(background)) - pdf.set_line_width(size) + pdf.set_line_width(size * scale) rotate = __.get("rotate") if rotate: rotations.append((rotate, x1, y2)) @@ -401,6 +415,7 @@ def _barcode( text="", font="interleaved 2of5 nt", size=1, + scale=1.0, foreground=0, **__, ): @@ -417,7 +432,7 @@ def _barcode( None, pdf.interleaved2of5, (text, x1, y1), - {"w": size, "h": y2 - y1}, + {"w": size * scale, "h": y2 - y1}, rotations, ) @@ -430,6 +445,7 @@ def _code39( y2=0, text="", size=1.5, + scale=1.0, foreground=0, x=None, y=None, @@ -450,7 +466,9 @@ def _code39( rotate = __.get("rotate") if rotate: rotations.append((rotate, x1, y2)) - self._render_rotated(None, pdf.code39, (text, x1, y1, size, h), {}, rotations) + self._render_rotated( + None, pdf.code39, (text, x1, y1, size * scale, h), {}, rotations + ) # Added by Derek Schwalenberg Schwalenberg1013@gmail.com to allow (url) links in # templates (using write method) 2014-02-22 @@ -465,6 +483,7 @@ def _write( text="", font="helvetica", size=10, + scale=1.0, bold=False, italic=False, underline=False, @@ -488,7 +507,7 @@ def _write( style += "I" if underline: style += "U" - pdf.set_font(font, style, size) + pdf.set_font(font, style, size * scale) rotate = __.get("rotate") if rotate: rotations.append((rotate, x1, y2)) @@ -510,25 +529,43 @@ def _render_rotated(self, pos, func, args, kwargs, rotations): self.pdf.set_xy(*pos) func(*args, **kwargs) - def render(self, offsetx=0.0, offsety=0.0, rotate=0.0): + def render(self, offsetx=0.0, offsety=0.0, rotate=0.0, scale=1.0): + """ + Add the contents of the template to the PDF document. + + Arguments: + + offsetx, offsety (float): + Place the template to move its origin to the given coordinates. + + rotate (float): + Rotate the inserted template around its (offset) origin. + + scale (float): + Scale the inserted template by this factor. + """ sorted_elements = sorted(self.elements, key=lambda x: x["priority"]) for element in sorted_elements: - element = element.copy() - element["text"] = self.texts.get( - element["name"].lower(), element.get("text", "") - ) - element["x1"] = element["x1"] + offsetx - element["y1"] = element["y1"] + offsety - element["x2"] = element["x2"] + offsetx - element["y2"] = element["y2"] + offsety - handler_name = element["type"].upper() + ele = element.copy() # don't want to modify the callers original + ele["text"] = self.texts.get(ele["name"].lower(), ele.get("text", "")) + if scale != 1.0: + ele["x1"] = ele["x1"] * scale + ele["y1"] = ele["y1"] * scale + ele["x2"] = ele["x1"] + ((ele["x2"] - element["x1"]) * scale) + ele["y2"] = ele["y1"] + ((ele["y2"] - element["y1"]) * scale) + ele["x1"] = ele["x1"] + offsetx + ele["x2"] = ele["x2"] + offsetx + ele["y1"] = ele["y1"] + offsety + ele["y2"] = ele["y2"] + offsety + ele["scale"] = scale + handler_name = ele["type"].upper() if rotate: # don't rotate by 0.0 degrees rotations = [ (rotate, offsetx, offsety), ] - self.handlers[handler_name](rotations, **element) + self.handlers[handler_name](rotations, **ele) else: - self.handlers[handler_name]([], **element) + self.handlers[handler_name]([], **ele) self.texts = {} # reset modified entries for the next page diff --git a/test/template/flextemplate_offset.pdf b/test/template/flextemplate_offset.pdf index d23f4d309dddf4ad9e24d1c40b4d6cae5e9b3f68..adb42f52f0cac5c54e538a99c12494925385b91a 100644 GIT binary patch delta 481 zcmZqYT+2D3zTV8p&W@|Nq$o8pm#bpV-r&=DhYSSvJlB49!z(3nzi^*N?$S6duR4an zrVzoFk>F^m65oa||Wh^ZC#8EcbDFGyiw4%3ik&N56xH zlQ(#5w|Q9C{By^KMjeq%?qse@T-G}JEuAk~Gz~bOi!SSDQmSVQF_l{tJVE8Uesc?V z=#?KXuhY2Yb+_>G^-fEXU1fe}@0G9DpBXeK`W(J!@_ogy41$Qc8lth75Yn)lY{GJgC9=VexbZnFMC02JS(TR_oUSO zp+ahlg!!JQ`#pZ0lwejD4Pq9aQ`1#5e+7hQ66=II0j-~6VhRa<{K}+HeWx}Rf85aA{ifcieD5_aHU7Q(D&I|6efsY0 zxcR@sJX0Oltcw=WRpt?2`d419;;+RTzPeD6|2^w_zklS`p2+3Om713V4ED{}7+*1p zn;Kdw7=VC6o&pz`VPIfxU^w|Yv!%9~fhmTZ1vW8L%gNR(&YWgOT&k+B{%%|Vn-j@R delta 436 zcmZ3>+0HqkzTU{h&W@|Nq$o8pm#bpV-XL$k!v-REo{MZ|>D=+p`sF4eg^iA_F$|7D zoTjTZChib(KT;;6p4&UiclSqclh1F=^V=S4E_RXpbHhpVX!5a|x$}Pavg@{cy7e?H zoMO=XVS4+n^&7o}qdzzatXvr4xa5G?diNs?E=d_BM?zbVGBekI<_&HBot5+Gh|h6_ zGdnb;Y8HlXZhV^-WYy|bw1{^V{~a6iv*mYn7ix&T%eWmKGXGoJn*QgD_RR^G>IiJO zAZH&c)ljHfyI7fzN3~O+IF&Pcht1TZ6+zP{)NOn&YooGl0;{s?%mM>}#m$Wtg$71x zH&xg8r_S7T+Vj_W{c_iZL8hYHv|j0*ZtrK5IQByP-@T@TP2bKiPjc5d&3AI8?b^++ z_sE)CbzV7ozfV&(M?N^~{=O@VKR3xUP2#ANpJ4a1yy9kQe(c_s1^bg@-h8Ron83JW z^H;`?jN&F1h6)BCppd7)1!fo+n3)+*{>yBsZE9wYA!m+7%)-cUaw3Z}r;!Pls;aBM G8y5hk=CgYM diff --git a/test/template/flextemplate_rotation.pdf b/test/template/flextemplate_rotation.pdf index 8983f30afcab99ca1a93b9cc5e67d92c9f5fa12c..26145af30efdc1f70828f31e707fd3601af86e0b 100644 GIT binary patch delta 31361 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 F{{TR*UV{Jt delta 4461 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 B$>9J1 diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 9b9b031ef..bb07f72ed 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -50,10 +50,10 @@ def test_flextemplate_offset(tmp_path): templ.render(offsetx=50, offsety=50) templ["label"] = "Offset: 50 / 120 mm" templ.render(offsetx=50, offsety=120) - templ["label"] = "Offset: 120 / 50 mm" - templ.render(offsetx=120, offsety=50) - templ["label"] = "Offset: 120 / 120 mm, Rotate: 30°" - templ.render(offsetx=120, offsety=120, rotate=30.0) + 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) assert_pdf_equal(pdf, HERE / "flextemplate_offset.pdf", tmp_path) @@ -206,7 +206,9 @@ def test_flextemplate_rotation(tmp_path): templ["qrcode"] = qrcode.make("Test 0").get_image() templ.render(offsetx=100, offsety=100, rotate=5) pdf.add_page() + scale = 1.2 for i in range(0, 360, 6): templ["qrcode"] = qrcode.make("Test 0").get_image() - templ.render(offsetx=100, offsety=100, rotate=i) + templ.render(offsetx=100, offsety=130, rotate=i, scale=scale) + scale -= 0.01 assert_pdf_equal(pdf, HERE / "flextemplate_rotation.pdf", tmp_path) From 6f2c98ffdfb0e2785b2885e12c1688a23d86f372 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 2 Oct 2021 11:45:49 +0200 Subject: [PATCH 27/28] empty text field - consistency between T and W --- docs/Templates.md | 4 ++-- fpdf/template.py | 2 ++ test/template/test_template.py | 13 ++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index 2441d5ee8..82cb214a2 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -164,7 +164,7 @@ Dimensions (except font size, which always uses points) are given in user define * '__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 + * '__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 @@ -193,7 +193,7 @@ Dimensions (except font size, which always uses points) are given in user define * __text__: default string, can be replaced at runtime * displayed text for 'T' and 'W' * data to encode for barcode types - * _optional_ + * _optional_ (if missing for text/write, the element is ignored) * default: empty * __priority__: Z-order (int value) * _optional_ diff --git a/fpdf/template.py b/fpdf/template.py index b9711865f..9b436eb8e 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -492,6 +492,8 @@ def _write( **__, ): # pylint: disable=unused-argument + if not text: + return pdf = self.pdf if pdf.text_color != _rgb_as_str(foreground): pdf.set_text_color(*_rgb(foreground)) diff --git a/test/template/test_template.py b/test/template/test_template.py index f318cd7db..98a211699 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -189,9 +189,20 @@ def test_template_badinput(tmp_path): with raises(KeyError): tmpl = Template(elements=elements) tmpl.render() - elements = [{"name": "n", "type": "T", "x1": 0, "y1": 0, "x2": 0, "y2": "x"}] + elements = [ + { + "name": "n", + "type": "T", + "x1": 0, + "y1": 0, + "x2": 0, + "y2": "x", + "text": "Hello!", + } + ] with raises(TypeError): tmpl = Template(elements=elements) + tmpl["n"] = "hello" tmpl.render() tmpl = Template() with raises(FPDFException): From 2b2d82e3252e0f13283e9bde101b868cfeda595a Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 2 Oct 2021 13:04:54 +0200 Subject: [PATCH 28/28] Enforce user input types as early as possible --- docs/Templates.md | 2 +- fpdf/template.py | 55 +++++++++++++++++++++++++++--- test/template/test_flextemplate.py | 7 ++++ test/template/test_template.py | 22 ++++++++---- 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index 82cb214a2..5dddefeef 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -207,7 +207,7 @@ Dimensions (except font size, which always uses points) are given in user define * _optional_ * default: 0.0 - no rotation -Fields that are not relevant to a specific element type will be ignored there, but if not left empty in a CSV file, they must still adhere to the specified data type. +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). # How to create a template # diff --git a/fpdf/template.py b/fpdf/template.py index 9b436eb8e..84a6e20aa 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -69,6 +69,28 @@ def load_elements(self, elements): elements (list of dicts): A template definition in a list of dicts """ + key_config = { + # key: type + "name": (str, type(None)), + "type": (str, type(None)), + "x1": (int, float), + "y1": (int, float), + "x2": (int, float), + "y2": (int, float), + "font": (str, type(None)), + "size": (int, float), + "bold": int, + "italic": int, + "underline": int, + "foreground": int, + "background": int, + "align": (str, type(None)), + "text": (str, type(None)), + "priority": int, + "multiline": (bool, type(None)), + "rotate": (int, float), + } + self.elements = elements self.keys = [] for e in elements: @@ -84,6 +106,11 @@ def load_elements(self, elements): e["x2"] = 0 else: raise KeyError("Mandatory key 'x2' missing in input data") + for k, t in key_config.items(): + if k in e and not isinstance(e[k], t): + raise TypeError( + f'Value of element item "{k}" must be {t}, not {type(e[k])}.' + ) self.keys.append(e["name"].lower()) @staticmethod @@ -555,10 +582,12 @@ def render(self, offsetx=0.0, offsety=0.0, rotate=0.0, scale=1.0): ele["y1"] = ele["y1"] * scale ele["x2"] = ele["x1"] + ((ele["x2"] - element["x1"]) * scale) ele["y2"] = ele["y1"] + ((ele["y2"] - element["y1"]) * scale) - ele["x1"] = ele["x1"] + offsetx - ele["x2"] = ele["x2"] + offsetx - ele["y1"] = ele["y1"] + offsety - ele["y2"] = ele["y2"] + offsety + if offsetx: + ele["x1"] = ele["x1"] + offsetx + ele["x2"] = ele["x2"] + offsetx + if offsety: + ele["y1"] = ele["y1"] + offsety + ele["y2"] = ele["y2"] + offsety ele["scale"] = scale handler_name = ele["type"].upper() if rotate: # don't rotate by 0.0 degrees @@ -624,7 +653,23 @@ def __init__( creator (str): The creator of the document. """ - + if infile: + warnings.warn( + '"infile" is unused and will soon be deprecated', + PendingDeprecationWarning, + ) + for arg in ( + "format", + "orientation", + "unit", + "title", + "author", + "subject", + "creator", + "keywords", + ): + if not isinstance(locals()[arg], str): + raise TypeError(f'Argument "{arg}" must be of type str.') pdf = FPDF(format=format, orientation=orientation, unit=unit) pdf.set_title(title) pdf.set_author(author) diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index bb07f72ed..fc9854c4c 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -1,4 +1,5 @@ from pathlib import Path +from pytest import raises import qrcode from fpdf.fpdf import FPDF from fpdf.template import FlexTemplate @@ -212,3 +213,9 @@ def test_flextemplate_rotation(tmp_path): templ.render(offsetx=100, offsety=130, rotate=i, scale=scale) scale -= 0.01 assert_pdf_equal(pdf, HERE / "flextemplate_rotation.pdf", tmp_path) + + +# pylint: disable=unused-argument +def test_flextemplate_badinput(tmp_path): + with raises(TypeError): + FlexTemplate("NotAnFPDF()Instance") diff --git a/test/template/test_template.py b/test/template/test_template.py index 98a211699..42de60cd4 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -65,7 +65,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": "Lorem ipsum dolor sit amet, consectetur adipisici elit", "priority": 2, - "multiline": 1, + "multiline": True, }, { "name": "box", @@ -83,7 +83,6 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": None, "priority": 0, - "multiline": 0, }, { "name": "box_x", @@ -101,7 +100,6 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": None, "priority": 2, - "multiline": 0, }, { "name": "line1", @@ -119,7 +117,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": None, "priority": 3, - "multiline": 0, + "multiline": False, }, { "name": "barcode", @@ -137,7 +135,7 @@ def test_template_nominal_hardcoded(tmp_path): "align": "I", "text": "200000000001000159053338016581200810081", "priority": 3, - "multiline": 0, + "multiline": False, }, ] tmpl = Template(format="A4", elements=elements, title="Sample Invoice") @@ -182,6 +180,18 @@ def test_template_multipage(tmp_path): # pylint: disable=unused-argument def test_template_badinput(tmp_path): """Testing Template() with non-conforming definitions.""" + for arg in ( + "format", + "orientation", + "unit", + "title", + "author", + "subject", + "creator", + "keywords", + ): + with raises(TypeError): + Template(**{arg: 7}) elements = [{}] with raises(KeyError): tmpl = Template(elements=elements) @@ -347,7 +357,7 @@ def test_template_split_multicell(tmp_path): "align": "I", "text": "Lorem ipsum", "priority": 2, - "multiline": 1, + "multiline": True, } ] text = (