From 71bd53d7d1b0e3a66e4866d797267834779f32ca Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 18 Sep 2021 16:33:43 +0200 Subject: [PATCH 01/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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 zcmeHNcU+Un)?P&c5rvgiFM^O&tOy~200BZ1kS0;88j2VKhLS)MDWQp=sH}nrA}zQo zA}bk(Wpq+&&$RdCYl|~`h zy6}0%Wkt1<+GY4JO)P$s&pIF7d_{@1P5E>Q>1zM_XL3?V34_@CZ$?5h4wW(_i;D#B zUk>u=#U78?Z&sY614DmkWTkcwWUf1KjQ0fQMUt5ib8UfKhEr)*Y0~D5wvD{}#T7-5 zCyh>>rS9YENA1|CC3fu+M@)iuN%yRG!{^iEH)zy_kharil8UCZtbuy5;+a_FFi zINjZ+5wrQJT|KuVsXIH*skaMtFZ~<2oU8d3Q0FrG%vtW|2685BZ<(vdHW{P`iu)>B zA)6(lTQ|CV(;z3e^ALS)c#wvXP^7Lh%*a&jMCnNl{bQ1w*s23VU*9dv0XAJ8-gYBXYDh0nj zcXf85dalH>n%HEU9MfNX*90f-mmohfGw@L<>i&7hp|v}Njtv`4^Ueyiv&V?c&KlIF z?_~^7S;b+u+%iR;bSIjp7n`}~$nY(?ur8dMU@@ zT@Hp*vq<-cX0Q34GtNjJznYPS8uVR{qorQJoWi{y<9D9*imr>YJs%|C?5QOwbkb#cpBMq3_75Jo zTxsjzeC#2gQKWLuQ%#{b>E~O_4@`zDs%@1sOB1Q~kxp$s>qvDJ%FH1Uax^9Qi6w7kxZ?V3+py7v|_ctb47kZ)W_W^~U|_rd-|iRRscB%T#(#{PME$7olUwe(W941R9*H!z<8p3zZo93y|=u_^4v(0taj(Vt0 zcr>fE;MK(6^`36!8H-AeE_I~gGjm7safzx?nw^ay3xbZR*>PDSFP^wwTfk?D!3#E; zdET2RoE*7_iy*y+>tuJWFD?2C~0+?-bNrNyD@ zky^6qCwgI}8}Zo@|A3M-xzteR#}mGvO4<{;{OAkwV~eW6>BQh8vuy&^8H+o*@|hnu zFPlaj4?26MB{oYK2!I~qI1vWj}_`9l-Cq0xjsC9huMD$?S{dWTo3J|@4Enapk z?rm~2wO__^{#*Z1g0p2#CWm#%#eF1wXAHvM5wC1z8D_J;lrvgmmE-P98fi?NeztR@ z{nFy5sEV=6wI%x_f**{zSG1$tB*|{6742DS7US8m79@6$bxWu+!QX9QR(XOMV#4fF zvfMb}?l9tNkT8Q27^BZskudx{OV7Y?L9>!Jn?3GI7tXk9PXVJ{wS}YNDY=sq>o!_M zkl0ciw$l{`uu_)CZ>ie6clptyGaU4AuVG=2s#if4<|ZQqwtDU&*bYQ2Qhk4t+UX`0L6 zEl_kaOibD()E$eKQL6srZ502Yc`sTwX-Fp;HPc#E*j@F>emZoVWUG1m^sAuy^;W7k zuzf;LvEOk!X-uo!6;oqw-D5Qp<>}t8e^B%&W>7A%2_GJ4iRrE~4sAMoU(D6R0Ann` zq!&rSIl36*&`5ewV5B9+_9ND|EyqBF&G|C9RB?+4{;|Gb&S-X|sbUPhj;NnpD#4RS zFN#hSOAgk>bdTSn-xqV|oX1%Y-vA{^&Uqfm9D0H4tjcYAH<2&7bT3a1y>7Yp5zOFn zZwlwmO7GpAJ3NwkbR%)QYnihKh9VZmJ!gkCk`5$_-4yK`UgH4i-o-i3!~xxJnz=|` z$wxpbrHM-{g*7~Nc%nrh;{nla$~fck{TIbeTv!ubj66EGIk4ssa9(9t!y~6@Hz({7 zPscPsXE*0>$vl`7dCkIIQwh?$hT*o40sh{Fs*kyCO{jJ6y(dGxxK$hkqU3yj2J zQa8=!qsEZmxmg!}5y&$a^Lkc0CDF|$4mT{sryIp?s%ZD&qk`I0whG0y*tcw7estdf0_>hdANPf%TLE^kU0lTrIPUO(H=EXx>B738 zl)}GGJhhLzLZyMJ+UK!wd$s*7kJd!q6Xs1wiU{~FJWPa_aSABA%)f4e4Ah=c5X>JU zz(6SjDd>}%A#(P;-^rQR*u3G8$j1~#uHu<*WW$bbap(Hpq*5Izcr&1G5%T68x2D2# z;6u5(J-4QS1%>NqV{c1HusB`eWut*4uKz6e3-;>VdE1*v2urYsiHON zlPa(H?QRBjs1G)QX?SS|qxnOgJrAfm3J3lm!=eM~N+Mb6?f8iXlZj;*mtnXO(7g=9DEEaG7|gjZ2w1pty?Uai-{og&lj0m%BFxK3x*gygo+iQ@4?%vL zKWH3+{H}uFv(JztWd-Q~^0HdeR{hrbJ&aB-!lPAN*r5*X;HO(JiJX=&IMsGASXNL4 zi{NULuvHLL$^vtP5a#XZ0xoKiVYEizVi>=jJ?x!pqI})hcWERt`)VmNOq+q^MjB|k zzycvKwD7RNWyncf!g8OdSDqXBx6~$X{S0N?gyftkZ*Md?MhHM=F?Cz*Ct?7PVEWv zs!-j&nek8NX^h%)z9h0kedkZcgLy88D>I;bC0ru|x}k7Y z5T|zd3c9a$0C%oLoWrxLtdAN^J^brxO{&!8_1GJ+=6kXv(ktEv~_?RW>mLV_z= zm=^=KD%#8&=2I1m>-MX|OrLZo_KBJ!xezwA`u$+|RN>3UQy)inne|Rve!-?-DMIo= zOn}2cQ#9&JwqKeoN~LzI$*Om(D(;H#iLivV!uJpKia!#qnlE;^x7LhNGX`oO$~}uVscJ zx$2}2hiQD8>$w4BL6?TT^1dW{kg|+#z`%39AY+nEa!_ziu*1OFyH!gzF&>1`ohh!5 zc;ZijY|OJMaGV6?Hx>fV`aynkWEf#AKu-&Qw9iVj0o6bMmLkWw(->hKs<)ZKIlr7~ z-JlmOXIi7fYgRHX^Wim6rsXW~@KK}CFe3`rb9&X_U9r(IO(O56Tazx{OXi7x5J5Mj zfSfN5GVXE{*RpZ876Q4SjY~Hc@C3${)=CfGcUsI~n4S6(F$dw-iU}SrxpQWljD~B?;H5U#XRDnC~xlxj1j>MStsu6qqufLLQe6WlmiAP_ccph`*m#(}%!Djjp5; z#_&Ep6`3&)X`^zV+xKRdhR-MI`$}K58G3)O&4qe#aqb4yr14xc>vgB6*V|7;qR&Tq zJ3d_$QZbXpPQJTWYLqteV$WNv_->Bq;(T4`Saq$fs#2lc5v+H6=l49b1EuHO!=vvd zVd&e*rlDhQSGS+vJ}^im|ER+(eJ>y6@?caHGe#MV849t}XYwmNaVxK_Z&YQvyi?1@ zZcjDnsHDED+%Ug=Z&OXFL3J`S^>u}La&4lFxe>gRd$8l1@{0;CQ_B6SqH>WF12)pS zPCKS?!6%){V&TEIqwW>yhcip9UrdxtLuCnb-YTq*&mE<%P@KGoz#N% zaC4l+F{gT)#&gDZoyov?bm%Yopm*#moVk3P)zXELTY#>H%##}1NsoTaH5rew)$L~< zIMPaPU9$9gL$c0`Cl_AyL07G1$H&8~)2IV@EerP%#9hh4Eb^%+a7(mWtck|f+s#qr zPP(&I>3f)2p?fQ%b~{WH3h%;fuE&)#-pa43idr0xhLdKSZk0uk)#uDisXYICD;*dHOZbZ zoD66}p;6LEMVO)-LRt}R#rHE$s_`bPZxZ2_YC9FkoC*ldlM>K)^L>l!NfpVDa z;z}k#^s^!#{10ekJ|F|p$$+DFp~3)heT)K7$J59Z7y<=B2b5D`vamnUx7>Qpg#?If zv<#RIxXDplsuK(WO%~v}SLkV3gu<`*>9IJC)I(_glBGpBh8n1{4r!Yh!@}K)Y)GyD zv?TQPZU1&|t%6XLP;t8bF2xqZb)_*^x=+Y{E$X!Q0=`XNR1tN%cc*F;+!hs*?M>bL?{l-#J%HcdH~s-h7VB#L?*4_S;<EDvW{>x_F}&BGo{9% zwte}qDIdld1behmt!mHuE9@C(haK^$H8lGB4RBqR1PA5DLE5FYep6hN|=S9?GlXI z%s(D75&L0R`Ez(pLD`%&n06h?L`Wu_QV>-VbL|Fep?5SSZlWJVhQw@v;=(SrH#(*5 zZ+}Afd++wi7V5A|zcu*~wDm+L`9~r1)71snuqOgg3-YSA>sva_Y5CA(G;`CUYv42SAWp?zqFwdKD?aO7>y&N947w zlipM}^R~BwXWUe^mQulTUK_o2a zhHj0socrfyeRFye5+Vq+5D@X-qV%1b%X?=N!-rfq<=q+UF zP4Of9{zRb&KT%w2jcbAx*!~#GR*OAg84yGrw$uW9BLBZt2p-uYm*XLMO$*)g%3G+v ztody(>^ifyhr5QFM0pW1ySL)TqpVjNq8R_$U0x)_;t(2Qk#2o7eq$#!Zbpr#iLJl&oyU%Ri1&a|32r$#Ijt&#zGW99`T5OFP)ETeuhxYxY(EQwh-FpF@>DJ^)6ip2$pp9-nyy2^2Uiz?xzBGz{-BpS@REu00(Zlw{oPi!*Z`Y=e-kSrsHwY zS>w&lh|bS3$)nzaMvSktpYAm3uQ>_X^=ArsX4vDP1WPoYA!HGM0?Mzb34=TTPrf2r zMzzDe^V)!)VEciNSeA1V#bF14`MW-AQnpE+EZCyJ=?qtBPi94P1C)2?Ma2Td48g7h zPU^~P_k$h}iorbUj`!OGSR2941x{^BW_(3F(e*?o;t$m)W@{A${gtWszfAv^c*cuP zj0iZR5fz*m+X zpc(k!dI%UqeJnTp%;g|{CW-*F0iLwch-8u$c=QI_ucd@Q%cBuUSp)(t1AYg7LDC?8 zY4hi|K!TUqf3j@;Tro zmP7#T2jDk;F^(sNV9zH5L&)+$7JUA}w^Sc&_y8IuuF<@MyoSL;p@6N*?{YegIamNB&9{AtQtO zt$hd?1OmKD{HY&}VoP)eyc#r}CPX>`!Ub+jCWEJrD^DBsNcLpd@|-TuGnHmbp)F&G 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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?= 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 pE20$s5T~f1psdc@6Z>6JiVBK~==*mMpA Date: Fri, 1 Oct 2021 14:49:21 +0200 Subject: [PATCH 26/67] 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/67] 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/67] 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 = ( From ba9ab80517d1f75793c324ccbb3adee3437c2b70 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 2 Oct 2021 15:21:38 +0200 Subject: [PATCH 29/67] Fix to make sure deprecated code39 arguments still work --- docs/Templates.md | 2 +- fpdf/template.py | 26 ++++++++++++++++---------- test/template/test_template.py | 23 ++++++++++++++++++++++- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index 5dddefeef..0bbd00f63 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -163,7 +163,7 @@ Dimensions (except font size, which always uses points) are given in user define * '__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. + * Incompatible change: A previous implementation of this type used the non-standard element keys "x", "y", "w", and "h", which are now deprecated (but still work for the moment). * '__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 diff --git a/fpdf/template.py b/fpdf/template.py index a7fd294a5..690f34dd9 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -99,6 +99,19 @@ def load_elements(self, elements): e["priority"] = 0 for k in ("name", "type", "x1", "y1", "y2"): if k not in e: + if e["type"] == "C39": + # lots of legacy special casing. + # We need to do that here, so that rotation and scaling + # still work. + if k == "x1" and "x" in e: + e["x1"] = e["x"] + continue + if k == "y1" and "y" in e: + e["y1"] = e["y"] + continue + if k == "y2" and "h" in e: + e["y2"] = e["y1"] + e["h"] + continue raise KeyError(f"Mandatory key '{k}' missing in input data") # x2 is optional for barcode types, but needed for offset rendering if "x2" not in e: @@ -106,6 +119,9 @@ def load_elements(self, elements): e["x2"] = 0 else: raise KeyError("Mandatory key 'x2' missing in input data") + if not "size" in e and e["type"] == "C39": + if "w" in e: + e["size"] = e["w"] for k, t in key_config.items(): if k in e and not isinstance(e[k], t): raise TypeError( @@ -485,16 +501,6 @@ def _code39( "code39 arguments x/y/w/h are deprecated, please use x1/y1/y2/size instead", PendingDeprecationWarning, ) - if x1 is None and x is not None: - x1 = x - if y1 is None and y is not None: - y1 = y - if size is None and w is not None: - size = w - if x2 is None: - x2 = x1 + size - if y2 is None and h is not None: - y2 = y1 + h pdf = self.pdf if pdf.fill_color.lower() != _rgb_as_str(foreground): pdf.set_fill_color(*_rgb(foreground)) diff --git a/test/template/test_template.py b/test/template/test_template.py index 42de60cd4..7feeeb317 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -1,5 +1,5 @@ from pathlib import Path -from pytest import raises +from pytest import raises, warns import qrcode @@ -244,6 +244,27 @@ def test_template_code39(tmp_path): # issue-161 assert_pdf_equal(tmpl, HERE / "template_code39.pdf", tmp_path) +def test_template_code39_legacy(tmp_path): + # check that old parameters still work + # This uses the same values as above, and compares to the same file. + elements = [ + { + "name": "code39", + "type": "C39", + "x": 40, + "y": 50, + "w": 1.5, + "h": 20, + "text": "*Code 39 barcode*", + "priority": 1, + }, + ] + with warns(PendingDeprecationWarning): + tmpl = Template(format="A4", title="Sample Code 39 barcode", elements=elements) + tmpl.add_page() + assert_pdf_equal(tmpl, HERE / "template_code39.pdf", tmp_path) + + def test_template_code39_defaultheight(tmp_path): # height <= 0 invokes default elements = [ { From a746ef7a0d5aafbb750b83dde7f7dba062e8f7e4 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 2 Oct 2021 19:16:31 +0200 Subject: [PATCH 30/67] sync to upstream --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da650a347..338f5a21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,6 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). - `Template`: CSV column types are now safely parsed, thanks to @gmischler - `cell(..., markdown=True)` "leaked" its final style (bold / italics / underline) onto the following cells ### 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 7c9fcdb3e4594958be470100cce9ec92251e0f24 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 2 Oct 2021 21:09:30 +0200 Subject: [PATCH 31/67] some more test coverage --- fpdf/template.py | 6 +-- test/template/template_nominal_hardcoded.pdf | Bin 22662 -> 22817 bytes test/template/test_template.py | 47 +++++++++++++++++-- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index 690f34dd9..8af0dee32 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -79,9 +79,9 @@ def load_elements(self, elements): "y2": (int, float), "font": (str, type(None)), "size": (int, float), - "bold": int, - "italic": int, - "underline": int, + "bold": object, # "bool or equivalent" + "italic": object, + "underline": object, "foreground": int, "background": int, "align": (str, type(None)), diff --git a/test/template/template_nominal_hardcoded.pdf b/test/template/template_nominal_hardcoded.pdf index de1add4f8258d8e44c9c9df2233c6a9202c980d7..bb187336d4b9c98360a4cc090b127c5163fca37f 100644 GIT binary patch delta 1291 zcmZqM$hdG50N&u zuC{5d$h@_M=bm52_H(aI`y^NHvezURm(1%Px1JNHFEA}>Ze`lFu$P!W8TCVNd zRQGSgf`4|kckh=4oK;Fr(pbAtq+{v^vxsM6hGsX`Km7VgP(w(pv3@~)d%tFK-h{;o zXA1VVcK5U7mls&F9d!7k-f?*Ioe9zfni0%8?(0we7nMuo`7CzD!qkV!az%CHRo*>6 ziniA7h(0{~?ze}p{yq1Os0}`Mzn^mxL#shW#H(xxP5t&c9Wv*n`qbYYJlXjmXjSw6 zkEbUo1sNuLZ%pX4-FANW->T|*o|m6ah$0I)u9@U=qJvVJ~=9yieEji!rRJxk(F{?eiHLrJP{@Yv6 z_vB9W;|onOfkHaRx8G|xemv{_wxBEX61m&o?_?8cv;I+(@%^^*oh5fYJ(G2ol(fvL zzQMiswTs!U^N9;B6S~c`B3=oms{E9`y5fYJ^sy8pakq75TF!#LCW;$hZvV0*dc`t} z%_n1o*{r2a`JWnIG>S<)o&Wm3;l-Y$^u=E}v+EcAHHyxVU%D;%($1K-T)`QWH#~o` zB>3h!L%~FCzB{*Xeeq>2-jevP`>aa3YWbx@g59B{dT`vqRN`G+g{`1lRL$qu9Q#Ze669hd#}dj4UxI8TdwY0 zxpT+F>HDVTM%34`-!AXe=*yoQe$>^0aH^ zi*wgLITxA}+kCU&aMpK;*C|Cx&o5kG>3Ys;M_2942zB9`mg%BXZmu(5{i2BPnAVNe z!n028dL!e*K7I4VnU|m3QC}0veIt0{Qg2>qu{D0zRD7p>yR+S=wEl$5wRnz46WqhI z3+8n73SV1yD?v8qT+y>9uXw|LPMtGpNz>WSZ@Gldty6mV#!Kj2u&FoxCSn+m2{@mbP-5htDXO%{6&v{?z20{+K{d(YF(|6SaK& zBK(&g<$V2b^~tu?+xNRZkJ`Fhz(u2qJ1V5mwOZ>Rlh4wW9g}}Df9KJ4%FjvhPs+(G zEKQyKfyI6Dbr%20YyE>KSF+YIT5kTqy44d%t@FRYW~A?CtYEHSpb*3b%*Xl>{z+M> z$t4OlHeC9ih6)xi37}kRUP^va7MG#HpDSHZjEzGchql6EiX}HNv#Y z*w7eVosqGn5m?LzY)MH`VrEWi5f{j_!I@R53Wi2p`a${mB?^W>D}W)Q@0pjDuV4r? d8xlOlC5c5P6-B9OTt-GFmS$Y4s;>TSTmZbgDc%48 delta 1170 zcmZ3uiLq@Xf-jlO5>DV`$?O&FL-3@ud zF2FLSYr=$vRsU*h)fe9Rb}X;OMeg~p8_E4|h1=F|ZdiHX{LP~>=j;UojtL~xamzpN zVyXRK>GrquPkr@uN7L?}&V^grf)v6I-Fl$2y<_9SGDh*u?h8C3Sn8Yp$=+Mmv(vG! z$?))3u5#NURJ-i-0<9~o(>5_IaOBuC2GApZ}E8h!V#r*K!VM*5|Nml+F6FTqy z-2Sy}W7XHn2VH`eb5E!miKIV>s?RGsAFLNRxk^bqvoq@XuE_iAw{M8H^ZlWbxLEk7 zlhdQ5 zCBNiTJ*sc`D>vWKDL?)CX^X%7Q{l;C$+B^5`;Pv#_#f9@_^$l_h2)q(p*e5ktJ?S0 z+k{nHPhIqlN86(2oaq9CJBObxod2G$aPl^J)!vO0E(sRBOFaK&o$$>J`}Wpn4Z62h zB&^ieP(FEog`2C{Ed#a5I}M7q21<5iEaL3+-M>`*u9NA}nZD0v8Qi$JqEe&3bJ|0$ z=(_0t$6SoN=JsB5U(xyPbk0H%rnK9JFCVNeYt^jZ5_luH=+erZ>oYnwh_b)S&Hc5k z$?}%L_p_;!Hczt7eAsbxANvu>lONXx-|3D~N>kTeByDYdO>}l_Ue`g}8~>`Muhl-h z61_aO^wK1zO?Ty|WR$$qtbDcmR7lIXt$ws*>5I1d!kX1i-QTt-%SPUOX*k#G;Ler1jve*%e!ctRwy>h-THDqg%-NB+ zwYsO&$Z}%wi`Q3_(q&_g?( zmb_EGwu<%5$`hBCw()X@sb^1e^R9jseatF_>$N{a;fckoO)dJ49uut34$s}drd^70x@kEj)?QaPrG=SutZ% z1p^RJ$W!0~GmMN3OwA_CL|CO68KOxU7#bNFVTc(UTA+y;85mmvtw%G~(82&i%*fPY z@`VT)6GJZjp#1z21w)_`psV#g^V0GaEMYz Date: Sat, 2 Oct 2021 21:21:29 +0200 Subject: [PATCH 32/67] pylint asking for style points... --- test/template/test_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/template/test_template.py b/test/template/test_template.py index 20eeb866d..e108942bd 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -192,7 +192,7 @@ def test_template_item_access(tmp_path): } ] templ = Template(elements=elements) - assert ("notthere" in templ) == False + assert ("notthere" in templ) is False with raises(FPDFException): templ["notthere"] = "something" defaultval = templ["name"] # find in default data From d09688c24ff3b592ecd346e9807c3376c3932ae1 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 2 Oct 2021 21:31:51 +0200 Subject: [PATCH 33/67] picky black... --- fpdf/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fpdf/template.py b/fpdf/template.py index 8af0dee32..668fe1cc3 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -79,7 +79,7 @@ def load_elements(self, elements): "y2": (int, float), "font": (str, type(None)), "size": (int, float), - "bold": object, # "bool or equivalent" + "bold": object, # "bool or equivalent" "italic": object, "underline": object, "foreground": int, From 294287cb2c3230c3ce0a3ab717eb503f50f31ae1 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 8 Oct 2021 20:42:52 +0200 Subject: [PATCH 34/67] Change background default to transparent --- docs/Templates.md | 3 +- fpdf/template.py | 37 +++++++++++++------ test/template/flextemplate_multipage.pdf | Bin 1941 -> 1887 bytes test/template/flextemplate_offset.pdf | Bin 1197 -> 1156 bytes test/template/flextemplate_rotation.pdf | Bin 37173 -> 36907 bytes test/template/template_justify.pdf | Bin 1419 -> 1375 bytes test/template/template_multipage.pdf | Bin 2415 -> 2377 bytes test/template/template_nominal_csv.pdf | Bin 1478 -> 1462 bytes test/template/template_nominal_hardcoded.pdf | Bin 22817 -> 22801 bytes test/template/test_flextemplate.py | 1 + test/template/test_template.py | 15 ++++++++ 11 files changed, 43 insertions(+), 13 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index 0bbd00f63..40a639ec8 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -186,7 +186,8 @@ Dimensions (except font size, which always uses points) are given in user define * default: false * __foreground, background__: text and fill colors (int value, commonly given in hex as 0xRRGGBB) * _optional_ - * default: 0x000000/0xFFFFFF + * default: foreground 0x000000 = black; background None/empty = transparent + * Incompatible change: Up to 2.4.5, the default background for text and rect elements was solid white, with no way to make them transparent. * __align__: text alignment, '__L__': left, '__R__': right, '__C__': center * _optional_ * default: 'L' diff --git a/fpdf/template.py b/fpdf/template.py index 668fe1cc3..ef7f3a358 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -124,8 +124,9 @@ def load_elements(self, elements): e["size"] = e["w"] for k, t in key_config.items(): if k in e and not isinstance(e[k], t): + ttype = t.__name__ if isinstance(t, type) else ' or '.join([f"'{x.__name__}'" for x in t]) raise TypeError( - f'Value of element item "{k}" must be {t}, not {type(e[k])}.' + f"Value of element item '{k}' must be {ttype}, not '{type(e[k]).__name__}'." ) self.keys.append(e["name"].lower()) @@ -225,6 +226,8 @@ def _varsep_float(s, default="0"): self.keys = [val["name"].lower() for val in self.elements] def __setitem__(self, name, value): + assert isinstance(name, str), f"name must be of type 'str', not '{type(name).__name__}'." + # value has too many valid types to reasonably check here if name.lower() not in self.keys: raise FPDFException(f"Element not loaded, cannot set item: {name}") self.texts[name.lower()] = value @@ -233,11 +236,13 @@ def __setitem__(self, name, value): set = __setitem__ def __contains__(self, name): + assert isinstance(name, str), f"name must be of type 'str', not '{type(name).__name__}'." return name.lower() in self.keys def __getitem__(self, name): + assert isinstance(name, str), f"name must be of type 'str', not '{type(name).__name__}'." if name not in self.keys: - return None + raise KeyError(name) key = name.lower() if key in self.texts: # text for this page: @@ -305,7 +310,7 @@ def _text( underline=False, align="", foreground=0, - background=0xFFFFFF, + background=None, multiline=None, **__, ): @@ -314,8 +319,12 @@ def _text( 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): - pdf.set_fill_color(*_rgb(background)) + if background is None: + fill = False + else: + fill = True + if pdf.fill_color != _rgb_as_str(background): + pdf.set_fill_color(*_rgb(background)) font = font.strip().lower() style = "" @@ -346,7 +355,7 @@ def _text( "border": 0, "ln": 0, "align": align, - "fill": True, + "fill": fill, }, rotations, ) @@ -361,7 +370,7 @@ def _text( "txt": text, "border": 0, "align": align, - "fill": True, + "fill": fill, }, rotations, ) @@ -380,7 +389,7 @@ def _text( "border": 0, "ln": 0, "align": align, - "fill": True, + "fill": fill, }, rotations, ) @@ -418,20 +427,24 @@ def _rect( size=0, scale=1.0, foreground=0, - background=0xFFFFFF, + background=None, **__, ): 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)) + if background is None: + style = "D" + else: + style = "FD" + if pdf.fill_color != _rgb_as_str(background): + pdf.set_fill_color(*_rgb(background)) pdf.set_line_width(size * scale) 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 + None, pdf.rect, (x1, y1, x2 - x1, y2 - y1), {"style": style}, rotations ) def _image(self, rotations, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): diff --git a/test/template/flextemplate_multipage.pdf b/test/template/flextemplate_multipage.pdf index a466312b7ecf1a16cb1d588cb5b2cdea2c5eb923..c62fb2e7922773eedea9249819e0ef0b7b156db7 100644 GIT binary patch delta 901 zcmbQrf1htcUA>Woft?*!aY<2XVlG$3oV`K2{f-z2y!k5{>?Antj<6ia`r-GLOaj~86 zt!=h-3ZBZ&lUeiiPR5>f9jq&kOq$VgopE+F=S<;49kUPIGkAHczS$wu=TcVhqjl~T z$BrJhsC#3#K7rNjWyeRMhpj>zZFRMaEbek(CMZlew@x9pRU&#)w3&L&&Hq={waMxpnSXtqwVD6d4ap(Dw|8%Qc0b~F=D+3D z-c|b;HPY@qpS+*3qu#_6C8$D9_X!&c>^UF)MUe0E?YG-*+um`|@rViI{>;oVDKsEW zb(%t*y#2F@o=i^~z9scveY&d1{@B6xwh0_{rukJnDvuv{dL#egHhsaS@4`+tirOw( z($&W_F7*Ar%w`wM#`mE2q7}!%3*2wcHzX&p^@$tTf3OY_=AZDVv;2nYye*k6|Frk) zxSdy@!?(Uc_8>1yQ(^_@-|Z=K@6;#yB(74)&(!F4X ztc2^98Ee=LdCcnPZvHB&=hgm9{7%KX9dDMcyL$7ov#jiNcXgk|!d5&+{a!oo87=;C zM=?6W)?2o#sZ4cFyy>5qdjXwSc+SRprj)h_U;MYhI=M@5Z|un>ZL6kcey=#Tr0dQ4 z6fVJ|qRY-6+RSA2HG|EoE9kE9QPXeoy{5O64rZ+0x2mzMZ_bvf!ai3n~ZTHN7 z?@#vs^z;3fSbjtP<9{|`PMes`S}bWyV#Wpv1|Xo2r@#ef7#JEGnoVBDW));?jxJ?j qYG{fsW@u<+gdt{RXn`SSVr(#3j$LkY0J}Apsf7iXs;aBM8y5gkr^^K#P*V^NPFBc^&XMB~k-k;W_xbPiUFv z`{2t{-f&I&_eneEVb8-$;m&6dr+)om$n;p^@S*r>0h<@+trSb!R=t`MbL;ZDU-P$r zc1xeQ>(_0|rHkJNd;PtAy{aVkL$CwaL3yT4KKtZodE+o3QA&A&Yb#y`Q}P=Z&%zZO%K7bpH7?@9m9^ zA66P>@chYJSGD7ZgTv1o?;q|uFUKq|eX4Ay-M!xHHzZXW6E-QjOGw3VWkd7N{Y`d7-}D-2C_7 z^LJm$PB1NCym!E0PwPLsWjx!HMLL~adE8(7DrFx(`$FQdLFE723@jHVuWzfmb@Hmo z^9Nh4?wb8~uU@6$ea&06w)E{PQ~PSikhj-`zVgl7ZpLBVer>IZM}5?ESs`Y%5;L*W z++OTiOj#W}E5p86%?r*;-*G^8)$GErF$JGd$v#6 zW~TY)HFe=r=h#eQ&6eCPeq(LDMF^{5Q%sPV+8HM4TU$>_9$S!pH%jL1lgxjHMrtD4 zd)|EZt-l$+-#+{N#^RKJr@OnPx;M_&t==N#-ag^W`m4N|A)4JbHL_x?^Q1p>)^_N7 z-*x%_>2Yhxa+`ZE3#{~h{AX}`6}6o!H7^C4SAeYiq%1B&1p@^jy_uKAmr2aRM8NhHz{0IIaE_y7O^ diff --git a/test/template/flextemplate_offset.pdf b/test/template/flextemplate_offset.pdf index adb42f52f0cac5c54e538a99c12494925385b91a..b44645d994eba8951ee894ab0d913ef0ee3d5a8b 100644 GIT binary patch delta 433 zcmZ3>*}^%YzTU{t&W@|Nq$o8pm#bpV-k{TdhYbYwJlB545%hT7e9tNuCKbIECiVeS zML12J98~MN{ZroNICnSiSP-{oTiP{a>!{}4O;gr?IM~`zFk8!h>*ZCOR&-3^n-H$~ zb?xD-SvL+}3ERo=KKrRonPgzYM2+5w6<3q$7B38T-hH$*dQVdQhp#MbTZ=EQE_Z9? zdaJDWZu{-;dHWmG&pf{PeR{_Hor0(5Z<=!IelthmmvrYtpB-6#%&W-gIih)eT6f{j z>|C|n=;=ulOSFPYTZ=S~FZ0i|opY^=rzvwvW}h!N(7qYV-`8{AFM8#BSU%(Fmy;5O zOrG^^SJU>s7Q6Fe^TbExrT!c#%3dEIe=4wKcoekg&X)@b6V3-sC~E0?G-Zza7M*j8 zZQ5Su2;HCS9KFTu?c%q|Z~u$Uy~UCqbkaQW#tTWgxNom*-#0Bi|8j4_;`mRK-uP_( z%J`8{+{D~M!2kpl@)Wqh3Fk>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{J8DBAq zn;Kdw7=VC6o&pz`VPIfxU^w{-v!%9~fhmTZ1vW8L%gGij&YWgOT&k+B{%%|VlZ44Z diff --git a/test/template/flextemplate_rotation.pdf b/test/template/flextemplate_rotation.pdf index 26145af30efdc1f70828f31e707fd3601af86e0b..6740a6faf95c60e12be4029b41e9a34e2aca0db0 100644 GIT binary patch delta 31929 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 f?|NLQ;~Nhet(EmHPhyM?d4G delta 32197 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 AP5=M^ delta 584 zcmcc5)y+MjzTVWx&W@|Nq$o8pm#bpV)Jx|lO)(H)`|#W5X!(^@qRej&6l{&*pC-Qg z17p%9<87`&X7y#?KfXQlf=BK-6N}Y{$FJXgC|y45Z^!k!%O&OaZ^>wP(>&vDb9Q%_ zp6IsY6}zsK{=d3%+3cz_>vxA;o!y?7!6*Ffapl*A_VbkcO{X*`tKRLP|pE8SXzB)qc3B1?vgk`gH2#2Kz>_Y3-eBau^Cd^&(P= zvOC@z{uh(k_kLH}t9I%9Iqx3yKAw8~-iFlZ8n=@XU;H07eQNb8`1Qo}%c1kG=07Gn z&YK(Ya7Wyp6G6M#ZN&Y|qi(;cZ`aM;(f+Ms>CXv8I$~ekE*4yy+3X%E_SR>IhvmWI zM_Ts6l~a=@OiP%XBBsa4ZEJq}f!y-E#joFA+!}s2{b%ZnNt`m>SK_Nq$9-Ejv2Nma zk);=l9;@o4irRU`T#5|vVmW;JQ@)bQhoaA4d!=so`rmyh%@=-u$#Y+cl^L2g)fYCUF{s@ z{X9AzEQPF}XJ}1%=g+#>*z+OlQl_O1QVHz$w5nw9MOVyy)wDL??uysYZ8Bkulp+Zb1oT27HV;D-mOp1Yvu)ZEWW@rQHjCtdup{hW-9Qxspgd;9;-{JH+; z>bGUS+J1PSq;4G}qvocwlW#J1@EfCs+2jHywffRjgSV5^KD~dxJ?HuXqr@{jHR|Q8 zckW0hI4NmdGti&(&1(Zk*SG2#v)L=s6C}Udp5*l6^OC!ISZ+>eiPfI+4>ngTXD?1$ zysqB;?h6m$`|JG7f9#T(%j(~vb0D_rRF7HnvL@4-FDIorb|zim^0?R99lqmMeXR3G zF@p`SfFW0(rN@%_CY<4|-F9`A_lB3bJr`*RdEWljqS7=c)}u&n?MsiVCuVZ=rmWy{ zYBCG+f3iGERFOM(`t3C(J9rxR>1=u-8u#shK+;2>WCdH7N%6pt+m!r#`SC~FW}J8X z9$J+4_)N;62{E)PewATi?hv`z|ke?)3s!$Pz;;v=eu^VE3X)D;Q7_}ghiEIwg1&ZrgYJ{ z-`=a<=`X$aY{kt5ukQt#TUpuDOft~NSoH>%YF($!A85#>@rCLoaHvGFSyT0et+RvdoBWe$xRezQo zd+qAutko~;|M1+M)T&wdFL`Udz`m26%@q}^<4;!^{9seI-zT_vD{Bvvn2Di+0SGAM zDR6-q28QOwmXpIdtm@4z(WMMbP0caHEKM=g85$U4s53M*##Co!Vt{U*p@pdlSd1Up WO_s*SljS+xIgQP^R8?L5-M9dj7wk{~ delta 1176 zcmX>p^j>H}J-@MuogG(kNl|KIE?32zsT2ED>ves69t)}ddUOBz4YLgz9JLMWAL$)B zYs<_nVI--QV!YOWQID;+c!u-$0f-WAvWU2IS0!yyce^qSU)_#^#lcl6e(No0LDD!ijlaOPX_@o+#Y^xyKqGe~FESzaO$95BA zvQAj~3pbfnO%oQJjaTDZ>?pljDP^Io@9a>%>5ebno=6I)xXI{z=~86cLL054_S&0! z9xa<{YkTar#r)o@y4{bT`IwpS){r!rq9NDoG&5mMeVMt&VvVBoO*gI^-1e`sdG}1e zeL~yms}~>s`QtQu@^kasCSOk{-tQ5+FSLID)yZcWJNQjZQNnL>3X@v>*2`>C(@iGU z*Y8hW&NhuRIqiYr$u(fh{g=0L7R_r+z4Bc}UBgnNGK9an zAnfAHf_n!)Z#;Kf##FlWujMN@cdJitUzIekKYcOsgUbn~J4=_J$uQ2)ia6{(e^28C zZML-yPhxjew66Yp-Bcu>k$JV8%BuPm&zvS4t7Em7@zj^p(0;9RuCU2@t+_@)5-NaR!+nA0n4D?{``Fh>x zT~%txJD+K(u?OGOUV36|ANb$maEYaT#2;IS`_GOr>aE(9bxPJ|O6ks(vo5}5UlMX^ z+2pm%z@Rio4N4alwfeHVTMT=>XPwx;|9)pI^L9vBYCL#z3KW)xje!$QqMKeX$k;bK z+qSogZJOi#iE|b_S@0y#HsryYMVJ5Cv>(r%yWKL&?8UC)E4+PMp5DIpvMPT1;>`~_ zC$R4D^`Ch$EnvDq<9WSGE=5p^JbAm|BbWNsS0Y{88yXAQh3Z2?id8%wSF>KIvAnHV z**{axQ^<9q$1L44P*_d`h2^vmzBzk+-#mbXrM-jsq=i}>oVO}cmFy)e9&7g6GI^G~_V{%El+NOm*;8%rh(AC2qG(0loHgp{$w`a5CpkJd zKDwZ>gz?xfljpC2VVUQfck1RN_IF|Nr#G#eCob)K`?GPWv(&CLKX1J%P_CUUEdBFK z;Qr}f6FzWHl|C)Gxu3O%NzB|*!2kpl@)Wqh3FS+nVOqph?$vNV2D{*LiF+@yUEhraPm(McTRItE>%@me>W}w%a{HR diff --git a/test/template/template_nominal_csv.pdf b/test/template/template_nominal_csv.pdf index 538312884378a72a539421e1b4581cc2e1523cb5..286166cbbb9b31b8374ae663fda462bd3c159efb 100644 GIT binary patch delta 494 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 Date: Fri, 8 Oct 2021 21:19:01 +0200 Subject: [PATCH 35/67] Add ellipse element to templates --- docs/Templates.md | 7 ++--- fpdf/template.py | 32 +++++++++++++++++++++++ test/template/flextemplate_multipage.pdf | Bin 1887 -> 2196 bytes test/template/flextemplate_rotation.pdf | Bin 36907 -> 41794 bytes test/template/test_flextemplate.py | 18 +++++++++++++ 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/docs/Templates.md b/docs/Templates.md index 40a639ec8..2ad8a286d 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -161,13 +161,14 @@ Dimensions (except font size, which always uses points) are given in user define * '__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 + * '__E__': Ellipse - draws an ellipse inside the bounding box * '__BC__': Barcode - inserts an "Interleaved 2 of 5" type barcode * '__C39__': Code 39 - inserts a "Code 39" type barcode * Incompatible change: A previous implementation of this type used the non-standard element keys "x", "y", "w", and "h", which are now deprecated (but still work for the moment). * '__W__': "Write" - uses the `FPDF.write()` method to add text to the page * _mandatory_ * __x1, y1, x2, y2__: top-left, bottom-right coordinates, defining a bounding box in most cases - * for multiline text, this is the bounding box for just the first line, not the complete box + * for multiline text, this is the bounding box of just the first line, not the complete box * for the barcodes types, the height of the barcode is `y2 - y1`, x2 is ignored. * _mandatory_ ("x2" _optional_ for the barcode types) * __font__: the name of a font type for the text types @@ -175,7 +176,7 @@ Dimensions (except font size, which always uses points) are given in user define * default: "helvetica" * __size__: the size property of the element (float value) * for text, the font size (in points!) - * for line and rect, the line width + * for line, box, and ellipse, the line width * for the barcode types, the width of one bar * _optional_ * default: 10 for text, 2 for 'BC', 1.5 for 'C39' @@ -187,7 +188,7 @@ Dimensions (except font size, which always uses points) are given in user define * __foreground, background__: text and fill colors (int value, commonly given in hex as 0xRRGGBB) * _optional_ * default: foreground 0x000000 = black; background None/empty = transparent - * Incompatible change: Up to 2.4.5, the default background for text and rect elements was solid white, with no way to make them transparent. + * Incompatible change: Up to 2.4.5, the default background for text and box elements was solid white, with no way to make them transparent. * __align__: text alignment, '__L__': left, '__R__': right, '__C__': center * _optional_ * default: 'L' diff --git a/fpdf/template.py b/fpdf/template.py index ef7f3a358..28b77fe38 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -54,6 +54,7 @@ def __init__(self, pdf, elements=None): "L": self._line, "I": self._image, "B": self._rect, + "E": self._ellipse, "BC": self._barcode, "C39": self._code39, "W": self._write, @@ -447,6 +448,37 @@ def _rect( None, pdf.rect, (x1, y1, x2 - x1, y2 - y1), {"style": style}, rotations ) + def _ellipse( + self, + rotations, + *_, + x1=0, + y1=0, + x2=0, + y2=0, + size=0, + scale=1.0, + foreground=0, + background=None, + **__, + ): + pdf = self.pdf + if pdf.draw_color.lower() != _rgb_as_str(foreground): + pdf.set_draw_color(*_rgb(foreground)) + if background is None: + style = "D" + else: + style = "FD" + if pdf.fill_color != _rgb_as_str(background): + pdf.set_fill_color(*_rgb(background)) + pdf.set_line_width(size * scale) + rotate = __.get("rotate") + if rotate: + rotations.append((rotate, x1, y2)) + self._render_rotated( + None, pdf.ellipse, (x1, y1, x2 - x1, y2 - y1), {"style": style}, rotations + ) + def _image(self, rotations, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): if text: rotate = __.get("rotate") diff --git a/test/template/flextemplate_multipage.pdf b/test/template/flextemplate_multipage.pdf index c62fb2e7922773eedea9249819e0ef0b7b156db7..3e161ddd04aeba4616ed48b64089f7f59781d807 100644 GIT binary patch delta 1247 zcmcc5H$`wlUA>99rJWsDaY<2XVlG$3oV_7C{f-z2y!|X1?6mRNc403SheeLspV%8+ zot8Y9@S}Fl8R_-TO&JYp=Q7rrAD8wv?+sJD=w7$wT8ZucNRxXjo@ivQ^-=M#-m{*+ zWP7f~q};imCOTzIE&sB|sAZAFp^Zi@O1J!OuIsvydf372i9uZbp@`(rD@we*o~31; zC+AHNC>L-r+?2cOp@QC$g^pntnG~z_KJx9H&^_TNoBV; zlpWA>HUHmu(7TJLu(4G0*LUsNQ*_mDPCI<9TePXazh6FxrzTHCi}n23cD{9aCNECD z*;a7ZSB`Z)*CDIcSqdv&UhqraaN*R?9gXXsH!|;7VW{@OZqeoq_KaQ0Z1r9VtRn0) zThu~(G?{HJ;xAjQ4V;cs_~n3%oS_WQGV^QU+2 zZaBBFF#X-JA8Ym9uPwef(?;9hgL4trov3`}UlDH#*J#IV;9MMCkYt<}ayUNhTgCbJ z^@}C%yT{*ipMT-@Pun?%Zr_Q1eeCw;*3ih53(wkBecx?d^)Pc~W2c5&&oie*C*GH+ z-I6a;_fcHXEpGHwyt`v&8Na%Z%Zj_LK6VRCg)gT)jAZ1s>=E=3j0#&H5|R9WQ-qRt zBv0Yi@HZLh=PyP{uAUe(_vh){@B4QDtJvas`pH+Dk54jxrawsulst1gV~xA;=d|oA zAGhD-eC&HE_*r|t^VJ2Z8~a{3&+G9s{yTrR{4t5Ve~INc!vE;+TR-FO<}b{tOyb7I zmI?+Sppd7)1!fo+ni-i+E?~1vH#0UvmoqRoG(i_LG%+^B5HmG0#}G5eVwbt4@nivZ QS1t=nb1qd?SARDy0OcM$`Tzg` delta 944 zcmbOtc%N@VUA>Woft?*!aY<2XVlG$3oV`K2{f-z2y!k5{>?Antj<6ia`r-GLOaj~86 zt!=h-3ZBZ&lUeiiPR5>f9jq&kOq$VgopE+F=S<;49kUPIGkAHczS$wu=TcVhqjl~T z$BrJhsC#3#K7rNjWyeRMhpj>zZFRMaEbek(CMZlew@x9pRU&#)w3&L&&Hq={waMxpnSXtqwVD6d4ap(Dw|8%Qc0b~F=D+3D z-c|b;HPY@qpPa?iUTWPj{nd)ow#I@A2B9hJuqJiU?saGSnh(|2Jf8%1pw zE$QlG8W;M0UuLt5W#fC$d(n#H;05kC=Npm}*!slljXzk22=h<)(^-B)b>5cDmVerN zcHGXZ&*59&AbXIPr75w3^Y8W)xp(RleG*rxWb#irdFy2|myg{Fo2RE4cr<5RE9==( zydv}31Mkco`SaYX{bpobylvn9ZN=7*tx9fh|Laz*{P)P#s(#_WO541KC+!z^Pw8H; zLRP|c%ZxSbhCKCVb2on#)$?k9CVr=4-HtcQ)?K}M*;!V0y1Tm1Vqq&DqkgZQ_ly?* zxT6>yVe2j1)l{ZBC*Jf=%)NllD?DdoJyS|sgfITvV4d70xHtCXlD1V-Grw0HThjGr zeF~T0QPE{*4{c_$`kKM!)fIGC_^9bO`CijoN(VDm?_1Sa);DL%R9|7Yug6cxzyEdp z@7vpx{i3waFQ0n->GH^o@0y7K@jSbBvFJ-r^H!(Ix zmoqRmG({IPG&C~85Hm8gzz{RRVwZ`92|`^-QDSCJY7v)>4VQj!=HyroT`p4#3ocbv ISARDy08eC~=l}o! diff --git a/test/template/flextemplate_rotation.pdf b/test/template/flextemplate_rotation.pdf index 6740a6faf95c60e12be4029b41e9a34e2aca0db0..9a1c58924ebf678ee5a50fb9ce096062a49f5333 100644 GIT binary patch literal 41794 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%0AhH3s$xfL;?Oc`>fC-Pgb zGgv>1g7d2S9K*kvbHzq?wfQd59NuzK#m*-L5{3#2XAgP7O~*tWMT7Ko!!OJt0C2*7c8EY%ys*B+oGsLqKm_r7$==o3*b`z`G-Q%bDyPb}&B&tn4M0z_7 z>w>t}pcfRI-(@G@!j{bi}yR98pK9>`t&B!7JUo^(J zGTVTEi&-9_PuPv|*(}~J?=2#WAC`3j6iOgqgLx`q5r(6aBdEM62R^XwTmnPds0;~s z#HEiS5AxSNFeSW@I5h_&mD`*iOHRDDpwdl2PPe2<1-|FFjjXHX7kiQzof(tZJo^KoxID z_U+prjjcV|)wHi_eG#VTfi#npHIN=@+q+xGN$0>8ZvEVsf5Ngy)|5(uzN7o0n6X7a zq>biPKm5XU!7;=9-MaREOI+Dh8pMLRmUb=}Xw&TuKf{$V|p%#|%M z(NR^A=8{{T!ktv!Y0bDL}) z=^vcdewASjPcK8G*@A>+&5+CZc2%5c8yf}QWv@c!KxevTXOQtTIrDq1q25+SsNv0v zU++!lX$@?(MHOvEJn5*>2@S^}a~C?J+)}gk>rn$fR&E%?#`?IFYL+l2Oi$4n!;v7# zbwpAP!t3b>iFb_aP;u#rT_5l7&Mn>3;a9tz2Fkl{Yr5Nyed$33trdq9q%_<2glQ~h zAcOBtHn|evM1c(ZsGl?X@j>wt3Z{2ZaMLf-hAK%%E z*t=FuRJrOKsjo9k+gXV0MK)u!ZCGdV39Xos@zY=z`claO>l~1`*-%}hdFV3kqO~Qi|Z;*R?!Xadb_Di5j>1&Vf!^n5s5gI)?n)z zX?x(qvbZYwyVgATfHE$#AI8-uE|$t)tL3aDLd?aGMc1;l7JWv$w@r{j7fhT7EYzyT zyCiEw=S&WSPU|XT#xfR*Q2cLeD#FT~8_EXi)bC?dXq&;k`F*kiSs8(!A4x*@BXH40 z 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 delta 31941 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 diff --git a/test/template/flextemplate_offset.pdf b/test/template/flextemplate_offset.pdf index b44645d994eba8951ee894ab0d913ef0ee3d5a8b..7522bc8c8e9141b09589c5972e11f3f213cd178b 100644 GIT binary patch delta 316 zcmV-C0mJ@;34{rdoPXa*k4S*T1(oIps*)g~LfdO-lCG103vN;r?|8CQ=@^I7*RSi&+MU3ZP@HGec>eJ)F>{5=R0M}OKS zd^!jNhlz*ANSt%6Ls4exTB=#w+N4-G9Z#b0$cJ)l`jpneNGV9csZyx&*fbLv7>U#L z?Jwc$ka}5$2c$6aWQ;fgr@z2vw{7cHXtals;yZw|&?JP2cl- O5sdx->pZi{0lES?37K>N delta 316 zcmV-C0mJ@;34{rdoPVE5dPD*wE~qp=P?Z!3H9|loev40J?QBDfgSenxytZc6Gdo&? zyMmOj@WF+MaGNSWwbQDysECvfkXI@7t;1@X*u&ITxd88L{)n>ix7ToBwk>YoC2xKEl@#FGvIRN5PqTKY*V35yJ zkaC2I;*^|6v@k5rqdbP`Y!cs{LtC^)?W65u?f({?+5~4($v0ux=oubf_Uk?Gf~C*t Oy|G}0ZS(nCqN)ZkFk%{`#M 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 z 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%0AhH3s$xfL;?Oc`>fC-Pgb zGgv>1g7d2S9K*kvbHzq?wfQd59NuzK#m*-L5{3#2XAgP7O~*tWMT7Ko!!OJt0C2*7c8EY%ys*B+oGsLqKm_r7$==o3*b`z`G-Q%bDyPb}&B&tn4M0z_7 z>w>t}pcfRI-(@G@!j{bi}yR98pK9>`t&B!7JUo^(J zGTVTEi&-9_PuPv|*(}~J?=2#WAC`3j6iOgqgLx`q5r(6aBdEM62R^XwTmnPds0;~s z#HEiS5AxSNFeSW@I5h_&mD`*iOHRDDpwdl2PPe2<1-|FFjjXHX7kiQzof(tZJo^KoxID z_U+prjjcV|)wHi_eG#VTfi#npHIN=@+q+xGN$0>8ZvEVsf5Ngy)|5(uzN7o0n6X7a zq>biPKm5XU!7;=9-MaREOI+Dh8pMLRmUb=}Xw&TuKf{$V|p%#|%M z(NR^A=8{{T!ktv!Y0bDL}) z=^vcdewASjPcK8G*@A>+&5+CZc2%5c8yf}QWv@c!KxevTXOQtTIrDq1q25+SsNv0v zU++!lX$@?(MHOvEJn5*>2@S^}a~C?J+)}gk>rn$fR&E%?#`?IFYL+l2Oi$4n!;v7# zbwpAP!t3b>iFb_aP;u#rT_5l7&Mn>3;a9tz2Fkl{Yr5Nyed$33trdq9q%_<2glQ~h zAcOBtHn|evM1c(ZsGl?X@j>wt3Z{2ZaMLf-hAK%%E z*t=FuRJrOKsjo9k+gXV0MK)u!ZCGdV39Xos@zY=z`claO>l~1`*-%}hdFV3kqO~Qi|Z;*R?!Xadb_Di5j>1&Vf!^n5s5gI)?n)z zX?x(qvbZYwyVgATfHE$#AI8-uE|$t)tL3aDLd?aGMc1;l7JWv$w@r{j7fhT7EYzyT zyCiEw=S&WSPU|XT#xfR*Q2cLeD#FT~8_EXi)bC?dXq&;k`F*kiSs8(!A4x*@BXH40 z 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 diff --git a/test/template/template_multipage.pdf b/test/template/template_multipage.pdf index a4b11dbf5d0786c3aee63b1a186a498907fc7e14..15c0e2c20500c8ffce24f4b433f2d5c30efc422d 100644 GIT binary patch delta 999 zcmVI|4J)wf;RwxrAQw3Ix zW!Fr@r6{sl(3hPtdvvY>D@w(=Fj^|JC7@QEOF|#Y;?AMC3T%Ivs4VXDm8e+rf@&35 z`}ajS5Q;Bb3m!=-?0|_Y?9_)|k*#iF?V7D|VeLQcgj_lA$P4G!;!zjYuHl??zH;7+ ztmai#yVbv9j0Yn@p&SMQ#bA>&fvuQl(eg#);mf~2?O}Ubq}u#9xabgboN{{WP*2R* zbH>En9=iSR_0$N>E&c(Yh;wnXdIEp}e>flu+&=+B#`1MD0-G14c~#jls$W+A_O<=k zUSG}C?_Nm~VdUkK-5;NS&Gd=c*s#CUJM^06DfdYXT^Rb*Si-19G<$0(qYi^FqeHhR zRM6ZCWnyHiz>2Y)nrYY+MKufhax!L*&Q)MVsU#OhOJ%hL)Jk$m=tJ4uITTlce+?6r z&3)bym1tg2tpaQRy$ALS`XoF|>HtatH@aH}(791li?V*FK=fzliydwxQb1_h)7Y$ubf1tovL$A-}h z{B7Q}@amla6B0pt1!ry3KN7spjQ+m7cGn}Hfpz9jPC-!<7)7)QdkD@j*x~&KzOH%E zB{V^0-) VytwOrZo9d_f8nNnwX>54Is|-d^_l|bu$8+H>A1f>=@NAJAeDyer)fybN7b}N+OKB zJhEMP`ZbqNm5mMeOT9y{nV#~T#L$IdPK_muT12z9hBE3f_%b?7dqM@xtxzULrV6YW z%dVM*OHpLApf5XP_UK#%R+Nf!VYF0cOF*qSmxMl)#hpWO71)0;QCZyQD^aoL1=T9B z_V0^uAQWG=7Ce$v*Z~t)*r^Y_B3s?U+BI9_!rFh>3Au9Kkr&Rd#iK5)UBfx)eC50s zS!}EvTl@p|M010)dIEp}f5>4pxPJnMjOFWQ1U4^7^Qy9ARKKkJ?Q8q7 zy}p{O-@TF~!pO@dyFWhvn&}g>v0;Cycjz_CQ|^-(x-j&qv4l~JX!h1nMjZxUMu%=s zsGzwO%EZW2ffZvpHPf&uifR`0*#3N0N#^PIG1toujj}4=D z@V9x>!mD=!SL{*mB)X7u;vwYwhq46HMMatex?z$l_c*h3J*V2Ae`_`2pr zNoaoNP46Rbu1Kr8L(Xuh8xjVANQA7wgM7n`;-TBZ75tvW*O0a9P!u!`aQhuz>>pl2 z`y<=H`DCPKXzoNK8~c@wZtjy%1{Z&EgVT2QSrMIjGy^3?8PgNRHP=*0pzm_8+8iDZ zz!ZxFqrv4K7&4Zxn-SPNBF&S^!l-^(`OD|_b$j-9u73VRk_aO&m+Z2;`!&-iW@E$t zQZMK=%Tw-?Gn6p&sj-Ani)i-NP(}rVFQY@ZC)AyZr~B9{${KyZyJ@#h85N_?nJ|JXbkAma31=*lWho Vd2!eM+;($;|2Xe{$Fq|MIt2Mv01^NI diff --git a/test/template/template_nominal_csv.pdf b/test/template/template_nominal_csv.pdf index ed4eded8d8a086eb2a4880061c1b19e019529413..127d952e96ea588f8aaaadd5cc2d70ff39bda2ff 100644 GIT binary patch delta 667 zcmaFD^NeRgL%q439anKlQEFl?SH+y((1U%K4FvW+*M7#)&S#ulZDZFIF=5Ao);|nU zyH+nMi(I92LV~N(i}w$go#%M^ zyXMw>{#WYV!|N_}4QE0fmoHd(;BR@w*57}ne#u9&<*6&0I#1Sa?Kx$a zvhZzU@j?;hs@*}JyrJ$(&pvLQq_t^(kfuvX(3ETGb6pp%a5oMJDp_2;sA8RN^_wCFgx> zc1j#yQMYe~{`}wD|DOH5dExEpp+bpIejeO$`+tpl{ zboK0-*iEMA*4Q5QJJDO|KF@N$NWazQGS25VUwbRh=dr}y;m9jg?o~`u(U|gCae>5| zBA*SR!NyT`9WnoY_TMji{r^yaSfWL&h2=&|k-1N9FO*?FCvmF%Y@+q$|LnSK9bKFI zn4U96r}h2mT^gKc=f7I^eB-+7vg;q$OYMGMSj#@=t;{uk z&SnP{r?3Z&`;y!~ZiqfC{Ab>Y^AmQy3J@{jE%?omzso*<;oq6559)-1j7`cHPqKZHy0D6iM+$>$bbSm|yY5H#hYZJ^CnzI2_x zlJeCxsdFC)AG`Q*SM}Zh){pD;tFL_aoyziS`rb>g3W^z)&7G8BQt_feG5O~%sm7~< zndi)udtN=C{qx(_+WYrvZ6tTuFIG8p; Date: Mon, 1 Nov 2021 00:26:06 +0100 Subject: [PATCH 50/67] code cleanup --- fpdf/template.py | 16 +- test/template/test_flextemplate.py | 243 ++++++++++++++++++++--------- 2 files changed, 184 insertions(+), 75 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index 1eebda673..2b7ba9574 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -353,14 +353,20 @@ def _text( pdf.set_xy(x1, y1) width, height = x2 - x1, y2 - y1 if multiline is None: # write without wrapping/trimming (default) - pdf.cell(w=width, h=height, txt=text, border=0, ln=0, align=align, fill=fill) + pdf.cell( + w=width, h=height, txt=text, border=0, ln=0, align=align, fill=fill + ) elif multiline: # automatic word - warp - pdf.multi_cell(w=width, h=height, txt=text, border=0, align=align, fill=fill) + pdf.multi_cell( + w=width, h=height, txt=text, border=0, align=align, fill=fill + ) 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=fill) + pdf.cell( + w=width, h=height, txt=text, border=0, ln=0, align=align, fill=fill + ) def _line( self, @@ -560,13 +566,13 @@ def render(self, offsetx=0.0, offsety=0.0, rotate=0.0, scale=1.0): handler_name = ele["type"].upper() if rotate: # don't rotate by 0.0 degrees with self.pdf.rotation(rotate, offsetx, offsety): - if "rotate" in ele and ele['rotate']: + if "rotate" in ele and ele["rotate"]: with self.pdf.rotation(ele["rotate"], ele["x1"], ele["y1"]): self.handlers[handler_name](**ele) else: self.handlers[handler_name](**ele) else: - if "rotate" in ele and ele['rotate']: + if "rotate" in ele and ele["rotate"]: with self.pdf.rotation(ele["rotate"], ele["x1"], ele["y1"]): self.handlers[handler_name](**ele) else: diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 86add6ea3..4a1394992 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -241,75 +241,179 @@ def test_flextemplate_badinput(tmp_path): def test_flextemplate_elements(tmp_path): """Check that all elements end up in the right place.""" grid_elements = ( - dict(name='v02', type='L', x1=20, y1=20, x2=20, y2=280), - dict(name='v04', type='L', x1=40, y1=20, x2=40, y2=280), - dict(name='v06', type='L', x1=60, y1=20, x2=60, y2=280), - dict(name='v08', type='L', x1=80, y1=20, x2=80, y2=280), - dict(name='v10', type='L', x1=100, y1=20, x2=100, y2=280), - dict(name='v12', type='L', x1=120, y1=20, x2=120, y2=280), - dict(name='v14', type='L', x1=140, y1=20, x2=140, y2=280), - dict(name='v15', type='L', x1=160, y1=20, x2=160, y2=280), - dict(name='v18', type='L', x1=180, y1=20, x2=180, y2=280), - dict(name='h02', type='L', x1=20, y1=20, x2=180, y2=20), - dict(name='h04', type='L', x1=20, y1=40, x2=180, y2=40), - dict(name='h06', type='L', x1=20, y1=60, x2=180, y2=60), - dict(name='h08', type='L', x1=20, y1=80, x2=180, y2=80), - dict(name='h10', type='L', x1=20, y1=100, x2=180, y2=100), - dict(name='h12', type='L', x1=20, y1=120, x2=180, y2=120), - dict(name='h14', type='L', x1=20, y1=140, x2=180, y2=140), - dict(name='h16', type='L', x1=20, y1=160, x2=180, y2=160), - dict(name='h18', type='L', x1=20, y1=180, x2=180, y2=180), - dict(name='h20', type='L', x1=20, y1=200, x2=180, y2=200), - dict(name='h22', type='L', x1=20, y1=220, x2=180, y2=220), - dict(name='h24', type='L', x1=20, y1=240, x2=180, y2=240), - dict(name='h26', type='L', x1=20, y1=260, x2=180, y2=260), - dict(name='h28', type='L', x1=20, y1=280, x2=180, y2=280), - ) + dict(name="v02", type="L", x1=20, y1=20, x2=20, y2=280), + dict(name="v04", type="L", x1=40, y1=20, x2=40, y2=280), + dict(name="v06", type="L", x1=60, y1=20, x2=60, y2=280), + dict(name="v08", type="L", x1=80, y1=20, x2=80, y2=280), + dict(name="v10", type="L", x1=100, y1=20, x2=100, y2=280), + dict(name="v12", type="L", x1=120, y1=20, x2=120, y2=280), + dict(name="v14", type="L", x1=140, y1=20, x2=140, y2=280), + dict(name="v15", type="L", x1=160, y1=20, x2=160, y2=280), + dict(name="v18", type="L", x1=180, y1=20, x2=180, y2=280), + dict(name="h02", type="L", x1=20, y1=20, x2=180, y2=20), + dict(name="h04", type="L", x1=20, y1=40, x2=180, y2=40), + dict(name="h06", type="L", x1=20, y1=60, x2=180, y2=60), + dict(name="h08", type="L", x1=20, y1=80, x2=180, y2=80), + dict(name="h10", type="L", x1=20, y1=100, x2=180, y2=100), + dict(name="h12", type="L", x1=20, y1=120, x2=180, y2=120), + dict(name="h14", type="L", x1=20, y1=140, x2=180, y2=140), + dict(name="h16", type="L", x1=20, y1=160, x2=180, y2=160), + dict(name="h18", type="L", x1=20, y1=180, x2=180, y2=180), + dict(name="h20", type="L", x1=20, y1=200, x2=180, y2=200), + dict(name="h22", type="L", x1=20, y1=220, x2=180, y2=220), + dict(name="h24", type="L", x1=20, y1=240, x2=180, y2=240), + dict(name="h26", type="L", x1=20, y1=260, x2=180, y2=260), + dict(name="h28", type="L", x1=20, y1=280, x2=180, y2=280), + ) text_elements = ( - dict(name='t', type='T', x1=0, y1=0, x2=10, y2=4, text="text0", background=0xffff00), - dict(name='t2', type='T', x1=20, y1=0, x2=30, y2=4, text="text2", background=0xffbb00), - dict(name='t3', type='T', x1=00, y1=30, x2=10, y2=34, text="text3", background=0xbbbf00, rotate=10), - ) + dict( + name="t", + type="T", + x1=0, + y1=0, + x2=10, + y2=4, + text="text0", + background=0xFFFF00, + ), + dict( + name="t2", + type="T", + x1=20, + y1=0, + x2=30, + y2=4, + text="text2", + background=0xFFBB00, + ), + dict( + name="t3", + type="T", + x1=00, + y1=30, + x2=10, + y2=34, + text="text3", + background=0xBBBF00, + rotate=10, + ), + ) ml_elements = ( - dict(name='ml', type='T', x1=0, y1=0, x2=15, y2=4, text="Lorem ipsum dolor sit amet", multiline=True), - dict(name='ml2', type='T', x1=20, y1=0, x2=35, y2=4, text="Lorem ipsum dolor sit amet", multiline=True), - dict(name='ml3', type='T', x1=00, y1=30, x2=15, y2=34, text="Lorem ipsum dolor sit amet", multiline=True, rotate=10), - ) + dict( + name="ml", + type="T", + x1=0, + y1=0, + x2=15, + y2=4, + text="Lorem ipsum dolor sit amet", + multiline=True, + ), + dict( + name="ml2", + type="T", + x1=20, + y1=0, + x2=35, + y2=4, + text="Lorem ipsum dolor sit amet", + multiline=True, + ), + dict( + name="ml3", + type="T", + x1=00, + y1=30, + x2=15, + y2=34, + text="Lorem ipsum dolor sit amet", + multiline=True, + rotate=10, + ), + ) write_elements = ( - dict(name='w', type='W', x1=0, y1=0, x2=10, y2=4, text="write0", background=0xffff00), - dict(name='w2', type='W', x1=20, y1=0, x2=30, y2=4, text="write2", background=0xffbb00), - dict(name='w3', type='W', x1=00, y1=30, x2=10, y2=34, text="write3", background=0xbbbf00, rotate=10), - ) + dict( + name="w", + type="W", + x1=0, + y1=0, + x2=10, + y2=4, + text="write0", + background=0xFFFF00, + ), + dict( + name="w2", + type="W", + x1=20, + y1=0, + x2=30, + y2=4, + text="write2", + background=0xFFBB00, + ), + dict( + name="w3", + type="W", + x1=00, + y1=30, + x2=10, + y2=34, + text="write3", + background=0xBBBF00, + rotate=10, + ), + ) line_elements = ( - dict(name='l', type='L', x1=0, y1=0, x2=10, y2=0, size=2), - dict(name='l2', type='L', x1=20, y1=0, x2=30, y2=0, size=2), - dict(name='l3', type='L', x1=00, y1=30, x2=10, y2=30, size=2, rotate=10), - ) + dict(name="l", type="L", x1=0, y1=0, x2=10, y2=0, size=2), + dict(name="l2", type="L", x1=20, y1=0, x2=30, y2=0, size=2), + dict(name="l3", type="L", x1=00, y1=30, x2=10, y2=30, size=2, rotate=10), + ) box_elements = ( - dict(name='b', type='B', x1=0, y1=0, x2=10, y2=4), - dict(name='b2', type='B', x1=20, y1=0, x2=30, y2=4), - dict(name='b3', type='B', x1=00, y1=30, x2=10, y2=34, rotate=10), - ) + dict(name="b", type="B", x1=0, y1=0, x2=10, y2=4), + dict(name="b2", type="B", x1=20, y1=0, x2=30, y2=4), + dict(name="b3", type="B", x1=00, y1=30, x2=10, y2=34, rotate=10), + ) ellipse_elements = ( - dict(name='e', type='E', x1=0, y1=0, x2=10, y2=4), - dict(name='e2', type='E', x1=20, y1=0, x2=30, y2=4), - dict(name='e3', type='E', x1=00, y1=30, x2=10, y2=34, rotate=10), - ) + dict(name="e", type="E", x1=0, y1=0, x2=10, y2=4), + dict(name="e2", type="E", x1=20, y1=0, x2=30, y2=4), + dict(name="e3", type="E", x1=00, y1=30, x2=10, y2=34, rotate=10), + ) bc_elements = ( - dict(name='bc', type='BC', x1=0, y1=0, x2=10, y2=4, text="98", size=1), - dict(name='bc2', type='BC', x1=20, y1=0, x2=30, y2=4, text="01", size=1), - dict(name='bc3', type='BC', x1=00, y1=30, x2=10, y2=34, text="01", size=1, rotate=10), - ) + dict(name="bc", type="BC", x1=0, y1=0, x2=10, y2=4, text="98", size=1), + dict(name="bc2", type="BC", x1=20, y1=0, x2=30, y2=4, text="01", size=1), + dict( + name="bc3", + type="BC", + x1=00, + y1=30, + x2=10, + y2=34, + text="01", + size=1, + rotate=10, + ), + ) c39_elements = ( - dict(name='c39', type='C39', x1=0, y1=0, x2=10, y2=4, text="*xy*", size=0.7), - dict(name='c39_2', type='C39', x1=20, y1=0, x2=30, y2=4, text="*01*", size=0.7), - dict(name='c39_3', type='C39', x1=00, y1=30, x2=10, y2=34, text="*01*", size=0.7, rotate=10), - ) + dict(name="c39", type="C39", x1=0, y1=0, x2=10, y2=4, text="*xy*", size=0.7), + dict(name="c39_2", type="C39", x1=20, y1=0, x2=30, y2=4, text="*01*", size=0.7), + dict( + name="c39_3", + type="C39", + x1=00, + y1=30, + x2=10, + y2=34, + text="*01*", + size=0.7, + rotate=10, + ), + ) img_elements = ( - dict(name='i', type='I', x1=0, y1=0, x2=10, y2=10, size=0.7), - dict(name='i2', type='I', x1=20, y1=0, x2=30, y2=10, size=0.7), - dict(name='i3', type='I', x1=00, y1=30, x2=10, y2=40, size=0.7, rotate=10), - ) + dict(name="i", type="I", x1=0, y1=0, x2=10, y2=10, size=0.7), + dict(name="i2", type="I", x1=20, y1=0, x2=30, y2=10, size=0.7), + dict(name="i3", type="I", x1=00, y1=30, x2=10, y2=40, size=0.7, rotate=10), + ) pdf = FPDF() grid_t = FlexTemplate(pdf, grid_elements) pdf.add_page() @@ -367,18 +471,17 @@ def test_flextemplate_elements(tmp_path): grid_t.render() img = qrcode.make("Test 0").get_image() img_t = FlexTemplate(pdf, img_elements) - img_t['i'] = img - img_t['i2'] = img - img_t['i3'] = img + img_t["i"] = img + img_t["i2"] = img + img_t["i3"] = img img_t.render(offsetx=40, offsety=40) - img_t['i'] = img - img_t['i2'] = img - img_t['i3'] = img + img_t["i"] = img + img_t["i2"] = img + img_t["i3"] = img img_t.render(offsetx=40, offsety=100, rotate=30) - img_t['i'] = img - img_t['i2'] = img - img_t['i3'] = img + img_t["i"] = img + img_t["i2"] = img + img_t["i3"] = img img_t.render(offsetx=80, offsety=200, rotate=45) assert_pdf_equal(pdf, HERE / "flextemplate_elements.pdf", tmp_path) - From f4f2dd0a0b13416393faef258485680623fb74dc Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Mon, 1 Nov 2021 00:54:40 +0100 Subject: [PATCH 51/67] merging updates from upstream --- test/template/flextemplate_elements.pdf | Bin 12335 -> 11486 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/template/flextemplate_elements.pdf b/test/template/flextemplate_elements.pdf index d527088537c2d466b91a5966876bf534bb66db19..179103f705a7e4f4a9ad6fffac2902bdc0d2e898 100644 GIT binary patch delta 114 zcmZ3Va4&MhADPWuvfVtBkLZ*z8c(*=HJfaq%QLxJH<{6N@_k)>76UV5gUv#E3mL`D zP0SPwKtLf+feXwqG_Wu*oP1ATM%rF Nfhm`&s;j>n7XaRb9=iYl delta 770 zcmcZ?xjtdT9~qut-^Ai<1tVhx1BIZ?zhruO7>y?z$po>u7bR9sZkO?AG@N{0MyKA? z+{n(3tGJ{nH8Gc~V$RzOhP(_0JO?&>Qs3+JVW*s7lg4MJ&(;t2i3N7AaJ{0GCAhUC z%0+h(wYc?EG>aUg>S^F7QruBb&A=qZVyL~o)QVKrTSTmaYG9q9l7 From 35df15bda2c3b1256ebeab9094290abd5b389032 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Mon, 1 Nov 2021 00:56:16 +0100 Subject: [PATCH 52/67] Update flextemplate_rotation.pdf --- test/template/flextemplate_rotation.pdf | Bin 41299 -> 40450 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/template/flextemplate_rotation.pdf b/test/template/flextemplate_rotation.pdf index 9dd7a856c6de46c999d84a92ce5113423c0dd980..60de753082e923fb75cb11e9029afa7b7682cbc8 100644 GIT binary patch delta 151 zcmcb7h^c80(*~LT&076kJd+>Kt7SBpoH*Z%(QxvN`FV^+li3&Ou^L-in3zqrS}-@m z(%eA700b2B6u7_)69WSyQ-oMaQDSCJY7v)>4VQj!W>u;fkF_M ozGq%qzJejp7$C>aj;pvNv8be?C^e1C#K6$Vl1o+9)!&T^04cjG4*&oF delta 775 zcmZqL!*ux&(*~J-o?zd^;%o&&0|f(xpv_YKJv>Z?29tyOf>_*(5-TS!@AGFgocz5{ zr{2`u$j*+dxTGjGF_)`i&f5!yybK0B2R3|C-|O^Yr<`Gv#%HF_)(`fH1$M7+y`q#Q zxV0n7MRyUkxb;*tiyWisY2YVP+)+=>z$C?DsJ*__id5F)@qWuDPnq36Icg5iWc@kX zj7F2A=j1UOPd+tAkJZG$%-nSH-#K%&EzE#P5(E_T6u7_)69aQgOAIj!QTSTmUxzBa;9C From 5ebb144077daf3df989424e78d9ce14a7bfd5b0b Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Tue, 2 Nov 2021 22:13:38 +0100 Subject: [PATCH 53/67] Updating changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdac6ba81..1fa3786a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). ## [2.4.6] - not released yet ### Added +- Temporary changes to graphics state variables are now possible by `with FPDF.local_context():`, thanks to @gmischler - a mechanism to detect & downscale oversized images, _cf._ [documentation](https://pyfpdf.github.io/fpdf2/Images.html#oversized-images-detection-downscaling). [Feedbacks](https://github.com/PyFPDF/fpdf2/discussions) on this new feature are welcome! @@ -28,6 +29,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). - [`FPDF.solid_arc`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.solid_arc): new method added. It enables to draw solid arcs in a PDF document. A solid arc combines an arc and a triangle to form a pie slice. ### Fixed +- All graphics state manipulations are now possible within a rotation context, thanks to @gmischler - The exception making the "x2" template field optional for barcode elements did not work correctly, fixed by @gmischler ### Changed - All template elements now have a transparent default background instead of white, thanks to @gmischler From 604a6b007a98d714553b09da37749190abb90736 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 10 Nov 2021 21:12:44 +0100 Subject: [PATCH 54/67] include line_width in graphics context stack --- CHANGELOG.md | 2 ++ fpdf/fpdf.py | 21 ++++++++++----------- fpdf/graphics_state.py | 9 +++++++++ test/graphics_context.pdf | Bin 1250 -> 1248 bytes test/rotation.pdf | Bin 1509 -> 1510 bytes test/template/flextemplate_elements.pdf | Bin 11486 -> 11525 bytes test/template/flextemplate_multipage.pdf | Bin 2367 -> 2373 bytes test/template/flextemplate_offset.pdf | Bin 1156 -> 1157 bytes test/template/flextemplate_rotation.pdf | Bin 40450 -> 40521 bytes test/template/template_multipage.pdf | Bin 3021 -> 3032 bytes test/template/template_nominal_csv.pdf | Bin 1638 -> 1641 bytes test_regular_polygon.pdf | Bin 0 -> 1813 bytes 12 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 test_regular_polygon.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 0601e3a4f..3b42366e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). ## [2.4.6] - not released yet ### Added +- Temporary changes to graphics state variables are now possible by `with FPDF.local_context():`, thanks to @gmischler - a mechanism to detect & downscale oversized images, _cf._ [documentation](https://pyfpdf.github.io/fpdf2/Images.html#oversized-images-detection-downscaling). [Feedbacks](https://github.com/PyFPDF/fpdf2/discussions) on this new feature are welcome! @@ -30,6 +31,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). It enables to draw solid arcs in a PDF document. A solid arc combines an arc and a triangle to form a pie slice. - [`FPDF.regular_polygon`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.regular_polygon): new method added. ### Fixed +- All graphics state manipulations are now possible within a rotation context, thanks to @gmischler - The exception making the "x2" template field optional for barcode elements did not work correctly, fixed by @gmischler ### Changed - All template elements now have a transparent default background instead of white, thanks to @gmischler diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 66b2f28db..3795f4699 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -266,6 +266,8 @@ def __init__( self.lasth = 0 # height of last cell printed self.current_font = {} # current font self.str_alias_nb_pages = "{nb}" + # Scale factor + self.k = get_scale_factor(unit) # graphics state variables from the stack self.font_family = "" # current font family self.font_style = "" # current font style @@ -276,6 +278,7 @@ def __init__( self.fill_color = "0 g" self.text_color = "0 g" self.dash_pattern = "[] 0 d" + self.line_width = 0.567 / self.k # line width (0.2 mm) # font_size is initialized below after the standard fonts have been set up # end of grapics state variables self.ws = 0 # word spacing @@ -325,8 +328,6 @@ def __init__( "couriernew": "courier", "timesnewroman": "times", } - # Scale factor - self.k = get_scale_factor(unit) self.dw_pt, self.dh_pt = get_page_format(format, self.k) self._set_orientation(orientation, self.dw_pt, self.dh_pt) @@ -341,7 +342,6 @@ def __init__( self.set_margins(margin, margin) self.x, self.y = self.l_margin, self.t_margin self.c_margin = margin / 10.0 # Interior cell margin (1 mm) - self.line_width = 0.567 / self.k # line width (0.2 mm) # sets self.auto_page_break, self.b_margin & self.page_break_trigger: self.set_auto_page_break(True, 2 * margin) self.set_display_mode("fullwidth") # Full width display mode @@ -1788,14 +1788,12 @@ def rotation(self, angle, x=None, y=None): angle *= math.pi / 180 c, s = math.cos(angle), math.sin(angle) cx, cy = x * self.k, (self.h - y) * self.k - self._out( - f"q {c:.5F} {s:.5F} {-s:.5F} {c:.5F} {cx:.2F} {cy:.2F} cm " - f"1 0 0 1 {-cx:.2F} {-cy:.2F} cm\n" - ) - self._push_local_stack() - yield - self._pop_local_stack() - self._out("Q\n") + with self.local_context(): + self._out( + f"{c:.5F} {s:.5F} {-s:.5F} {c:.5F} {cx:.2F} {cy:.2F} cm " + f"1 0 0 1 {-cx:.2F} {-cy:.2F} cm\n" + ) + yield @check_page @contextmanager @@ -1819,6 +1817,7 @@ def local_context(self): font_size_pt font_size dash_pattern + line_width """ self._push_local_stack() self._out("\nq ") diff --git a/fpdf/graphics_state.py b/fpdf/graphics_state.py index c3e9dd4df..1a424ae67 100644 --- a/fpdf/graphics_state.py +++ b/fpdf/graphics_state.py @@ -23,6 +23,7 @@ def __init__(self): font_size_pt=0, font_size=0, dash_pattern="[] 0 d", + line_width=0, ), ] @@ -111,3 +112,11 @@ def dash_pattern(self): @dash_pattern.setter def dash_pattern(self, v): self.__statestack[-1]["dash_pattern"] = v + + @property + def line_width(self): + return self.__statestack[-1]["line_width"] + + @line_width.setter + def line_width(self, v): + self.__statestack[-1]["line_width"] = v diff --git a/test/graphics_context.pdf b/test/graphics_context.pdf index 4d6f59f62a426b17be85fdb5f9080567886b1e8a..607e59d554124a929a8f50547daab70ba877951c 100644 GIT binary patch delta 403 zcmaFF`G9jmL%q439anKlQEFl?SH+yUlYIS{4S8C>TON%*q;#-M&Hip!6RY)-os3&J zRJyDdIX(JfG1)WG?eL-;CGT(5|K3CtU^QmSeHDHNcQDGc>~xvoOXYW@#{a8jA~uF_)^U ItG^o;01$(qkpKVy delta 405 zcmaFB`G|8uL%pS)9anKlQEFl?SH+yUlTQ0F8}hh*|Jk*>#kJ|JkKEm`Cf40vpV==c zO~|+^xI!qt-*~1-T8LPxaO9@fgWnX_PhoN4Fy&HJ Jb@g}S0syROsW<=t diff --git a/test/rotation.pdf b/test/rotation.pdf index 3ea4a5f99503762490b29e6051dba269fc92acc1..c038462801fa88a89d8c29b269ae87840f94cb15 100644 GIT binary patch delta 220 zcmaFL{fv7;L%p$`9anKlQEFl?SH+y%o{d}$20Sj`>yG|)NDj&gE*HQ1hb>)Dz5c#P zwBtmUcRQ**54@SN=uyeLhR9S=$Ke0GE;B8ZR|bf!kzTXOaFf};m8TBpNZxnRSvU81 z$obu#2Ja%~yyd^&?s2fC_0oq&`lqaw?{TH(r9f>=&D;1-fkn*BK*0b66!H|fzzhRJ k14HAA-7+CtX{)i-VTaqiu>eBH`x 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 delta 6251 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 QYGfb@a!$^N+A8G#2Okl`QUCw| diff --git a/test/template/flextemplate_multipage.pdf b/test/template/flextemplate_multipage.pdf index 45e5576e643b5a888471d4121832a0e83da5efac..b8ea16774c235aaa8e5b59118dc7ee2b0d7edeb3 100644 GIT binary patch delta 1388 zcmZXRdo&XY0LHV_)rxGIN^>xetS&ajIId^p(LB3=1J)H%Z>Q z_r{QCZsif$?mXJoiiwiPY>`B?+kO01-yh%k&hPu<`{R2E?gQu1Xx0dOdth9AEHUH~ zkjn1Hc6vWSs4uNT0-a;}qmLH4UvWuS-$?J()AN((eM(_3HS*e(PiuRzCwlsuZCm37 zBnUWV?gZJZZMDQ}hUIzf`tu=|w3f#k?=iVzy1wW+TnC=v|Fc_kgQK;lF8Hh8krqnp zv+!#B!cLgZRMB!Q{asC-|8)~fDZatpvp3-fr33l{)mmx`kF>PnA!h2aZ^5!Wv)GBT zK9-&Zp>}F4VX8|*W6N1|v8&vDzdhPx+<`CJPE~AdC3rZE^?f)z5Vt^xgV{k>YvyKT zOx-`^bFY^OKFZg2^4Sk=Wm0f0b@jq$bjfaFFSOAdL=%oSHJH&mh%oz@$olEQyP{(n zp#N4;`6ioq`$oc8^pcT+%WGByf`)tZlgU`|C1U{H6{He$9B}Q9O5NZu(i8apBb6LG z<408;ot#W#2Yq0KRW;2D|-9=j*9A-Lj5*Yvr{BDb1Vf!a+t9Hz?EW zCPYtjt!G2e<>v1rUih*s5kUgtMijEF(hkL$&*l@JH41)DE#bz(?JotbZk;RDgVGIR zZ=w=3@%Jvc5F?BQaWl?N9=N|jRJTMx`B`1?I|fvGo<%YBY5~8ws*qKjRTR60^~y`# zK}%yofW)Y<|KI+aqC*R)04l%&S+D}orvbKhj)QBQMM`P#)N(#K;7DNLQ@!>j+;WLO z85MCa%_rSaPckKs+IvvPU=a0kSrS{OuB1cGumTv;HRcUjE;S)%Cf5b>wPcA>PVu{) z=N9=SVY#yAdEk#suk%XoyRa1@`=F#hg*W=$na&e@Ym$H)Tr*Na#8L;2Wg624I{dw- zO)6uIoBcV)BPFN0_049{vk{X!4&sh5=!WmJp^*R~bvKj%8m~N_wv437 zO?&6)1?}p$+)3g0VJndfxxjG-lcPPOWPljtc{y2Wpnhtk?dD2J>}^JMdE(hKkJiml zDJ4-+_0UgCW5x41;b1|dJ?(DqDBd+_zT3PssUv66IX3(-WU{V?HGKa!e3;#F*C6S9 zmL|g{u8xx2y27@Fzc_?_JL19QeXMoy0JC0i;+%mJ#_*e#7nN%XPznV!7w;JT_JKJ! z=CpTe_{DX!$n-4dRG@TpHz%oSps;6pGeRSGR84ac#wblOGO#wKEy^juE=DjJzrhW> z5h=!S942oeY_9e@TxY807+buW{1pMJK;?!tskL}w1QtA)Z@!a^u3W)gU|jb3&<(@j z3M2Tq!$QnpI@b>eD@j~73lO)7hu4S0akKKO>99`n%r=h@m&}AFMBE z)qbk>w<}F)LodTV*PbMAl8#ZGAW7YhhB`>N|9NLYfQjouc2p~tv^jJaRl5en)cO}T zB$s_`Dq;ypI%=fI@*pxcY1hX?F6Hd);d}1c=q>^s{$+|GVinDwYBnmRIEX{@j*Ij* zgVAO2`chO`kiHa?=-A#g%aB$G>+cvEf%=ZwAirO-K_d#}Dkt}# NQ9wgOSD%x>zX7t6lAr(p delta 1411 zcmZvZX;9J$0L9JnV$%S1b<|2p@+LF_3GZvIT#r@p)=Du;r4>y&ls|Z+mAaZH>wS@F z2Cb$h=C#y%P>51#P@b7(Ub*7YD7x*=e%PsfpWeLpW_};uAJ9%{HUMBSR#u?cI1=Gh zH0WY+yBFK_2@*WNtmrP@^?d`e&Igy3V+^{d{ptwE4~Gl0S3oB5skz&1NZ;>YRGB6qv|W z$h!WD@Yn`Dg$(8ZzMGC?ZS5dowR9yM5p0ZEH5o^F8fvQoOPpXvbj1rt)I0{ z-lp#U&PPdM2-4v7ducXT2U5VNxlCuO~X)Z$dXWb9C5q z{tE^8NYpuIl3TKk&iLXdV#fK$2d&#-nz&IeQ!cVj}fqj=Ao z1Q-}y@Ih+E!7Z}LECAf{#u}J;8An5P|Q5Q80RGP)9K>?Q+nRqjhQ=34Ku2 zPf+P~ql4QO?zFGzmH^twsFgCcgR*ukeFN5_Q9t z6_yrpc_d1Z+4ww)l-YP#xZ@p{f)nan!^tn4ajC+YXIzQ{CfGzRm^aoCx0%md9Z$9E zu+D3)u^4kqa7q~F2exg59sRr(vX*&!@BO47#98fLR|_a3h@fYE@t0a~a~fMf>7pO- zKqPZUS{12{{9D>YZO}gkX^O$3bMf*x0QGf)#F(0WotdJM|C3?Ozu93;za?SO|6#MR=F^#=yA-FUvZ0tQ`N~qwXWMgiE)~3CGQMz;zLs`rTdAg)r@DIRlM}y+a(+#@ zb})18356L!Ge6zfBETxq@%YPM{fE(hdv$&?ubpEjsqCll^8ChIYi?Ja?EPmTRDZ;* zOW=fC!=w2(|7~JFvS7)MmkSOm%%7=Xb(7)jxi+B$nV>8CCj6MNVzcM|y%YAF;Qizt z{lorKu=hhBt;b7WOt0>HzWr|1@$AHx)m5&GuFF3cd@Hm04C7};QA-5_5Kzcd-~uxY l49v_7CtqQ<)-=VCF*m~$voN%rY{BBfVZ^1X>gw;t1ppk*x4!@Y delta 383 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°T97pRdVz2 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 delta 1704 zcmZXTdpOgJAIA@;g`pjnj8iTdxh1n%$Yt4uCXxqd$v4d;%rQ*1v2r^y$vTvwbI$L1e$Vs!)#tCz^ZL9#&-3}?{oz5`(44CP%Fz)V zM~)2*iUMC|W_b^0Pr;SCq-K{RAwdM!^6HSAk&4Urc;i6M1*l&nW1Lss9q=i)mF0yw zd~HsoB$scm(f+eMHnp2wrQw)#ArP-0t}AL_%g-%XC|4AcRMb%^p17MAQ)iTDtPP9R zE9ao+Fp@Yyi|*S*L5wz)40wb9GD8VN+t{Zwda4J^R*y}?Dg+UqoS%=6Dxi7R36Ap;4Ye0}v$qn#|af2|YHN@4H&gG5fCFBP)7 zuSB6AUYhEV_JRymoc(ns8|=0jgHWfN{5l#{`WPvKSls?O%+erK^-@tR3m z+LaDv*XD`FYOo(bM0)6iTkk#NCO=Lg%aTfNkTrAd>wupzZ8p3^WS+q2zIZ)U)((y= zsy?(LDX8|9TmydE>nq*B+kAJ!;;f<$!F2g*rr9z#!#}q^I!Mf@j|mc^ms3f$@17|x zOuY7ljyH&y%8xXn( zNx8)M45rA-+CXWKzDkoUWK9`)6f&?!XJ*4d=rmq)=Le2ej~H@!dIn zbltOgc%z2>r)ZbkuQqy$e6$SKw|^}jDSI1wm*PEXSrb6qS#RCu`3JDa-uV+$gJ$P$ z33N*V_J_<#USl`c?gL}aWP_k|PIHs%`?MNwMU617*h90Zz_uBAarofS;PW*-H-2R@ ze&ng=1@1BjB0fKsm&3fD3;zv9z-z-}@T|=;AFtZ8I6I#R5{`;x<(R$IrsQt%?U(dl z*)ok-59%(xr(di^2JeSBU&Bx|cd66dOB5peC>rKJ<8*emro?5m+J!z}>Kh&-n`ZA} z`#8V)mzTVV{#?qacITS}NWjAkaf97Z!OpjLsmU^!JY`U(3@Wb>1Ouw!r-!Q}Yty0? zv1SIz4#fir7dqWr3Mp40KrAA=XCQqZ@z7nV>n;ydB+rktIkPy}K50Q#^vWfdla}ReYAo)l zc=9WOj_rMYsTvD|Gopr#d!2sS)()U8JVLl@(yZt+XghS$g6z_JLnXZ0YZu zv@(w@L;4w9b6YxM3H}jrluX5d*!8ClI#+`0Gzae&r@MQ4HQ%u?3Rzzyfr$C8u|X|f z#1qHr4GL2o^VuwH%w8VcbCSPc?wL6c8Z&KNpT2ofPdy-8!~T{RS4Pc;dtWyZ-`a_9 z32E)ehqQ!o6*`)PxzOjouqzIiYxh$A`1B$XIxOFI6xRPf(7-r9!|9kMZ??FH$o%J( zoo7YEpBn{&9hwIOSZNdNOo@Zt9OIJQ2QVeIOq~;BE@8{4H;Y~EZ<>Vc8JYobU$UAtKhhEX~4sH2SEu)?*6f`?-(USao zpEsxfI`d(n;S1(JdG|^_ypm<)lw!Wie5aJt@a5lH=NA^C6^@+?)E2P4I2i4^{`q_6 zn#k*{(t&ql%xn8go8SGAxo%3#+$9Mnx`6d0?@>eDz zT1h%QwNf2#e?H~$kyh?MQgENy@zABs^|FoMnoO=&?Yq~fDzg2~x##_NU%z|zHYc;- ztNqLjg9}Aw@A~cMrFX8}bkXL-BONQxFEgYLbzOVjW-^O^X^UF!$J8r}%2Q3YmpVya zT^C@~lA^UCL1VYts!O1R@wFv-AzaJ*5uw461%o;gL2T)`;B7f zwm)(dySA;MDG?Tkrp! z+`i}3;g{z;7y5owia-C?UU#W$ zI1}o)e8I{Cf6FVj{{AcVOFohMs!7m6rX?GEzf4Rv37_HpYZ ztxfxbG+j!9rd&&(>$-4-yKz8J$>Qoo73*}X-?Z7?x1?l;>Ta#z5Gyq^K{6` zg9b|*w^{9zY0Qk2nZWw4?7)PLe`0zMysB?0IqzGuQ{woFx_vYB=l|aR_w4V@3vW*k z6-s>a^Wcu#|7)zYeWuQx8ocJ-!s|Z%l~PGBoMyVEr@CilT)P(FF3T40e%ePp>6%~~n5S8FzF?On;u?$4z*EHatHy?CirG*i~$ohn}z>b;Zc%0AH6`Rb9K zd}o`fe*KDT4W&Q!a0d6xS{ z`mHvXaXz>C+FN-(k0tI7M_!?FuVRvl#+1*B3nbPQ`D_RcHjc9Ei23)k|9;u)|Azv^ z5-nmaEH_%teR6xD4Es5WQ|)IHtuOy)*JbPI;!4d+DK6POlj$LosEL9B2q@$!aDf>H xh6bjlllxh%0}aq+3@pqHF~lrQEz!jc4b3sdj7-fZOS8Fg7;veoy863u0RVzYAdmn6 diff --git a/test_regular_polygon.pdf b/test_regular_polygon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b1387d8366f5503d4190cb5db38a66d70427804e GIT binary patch literal 1813 zcmZXVc~BE~6vwqSm9}(y z&fv`xl+g`$h215bq?#(1fxO}F4q|rG;8M2&P5U-qXKG=YU1+fuF>u7!GYg3Y@=EV> zd3M;VP@8AhULDd89TSKOd_@?%nu`?Fxc~8b;YPXlqu#!~*?#HI$7fwT+++1j+;v40 zA|26{&HVXp_E9Y{(Q{vGDnTQhg@Z|3>vLiYT3(byy!8G@SFu#jy-Lv?3Qe4xR?+`- zAkeSQjJ}+LgsW65bEV>+Do;TC+#csxiWT9O#9eew(E8-&RA!OIQVLSGtWo8Va7bbv z{N=To)IDYO8cT~{E|Web7_RK|e4c3gdVbfvn^PI(j=eOUo%u$7w$=6`6ifHAEtZ#F zoL77*c`>H0>hIe--pc#Zp-~X~vV$(b~$tE|vDZ z8PH_rU;)SZd2aK{@5#zTa16-1aH;5GNybH8yu3nk6~4w^Y6h zP(8|Lk31apQRoamKnq}P} z#(Sdb2#X&sc-a8nVOfF4Yl2!6M%p*U{OvJi*rMmaMr6g6%*~YEOlC{i*Tq6*X{@FF z2KPv_H@7`S-gaE!br1cXy)#qPRio~-`%QKPZF$g-LyrttlU2FKnxd^+BP0`4t92Pk zTs7dS_cJKc9{aYQ&$7Hap`?u#Ut3YtHh&pr3(uh*ZLluVSicxQCmk2IJ#8odxZdfG zLsm0QM_ANe@KOWbr>z^{4uPb#eoXZ+qw3DtMfV5&d!f#P2{q$EM;ynuz)tuiI;#W( z4tL$H?sH)G$D0>H^y0h*2eU@*6vK|XH|I2IM6k^S!&do;4*MIcViq#8I>V!6*LFSF zmfq0}PoQj%%CCjNb&+yt=w26GmvfFw)|8kw-89inBz2@HMF3p`>xbe;9TYfB`dEFv< zuQEi*a}1wncDpuI6VQ-D6*lB|5f@jS@zVabfVG!DhX|jZHZmt|VgU6-w-H4qu$Z7^ zgxF_@O9mkSI9W0nW&~hJH~JGZInRItj4J11Ymqvpc{w7 z;(+TxlaJ}@Mt0A*K*a|ii%ll@Umt04KliyW`M;0JW`C~7=92T~y&jIiadHGB2N?i` t#%)5#^q>$F#X)0J$QdS9tVcm(TSJVMB;zoKzpaDG;VuE}?R^79z`vl@=i&eW literal 0 HcmV?d00001 From 29c42669215158ec3f6fc04fec01dab2d06c6f81 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 10 Nov 2021 22:01:20 +0100 Subject: [PATCH 55/67] hopefully fixing line endings --- CHANGELOG.md | 514 +++++++++++++++++++++++++-------------------------- 1 file changed, 257 insertions(+), 257 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b42366e2..7454adec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,257 +1,257 @@ -Changelog ---------- - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/) -and this project adheres to [Semantic Versioning](http://semver.org/), -and [PEP 440](https://www.python.org/dev/peps/pep-0440/). - -## [2.4.6] - not released yet -### Added -- Temporary changes to graphics state variables are now possible by `with FPDF.local_context():`, thanks to @gmischler -- a mechanism to detect & downscale oversized images, - _cf._ [documentation](https://pyfpdf.github.io/fpdf2/Images.html#oversized-images-detection-downscaling). - [Feedbacks](https://github.com/PyFPDF/fpdf2/discussions) on this new feature are welcome! -- New `set_dash_pattern()`, which works with all lines and curves, thanks to @gmischler. -- Templates now support drawing ellipses, thanks to @gmischler -- New documentation on how to display equations, using Google Charts or `matplotlib`: [Maths](https://pyfpdf.github.io/fpdf2/Maths.html) -- The whole documentation can now be downloaded as a PDF: [fpdf2-manual.pdf](pyfpdf.github.io/fpdf2/fpdf2-manual.pdf) -- New sections have been added to [the tutorial](https://pyfpdf.github.io/fpdf2/Tutorial.html), thanks to @portfedh: - - 5. [Creating Tables](https://pyfpdf.github.io/fpdf2/Tutorial.html#tuto-5-creating-tables) - 6. [Creating links and mixing text styles](https://pyfpdf.github.io/fpdf2/Tutorial.html#tuto-6-creating-links-and-mixing-text-styles) -- New translation of the tutorial in Hindi, thanks to @Mridulbirla13: [हिंदी संस्करण](https://pyfpdf.github.io/fpdf2/Tutorial-हिंदी.html); [Deutsch](https://pyfpdf.github.io/fpdf2/Tutorial-de.html), thanks to @digidigital; and [Italian](https://pyfpdf.github.io/fpdf2/Tutorial-it.html) thanks to @xit4; [Русский](https://pyfpdf.github.io/fpdf2/Tutorial-ru.html) thanks to @AABur; and [português](https://pyfpdf.github.io/fpdf2/Tutorial-pt.html) thanks to @fuscati; [français](https://pyfpdf.github.io/fpdf2/Tutorial-fr.html), thanks to @Tititesouris -- While images transparency is still handled by default through the use of `SMask`, - this can be disabled by setting `pdf.allow_images_transparency = False` - in order to allow compliance with [PDF/A-1](https://en.wikipedia.org/wiki/PDF/A#Description) -- [`FPDF.arc`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.arc): new method added. - It enables to draw arcs in a PDF document. -- [`FPDF.solid_arc`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.solid_arc): new method added. - It enables to draw solid arcs in a PDF document. A solid arc combines an arc and a triangle to form a pie slice. -- [`FPDF.regular_polygon`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.regular_polygon): new method added. -### Fixed -- All graphics state manipulations are now possible within a rotation context, thanks to @gmischler -- The exception making the "x2" template field optional for barcode elements did not work correctly, fixed by @gmischler -### Changed -- All template elements now have a transparent default background instead of white, thanks to @gmischler -- To reduce the size of generated PDFs, no `SMask` entry is inserted for images that are fully opaque - (= with an alpha channel containing only 0xff characters) -- The `rect`, `ellipse` & `circle` all have a `style` parameter in common. - They now all properly accept a value of `"D"` and raise a `ValueError` for invalid values. -### Deprecated -- `dashed_line()` is now deprecated in favor of `set_dash_pattern()` - -## [2.4.5] - 2021-10-03 -### Fixed -- ensure support for old field names in `Template.code39` for backward compatibility - -## [2.4.4] - 2021-10-01 -### Added -- `Template()` has gained a more flexible cousin `FlexTemplate()`, _cf._ [documentation](https://pyfpdf.github.io/fpdf2/Templates.html), 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 -- a subclass of `HTML2FPDF` can now easily be used by setting `FPDF.HTML2FPDF_CLASS`, - _cf._ [documentation](https://pyfpdf.github.io/fpdf2/DocumentOutlineAndTableOfContents.html#with-html) -### 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 -- `cell(..., markdown=True)` "leaked" its final style (bold / italics / underline) onto the following cells -### Changed -- `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. -### Deprecated -- `Template`: `code39` barcode input field names changed from `x/y/w/h` to `x1/y1/y2/size` - -## [2.4.3] - 2021-09-01 -### Added -- support for **emojis**! More precisely unicode characters above `0xFFFF` in general, thanks to @moe-25 -- `Template` can now insert justified text -- [`get_scale_factor`](https://pyfpdf.github.io/fpdf2/fpdf/util.html#fpdf.util.get_scale_factor) utility function to obtain `FPDF.k` without having to create a document -- [`convert_unit`](https://pyfpdf.github.io/fpdf2/fpdf/util.html#fpdf.util.convert_unit) utility function to convert a number, `x,y` point, or list of `x,y` points from one unit to another unit -### Changed -- `fpdf.FPDF()` constructor now accepts ints or floats as a unit, and raises a `ValueError` if an invalid unit is provided. -### Fixed -- `Template` `background` property is now properly supported - [#203](https://github.com/PyFPDF/fpdf2/pull/203) - ⚠️ Beware that its default value changed from `0` to `0xffffff`, as a value of **zero would render the background as black**. -- `Template.parse_csv`: preserving numeric values when using CSV based templates - [#205](https://github.com/PyFPDF/fpdf2/pull/205) -- the code snippet to generate Code 39 barcodes in the documentation was missing the start & end `*` characters. -This has been fixed, and a warning is now triggered by the [`FPDF.code39`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.code39) method when those characters are missing. -### Fixed -- Detect missing `uni=True` when loading cached fonts (page numbering was missing digits) - -## [2.4.2] - 2021-06-29 -### Added -- disable font caching when `fpdf.FPDF` constructor invoked with `font_cache_dir=None`, thanks to @moe-25 ! -- [`FPDF.circle`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.circle): new method added, thanks to @viraj-shah18 ! -- `write_html`: support setting HTML font colors by name and short hex codes -- [`FPDF.will_page_break`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.will_page_break) -utility method to let users know in advance when adding an elemnt will trigger a page break. -This can be useful to repeat table headers on each page for exemple, -_cf._ [documentation on Tables](https://pyfpdf.github.io/fpdf2/Tables.html#repeat-table-header-on-each-page). -- [`FPDF.set_link`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) now support a new optional `x` parameter to set the horizontal position after following the link -### Fixed -- fixed a bug when `fpdf.Template` was used to render QRCodes, due to a forced conversion to string (#175) - -## [2.4.1] - 2021-06-12 -### Fixed -- erroneous page breaks occured for full-width / full-height images -- rendering issue of non-ASCII characaters with unicode fonts - -## [2.4.0] - 2021-06-11 -### Changed -- now `fpdf2` uses the newly supported `DCTDecode` image filter for JPEG images, - instead of `FlateDecode` before, in order to improve the compression ratio without any image quality loss. - On test images, this reduced the size of embeded JPEG images by 90%. -- `FPDF.cell`: the `w` (width) parameter becomes optional, with a default value of `None`, meaning to generate a cell with the size of the text content provided -- the `h` (height) parameter of the `cell`, `multi_cell` & `write` methods gets a default value change, `None`, meaning to use the current font size -- removed the useless `w` & `h` parameters of the `FPDF.text_annotation()` method -### Added -- new `FPDF.add_action()` method, documented in the [Annotations section](https://pyfpdf.github.io/fpdf2/Annotations.html) -- `FPDF.cell`: new optional `markdown=True` parameter that enables basic Markdown-like styling: `**bold**, __italics__, --underlined--` -- `FPDF.cell`: new optional boolean `center` parameter that positions the cell horizontally -- `FPDF.set_link`: new optional `zoom` parameter that sets the zoom level after following the link. - Currently ignored by Sumatra PDF Reader, but observed by Adobe Acrobat reader. -- `write_html`: now support `align="justify"` -- new method `FPDF.image_filter` to control the image filters used for images -- `FPDF.add_page`: new optional `duration` & `transition` parameters - used for [presentations (documentation page)](https://pyfpdf.github.io/fpdf2/Presentations.html) -- extra documentation on [how to configure different page formats for specific pages](https://pyfpdf.github.io/fpdf2/PageFormatAndOrientation.html) -- support for Code 39 barcodes in `fpdf.template`, using `type="C39"` -### Fixed -- avoid an `Undefined font` error when using `write_html` with unicode bold or italics fonts -### Deprecated -- the `FPDF.set_doc_option()` method is deprecated in favour of just setting the `core_fonts_encoding` property - on an instance of `FPDF` -- the `fpdf.SYSTEM_TTFONTS` configurable module constant is now ignored - -## [2.3.5] - 2021-05-12 -### Fixed -- a bug in the `deprecation` module that prevented to configure `fpdf2` constants at the module level - -## [2.3.4] - 2021-04-30 -### Fixed -- a "fake duplicates" bug when a `Pillow.Image.Image` was passed to `FPDF.image` - -## [2.3.3] - 2021-04-21 -### Added -- new features: **document outline & table of contents**! Check out the new dedicated [documentation page](https://pyfpdf.github.io/fpdf2/DocumentOutlineAndTableOfContents.html) for more information -- new method `FPDF.text_annotation` to insert... Text Annotations -- `FPDF.image` now also accepts an `io.BytesIO` as input -### Fixed -- `write_html`: properly handling `` inside `` & allowing to center them horizontally - -## [2.3.2] - 2021-03-27 -### Added -- `FPDF.set_xmp_metadata` -- made `
  • ` bullets & indentation configurable through class attributes, instance attributes or optional method arguments, _cf._ [`test_customize_ul`](https://github.com/PyFPDF/fpdf2/blob/2.3.2/test/html/test_html.py#L242) -### Fixed -- `FPDF.multi_cell`: line wrapping with justified content and unicode fonts, _cf._ [#118](https://github.com/PyFPDF/fpdf2/issues/118) -- `FPDF.multi_cell`: when `ln=3`, automatic page breaks now behave correctly at the bottom of pages - -## [2.3.1] - 2021-02-28 -### Added -- `FPDF.polyline` & `FPDF.polygon` : new methods added by @uovodikiwi - thanks! -- `FPDF.set_margin` : new method to set the document right, left, top & bottom margins to the same value at once -- `FPDF.image` now accepts new optional `title` & `alt_text` parameters defining the image title - and alternative text describing it, for accessibility purposes -- `FPDF.link` now honor its `alt_text` optional parameter and this alternative text describing links - is now properly included in the resulting PDF document -- the document language can be set using `FPDF.set_lang` -### Fixed -- `FPDF.unbreakable` so that no extra page jump is performed when `FPDF.multi_cell` is called inside this context -### Deprecated -- `fpdf.FPDF_CACHE_MODE` & `fpdf.FPDF_CACHE_DIR` in favor of a configurable new `font_cache_dir` optional argument of the `fpdf.FPDF` constructor - -## [2.3.0] - 2021-01-29 -Many thanks to [@eumiro](https://github.com/PyFPDF/fpdf2/pulls?q=is%3Apr+author%3Aeumiro) & [@fbernhart](https://github.com/PyFPDF/fpdf2/pulls?q=is%3Apr+author%3Aeumiro) for their contributions to make `fpdf2` code cleaner! -### Added -- `FPDF.unbreakable` : a new method providing a context-manager in which automatic page breaks are disabled. - _cf._ https://pyfpdf.github.io/fpdf2/PageBreaks.html -- `FPDF.epw` & `FPDF.eph` : new `@property` methods to retrieve the **effective page width / height**, that is the page width / height minus its horizontal / vertical margins. -- `FPDF.image` now accepts also a `Pillow.Image.Image` as input -- `FPDF.multi_cell` parameters evolve in order to generate tables with multiline text in cells: - * its `ln` parameter now accepts a value of `3` that sets the new position to the right without altering vertical offset - * a new optional `max_line_height` parameter sets a maximum height of each sub-cell generated -- new documentation pages : how to add content to existing PDFs, HTML, links, tables, text styling & page breaks -- all PDF samples are now validated using 3 different PDF checkers -### Fixed -- `FPDF.alias_nb_pages`: fixed this feature that was broken since v2.0.6 -- `FPDF.set_font`: fixed a bug where calling it several times, with & without the same parameters, -prevented strings passed first to the text-rendering methods to be displayed. -### Deprecated -- the `dest` parameter of `FPDF.output` method - -## [2.2.0] - 2021-01-11 -### Added -- new unit tests, a code formatter (`black`) and a linter (`pylint`) to improve code quality -- new boolean parameter `table_line_separators` for `write_html` & underlying `HTML2FPDF` constructor -### Changed -- the documentation URL is now simply https://pyfpdf.github.io/fpdf2/ -### Removed -- dropped support for external font definitions in `.font` Python files, that relied on a call to `exec` -### Deprecated -- the `type` parameter of `FPDF.image` method -- the `infile` parameter of `Template` constructor -- the `dest` parameter of `Template.render` method - -## [2.1.0] - 2020-12-07 -### Added -* [Introducing a rect_clip() function](https://github.com/reingart/pyfpdf/pull/158) -* [Adding support for Contents alt text on Links](https://github.com/reingart/pyfpdf/pull/163) -### Modified -* [Making FPDF.output() x100 time faster by using a bytearray buffer](https://github.com/reingart/pyfpdf/pull/164) -* Fix user's font path ([issue](https://github.com/reingart/pyfpdf/issues/166) [PR](https://github.com/PyFPDF/fpdf2/pull/14)) -### Deprecated -* [Deprecating .rotate() and introducing .rotation() context manager](https://github.com/reingart/pyfpdf/pull/161) -### Fixed -* [Fixing #159 issue with set_link + adding GitHub Actions pipeline & badges](https://github.com/reingart/pyfpdf/pull/160) -* `User defined path to font is ignored` -### Removed -* non-necessary dependency on `numpy` -* support for Python 2 - -## [2.0.6] - 2020-10-26 -### Added -* Python 3.9 is now supported - -## [2.0.5] - 2020-04-01 -### Added -* new specific exceptions: `FPDFException` & `FPDFPageFormatException` -* tests to increase line coverage in `image_parsing` module -* a test which uses most of the HTML features -### Fixed -* handling of fonts by the HTML mixin (weight and style) - thanks `cgfrost`! - -## [2.0.4] - 2020-03-26 -### Fixed -* images centering - thanks `cgfrost`! -* added missing import statment for `urlopen` in `image_parsing` module -* changed urlopen import from `six` library to maintain python2 compatibility - -## [2.0.3] - 2020-01-03 -### Added -* Ability to use a `BytesIO` buffer directly. This can simplify loading `matplotlib` plots into the PDF. -### Modified -* `load_resource` now return argument if type is `BytesIO`, else load. - -## [2.0.1] - 2018-11-15 -### Modified -* introduced a dependency to `numpy` to improve performances by replacing pixel regexes in image parsing (s/o @pennersr) - -## [2.0.0] - 2017-05-04 -### Added -* support for more recent Python versions -* more documentation -### Fixed -* PDF syntax error when version is > 1.3 due to an invalid `/Transparency` dict -### Modified -* turned `accept_page_break` into a property -* unit tests now use the standard `unittest` lib -* massive code cleanup using `flake8` +Changelog +--------- + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/), +and [PEP 440](https://www.python.org/dev/peps/pep-0440/). + +## [2.4.6] - not released yet +### Added +- Temporary changes to graphics state variables are now possible by `with FPDF.local_context():`, thanks to @gmischler +- a mechanism to detect & downscale oversized images, + _cf._ [documentation](https://pyfpdf.github.io/fpdf2/Images.html#oversized-images-detection-downscaling). + [Feedbacks](https://github.com/PyFPDF/fpdf2/discussions) on this new feature are welcome! +- New `set_dash_pattern()`, which works with all lines and curves, thanks to @gmischler. +- Templates now support drawing ellipses, thanks to @gmischler +- New documentation on how to display equations, using Google Charts or `matplotlib`: [Maths](https://pyfpdf.github.io/fpdf2/Maths.html) +- The whole documentation can now be downloaded as a PDF: [fpdf2-manual.pdf](pyfpdf.github.io/fpdf2/fpdf2-manual.pdf) +- New sections have been added to [the tutorial](https://pyfpdf.github.io/fpdf2/Tutorial.html), thanks to @portfedh: + + 5. [Creating Tables](https://pyfpdf.github.io/fpdf2/Tutorial.html#tuto-5-creating-tables) + 6. [Creating links and mixing text styles](https://pyfpdf.github.io/fpdf2/Tutorial.html#tuto-6-creating-links-and-mixing-text-styles) +- New translation of the tutorial in Hindi, thanks to @Mridulbirla13: [हिंदी संस्करण](https://pyfpdf.github.io/fpdf2/Tutorial-हिंदी.html); [Deutsch](https://pyfpdf.github.io/fpdf2/Tutorial-de.html), thanks to @digidigital; and [Italian](https://pyfpdf.github.io/fpdf2/Tutorial-it.html) thanks to @xit4; [Русский](https://pyfpdf.github.io/fpdf2/Tutorial-ru.html) thanks to @AABur; and [português](https://pyfpdf.github.io/fpdf2/Tutorial-pt.html) thanks to @fuscati; [français](https://pyfpdf.github.io/fpdf2/Tutorial-fr.html), thanks to @Tititesouris +- While images transparency is still handled by default through the use of `SMask`, + this can be disabled by setting `pdf.allow_images_transparency = False` + in order to allow compliance with [PDF/A-1](https://en.wikipedia.org/wiki/PDF/A#Description) +- [`FPDF.arc`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.arc): new method added. + It enables to draw arcs in a PDF document. +- [`FPDF.solid_arc`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.solid_arc): new method added. + It enables to draw solid arcs in a PDF document. A solid arc combines an arc and a triangle to form a pie slice. +- [`FPDF.regular_polygon`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.regular_polygon): new method added. +### Fixed +- All graphics state manipulations are now possible within a rotation context, thanks to @gmischler +- The exception making the "x2" template field optional for barcode elements did not work correctly, fixed by @gmischler +### Changed +- All template elements now have a transparent default background instead of white, thanks to @gmischler +- To reduce the size of generated PDFs, no `SMask` entry is inserted for images that are fully opaque + (= with an alpha channel containing only 0xff characters) +- The `rect`, `ellipse` & `circle` all have a `style` parameter in common. + They now all properly accept a value of `"D"` and raise a `ValueError` for invalid values. +### Deprecated +- `dashed_line()` is now deprecated in favor of `set_dash_pattern()` + +## [2.4.5] - 2021-10-03 +### Fixed +- ensure support for old field names in `Template.code39` for backward compatibility + +## [2.4.4] - 2021-10-01 +### Added +- `Template()` has gained a more flexible cousin `FlexTemplate()`, _cf._ [documentation](https://pyfpdf.github.io/fpdf2/Templates.html), 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 +- a subclass of `HTML2FPDF` can now easily be used by setting `FPDF.HTML2FPDF_CLASS`, + _cf._ [documentation](https://pyfpdf.github.io/fpdf2/DocumentOutlineAndTableOfContents.html#with-html) +### 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 +- `cell(..., markdown=True)` "leaked" its final style (bold / italics / underline) onto the following cells +### Changed +- `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. +### Deprecated +- `Template`: `code39` barcode input field names changed from `x/y/w/h` to `x1/y1/y2/size` + +## [2.4.3] - 2021-09-01 +### Added +- support for **emojis**! More precisely unicode characters above `0xFFFF` in general, thanks to @moe-25 +- `Template` can now insert justified text +- [`get_scale_factor`](https://pyfpdf.github.io/fpdf2/fpdf/util.html#fpdf.util.get_scale_factor) utility function to obtain `FPDF.k` without having to create a document +- [`convert_unit`](https://pyfpdf.github.io/fpdf2/fpdf/util.html#fpdf.util.convert_unit) utility function to convert a number, `x,y` point, or list of `x,y` points from one unit to another unit +### Changed +- `fpdf.FPDF()` constructor now accepts ints or floats as a unit, and raises a `ValueError` if an invalid unit is provided. +### Fixed +- `Template` `background` property is now properly supported - [#203](https://github.com/PyFPDF/fpdf2/pull/203) + ⚠️ Beware that its default value changed from `0` to `0xffffff`, as a value of **zero would render the background as black**. +- `Template.parse_csv`: preserving numeric values when using CSV based templates - [#205](https://github.com/PyFPDF/fpdf2/pull/205) +- the code snippet to generate Code 39 barcodes in the documentation was missing the start & end `*` characters. +This has been fixed, and a warning is now triggered by the [`FPDF.code39`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.code39) method when those characters are missing. +### Fixed +- Detect missing `uni=True` when loading cached fonts (page numbering was missing digits) + +## [2.4.2] - 2021-06-29 +### Added +- disable font caching when `fpdf.FPDF` constructor invoked with `font_cache_dir=None`, thanks to @moe-25 ! +- [`FPDF.circle`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.circle): new method added, thanks to @viraj-shah18 ! +- `write_html`: support setting HTML font colors by name and short hex codes +- [`FPDF.will_page_break`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.will_page_break) +utility method to let users know in advance when adding an elemnt will trigger a page break. +This can be useful to repeat table headers on each page for exemple, +_cf._ [documentation on Tables](https://pyfpdf.github.io/fpdf2/Tables.html#repeat-table-header-on-each-page). +- [`FPDF.set_link`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) now support a new optional `x` parameter to set the horizontal position after following the link +### Fixed +- fixed a bug when `fpdf.Template` was used to render QRCodes, due to a forced conversion to string (#175) + +## [2.4.1] - 2021-06-12 +### Fixed +- erroneous page breaks occured for full-width / full-height images +- rendering issue of non-ASCII characaters with unicode fonts + +## [2.4.0] - 2021-06-11 +### Changed +- now `fpdf2` uses the newly supported `DCTDecode` image filter for JPEG images, + instead of `FlateDecode` before, in order to improve the compression ratio without any image quality loss. + On test images, this reduced the size of embeded JPEG images by 90%. +- `FPDF.cell`: the `w` (width) parameter becomes optional, with a default value of `None`, meaning to generate a cell with the size of the text content provided +- the `h` (height) parameter of the `cell`, `multi_cell` & `write` methods gets a default value change, `None`, meaning to use the current font size +- removed the useless `w` & `h` parameters of the `FPDF.text_annotation()` method +### Added +- new `FPDF.add_action()` method, documented in the [Annotations section](https://pyfpdf.github.io/fpdf2/Annotations.html) +- `FPDF.cell`: new optional `markdown=True` parameter that enables basic Markdown-like styling: `**bold**, __italics__, --underlined--` +- `FPDF.cell`: new optional boolean `center` parameter that positions the cell horizontally +- `FPDF.set_link`: new optional `zoom` parameter that sets the zoom level after following the link. + Currently ignored by Sumatra PDF Reader, but observed by Adobe Acrobat reader. +- `write_html`: now support `align="justify"` +- new method `FPDF.image_filter` to control the image filters used for images +- `FPDF.add_page`: new optional `duration` & `transition` parameters + used for [presentations (documentation page)](https://pyfpdf.github.io/fpdf2/Presentations.html) +- extra documentation on [how to configure different page formats for specific pages](https://pyfpdf.github.io/fpdf2/PageFormatAndOrientation.html) +- support for Code 39 barcodes in `fpdf.template`, using `type="C39"` +### Fixed +- avoid an `Undefined font` error when using `write_html` with unicode bold or italics fonts +### Deprecated +- the `FPDF.set_doc_option()` method is deprecated in favour of just setting the `core_fonts_encoding` property + on an instance of `FPDF` +- the `fpdf.SYSTEM_TTFONTS` configurable module constant is now ignored + +## [2.3.5] - 2021-05-12 +### Fixed +- a bug in the `deprecation` module that prevented to configure `fpdf2` constants at the module level + +## [2.3.4] - 2021-04-30 +### Fixed +- a "fake duplicates" bug when a `Pillow.Image.Image` was passed to `FPDF.image` + +## [2.3.3] - 2021-04-21 +### Added +- new features: **document outline & table of contents**! Check out the new dedicated [documentation page](https://pyfpdf.github.io/fpdf2/DocumentOutlineAndTableOfContents.html) for more information +- new method `FPDF.text_annotation` to insert... Text Annotations +- `FPDF.image` now also accepts an `io.BytesIO` as input +### Fixed +- `write_html`: properly handling `` inside `` & allowing to center them horizontally + +## [2.3.2] - 2021-03-27 +### Added +- `FPDF.set_xmp_metadata` +- made `
  • ` bullets & indentation configurable through class attributes, instance attributes or optional method arguments, _cf._ [`test_customize_ul`](https://github.com/PyFPDF/fpdf2/blob/2.3.2/test/html/test_html.py#L242) +### Fixed +- `FPDF.multi_cell`: line wrapping with justified content and unicode fonts, _cf._ [#118](https://github.com/PyFPDF/fpdf2/issues/118) +- `FPDF.multi_cell`: when `ln=3`, automatic page breaks now behave correctly at the bottom of pages + +## [2.3.1] - 2021-02-28 +### Added +- `FPDF.polyline` & `FPDF.polygon` : new methods added by @uovodikiwi - thanks! +- `FPDF.set_margin` : new method to set the document right, left, top & bottom margins to the same value at once +- `FPDF.image` now accepts new optional `title` & `alt_text` parameters defining the image title + and alternative text describing it, for accessibility purposes +- `FPDF.link` now honor its `alt_text` optional parameter and this alternative text describing links + is now properly included in the resulting PDF document +- the document language can be set using `FPDF.set_lang` +### Fixed +- `FPDF.unbreakable` so that no extra page jump is performed when `FPDF.multi_cell` is called inside this context +### Deprecated +- `fpdf.FPDF_CACHE_MODE` & `fpdf.FPDF_CACHE_DIR` in favor of a configurable new `font_cache_dir` optional argument of the `fpdf.FPDF` constructor + +## [2.3.0] - 2021-01-29 +Many thanks to [@eumiro](https://github.com/PyFPDF/fpdf2/pulls?q=is%3Apr+author%3Aeumiro) & [@fbernhart](https://github.com/PyFPDF/fpdf2/pulls?q=is%3Apr+author%3Aeumiro) for their contributions to make `fpdf2` code cleaner! +### Added +- `FPDF.unbreakable` : a new method providing a context-manager in which automatic page breaks are disabled. + _cf._ https://pyfpdf.github.io/fpdf2/PageBreaks.html +- `FPDF.epw` & `FPDF.eph` : new `@property` methods to retrieve the **effective page width / height**, that is the page width / height minus its horizontal / vertical margins. +- `FPDF.image` now accepts also a `Pillow.Image.Image` as input +- `FPDF.multi_cell` parameters evolve in order to generate tables with multiline text in cells: + * its `ln` parameter now accepts a value of `3` that sets the new position to the right without altering vertical offset + * a new optional `max_line_height` parameter sets a maximum height of each sub-cell generated +- new documentation pages : how to add content to existing PDFs, HTML, links, tables, text styling & page breaks +- all PDF samples are now validated using 3 different PDF checkers +### Fixed +- `FPDF.alias_nb_pages`: fixed this feature that was broken since v2.0.6 +- `FPDF.set_font`: fixed a bug where calling it several times, with & without the same parameters, +prevented strings passed first to the text-rendering methods to be displayed. +### Deprecated +- the `dest` parameter of `FPDF.output` method + +## [2.2.0] - 2021-01-11 +### Added +- new unit tests, a code formatter (`black`) and a linter (`pylint`) to improve code quality +- new boolean parameter `table_line_separators` for `write_html` & underlying `HTML2FPDF` constructor +### Changed +- the documentation URL is now simply https://pyfpdf.github.io/fpdf2/ +### Removed +- dropped support for external font definitions in `.font` Python files, that relied on a call to `exec` +### Deprecated +- the `type` parameter of `FPDF.image` method +- the `infile` parameter of `Template` constructor +- the `dest` parameter of `Template.render` method + +## [2.1.0] - 2020-12-07 +### Added +* [Introducing a rect_clip() function](https://github.com/reingart/pyfpdf/pull/158) +* [Adding support for Contents alt text on Links](https://github.com/reingart/pyfpdf/pull/163) +### Modified +* [Making FPDF.output() x100 time faster by using a bytearray buffer](https://github.com/reingart/pyfpdf/pull/164) +* Fix user's font path ([issue](https://github.com/reingart/pyfpdf/issues/166) [PR](https://github.com/PyFPDF/fpdf2/pull/14)) +### Deprecated +* [Deprecating .rotate() and introducing .rotation() context manager](https://github.com/reingart/pyfpdf/pull/161) +### Fixed +* [Fixing #159 issue with set_link + adding GitHub Actions pipeline & badges](https://github.com/reingart/pyfpdf/pull/160) +* `User defined path to font is ignored` +### Removed +* non-necessary dependency on `numpy` +* support for Python 2 + +## [2.0.6] - 2020-10-26 +### Added +* Python 3.9 is now supported + +## [2.0.5] - 2020-04-01 +### Added +* new specific exceptions: `FPDFException` & `FPDFPageFormatException` +* tests to increase line coverage in `image_parsing` module +* a test which uses most of the HTML features +### Fixed +* handling of fonts by the HTML mixin (weight and style) - thanks `cgfrost`! + +## [2.0.4] - 2020-03-26 +### Fixed +* images centering - thanks `cgfrost`! +* added missing import statment for `urlopen` in `image_parsing` module +* changed urlopen import from `six` library to maintain python2 compatibility + +## [2.0.3] - 2020-01-03 +### Added +* Ability to use a `BytesIO` buffer directly. This can simplify loading `matplotlib` plots into the PDF. +### Modified +* `load_resource` now return argument if type is `BytesIO`, else load. + +## [2.0.1] - 2018-11-15 +### Modified +* introduced a dependency to `numpy` to improve performances by replacing pixel regexes in image parsing (s/o @pennersr) + +## [2.0.0] - 2017-05-04 +### Added +* support for more recent Python versions +* more documentation +### Fixed +* PDF syntax error when version is > 1.3 due to an invalid `/Transparency` dict +### Modified +* turned `accept_page_break` into a property +* unit tests now use the standard `unittest` lib +* massive code cleanup using `flake8` From 516be967eec19517d4e3266c69a8f15325319aa7 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 12 Nov 2021 19:58:05 +0100 Subject: [PATCH 56/67] changes discussed in PR review --- fpdf/fpdf.py | 40 ++++++++++++++++++++-------------------- fpdf/graphics_state.py | 3 ++- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 3795f4699..7992fb951 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -247,8 +247,8 @@ def __init__( `None` disables font chaching. The default is `True`, meaning the current folder. """ - GraphicsStateMixin.__init__(self) - # Initialization of properties + super().__init__() + # Initialization of instance attributes self.offsets = {} # array of object offsets self.page = 0 # current page number self.n = 2 # current object number @@ -266,21 +266,7 @@ def __init__( self.lasth = 0 # height of last cell printed self.current_font = {} # current font self.str_alias_nb_pages = "{nb}" - # Scale factor - self.k = get_scale_factor(unit) - # graphics state variables from the stack - self.font_family = "" # current font family - self.font_style = "" # current font style - self.font_size_pt = 12 # current font size in points - self.font_stretching = 100 # current font stretching - self.underline = 0 # underlining flag - self.draw_color = "0 G" - self.fill_color = "0 g" - self.text_color = "0 g" - self.dash_pattern = "[] 0 d" - self.line_width = 0.567 / self.k # line width (0.2 mm) - # font_size is initialized below after the standard fonts have been set up - # end of grapics state variables + self.ws = 0 # word spacing self.angle = 0 # used by deprecated method: rotate() self.font_cache_dir = font_cache_dir @@ -328,13 +314,27 @@ def __init__( "couriernew": "courier", "timesnewroman": "times", } + # Scale factor + self.k = get_scale_factor(unit) + + # Graphics state variables defined as properties by GraphicsStateMixin. + # We set their default values here. + self.font_family = "" # current font family + self.font_style = "" # current font style + self.font_size_pt = 12 # current font size in points + self.font_size = self.font_size_pt / self.k + self.font_stretching = 100 # current font stretching + self.underline = 0 # underlining flag + self.draw_color = "0 G" + self.fill_color = "0 g" + self.text_color = "0 g" + self.dash_pattern = "[] 0 d" + self.line_width = 0.567 / self.k # line width (0.2 mm) + # end of grapics state variables self.dw_pt, self.dh_pt = get_page_format(format, self.k) self._set_orientation(orientation, self.dw_pt, self.dh_pt) self.def_orientation = self.cur_orientation - # another one from the graphics state stack - self.font_size = self.font_size_pt / self.k - # Page spacing # Page margins (1 cm) margin = (7200 / 254) / self.k diff --git a/fpdf/graphics_state.py b/fpdf/graphics_state.py index 1a424ae67..f5f8dab4a 100644 --- a/fpdf/graphics_state.py +++ b/fpdf/graphics_state.py @@ -10,7 +10,7 @@ class GraphicsStateMixin: directly by user code. """ - def __init__(self): + def __init__(self, *args, **kwargs): self.__statestack = [ dict( draw_color="0 G", @@ -26,6 +26,7 @@ def __init__(self): line_width=0, ), ] + super().__init__(*args, **kwargs) def _push_local_stack(self): self.__statestack.append(self.__statestack[-1].copy()) From 18bd9768407d0e8d33537feb0efd0dfb2f09afa6 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 12 Nov 2021 20:00:12 +0100 Subject: [PATCH 57/67] migrating test_regular_polygon.py to standard fixture --- ..._polygon.pdf => class_regular_polygon.pdf} | Bin 1813 -> 1813 bytes test/shapes/test_regular_polygon.py | 130 ++++++++++-------- test_regular_polygon.pdf | Bin 1813 -> 0 bytes 3 files changed, 70 insertions(+), 60 deletions(-) rename test/shapes/{test_regular_polygon.pdf => class_regular_polygon.pdf} (98%) delete mode 100644 test_regular_polygon.pdf diff --git a/test/shapes/test_regular_polygon.pdf b/test/shapes/class_regular_polygon.pdf similarity index 98% rename from test/shapes/test_regular_polygon.pdf rename to test/shapes/class_regular_polygon.pdf index 116683a7ed51b0e3e77e229977be63e1b4f2410b..b0830b83278c6432f2af152dd1f0670e5df45d87 100644 GIT binary patch delta 27 hcmbQrHOEXJDBV$8DO9LR-e2UeZ5ddu;2VMXG delta 27 icmbQrHSm9}(y z&fv`xl+g`$h215bq?#(1fxO}F4q|rG;8M2&P5U-qXKG=YU1+fuF>u7!GYg3Y@=EV> zd3M;VP@8AhULDd89TSKOd_@?%nu`?Fxc~8b;YPXlqu#!~*?#HI$7fwT+++1j+;v40 zA|26{&HVXp_E9Y{(Q{vGDnTQhg@Z|3>vLiYT3(byy!8G@SFu#jy-Lv?3Qe4xR?+`- zAkeSQjJ}+LgsW65bEV>+Do;TC+#csxiWT9O#9eew(E8-&RA!OIQVLSGtWo8Va7bbv z{N=To)IDYO8cT~{E|Web7_RK|e4c3gdVbfvn^PI(j=eOUo%u$7w$=6`6ifHAEtZ#F zoL77*c`>H0>hIe--pc#Zp-~X~vV$(b~$tE|vDZ z8PH_rU;)SZd2aK{@5#zTa16-1aH;5GNybH8yu3nk6~4w^Y6h zP(8|Lk31apQRoamKnq}P} z#(Sdb2#X&sc-a8nVOfF4Yl2!6M%p*U{OvJi*rMmaMr6g6%*~YEOlC{i*Tq6*X{@FF z2KPv_H@7`S-gaE!br1cXy)#qPRio~-`%QKPZF$g-LyrttlU2FKnxd^+BP0`4t92Pk zTs7dS_cJKc9{aYQ&$7Hap`?u#Ut3YtHh&pr3(uh*ZLluVSicxQCmk2IJ#8odxZdfG zLsm0QM_ANe@KOWbr>z^{4uPb#eoXZ+qw3DtMfV5&d!f#P2{q$EM;ynuz)tuiI;#W( z4tL$H?sH)G$D0>H^y0h*2eU@*6vK|XH|I2IM6k^S!&do;4*MIcViq#8I>V!6*LFSF zmfq0}PoQj%%CCjNb&+yt=w26GmvfFw)|8kw-89inBz2@HMF3p`>xbe;9TYfB`dEFv< zuQEi*a}1wncDpuI6VQ-D6*lB|5f@jS@zVabfVG!DhX|jZHZmt|VgU6-w-H4qu$Z7^ zgxF_@O9mkSI9W0nW&~hJH~JGZInRItj4J11Ymqvpc{w7 z;(+TxlaJ}@Mt0A*K*a|ii%ll@Umt04KliyW`M;0JW`C~7=92T~y&jIiadHGB2N?i` t#%)5#^q>$F#X)0J$QdS9tVcm(TSJVMB;zoKzpaDG;VuE}?R^79z`vl@=i&eW From 99a03a86f3b1779799bac9172270abb76b9feff5 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 24 Feb 2022 21:15:39 +0100 Subject: [PATCH 58/67] initial working write() refactor --- fpdf/fpdf.py | 128 ++++++++++++++++++++------------------------- fpdf/line_break.py | 15 +++++- 2 files changed, 70 insertions(+), 73 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 7ea8851b8..7f65b28c2 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -2080,9 +2080,13 @@ def _render_styled_cell_text( (in any order): `L`: left ; `T`: top ; `R`: right ; `B`: bottom. Default value: 0. ln (int): Indicates where the current position should go after the call. - Possible values are: `0`: to the right ; `1`: to the beginning of the - next line ; `2`: below. Putting 1 is equivalent to putting 0 and calling - `ln` just after. Default value: 0. + Possible values are: + `-1`: On the same line at the end of the actual text, + `0`: to the right + `1`: to the beginning of the next line + `2`: below. + Putting 1 is equivalent to putting 0 and calling `ln` just after. + Default value: 0. align (str): Allows to center or align the text inside the cell. Possible values are: `L` or empty string: left align (default value) ; `C`: center ; `R`: right align @@ -2157,6 +2161,7 @@ def _render_styled_cell_text( f"{(x + w) * k:.2f} {(self.h - (y + h)) * k:.2f} l S " ) + s_width, underlines = 0, [] if styled_txt_frags: if align == "R": dx = w - self.c_margin - styled_txt_width @@ -2169,7 +2174,6 @@ def _render_styled_cell_text( s += f"q {self.text_color} " prev_font_style, prev_underline = self.font_style, self.underline - s_width, underlines = 0, [] s += ( f"BT {(self.x + dx) * k:.2f} " f"{(self.h - self.y - 0.5 * h - 0.3 * self.font_size) * k:.2f} Td" @@ -2275,6 +2279,8 @@ def _render_styled_cell_text( self.y += h # Go to next line if ln == 1: self.x = self.l_margin + elif ln < 0: # temporary workaround; end of added text. + self.x += s_width else: self.x += w @@ -2470,9 +2476,6 @@ def multi_cell( self.add_page = lambda *args, **kwargs: None self._perform_page_break_if_need_be = lambda *args, **kwargs: None - # Store this information for manipulating position. - location = (self.get_x(), self.get_y()) - # If width is 0, set width to available width between margins if w == 0: w = self.w - self.r_margin - self.x @@ -2567,7 +2570,7 @@ def multi_cell( _add_page, _perform_page_break_if_need_be, ) - self.set_xy(*location) # restore location + self.set_xy(prev_x, prev_y) # restore location result = [] for text_line in text_lines: characters = [] @@ -2610,69 +2613,51 @@ def write(self, h=None, txt="", link=""): ) if h is None: h = self.font_size - txt = self.normalize_text(txt) - w = self.w - self.r_margin - self.x - wmax = (w - 2 * self.c_margin) * 1000 / self.font_size - s = txt.replace("\r", "") - nb = len(s) - sep = -1 - i = 0 - j = 0 - l = 0 - nl = 1 - while i < nb: - # Get next character - c = s[i] - if c == "\n": - # Explicit line break - self.cell(w, h, substr(s, j, i - j), ln=2, link=link) - i += 1 - sep = -1 - j = i - l = 0 - if nl == 1: - self.x = self.l_margin - w = self.w - self.r_margin - self.x - wmax = (w - 2 * self.c_margin) * 1000 / self.font_size - nl += 1 - continue - if c == " ": - sep = i - if self.unifontsubset: - l += self.get_string_width(c, True) / self.font_size * 1000 - else: - l += _char_width(self.current_font, c) - if l > wmax: - # Automatic line break - if sep == -1: - if self.x > self.l_margin: - # Move to next line - self.x = self.l_margin - self.y += h - w = self.w - self.r_margin - self.x - wmax = (w - 2 * self.c_margin) * 1000 / self.font_size - i += 1 - nl += 1 - continue - if i == j: - i += 1 - self.cell(w, h, substr(s, j, i - j), ln=2, link=link) - else: - self.cell(w, h, substr(s, j, sep - j), ln=2, link=link) - i = sep + 1 - sep = -1 - j = i - l = 0 - if nl == 1: - self.x = self.l_margin - w = self.w - self.r_margin - self.x - wmax = (w - 2 * self.c_margin) * 1000 / self.font_size - nl += 1 + + page_break_triggered = False + normalized_string = self.normalize_text(txt).replace("\r", "") + styled_text_fragments = self._preload_font_styles(normalized_string, False) + + text_lines = [] + multi_line_break = MultiLineBreak( + styled_text_fragments, self.get_normalized_string_width_with_style + ) + prev_x = self.x + # first line from current x position to right margin + first_width = self.w - prev_x - self.r_margin - 2 * self.c_margin + first_fswidth = first_width * 1000 / self.font_size + text_line = multi_line_break.get_line_of_given_width( + first_fswidth, no_wordsplit=True + ) + # remaining lines fill between margins + full_width = self.w - self.l_margin - self.r_margin - 2 * self.c_margin + full_fswidth = full_width * 1000 / self.font_size + while (text_line) is not None: + text_lines.append(text_line) + text_line = multi_line_break.get_line_of_given_width(full_fswidth) + if not text_lines: + return False + + self.ws = 0 # currently only left aligned, so no word spacing + for text_line_index, text_line in enumerate(text_lines): + if text_line_index == 0: + line_width = first_width else: - i += 1 - # Last chunk - if i != j: - self.cell(l / 1000 * self.font_size, h, substr(s, j), link=link) + line_width = full_width + self.ln() + new_page = self._render_styled_cell_text( + line_width, + h=h, + styled_txt_frags=text_line.fragments, + border=0, + ln=-1, + align="L", + fill=False, + link=link, + ) + page_break_triggered = page_break_triggered or new_page + + return page_break_triggered @check_page def image( @@ -3431,8 +3416,7 @@ def _putfonts(self): # check if self has a attr mtd which is callable (method) if not callable(getattr(self, mtd, None)): raise FPDFException(f"Unsupported font type: {my_type}") - # pylint: disable=no-member - self.mtd(font) + self.mtd(font) # pylint: disable=no-member def _putTTfontwidths(self, font, maxUni): if font["unifilename"] is None: diff --git a/fpdf/line_break.py b/fpdf/line_break.py index dd5b30c1d..d58eeaf0e 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -201,11 +201,16 @@ def _get_character_width(self, character, style=""): character = HYPHEN return self.size_by_style(character, style) - def get_line_of_given_width(self, maximum_width): + # pylint: disable=too-many-return-statements + def get_line_of_given_width(self, maximum_width, no_wordsplit=False): if self.fragment_index == len(self.styled_text_fragments): return None + last_fragment_index = self.fragment_index + last_character_index = self.character_index + line_full = False + current_line = CurrentLine() while self.fragment_index < len(self.styled_text_fragments): @@ -237,6 +242,9 @@ def get_line_of_given_width(self, maximum_width): ) = current_line.automatic_break(self.justify) self.character_index += 1 return line + if no_wordsplit: + line_full = True + break return current_line.manual_break() current_line.add_character( @@ -251,4 +259,9 @@ def get_line_of_given_width(self, maximum_width): self.character_index += 1 if current_line.width: + if line_full and no_wordsplit: + # roll back and return empty line to trigger line break + self.fragment_index = last_fragment_index + self.character_index = last_character_index + return CurrentLine() return current_line.manual_break() From 0d907397657260182b8a351698b902873edddd72 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 25 Feb 2022 14:04:53 +0100 Subject: [PATCH 59/67] replace ln=0 internally with newpos_x/newpos_y --- fpdf/fpdf.py | 141 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 34 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 7f65b28c2..4b04c3cf8 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -97,6 +97,28 @@ class DocumentState(IntEnum): CLOSED = 3 # EOF printed +class X(IntEnum): + LEFT = 1 # self.x + RIGHT = 2 # self.x + w + START = 3 # left end of actual text + END = 4 # right end of actual text + CENTER = 5 # center of actual text + LMARGIN = 6 # self.l_margin + RMARGIN = 7 # self.w - self.r_margin + LPAGE = 8 # 0.0 + RPAGE = 9 # self.w + + +class Y(IntEnum): + TOP = 1 # self.y + LAST = 2 # top of last line (TOP for single lines) + NEXT = 3 # LAST + h + TMARGIN = 4 # self.t_margin + BMARGIN = 5 # self.h - self.b_margin + TPAGE = 6 # 0.0 + BPAGE = 7 # self.h + + class Annotation(NamedTuple): type: str x: int @@ -2030,6 +2052,14 @@ def cell( "ignored" ) border = 1 + newpos_x = X.RIGHT + newpos_y = Y.TOP + if ln == 1: + newpos_x = X.LMARGIN + newpos_y = Y.NEXT + elif ln == 2: + newpos_x = X.LEFT + newpos_y = Y.NEXT # Font styles preloading must be performed before any call to FPDF.get_string_width: txt = self.normalize_text(txt) styled_txt_frags = self._preload_font_styles(txt, markdown) @@ -2038,11 +2068,12 @@ def cell( h, styled_txt_frags, border, - ln, - align, - fill, - link, - center, + newpos_x=newpos_x, + newpos_y=newpos_y, + align=align, + fill=fill, + link=link, + center=center, ) def _render_styled_cell_text( @@ -2051,7 +2082,8 @@ def _render_styled_cell_text( h=None, styled_txt_frags=(), border=0, - ln=0, + newpos_x=X.RIGHT, + newpos_y=Y.TOP, align="", fill=False, link="", @@ -2079,14 +2111,24 @@ def _render_styled_cell_text( or a string containing some or all of the following characters (in any order): `L`: left ; `T`: top ; `R`: right ; `B`: bottom. Default value: 0. - ln (int): Indicates where the current position should go after the call. - Possible values are: - `-1`: On the same line at the end of the actual text, - `0`: to the right - `1`: to the beginning of the next line - `2`: below. - Putting 1 is equivalent to putting 0 and calling `ln` just after. - Default value: 0. + newpos_x: Current position in x after the call. + X.LEFT - left end of the cell + X.RIGHT - right end of the cell (default) + X.START - start of actual text + X.END - end of actual text + X.CENTER - center of actual text + X.LMARGIN - left page margin (start of printable area) + X.RMARGIN - right page margin (end of printable area) + X.LPAGE - left edge of page + X.RPAGE - right edge of page + newpos_y: Current position in y after the call. + Y.TOP - top of the first line (default) + Y.LAST - top of the last line (same as TOP for single-line text) + Y.NEXT - top of next line (bottom of current text) + Y.TMARGIN - top page margin (start of printable area) + Y.BMARGIN - bottom page margin (end of printable area) + Y.TPAGE - top edge of page + Y.BPAGE - bottom edge of page align (str): Allows to center or align the text inside the cell. Possible values are: `L` or empty string: left align (default value) ; `C`: center ; `R`: right align @@ -2161,6 +2203,7 @@ def _render_styled_cell_text( f"{(x + w) * k:.2f} {(self.h - (y + h)) * k:.2f} l S " ) + s_start = self.x s_width, underlines = 0, [] if styled_txt_frags: if align == "R": @@ -2169,6 +2212,7 @@ def _render_styled_cell_text( dx = (w - styled_txt_width) / 2 else: dx = self.c_margin + s_start += dx if self.fill_color != self.text_color: s += f"q {self.text_color} " @@ -2247,7 +2291,8 @@ def _render_styled_cell_text( self.underline = underline s_width += self.get_string_width(txt_frag, True) s += " ET" - # Restoring font style & underline mode after handling changes by Markdown annotations: + # Restoring font style & underline mode after handling changes + # by Markdown annotations: if not self._markdown_leak_end_style: if self.font_style != prev_font_style: self.font_style = prev_font_style @@ -2275,14 +2320,36 @@ def _render_styled_cell_text( self._out(s) self.lasth = h - if ln > 0: - self.y += h # Go to next line - if ln == 1: - self.x = self.l_margin - elif ln < 0: # temporary workaround; end of added text. - self.x += s_width - else: + # X.LEFT -> self.x stays the same + if newpos_x == X.RIGHT: self.x += w + elif newpos_x == X.START: + self.x = s_start + elif newpos_x == X.END: + self.x = s_start + s_width - self.c_margin + elif newpos_x == X.CENTER: + self.x = (s_start + s_width) / 2.0 + elif newpos_x == X.LMARGIN: + self.x = self.l_margin + elif newpos_x == X.RMARGIN: + self.x = self.w - self.r_margin + elif newpos_x == X.LPAGE: + self.x = 0.0 + elif newpos_x == X.RPAGE: + self.x = self.w + + # Y.TOP: -> self.y stays the same + # Y.LAST: -> self.y stays the same (single line) + if newpos_y == Y.NEXT: + self.y += h + if newpos_y == Y.TMARGIN: + self.y = self.t_margin + if newpos_y == Y.BMARGIN: + self.y = self.h - self.b_margin + if newpos_y == Y.TPAGE: + self.y = 0.0 + if newpos_y == Y.BPAGE: + self.y = self.h return page_break_triggered @@ -2465,6 +2532,15 @@ def multi_cell( "Parameter 'w' and 'h' must be numbers, not strings." " You can omit them by passing string content with txt=" ) + newpos_x = X.RIGHT + newpos_y = Y.NEXT + if ln == 1: + newpos_x = X.LMARGIN + elif ln == 2: + newpos_x = X.LEFT + elif ln == 3: + newpos_y = Y.TOP + page_break_triggered = False if split_only: _out, _add_page, _perform_page_break_if_need_be = ( @@ -2543,25 +2619,21 @@ def multi_cell( "B" if "B" in border and is_last_line else "", ) ), - ln=(2 if not is_last_line else (0 if ln == 3 else ln)), + newpos_x=newpos_x if is_last_line else X.LEFT, + newpos_y=newpos_y if is_last_line else Y.NEXT, align=align, fill=fill, link=link, ) - if is_last_line and new_page and ln == 3: - # When a page jump is performed and ln=3, - # we stick to that new vertical offset. + if is_last_line and new_page and newpos_y == Y.TOP: + # When a page jump is performed and the requested y is TOP (ln=3), + # pretend we started at the top of the text block on the new page. # cf. test_multi_cell_table_with_automatic_page_break prev_y = self.y page_break_triggered = page_break_triggered or new_page - new_x, new_y = { - 0: (self.x, self.y + h), - 1: (self.l_margin, self.y), - 2: (prev_x, self.y), - 3: (self.x, prev_y), - }[ln] - self.set_xy(new_x, new_y) + if newpos_y == Y.TOP: # We may have jumped a few lines -> reset + self.y = prev_y if split_only: # restore writing functions @@ -2650,7 +2722,8 @@ def write(self, h=None, txt="", link=""): h=h, styled_txt_frags=text_line.fragments, border=0, - ln=-1, + newpos_x=X.END, + newpos_y=Y.TOP, align="L", fill=False, link=link, From 6e6878050256e965b7d01456a3821556ad8aa4a2 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 25 Feb 2022 18:40:55 +0100 Subject: [PATCH 60/67] renaming test/cells to test/text, moving rext related tests there --- test/{cells => text}/__init__.py | 0 test/{cells => text}/cell_centering.pdf | Bin test/{cells => text}/cell_markdown.pdf | Bin test/{cells => text}/cell_markdown_bleeding.pdf | Bin .../{cells => text}/cell_markdown_right_aligned.pdf | Bin .../cell_markdown_with_ttf_fonts.pdf | Bin test/{cells => text}/cell_table_unbreakable.pdf | Bin test/{cells => text}/cell_table_with_pagebreak.pdf | Bin test/{cells => text}/cell_without_w_nor_h.pdf | Bin test/{cells => text}/ln_0.pdf | Bin test/{cells => text}/ln_1.pdf | Bin .../ln_positioning_and_page_breaking_for_cell.pdf | Bin ..._positioning_and_page_breaking_for_multicell.pdf | Bin test/{cells => text}/multi_cell_ln_0.pdf | Bin test/{cells => text}/multi_cell_ln_1.pdf | Bin test/{cells => text}/multi_cell_ln_3.pdf | Bin test/{cells => text}/multi_cell_ln_3_table.pdf | Bin test/{cells => text}/multi_cell_markdown.pdf | Bin .../multi_cell_markdown_with_ttf_fonts.pdf | Bin .../multi_cell_table_unbreakable.pdf | Bin .../multi_cell_table_unbreakable2.pdf | Bin .../table_with_headers_on_every_page.pdf | Bin test/{cells => text}/test_cell.py | 0 test/{ => text}/test_line_break.py | 0 test/{ => text}/test_markdown_parse.py | 0 test/{cells => text}/test_multi_cell.py | 0 .../test_multi_cell_justified_with_unicode_font.pdf | Bin test/{cells => text}/test_multi_cell_markdown.py | 0 ...t_multi_cell_table_with_automatic_page_break.pdf | Bin 29 files changed, 0 insertions(+), 0 deletions(-) rename test/{cells => text}/__init__.py (100%) rename test/{cells => text}/cell_centering.pdf (100%) rename test/{cells => text}/cell_markdown.pdf (100%) rename test/{cells => text}/cell_markdown_bleeding.pdf (100%) rename test/{cells => text}/cell_markdown_right_aligned.pdf (100%) rename test/{cells => text}/cell_markdown_with_ttf_fonts.pdf (100%) rename test/{cells => text}/cell_table_unbreakable.pdf (100%) rename test/{cells => text}/cell_table_with_pagebreak.pdf (100%) rename test/{cells => text}/cell_without_w_nor_h.pdf (100%) rename test/{cells => text}/ln_0.pdf (100%) rename test/{cells => text}/ln_1.pdf (100%) rename test/{cells => text}/ln_positioning_and_page_breaking_for_cell.pdf (100%) rename test/{cells => text}/ln_positioning_and_page_breaking_for_multicell.pdf (100%) rename test/{cells => text}/multi_cell_ln_0.pdf (100%) rename test/{cells => text}/multi_cell_ln_1.pdf (100%) rename test/{cells => text}/multi_cell_ln_3.pdf (100%) rename test/{cells => text}/multi_cell_ln_3_table.pdf (100%) rename test/{cells => text}/multi_cell_markdown.pdf (100%) rename test/{cells => text}/multi_cell_markdown_with_ttf_fonts.pdf (100%) rename test/{cells => text}/multi_cell_table_unbreakable.pdf (100%) rename test/{cells => text}/multi_cell_table_unbreakable2.pdf (100%) rename test/{cells => text}/table_with_headers_on_every_page.pdf (100%) rename test/{cells => text}/test_cell.py (100%) rename test/{ => text}/test_line_break.py (100%) rename test/{ => text}/test_markdown_parse.py (100%) rename test/{cells => text}/test_multi_cell.py (100%) rename test/{cells => text}/test_multi_cell_justified_with_unicode_font.pdf (100%) rename test/{cells => text}/test_multi_cell_markdown.py (100%) rename test/{cells => text}/test_multi_cell_table_with_automatic_page_break.pdf (100%) diff --git a/test/cells/__init__.py b/test/text/__init__.py similarity index 100% rename from test/cells/__init__.py rename to test/text/__init__.py diff --git a/test/cells/cell_centering.pdf b/test/text/cell_centering.pdf similarity index 100% rename from test/cells/cell_centering.pdf rename to test/text/cell_centering.pdf diff --git a/test/cells/cell_markdown.pdf b/test/text/cell_markdown.pdf similarity index 100% rename from test/cells/cell_markdown.pdf rename to test/text/cell_markdown.pdf diff --git a/test/cells/cell_markdown_bleeding.pdf b/test/text/cell_markdown_bleeding.pdf similarity index 100% rename from test/cells/cell_markdown_bleeding.pdf rename to test/text/cell_markdown_bleeding.pdf diff --git a/test/cells/cell_markdown_right_aligned.pdf b/test/text/cell_markdown_right_aligned.pdf similarity index 100% rename from test/cells/cell_markdown_right_aligned.pdf rename to test/text/cell_markdown_right_aligned.pdf diff --git a/test/cells/cell_markdown_with_ttf_fonts.pdf b/test/text/cell_markdown_with_ttf_fonts.pdf similarity index 100% rename from test/cells/cell_markdown_with_ttf_fonts.pdf rename to test/text/cell_markdown_with_ttf_fonts.pdf diff --git a/test/cells/cell_table_unbreakable.pdf b/test/text/cell_table_unbreakable.pdf similarity index 100% rename from test/cells/cell_table_unbreakable.pdf rename to test/text/cell_table_unbreakable.pdf diff --git a/test/cells/cell_table_with_pagebreak.pdf b/test/text/cell_table_with_pagebreak.pdf similarity index 100% rename from test/cells/cell_table_with_pagebreak.pdf rename to test/text/cell_table_with_pagebreak.pdf diff --git a/test/cells/cell_without_w_nor_h.pdf b/test/text/cell_without_w_nor_h.pdf similarity index 100% rename from test/cells/cell_without_w_nor_h.pdf rename to test/text/cell_without_w_nor_h.pdf diff --git a/test/cells/ln_0.pdf b/test/text/ln_0.pdf similarity index 100% rename from test/cells/ln_0.pdf rename to test/text/ln_0.pdf diff --git a/test/cells/ln_1.pdf b/test/text/ln_1.pdf similarity index 100% rename from test/cells/ln_1.pdf rename to test/text/ln_1.pdf diff --git a/test/cells/ln_positioning_and_page_breaking_for_cell.pdf b/test/text/ln_positioning_and_page_breaking_for_cell.pdf similarity index 100% rename from test/cells/ln_positioning_and_page_breaking_for_cell.pdf rename to test/text/ln_positioning_and_page_breaking_for_cell.pdf diff --git a/test/cells/ln_positioning_and_page_breaking_for_multicell.pdf b/test/text/ln_positioning_and_page_breaking_for_multicell.pdf similarity index 100% rename from test/cells/ln_positioning_and_page_breaking_for_multicell.pdf rename to test/text/ln_positioning_and_page_breaking_for_multicell.pdf diff --git a/test/cells/multi_cell_ln_0.pdf b/test/text/multi_cell_ln_0.pdf similarity index 100% rename from test/cells/multi_cell_ln_0.pdf rename to test/text/multi_cell_ln_0.pdf diff --git a/test/cells/multi_cell_ln_1.pdf b/test/text/multi_cell_ln_1.pdf similarity index 100% rename from test/cells/multi_cell_ln_1.pdf rename to test/text/multi_cell_ln_1.pdf diff --git a/test/cells/multi_cell_ln_3.pdf b/test/text/multi_cell_ln_3.pdf similarity index 100% rename from test/cells/multi_cell_ln_3.pdf rename to test/text/multi_cell_ln_3.pdf diff --git a/test/cells/multi_cell_ln_3_table.pdf b/test/text/multi_cell_ln_3_table.pdf similarity index 100% rename from test/cells/multi_cell_ln_3_table.pdf rename to test/text/multi_cell_ln_3_table.pdf diff --git a/test/cells/multi_cell_markdown.pdf b/test/text/multi_cell_markdown.pdf similarity index 100% rename from test/cells/multi_cell_markdown.pdf rename to test/text/multi_cell_markdown.pdf diff --git a/test/cells/multi_cell_markdown_with_ttf_fonts.pdf b/test/text/multi_cell_markdown_with_ttf_fonts.pdf similarity index 100% rename from test/cells/multi_cell_markdown_with_ttf_fonts.pdf rename to test/text/multi_cell_markdown_with_ttf_fonts.pdf diff --git a/test/cells/multi_cell_table_unbreakable.pdf b/test/text/multi_cell_table_unbreakable.pdf similarity index 100% rename from test/cells/multi_cell_table_unbreakable.pdf rename to test/text/multi_cell_table_unbreakable.pdf diff --git a/test/cells/multi_cell_table_unbreakable2.pdf b/test/text/multi_cell_table_unbreakable2.pdf similarity index 100% rename from test/cells/multi_cell_table_unbreakable2.pdf rename to test/text/multi_cell_table_unbreakable2.pdf diff --git a/test/cells/table_with_headers_on_every_page.pdf b/test/text/table_with_headers_on_every_page.pdf similarity index 100% rename from test/cells/table_with_headers_on_every_page.pdf rename to test/text/table_with_headers_on_every_page.pdf diff --git a/test/cells/test_cell.py b/test/text/test_cell.py similarity index 100% rename from test/cells/test_cell.py rename to test/text/test_cell.py diff --git a/test/test_line_break.py b/test/text/test_line_break.py similarity index 100% rename from test/test_line_break.py rename to test/text/test_line_break.py diff --git a/test/test_markdown_parse.py b/test/text/test_markdown_parse.py similarity index 100% rename from test/test_markdown_parse.py rename to test/text/test_markdown_parse.py diff --git a/test/cells/test_multi_cell.py b/test/text/test_multi_cell.py similarity index 100% rename from test/cells/test_multi_cell.py rename to test/text/test_multi_cell.py diff --git a/test/cells/test_multi_cell_justified_with_unicode_font.pdf b/test/text/test_multi_cell_justified_with_unicode_font.pdf similarity index 100% rename from test/cells/test_multi_cell_justified_with_unicode_font.pdf rename to test/text/test_multi_cell_justified_with_unicode_font.pdf diff --git a/test/cells/test_multi_cell_markdown.py b/test/text/test_multi_cell_markdown.py similarity index 100% rename from test/cells/test_multi_cell_markdown.py rename to test/text/test_multi_cell_markdown.py diff --git a/test/cells/test_multi_cell_table_with_automatic_page_break.pdf b/test/text/test_multi_cell_table_with_automatic_page_break.pdf similarity index 100% rename from test/cells/test_multi_cell_table_with_automatic_page_break.pdf rename to test/text/test_multi_cell_table_with_automatic_page_break.pdf From 2c85c61bb18100b2d2c0995440061e2dc025ac7f Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 26 Feb 2022 15:18:48 +0100 Subject: [PATCH 61/67] test cases for write() and _render_styled_cell_text() --- fpdf/__init__.py | 4 ++ fpdf/fpdf.py | 28 ++++------ test/text/test_render_styled.py | 88 ++++++++++++++++++++++++++++++++ test/text/test_write.py | 64 +++++++++++++++++++++++ test/text/write_page_break.pdf | Bin 0 -> 2252 bytes test/text/write_soft_hyphen.pdf | Bin 0 -> 1354 bytes 6 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 test/text/test_render_styled.py create mode 100644 test/text/test_write.py create mode 100644 test/text/write_page_break.pdf create mode 100644 test/text/write_soft_hyphen.pdf diff --git a/fpdf/__init__.py b/fpdf/__init__.py index a18a7e668..94c9c25fb 100644 --- a/fpdf/__init__.py +++ b/fpdf/__init__.py @@ -3,6 +3,8 @@ from .fpdf import ( FPDF, + X, + Y, FPDFException, TitleStyle, FPDF_FONT_DIR as _FPDF_FONT_DIR, @@ -39,6 +41,8 @@ "__license__", # Classes "FPDF", + "X", + "Y", "Template", "FlexTemplate", "TitleStyle", diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 4b04c3cf8..1538a156f 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -105,8 +105,6 @@ class X(IntEnum): CENTER = 5 # center of actual text LMARGIN = 6 # self.l_margin RMARGIN = 7 # self.w - self.r_margin - LPAGE = 8 # 0.0 - RPAGE = 9 # self.w class Y(IntEnum): @@ -115,8 +113,6 @@ class Y(IntEnum): NEXT = 3 # LAST + h TMARGIN = 4 # self.t_margin BMARGIN = 5 # self.h - self.b_margin - TPAGE = 6 # 0.0 - BPAGE = 7 # self.h class Annotation(NamedTuple): @@ -2119,16 +2115,12 @@ def _render_styled_cell_text( X.CENTER - center of actual text X.LMARGIN - left page margin (start of printable area) X.RMARGIN - right page margin (end of printable area) - X.LPAGE - left edge of page - X.RPAGE - right edge of page newpos_y: Current position in y after the call. Y.TOP - top of the first line (default) Y.LAST - top of the last line (same as TOP for single-line text) Y.NEXT - top of next line (bottom of current text) Y.TMARGIN - top page margin (start of printable area) Y.BMARGIN - bottom page margin (end of printable area) - Y.TPAGE - top edge of page - Y.BPAGE - bottom edge of page align (str): Allows to center or align the text inside the cell. Possible values are: `L` or empty string: left align (default value) ; `C`: center ; `R`: right align @@ -2328,15 +2320,11 @@ def _render_styled_cell_text( elif newpos_x == X.END: self.x = s_start + s_width - self.c_margin elif newpos_x == X.CENTER: - self.x = (s_start + s_width) / 2.0 + self.x = (s_start + s_start + s_width) / 2.0 elif newpos_x == X.LMARGIN: self.x = self.l_margin elif newpos_x == X.RMARGIN: self.x = self.w - self.r_margin - elif newpos_x == X.LPAGE: - self.x = 0.0 - elif newpos_x == X.RPAGE: - self.x = self.w # Y.TOP: -> self.y stays the same # Y.LAST: -> self.y stays the same (single line) @@ -2346,10 +2334,6 @@ def _render_styled_cell_text( self.y = self.t_margin if newpos_y == Y.BMARGIN: self.y = self.h - self.b_margin - if newpos_y == Y.TPAGE: - self.y = 0.0 - if newpos_y == Y.BPAGE: - self.y = self.h return page_break_triggered @@ -4251,4 +4235,12 @@ def _is_xml(img: io.BytesIO): sys.modules[__name__].__class__ = WarnOnDeprecatedModuleAttributes -__all__ = ["FPDF", "load_cache", "get_page_format", "TitleStyle", "PAGE_FORMATS"] +__all__ = [ + "FPDF", + "X", + "Y", + "load_cache", + "get_page_format", + "TitleStyle", + "PAGE_FORMATS", +] diff --git a/test/text/test_render_styled.py b/test/text/test_render_styled.py new file mode 100644 index 000000000..e02158631 --- /dev/null +++ b/test/text/test_render_styled.py @@ -0,0 +1,88 @@ +from pathlib import Path + +import pytest + +import fpdf +from fpdf.line_break import MultiLineBreak +from test.conftest import assert_pdf_equal + +HERE = Path(__file__).resolve().parent + + +def test_render_styled_newpos(tmp_path): + doc = fpdf.FPDF() + # doc.add_page() + doc.set_font("helvetica", size=24) + doc.set_margin(10) + twidth = 100 + + data = ( + # txt, align, newpos_x, newpos_y, target x/y + ["Left Top L", "L", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], + ["Left Top R", "R", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], + ["Left Top C", "C", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], + ["Left Top J", "J", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], + ["Right Last L", "L", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], + ["Right Last R", "R", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], + ["Right Last C", "C", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], + ["Right Last J", "J", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], + ["Start Next L", "L", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], + ["Start Next R", "R", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], + ["Start Next C", "C", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], + ["Start Next J", "J", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], + ["End TMargin L", "L", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], + ["End TMargin R", "R", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], + ["End TMargin C", "C", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], + ["End TMargin J", "J", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], + ["Center TOP L", "L", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], + ["Center TOP R", "R", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], + ["Center TOP C", "C", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], + ["Center TOP J", "J", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], + ["LMargin BMargin L", "L", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], + ["LMargin BMargin R", "R", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], + ["LMargin BMargin C", "C", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], + ["LMargin BMargin J", "J", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], + ["RMargin Top L", "L", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], + ["RMargin Top R", "R", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], + ["RMargin Top C", "C", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], + ["RMargin Top J", "J", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], + ) + + for i, item in enumerate(data): + i = i % 4 + if i == 0: + doc.add_page() + doc.x = 20 + doc.y = 20 + (i * 20) + s = item[0] + align = item[1] + newx = item[2] + newy = item[3] + expx = item[4] + expy = item[5] + frags = doc._preload_font_styles(s, False) + mlb = MultiLineBreak( + frags, + doc.get_normalized_string_width_with_style, + justify=(align == "J"), + ) + line = mlb.get_line_of_given_width(twidth * 1000 / doc.font_size) + new_page = doc._render_styled_cell_text( + twidth, + styled_txt_frags=line.fragments, + border=1, + align=align, # "L" if align == "J" else align, + newpos_x=newx, + newpos_y=newy, + ) + sw = doc.get_string_width(line.fragments[0].string) + if expx == "s": + expx = doc.x + twidth - sw + with doc.rotation(i * -15, doc.x, doc.y): + doc.circle(doc.x - 3, doc.y - 3, 6) + doc.line(doc.x - 3, doc.y, doc.x + 3, doc.y) + doc.line(doc.x, doc.y - 3, doc.x, doc.y + 3) + # assert doc.x == expx, f"Resulting x position {doc.x} != {expx}" + # assert doc.y == expy, f"Resulting y position {doc.y} != {expy}" + + assert_pdf_equal(doc, HERE / "render_styled_newpos.pdf", tmp_path) diff --git a/test/text/test_write.py b/test/text/test_write.py new file mode 100644 index 000000000..c257ea0db --- /dev/null +++ b/test/text/test_write.py @@ -0,0 +1,64 @@ +from pathlib import Path + +import pytest + +import fpdf +from test.conftest import assert_pdf_equal + +HERE = Path(__file__).resolve().parent + +# pylint: disable=all +text_data = ( + "Lorem ipsum Ut nostrud irure reprehenderit anim nostrud dolore sed " + "ut Excepteur dolore ut sunt irure consectetur tempor eu tempor " + "nostrud dolore sint exercitation aliquip velit ullamco esse dolore " + "mollit ea sed voluptate commodo amet eiusmod incididunt Excepteur " + "Excepteur officia est ea dolore sed id in cillum incididunt quis ex " + "id aliqua ullamco reprehenderit cupidatat in quis pariatur ex et " + "veniam consectetur et minim minim nulla ea in quis Ut in " + "consectetur cillum aliquip pariatur qui quis sint reprehenderit " + "anim incididunt laborum dolor dolor est dolor fugiat ut officia do " + "dolore deserunt nulla voluptate officia mollit elit consequat ad " + "aliquip non nulla dolor nisi magna consectetur anim sint officia " + "sit tempor anim do laboris ea culpa eu veniam sed cupidatat in anim " + "fugiat culpa enim Ut cillum in exercitation magna nostrud aute " + "proident laboris est ullamco nulla occaecat nulla proident " + "consequat in ut labore non sit id cillum ut ea quis est ut dolore " + "nisi aliquip aute pariatur ullamco ut cillum Duis nisi elit sit " + "cupidatat do Ut aliqua irure sunt sunt proident sit aliqua in " + "dolore Ut in sint sunt exercitation aliquip elit velit dolor nisi " + "" +) +# pylint: enable=all + + +def test_write_page_break(tmp_path): + doc = fpdf.FPDF() + doc.add_page() + doc.set_font("helvetica", size=24) + doc.y = 20 + doc.write(txt=text_data) + doc.write(txt=text_data) + assert_pdf_equal(doc, HERE / "write_page_break.pdf", tmp_path) + + +def test_write_soft_hyphen(tmp_path): + s = "Donau\u00addamp\u00adfschiff\u00adfahrts\u00adgesellschafts\u00adkapitäns\u00admützen\u00adstreifen. " + doc = fpdf.FPDF() + doc.add_page() + doc.set_font("helvetica", size=24) + doc.y = 20 + doc.write(txt=s) + doc.set_font("helvetica", size=24, style="B") + doc.write(txt=s) + doc.set_font("helvetica", size=24, style="I") + doc.write(txt=s) + doc.set_font("helvetica", size=24) + doc.write(txt=s) + doc.set_font("helvetica", size=24, style="B") + doc.write(txt=s) + doc.set_font("helvetica", size=24, style="I") + doc.write(txt=s) + doc.set_font("helvetica", size=24) + doc.write(txt=s) + assert_pdf_equal(doc, HERE / "write_soft_hyphen.pdf", tmp_path) diff --git a/test/text/write_page_break.pdf b/test/text/write_page_break.pdf new file mode 100644 index 0000000000000000000000000000000000000000..903e7d59e101ec19c2127ea8c63a197bf83aca0f GIT binary patch literal 2252 zcmbVOX;@QN8de+@qacESZ53`)!Xl95h6IujWC^4oOISp(STKYO1VaMJ4FnY|Ehxnr zETRLWpe#C7(1=7)AW(KXxYGd9%@Q<*?zX?0%5utdWh)0004-g#}( zYuLt}d$g^j*5!JXU7wJ~5!JjeR{z4;PIqm?~lqaNz z)}7>6j`(>0^wAME*SikBe7F13CzSA&-+jj1a5wE>TD@hTs{{6z8k6Rl3chyO1eVH> z!GSmPJ^e;wD~hP=?|ijkfyJgz zX{GKV7n}*hyV54?N2@#QRIffryPa~BTVEbH4)aU$M-5EW;qlDr8<3~J|Ft;!wa_JF2dE+?|NQbIvx2?nx;K0wH@B` zWqF|1&0jkLb14H}kIZzZTQvL0E_k~OmM!IG`ff39{@D)pHceWUa7Xsk#kCC?YiWG4LQR_Rd)HQ*fqza3wF40W7Dif-#`}4b@)+bNq5VM zBRBhS8lkmF0okb~tMk*as~W1~dapQ)-rJ@+U`z@O+vJt+9FzS*_Z5EB7*)a2pOyL zy+;}iUN1L{p(JvD*q`}wD6;Nnbs24Tl_q_j-uhRIPZJuLz28f-X1G$AZZ3=8SX$q9 z{;S|t>-)yO1yL?iwL+_Dm9JsmP~!@DVUhJJQ?tX(!F~$W*0%Zjzv~B_3&{M~_EfU_ z-p3BD1<6A?MEdn|E3iNJSq<<_RElWh47JF&_f+8K*%f(O2Mn4++aAuD^RZ_&jYn8L zhlq=cdpqv`nw-xbUpshX1=glMh5dDOCn@?vXezbua@oAB$}d_uZ)U1BlyzfQ0=DH& zUAUf6xFXB^Q+awu%3@++^aH7V;ebsC9o%$JH?R=unQ{-<^=zg7`CAj!@l}kE>LZv&GiwJ<=Pf`+GK%n4+?isltYbdw7j^rl z!hv%g6Zal?C){KjnChxr3_?ttu ze|E<~ZH`4*^0mg3PouM|>mrQVxI7A(VLnij+tV*OVcx%xIb6@Jt(Yd=>SdMnONV}> z*Dh)ubvax8{%J&S^Y`w0F^1sBixmTrs1t+$=!p&a97A9VCCL3x!Vm(WSRThyCvkw9hebZ+_=@xmk!sU3c)-s2SW&z#K5n;2Gs|#R0PFghyX~$par~XfD?t{NCD!2B@CpHkUc~jAovgF3?jSx zXAFsus^7(^$O^rKQJs=k712#C zg6>2XNYRCmip+;1q6EW-ii9ZW0)a(T6ciNoVEsqeTGkiwJj|T)ojL!1{_p$0DX8`b zi-i&!vSA);k!IxaaJB6zg1KrbP7pkGqEaltZWwW4VrZrwB?h)m06tBnuy0^HTbGp~ zqQ+PhwoFG0$_gbq=7NeuiJwHZ7zD2*YMdsp)gizcgX%;|qPCvCqvh%YenH@8PvqqF z%QsdJ+n)@bzWD2O;?wJ^Z=R3c zH0Rq28^%7SKGT7QvBCbw`IX74P58yxYpKrN$8-0+-O;l$GTi-T6>2}Sa;csg?poOU zp)0DaS@hL8a?X6;X?fS5_j`_he97P;y#G{AZ=k0=zhmU)P;XJNdS9vUaA(nvoQ54+ zniFx-+kJ=Dje4#Op0C@vcG;^-t#y`;ngjP{?W!)aGTPECqzPn%Pm#kw+$K3@;Cd!j zcpKt;n#sgLTosAQl2>cP^*n4jL`R9$joo&k#4T<^|7CGxM-760i1SJYVKdBaAWAEt za#TWGjTxcijlkLgQUp|xPFCZX+bXN&sv%DT8S$Odj$iE6l-SIYxTc>Z6j9_B6WVwt z#jqBikN|9;Ytb4)aXklq!CWnA1F|ZTU=eTn?MuHaK$dYXJsy|{m{AVS30g5Dhs|u5 zRS;&hadn|cGl^0RAH(v$Q0|P}K5z$>HPsLP!G-=(L3E0O)g}lc52GmKl$dd9!o)s_ zN{SX|K8}NH(>4yJh*}VRjMzDOs39gl^BmfSW0p zC``>Ey1+z}XQ?j93en-`2dyG^5LO!()-;M4r65{`+N5D7%D8L>m2^7Jg220+sGuMa G3ZlPGgs0R1 literal 0 HcmV?d00001 From 427b61b6270ef18cbf652244075f2282983ee459 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 2 Mar 2022 23:26:45 +0100 Subject: [PATCH 62/67] Move word spacing code to _render_styled_cell_text() --- CHANGELOG.md | 1 + docs/PageBreaks.md | 5 +- docs/Tutorial.md | 4 +- fpdf/fpdf.py | 155 ++++++++++-------- fpdf/line_break.py | 13 +- test/html/html_headings_line_height.pdf | Bin 3593 -> 3591 bytes test/outline/2_pages_outline.pdf | Bin 22822 -> 22819 bytes test/outline/html_toc.pdf | Bin 4254 -> 4253 bytes .../multi_cell_markdown_with_ttf_fonts.pdf | Bin 21211 -> 21168 bytes test/text/render_styled_newpos.pdf | Bin 0 -> 6566 bytes test/text/test_cell.py | 5 +- ...multi_cell_justified_with_unicode_font.pdf | Bin 14385 -> 14361 bytes test/text/test_render_styled.py | 85 +++++----- test/text/write_page_break.pdf | Bin 2252 -> 2253 bytes 14 files changed, 145 insertions(+), 123 deletions(-) create mode 100644 test/text/render_styled_newpos.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e19d0f5..bf7d834ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). - documentation on combining `borb` & `fpdf2`: [Creating a borb.pdf.document.Document from a FPDF instance](https://pyfpdf.github.io/fpdf2/ExistingPDFs.html) ### Changed +- `write()` now supports soft hyphen characters, thanks to @gmischler - `image()` method now insert `.svg` images as PDF paths - log level of `_substitute_page_number()` has been lowered from `INFO` to `DEBUG` diff --git a/docs/PageBreaks.md b/docs/PageBreaks.md index ca7c3630e..5be73ae60 100644 --- a/docs/PageBreaks.md +++ b/docs/PageBreaks.md @@ -1,7 +1,8 @@ # Page breaks # -By default, `fpdf2` will automatically perform page breaks whenever a cell is rendered at the bottom of a page -with a height greater than the page bottom margin. +By default, `fpdf2` will automatically perform page breaks whenever a cell or +the text from a `write()` is rendered at the bottom of a page with a height +greater than the page bottom margin. This behaviour can be controlled using the [`set_auto_page_break`](fpdf/fpdf.html#fpdf.fpdf.FPDF.set_auto_page_break) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 620a1278a..e8371afbb 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -144,10 +144,10 @@ to 1 mm (against 0.2 by default) with [set_line_width](fpdf/fpdf.html#fpdf.fpdf.FPDF.set_line_width). Finally, we output the cell (the last parameter to true indicates that the background must be filled). -The method used to print the paragraphs is [multi_cell](fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell). +The method used to print the paragraphs is [multi_cell](fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell). Text is justified by default. Each time a line reaches the right extremity of the cell or a carriage return character is met, a line break is issued and a new cell automatically created -under the current one. Text is justified by default. +under the current one. An automatic break is performed at the location of the nearest space or soft-hyphen (\u00ad) character before the right limit. A soft-hyphen will be replaced by a normal hyphen when triggering a line break, and ignored otherwise. Two document properties are defined: the title ([set_title](fpdf/fpdf.html#fpdf.fpdf.FPDF.set_title)) and the author diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 1538a156f..e6907a970 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -42,7 +42,7 @@ from .fonts import fpdf_charwidths from .graphics_state import GraphicsStateMixin from .image_parsing import get_img_info, load_image, SUPPORTED_IMAGE_FILTERS -from .line_break import Fragment, MultiLineBreak +from .line_break import Fragment, TextLine, MultiLineBreak from .outline import serialize_outline, OutlineSection from . import drawing from .recorder import FPDFRecorder @@ -102,9 +102,10 @@ class X(IntEnum): RIGHT = 2 # self.x + w START = 3 # left end of actual text END = 4 # right end of actual text - CENTER = 5 # center of actual text - LMARGIN = 6 # self.l_margin - RMARGIN = 7 # self.w - self.r_margin + WCONT = 5 # continuation point for write() + CENTER = 6 # center of actual text + LMARGIN = 7 # self.l_margin + RMARGIN = 8 # self.w - self.r_margin class Y(IntEnum): @@ -2060,9 +2061,9 @@ def cell( txt = self.normalize_text(txt) styled_txt_frags = self._preload_font_styles(txt, markdown) return self._render_styled_cell_text( + TextLine(styled_txt_frags, 0.0, 0, False), w, h, - styled_txt_frags, border, newpos_x=newpos_x, newpos_y=newpos_y, @@ -2074,9 +2075,9 @@ def cell( def _render_styled_cell_text( self, + text_line, w=None, h=None, - styled_txt_frags=(), border=0, newpos_x=X.RIGHT, newpos_y=Y.TOP, @@ -2088,42 +2089,43 @@ def _render_styled_cell_text( """ Prints a cell (rectangular area) with optional borders, background color and character string. The upper-left corner of the cell corresponds to the current - position. The text can be aligned or centered. After the call, the current - position moves to the right or to the next line. It is possible to put a link - on the text. + position. The text can be aligned, centered or justified. After the call, the + current position moves to the requested new position. It is possible to put a + link on the text. If automatic page breaking is enabled and the cell goes beyond the limit, a page break is performed before outputting. Args: + text_line (TextLine instance): Contains the (possibly empty) tuple of + fragments to render. w (int): Cell width. Default value: None, meaning to fit text width. If 0, the cell extends up to the right margin. h (int): Cell height. Default value: None, meaning an height equal to the current font size. - styled_txt_frags (tuple): Tuple of fragments to render. - Default value: empty tuple. border: Indicates if borders must be drawn around the cell. The value can be either a number (`0`: no border ; `1`: frame) or a string containing some or all of the following characters (in any order): `L`: left ; `T`: top ; `R`: right ; `B`: bottom. Default value: 0. - newpos_x: Current position in x after the call. + newpos_x (Enum X): New current position in x after the call. X.LEFT - left end of the cell X.RIGHT - right end of the cell (default) X.START - start of actual text X.END - end of actual text + X.WCONT - for write() to continue next (slightly left of X.END) X.CENTER - center of actual text X.LMARGIN - left page margin (start of printable area) X.RMARGIN - right page margin (end of printable area) - newpos_y: Current position in y after the call. + newpos_y (Enum Y): New current position in y after the call. Y.TOP - top of the first line (default) Y.LAST - top of the last line (same as TOP for single-line text) Y.NEXT - top of next line (bottom of current text) Y.TMARGIN - top page margin (start of printable area) Y.BMARGIN - bottom page margin (end of printable area) - align (str): Allows to center or align the text inside the cell. - Possible values are: `L` or empty string: left align (default value) ; - `C`: center ; `R`: right align + align (str): Allows to align the text inside the cell. + Possible values are: `L` or empty string: left align (default value); + `C`: center; `R`: right align; `J`: justify (if more than one word) fill (bool): Indicates if the cell background must be painted (`True`) or transparent (`False`). Default value: False. link (str): optional link to add on the cell, internal @@ -2142,15 +2144,18 @@ def _render_styled_cell_text( "ignored" ) border = 1 - styled_txt_width = 0 - for styled_txt_frag in styled_txt_frags: - styled_txt_width += self.get_string_width(styled_txt_frag.string) + styled_txt_width = text_line.text_width / 1000 * self.font_size + if not styled_txt_width: + for styled_txt_frag in text_line.fragments: + styled_txt_width += self.get_string_width(styled_txt_frag.string) if w == 0: w = self.w - self.r_margin - self.x elif w is None: - if not styled_txt_frags: - raise ValueError("A 'txt' parameter must be provided if 'w' is None") - w = styled_txt_width + 2 + if not text_line.fragments: + raise ValueError( + "A 'text_line' parameter with fragments must be provided if 'w' is None" + ) + w = styled_txt_width + self.c_margin + self.c_margin if h is None: h = self.font_size # pylint: disable=invalid-unary-operand-type @@ -2197,7 +2202,7 @@ def _render_styled_cell_text( s_start = self.x s_width, underlines = 0, [] - if styled_txt_frags: + if text_line.fragments: if align == "R": dx = w - self.c_margin - styled_txt_width elif align == "C": @@ -2214,12 +2219,20 @@ def _render_styled_cell_text( f"BT {(self.x + dx) * k:.2f} " f"{(self.h - self.y - 0.5 * h - 0.3 * self.font_size) * k:.2f} Td" ) - # If multibyte, Tw has no effect - do word spacing using an - # adjustment before each space - if self.ws and self.unifontsubset: + + word_spacing = 0 + if align == "J" and text_line.number_of_spaces_between_words: + word_spacing = ( + w - self.c_margin - self.c_margin - styled_txt_width + ) / text_line.number_of_spaces_between_words + if word_spacing and self.unifontsubset: + # If multibyte, Tw has no effect - do word spacing using an + # adjustment before each space space = escape_parens(" ".encode("utf-16-be").decode("latin-1")) - s += " 0 Tw" - for frag in styled_txt_frags: + if self.ws > 0: + s += " 0 Tw" + self.ws = 0 + for frag in text_line.fragments: txt_frag = frag.string style = frag.style underline = frag.underline @@ -2246,15 +2259,23 @@ def _render_styled_cell_text( s += f"({word}) " is_last_word = (i + 1) == len(words) if not is_last_word: - adj = -(self.ws * self.k) * 1000 / self.font_size_pt - s += f"{adj}({space}) " + adj = -(word_spacing * self.k) * 1000 / self.font_size_pt + s += f"{adj:.3f}({space}) " if underline: underlines.append((self.x + dx + s_width, txt_frag)) self.underline = underline - s_width += self.get_string_width(txt_frag, True) + s_width += self.get_string_width( + txt_frag, True + ) + word_spacing * txt_frag.count(" ") s += "] TJ" else: - for frag in styled_txt_frags: + if word_spacing and word_spacing != self.ws: + self._out(f"{word_spacing * self.k:.3f} Tw") + elif self.ws > 0: + self._out("0 Tw") + self.ws = word_spacing + + for frag in text_line.fragments: txt_frag = frag.string style = frag.style underline = frag.underline @@ -2281,7 +2302,9 @@ def _render_styled_cell_text( if underline: underlines.append((self.x + dx + s_width, txt_frag)) self.underline = underline - s_width += self.get_string_width(txt_frag, True) + s_width += self.get_string_width( + txt_frag, True + ) + self.ws * txt_frag.count(" ") s += " ET" # Restoring font style & underline mode after handling changes # by Markdown annotations: @@ -2318,6 +2341,8 @@ def _render_styled_cell_text( elif newpos_x == X.START: self.x = s_start elif newpos_x == X.END: + self.x = s_start + s_width + elif newpos_x == X.WCONT: self.x = s_start + s_width - self.c_margin elif newpos_x == X.CENTER: self.x = (s_start + s_start + s_width) / 2.0 @@ -2445,14 +2470,14 @@ def _perform_page_break_if_need_be(self, h): def _perform_page_break(self): x, ws = self.x, self.ws + # Reset word spacing + # We want each page to start with the default value, so that splitting + # the document between pages later doesn't cause any trouble. if ws > 0: self.ws = 0 self._out("0 Tw") self.add_page(same=True) self.x = x # restore x but not y after drawing header - if ws > 0: - self.ws = ws - self._out(f"{ws * self.k:.3f} Tw") def _has_next_page(self): return self.pages_count > self.page @@ -2473,9 +2498,10 @@ def multi_cell( markdown=False, ): """ - This method allows printing text with line breaks. They can be automatic (as - soon as the text reaches the right border of the cell) or explicit (via the - `\n` character). As many cells as necessary are stacked, one below the other. + This method allows printing text with line breaks. They can be automatic + (breaking at the most recent space or soft-hyphen character) as soon as the text + reaches the right border of the cell, or explicit (via the `\\n` character). + As many cells as necessary are stacked, one below the other. Text can be aligned, centered or justified. The cell block can be framed and the background painted. @@ -2541,7 +2567,7 @@ def multi_cell( w = self.w - self.r_margin - self.x if h is None: h = self.font_size - maximum_allowed_width = (w - 2 * self.c_margin) * 1000 / self.font_size + maximum_allowed_emwidth = (w - 2 * self.c_margin) * 1000 / self.font_size # Calculate text length txt = self.normalize_text(txt) @@ -2564,10 +2590,12 @@ def multi_cell( self.get_normalized_string_width_with_style, justify=(align == "J"), ) - text_line = multi_line_break.get_line_of_given_width(maximum_allowed_width) + text_line = multi_line_break.get_line_of_given_width(maximum_allowed_emwidth) while (text_line) is not None: text_lines.append(text_line) - text_line = multi_line_break.get_line_of_given_width(maximum_allowed_width) + text_line = multi_line_break.get_line_of_given_width( + maximum_allowed_emwidth + ) for text_line_index, text_line in enumerate(text_lines): is_last_line = text_line_index == len(text_lines) - 1 @@ -2578,23 +2606,10 @@ def multi_cell( else: current_cell_height = h - word_spacing = 0 - if text_line.justify: - word_spacing = ( - (maximum_allowed_width - text_line.text_width) - / 1000 - * self.font_size - / text_line.number_of_spaces_between_words - ) - self._out(f"{word_spacing * self.k:.3f} Tw") - elif self.ws > 0: - self._out("0 Tw") - self.ws = word_spacing - new_page = self._render_styled_cell_text( + text_line, w, h=current_cell_height, - styled_txt_frags=text_line.fragments, border="".join( ( "T" if "T" in border and text_line_index == 0 else "", @@ -2605,7 +2620,7 @@ def multi_cell( ), newpos_x=newpos_x if is_last_line else X.LEFT, newpos_y=newpos_y if is_last_line else Y.NEXT, - align=align, + align="L" if (align == "J" and is_last_line) else align, fill=fill, link=link, ) @@ -2638,9 +2653,6 @@ def multi_cell( if self.font_style != prev_font_style: self.font_style = prev_font_style self.current_font = self.fonts[self.font_family + self.font_style] - normalized_string += ( - f" /F{self.current_font['i']} {self.font_size_pt:.2f} Tf" - ) self.underline = prev_underline self._markdown_leak_end_style = False @@ -2650,8 +2662,9 @@ def multi_cell( def write(self, h=None, txt="", link=""): """ Prints text from the current position. - When the right margin is reached (or the \n character is met), - a line break occurs and text continues from the left margin. + When the right margin is reached, a line break occurs at the most recent + space or soft-hyphen character, and text continues from the left margin. + A manual break happens any time the \\n character is met, Upon method exit, the current position is left just at the end of the text. Args: @@ -2680,17 +2693,19 @@ def write(self, h=None, txt="", link=""): ) prev_x = self.x # first line from current x position to right margin - first_width = self.w - prev_x - self.r_margin - 2 * self.c_margin - first_fswidth = first_width * 1000 / self.font_size + first_width = self.w - prev_x - self.r_margin + first_emwidth = first_width * 1000 / self.font_size text_line = multi_line_break.get_line_of_given_width( - first_fswidth, no_wordsplit=True + first_emwidth, no_wordsplit=True ) # remaining lines fill between margins - full_width = self.w - self.l_margin - self.r_margin - 2 * self.c_margin - full_fswidth = full_width * 1000 / self.font_size + full_width = self.w - self.l_margin - self.r_margin + full_emwidth = full_width * 1000 / self.font_size while (text_line) is not None: text_lines.append(text_line) - text_line = multi_line_break.get_line_of_given_width(full_fswidth) + text_line = multi_line_break.get_line_of_given_width(full_emwidth) + if text_line: + text_lines.append(text_line) if not text_lines: return False @@ -2702,11 +2717,11 @@ def write(self, h=None, txt="", link=""): line_width = full_width self.ln() new_page = self._render_styled_cell_text( + text_line, line_width, h=h, - styled_txt_frags=text_line.fragments, border=0, - newpos_x=X.END, + newpos_x=X.WCONT, newpos_y=Y.TOP, align="L", fill=False, diff --git a/fpdf/line_break.py b/fpdf/line_break.py index d58eeaf0e..a337e982e 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -245,7 +245,7 @@ def get_line_of_given_width(self, maximum_width, no_wordsplit=False): if no_wordsplit: line_full = True break - return current_line.manual_break() + return current_line.manual_break(self.justify) current_line.add_character( character, @@ -258,10 +258,11 @@ def get_line_of_given_width(self, maximum_width, no_wordsplit=False): self.character_index += 1 + if line_full and no_wordsplit: + # roll back and return empty line to trigger continuation + # on the next line. + self.fragment_index = last_fragment_index + self.character_index = last_character_index + return CurrentLine().manual_break(self.justify) if current_line.width: - if line_full and no_wordsplit: - # roll back and return empty line to trigger line break - self.fragment_index = last_fragment_index - self.character_index = last_character_index - return CurrentLine() return current_line.manual_break() diff --git a/test/html/html_headings_line_height.pdf b/test/html/html_headings_line_height.pdf index 6456351fdf6e3460836f17610c02252d9f64293d..3ef10011294282c2c3d41b81af22d37bcb5805f3 100644 GIT binary patch delta 555 zcmeB_X_uK$UvF$-XUA1sQk0sQ%T+OF@08PihYbW=zyIxuJap{G-y2~8uA)LApZFDK zO>0*$3DW%VRmG%G=!sfc=7~ex)oJg3{7BO{qb|Y8y8GmnLq~fU7IGwS(B84>shDy{ z#)TP*hY#vK?T=1s6@z_WmRPjVKlSYEpEQBwZ?XW0cyryT02f9tKfL?cx`C?ZGx z{rtpP>AS;cq{c&+|XO-o5_3mypA zo%cpv)?yx=C_-%g6j+rK( z7Ju7zuvfYu(El^<-9wx2GG6BrHB~SG0fjsTE-=Huz|7oo@<|?RTSE*Pa|>e(F$+@@ zbTLCiGlR*Fyu!9-7&7MO7^;m7%`r?hGBr1zJdsye6vG5#153lnJ9#}g47pTQUH#p- E0O%goR{#J2 delta 588 zcmZpd>6DpJUvFYyXUA1sQk0sQ%T+OF@062%hYbYU-q&_n9y(ePzfLqHu$5)%AEu7B zO&m_jQlyG_DsJ1pRLF;<(`(NuNgTHvz!_NS`2Tf@7VZMOt~ZD z!VJa32hTlZI`v-YO}1#jvw(X~bQZ2yUb^v7QvT*=+yzUg9O|gg^-^7;kvczk#@aZ! z`w4T>U-QpMxo;4=w|-gij^AFN|Nr04zu$hU%9&rHJJyJ<+k8}$MI)Enq*JctwATK) zem(qY+VSm=r?@EwJzOGak;v`)@Yr^ zm~lhTWUs7Ao%fH4mN~5MTM}AA?*@gK-{z8aI@BTe`9Z;Fjgw;t1ppb{*3f9FIW`uQHF(3q$ijX@?>GUZ3ar!KFBbJ@wUm}fWgxDCa@nLBFNZVFuG zxqIX+F@JyZsr6)AfiKj{m)$Sl8h^y|$EuOh zT`Ao`E7sVuOpC4smF!)XJv33a~L;FeNhj7@kq?84Z>mBD?@t;xT>_Go)eM%QN*qmIJcAO2PFk71wjsN;w7{r7j> zdp&3TkSEYH$iH*w;3zRj)+P-1&Z{{qu{) zQy)CNJ;|}ub*a;8u8Vxe8ET!nbjW4?D%$DKtJ0&R3)laY9p`M^ zP#ybx{{p+)7INP@*WudM(6cL}dXN03)wR3KTY3+@@v!wOj!L?6X6yRu9cdTO*;@uQ z4d?bXIF#rHJ_Wm*4| zjAaHenF)X?OwwZtqo7vd7-E7b)3xe+PKa>*g#r`Eoicci5d>;wX{O0s4~&rTA){(` zV1#|aX^i;6s2_~^!MGnx@`K4j^Y2-XKvO9FUfOc=-qCY%6*L?ZeC zVJ<}(>cgcuNP#+v7}nQP%z#^zFd&3dKI@D+JCRrmwLa;Xat@SGi3256mV9m1 zT&0QxgixWM2o-5rkbx2gcK~L@%i2qtQ3a4;oXx76J0}T1kZH@pG&O(o_7Y}70M^?6 z02Xp0Kx5805W*z{T;($N_E7&{h?olmE|d_oRU#NXi9{t>%V97j32a*xO9}<7C82m3 z&JvU{1x;ivnfegQjDfJq5*%$==9;V1{?F|ISztg$K@w1`X!EIWtY|3&7>D8%NtrvR z&io%?MSt1NU)yaR2}S delta 2413 zcmZuxeN2>f9G~DdcCjg*DM%7r(1c0;-k#_8{Iolb6T}M?4opF@$pVWF7D{FXJj>Dp zQs%s~z+jU@aJq(iZ0hXokMJcW5Gt-Pn{T;cNJAau)Y-$?l6$^)f89RM_vQ2b{661r zt9MGfcgj&`lqy@T#?2L*iZ+xQw>F&~xKMTpO*dSu`nK@1 zcefZW7&g!FhFI^hpc!ygk0?3g_>9c*0^Z z?XH|wy-jhOhOd@>l6`QzeCe#g{?I$tySGE%-1qUZq0S@cYHagI!@gTwQvLX7m1Xtt z!#maCFRrUgr`2anxzp=2S2y>BX1*17r1F7bi}TdzR|6+5(;KPEj^f$3EdP+M&vz}j zHtmYdm+K74e`D@-wYTMu?dF*Ums5NxjgMiiRsjP_v`klRNs&Pd0n` ztINCG>(gpqN&V8Ecs=dj-1eOFd2UC3dwF`|S+}Dk?PQI^*r=KZ+IISD9xBc@nwIT6 z?%0*MeM|Stm+UPWb!+xI(%+pZ+Utow*w7l+7L&8bl+kCldmGHJw;ebcQRN(R7sn)= zsz{8^+V`xTh5tzG9geQU?nv(W8#VX$c$P*jNGeQ!`>cDO=q!v#*pnIYm&0S#W*E9z zu;bcP(;|1pY@hvy1CL%f=W{&_9h8bnUD;++XM1#*^qd@vCVsJHu}1$NBnZK-@3XwB zg7lv_<{V&LA^=mE&|?atAXkwv#016uTy-)hL==5NfeGYJ8H{6uf?Up@G~V183BTrBiKa2u!+F>C&2{az#hpOYl4A6Fd;;5OiZW?=b9`GI06%i07^ur zddp(MT_0%`6sq?RCW3(hC}K=62*s{Gtpbq%J4z)mE@hJ5y_ia_xK$>LECY6w%You( z+E7IXT&ZxCuQI8K19l9qE@lMsVpM?cOk#jPlLcVJ6y~2v+nlG~VL||!l4dBqd=f>V zv7~hdaWVqJNnD!TV`6hPH)#h#pRDBq)-u7sAu{DKSAy=6WpE_1tf&;QmW48zp`Rev z9zai!69J~9zFQ=lw6;T8{ zLIOh*3W|VQg2A;AGCU80^CI9@p;gpLO%MW4LJ)c4;7JB1hz3rly&cikUvl-X$q3`d LgoM@kQzMikVAK?m6=O&BkMYjhRnRGI70|6nWmv z!+By-?}r(iTS8Y{vZ>SvFyC?T{w~+Qg10{Iy1t^%^jYZF)mv`AZLCj-Y)w}G@hsfW z%JsyuuIaH;xy=ei%fn(Gu8C0jxpH$$>|5R0LekeKPiNjy&XT!T)x=Gbdu8Iv?O*(M zDQe%X*l2p{dtduG-IWEdhaCihH1EG%#_{ChudPq`ZfPIASS-zzKUt!ChG!YWhnRHB zmpzO6PKZCXe-eBq`De`E@-CZs;pabj2fRNK%@brTp=i;`5b-0<>`w2BUbp<-9FN`B zJaPY*sP1`XX%=^A%UY31ix)P&`Eg6q^x5&PZxm)vSiIXu_jYh_P3oHWlZ?OgwMm}# zU#2X4zl86r_~soPI&5MlmI?+Sppd7)1!fo+nV6VP{>9f_Z;l~jibb`Vkuiokb8|xs zF-s#8bTMNCQ%o^K3#gbE!b!%)#u$z?h#CVa%nf>gw;t1ptQp B;XeQX delta 571 zcmbQMI8Sjy5_`R|ft?*!aY<2XVlG$3oUM}%1|3!qasB?cYxfmNcIQcEg|8#T-^qMt zp0Y=SuW^dQ*Uze9*4viOed#1-aG-j+_47%OGpB8=Z0)~U+kD%dZ{B7d=gWyV&s;n4 zNbRupqtt_v>jTqScWYnVvg6SE9d4iHPJi5UdBu0rXQh7^Z<%{n{$Ty8zSChJRM+hJ zG;4yq_Pp{?-5(QIZ`u5bDg9}aX!|*d{WqfPd~&4t9eV{13QytgyAYKtJEd{)!Hd!} zr_K3v`QWiv1{H$Z=1U$6_9{DSt##i2Rw=+iJZ|a-1IO$}#~nL3`(!73L_Vqh;df=4 z>msv8tB($mf1T}~+Dtuvw0^nHrHG$8_Nu(|nhvk%X`dQ2p{F5LB1HR7%PlFMbE~HH z>^~@Rraa2%g=2!ONX~W5l`K(GCw;f?y&7D1XocLWUdvyfuY8g<)eXDWI&bcR?bpv# z7G9Y9;s;ad_OBwFw{z&QiJ2NG7=VC6o&pz`VPIrpVmA3FU$?CVhKwl|)n-N}80yT; zjV2%D7q&G;moYXl#S}BNz!Wn!wwNp^Agqbu24ho8OffT4?U>0?=t#`j%{`k69&GC;u z_s%}D-5t~8&(c|T`PJu}qO*dyXPKp^99dzw=kK66QTII~Qcf3IcZ zk7J+aTl`~KbF^JyGZRy-pSX#IrGfznDC8+{ff+^yMkYp+rv+Hno0^-V$r%`!8yTRB z85$UwVTf6nVXCt6nqoM{$iUbP-FHR?CPv1SyMjD8jm)`JRbBnvxB#?S4;BCb delta 893 zcmdn6l=1db#tHTH=B9RbT*W0tsfoE<6?0lASqC3B5NJ7Hc|6hS2YdJ}RwsV%P0?u! zJl67MNVGL6vHJeAQ$Lz~Qmw4ZqHxc?zhT>j`YqFMJ+iDX`|(99!1I{puKb$9omb9W z5sIx?!6IyW>(-`aPTyNqtR6=*N}fsJ<+J?rr zOVpNI?lyRx7Zdu3eV20MDUMr4bC_J&yw5+9jb3{`(D?F)I=;2+mNxNzES6_azNq>n zvv#(}U7OhCs*-JIH!S!b{^4JBr-4?~#H(!z=@a^2TNQuRfAvSmkS*kl*{b>es~q>4 zs@}R#Ds5*MZ#K)nV^(JBk|$H^r;0kBoL1}oaM`@~3H@bm|0@z zwXifo76aN31VAzL5Hd8jFhf=dQf6oZ3>!3y4NZ(JFvLs^j7-qgnHpH4hnAtKv7Hfy i#inK$jxjPYF+}$rP|VD1a(s{nr?~-_s;aBM8y5gOS|KX{ diff --git a/test/text/render_styled_newpos.pdf b/test/text/render_styled_newpos.pdf new file mode 100644 index 0000000000000000000000000000000000000000..08bca61d28d7436d2ab199040bd4452049250be3 GIT binary patch literal 6566 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 literal 0 HcmV?d00001 diff --git a/test/text/test_cell.py b/test/text/test_cell.py index caec5426b..4dab7083f 100644 --- a/test/text/test_cell.py +++ b/test/text/test_cell.py @@ -155,7 +155,10 @@ def test_cell_missing_text_or_width(tmp_path): pdf.set_font("Times", size=16) with pytest.raises(ValueError) as error: pdf.cell() - assert str(error.value) == "A 'txt' parameter must be provided if 'w' is None" + assert ( + str(error.value) + == "A 'text_line' parameter with fragments must be provided if 'w' is None" + ) def test_cell_centering(tmp_path): diff --git a/test/text/test_multi_cell_justified_with_unicode_font.pdf b/test/text/test_multi_cell_justified_with_unicode_font.pdf index 1adf97daa6c3bb3c917d148ca0146ef6f6a572e7..a7799d8c37f60cec3726ad63550a70959d106ffa 100644 GIT binary patch delta 427 zcmdm3FtcDnUA>{DnVlV1aY<2XVlG$3oZJb9{7nV|t@D|}8W$*7-REeY+Qky#Taxfj zE$`UDgN;Y`zdx9eWcXJ1{_cId_c5F1sdZ1f_s;Y3zeOIhsnaIbU5Nay5grs8a_y?a z`x0M+gCb%%7BdPt&jjDPx>YB1UPF-AhJ-&&1{=BiH}cj`_?;DGy;=Q$kT#3jHo;HE zw~z3x`Iz&5{lA0Oqm7rc4b8+hd1oipJIMo&#o+a=lk-~Z59_UNPKCCYdIutkd* zo&TM%e`Di@hpyW@f-jxl_nUckh1H79>5T2BVy4Cl1|Xo2r@#ef7#f=!n@?sjx2iX` zM3XWwFtsqi5VJHeLKic%FfhXuGdDpOGcvF=#}G4yiYX(z$-=}CO`)N&g{9f#2j(7} N1_oTJs;>TSTmYkHmGS@p delta 451 zcmbPPu(4o5UA>W!ft?*!aY<2XVlG$3oYskk`G*VyTH_Tbm~}1)NT^FTx^iR3k&3k; zY-#zeYhPVxY;4_sUe#H3n)A22pX}z(d!JjOvF0XQT=)4SU+-P4mB^l1_j4*&dQHyE zs$s^)D;kqw#|!cu&$I0m(EYo!}0^wpC9*P3(+m2-;S#Ct;bDW@xFNUrNgznc~(O z;Z28}&K>?5!jw@N!t3wTu9sCUUwlLA{q7n6otZaI@n5$gW5&N->gFv$k^eUPGq#(G znVTyZfPg}t0vDKJXl!m~IQfm4RlS)pnv{WonSlw0n5B^!x|pGbu>q!-r3Jc}k)e?x lhL{OdOc~it7G|bs3Jr}djSMEAF!$gzGT~BHb@g}S0swp@oPGcR diff --git a/test/text/test_render_styled.py b/test/text/test_render_styled.py index e02158631..f16b0561d 100644 --- a/test/text/test_render_styled.py +++ b/test/text/test_render_styled.py @@ -1,7 +1,5 @@ from pathlib import Path -import pytest - import fpdf from fpdf.line_break import MultiLineBreak from test.conftest import assert_pdf_equal @@ -10,42 +8,50 @@ def test_render_styled_newpos(tmp_path): + """ + Verify that _render_styled_cell_text() places the new position + in the right places in all possible combinations of alignment, + newpos_x, and newpos_y. + """ doc = fpdf.FPDF() - # doc.add_page() - doc.set_font("helvetica", size=24) + doc.set_font("helvetica", style="U", size=24) doc.set_margin(10) twidth = 100 data = ( - # txt, align, newpos_x, newpos_y, target x/y - ["Left Top L", "L", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], - ["Left Top R", "R", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], - ["Left Top C", "C", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], - ["Left Top J", "J", fpdf.X.LEFT, fpdf.Y.TOP, 20, 20], - ["Right Last L", "L", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], - ["Right Last R", "R", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], - ["Right Last C", "C", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], - ["Right Last J", "J", fpdf.X.RIGHT, fpdf.Y.LAST, 70, 30], - ["Start Next L", "L", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], - ["Start Next R", "R", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], - ["Start Next C", "C", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], - ["Start Next J", "J", fpdf.X.START, fpdf.Y.NEXT, "s", 40 + doc.font_size], - ["End TMargin L", "L", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], - ["End TMargin R", "R", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], - ["End TMargin C", "C", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], - ["End TMargin J", "J", fpdf.X.END, fpdf.Y.TMARGIN, "e", 10], - ["Center TOP L", "L", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], - ["Center TOP R", "R", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], - ["Center TOP C", "C", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], - ["Center TOP J", "J", fpdf.X.CENTER, fpdf.Y.TOP, 50, doc.h - 10], - ["LMargin BMargin L", "L", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], - ["LMargin BMargin R", "R", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], - ["LMargin BMargin C", "C", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], - ["LMargin BMargin J", "J", fpdf.X.LMARGIN, fpdf.Y.BMARGIN, 10, 70], - ["RMargin Top L", "L", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], - ["RMargin Top R", "R", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], - ["RMargin Top C", "C", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], - ["RMargin Top J", "J", fpdf.X.RMARGIN, fpdf.Y.TOP, doc.w - 10, 80], + # txt, align, newpos_x, newpos_y + ["Left Top L", "L", fpdf.X.LEFT, fpdf.Y.TOP], + ["Left Top R", "R", fpdf.X.LEFT, fpdf.Y.TOP], + ["Left Top C", "C", fpdf.X.LEFT, fpdf.Y.TOP], + ["Left Top J", "J", fpdf.X.LEFT, fpdf.Y.TOP], + ["Right Last L", "L", fpdf.X.RIGHT, fpdf.Y.LAST], + ["Right Last R", "R", fpdf.X.RIGHT, fpdf.Y.LAST], + ["Right Last C", "C", fpdf.X.RIGHT, fpdf.Y.LAST], + ["Right Last J", "J", fpdf.X.RIGHT, fpdf.Y.LAST], + ["Start Next L", "L", fpdf.X.START, fpdf.Y.NEXT], + ["Start Next R", "R", fpdf.X.START, fpdf.Y.NEXT], + ["Start Next C", "C", fpdf.X.START, fpdf.Y.NEXT], + ["Start Next J", "J", fpdf.X.START, fpdf.Y.NEXT], + ["End TMargin L", "L", fpdf.X.END, fpdf.Y.TMARGIN], + ["End TMargin R", "R", fpdf.X.END, fpdf.Y.TMARGIN], + ["End TMargin C", "C", fpdf.X.END, fpdf.Y.TMARGIN], + ["End TMargin J", "J", fpdf.X.END, fpdf.Y.TMARGIN], + ["WCont Top L", "L", fpdf.X.WCONT, fpdf.Y.TOP], + ["WCont Top R", "R", fpdf.X.WCONT, fpdf.Y.TOP], + ["WCont Top C", "C", fpdf.X.WCONT, fpdf.Y.TOP], + ["WCont Top J", "J", fpdf.X.WCONT, fpdf.Y.TOP], + ["Center TOP L", "L", fpdf.X.CENTER, fpdf.Y.TOP], + ["Center TOP R", "R", fpdf.X.CENTER, fpdf.Y.TOP], + ["Center TOP C", "C", fpdf.X.CENTER, fpdf.Y.TOP], + ["Center TOP J", "J", fpdf.X.CENTER, fpdf.Y.TOP], + ["LMargin BMargin L", "L", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], + ["LMargin BMargin R", "R", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], + ["LMargin BMargin C", "C", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], + ["LMargin BMargin J", "J", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], + ["RMargin Top L", "L", fpdf.X.RMARGIN, fpdf.Y.TOP], + ["RMargin Top R", "R", fpdf.X.RMARGIN, fpdf.Y.TOP], + ["RMargin Top C", "C", fpdf.X.RMARGIN, fpdf.Y.TOP], + ["RMargin Top J", "J", fpdf.X.RMARGIN, fpdf.Y.TOP], ) for i, item in enumerate(data): @@ -58,8 +64,7 @@ def test_render_styled_newpos(tmp_path): align = item[1] newx = item[2] newy = item[3] - expx = item[4] - expy = item[5] + # pylint: disable=protected-access frags = doc._preload_font_styles(s, False) mlb = MultiLineBreak( frags, @@ -67,22 +72,18 @@ def test_render_styled_newpos(tmp_path): justify=(align == "J"), ) line = mlb.get_line_of_given_width(twidth * 1000 / doc.font_size) - new_page = doc._render_styled_cell_text( + doc._render_styled_cell_text( + line, twidth, - styled_txt_frags=line.fragments, border=1, align=align, # "L" if align == "J" else align, newpos_x=newx, newpos_y=newy, ) - sw = doc.get_string_width(line.fragments[0].string) - if expx == "s": - expx = doc.x + twidth - sw + # mark the new position in the file with crosshairs for verification with doc.rotation(i * -15, doc.x, doc.y): doc.circle(doc.x - 3, doc.y - 3, 6) doc.line(doc.x - 3, doc.y, doc.x + 3, doc.y) doc.line(doc.x, doc.y - 3, doc.x, doc.y + 3) - # assert doc.x == expx, f"Resulting x position {doc.x} != {expx}" - # assert doc.y == expy, f"Resulting y position {doc.y} != {expy}" assert_pdf_equal(doc, HERE / "render_styled_newpos.pdf", tmp_path) diff --git a/test/text/write_page_break.pdf b/test/text/write_page_break.pdf index 903e7d59e101ec19c2127ea8c63a197bf83aca0f..3cbcb37dde50000a0078fcfea9dff558ebad8653 100644 GIT binary patch delta 1333 zcmV-51*39URK{w@L1?EDG!7bnqU5NB1tvxR5pKWMsHTdjxPOg<0bP7s2g_imYZGBcNDKHx-3rUBP!d=temgP1 z13OB~bsg(l1!xTJ;?70H^{Vr`kc9*vs&fNj{b~DNaRsbL`YN2EUpc2q!VB37E;uQ% zkZ68W_q%8XqkmKRSUIRlvM=No^e3SeT}Rbi1fXmUN#MVS+;iUXn#7NKtc%~eL2yQy z`UrtHFfEv`4L)<+Imu6IJWvYOuj)ywy&ykLPw9htO@+)^$E1CT`xcI{giQ&@vZjO4 zubft$RtZOiuCsb^FySsNfh<`VHtS0$m$6?Jm6@a5b?E{ah z%odB3XhC}9oMTcxNJPb^jCc}9lK)5Uct$CVfS$d}+!IY*tWcBML7pI6JddHnOxH4e zVNy=nz>a30drKHq*raruw}7?2{xC6PUj{#`l95tvY0!V^;dVw%Z27MUTG8D zHFbbc%Bjv7uDYZGjQPb&5_57%xGx?ZV4**n4VP7?RD!u)WNuP_Y%tRR@r{0bBndWg zbt*#KE@M{Xbq{_Epsh}x1IxMp_#DPgzi|9wM@vVma%Ev{3V57-l);wUAPhzK^A%dQo78cfIJ50(yXe~e0BV$}=3ww( zNayn_z~H3J$wnt}%;o7lDJ}ebYIesS{yu#j;LDFawB4?0V7S2dAv`+lcl$H+&)x3O z!64xi>5(K!uN}O3S^(dHBnU-sUVv9hOrrsdvwy`us3lr*pJ8~tr}a&yjeaI6oNz!o z@n&I3IBx?O9b8e9m|!$|!OjTTZm0~-0JE1oN*#3x^Yj5-^Mau8fENSadGRXi+|zUYF=N0@D9x7LL&)2l1?E= zTz}98dTRY@l%LsM%&*qKW65GNN|6H$C0fLZIuNorpp9O+Nu#EPYG7N^n8b?~w{Ao9 zAljCtFMOH9X*P8fr>4F0j=PMWg`s1tS2Jhp^MouYp-fZ7I`-#nfRl{maOQ+3JRNK1 z{0_dO9f?_Fbd5W9_f2ZEIP2Le&tA{gkblG+agfWguB%a_4kdu(tr6yxK)81oAL@_w#;@yxJwX>Wrjm*?igD7*6(z&Zk{zq|=~-F-3cTR;a`9HW|e zEHMOO+xBy|?Ed;MLe{gSC5&+_*@7B(=x%ZOxXxXrb&vBFK@T(S_Ras@##Ok+x(H1? z z9^`0j4?iXtl$o?`L29cJG>#g7;n1R%6No|fAL06Y3w10aRDZnNs*XUxU<-DF=eGvO z39XLjbY|=@aiqMCNOSZ?4F;PDC%QXfKPnAq<68*EP^!x@qQpGpFaf@yZsa5;sRj_8 z{A1+^uh>ypw(CUSC_rP7!=1A`xL$Sc77AbBtLp4XurAxaSFHlkBd@JGCc;r#Lq7yL zG_sW~F|nd(d4E&4n{1@uNJZisjZb4mw{)=3$gU$bR{^M6Lln?=hIhIo);%pE@v#-Z zv7>ASRq7%*JdSiu%#T7#;bfzjI1$f-6#Yg|I_)L-X?}97Q=)xv`M5emT<20(5ihX6 zEO9+*C4XC_B2&6%85N#D4pk=VRnrJ;M3gXrEs{`nHh}Y~Q|62PDRaj!wRWh>`mUc_ z+IV*b-*_qCYl5Oq0?hTrPKY(xMBEn#?ZNzfG&?RUswBap7Y#pRKTMH%gu6jMKN1DI zaOM3FB6>nqbGv)Ld7!;X&H?izg|A_r^ee}&Zc7w}-;k9Q&iyR2r@vMu4!O5}eNGEP+-M2|OOg!%{n zE|XIOi+?sgJ_>Vma%Ev{3V57-ltFXbAP|M``4xL?FRm<0k#p^|J@l;n04mn$W)LU{ z-2D752#8fYy)a|Re17}hTetA@p*i#e{CW5~!j~UMXuCtxz<7c0V|a8r9*$=ip1Q-S zgHggK(i2INUORa6v;w{ZNf3(Oya2D1m?r}k7k`VtQA@PsKEwFBrwvV}O@1LMoNz#T z<1NCHaNY(mI=G@HG1rLhu^^0IadH^%IG9P(oK79|0Ez;C4!={w+V&o7BBlJijYi9d zF#fdBR?_uwk5A_e+qnQDD7tY^iC@MtUm9iXcGj?=_>QYfU%mJj76RUZxm;)>!Oyo> z2!9e+w1FN+uX%lO{%Vw8*mx|I*(k+2F&xncPLyTK>_AOxO9hx206J)Sap7D~rhuNKbMSEPO@0c_=g6(#J77IDeO#0gKHYgv6? z0!Ve>>-dg#B4!a!y$z|&>a1t0JR7=LLw^$Q4B4NG{bdr4HE+#kpqVCtlSmQ%2iELY zAg0CYe5w{g2ql=v` Date: Thu, 3 Mar 2022 23:56:53 +0100 Subject: [PATCH 63/67] print_sh option for write() and multi_cell() --- fpdf/fpdf.py | 12 +++++++++-- fpdf/line_break.py | 21 +++++++++++-------- .../end_to_end_legacy/charmap/test_charmap.py | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index e6907a970..422648c2d 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -2496,6 +2496,7 @@ def multi_cell( ln=0, max_line_height=None, markdown=False, + print_sh=False, ): """ This method allows printing text with line breaks. They can be automatic @@ -2530,6 +2531,8 @@ def multi_cell( max_line_height (int): optional maximum height of each sub-cell generated markdown (bool): enable minimal markdown-like markup to render part of text as bold / italics / underlined. Default to False. + print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable + character, instead of a line breaking opportunity. Default value: False Using `ln=3` and `maximum height=pdf.font_size` is useful to build tables with multiline text in cells. @@ -2589,6 +2592,7 @@ def multi_cell( styled_text_fragments, self.get_normalized_string_width_with_style, justify=(align == "J"), + print_sh=print_sh, ) text_line = multi_line_break.get_line_of_given_width(maximum_allowed_emwidth) while (text_line) is not None: @@ -2659,7 +2663,7 @@ def multi_cell( return page_break_triggered @check_page - def write(self, h=None, txt="", link=""): + def write(self, h=None, txt="", link="", print_sh=False): """ Prints text from the current position. When the right margin is reached, a line break occurs at the most recent @@ -2672,6 +2676,8 @@ def write(self, h=None, txt="", link=""): txt (str): text content link (str): optional link to add on the text, internal (identifier returned by `add_link`) or external URL. + print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable + character, instead of a line breaking opportunity. Default value: False """ if not self.font_family: raise FPDFException("No font set, you need to call set_font() beforehand") @@ -2689,7 +2695,9 @@ def write(self, h=None, txt="", link=""): text_lines = [] multi_line_break = MultiLineBreak( - styled_text_fragments, self.get_normalized_string_width_with_style + styled_text_fragments, + self.get_normalized_string_width_with_style, + print_sh=print_sh, ) prev_x = self.x # first line from current x position to right margin diff --git a/fpdf/line_break.py b/fpdf/line_break.py index a337e982e..d8055a3b6 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -61,7 +61,8 @@ def __eq__(self, other): class CurrentLine: - def __init__(self): + def __init__(self, print_sh=False): + self.print_sh = print_sh self.fragments = [] self.width = 0 self.number_of_spaces = 0 @@ -75,6 +76,8 @@ def __init__(self): # SpaceHint is used fo this purpose. # 3 - position of last inserted soft-hyphen # HyphenHint is used fo this purpose. + # If print_sh=True, soft-hyphen is treated as + # a normal printable character. # The purpose of multiple positions tracking - to have an ability # to break in multiple places, depending on condition. self.space_break_hint = None @@ -115,7 +118,7 @@ def add_character( self.number_of_spaces, ) self.number_of_spaces += 1 - elif character == SOFT_HYPHEN: + elif character == SOFT_HYPHEN and not self.print_sh: self.hyphen_break_hint = HyphenHint( original_fragment_index, original_character_index, @@ -129,7 +132,7 @@ def add_character( underline, ) - if character != SOFT_HYPHEN: + if character != SOFT_HYPHEN or self.print_sh: self.width += character_width active_fragment.characters.append(character) @@ -185,18 +188,18 @@ def automatic_break(self, justify): class MultiLineBreak: - def __init__(self, styled_text_fragments, size_by_style, justify=False): - + def __init__( + self, styled_text_fragments, size_by_style, justify=False, print_sh=False + ): self.styled_text_fragments = styled_text_fragments - self.size_by_style = size_by_style self.justify = justify - + self.print_sh = print_sh self.fragment_index = 0 self.character_index = 0 def _get_character_width(self, character, style=""): - if character == SOFT_HYPHEN: + if character == SOFT_HYPHEN and not self.print_sh: # HYPHEN is inserted instead of SOFT_HYPHEN character = HYPHEN return self.size_by_style(character, style) @@ -211,7 +214,7 @@ def get_line_of_given_width(self, maximum_width, no_wordsplit=False): last_character_index = self.character_index line_full = False - current_line = CurrentLine() + current_line = CurrentLine(print_sh=self.print_sh) while self.fragment_index < len(self.styled_text_fragments): current_fragment = self.styled_text_fragments[self.fragment_index] diff --git a/test/end_to_end_legacy/charmap/test_charmap.py b/test/end_to_end_legacy/charmap/test_charmap.py index 4ddfcd252..4f50e6d78 100644 --- a/test/end_to_end_legacy/charmap/test_charmap.py +++ b/test/end_to_end_legacy/charmap/test_charmap.py @@ -56,7 +56,7 @@ def test_first_999_chars(font_filename, tmp_path): # Create a PDF with the first 999 charters defined in the font: for counter, character in enumerate(ttf.saveChar, 0): - pdf.write(8, f"{counter:03}) {character:03x} - {character:c}") + pdf.write(8, f"{counter:03}) {character:03x} - {character:c}", print_sh=True) pdf.ln() if counter >= 999: break From ff3b19a6b1dedfe0b10954f7bdf0725c17739665 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 4 Mar 2022 00:09:12 +0100 Subject: [PATCH 64/67] tabs to spaces --- fpdf/fpdf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 5985984ed..0f0a696c1 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -2531,7 +2531,7 @@ def multi_cell( markdown (bool): enable minimal markdown-like markup to render part of text as bold / italics / underlined. Default to False. print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable - character, instead of a line breaking opportunity. Default value: False + character, instead of a line breaking opportunity. Default value: False Using `ln=3` and `maximum height=pdf.font_size` is useful to build tables with multiline text in cells. @@ -2676,7 +2676,7 @@ def write(self, h=None, txt="", link="", print_sh=False): link (str): optional link to add on the text, internal (identifier returned by `add_link`) or external URL. print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable - character, instead of a line breaking opportunity. Default value: False + character, instead of a line breaking opportunity. Default value: False """ if not self.font_family: raise FPDFException("No font set, you need to call set_font() beforehand") From 5884cc89f20692ef8700aeaa8ccc83e155c648d6 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 5 Mar 2022 00:41:54 +0100 Subject: [PATCH 65/67] Apply PR review --- fpdf/__init__.py | 8 +- fpdf/fpdf.py | 131 +++++++++++++----------- fpdf/line_break.py | 14 ++- test/html/html_headings_line_height.pdf | Bin 3591 -> 3593 bytes test/outline/html_toc.pdf | Bin 4253 -> 4254 bytes test/text/test_render_styled.py | 64 ++++++------ test/text/write_page_break.pdf | Bin 2253 -> 2252 bytes 7 files changed, 118 insertions(+), 99 deletions(-) diff --git a/fpdf/__init__.py b/fpdf/__init__.py index 94c9c25fb..49a8b84f4 100644 --- a/fpdf/__init__.py +++ b/fpdf/__init__.py @@ -3,8 +3,8 @@ from .fpdf import ( FPDF, - X, - Y, + XPos, + YPos, FPDFException, TitleStyle, FPDF_FONT_DIR as _FPDF_FONT_DIR, @@ -41,8 +41,8 @@ "__license__", # Classes "FPDF", - "X", - "Y", + "XPos", + "YPos", "Template", "FlexTemplate", "TitleStyle", diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 0f0a696c1..6a1d73f30 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -96,7 +96,19 @@ class DocumentState(IntEnum): CLOSED = 3 # EOF printed -class X(IntEnum): +class XPos(IntEnum): + """ + Positional values in horizontal direction for use after printing text. + LEFT - left end of the cell + RIGHT - right end of the cell (default) + START - start of actual text + END - end of actual text + WCONT - for write() to continue next (slightly left of END) + CENTER - center of actual text + LMARGIN - left page margin (start of printable area) + RMARGIN - right page margin (end of printable area) + """ + LEFT = 1 # self.x RIGHT = 2 # self.x + w START = 3 # left end of actual text @@ -107,7 +119,16 @@ class X(IntEnum): RMARGIN = 8 # self.w - self.r_margin -class Y(IntEnum): +class YPos(IntEnum): + """ + Positional values in vertical direction for use after printing text. + TOP - top of the first line (default) + LAST - top of the last line (same as TOP for single-line text) + NEXT - top of next line (bottom of current text) + TMARGIN - top page margin (start of printable area) + BMARGIN - bottom page margin (end of printable area) + """ + TOP = 1 # self.y LAST = 2 # top of last line (TOP for single lines) NEXT = 3 # LAST + h @@ -2048,19 +2069,24 @@ def cell( "ignored" ) border = 1 - newpos_x = X.RIGHT - newpos_y = Y.TOP + newpos_x = XPos.RIGHT + newpos_y = YPos.TOP if ln == 1: - newpos_x = X.LMARGIN - newpos_y = Y.NEXT + newpos_x = XPos.LMARGIN + newpos_y = YPos.NEXT elif ln == 2: - newpos_x = X.LEFT - newpos_y = Y.NEXT + newpos_x = XPos.LEFT + newpos_y = YPos.NEXT # Font styles preloading must be performed before any call to FPDF.get_string_width: txt = self.normalize_text(txt) styled_txt_frags = self._preload_font_styles(txt, markdown) return self._render_styled_cell_text( - TextLine(styled_txt_frags, 0.0, 0, False), + TextLine( + styled_txt_frags, + text_width=0.0, + number_of_spaces_between_words=0, + justify=False, + ), w, h, border, @@ -2078,8 +2104,8 @@ def _render_styled_cell_text( w=None, h=None, border=0, - newpos_x=X.RIGHT, - newpos_y=Y.TOP, + newpos_x=XPos.RIGHT, + newpos_y=YPos.TOP, align="", fill=False, link="", @@ -2107,21 +2133,8 @@ def _render_styled_cell_text( or a string containing some or all of the following characters (in any order): `L`: left ; `T`: top ; `R`: right ; `B`: bottom. Default value: 0. - newpos_x (Enum X): New current position in x after the call. - X.LEFT - left end of the cell - X.RIGHT - right end of the cell (default) - X.START - start of actual text - X.END - end of actual text - X.WCONT - for write() to continue next (slightly left of X.END) - X.CENTER - center of actual text - X.LMARGIN - left page margin (start of printable area) - X.RMARGIN - right page margin (end of printable area) - newpos_y (Enum Y): New current position in y after the call. - Y.TOP - top of the first line (default) - Y.LAST - top of the last line (same as TOP for single-line text) - Y.NEXT - top of next line (bottom of current text) - Y.TMARGIN - top page margin (start of printable area) - Y.BMARGIN - bottom page margin (end of printable area) + newpos_x (Enum XPos): New current position in x after the call. + newpos_y (Enum YPos): New current position in y after the call. align (str): Allows to align the text inside the cell. Possible values are: `L` or empty string: left align (default value); `C`: center; `R`: right align; `J`: justify (if more than one word) @@ -2219,7 +2232,9 @@ def _render_styled_cell_text( f"{(self.h - self.y - 0.5 * h - 0.3 * self.font_size) * k:.2f} Td" ) - word_spacing = 0 + word_spacing = ( + 0 # precursor to self.ws, or manual spacing of unicode fonts. + ) if align == "J" and text_line.number_of_spaces_between_words: word_spacing = ( w - self.c_margin - self.c_margin - styled_txt_width @@ -2334,29 +2349,29 @@ def _render_styled_cell_text( self._out(s) self.lasth = h - # X.LEFT -> self.x stays the same - if newpos_x == X.RIGHT: + # XPos.LEFT -> self.x stays the same + if newpos_x == XPos.RIGHT: self.x += w - elif newpos_x == X.START: + elif newpos_x == XPos.START: self.x = s_start - elif newpos_x == X.END: + elif newpos_x == XPos.END: self.x = s_start + s_width - elif newpos_x == X.WCONT: + elif newpos_x == XPos.WCONT: self.x = s_start + s_width - self.c_margin - elif newpos_x == X.CENTER: + elif newpos_x == XPos.CENTER: self.x = (s_start + s_start + s_width) / 2.0 - elif newpos_x == X.LMARGIN: + elif newpos_x == XPos.LMARGIN: self.x = self.l_margin - elif newpos_x == X.RMARGIN: + elif newpos_x == XPos.RMARGIN: self.x = self.w - self.r_margin - # Y.TOP: -> self.y stays the same - # Y.LAST: -> self.y stays the same (single line) - if newpos_y == Y.NEXT: + # YPos.TOP: -> self.y stays the same + # YPos.LAST: -> self.y stays the same (single line) + if newpos_y == YPos.NEXT: self.y += h - if newpos_y == Y.TMARGIN: + if newpos_y == YPos.TMARGIN: self.y = self.t_margin - if newpos_y == Y.BMARGIN: + if newpos_y == YPos.BMARGIN: self.y = self.h - self.b_margin return page_break_triggered @@ -2531,7 +2546,7 @@ def multi_cell( markdown (bool): enable minimal markdown-like markup to render part of text as bold / italics / underlined. Default to False. print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable - character, instead of a line breaking opportunity. Default value: False + character, instead of a line breaking opportunity. Default value: False Using `ln=3` and `maximum height=pdf.font_size` is useful to build tables with multiline text in cells. @@ -2544,14 +2559,14 @@ def multi_cell( "Parameter 'w' and 'h' must be numbers, not strings." " You can omit them by passing string content with txt=" ) - newpos_x = X.RIGHT - newpos_y = Y.NEXT + newpos_x = XPos.RIGHT + newpos_y = YPos.NEXT if ln == 1: - newpos_x = X.LMARGIN + newpos_x = XPos.LMARGIN elif ln == 2: - newpos_x = X.LEFT + newpos_x = XPos.LEFT elif ln == 3: - newpos_y = Y.TOP + newpos_y = YPos.TOP page_break_triggered = False if split_only: @@ -2621,20 +2636,20 @@ def multi_cell( "B" if "B" in border and is_last_line else "", ) ), - newpos_x=newpos_x if is_last_line else X.LEFT, - newpos_y=newpos_y if is_last_line else Y.NEXT, + newpos_x=newpos_x if is_last_line else XPos.LEFT, + newpos_y=newpos_y if is_last_line else YPos.NEXT, align="L" if (align == "J" and is_last_line) else align, fill=fill, link=link, ) - if is_last_line and new_page and newpos_y == Y.TOP: + if is_last_line and new_page and newpos_y == YPos.TOP: # When a page jump is performed and the requested y is TOP (ln=3), # pretend we started at the top of the text block on the new page. # cf. test_multi_cell_table_with_automatic_page_break prev_y = self.y page_break_triggered = page_break_triggered or new_page - if newpos_y == Y.TOP: # We may have jumped a few lines -> reset + if newpos_y == YPos.TOP: # We may have jumped a few lines -> reset self.y = prev_y if split_only: @@ -2676,7 +2691,7 @@ def write(self, h=None, txt="", link="", print_sh=False): link (str): optional link to add on the text, internal (identifier returned by `add_link`) or external URL. print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable - character, instead of a line breaking opportunity. Default value: False + character, instead of a line breaking opportunity. Default value: False """ if not self.font_family: raise FPDFException("No font set, you need to call set_font() beforehand") @@ -2701,13 +2716,13 @@ def write(self, h=None, txt="", link="", print_sh=False): prev_x = self.x # first line from current x position to right margin first_width = self.w - prev_x - self.r_margin - first_emwidth = first_width * 1000 / self.font_size + first_emwidth = (first_width - 2 * self.c_margin) * 1000 / self.font_size text_line = multi_line_break.get_line_of_given_width( - first_emwidth, no_wordsplit=True + first_emwidth, wordsplit=False ) # remaining lines fill between margins full_width = self.w - self.l_margin - self.r_margin - full_emwidth = full_width * 1000 / self.font_size + full_emwidth = (full_width - 2 * self.c_margin) * 1000 / self.font_size while (text_line) is not None: text_lines.append(text_line) text_line = multi_line_break.get_line_of_given_width(full_emwidth) @@ -2728,8 +2743,8 @@ def write(self, h=None, txt="", link="", print_sh=False): line_width, h=h, border=0, - newpos_x=X.WCONT, - newpos_y=Y.TOP, + newpos_x=XPos.WCONT, + newpos_y=YPos.TOP, align="L", fill=False, link=link, @@ -4260,8 +4275,8 @@ def _is_xml(img: io.BytesIO): __all__ = [ "FPDF", - "X", - "Y", + "XPos", + "YPos", "load_cache", "get_page_format", "TitleStyle", diff --git a/fpdf/line_break.py b/fpdf/line_break.py index d8055a3b6..9b361776a 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -62,6 +62,12 @@ def __eq__(self, other): class CurrentLine: def __init__(self, print_sh=False): + """ + Per-line text fragment management for use by MultiLineBreak. + Args: + print_sh (bool): If true, a soft-hyphen will be rendered + normally, instead of triggering a line break. Default: False + """ self.print_sh = print_sh self.fragments = [] self.width = 0 @@ -76,8 +82,6 @@ def __init__(self, print_sh=False): # SpaceHint is used fo this purpose. # 3 - position of last inserted soft-hyphen # HyphenHint is used fo this purpose. - # If print_sh=True, soft-hyphen is treated as - # a normal printable character. # The purpose of multiple positions tracking - to have an ability # to break in multiple places, depending on condition. self.space_break_hint = None @@ -205,7 +209,7 @@ def _get_character_width(self, character, style=""): return self.size_by_style(character, style) # pylint: disable=too-many-return-statements - def get_line_of_given_width(self, maximum_width, no_wordsplit=False): + def get_line_of_given_width(self, maximum_width, wordsplit=True): if self.fragment_index == len(self.styled_text_fragments): return None @@ -245,7 +249,7 @@ def get_line_of_given_width(self, maximum_width, no_wordsplit=False): ) = current_line.automatic_break(self.justify) self.character_index += 1 return line - if no_wordsplit: + if not wordsplit: line_full = True break return current_line.manual_break(self.justify) @@ -261,7 +265,7 @@ def get_line_of_given_width(self, maximum_width, no_wordsplit=False): self.character_index += 1 - if line_full and no_wordsplit: + if line_full and not wordsplit: # roll back and return empty line to trigger continuation # on the next line. self.fragment_index = last_fragment_index diff --git a/test/html/html_headings_line_height.pdf b/test/html/html_headings_line_height.pdf index 3ef10011294282c2c3d41b81af22d37bcb5805f3..6456351fdf6e3460836f17610c02252d9f64293d 100644 GIT binary patch delta 588 zcmZpd>6DpJUvFYyXUA1sQk0sQ%T+OF@062%hYbYU-q&_n9y(ePzfLqHu$5)%AEu7B zO&m_jQlyG_DsJ1pRLF;<(`(NuNgTHvz!_NS`2Tf@7VZMOt~ZD z!VJa32hTlZI`v-YO}1#jvw(X~bQZ2yUb^v7QvT*=+yzUg9O|gg^-^7;kvczk#@aZ! z`w4T>U-QpMxo;4=w|-gij^AFN|Nr04zu$hU%9&rHJJyJ<+k8}$MI)Enq*JctwATK) zem(qY+VSm=r?@EwJzOGak;v`)@Yr^ zm~lhTWUs7Ao%fH4mN~5MTM}AA?*@gK-{z8aI@BTe`9Z;Fjgw;t1ppb{*30*$3DW%VRmG%G=!sfc=7~ex)oJg3{7BO{qb|Y8y8GmnLq~fU7IGwS(B84>shDy{ z#)TP*hY#vK?T=1s6@z_WmRPjVKlSYEpEQBwZ?XW0cyryT02f9tKfL?cx`C?ZGx z{rtpP>AS;cq{c&+|XO-o5_3mypA zo%cpv)?yx=C_-%g6j+rK( z7Ju7zuvfYu(El^<-9wx2GG6BrHB~SG0fjsTE-=Huz|7oo@<|?RTSE*Pa|>e(F$+@@ zbTLCiGlR*Fyu!9-7&7MO7^;m7%`r?hGBr1zJdsye6vG5#153lnJ9#}g47pTQUH#p- E0O%goR{#J2 diff --git a/test/outline/html_toc.pdf b/test/outline/html_toc.pdf index a813dc1881f2232296ac8024530d173e6257820f..c7a33917467d2027f15df0bb3dad18c51a59d5a0 100644 GIT binary patch delta 571 zcmbQMI8Sjy5_`R|ft?*!aY<2XVlG$3oUM}%1|3!qasB?cYxfmNcIQcEg|8#T-^qMt zp0Y=SuW^dQ*Uze9*4viOed#1-aG-j+_47%OGpB8=Z0)~U+kD%dZ{B7d=gWyV&s;n4 zNbRupqtt_v>jTqScWYnVvg6SE9d4iHPJi5UdBu0rXQh7^Z<%{n{$Ty8zSChJRM+hJ zG;4yq_Pp{?-5(QIZ`u5bDg9}aX!|*d{WqfPd~&4t9eV{13QytgyAYKtJEd{)!Hd!} zr_K3v`QWiv1{H$Z=1U$6_9{DSt##i2Rw=+iJZ|a-1IO$}#~nL3`(!73L_Vqh;df=4 z>msv8tB($mf1T}~+Dtuvw0^nHrHG$8_Nu(|nhvk%X`dQ2p{F5LB1HR7%PlFMbE~HH z>^~@Rraa2%g=2!ONX~W5l`K(GCw;f?y&7D1XocLWUdvyfuY8g<)eXDWI&bcR?bpv# z7G9Y9;s;ad_OBwFw{z&QiJ2NG7=VC6o&pz`VPIrpVmA3FU$?CVhKwl|)n-N}80yT; zjV2%D7q&G;moYXl#S}BNz!Wn!wwNp^Agqbu24ho8OffT4@kQzMikVAK?m6=O&BkMYjhRnRGI70|6nWmv z!+By-?}r(iTS8Y{vZ>SvFyC?T{w~+Qg10{Iy1t^%^jYZF)mv`AZLCj-Y)w}G@hsfW z%JsyuuIaH;xy=ei%fn(Gu8C0jxpH$$>|5R0LekeKPiNjy&XT!T)x=Gbdu8Iv?O*(M zDQe%X*l2p{dtduG-IWEdhaCihH1EG%#_{ChudPq`ZfPIASS-zzKUt!ChG!YWhnRHB zmpzO6PKZCXe-eBq`De`E@-CZs;pabj2fRNK%@brTp=i;`5b-0<>`w2BUbp<-9FN`B zJaPY*sP1`XX%=^A%UY31ix)P&`Eg6q^x5&PZxm)vSiIXu_jYh_P3oHWlZ?OgwMm}# zU#2X4zl86r_~soPI&5MlmI?+Sppd7)1!fo+nV6VP{>9f_Z;l~jibb`Vkuiokb8|xs zF-s#8bTMNCQ%o^K3#gbE!b!%)#u$z?h#CVa%nf>gw;t1ptQp B;XeQX diff --git a/test/text/test_render_styled.py b/test/text/test_render_styled.py index f16b0561d..274d13d90 100644 --- a/test/text/test_render_styled.py +++ b/test/text/test_render_styled.py @@ -20,38 +20,38 @@ def test_render_styled_newpos(tmp_path): data = ( # txt, align, newpos_x, newpos_y - ["Left Top L", "L", fpdf.X.LEFT, fpdf.Y.TOP], - ["Left Top R", "R", fpdf.X.LEFT, fpdf.Y.TOP], - ["Left Top C", "C", fpdf.X.LEFT, fpdf.Y.TOP], - ["Left Top J", "J", fpdf.X.LEFT, fpdf.Y.TOP], - ["Right Last L", "L", fpdf.X.RIGHT, fpdf.Y.LAST], - ["Right Last R", "R", fpdf.X.RIGHT, fpdf.Y.LAST], - ["Right Last C", "C", fpdf.X.RIGHT, fpdf.Y.LAST], - ["Right Last J", "J", fpdf.X.RIGHT, fpdf.Y.LAST], - ["Start Next L", "L", fpdf.X.START, fpdf.Y.NEXT], - ["Start Next R", "R", fpdf.X.START, fpdf.Y.NEXT], - ["Start Next C", "C", fpdf.X.START, fpdf.Y.NEXT], - ["Start Next J", "J", fpdf.X.START, fpdf.Y.NEXT], - ["End TMargin L", "L", fpdf.X.END, fpdf.Y.TMARGIN], - ["End TMargin R", "R", fpdf.X.END, fpdf.Y.TMARGIN], - ["End TMargin C", "C", fpdf.X.END, fpdf.Y.TMARGIN], - ["End TMargin J", "J", fpdf.X.END, fpdf.Y.TMARGIN], - ["WCont Top L", "L", fpdf.X.WCONT, fpdf.Y.TOP], - ["WCont Top R", "R", fpdf.X.WCONT, fpdf.Y.TOP], - ["WCont Top C", "C", fpdf.X.WCONT, fpdf.Y.TOP], - ["WCont Top J", "J", fpdf.X.WCONT, fpdf.Y.TOP], - ["Center TOP L", "L", fpdf.X.CENTER, fpdf.Y.TOP], - ["Center TOP R", "R", fpdf.X.CENTER, fpdf.Y.TOP], - ["Center TOP C", "C", fpdf.X.CENTER, fpdf.Y.TOP], - ["Center TOP J", "J", fpdf.X.CENTER, fpdf.Y.TOP], - ["LMargin BMargin L", "L", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], - ["LMargin BMargin R", "R", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], - ["LMargin BMargin C", "C", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], - ["LMargin BMargin J", "J", fpdf.X.LMARGIN, fpdf.Y.BMARGIN], - ["RMargin Top L", "L", fpdf.X.RMARGIN, fpdf.Y.TOP], - ["RMargin Top R", "R", fpdf.X.RMARGIN, fpdf.Y.TOP], - ["RMargin Top C", "C", fpdf.X.RMARGIN, fpdf.Y.TOP], - ["RMargin Top J", "J", fpdf.X.RMARGIN, fpdf.Y.TOP], + ["Left Top L", "L", fpdf.XPos.LEFT, fpdf.YPos.TOP], + ["Left Top R", "R", fpdf.XPos.LEFT, fpdf.YPos.TOP], + ["Left Top C", "C", fpdf.XPos.LEFT, fpdf.YPos.TOP], + ["Left Top J", "J", fpdf.XPos.LEFT, fpdf.YPos.TOP], + ["Right Last L", "L", fpdf.XPos.RIGHT, fpdf.YPos.LAST], + ["Right Last R", "R", fpdf.XPos.RIGHT, fpdf.YPos.LAST], + ["Right Last C", "C", fpdf.XPos.RIGHT, fpdf.YPos.LAST], + ["Right Last J", "J", fpdf.XPos.RIGHT, fpdf.YPos.LAST], + ["Start Next L", "L", fpdf.XPos.START, fpdf.YPos.NEXT], + ["Start Next R", "R", fpdf.XPos.START, fpdf.YPos.NEXT], + ["Start Next C", "C", fpdf.XPos.START, fpdf.YPos.NEXT], + ["Start Next J", "J", fpdf.XPos.START, fpdf.YPos.NEXT], + ["End TMargin L", "L", fpdf.XPos.END, fpdf.YPos.TMARGIN], + ["End TMargin R", "R", fpdf.XPos.END, fpdf.YPos.TMARGIN], + ["End TMargin C", "C", fpdf.XPos.END, fpdf.YPos.TMARGIN], + ["End TMargin J", "J", fpdf.XPos.END, fpdf.YPos.TMARGIN], + ["WCont Top L", "L", fpdf.XPos.WCONT, fpdf.YPos.TOP], + ["WCont Top R", "R", fpdf.XPos.WCONT, fpdf.YPos.TOP], + ["WCont Top C", "C", fpdf.XPos.WCONT, fpdf.YPos.TOP], + ["WCont Top J", "J", fpdf.XPos.WCONT, fpdf.YPos.TOP], + ["Center TOP L", "L", fpdf.XPos.CENTER, fpdf.YPos.TOP], + ["Center TOP R", "R", fpdf.XPos.CENTER, fpdf.YPos.TOP], + ["Center TOP C", "C", fpdf.XPos.CENTER, fpdf.YPos.TOP], + ["Center TOP J", "J", fpdf.XPos.CENTER, fpdf.YPos.TOP], + ["LMargin BMargin L", "L", fpdf.XPos.LMARGIN, fpdf.YPos.BMARGIN], + ["LMargin BMargin R", "R", fpdf.XPos.LMARGIN, fpdf.YPos.BMARGIN], + ["LMargin BMargin C", "C", fpdf.XPos.LMARGIN, fpdf.YPos.BMARGIN], + ["LMargin BMargin J", "J", fpdf.XPos.LMARGIN, fpdf.YPos.BMARGIN], + ["RMargin Top L", "L", fpdf.XPos.RMARGIN, fpdf.YPos.TOP], + ["RMargin Top R", "R", fpdf.XPos.RMARGIN, fpdf.YPos.TOP], + ["RMargin Top C", "C", fpdf.XPos.RMARGIN, fpdf.YPos.TOP], + ["RMargin Top J", "J", fpdf.XPos.RMARGIN, fpdf.YPos.TOP], ) for i, item in enumerate(data): diff --git a/test/text/write_page_break.pdf b/test/text/write_page_break.pdf index 3cbcb37dde50000a0078fcfea9dff558ebad8653..903e7d59e101ec19c2127ea8c63a197bf83aca0f 100644 GIT binary patch delta 1332 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 z Date: Sat, 5 Mar 2022 14:08:29 +0100 Subject: [PATCH 66/67] newpos_[xy] to new[xy], annotations, docstring fixes --- fpdf/drawing.py | 4 +- fpdf/fpdf.py | 257 ++++++++++++++++---------------- fpdf/line_break.py | 94 ++++++------ test/text/test_render_styled.py | 8 +- 4 files changed, 184 insertions(+), 179 deletions(-) diff --git a/fpdf/drawing.py b/fpdf/drawing.py index 738df9ffb..b3776be5e 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -512,7 +512,7 @@ def mag(self): The scalar result of the distance computation. """ - return (self.x**2 + self.y**2) ** 0.5 + return (self.x ** 2 + self.y ** 2) ** 0.5 @force_document def __add__(self, other): @@ -2660,7 +2660,7 @@ def _approximate_arc(self, last_item): lam_da = (prime.x / radii.x) ** 2 + (prime.y / radii.y) ** 2 if lam_da > 1: - radii = Point(x=(lam_da**0.5) * radii.x, y=(lam_da**0.5) * radii.y) + radii = Point(x=(lam_da ** 0.5) * radii.x, y=(lam_da ** 0.5) * radii.y) sign = (self.large != self.sweep) - (self.large == self.sweep) rxry2 = (radii.x * radii.y) ** 2 diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index b20123728..fa4e02bde 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -418,7 +418,7 @@ def set_margin(self, margin): Sets the document right, left, top & bottom margins to the same value. Args: - margin (int): margin in the unit specified to FPDF constructor + margin (float): margin in the unit specified to FPDF constructor """ self.set_margins(margin, margin) self.set_auto_page_break(self.auto_page_break, margin) @@ -430,9 +430,9 @@ def set_margins(self, left, top, right=-1): Also sets the current FPDF.y on the page to this minimum vertical position. Args: - left (int): left margin in the unit specified to FPDF constructor - top (int): top margin in the unit specified to FPDF constructor - right (int): optional right margin in the unit specified to FPDF constructor + left (float): left margin in the unit specified to FPDF constructor + top (float): top margin in the unit specified to FPDF constructor + right (float): optional right margin in the unit specified to FPDF constructor """ self.set_left_margin(left) if self.y < top or self.y == self.t_margin: @@ -448,7 +448,7 @@ def set_left_margin(self, margin): Also sets the current FPDF.x on the page to this minimum horizontal position. Args: - margin (int): margin in the unit specified to FPDF constructor + margin (float): margin in the unit specified to FPDF constructor """ if self.x < margin or self.x == self.l_margin: self.x = margin @@ -459,7 +459,7 @@ def set_top_margin(self, margin): Sets the document top margin. Args: - margin (int): margin in the unit specified to FPDF constructor + margin (float): margin in the unit specified to FPDF constructor """ self.t_margin = margin @@ -468,7 +468,7 @@ def set_right_margin(self, margin): Sets the document right margin. Args: - margin (int): margin in the unit specified to FPDF constructor + margin (float): margin in the unit specified to FPDF constructor """ self.r_margin = margin @@ -479,7 +479,7 @@ def set_auto_page_break(self, auto, margin=0): Args: auto (bool): enable or disable this mode - margin (int): optional bottom margin (distance from the bottom of the page) + margin (float): optional bottom margin (distance from the bottom of the page) in the unit specified to FPDF constructor """ self.auto_page_break = auto @@ -931,7 +931,7 @@ def set_line_width(self, width): The method can be called before the first page is created and the value is retained from page to page. Args: - width (int): the width in user unit + width (float): the width in user unit """ self.line_width = width if self.page > 0: @@ -1075,10 +1075,10 @@ def line(self, x1, y1, x2, y2): Draw a line between two points. Args: - x1 (int): Abscissa of first point - y1 (int): Ordinate of first point - x2 (int): Abscissa of second point - y2 (int): Ordinate of second point + x1 (float): Abscissa of first point + y1 (float): Ordinate of first point + x2 (float): Abscissa of second point + y2 (float): Ordinate of second point """ self._out( f"{x1 * self.k:.2f} {(self.h - y1) * self.k:.2f} m {x2 * self.k:.2f} " @@ -1129,12 +1129,12 @@ def dashed_line(self, x1, y1, x2, y2, dash_length=1, space_length=1): - use set_dash_pattern() and the normal drawing operations instead Args: - x1 (int): Abscissa of first point - y1 (int): Ordinate of first point - x2 (int): Abscissa of second point - y2 (int): Ordinate of second point - dash_length (int): Length of the dash - space_length (int): Length of the space between 2 dashes + x1 (float): Abscissa of first point + y1 (float): Ordinate of first point + x2 (float): Abscissa of second point + y2 (float): Ordinate of second point + dash_length (float): Length of the dash + space_length (float): Length of the space between 2 dashes """ warnings.warn( "dashed_line() is deprecated, and will be removed in a future release. " @@ -1152,11 +1152,11 @@ def rect(self, x, y, w, h, style=None): It can be drawn (border only), filled (with no border) or both. Args: - x (int): Abscissa of upper-left bounging box. - y (int): Ordinate of upper-left bounging box. - w (int): Width. - h (int): Height. - style (int): Style of rendering. Possible values are: + x (float): Abscissa of upper-left bounging box. + y (float): Ordinate of upper-left bounging box. + w (float): Width. + h (float): Height. + style (str): Style of rendering. Possible values are: * `D` or empty string: draw border. This is the default value. * `F`: fill * `DF` or `FD`: draw and fill @@ -1174,11 +1174,11 @@ def ellipse(self, x, y, w, h, style=None): It can be drawn (border only), filled (with no border) or both. Args: - x (int): Abscissa of upper-left bounging box. - y (int): Ordinate of upper-left bounging box. - w (int): Width. - h (int): Height. - style (int): Style of rendering. Possible values are: + x (float): Abscissa of upper-left bounging box. + y (float): Ordinate of upper-left bounging box. + w (float): Width. + h (float): Height. + style (str): Style of rendering. Possible values are: * `D` or empty string: draw border. This is the default value. * `F`: fill * `DF` or `FD`: draw and fill @@ -1230,10 +1230,10 @@ def circle(self, x, y, r, style=None): It can be drawn (border only), filled (with no border) or both. Args: - x (int): Abscissa of upper-left bounging box. - y (int): Ordinate of upper-left bounging box. - r (int): Radius of the circle. - style (int): Style of rendering. Possible values are: + x (float): Abscissa of upper-left bounging box. + y (float): Ordinate of upper-left bounging box. + r (float): Radius of the circle. + style (str): Style of rendering. Possible values are: * `D` or None: draw border. This is the default value. * `F`: fill * `DF` or `FD`: draw and fill @@ -1248,12 +1248,12 @@ def regular_polygon(self, x, y, numSides, polyWidth, rotateDegrees=0, style=None Style can also be applied (fill, border...) Args: - x (int): Abscissa of upper-left bounding box. - y (int): Ordinate of upper-left bounding box. + x (float): Abscissa of upper-left bounding box. + y (float): Ordinate of upper-left bounding box. numSides (int): Number of sides for polygon. - polyWidth (int): width of the polygon. - rotateDegrees (int): degree amount to rotate polygon. (can be left blank) - style (int): Style of rendering. Possible values are: (can be left blank) + polyWidth (float): width of the polygon. + rotateDegrees (float): degree amount to rotate polygon. (can be left blank) + style (str): Style of rendering. Possible values are: (can be left blank) * `D` or None: draw border. This is the default value. * `F`: fill * `DF` or `FD`: draw and fill @@ -1295,15 +1295,15 @@ def arc( """ Outputs an arc. It can be drawn (border only), filled (with no border) or both. - a (int): Semi-major axis diameter. - b (int): Semi-minor axis diameter, if None, equals to a (default: None). - start_angle (int): Start angle of the arc (in degrees). - end_angle (int): End angle of the arc (in degrees). - inclination (int): Inclination of the arc in respect of the x-axis (default: 0). + a (float): Semi-major axis diameter. + b (float): Semi-minor axis diameter, if None, equals to a (default: None). + start_angle (float): Start angle of the arc (in degrees). + end_angle (float): End angle of the arc (in degrees). + inclination (float): Inclination of the arc in respect of the x-axis (default: 0). clockwise (bool): Way of drawing the arc (True: clockwise, False: counterclockwise) (default: False). start_from_center (bool): Start drawing from the center of the circle (default: False). end_at_center (bool): End drawing at the center of the circle (default: False). - style (int): Style of rendering. Possible values are: + style (str): Style of rendering. Possible values are: * `D` or None: draw border. This is the default value. * `F`: fill * `DF` or `FD`: draw and fill @@ -1434,15 +1434,15 @@ def solid_arc( It can be drawn (border only), filled (with no border) or both. Args: - x (int): Abscissa of upper-left bounging box. - y (int): Ordinate of upper-left bounging box. - a (int): Semi-major axis. - b (int): Semi-minor axis, if None, equals to a (default: None). - start_angle (int): Start angle of the arc (in degrees). - end_angle (int): End angle of the arc (in degrees). - inclination (int): Inclination of the arc in respect of the x-axis (default: 0). + x (float): Abscissa of upper-left bounging box. + y (float): Ordinate of upper-left bounging box. + a (float): Semi-major axis. + b (float): Semi-minor axis, if None, equals to a (default: None). + start_angle (float): Start angle of the arc (in degrees). + end_angle (float): End angle of the arc (in degrees). + inclination (float): Inclination of the arc in respect of the x-axis (default: 0). clockwise (bool): Way of drawing the arc (True: clockwise, False: counterclockwise) (default: False). - style (int): Style of rendering. Possible values are: + style (str): Style of rendering. Possible values are: * `D` or None: draw border. This is the default value. * `F`: fill * `DF` or `FD`: draw and fill @@ -1618,7 +1618,7 @@ def set_font(self, family=None, style="", size=0): style (str): empty string (by default) or a combination of one or several letters among B (bold), I (italic) and U (underline). Bold and italic styles do not apply to Symbol and ZapfDingbats fonts. - size (int): in points. The default value is the current size. + size (float): in points. The default value is the current size. """ if not family: family = self.font_family @@ -1692,7 +1692,7 @@ def set_font_size(self, size): Configure the font size in points Args: - size (int): font size in points + size (float): font size in points """ if self.font_size_pt == size: return @@ -1711,7 +1711,7 @@ def set_stretching(self, stretching): By default, no stretching is set (which is equivalent to a value of 100). Args: - stretching (int): horizontal stretching (scaling) in percents. + stretching (float): horizontal stretching (scaling) in percents. """ if self.font_stretching == stretching: return @@ -1737,13 +1737,13 @@ def set_link(self, link, y=0, x=0, page=-1, zoom="null"): Args: link (int): a link identifier returned by `add_link`. - y (int): optional ordinate of target position. + y (float): optional ordinate of target position. The default value is 0 (top of page). - x (int): optional abscissa of target position. + x (float): optional abscissa of target position. The default value is 0 (top of page). page (int): optional number of target page. -1 indicates the current page, which is the default value. - zoom (int): optional new zoom level after following the link. + zoom (float): optional new zoom level after following the link. Currently ignored by Sumatra PDF Reader, but observed by Adobe Acrobat reader. """ self.links[link] = DestinationXYZ( @@ -1784,10 +1784,10 @@ def text_annotation(self, x, y, text): Puts a text annotation on a rectangular area of the page. Args: - x (int): horizontal position (from the left) to the left side of the link rectangle - y (int): vertical position (from the top) to the bottom side of the link rectangle - w (int): width of the link rectangle - h (int): width of the link rectangle + x (float): horizontal position (from the left) to the left side of the link rectangle + y (float): vertical position (from the top) to the bottom side of the link rectangle + w (float): width of the link rectangle + h (float): width of the link rectangle text (str): text to display """ self.annots[self.page].append( @@ -1808,10 +1808,10 @@ def add_action(self, action, x, y, w, h): Args: action (fpdf.actions.Action): the action to add - x (int): horizontal position (from the left) to the left side of the link rectangle - y (int): vertical position (from the top) to the bottom side of the link rectangle - w (int): width of the link rectangle - h (int): width of the link rectangle + x (float): horizontal position (from the left) to the left side of the link rectangle + y (float): vertical position (from the top) to the bottom side of the link rectangle + w (float): width of the link rectangle + h (float): width of the link rectangle """ self.annots[self.page].append( Annotation( @@ -1832,8 +1832,8 @@ def text(self, x, y, txt=""): but it is usually easier to use the `cell()`, `multi_cell() or `write()` methods. Args: - x (int): abscissa of the origin - y (int): ordinate of the origin + x (float): abscissa of the origin + y (float): ordinate of the origin txt (str): string to print """ if not self.font_family: @@ -1994,9 +1994,9 @@ def cell( page break is performed before outputting. Args: - w (int): Cell width. Default value: None, meaning to fit text width. + w (float): Cell width. Default value: None, meaning to fit text width. If 0, the cell extends up to the right margin. - h (int): Cell height. Default value: None, meaning an height equal + h (float): Cell height. Default value: None, meaning an height equal to the current font size. txt (str): String to print. Default value: empty string. border: Indicates if borders must be drawn around the cell. @@ -2034,14 +2034,14 @@ def cell( "ignored" ) border = 1 - newpos_x = XPos.RIGHT - newpos_y = YPos.TOP + new_x = XPos.RIGHT + new_y = YPos.TOP if ln == 1: - newpos_x = XPos.LMARGIN - newpos_y = YPos.NEXT + new_x = XPos.LMARGIN + new_y = YPos.NEXT elif ln == 2: - newpos_x = XPos.LEFT - newpos_y = YPos.NEXT + new_x = XPos.LEFT + new_y = YPos.NEXT # Font styles preloading must be performed before any call to FPDF.get_string_width: txt = self.normalize_text(txt) styled_txt_frags = self._preload_font_styles(txt, markdown) @@ -2055,8 +2055,8 @@ def cell( w, h, border, - newpos_x=newpos_x, - newpos_y=newpos_y, + new_x=new_x, + new_y=new_y, align=align, fill=fill, link=link, @@ -2065,16 +2065,16 @@ def cell( def _render_styled_cell_text( self, - text_line, - w=None, - h=None, - border=0, - newpos_x=XPos.RIGHT, - newpos_y=YPos.TOP, - align="", - fill=False, - link="", - center=False, + text_line: TextLine, + w: float = None, + h: float = None, + border: Union[str, int] = 0, + new_x: XPos = XPos.RIGHT, + new_y: YPos = YPos.TOP, + align: str = "", + fill: bool = False, + link: str = "", + center: bool = False, ): """ Prints a cell (rectangular area) with optional borders, background color and @@ -2089,17 +2089,17 @@ def _render_styled_cell_text( Args: text_line (TextLine instance): Contains the (possibly empty) tuple of fragments to render. - w (int): Cell width. Default value: None, meaning to fit text width. + w (float): Cell width. Default value: None, meaning to fit text width. If 0, the cell extends up to the right margin. - h (int): Cell height. Default value: None, meaning an height equal + h (float): Cell height. Default value: None, meaning an height equal to the current font size. border: Indicates if borders must be drawn around the cell. The value can be either a number (`0`: no border ; `1`: frame) or a string containing some or all of the following characters (in any order): `L`: left ; `T`: top ; `R`: right ; `B`: bottom. Default value: 0. - newpos_x (Enum XPos): New current position in x after the call. - newpos_y (Enum YPos): New current position in y after the call. + new_x (Enum XPos): New current position in x after the call. + new_y (Enum YPos): New current position in y after the call. align (str): Allows to align the text inside the cell. Possible values are: `L` or empty string: left align (default value); `C`: center; `R`: right align; `J`: justify (if more than one word) @@ -2315,28 +2315,28 @@ def _render_styled_cell_text( self.lasth = h # XPos.LEFT -> self.x stays the same - if newpos_x == XPos.RIGHT: + if new_x == XPos.RIGHT: self.x += w - elif newpos_x == XPos.START: + elif new_x == XPos.START: self.x = s_start - elif newpos_x == XPos.END: + elif new_x == XPos.END: self.x = s_start + s_width - elif newpos_x == XPos.WCONT: + elif new_x == XPos.WCONT: self.x = s_start + s_width - self.c_margin - elif newpos_x == XPos.CENTER: + elif new_x == XPos.CENTER: self.x = (s_start + s_start + s_width) / 2.0 - elif newpos_x == XPos.LMARGIN: + elif new_x == XPos.LMARGIN: self.x = self.l_margin - elif newpos_x == XPos.RMARGIN: + elif new_x == XPos.RMARGIN: self.x = self.w - self.r_margin # YPos.TOP: -> self.y stays the same # YPos.LAST: -> self.y stays the same (single line) - if newpos_y == YPos.NEXT: + if new_y == YPos.NEXT: self.y += h - if newpos_y == YPos.TMARGIN: + if new_y == YPos.TMARGIN: self.y = self.t_margin - if newpos_y == YPos.BMARGIN: + if new_y == YPos.BMARGIN: self.y = self.h - self.b_margin return page_break_triggered @@ -2486,8 +2486,8 @@ def multi_cell( the background painted. Args: - w (int): cell width. If 0, they extend up to the right margin of the page. - h (int): cell height. Default value: None, meaning to use the current font size. + w (float): cell width. If 0, they extend up to the right margin of the page. + h (float): cell height. Default value: None, meaning to use the current font size. txt (str): strign to print. border: Indicates if borders must be drawn around the cell. The value can be either a number (`0`: no border ; `1`: frame) @@ -2507,7 +2507,7 @@ def multi_cell( Possible values are: `0`: to the bottom right ; `1`: to the beginning of the next line ; `2`: below with the same horizontal offset ; `3`: to the right with the same vertical offset. Default value: 0. - max_line_height (int): optional maximum height of each sub-cell generated + max_line_height (float): optional maximum height of each sub-cell generated markdown (bool): enable minimal markdown-like markup to render part of text as bold / italics / underlined. Default to False. print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable @@ -2524,14 +2524,14 @@ def multi_cell( "Parameter 'w' and 'h' must be numbers, not strings." " You can omit them by passing string content with txt=" ) - newpos_x = XPos.RIGHT - newpos_y = YPos.NEXT + new_x = XPos.RIGHT + new_y = YPos.NEXT if ln == 1: - newpos_x = XPos.LMARGIN + new_x = XPos.LMARGIN elif ln == 2: - newpos_x = XPos.LEFT + new_x = XPos.LEFT elif ln == 3: - newpos_y = YPos.TOP + new_y = YPos.TOP page_break_triggered = False if split_only: @@ -2601,20 +2601,20 @@ def multi_cell( "B" if "B" in border and is_last_line else "", ) ), - newpos_x=newpos_x if is_last_line else XPos.LEFT, - newpos_y=newpos_y if is_last_line else YPos.NEXT, + new_x=new_x if is_last_line else XPos.LEFT, + new_y=new_y if is_last_line else YPos.NEXT, align="L" if (align == "J" and is_last_line) else align, fill=fill, link=link, ) - if is_last_line and new_page and newpos_y == YPos.TOP: + if is_last_line and new_page and new_y == YPos.TOP: # When a page jump is performed and the requested y is TOP (ln=3), # pretend we started at the top of the text block on the new page. # cf. test_multi_cell_table_with_automatic_page_break prev_y = self.y page_break_triggered = page_break_triggered or new_page - if newpos_y == YPos.TOP: # We may have jumped a few lines -> reset + if new_y == YPos.TOP: # We may have jumped a few lines -> reset self.y = prev_y if split_only: @@ -2642,7 +2642,9 @@ def multi_cell( return page_break_triggered @check_page - def write(self, h=None, txt="", link="", print_sh=False): + def write( + self, h: float = None, txt: str = "", link: str = "", print_sh: bool = False + ): """ Prints text from the current position. When the right margin is reached, a line break occurs at the most recent @@ -2651,7 +2653,7 @@ def write(self, h=None, txt="", link="", print_sh=False): Upon method exit, the current position is left just at the end of the text. Args: - h (int): line height. Default value: None, meaning to use the current font size. + h (float): line height. Default value: None, meaning to use the current font size. txt (str): text content link (str): optional link to add on the text, internal (identifier returned by `add_link`) or external URL. @@ -2708,8 +2710,8 @@ def write(self, h=None, txt="", link="", print_sh=False): line_width, h=h, border=0, - newpos_x=XPos.WCONT, - newpos_y=YPos.TOP, + new_x=XPos.WCONT, + new_y=YPos.TOP, align="L", fill=False, link=link, @@ -2747,15 +2749,15 @@ def image( Args: name: either a string representing a file path to an image, an URL to an image, an io.BytesIO, or a instance of `PIL.Image.Image` - x (int): optional horizontal position where to put the image on the page. + x (float): optional horizontal position where to put the image on the page. If not specified or equal to None, the current abscissa is used. - y (int): optional vertical position where to put the image on the page. + y (float): optional vertical position where to put the image on the page. If not specified or equal to None, the current ordinate is used. After the call, the current ordinate is moved to the bottom of the image - w (int): optional width of the image. If not specified or equal to zero, + w (float): optional width of the image. If not specified or equal to zero, it is automatically calculated from the image size. Pass `pdf.epw` to scale horizontally to the full page width. - h (int): optional height of the image. If not specified or equal to zero, + h (float): optional height of the image. If not specified or equal to zero, it is automatically calculated from the image size. Pass `pdf.eph` to scale horizontally to the full page height. type (str): [**DEPRECATED**] unused, will be removed in a later version. @@ -2994,7 +2996,7 @@ def ln(self, h=None): the amount passed as parameter. Args: - h (int): The height of the break. + h (float): The height of the break. By default, the value equals the height of the last printed cell. """ self.x = self.l_margin @@ -3010,7 +3012,7 @@ def set_x(self, x): If the value provided is negative, it is relative to the right of the page. Args: - x (int): the new current abscissa + x (float): the new current abscissa """ self.x = x if x >= 0 else self.w + x @@ -3024,7 +3026,7 @@ def set_y(self, y): If the value provided is negative, it is relative to the bottom of the page. Args: - y (int): the new current ordinate + y (float): the new current ordinate """ self.x = self.l_margin self.y = y if y >= 0 else self.h + y @@ -3035,8 +3037,8 @@ def set_xy(self, x, y): If the values provided are negative, they are relative respectively to the right and bottom of the page. Args: - x (int): the new current abscissa - y (int): the new current ordinate + x (float): the new current abscissa + y (float): the new current ordinate """ self.set_y(y) self.set_x(x) @@ -4220,7 +4222,6 @@ def _is_xml(img: io.BytesIO): "FPDF", "XPos", "YPos", - "load_cache", "get_page_format", "TitleStyle", "PAGE_FORMATS", diff --git a/fpdf/line_break.py b/fpdf/line_break.py index 9b361776a..bcaadd3e3 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -1,4 +1,4 @@ -from collections import namedtuple +from typing import NamedTuple, Any, Union, Sequence SOFT_HYPHEN = "\u00ad" HYPHEN = "\u002d" @@ -7,23 +7,23 @@ class Fragment: - def __init__(self, style, underlined, characters=None): + def __init__(self, style: str, underlined: bool, characters: str = None): self.characters = [] if characters is None else characters self.style = style self.underline = underlined @classmethod - def from_string(cls, string, style, underlined): + def from_string(cls, string: str, style: str, underlined: bool): return cls(style, underlined, list(string)) - def trim(self, index): + def trim(self, index: int): self.characters = self.characters[:index] @property def string(self): return "".join(self.characters) - def __eq__(self, other): + def __eq__(self, other: Any): return ( self.characters == other.characters and self.style == other.style @@ -31,37 +31,37 @@ def __eq__(self, other): ) -TextLine = namedtuple( - "TextLine", - ("fragments", "text_width", "number_of_spaces_between_words", "justify"), -) - -SpaceHint = namedtuple( - "SpaceHint", - ( - "original_fragment_index", - "original_character_index", - "current_line_fragment_index", - "current_line_character_index", - "width", - "number_of_spaces", - ), -) - -HyphenHint = namedtuple( - "HyphenHint", - SpaceHint._fields - + ( - "character_to_append", - "character_to_append_width", - "character_to_append_style", - "character_to_append_underline", - ), -) +class TextLine(NamedTuple): + fragments: tuple + text_width: float + number_of_spaces_between_words: int + justify: bool + + +class SpaceHint(NamedTuple): + original_fragment_index: int + original_character_index: int + current_line_fragment_index: int + current_line_character_index: int + width: float + number_of_spaces: int + + +class HyphenHint(NamedTuple): + original_fragment_index: int + original_character_index: int + current_line_fragment_index: int + current_line_character_index: int + width: float + number_of_spaces: int + character_to_append: str + character_to_append_width: float + character_to_append_style: str + character_to_append_underline: bool class CurrentLine: - def __init__(self, print_sh=False): + def __init__(self, print_sh: bool = False): """ Per-line text fragment management for use by MultiLineBreak. Args: @@ -89,12 +89,12 @@ def __init__(self, print_sh=False): def add_character( self, - character, - character_width, - style, - underline, - original_fragment_index, - original_character_index, + character: str, + character_width: float, + style: str, + underline: bool, + original_fragment_index: int, + original_character_index: int, ): assert character != NEWLINE @@ -140,7 +140,7 @@ def add_character( self.width += character_width active_fragment.characters.append(character) - def _apply_automatic_hint(self, break_hint): + def _apply_automatic_hint(self, break_hint: Union[SpaceHint, HyphenHint]): """ This function mutates the current_line, applying one of the states observed in the past and stored in @@ -152,7 +152,7 @@ def _apply_automatic_hint(self, break_hint): self.number_of_spaces = break_hint.number_of_spaces self.width = break_hint.width - def manual_break(self, justify=False): + def manual_break(self, justify: bool = False): return TextLine( fragments=self.fragments, text_width=self.width, @@ -163,7 +163,7 @@ def manual_break(self, justify=False): def automatic_break_possible(self): return self.hyphen_break_hint is not None or self.space_break_hint is not None - def automatic_break(self, justify): + def automatic_break(self, justify: bool): assert self.automatic_break_possible() if self.hyphen_break_hint is not None and ( self.space_break_hint is None @@ -193,7 +193,11 @@ def automatic_break(self, justify): class MultiLineBreak: def __init__( - self, styled_text_fragments, size_by_style, justify=False, print_sh=False + self, + styled_text_fragments: Sequence, + size_by_style: Sequence, + justify: bool = False, + print_sh: bool = False, ): self.styled_text_fragments = styled_text_fragments self.size_by_style = size_by_style @@ -202,14 +206,14 @@ def __init__( self.fragment_index = 0 self.character_index = 0 - def _get_character_width(self, character, style=""): + def _get_character_width(self, character: str, style: str = ""): if character == SOFT_HYPHEN and not self.print_sh: # HYPHEN is inserted instead of SOFT_HYPHEN character = HYPHEN return self.size_by_style(character, style) # pylint: disable=too-many-return-statements - def get_line_of_given_width(self, maximum_width, wordsplit=True): + def get_line_of_given_width(self, maximum_width: float, wordsplit: bool = True): if self.fragment_index == len(self.styled_text_fragments): return None diff --git a/test/text/test_render_styled.py b/test/text/test_render_styled.py index 274d13d90..9b4e855ef 100644 --- a/test/text/test_render_styled.py +++ b/test/text/test_render_styled.py @@ -11,7 +11,7 @@ def test_render_styled_newpos(tmp_path): """ Verify that _render_styled_cell_text() places the new position in the right places in all possible combinations of alignment, - newpos_x, and newpos_y. + new_x, and new_y. """ doc = fpdf.FPDF() doc.set_font("helvetica", style="U", size=24) @@ -19,7 +19,7 @@ def test_render_styled_newpos(tmp_path): twidth = 100 data = ( - # txt, align, newpos_x, newpos_y + # txt, align, new_x, new_y ["Left Top L", "L", fpdf.XPos.LEFT, fpdf.YPos.TOP], ["Left Top R", "R", fpdf.XPos.LEFT, fpdf.YPos.TOP], ["Left Top C", "C", fpdf.XPos.LEFT, fpdf.YPos.TOP], @@ -77,8 +77,8 @@ def test_render_styled_newpos(tmp_path): twidth, border=1, align=align, # "L" if align == "J" else align, - newpos_x=newx, - newpos_y=newy, + new_x=newx, + new_y=newy, ) # mark the new position in the file with crosshairs for verification with doc.rotation(i * -15, doc.x, doc.y): From 3a72364d160bbeebc46c247b5cc9de88a876012f Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 6 Mar 2022 20:52:53 +0100 Subject: [PATCH 67/67] revert drawing.py, after black 22.1 made up its mind --- fpdf/drawing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fpdf/drawing.py b/fpdf/drawing.py index b3776be5e..738df9ffb 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -512,7 +512,7 @@ def mag(self): The scalar result of the distance computation. """ - return (self.x ** 2 + self.y ** 2) ** 0.5 + return (self.x**2 + self.y**2) ** 0.5 @force_document def __add__(self, other): @@ -2660,7 +2660,7 @@ def _approximate_arc(self, last_item): lam_da = (prime.x / radii.x) ** 2 + (prime.y / radii.y) ** 2 if lam_da > 1: - radii = Point(x=(lam_da ** 0.5) * radii.x, y=(lam_da ** 0.5) * radii.y) + radii = Point(x=(lam_da**0.5) * radii.x, y=(lam_da**0.5) * radii.y) sign = (self.large != self.sweep) - (self.large == self.sweep) rxry2 = (radii.x * radii.y) ** 2
  • +CQNA9(*r1%~ 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 diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 58b1f6301..e88fde22f 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -85,6 +85,14 @@ def test_flextemplate_multipage(tmp_path): "x2": 50, "y2": 0, }, + { + "name": "e", + "type": "E", + "x1": 0, + "y1": 50, + "x2": 50, + "y2": 0, + }, { "name": "label", "type": "T", @@ -200,6 +208,16 @@ def test_flextemplate_rotation(tmp_path): "y2": 10, "rotate": 45, }, + { + "name": "ellipse", + "type": "E", + "x1": 50, + "y1": -10, + "x2": 70, + "y2": 0, + "background":0x88FFFF, + "rotate": -45, + }, ] pdf = FPDF() pdf.add_page() From 753c2465a203d930038d8651f923219e70697214 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 8 Oct 2021 21:44:56 +0200 Subject: [PATCH 36/67] Bugfix skipping check for x2 with barcods --- fpdf/template.py | 4 ++-- test/template/flextemplate_multipage.pdf | Bin 2196 -> 2375 bytes test/template/mycsvfile.csv | 8 +++++--- test/template/template_multipage.pdf | Bin 2377 -> 3021 bytes test/template/template_nominal_csv.pdf | Bin 1462 -> 1636 bytes test/template/test_flextemplate.py | 1 - 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index 28b77fe38..ade4292d5 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -116,7 +116,7 @@ def load_elements(self, elements): raise KeyError(f"Mandatory key '{k}' missing in input data") # x2 is optional for barcode types, but needed for offset rendering if "x2" not in e: - if e["type"] in ["B", "C39"]: + if e["type"] in ["BC", "C39"]: e["x2"] = 0 else: raise KeyError("Mandatory key 'x2' missing in input data") @@ -210,7 +210,7 @@ def _varsep_float(s, default="0"): vs = val.strip() if not vs: if cfg[2]: # mandatory - if cfg[0] == "x2" and row["type"] in ["B", "C39"]: + if cfg[0] == "x2" and row[1] in ["BC", "C39"]: # two types don't need x2, but offset rendering does pass else: diff --git a/test/template/flextemplate_multipage.pdf b/test/template/flextemplate_multipage.pdf index 3e161ddd04aeba4616ed48b64089f7f59781d807..fd3baeef47b748b9d3ce1b299f857dd6bcd4962d 100644 GIT binary patch delta 888 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 delta 1311 zcmX>reo|;cUA?i9k)0h^aY<2XVlG$3oZd<2{SF)O9D85OCI2uW`Fa0iIY()a-3uJ= zG3?6BNM0oM=&R@KvRL!RvQD9&pWkn`JAPo1!WYK6u6B;{ejXhUmO|FgGqk3>^JiUb z?D>#&DbvyhsRZ_WT2-?5qATXUYFZm`cg5@bk=Khp&$OLY`pvxlK<1o-_rqW4vM8l6 zzPsg}wuNt&RM>~B=Ia%|b~$P*gogLMFWCHhU+a&F1{>ZuF6zo!$D&zksdC}lhMfwn z4SwrYIv07U<=W|cD4lrcHHkmN*L{`yIhTwh3$-{nZ`s%e&d3UF(b=N8J@#pVB-h`F z6hnC{){^p{m{s3f0MKiY70(LoWC_M+=H}TvyM%Rhnw>c9Pns_wTpo zTt8rxc!sA&y`1&V9q9xoC5>wa`jftSZQ$toR$XH@dqsMJT|f1RKCk6kizS^Zmd4#ZZS>M?6x)?`}q<)k#n&ZG-m z9`{{%G5b^KRcmi_#vSNqM|z<|IdF#>2JqJHn{5YFE}NS(_=PJ6F!S_>x_tf7cLBlBfGl~pUAIZQZK z$9kb??+wMu{+V(fLaq}%qSllF)9%eimFE{{O$*_hGx35)+bRW>jO4E_b!$QeH(e;% zdD82*xBQD!O5Je}S2DE;aHq#gF~9Uz`S~<;Dce1m& zqGEOY=_-RCY|8fg1UFA+w_%)Yz!^Sy5~nPq;p7dRevC$w|8wRs8c)vSk`XsCF;p-B z0fjsTE-=Hu(A?N^@I_YdG1Zxw7@(VHXklss z5)(jil%=upuged!L7&5#*Civ-wt0!?IDK(2@Hm zbH?`FT3OSUYB~L>p3}e0Gdg;&i|4&P&(qI8IeKtmN()eqLD1KIg5> zHGa-!2NkEV2aWrZ+&*rIJ}mra-ih-QcD@P_G2t!v&5^&$K7ZlgnW+!zgo2Ds$`?<# zSil#VvHO0E#?g*@c_Bg0R|7LY7cE@U^(H@rOYkVK*XzmW7F}5BZX6Ia<)Uq%%~ig1 zoxhUu)itSe9|#}2_;FYD-T(F0kM*mseD2ot|yFwCN!|My* zN$5_!skb0(&h8ewDMwu|c(R)Lgou5wRMEP&$9XH)n!mr5WH{#PM{~=a486f99DZZv z`HM=?scCN~tu0J@%Vn(?{!6y<*`FYrS7&ai&N+NJ{FiNaecX}#I(7ERHWHR4Qw}RE zFphZYlc4REvc;yursk)<{jRTDpIA-qx147wBK!0;r%d&^83%r!t+T%TpPzL}OW)=P zj8B=w&CHDy3_w63Pk{@}FfcSQHJ*Hj#WL93)B;`3z{1oLL(J0D99_)N&>U0D$kcRl S2CEB~v8e@@s;aBM8y5hLizSu- delta 495 zcmaFDvyFQ~UA?i1p`9I9aY<2XVlG$3oZd-i{SF)O9DiTSCI2uW`T60(eg!rmag!I^ z3NMx|ENxoJ@$uUd-`(qOJM4Dx%=!QCpV5a2g1;E<-}L1SFXz$mV0p@vmbxWi&;9hC zb1wY)8mt0@0Of@;p4gk z6O7iq>VNiJq0VZtS)A($r;fUCOJ9_zP+gNB(U-8`8M{{katte^-D`v@w5LBXg_ Date: Fri, 8 Oct 2021 22:11:15 +0200 Subject: [PATCH 37/67] More template tests --- test/template/template_textstyles.pdf | Bin 0 -> 1546 bytes test/template/test_template.py | 157 ++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 test/template/template_textstyles.pdf diff --git a/test/template/template_textstyles.pdf b/test/template/template_textstyles.pdf new file mode 100644 index 0000000000000000000000000000000000000000..96c722238866d4774f00ed3a7b151d21e91ce1b8 GIT binary patch literal 1546 zcmbtUYiJW$6t)@^N9t?0K0xbLn?=EP<|Z>~B5Acr8d6`WX^X2RE0g3lojRS-nMq0O z4=Fy7`l=`vZMAL{k*=Vu6tS{`t%6W!7exJGeIUxNvb(y94+PP3(?m=9r|A7LbMLw5 zanJXibIhe4Z!XTWGFHSPHB`emoospiW`fvKF-jQtYD6&*hT8zc28gc4v@p?;Wk}#w z6$9>d#M5(OAw!f1^+JzHZ(d0@h=y3NEE>c^!fFJZuOdp+s76-K3O2f-5wVtuw;exx zZA~vX)$`(GQ$bH7nS1{3+pLL`2b*7wYdllYd3N{I^1}A6&gP#on_lA9|M$MFe=_z& z{*L}$>2+z($3qzleWs_Av-=)S+jnY;Z=PxEpY3@a7a#rM4xb$CS-o-FyO$pxDC)1y zvvYpw&%V()VN%2Ff@4=}S}z^l6O7@E%d>)4_uuQ@)EX|Bm1${@bo`Y&`0Rrzn0aOX zuDhX}=Qj`FihFCu^!;tx8KKHAbbDo+)!`wy|};R&8CTsN7mjbnm6g#g7+CeHklhJ z)Jf!$pi`1YP^dHPN-3hFN=k4b$gplTMhRuu)g&T`E;WuSIk*xWLY~Er>^#o1JA%wt zDur~fUf`TzU7}9t4zWHW*AYVsi@7dUjxcOlEMz1qh}IFrDOP}UNr@tMoum{gx|9+O z3t}665PwLPw#1+`za>uosN;N_fBUHYC&2oyTxbgp(G6{?so^qWph_0{8L{Og4#bqy zLJPZ+e|Pd<0ws{(&FO>TE2z079-*Xg(b literal 0 HcmV?d00001 diff --git a/test/template/test_template.py b/test/template/test_template.py index 5fd093f53..b24e92c6d 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -177,6 +177,163 @@ def test_template_multipage(tmp_path): assert_pdf_equal(tmpl, HERE / "template_multipage.pdf", tmp_path) +def test_template_textstyles(tmp_path): + """Testing bold, italic, underline in template and in tags.""" + elements = [ + { + "name": "tb", + "type": "T", + "x1": 20, + "y1": 20, + "x2": 30, + "y2": 25, + "text": "text bold", + "bold":True, + }, + { + "name": "ti", + "type": "T", + "x1": 20, + "y1": 30, + "x2": 30, + "y2": 35, + "text": "text italic", + "italic":True, + }, + { + "name": "tu", + "type": "T", + "x1": 20, + "y1": 40, + "x2": 30, + "y2": 45, + "text": "text underline", + "underline":True, + }, + { + "name": "tbiu", + "type": "T", + "x1": 20, + "y1": 50, + "x2": 30, + "y2": 55, + "text": "text all", + "bold":True, + "italic":True, + "underline":True, + }, + { + "name": "wb", + "type": "W", + "x1": 20, + "y1": 60, + "x2": 30, + "y2": 65, + "text": "write bold", + "bold":True, + }, + { + "name": "wi", + "type": "W", + "x1": 20, + "y1": 70, + "x2": 30, + "y2": 75, + "text": "write italic", + "italic":True, + }, + { + "name": "wu", + "type": "W", + "x1": 20, + "y1": 80, + "x2": 30, + "y2": 85, + "text": "write underline", + "underline":True, + }, + { + "name": "wbiu", + "type": "W", + "x1": 20, + "y1": 90, + "x2": 30, + "y2": 95, + "text": "write all", + "bold":True, + "italic":True, + "underline":True, + }, + + { + "name": "tbt", + "type": "T", + "x1": 20, + "y1": 100, + "x2": 30, + "y2": 105, + "text": "text bold tags", + }, + + { + "name": "tit", + "type": "T", + "x1": 20, + "y1": 110, + "x2": 30, + "y2": 115, + "text": "text italic tags", + }, + + { + "name": "tut", + "type": "T", + "x1": 20, + "y1": 120, + "x2": 30, + "y2": 125, + "text": "text underline tags", + }, + + { + "name": "wbt", + "type": "W", + "x1": 20, + "y1": 130, + "x2": 30, + "y2": 135, + "text": "write bold tags", + }, + + { + "name": "wit", + "type": "W", + "x1": 20, + "y1": 140, + "x2": 30, + "y2": 145, + "text": "write italic tags", + }, + + { + "name": "wut", + "type": "W", + "x1": 20, + "y1": 150, + "x2": 30, + "y2": 155, + "text": "write underline tags", + }, + + ] + tmpl = Template(elements=elements) + tmpl.add_page() + assert_pdf_equal(tmpl, HERE / "template_textstyles.pdf", tmp_path) + + + + + # pylint: disable=unused-argument def test_template_item_access(tmp_path): """Testing Template() getitem/setitem.""" From d5de0e2ba3923428bb780de782a846453b63789c Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Fri, 8 Oct 2021 22:25:08 +0200 Subject: [PATCH 38/67] code cleanup --- fpdf/template.py | 19 ++++++++++++--- test/template/test_flextemplate.py | 4 +-- test/template/test_template.py | 39 ++++++++++++------------------ 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/fpdf/template.py b/fpdf/template.py index ade4292d5..e60f89f64 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -125,7 +125,12 @@ def load_elements(self, elements): e["size"] = e["w"] for k, t in key_config.items(): if k in e and not isinstance(e[k], t): - ttype = t.__name__ if isinstance(t, type) else ' or '.join([f"'{x.__name__}'" for x in t]) + # pylint: disable=no-member + ttype = ( + t.__name__ + if isinstance(t, type) + else " or ".join([f"'{x.__name__}'" for x in t]) + ) raise TypeError( f"Value of element item '{k}' must be {ttype}, not '{type(e[k]).__name__}'." ) @@ -227,7 +232,9 @@ def _varsep_float(s, default="0"): self.keys = [val["name"].lower() for val in self.elements] def __setitem__(self, name, value): - assert isinstance(name, str), f"name must be of type 'str', not '{type(name).__name__}'." + assert isinstance( + name, str + ), f"name must be of type 'str', not '{type(name).__name__}'." # value has too many valid types to reasonably check here if name.lower() not in self.keys: raise FPDFException(f"Element not loaded, cannot set item: {name}") @@ -237,11 +244,15 @@ def __setitem__(self, name, value): set = __setitem__ def __contains__(self, name): - assert isinstance(name, str), f"name must be of type 'str', not '{type(name).__name__}'." + assert isinstance( + name, str + ), f"name must be of type 'str', not '{type(name).__name__}'." return name.lower() in self.keys def __getitem__(self, name): - assert isinstance(name, str), f"name must be of type 'str', not '{type(name).__name__}'." + assert isinstance( + name, str + ), f"name must be of type 'str', not '{type(name).__name__}'." if name not in self.keys: raise KeyError(name) key = name.lower() diff --git a/test/template/test_flextemplate.py b/test/template/test_flextemplate.py index 0a5ffb299..791ab2fcf 100644 --- a/test/template/test_flextemplate.py +++ b/test/template/test_flextemplate.py @@ -164,7 +164,7 @@ def test_flextemplate_rotation(tmp_path): "y2": 15, "text": "Lorem ipsum dolor sit amet, consectetur adipisici elit", "rotate": 90.0, - "background": 0xEEFFFF, + "background": 0xEEFFFF, "multiline": True, }, { @@ -214,7 +214,7 @@ def test_flextemplate_rotation(tmp_path): "y1": -10, "x2": 70, "y2": 0, - "background":0x88FFFF, + "background": 0x88FFFF, "rotate": -45, }, ] diff --git a/test/template/test_template.py b/test/template/test_template.py index b24e92c6d..e52930b8e 100644 --- a/test/template/test_template.py +++ b/test/template/test_template.py @@ -188,7 +188,7 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 25, "text": "text bold", - "bold":True, + "bold": True, }, { "name": "ti", @@ -198,7 +198,7 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 35, "text": "text italic", - "italic":True, + "italic": True, }, { "name": "tu", @@ -208,7 +208,7 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 45, "text": "text underline", - "underline":True, + "underline": True, }, { "name": "tbiu", @@ -218,9 +218,9 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 55, "text": "text all", - "bold":True, - "italic":True, - "underline":True, + "bold": True, + "italic": True, + "underline": True, }, { "name": "wb", @@ -230,7 +230,7 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 65, "text": "write bold", - "bold":True, + "bold": True, }, { "name": "wi", @@ -240,7 +240,7 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 75, "text": "write italic", - "italic":True, + "italic": True, }, { "name": "wu", @@ -250,7 +250,7 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 85, "text": "write underline", - "underline":True, + "underline": True, }, { "name": "wbiu", @@ -260,11 +260,10 @@ def test_template_textstyles(tmp_path): "x2": 30, "y2": 95, "text": "write all", - "bold":True, - "italic":True, - "underline":True, + "bold": True, + "italic": True, + "underline": True, }, - { "name": "tbt", "type": "T", @@ -274,7 +273,6 @@ def test_template_textstyles(tmp_path): "y2": 105, "text": "text bold tags", }, - { "name": "tit", "type": "T", @@ -284,7 +282,6 @@ def test_template_textstyles(tmp_path): "y2": 115, "text": "text italic tags", }, - { "name": "tut", "type": "T", @@ -294,7 +291,6 @@ def test_template_textstyles(tmp_path): "y2": 125, "text": "text underline tags", }, - { "name": "wbt", "type": "W", @@ -304,7 +300,6 @@ def test_template_textstyles(tmp_path): "y2": 135, "text": "write bold tags", }, - { "name": "wit", "type": "W", @@ -314,7 +309,6 @@ def test_template_textstyles(tmp_path): "y2": 145, "text": "write italic tags", }, - { "name": "wut", "type": "W", @@ -324,16 +318,12 @@ def test_template_textstyles(tmp_path): "y2": 155, "text": "write underline tags", }, - ] tmpl = Template(elements=elements) tmpl.add_page() assert_pdf_equal(tmpl, HERE / "template_textstyles.pdf", tmp_path) - - - # pylint: disable=unused-argument def test_template_item_access(tmp_path): """Testing Template() getitem/setitem.""" @@ -353,6 +343,7 @@ def test_template_item_access(tmp_path): with raises(FPDFException): templ["notthere"] = "something" with raises(KeyError): + # pylint: disable=pointless-statement templ["notthere"] defaultval = templ["name"] # find in default data assert defaultval == "default text" @@ -361,10 +352,12 @@ def test_template_item_access(tmp_path): assert defaultval == "new text" # bad type item access with raises(AssertionError): + # pylint: disable=pointless-statement templ[7] with raises(AssertionError): templ[7] = 8 with raises(AssertionError): + # pylint: disable=pointless-statement 7 in templ @@ -434,7 +427,7 @@ def test_template_badinput(tmp_path): with raises(AttributeError): with warns(PendingDeprecationWarning): tmpl = Template() - tmpl.render(dest='whatever') + tmpl.render(dest="whatever") def test_template_code39(tmp_path): # issue-161 From 605acb1b79a41a607b3ae9d85dbdd038e391591d Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 9 Oct 2021 21:46:20 +0200 Subject: [PATCH 39/67] list template changes to log --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 150992b58..5a55f6c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/), and [PEP 440](https://www.python.org/dev/peps/pep-0440/). ## [2.4.6] - not released yet +### Added +- Templates now support drawing ellipses. +### Fixed +- The exception making the "x2" template field optional for barcode elements did not work correctly. +### Changed +- All template elements now have a transparent default background instead of white. ## [2.4.5] - 2021-10-03 ### Fixed From a2562126753473740cb865dc82daea2e79c07857 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sun, 10 Oct 2021 00:58:47 +0200 Subject: [PATCH 40/67] expose FlexTemplate through __init__.__all__ --- fpdf/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fpdf/__init__.py b/fpdf/__init__.py index 743a9b52a..001fffcf4 100644 --- a/fpdf/__init__.py +++ b/fpdf/__init__.py @@ -9,7 +9,7 @@ FPDF_VERSION as _FPDF_VERSION, ) from .html import HTMLMixin, HTML2FPDF -from .template import Template +from .template import Template, FlexTemplate from .deprecation import WarnOnDeprecatedModuleAttributes FPDF_VERSION = _FPDF_VERSION @@ -39,6 +39,7 @@ # Classes "FPDF", "Template", + "FlexTemplate", "TitleStyle", "HTMLMixin", "HTML2FPDF", From 2582afb892074c8fc10c503b879b931e14fd084b Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Mon, 11 Oct 2021 22:44:30 +0200 Subject: [PATCH 41/67] bugfix: Keep track of nested rotation contexts --- fpdf/fpdf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 608b38245..075cef211 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -279,7 +279,7 @@ def __init__( self.image_filter = "AUTO" self.page_duration = 0 # optional pages display duration, cf. add_page() self.page_transition = None # optional pages transition, cf. add_page() - self._rotating = False + self._rotating = 0 # counting levels of nested rotation contexts self._markdown_leak_end_style = False # Only set if XMP metadata is added to the document: self._xmp_metadata_obj_id = None @@ -1552,9 +1552,9 @@ def rotation(self, angle, x=None, y=None): f"q {c:.5F} {s:.5F} {-s:.5F} {c:.5F} {cx:.2F} {cy:.2F} cm " f"1 0 0 1 {-cx:.2F} {-cy:.2F} cm\n" ) - self._rotating = True + self._rotating += 1 yield - self._rotating = False + self._rotating -= 1 self._out("Q\n") @property From a6501f3902afb55591f680af107f683113bf158e Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 13 Oct 2021 20:38:57 +0200 Subject: [PATCH 42/67] test fix: text file loaded in binary mode --- test/image/test_load_image.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/image/test_load_image.py b/test/image/test_load_image.py index a6be5b3f7..954ccc710 100644 --- a/test/image/test_load_image.py +++ b/test/image/test_load_image.py @@ -24,6 +24,8 @@ def test_load_text_file(): bc = contents.encode() resource = fpdf.image_parsing.load_image(str(file)).getvalue() + # loaded a text file in binary mode, may contain DOS style line endings. + resource = resource.replace(b"\r\n", b"\n") assert bytes(resource) == bc From f26a8736181213fb1229b88ac36c64d12e33c408 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 14 Oct 2021 20:47:19 +0200 Subject: [PATCH 43/67] new set_dash_pattern(); dashed_line() retired --- CHANGELOG.md | 1 + docs/Shapes.md | 3 +- docs/Templates.md | 4 +- fpdf/fpdf.py | 61 ++++++++++++++++++++++++++----- test/shapes/class_dash.pdf | Bin 947 -> 949 bytes test/shapes/dash_pattern.pdf | Bin 0 -> 1407 bytes test/shapes/test_dash_pattern.py | 55 ++++++++++++++++++++++++++++ 7 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 test/shapes/dash_pattern.pdf create mode 100644 test/shapes/test_dash_pattern.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a55f6c7f..76f8094ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). ### Fixed - The exception making the "x2" template field optional for barcode elements did not work correctly. ### Changed +- `dashed_line()` is deprecated in favour of the new `set_dash_pattern()`, which works with all lines and curves. - All template elements now have a transparent default background instead of white. ## [2.4.5] - 2021-10-03 diff --git a/docs/Shapes.md b/docs/Shapes.md index 893127b25..c7ed4139b 100644 --- a/docs/Shapes.md +++ b/docs/Shapes.md @@ -24,7 +24,8 @@ pdf = FPDF() pdf.add_page() pdf.set_line_width(0.5) pdf.set_draw_color(r=0, g=128, b=255) -pdf.dashed_line(x1=50, y1=50, x2=150, y2=100, dash_length=2, space_length=3) +pdf.set_dash_pattern(dash=2, gap=3) +pdf.line(x1=50, y1=50, x2=150, y2=100) pdf.output("blue_dashed_line.pdf") ``` diff --git a/docs/Templates.md b/docs/Templates.md index 2ad8a286d..d2c5caf86 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -85,7 +85,9 @@ 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): +pdf.set_dash_pattern(dash=1, gap=1) +pdf.line(x1, y1, x2, y2): +pdf.set_dash_pattern() # third page pdf.add_page() diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 075cef211..cce6df17a 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -272,6 +272,7 @@ def __init__( self.draw_color = "0 G" self.fill_color = "0 g" self.text_color = "0 g" + self.dash_pattern = "[] 0 d" self.ws = 0 # word spacing self.angle = 0 # used by deprecated method: rotate() self.font_cache_dir = font_cache_dir @@ -279,7 +280,7 @@ def __init__( self.image_filter = "AUTO" self.page_duration = 0 # optional pages display duration, cf. add_page() self.page_transition = None # optional pages transition, cf. add_page() - self._rotating = 0 # counting levels of nested rotation contexts + self._rotating = 0 # counting levels of nested rotation contexts self._markdown_leak_end_style = False # Only set if XMP metadata is added to the document: self._xmp_metadata_obj_id = None @@ -882,6 +883,47 @@ def set_line_width(self, width): if self.page > 0: self._out(f"{width * self.k:.2f} w") + def set_dash_pattern(self, dash=0, gap=0, phase=0): + """ + Set the current dash pattern for lines and curves. + + Args: + dash (float >= 0): + The length of the dashes in current units. + + gap (float >= 0): + The length of the gaps between dashes in current units. + If omitted, the dash length will be used. + + phase (float >= 0): + Where in the sequence to start drawing. + + Omitting 'dash' (= 0) resets the pattern to a solid line. + """ + assert ( + isinstance(dash, (int, float)) and dash >= 0 + ), "Dash length must not be a negative number." + assert ( + isinstance(gap, (int, float)) and gap >= 0 + ), "gap length must not be a negative number." + assert ( + isinstance(phase, (int, float)) and phase >= 0 + ), "Phase must not be a negative number." + if self._rotating: + raise FPDFException( + ".set_line_width() should not be called inside .rotation()" + ) + if dash: + if gap: + dstr = f"[{dash * self.k:.3f} {gap * self.k:.3f}] {phase *self.k:.3f} d" + else: + dstr = f"[{dash * self.k:.3f}] {phase *self.k:.3f} d" + else: + dstr = "[] 0 d" + if dstr != self.dash_pattern: + self.dash_pattern = dstr + self._out(dstr) + @check_page def line(self, x1, y1, x2, y2): """ @@ -934,16 +976,12 @@ def polygon(self, point_list, fill=False): """ self.polyline(point_list, fill=fill, polygon=True) - def _set_dash(self, dash_length=None, space_length=None): - dash = "" - if dash_length and space_length: - dash = f"{dash_length * self.k:.3f} {space_length * self.k:.3f}" - self._out(f"[{dash}] 0 d") - @check_page def dashed_line(self, x1, y1, x2, y2, dash_length=1, space_length=1): """ Draw a dashed line between two points. + **DEPRECATED** 2.4.6 + - use set_dash_pattern() and the normal drawing operations instead Args: x1 (int): Abscissa of first point @@ -953,9 +991,14 @@ def dashed_line(self, x1, y1, x2, y2, dash_length=1, space_length=1): dash_length (int): Length of the dash space_length (int): Length of the space between 2 dashes """ - self._set_dash(dash_length, space_length) + warnings.warn( + "dashed_line() is deprecated, and will be removed in a future release. " + "Use set_dash_pattern() and the normal drawing operations instead.", + PendingDeprecationWarning, + ) + self.set_dash_pattern(dash_length, space_length) self.line(x1, y1, x2, y2) - self._set_dash() + self.set_dash_pattern() @check_page def rect(self, x, y, w, h, style=None): diff --git a/test/shapes/class_dash.pdf b/test/shapes/class_dash.pdf index 1d4f5c6aa86e0dda31ad6ffaa17214720568d6aa..191569649a7bbbe754319f6a15c8ce2e82f3c5b1 100644 GIT binary patch delta 328 zcmdnYzLkAKeZ7f+ogG(kNl|KIE?32z)`^CBM-4dIzRzhARxn67!8z~B*5oT|ldZr1 zI@%jQX?n!V7M6eZA7?V<3oo?%GD&(xcdvt774xe!=@qkk9dtib+gH<3c1BTi1p^RJ$W!0~GYkw&EQ}|MGh1VkF*7!toX+gR NVZx=V>gw;t1pwKphs*!~ delta 328 zcmdnWzL|YOeZ8@TogG(kNl|KIE?32z+KGlmM+`VztLHZf|6ycgny+A-x&6zM+eRyk z|DM>GRK59A*eu6i{x%w4>b7}m+mWmWl}6Y^d(`1;SxTHJ}GgYwVcnJA0E;$OW0Cmv3GBM!OCSB;=8-ulRqdQ z&fS+#GE??as@nvQ-3=wPWiREr38dvs*tgvNTG^bXIwDr`LC09*&aFtzexP_e^;^O8 zO1}TM-`(0fcYk;uOC+a~$HEn_XPw*s-|d9 zLcXmLb(c)3XSgOYwT~+`FJ*H*V=JSmse%CrDC8+{ff)t{CKiU1C7G?U$e0;fPR?X@ O;V|M-Rdw}u;{pJ@i4DM>9-(09v8EJ<}qP0mjN8t#*t zmtK;gU}|m%)Kgqil$w~!RWax76zhCZN1oQ-KSlKx9ys&+q8k@$?l$L@W{w=OQd0ly zHX9$m$NuD-hsn)~_cHR;`?lKFnoSE{82>DBVgGy?%hT5`eVQsKKH2EG+fub})9NN< zPfcDR!+hMwylR(>!Mc*drH{1Lz0EJ*zwL1}s_gHcoiBK8C%){8a=B>ysUu*Fl zl@m95vu>%}&p6?COwUTM(eA;|&s)RU)<05e-*7!H=)2yNqGw5=_P&B^mwj6t=Vak4 z+cdMpRdlCHS?@Bgv!3$CQM^1)!()z!%=3=@8nrq9&FzKOWvM3avY}aXs@Se?lbSAG zwSC{cy!V+u_Z9x$bT@6u$L}-E7EFulS${aD(9Qmz<%2zQ3>NlghtE4`xh{{r>ZHMm zA7&RcFHUfa_6jMA>zuduf15~NSIDDIwJjeT?pk2aWq>?RcKX*BfHlZjk_G- zJCvsVo^h&p@9Vn#&>vLswhMK+lp8LPOIQF_W=xM{5&`pb3 zW?qS4`u@af%S|<0UAhhkq zbdb*kHQhSZ`7|yrE>Tu^*cG8Q2k5hQ-|FcqW^bi6>X})T&v=589W)g|6CfzTVkGBc zE`9IJlwyTwP+|{?<2_Tsd%2g0o19M(cesXYXi9)nKFpDbahon{j&G*a&=1M0h?F^+ofj$9SZ({>w z7%G5V83Zy_Kf*sLD>b=90Vo0FBK%>7WREj2^Oa=g=eYp0r-Fuym7%4XrJ<3rp`oP# z5NIOlGDp${DI=T{OA>SP(?P{VIb0%JR~Di!Et{h<8( n5(Q8|0ZU8IytI4;P*8&-qqroos08dcLql^zE>%@me>W}wK Date: Thu, 14 Oct 2021 21:06:30 +0200 Subject: [PATCH 44/67] test update for deprecated dashed_line() --- test/shapes/test_line.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/shapes/test_line.py b/test/shapes/test_line.py index d503a4172..bbb3456f2 100644 --- a/test/shapes/test_line.py +++ b/test/shapes/test_line.py @@ -1,4 +1,5 @@ from pathlib import Path +from pytest import warns import fpdf from test.conftest import assert_pdf_equal @@ -42,7 +43,8 @@ def test_dash(tmp_path): pdf.add_page() def draw_diagonal_dash(pdf, x, y, *a, **k): - pdf.dashed_line(x, y, x + size, y + size / 2, *a, **k) + with warns(PendingDeprecationWarning): + pdf.dashed_line(x, y, x + size, y + size / 2, *a, **k) for width in [0.71, 1, 2]: pdf.set_line_width(width) @@ -65,13 +67,14 @@ def draw_diagonal_dash(pdf, x, y, *a, **k): next_row(pdf) pdf.set_line_width(1) - x, y = pdf.get_x(), pdf.get_y() - pdf.dashed_line(x, y, x + 100, y + 80, 10, 3) - pdf.set_x(pdf.get_x() + 20) - x, y = pdf.get_x(), pdf.get_y() - pdf.dashed_line(x, y, x + 100, y + 80, 3, 20) - pdf.set_x(pdf.get_x() + 20) - x, y = pdf.get_x(), pdf.get_y() - pdf.dashed_line(x, y, x + 100, y + 80, 6, 17) + with warns(PendingDeprecationWarning): + x, y = pdf.get_x(), pdf.get_y() + pdf.dashed_line(x, y, x + 100, y + 80, 10, 3) + pdf.set_x(pdf.get_x() + 20) + x, y = pdf.get_x(), pdf.get_y() + pdf.dashed_line(x, y, x + 100, y + 80, 3, 20) + pdf.set_x(pdf.get_x() + 20) + x, y = pdf.get_x(), pdf.get_y() + pdf.dashed_line(x, y, x + 100, y + 80, 6, 17) assert_pdf_equal(pdf, HERE / "class_dash.pdf", tmp_path) From 007e00b3dc1f08578ca381317f64dd651e71b117 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 14 Oct 2021 21:19:55 +0200 Subject: [PATCH 45/67] mask unused argument in test --- test/shapes/test_dash_pattern.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/shapes/test_dash_pattern.py b/test/shapes/test_dash_pattern.py index 809d3a3fc..4acc37e9d 100644 --- a/test/shapes/test_dash_pattern.py +++ b/test/shapes/test_dash_pattern.py @@ -38,6 +38,7 @@ def draw_stuff(x, y): assert_pdf_equal(pdf, HERE / "dash_pattern.pdf", tmp_path) +# pylint: disable=unused-argument def test_dash_pattern_badinput(tmp_path): pdf = fpdf.FPDF() pdf.add_page() From a96bcc3d7d287f5921cde4d38d6ea3b8c44c5c39 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 14 Oct 2021 21:44:53 +0200 Subject: [PATCH 46/67] error message fix, expand test coverage --- fpdf/fpdf.py | 2 +- test/shapes/dash_pattern.pdf | Bin 1407 -> 1404 bytes test/shapes/test_dash_pattern.py | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index cce6df17a..f181d51ef 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -911,7 +911,7 @@ def set_dash_pattern(self, dash=0, gap=0, phase=0): ), "Phase must not be a negative number." if self._rotating: raise FPDFException( - ".set_line_width() should not be called inside .rotation()" + ".set_dash_pattern() should not be called inside .rotation()" ) if dash: if gap: diff --git a/test/shapes/dash_pattern.pdf b/test/shapes/dash_pattern.pdf index 73bc57338285ed718b51620832335791d880f17e..a8e97162f03b377bb5620eb800eca44e8ef45179 100644 GIT binary patch delta 678 zcmey*^@nRheZ855ogG(kNl|KIE?32zw^Mfai8%7Oe*VSPE|Bo*uSa+k)1|jOxmR{` zEL2o{_h4Of3jJf_cl+I*XD5AnTs41LM_BOh zKz+qI%lHoZ>{`?nBedHsxb@kRe#y0LZJ+e@lDN(>${xi+b3Q6(e}ly^!gCK z6E3ROUK1pHRSW#r8EgGI+~c&+Sn6=aTH{%VDq_+_j+LjGT$|{h?O%6>%gyb}Ws!+z zG@LAa@A6CtNi#WCShwoJoG*sTa|3^hAN75>^GdDT;;(c5+?C`#n}5l^vgl2E_1UxA z(_fv5{n7vV{?VK2MfbOy;-CHC!22SnZHt(Wcq|KTn$C0FcQwbx*&+*{>`}6*zJGJe zDFMTXgy)X@;i(<9oj|_{O-!0n@u)$_*2|npXkEwBLqNY(-`G;_nRIzy<0tctFHdW2 zKF0K!QPfny00b2B6u7_)0|QF~^U3E}tTheMWeg1rv51+OPBvn7;WV@0QdM>JcjE#8 DlG-{* delta 681 zcmeyv^`C1(eZ9G%ogG(kNl|KIE?32zw^OY1MICusfBzKKTX^8i?~86+thw8qSDHC; z#7as1v)gQZ{2u$0ZyqK$E8fehRG6w5P3YR|8R`)i)eE+t`(WtV&dv?C4=e3>qvMb8vqVbPI zn!$dp#dlOr-001^rE)*xgx@heE4@a$2R}b=4QE^bNU43p^|+w#dQXa;C577i3a(xD zZE>8Fg|BSW%o118ohoI$%ec;Z${R=V@;nWXIU+L8JN9eT=KMFe7h0F4nz+k`X3eQ$ zyS`0ox_H(0efRR-Xa3w*_`817-Lxeizt1#VFfFcU{o$BGH~W8<5BAJ4SlF8#KJTFA zx;*x(lLjY#m|f7kIKeI2E2Jo{bKc(nZ6bMHA&)xMwtQ@mTi0c>qws3S(QGYMp;aA@ z>|S3q?sA0hP@498#;M}Huj}@gAN)1T@`m}zGA6~6+?N5b&z02~YWC`T?*I1USpDnT zpr;LILN_gDnRz9G>H8C_m5c0sv_z~{8unbbTB+G{yfi4WCOs%w_UD#wPmUi+C~yIK z{p1p*mdjVpGI*v2CCfZt4)*pX&#Cc&cYwaWt8N-~{^b3+S1$Zf`#V|vc-^+^cWSDa zox9We-{ODloLZ&tzN`Oi;N<(Gr=Ftit>ustrCM>a;LesVf!&^hUgxwAZT>#jcQub- zGi#aLmx9o?8`D946V!C;ROi#UxVS`F;bB*V)*PVU+I_31tC+o&)~IJ@Q9k3j`7qOG zMo|j|0}xQiQ{VzK3=Av{4JMyvvDP#}moYRj#3E*9F Date: Fri, 15 Oct 2021 17:46:45 +0200 Subject: [PATCH 47/67] updates based on PR review --- fpdf/fpdf.py | 15 ++++++--------- test/shapes/test_dash_pattern.py | 14 ++++++++------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index f181d51ef..f23f311fd 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -900,15 +900,12 @@ def set_dash_pattern(self, dash=0, gap=0, phase=0): Omitting 'dash' (= 0) resets the pattern to a solid line. """ - assert ( - isinstance(dash, (int, float)) and dash >= 0 - ), "Dash length must not be a negative number." - assert ( - isinstance(gap, (int, float)) and gap >= 0 - ), "gap length must not be a negative number." - assert ( - isinstance(phase, (int, float)) and phase >= 0 - ), "Phase must not be a negative number." + if not (isinstance(dash, (int, float)) and dash >= 0): + raise ValueError("Dash length must be zero or a positive number.") + if not (isinstance(gap, (int, float)) and gap >= 0): + raise ValueError("gap length must be zero or a positive number.") + if not (isinstance(phase, (int, float)) and phase >= 0): + raise ValueError("Phase must be zero or a positive number.") if self._rotating: raise FPDFException( ".set_dash_pattern() should not be called inside .rotation()" diff --git a/test/shapes/test_dash_pattern.py b/test/shapes/test_dash_pattern.py index 37be70f1a..a4815c99c 100644 --- a/test/shapes/test_dash_pattern.py +++ b/test/shapes/test_dash_pattern.py @@ -30,6 +30,8 @@ def draw_stuff(x, y): pdf.set_dash_pattern(4, 6) draw_stuff(20, 100) pdf.set_dash_pattern(0.5, 9.5, 3.25) + # coverage: repeating the same pattern should not add it again + pdf.set_dash_pattern(0.5, 9.5, 3.25) draw_stuff(20, 100) # reset to solid pdf.set_dash_pattern() @@ -42,17 +44,17 @@ def draw_stuff(x, y): def test_dash_pattern_badinput(tmp_path): pdf = fpdf.FPDF() pdf.add_page() - with raises(AssertionError): + with raises(ValueError): pdf.set_dash_pattern(dash=-1) - with raises(AssertionError): + with raises(ValueError): pdf.set_dash_pattern(gap=-1) - with raises(AssertionError): + with raises(ValueError): pdf.set_dash_pattern(phase=-1) - with raises(AssertionError): + with raises(ValueError): pdf.set_dash_pattern(dash="yo") - with raises(AssertionError): + with raises(ValueError): pdf.set_dash_pattern(gap="hu") - with raises(AssertionError): + with raises(ValueError): pdf.set_dash_pattern(phase=None) with raises(fpdf.FPDFException): with pdf.rotation(30, 50, 50): From f5573ef4679c4da2180291cfaa70c851bdf6e0a1 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Mon, 18 Oct 2021 19:24:45 +0200 Subject: [PATCH 48/67] Graphics state stack implemented, rotation fixed --- CHANGELOG.md | 2 + fpdf/fpdf.py | 94 +++++++++++++------------ fpdf/graphics_state.py | 113 +++++++++++++++++++++++++++++++ test/graphics_context.pdf | Bin 0 -> 1250 bytes test/shapes/test_dash_pattern.py | 3 - test/test_graphics_context.py | 29 ++++++++ test/test_rotation.py | 26 +------ 7 files changed, 195 insertions(+), 72 deletions(-) create mode 100644 fpdf/graphics_state.py create mode 100644 test/graphics_context.pdf create mode 100644 test/test_graphics_context.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf368fc4..eb9d2c0db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). ## [2.4.6] - not released yet ### Added +- Temporary changes to graphics state variables are now possible by `with FPDF.local_context():`, thanks to @gmischler - New `set_dash_pattern()`, which works with all lines and curves, thanks to @gmischler. - Templates now support drawing ellipses, thanks to @gmischler - New sections have been added to [the tutorial](https://pyfpdf.github.io/fpdf2/Tutorial.html), thanks to @portfedh: @@ -16,6 +17,7 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/). 5. [Creating Tables](https://pyfpdf.github.io/fpdf2/Tutorial.html#tuto-5-creating-tables) 6. [Creating links and mixing text styles](https://pyfpdf.github.io/fpdf2/Tutorial.html#tuto-6-creating-links-and-mixing-text-styles) ### Fixed +- All graphics state manipulations are now possible within a rotation context, thanks to @gmischler - The exception making the "x2" template field optional for barcode elements did not work correctly, thanks to @gmischler ### Changed - All template elements now have a transparent default background instead of white, thanks to @gmischler diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index f23f311fd..c6840276f 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -44,6 +44,7 @@ from .recorder import FPDFRecorder from .structure_tree import MarkedContent, StructureTreeBuilder from .ttfonts import TTFontFile +from .graphics_state import GraphicsStateMixin from .util import ( enclose_in_parens, escape_parens, @@ -221,7 +222,7 @@ def wrapper(self, *args, **kwargs): return wrapper -class FPDF: +class FPDF(GraphicsStateMixin): "PDF Generation class" MARKDOWN_BOLD_MARKER = "**" MARKDOWN_ITALICS_MARKER = "__" @@ -246,6 +247,7 @@ def __init__( `None` disables font chaching. The default is `True`, meaning the current folder. """ + GraphicsStateMixin.__init__(self) # Initialization of properties self.offsets = {} # array of object offsets self.page = 0 # current page number @@ -263,16 +265,19 @@ def __init__( self.in_footer = 0 # flag set when processing footer self.lasth = 0 # height of last cell printed self.current_font = {} # current font + self.str_alias_nb_pages = "{nb}" + # graphics state variables from the stack self.font_family = "" # current font family self.font_style = "" # current font style self.font_size_pt = 12 # current font size in points self.font_stretching = 100 # current font stretching - self.str_alias_nb_pages = "{nb}" self.underline = 0 # underlining flag self.draw_color = "0 G" self.fill_color = "0 g" self.text_color = "0 g" self.dash_pattern = "[] 0 d" + # font_size is initialized below after the standard fonts have been set up + # end of grapics state variables self.ws = 0 # word spacing self.angle = 0 # used by deprecated method: rotate() self.font_cache_dir = font_cache_dir @@ -280,7 +285,6 @@ def __init__( self.image_filter = "AUTO" self.page_duration = 0 # optional pages display duration, cf. add_page() self.page_transition = None # optional pages transition, cf. add_page() - self._rotating = 0 # counting levels of nested rotation contexts self._markdown_leak_end_style = False # Only set if XMP metadata is added to the document: self._xmp_metadata_obj_id = None @@ -323,6 +327,7 @@ def __init__( self.dw_pt, self.dh_pt = get_page_format(format, self.k) self._set_orientation(orientation, self.dw_pt, self.dh_pt) self.def_orientation = self.cur_orientation + # another one from the graphics state stack self.font_size = self.font_size_pt / self.k # Page spacing @@ -680,8 +685,6 @@ def add_page( raise FPDFException( "A page cannot be added on a closed document, after calling output()" ) - if self._rotating: - raise FPDFException(".add_page() should not be called inside .rotation()") if self.state == DocumentState.UNINITIALIZED: self.open() family = self.font_family @@ -783,10 +786,6 @@ def set_draw_color(self, r, g=-1, b=-1): g (int): green component (between 0 and 255) b (int): blue component (between 0 and 255) """ - if self._rotating: - raise FPDFException( - ".set_draw_color() should not be called inside .rotation()" - ) if (r == 0 and g == 0 and b == 0) or g == -1: self.draw_color = f"{r / 255:.3f} G" else: @@ -806,10 +805,6 @@ def set_fill_color(self, r, g=-1, b=-1): g (int): green component (between 0 and 255) b (int): blue component (between 0 and 255) """ - if self._rotating: - raise FPDFException( - ".set_fill_color() should not be called inside .rotation()" - ) if (r == 0 and g == 0 and b == 0) or g == -1: self.fill_color = f"{r / 255:.3f} g" else: @@ -829,10 +824,6 @@ def set_text_color(self, r, g=-1, b=-1): g (int): green component (between 0 and 255) b (int): blue component (between 0 and 255) """ - if self._rotating: - raise FPDFException( - ".set_text_color() should not be called inside .rotation()" - ) if (r == 0 and g == 0 and b == 0) or g == -1: self.text_color = f"{r / 255:.3f} g" else: @@ -875,10 +866,6 @@ def set_line_width(self, width): Args: width (int): the width in user unit """ - if self._rotating: - raise FPDFException( - ".set_line_width() should not be called inside .rotation()" - ) self.line_width = width if self.page > 0: self._out(f"{width * self.k:.2f} w") @@ -906,10 +893,6 @@ def set_dash_pattern(self, dash=0, gap=0, phase=0): raise ValueError("gap length must be zero or a positive number.") if not (isinstance(phase, (int, float)) and phase >= 0): raise ValueError("Phase must be zero or a positive number.") - if self._rotating: - raise FPDFException( - ".set_dash_pattern() should not be called inside .rotation()" - ) if dash: if gap: dstr = f"[{dash * self.k:.3f} {gap * self.k:.3f}] {phase *self.k:.3f} d" @@ -1290,13 +1273,11 @@ def set_font(self, family=None, style="", size=0): raise ValueError( f"Unknown style provided (only B/I/U letters are allowed): {style}" ) - if self._rotating: - raise FPDFException(".set_font() should not be called inside .rotation()") if "U" in style: - self.underline = 1 + self.underline = True style = style.replace("U", "") else: - self.underline = 0 + self.underline = False if family in self.font_aliases and family + style not in self.fonts: warnings.warn( @@ -1357,10 +1338,6 @@ def set_font_size(self, size): Args: size (int): font size in points """ - if self._rotating: - raise FPDFException( - ".set_font_size() should not be called inside .rotation()" - ) if self.font_size_pt == size: return self.font_size_pt = size @@ -1380,10 +1357,6 @@ def set_stretching(self, stretching): Args: stretching (int): horizontal stretching (scaling) in percents. """ - if self._rotating: - raise FPDFException( - ".set_stretching() should not be called inside .rotation()" - ) if self.font_stretching == stretching: return self.font_stretching = stretching @@ -1562,13 +1535,14 @@ def rotate(self, angle, x=None, y=None): @contextmanager def rotation(self, angle, x=None, y=None): """ - This method allows to perform a rotation around a given center. It must be used as a context-manager using `with`: + This method allows to perform a rotation around a given center. + It must be used as a context-manager using `with`: with rotation(angle=90, x=x, y=y): pdf.something() - The rotation affects all elements which are printed inside the indented context - (with the exception of clickable areas). + The rotation affects all elements which are printed inside the indented + context (with the exception of clickable areas). Args: angle (float): angle in degrees @@ -1578,8 +1552,11 @@ def rotation(self, angle, x=None, y=None): Notes ----- - Only the rendering is altered. The `get_x()` and `get_y()` methods are not - affected, nor the automatic page break mechanism. + Only the rendering is altered. The `get_x()` and `get_y()` methods are + not affected, nor the automatic page break mechanism. + The rotation also establishes a local graphics state, so that any + graphics state settings changed within will not affect the operations + invoked after it has finished. """ if x is None: x = self.x @@ -1592,11 +1569,40 @@ def rotation(self, angle, x=None, y=None): f"q {c:.5F} {s:.5F} {-s:.5F} {c:.5F} {cx:.2F} {cy:.2F} cm " f"1 0 0 1 {-cx:.2F} {-cy:.2F} cm\n" ) - self._rotating += 1 + self._push_local_stack() yield - self._rotating -= 1 + self._pop_local_stack() self._out("Q\n") + @check_page + @contextmanager + def local_context(self): + """ + Create a local grapics state, which won't affect the surrounding code. + This method must be used as a context manager using `with`: + + with local_context(): + set_some_state() + draw_some_stuff() + + The affected settings are: + draw_color + fill_color + text_color + underline + font_style + font_stretching + font_family + font_size_pt + font_size + dash_pattern + """ + self._push_local_stack() + self._out("\nq ") + yield + self._out(" Q\n") + self._pop_local_stack() + @property def accept_page_break(self): """ diff --git a/fpdf/graphics_state.py b/fpdf/graphics_state.py new file mode 100644 index 000000000..c3e9dd4df --- /dev/null +++ b/fpdf/graphics_state.py @@ -0,0 +1,113 @@ +class GraphicsStateMixin: + """Mixin class for managing a stack of graphics state variables. + + To the subclassing library and its users, the variables look like + normal instance attributes. But by the magic of properties, we can + push and pop levels as needed, and users will always see and modify + just the current version. + + This class is mixed in by fpdf.FPDF(), and is not meant to be used + directly by user code. + """ + + def __init__(self): + self.__statestack = [ + dict( + draw_color="0 G", + fill_color="0 g", + text_color="0 g", + underline=False, + font_style="", + font_stretching=100, + font_family="", + font_size_pt=0, + font_size=0, + dash_pattern="[] 0 d", + ), + ] + + def _push_local_stack(self): + self.__statestack.append(self.__statestack[-1].copy()) + + def _pop_local_stack(self): + del self.__statestack[-1] + + @property + def draw_color(self): + return self.__statestack[-1]["draw_color"] + + @draw_color.setter + def draw_color(self, v): + self.__statestack[-1]["draw_color"] = v + + @property + def fill_color(self): + return self.__statestack[-1]["fill_color"] + + @fill_color.setter + def fill_color(self, v): + self.__statestack[-1]["fill_color"] = v + + @property + def text_color(self): + return self.__statestack[-1]["text_color"] + + @text_color.setter + def text_color(self, v): + self.__statestack[-1]["text_color"] = v + + @property + def underline(self): + return self.__statestack[-1]["underline"] + + @underline.setter + def underline(self, v): + self.__statestack[-1]["underline"] = v + + @property + def font_style(self): + return self.__statestack[-1]["font_style"] + + @font_style.setter + def font_style(self, v): + self.__statestack[-1]["font_style"] = v + + @property + def font_stretching(self): + return self.__statestack[-1]["font_stretching"] + + @font_stretching.setter + def font_stretching(self, v): + self.__statestack[-1]["font_stretching"] = v + + @property + def font_family(self): + return self.__statestack[-1]["font_family"] + + @font_family.setter + def font_family(self, v): + self.__statestack[-1]["font_family"] = v + + @property + def font_size_pt(self): + return self.__statestack[-1]["font_size_pt"] + + @font_size_pt.setter + def font_size_pt(self, v): + self.__statestack[-1]["font_size_pt"] = v + + @property + def font_size(self): + return self.__statestack[-1]["font_size"] + + @font_size.setter + def font_size(self, v): + self.__statestack[-1]["font_size"] = v + + @property + def dash_pattern(self): + return self.__statestack[-1]["dash_pattern"] + + @dash_pattern.setter + def dash_pattern(self, v): + self.__statestack[-1]["dash_pattern"] = v diff --git a/test/graphics_context.pdf b/test/graphics_context.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4d6f59f62a426b17be85fdb5f9080567886b1e8a GIT binary patch literal 1250 zcmbVMTSyd97$zgCA<8cF&_kRqMi+E;b{(C`MbXu5Eo*a?ZYDOndt66m=h)0yTa~01 zbIXXX5RwH&krmO6E@b!=T13%9zC`ze9=cf>TMuFX(Y2N}2s#flbG~!V|DW&wzJJnI z7dq303>#-d7DT+YoZZfsH`HOouT}z>gQtOYiUe5oaJ&bbBxLxoiLAqbgXk32O_a&z zxw#zH{VWQb#-lk^jba1wPEDb>5c`N9f|p=DKx>dSTZS_xHLw!o!W~Bs+*>tPm{b(| z(bm;c6!}mx`{|m6xdS;*Vy)u*nrBUq!eQaS(J3xjnnRZyo^Y+LHN9S{-FilQd^U2kslH>sr>-nUtv3URU>|Xxo)w{)+y^T@TLPy%QUl+`8b~)v}&rWjz~bwYTSN>^q*C zv&(f$E8VdBZChT`qhROs=*{Sb%Te`>J!Mz_)p>gx?!JBt&)ZW^ZFqI)UFTr$`@t=Z zr35!OelZ)LkLf<~gOxO1p?Gnb7^9>zIevxeH&F!>(o@Ot4iaL*ar{c`SCs-1MimxV z5@j*NDj_LT$dKep?vG?*a#Sa1%kc$@iP;z8U0AEfRP`wwUl#JxA(JqxK#C$AWL5P5 z;@7HrzHX``KtlYif5nFj8!9%^3y9`-do^`)2>kaynV2_+>jVBZh_4J&RKbJM5nqnO zz@#_`-Y$s09r1TD)NF{T-3}iDV)X#aJFSS_+{2jg>)hU2?4t;_!1Pb;vJ)~oz|2%7 zdLh^yEi7Cr$XT*rwF!c3fw3szl$3C4xaS-SRWuS{Z5Mzhi+7#74(s_o7Bj9cG?PkQ zNDpZmT$=4IQ5r}Hn7MWAAX+G>C!`4*xSF#dnPd5f2g(vfn~18>2$pFBl58OTH6NM7I`!#0sW+Xq7_6)!m Date: Mon, 1 Nov 2021 00:20:58 +0100 Subject: [PATCH 49/67] Simplify Templates again, making use of flexible rotation --- fpdf/template.py | 153 ++++------------------ test/template/flextemplate_elements.pdf | Bin 0 -> 12335 bytes test/template/flextemplate_multipage.pdf | Bin 2375 -> 2367 bytes test/template/flextemplate_offset.pdf | Bin 1156 -> 1156 bytes test/template/flextemplate_rotation.pdf | Bin 41794 -> 41299 bytes test/template/template_multipage.pdf | Bin 3021 -> 3021 bytes test/template/template_nominal_csv.pdf | Bin 1636 -> 1638 bytes test/template/test_flextemplate.py | 155 ++++++++++++++++++++++- 8 files changed, 177 insertions(+), 131 deletions(-) create mode 100644 test/template/flextemplate_elements.pdf diff --git a/fpdf/template.py b/fpdf/template.py index e60f89f64..1eebda673 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -307,7 +307,6 @@ def split_multicell(self, text, element_name): def _text( self, - rotations, *_, x1=0, y1=0, @@ -351,64 +350,20 @@ def _text( if underline: style += "U" pdf.set_font(font, style, size * scale) + 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) - self._render_rotated( - (x1, y1), - pdf.cell, - (), - { - "w": width, - "h": height, - "txt": text, - "border": 0, - "ln": 0, - "align": align, - "fill": fill, - }, - rotations, - ) + pdf.cell(w=width, h=height, txt=text, border=0, ln=0, align=align, fill=fill) elif multiline: # automatic word - warp - self._render_rotated( - (x1, y1), - pdf.multi_cell, - (), - { - "w": width, - "h": height, - "txt": text, - "border": 0, - "align": align, - "fill": fill, - }, - rotations, - ) + pdf.multi_cell(w=width, h=height, txt=text, border=0, align=align, fill=fill) else: # trim to fit exactly the space defined text = pdf.multi_cell( w=width, h=height, txt=text, align=align, split_only=True )[0] - self._render_rotated( - (x1, y1), - pdf.cell, - (), - { - "w": width, - "h": height, - "txt": text, - "border": 0, - "ln": 0, - "align": align, - "fill": fill, - }, - rotations, - ) + pdf.cell(w=width, h=height, txt=text, border=0, ln=0, align=align, fill=fill) def _line( self, - rotations, *_, x1=0, y1=0, @@ -419,18 +374,13 @@ def _line( 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 * scale) - rotate = __.get("rotate") - if rotate: - rotations.append((rotate, x1, y2)) - self._render_rotated(None, pdf.line, (x1, y1, x2, y2), {}, rotations) + if self.pdf.draw_color.lower() != _rgb_as_str(foreground): + self.pdf.set_draw_color(*_rgb(foreground)) + self.pdf.set_line_width(size * scale) + self.pdf.line(x1, y1, x2, y2) def _rect( self, - rotations, *_, x1=0, y1=0, @@ -452,16 +402,10 @@ def _rect( if pdf.fill_color != _rgb_as_str(background): pdf.set_fill_color(*_rgb(background)) pdf.set_line_width(size * scale) - rotate = __.get("rotate") - if rotate: - rotations.append((rotate, x1, y2)) - self._render_rotated( - None, pdf.rect, (x1, y1, x2 - x1, y2 - y1), {"style": style}, rotations - ) + pdf.rect(x1, y1, x2 - x1, y2 - y1, style=style) def _ellipse( self, - rotations, *_, x1=0, y1=0, @@ -483,29 +427,14 @@ def _ellipse( if pdf.fill_color != _rgb_as_str(background): pdf.set_fill_color(*_rgb(background)) pdf.set_line_width(size * scale) - rotate = __.get("rotate") - if rotate: - rotations.append((rotate, x1, y2)) - self._render_rotated( - None, pdf.ellipse, (x1, y1, x2 - x1, y2 - y1), {"style": style}, rotations - ) + pdf.ellipse(x1, y1, x2 - x1, y2 - y1, style=style) - def _image(self, rotations, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): + def _image(self, *_, x1=0, y1=0, x2=0, y2=0, text="", **__): if text: - 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, - ) + self.pdf.image(text, x1, y1, w=x2 - x1, h=y2 - y1, link="") def _barcode( self, - rotations, *_, x1=0, y1=0, @@ -524,20 +453,10 @@ def _barcode( pdf.set_fill_color(*_rgb(foreground)) font = font.lower().strip() if font == "interleaved 2of5 nt": - rotate = __.get("rotate") - if rotate: - rotations.append((rotate, x1, y2)) - self._render_rotated( - None, - pdf.interleaved2of5, - (text, x1, y1), - {"w": size * scale, "h": y2 - y1}, - rotations, - ) + pdf.interleaved2of5(text, x1, y1, w=size * scale, h=y2 - y1) def _code39( self, - rotations, *_, x1=0, y1=0, @@ -563,18 +482,12 @@ def _code39( h = y2 - y1 if h <= 0: h = 5 - rotate = __.get("rotate") - if rotate: - rotations.append((rotate, x1, y2)) - self._render_rotated( - None, pdf.code39, (text, x1, y1, size * scale, h), {}, rotations - ) + pdf.code39(text, x1, y1, size * scale, h) # Added by Derek Schwalenberg Schwalenberg1013@gmail.com to allow (url) links in # templates (using write method) 2014-02-22 def _write( self, - rotations, *_, x1=0, y1=0, @@ -610,26 +523,8 @@ def _write( if underline: style += "U" pdf.set_font(font, style, size * scale) - 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: - if pos: - self.pdf.set_xy(*pos) - func(*args, **kwargs) + pdf.set_xy(x1, y1) + pdf.write(5, text, link) def render(self, offsetx=0.0, offsety=0.0, rotate=0.0, scale=1.0): """ @@ -664,12 +559,18 @@ def render(self, offsetx=0.0, offsety=0.0, rotate=0.0, scale=1.0): 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, **ele) + with self.pdf.rotation(rotate, offsetx, offsety): + if "rotate" in ele and ele['rotate']: + with self.pdf.rotation(ele["rotate"], ele["x1"], ele["y1"]): + self.handlers[handler_name](**ele) + else: + self.handlers[handler_name](**ele) else: - self.handlers[handler_name]([], **ele) + if "rotate" in ele and ele['rotate']: + with self.pdf.rotation(ele["rotate"], ele["x1"], ele["y1"]): + self.handlers[handler_name](**ele) + else: + self.handlers[handler_name](**ele) self.texts = {} # reset modified entries for the next page diff --git a/test/template/flextemplate_elements.pdf b/test/template/flextemplate_elements.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d527088537c2d466b91a5966876bf534bb66db19 GIT binary patch literal 12335 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 delta 1216 zcmdllbX;gc1Eb}{MnzZcPwb7ZPD>t4_)$COjP!cvri=!)a~bQ*k4t--_l7B6bg$cT zt;BYJq{+P%Pc$;u`lxtV?^(}ZvOU*gQtsSO6P+@qmVenZ@wu%__q=oKcxLWg@Xuj- z?PsSu58O|AJ^p*;%Y))cE`M$3-jUdP;@=~|UJ?O62ZJLdl<@8;RE=*9f!dTXYqT5bCGzDPOTQYFZIKJyxp35gF+DIT