Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parsing of csv template files #219

Merged
merged 5 commits into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions docs/Templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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")

```
61 changes: 37 additions & 24 deletions fpdf/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

from .errors import FPDFException
from .fpdf import FPDF
from .util import try_to_type


def rgb(col):
Expand Down Expand Up @@ -74,26 +73,44 @@ def load_elements(self, elements):
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":
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",

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", 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),
)
self.elements = []
self.pg_no = 0
Expand All @@ -103,11 +120,7 @@ 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, ".")
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]

Expand Down
10 changes: 6 additions & 4 deletions test/template/mycsvfile.csv
Original file line number Diff line number Diff line change
@@ -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
Binary file modified test/template/template_nominal_csv.pdf
Binary file not shown.
Binary file modified test/template/template_nominal_hardcoded.pdf
Binary file not shown.
27 changes: 26 additions & 1 deletion test/template/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ def test_template_nominal_hardcoded(tmp_path):
"align": "I",
"text": "",
"priority": 2,
# multiline is optional, so we test some items without it.
},
{
"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",
Expand All @@ -62,6 +82,7 @@ def test_template_nominal_hardcoded(tmp_path):
"align": "I",
"text": None,
"priority": 0,
"multiline": 0,
},
{
"name": "box_x",
Expand All @@ -79,6 +100,7 @@ def test_template_nominal_hardcoded(tmp_path):
"align": "I",
"text": None,
"priority": 2,
"multiline": 0,
},
{
"name": "line1",
Expand All @@ -96,6 +118,7 @@ def test_template_nominal_hardcoded(tmp_path):
"align": "I",
"text": None,
"priority": 3,
"multiline": 0,
},
{
"name": "barcode",
Expand All @@ -113,6 +136,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")
Expand All @@ -124,7 +148,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()
Expand Down