├── MANIFEST.in
├── pdfdocument
├── __init__.py
├── utils.py
├── elements.py
└── document.py
├── .gitignore
├── tox.ini
├── setup.cfg
├── setup.py
├── CHANGELOG.rst
├── LICENSE
└── README.rst
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.rst
3 |
--------------------------------------------------------------------------------
/pdfdocument/__init__.py:
--------------------------------------------------------------------------------
1 | VERSION = (4, 0, 0)
2 | __version__ = ".".join(map(str, VERSION))
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *~
3 | .*.swp
4 | \#*#
5 | .DS_Store
6 | ._*
7 | /dist
8 | MANIFEST
9 | build
10 | *.egg-info
11 | .tox
12 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [testenv]
2 | basepython = python3
3 |
4 | [testenv:style]
5 | deps =
6 | black
7 | flake8
8 | isort
9 | changedir = {toxinidir}
10 | commands =
11 | isort --recursive setup.py pdfdocument
12 | black setup.py pdfdocument
13 | flake8 .
14 | skip_install = true
15 |
--------------------------------------------------------------------------------
/pdfdocument/utils.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django.http import HttpResponse
4 |
5 | from pdfdocument.document import PDFDocument
6 |
7 |
8 | FILENAME_RE = re.compile(r"[^A-Za-z0-9\-\.]+")
9 |
10 |
11 | def pdf_response(filename, as_attachment=True, pdfdocument=PDFDocument, **kwargs):
12 | response = HttpResponse(content_type="application/pdf")
13 | response["Content-Disposition"] = '%s; filename="%s.pdf"' % (
14 | "attachment" if as_attachment else "inline",
15 | FILENAME_RE.sub("-", filename),
16 | )
17 |
18 | return pdfdocument(response, **kwargs), response
19 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude=venv,build,docs,.tox,migrations
3 | ignore=E203,W503
4 | # max-complexity=10
5 | max-line-length=88
6 |
7 | [bdist_wheel]
8 | universal = 1
9 |
10 | [isort]
11 | multi_line_output=3
12 | include_trailing_comma=True
13 | force_grid_wrap=0
14 | use_parentheses=True
15 | line_length=88
16 | lines_after_imports=2
17 | known_django=django
18 | combine_as_imports=True
19 | sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
20 |
21 | [coverage:run]
22 | branch = True
23 | include =
24 | *pdfdocument*
25 | omit =
26 | *migrations*
27 | *tests*
28 | *.tox*
29 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | from io import open
5 |
6 | from setuptools import find_packages, setup
7 |
8 |
9 | def read(filename):
10 | path = os.path.join(os.path.dirname(__file__), filename)
11 | with open(path, encoding="utf-8") as handle:
12 | return handle.read()
13 |
14 |
15 | setup(
16 | name="pdfdocument",
17 | version=__import__("pdfdocument").__version__,
18 | description="Programmatic wrapper around ReportLab.",
19 | long_description=read("README.rst"),
20 | author="Matthias Kestenholz",
21 | author_email="mk@feinheit.ch",
22 | url="https://github.com/matthiask/pdfdocument/",
23 | packages=find_packages(exclude=["tests", "testapp"]),
24 | include_package_data=True,
25 | install_requires=["reportlab"],
26 | )
27 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | .. _changelog:
2 |
3 | Change log
4 | ==========
5 |
6 | `Next version`_
7 | ~~~~~~~~~~~~~~~
8 |
9 |
10 | `v4.0`_ (2020-04-09)
11 | ~~~~~~~~~~~~~~~~~~~~
12 |
13 | - Changed ``init_report`` and ``init_letter`` to not explicitly specify
14 | the font size of the document. This changes reports to use a default
15 | font size of ``9`` instead of ``8``.
16 | - Reformatted the code using ``black`` and ``isort``.
17 |
18 |
19 | `v3.3`_ (2019-03-04)
20 | ~~~~~~~~~~~~~~~~~~~~
21 |
22 | - Fixed a grave bug where ``mini_html`` would silently drop content
23 | outside of HTML elements.
24 |
25 |
26 | `v3.2`_ (2017-04-13)
27 | ~~~~~~~~~~~~~~~~~~~~
28 |
29 | - Imported ``reduce`` from ``functools`` for Python 3 compatibility.
30 |
31 |
32 | `v3.1`_ (2014-10-21)
33 | ~~~~~~~~~~~~~~~~~~~~
34 |
35 | - Started building universal wheels.
36 | - Added an option to ``init_letter`` to specify the X position of the
37 | address block.
38 |
39 |
40 | `v3.0`_ (2014-01-03)
41 | ~~~~~~~~~~~~~~~~~~~~
42 |
43 | - Added compatibility with Python 3.
44 |
45 |
46 | .. _v3.0: https://github.com/matthiask/pdfdocument/commit/fe085bdf9
47 | .. _v3.1: https://github.com/matthiask/pdfdocument/compare/v3.0...v3.1
48 | .. _v3.2: https://github.com/matthiask/pdfdocument/compare/v3.1...v3.2
49 | .. _v3.3: https://github.com/matthiask/pdfdocument/compare/v3.2...v3.3
50 | .. _v4.0: https://github.com/matthiask/pdfdocument/compare/v3.3...v4.0
51 | .. _Next version: https://github.com/matthiask/feincms3/compare/v4.0...master
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009-2010, FEINHEIT GmbH and individual contributors.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of FEINHEIT GmbH nor the names of its contributors
15 | may be used to endorse or promote products derived from this software
16 | without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/pdfdocument/elements.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.conf import settings
4 |
5 | from pdfdocument.document import mm
6 |
7 |
8 | def create_stationery_fn(*fns):
9 | def _fn(canvas, document):
10 | for fn in fns:
11 | fn(canvas, document.PDFDocument)
12 |
13 | return _fn
14 |
15 |
16 | class ExampleStationery(object):
17 | def __call__(self, canvas, pdfdocument):
18 | left_offset = 28.6 * mm
19 |
20 | canvas.saveState()
21 | canvas.setFont("%s-Bold" % pdfdocument.style.fontName, 10)
22 | canvas.drawString(26 * mm, 284 * mm, "PLATA")
23 | canvas.setFont("%s" % pdfdocument.style.fontName, 10)
24 | canvas.drawString(26 * mm + left_offset, 284 * mm, "Django Shop Software")
25 | pdfdocument.draw_watermark(canvas)
26 | canvas.restoreState()
27 |
28 | canvas.saveState()
29 | canvas.setFont("%s" % pdfdocument.style.fontName, 6)
30 | for i, text in enumerate(reversed([pdfdocument.doc.page_index_string()])):
31 | canvas.drawRightString(190 * mm, (8 + 3 * i) * mm, text)
32 |
33 | for i, text in enumerate(reversed(["PLATA", "Something"])):
34 | canvas.drawString(26 * mm + left_offset, (8 + 3 * i) * mm, text)
35 |
36 | logo = getattr(settings, "PDF_LOGO_SETTINGS", None)
37 | if logo:
38 | canvas.drawImage(
39 | os.path.join(
40 | settings.APP_BASEDIR, "metronom", "reporting", "images", logo[0]
41 | ),
42 | **logo[1]
43 | )
44 |
45 | canvas.restoreState()
46 |
47 |
48 | class PageFnWrapper(object):
49 | """
50 | Wrap an old-style page setup function
51 | """
52 |
53 | def __init__(self, fn):
54 | self.fn = fn
55 |
56 | def __call__(self, canvas, pdfdocument):
57 | self.fn(canvas, pdfdocument.doc)
58 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | PDFDocument
3 | ===========
4 |
5 | This is a wrapper for ReportLab which allows easy creation of PDF documents::
6 |
7 | from io import BytesIO
8 | from pdfdocument.document import PDFDocument
9 |
10 | def say_hello():
11 | f = BytesIO()
12 | pdf = PDFDocument(f)
13 | pdf.init_report()
14 | pdf.h1('Hello World')
15 | pdf.p('Creating PDFs made easy.')
16 | pdf.generate()
17 | return f.getvalue()
18 |
19 |
20 | Letters and reports
21 | ===================
22 |
23 | PDFDocument comes with two different PDF templates, letters and reports. The
24 | only difference is the layout of the first page: The letter has an additional
25 | frame for the address at the top and a smaller main content area.
26 |
27 | Usage is as follows::
28 |
29 | pdf.init_report()
30 | # Or:
31 | pdf.init_letter()
32 |
33 | The letter generates default styles using 9 point fonts as base size, the report
34 | uses 8 points. This can be changed by calling ``pdf.generate_style`` again.
35 |
36 | There exists also a special type of report, the confidential report, the only
37 | differences being that the confidentiality is marked using a red cross at the
38 | top of the first page and a watermark in the background.
39 |
40 |
41 | Styles
42 | ======
43 |
44 | The call to ``pdf.generate_style`` generates a set of predefined styles. (Yes
45 | it does!) That includes the following styles; this list is neither exhaustive
46 | nor a promise:
47 |
48 | - ``pdf.style.normal``
49 | - ``pdf.style.heading1``
50 | - ``pdf.style.heading2``
51 | - ``pdf.style.heading3``
52 | - ``pdf.style.small``
53 | - ``pdf.style.bold``
54 | - ``pdf.style.right``
55 | - ``pdf.style.indented``
56 | - ``pdf.style.paragraph``
57 | - ``pdf.style.table``
58 |
59 | Most of the time you will not use those attributes directly, except in the case
60 | of tables. Convenience methods exist for almost all styles as described in the
61 | next chapter.
62 |
63 |
64 | Content
65 | =======
66 |
67 | All content passed to the following methods is escaped by default. ReportLab
68 | supports a HTML-like markup language, if you want to use it directly you'll
69 | have to either use only ``pdf.p_markup`` or resort to creating
70 | ``pdfdocument.document.MarkupParagraph`` instances by hand.
71 |
72 |
73 | Headings
74 | --------
75 |
76 | ``pdf.h1``, ``pdf.h2``, ``pdf.h3``
77 |
78 |
79 | Paragraphs
80 | ----------
81 |
82 | ``pdf.p``, ``pdf.p_markup``, ``pdf.small``, ``pdf.smaller``
83 |
84 |
85 | Unordered lists
86 | ---------------
87 |
88 | ``pdf.ul``
89 |
90 | Mini-HTML
91 | ---------
92 |
93 | ``pdf.mini_html``
94 |
95 |
96 | Various elements
97 | ----------------
98 |
99 | ``pdf.hr``, ``pdf.hr_mini``, ``pdf.spacer``, ``pdf.pagebreak``,
100 | ``pdf.start_keeptogether``, ``pdf.end_keeptogether``, ``pdf.next_frame``,
101 |
102 |
103 | Tables
104 | ------
105 |
106 | ``pdf.table``, ``pdf.bottom_table``
107 |
108 |
109 | Canvas methods
110 | --------------
111 |
112 | Canvas methods work with the canvas directly, and not with Platypus objects.
113 | They are mostly useful inside stationery functions. You'll mostly use
114 | ReportLab's canvas methods directly, and only resort to the following methods
115 | for special cases.
116 |
117 | ``pdf.confidential``, ``pdf.draw_watermark``, ``pdf.draw_svg``
118 |
119 |
120 | Additional methods
121 | ------------------
122 |
123 | ``pdf.append``, ``pdf.restart``
124 |
125 |
126 | Django integration
127 | ==================
128 |
129 | PDFDocument has a few helpers for generating PDFs in Django views, most notably
130 | ``pdfdocument.utils.pdf_response``::
131 |
132 | from pdfdocument.utils import pdf_response
133 |
134 | def pdf_view(request):
135 | pdf, response = pdf_response('filename_without_extension')
136 | # ... more code
137 |
138 | pdf.generate()
139 | return response
140 |
141 |
142 | The SVG support uses svglib by Dinu Gherman. It can be found on PyPI:
143 |
144 |
--------------------------------------------------------------------------------
/pdfdocument/document.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | import copy
4 | import sys
5 | import unicodedata
6 | from functools import reduce
7 |
8 | from reportlab.lib import colors
9 | from reportlab.lib.enums import TA_RIGHT
10 | from reportlab.lib.fonts import addMapping
11 | from reportlab.lib.styles import getSampleStyleSheet
12 | from reportlab.lib.units import cm, mm
13 | from reportlab.pdfbase import pdfmetrics
14 | from reportlab.pdfbase.ttfonts import TTFont
15 | from reportlab.platypus import (
16 | BaseDocTemplate,
17 | CondPageBreak,
18 | Frame,
19 | KeepTogether,
20 | NextPageTemplate,
21 | PageBreak,
22 | PageTemplate,
23 | Paragraph as _Paragraph,
24 | Spacer,
25 | Table,
26 | )
27 | from reportlab.platypus.flowables import HRFlowable
28 |
29 |
30 | PY2 = sys.version_info[0] < 3
31 |
32 | if PY2:
33 | string_type = unicode # noqa
34 | else:
35 | string_type = str
36 |
37 |
38 | def register_fonts_from_paths(
39 | regular, italic=None, bold=None, bolditalic=None, font_name="Reporting"
40 | ):
41 | """
42 | Pass paths to TTF files which should be used for the PDFDocument
43 | """
44 |
45 | pdfmetrics.registerFont(TTFont("%s" % font_name, regular))
46 | pdfmetrics.registerFont(TTFont("%s-Italic" % font_name, italic or regular))
47 | pdfmetrics.registerFont(TTFont("%s-Bold" % font_name, bold or regular))
48 | pdfmetrics.registerFont(
49 | TTFont("%s-BoldItalic" % font_name, bolditalic or bold or regular)
50 | )
51 |
52 | addMapping("%s" % font_name, 0, 0, "%s" % font_name)
53 | addMapping("%s" % font_name, 0, 1, "%s-Italic" % font_name)
54 | addMapping("%s" % font_name, 1, 0, "%s-Bold" % font_name)
55 | addMapping("%s" % font_name, 1, 1, "%s-BoldItalic" % font_name)
56 |
57 |
58 | class Empty(object):
59 | pass
60 |
61 |
62 | def sanitize(text):
63 | REPLACE_MAP = [
64 | (u"&", "&"),
65 | (u"<", "<"),
66 | (u">", ">"),
67 | (u"ç", "ç"),
68 | (u"Ç", "Ç"),
69 | (u"\n", "
"),
70 | (u"\r", ""),
71 | ]
72 |
73 | for p, q in REPLACE_MAP:
74 | text = text.replace(p, q)
75 | return text
76 |
77 |
78 | def normalize(text):
79 | """
80 | Some layers of reportlab, PDF or font handling or whatever cannot handle
81 | german umlauts in decomposed form correctly. Normalize everything to
82 | NFKC.
83 | """
84 | if not isinstance(text, string_type):
85 | text = string_type(text)
86 | return unicodedata.normalize("NFKC", text)
87 |
88 |
89 | def MarkupParagraph(txt, *args, **kwargs):
90 | if not txt:
91 | return _Paragraph(u"", *args, **kwargs)
92 | return _Paragraph(normalize(txt), *args, **kwargs)
93 |
94 |
95 | def Paragraph(txt, *args, **kwargs):
96 | if not txt:
97 | return _Paragraph(u"", *args, **kwargs)
98 | return _Paragraph(sanitize(normalize(txt)), *args, **kwargs)
99 |
100 |
101 | class BottomTable(Table):
102 | """
103 | This table will automatically be moved to the bottom of the page using the
104 | BottomSpacer right before it.
105 | """
106 |
107 | pass
108 |
109 |
110 | class BottomSpacer(Spacer):
111 | def wrap(self, availWidth, availHeight):
112 | my_height = availHeight - self._doc.bottomTableHeight
113 |
114 | if my_height <= 0:
115 | return (self.width, availHeight)
116 | else:
117 | return (self.width, my_height)
118 |
119 |
120 | class RestartPageBreak(PageBreak):
121 | """
122 | Insert a page break and restart the page numbering.
123 | """
124 |
125 | pass
126 |
127 |
128 | class ReportingDocTemplate(BaseDocTemplate):
129 | def __init__(self, *args, **kwargs):
130 | BaseDocTemplate.__init__(self, *args, **kwargs)
131 | self.bottomTableHeight = 0
132 | self.bottomTableIsLast = False
133 | self.numPages = 0
134 | self._lastNumPages = 0
135 | self.setProgressCallBack(self._onProgress_cb)
136 |
137 | # For batch reports with several PDFs concatenated
138 | self.restartDoc = False
139 | self.restartDocIndex = 0
140 | self.restartDocPageNumbers = []
141 |
142 | def afterFlowable(self, flowable):
143 | self.numPages = max(self.canv.getPageNumber(), self.numPages)
144 | self.bottomTableIsLast = False
145 |
146 | if isinstance(flowable, BottomTable):
147 | self.bottomTableHeight = reduce(lambda p, q: p + q, flowable._rowHeights, 0)
148 |
149 | self.bottomTableIsLast = True
150 |
151 | elif isinstance(flowable, RestartPageBreak):
152 | self.restartDoc = True
153 | self.restartDocIndex += 1
154 | self.restartDocPageNumbers.append(self.page)
155 |
156 | # here the real hackery starts ... thanks Ralph
157 | def _allSatisfied(self):
158 | """ Called by multi-build - are all cross-references resolved? """
159 | if self._lastNumPages < self.numPages:
160 | return 0
161 | return BaseDocTemplate._allSatisfied(self)
162 |
163 | def _onProgress_cb(self, what, arg):
164 | if what == "STARTED":
165 | self._lastNumPages = self.numPages
166 | self.restartDocIndex = 0
167 | # self.restartDocPageNumbers = []
168 |
169 | def page_index(self):
170 | """
171 | Return the current page index as a tuple (current_page, total_pages)
172 |
173 | This is the ugliest thing I've done in the last two years.
174 | For this I'll burn in programmer hell.
175 |
176 | At least it is contained here.
177 |
178 | (Determining the total number of pages in reportlab is a mess
179 | anyway...)
180 | """
181 |
182 | current_page = self.page
183 | total_pages = self.numPages
184 |
185 | if self.restartDoc:
186 | if self.restartDocIndex:
187 | current_page = (
188 | current_page
189 | - self.restartDocPageNumbers[self.restartDocIndex - 1]
190 | + 1
191 | )
192 | if len(self.restartDocPageNumbers) > self.restartDocIndex:
193 | total_pages = (
194 | self.restartDocPageNumbers[self.restartDocIndex]
195 | - self.restartDocPageNumbers[self.restartDocIndex - 1]
196 | + 1
197 | )
198 | else:
199 | total_pages = self.restartDocPageNumbers[0]
200 |
201 | if self.bottomTableHeight:
202 | total_pages -= 1
203 |
204 | if self.bottomTableIsLast and current_page == 1:
205 | total_pages = max(1, total_pages)
206 |
207 | # Ensure total pages is always at least 1
208 | total_pages = max(1, total_pages)
209 |
210 | return (current_page, total_pages)
211 |
212 | def page_index_string(self):
213 | """
214 | Return page index string for the footer.
215 | """
216 | current_page, total_pages = self.page_index()
217 |
218 | return self.PDFDocument.page_index_string(current_page, total_pages)
219 |
220 |
221 | def dummy_stationery(c, doc):
222 | pass
223 |
224 |
225 | class PDFDocument(object):
226 | show_boundaries = False
227 | _watermark = None
228 |
229 | def __init__(self, *args, **kwargs):
230 | self.doc = ReportingDocTemplate(*args, **kwargs)
231 | self.doc.PDFDocument = self
232 | self.story = []
233 |
234 | self.font_name = kwargs.get("font_name", "Helvetica")
235 | self.font_size = kwargs.get("font_size", 9)
236 |
237 | def page_index_string(self, current_page, total_pages):
238 | return "Page %(current_page)d of %(total_pages)d" % {
239 | "current_page": current_page,
240 | "total_pages": total_pages,
241 | }
242 |
243 | def generate_style(self, font_name=None, font_size=None):
244 | self.style = Empty()
245 | self.style.fontName = font_name or self.font_name
246 | self.style.fontSize = font_size or self.font_size
247 |
248 | _styles = getSampleStyleSheet()
249 |
250 | self.style.normal = _styles["Normal"]
251 | self.style.normal.fontName = "%s" % self.style.fontName
252 | self.style.normal.fontSize = self.style.fontSize
253 | self.style.normal.firstLineIndent = 0
254 | # normal.textColor = '#0e2b58'
255 |
256 | self.style.heading1 = copy.deepcopy(self.style.normal)
257 | self.style.heading1.fontName = "%s" % self.style.fontName
258 | self.style.heading1.fontSize = 1.5 * self.style.fontSize
259 | self.style.heading1.leading = 2 * self.style.fontSize
260 | # heading1.leading = 10*mm
261 |
262 | self.style.heading2 = copy.deepcopy(self.style.normal)
263 | self.style.heading2.fontName = "%s-Bold" % self.style.fontName
264 | self.style.heading2.fontSize = 1.25 * self.style.fontSize
265 | self.style.heading2.leading = 1.75 * self.style.fontSize
266 | # heading2.leading = 5*mm
267 |
268 | self.style.heading3 = copy.deepcopy(self.style.normal)
269 | self.style.heading3.fontName = "%s-Bold" % self.style.fontName
270 | self.style.heading3.fontSize = 1.1 * self.style.fontSize
271 | self.style.heading3.leading = 1.5 * self.style.fontSize
272 | self.style.heading3.textColor = "#666666"
273 | # heading3.leading = 5*mm
274 |
275 | self.style.small = copy.deepcopy(self.style.normal)
276 | self.style.small.fontSize = self.style.fontSize - 0.9
277 |
278 | self.style.smaller = copy.deepcopy(self.style.normal)
279 | self.style.smaller.fontSize = self.style.fontSize * 0.75
280 |
281 | self.style.bold = copy.deepcopy(self.style.normal)
282 | self.style.bold.fontName = "%s-Bold" % self.style.fontName
283 |
284 | self.style.boldr = copy.deepcopy(self.style.bold)
285 | self.style.boldr.alignment = TA_RIGHT
286 |
287 | self.style.right = copy.deepcopy(self.style.normal)
288 | self.style.right.alignment = TA_RIGHT
289 |
290 | self.style.indented = copy.deepcopy(self.style.normal)
291 | self.style.indented.leftIndent = 0.5 * cm
292 |
293 | self.style.tablenotes = copy.deepcopy(self.style.indented)
294 | self.style.tablenotes.fontName = "%s-Italic" % self.style.fontName
295 |
296 | self.style.paragraph = copy.deepcopy(self.style.normal)
297 | self.style.paragraph.spaceBefore = 1
298 | self.style.paragraph.spaceAfter = 1
299 |
300 | self.style.bullet = copy.deepcopy(self.style.normal)
301 | self.style.bullet.bulletFontName = "Symbol"
302 | self.style.bullet.bulletFontSize = 7
303 | self.style.bullet.bulletIndent = 6
304 | self.style.bullet.firstLineIndent = 0
305 | self.style.bullet.leftIndent = 15
306 |
307 | self.style.numberbullet = copy.deepcopy(self.style.normal)
308 | self.style.numberbullet.bulletFontName = self.style.paragraph.fontName
309 | self.style.numberbullet.bulletFontSize = self.style.paragraph.fontSize
310 | self.style.numberbullet.bulletIndent = 0
311 | self.style.numberbullet.firstLineIndent = 0
312 | self.style.numberbullet.leftIndent = 15
313 |
314 | # alignment = TA_RIGHT
315 | # leftIndent = 0.4*cm
316 | # spaceBefore = 0
317 | # spaceAfter = 0
318 |
319 | self.style.tableBase = (
320 | ("FONT", (0, 0), (-1, -1), "%s" % self.style.fontName, self.style.fontSize),
321 | ("TOPPADDING", (0, 0), (-1, -1), 0),
322 | ("BOTTOMPADDING", (0, 0), (-1, -1), 1),
323 | ("LEFTPADDING", (0, 0), (-1, -1), 0),
324 | ("RIGHTPADDING", (0, 0), (-1, -1), 0),
325 | ("FIRSTLINEINDENT", (0, 0), (-1, -1), 0),
326 | ("VALIGN", (0, 0), (-1, -1), "TOP"),
327 | )
328 |
329 | self.style.table = self.style.tableBase + (
330 | ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
331 | )
332 |
333 | self.style.tableLLR = self.style.tableBase + (
334 | ("ALIGN", (2, 0), (-1, -1), "RIGHT"),
335 | ("VALIGN", (0, 0), (-1, 0), "BOTTOM"),
336 | )
337 |
338 | self.style.tableHead = self.style.tableBase + (
339 | (
340 | "FONT",
341 | (0, 0),
342 | (-1, 0),
343 | "%s-Bold" % self.style.fontName,
344 | self.style.fontSize,
345 | ),
346 | ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
347 | ("TOPPADDING", (0, 0), (-1, -1), 1),
348 | ("BOTTOMPADDING", (0, 0), (-1, -1), 2),
349 | ("LINEABOVE", (0, 0), (-1, 0), 0.2, colors.black),
350 | ("LINEBELOW", (0, 0), (-1, 0), 0.2, colors.black),
351 | )
352 |
353 | self.style.tableOptional = self.style.tableBase + (
354 | (
355 | "FONT",
356 | (0, 0),
357 | (-1, 0),
358 | "%s-Italic" % self.style.fontName,
359 | self.style.fontSize,
360 | ),
361 | ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
362 | ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
363 | ("RIGHTPADDING", (1, 0), (-1, -1), 2 * cm),
364 | )
365 |
366 | def init_templates(self, page_fn, page_fn_later=None):
367 | self.doc.addPageTemplates(
368 | [
369 | PageTemplate(id="First", frames=[self.frame], onPage=page_fn),
370 | PageTemplate(
371 | id="Later", frames=[self.frame], onPage=page_fn_later or page_fn
372 | ),
373 | ]
374 | )
375 | self.story.append(NextPageTemplate("Later"))
376 |
377 | def init_report(self, page_fn=dummy_stationery, page_fn_later=None):
378 | frame_kwargs = {
379 | "showBoundary": self.show_boundaries,
380 | "leftPadding": 0,
381 | "rightPadding": 0,
382 | "topPadding": 0,
383 | "bottomPadding": 0,
384 | }
385 |
386 | full_frame = Frame(2.6 * cm, 2 * cm, 16.4 * cm, 25 * cm, **frame_kwargs)
387 |
388 | self.doc.addPageTemplates(
389 | [
390 | PageTemplate(id="First", frames=[full_frame], onPage=page_fn),
391 | PageTemplate(
392 | id="Later", frames=[full_frame], onPage=page_fn_later or page_fn
393 | ),
394 | ]
395 | )
396 | self.story.append(NextPageTemplate("Later"))
397 |
398 | self.generate_style()
399 |
400 | def init_confidential_report(self, page_fn=dummy_stationery, page_fn_later=None):
401 | if not page_fn_later:
402 | page_fn_later = page_fn
403 |
404 | def _first_page_fn(canvas, doc):
405 | page_fn(canvas, doc)
406 | doc.PDFDocument.confidential(canvas)
407 | doc.PDFDocument.watermark("CONFIDENTIAL")
408 |
409 | self.init_report(page_fn=_first_page_fn, page_fn_later=page_fn_later)
410 |
411 | def init_letter(
412 | self,
413 | page_fn=dummy_stationery,
414 | page_fn_later=None,
415 | address_y=None,
416 | address_x=None,
417 | ):
418 | frame_kwargs = {
419 | "showBoundary": self.show_boundaries,
420 | "leftPadding": 0,
421 | "rightPadding": 0,
422 | "topPadding": 0,
423 | "bottomPadding": 0,
424 | }
425 |
426 | address_frame = Frame(
427 | address_x or 2.6 * cm,
428 | address_y or 20.2 * cm,
429 | 16.4 * cm,
430 | 4 * cm,
431 | **frame_kwargs
432 | )
433 | rest_frame = Frame(2.6 * cm, 2 * cm, 16.4 * cm, 18.2 * cm, **frame_kwargs)
434 | full_frame = Frame(2.6 * cm, 2 * cm, 16.4 * cm, 25 * cm, **frame_kwargs)
435 |
436 | self.doc.addPageTemplates(
437 | [
438 | PageTemplate(
439 | id="First", frames=[address_frame, rest_frame], onPage=page_fn
440 | ),
441 | PageTemplate(
442 | id="Later", frames=[full_frame], onPage=page_fn_later or page_fn
443 | ),
444 | ]
445 | )
446 | self.story.append(NextPageTemplate("Later"))
447 |
448 | self.generate_style()
449 |
450 | def watermark(self, watermark=None):
451 | self._watermark = watermark
452 |
453 | def restart(self):
454 | self.story.append(NextPageTemplate("First"))
455 | self.story.append(RestartPageBreak())
456 |
457 | def p(self, text, style=None):
458 | self.story.append(Paragraph(text, style or self.style.normal))
459 |
460 | def h1(self, text, style=None):
461 | self.story.append(Paragraph(text, style or self.style.heading1))
462 |
463 | def h2(self, text, style=None):
464 | self.story.append(Paragraph(text, style or self.style.heading2))
465 |
466 | def h3(self, text, style=None):
467 | self.story.append(Paragraph(text, style or self.style.heading3))
468 |
469 | def small(self, text, style=None):
470 | self.story.append(Paragraph(text, style or self.style.small))
471 |
472 | def smaller(self, text, style=None):
473 | self.story.append(Paragraph(text, style or self.style.smaller))
474 |
475 | def p_markup(self, text, style=None):
476 | self.story.append(MarkupParagraph(text, style or self.style.normal))
477 |
478 | def ul(self, items):
479 | for item in items:
480 | self.story.append(MarkupParagraph(item, self.style.bullet, bulletText=u"•"))
481 |
482 | def spacer(self, height=0.6 * cm):
483 | self.story.append(Spacer(1, height))
484 |
485 | def table(self, data, columns, style=None):
486 | self.story.append(Table(data, columns, style=style or self.style.table))
487 |
488 | def hr(self):
489 | self.story.append(HRFlowable(width="100%", thickness=0.2, color=colors.black))
490 |
491 | def hr_mini(self):
492 | self.story.append(HRFlowable(width="100%", thickness=0.2, color=colors.grey))
493 |
494 | def mini_html(self, html):
495 | """Convert a small subset of HTML into ReportLab paragraphs
496 |
497 | Requires lxml and BeautifulSoup."""
498 | import lxml.html
499 | import lxml.html.soupparser
500 |
501 | TAG_MAP = {
502 | "strong": "b",
503 | "em": "i",
504 | "br": "br", # Leave br tags alone
505 | }
506 |
507 | BULLETPOINT = u"•"
508 |
509 | def _p(text, list_bullet_point, style=None):
510 | if list_bullet_point:
511 | self.story.append(
512 | MarkupParagraph(
513 | text,
514 | style or self.style.paragraph,
515 | bulletText=list_bullet_point,
516 | )
517 | )
518 | else:
519 | self.story.append(MarkupParagraph(text, style or self.style.paragraph))
520 |
521 | def _remove_attributes(element):
522 | for key in element.attrib:
523 | del element.attrib[key]
524 |
525 | def _handle_element(element, list_bullet_point=False, style=None):
526 | _remove_attributes(element)
527 |
528 | if element.tag in TAG_MAP:
529 | element.tag = TAG_MAP[element.tag]
530 |
531 | if element.tag in ("ul",):
532 | for item in element:
533 | _handle_element(
534 | item, list_bullet_point=BULLETPOINT, style=self.style.bullet
535 | )
536 | list_bullet_point = False
537 | elif element.tag in ("ol",):
538 | for counter, item in enumerate(element):
539 | _handle_element(
540 | item,
541 | list_bullet_point=u"{}.".format(counter + 1),
542 | style=self.style.numberbullet,
543 | )
544 | list_bullet_point = False
545 | elif element.tag in ("p", "li"):
546 | for tag in reversed(list(element.iterdescendants())):
547 | _remove_attributes(tag)
548 | if tag.tag in TAG_MAP:
549 | tag.tag = TAG_MAP[tag.tag]
550 | else:
551 | tag.drop_tag()
552 |
553 | _p(
554 | lxml.html.tostring(element, method="xml", encoding=string_type),
555 | list_bullet_point,
556 | style,
557 | )
558 | else:
559 | if element.text:
560 | _p(element.text, list_bullet_point, style)
561 |
562 | for item in element:
563 | _handle_element(item, list_bullet_point, style)
564 |
565 | if element.tail:
566 | _p(element.tail, list_bullet_point, style)
567 |
568 | soup = lxml.html.soupparser.fromstring(html)
569 | _handle_element(soup)
570 |
571 | def pagebreak(self):
572 | self.story.append(PageBreak())
573 |
574 | def bottom_table(self, data, columns, style=None):
575 | obj = BottomSpacer(1, 1)
576 | obj._doc = self.doc
577 | self.story.append(obj)
578 |
579 | self.story.append(BottomTable(data, columns, style=style or self.style.table))
580 |
581 | def append(self, data):
582 | self.story.append(data)
583 |
584 | def generate(self):
585 | self.doc.multiBuild(self.story)
586 |
587 | def confidential(self, canvas):
588 | canvas.saveState()
589 |
590 | canvas.translate(18.5 * cm, 27.4 * cm)
591 |
592 | canvas.setLineWidth(3)
593 | canvas.setFillColorRGB(1, 0, 0)
594 | canvas.setStrokeGray(0.5)
595 |
596 | p = canvas.beginPath()
597 | p.moveTo(10, 0)
598 | p.lineTo(20, 10)
599 | p.lineTo(30, 0)
600 | p.lineTo(40, 10)
601 | p.lineTo(30, 20)
602 | p.lineTo(40, 30)
603 | p.lineTo(30, 40)
604 | p.lineTo(20, 30)
605 | p.lineTo(10, 40)
606 | p.lineTo(0, 30)
607 | p.lineTo(10, 20)
608 | p.lineTo(0, 10)
609 |
610 | canvas.drawPath(p, fill=1, stroke=0)
611 |
612 | canvas.restoreState()
613 |
614 | def draw_watermark(self, canvas):
615 | if self._watermark:
616 | canvas.saveState()
617 | canvas.rotate(60)
618 | canvas.setFillColorRGB(0.9, 0.9, 0.9)
619 | canvas.setFont("%s" % self.style.fontName, 120)
620 | canvas.drawCentredString(195 * mm, -30 * mm, self._watermark)
621 | canvas.restoreState()
622 |
623 | def draw_svg(self, canvas, path, xpos=0, ypos=0, xsize=None, ysize=None):
624 | from reportlab.graphics import renderPDF
625 | from svglib.svglib import svg2rlg
626 |
627 | drawing = svg2rlg(path)
628 | xL, yL, xH, yH = drawing.getBounds()
629 |
630 | if xsize:
631 | drawing.renderScale = xsize / (xH - xL)
632 | if ysize:
633 | drawing.renderScale = ysize / (yH - yL)
634 |
635 | renderPDF.draw(drawing, canvas, xpos, ypos, showBoundary=self.show_boundaries)
636 |
637 | def next_frame(self):
638 | self.story.append(CondPageBreak(20 * cm))
639 |
640 | def start_keeptogether(self):
641 | self.keeptogether_index = len(self.story)
642 |
643 | def end_keeptogether(self):
644 | keeptogether = KeepTogether(self.story[self.keeptogether_index :])
645 | self.story = self.story[: self.keeptogether_index]
646 | self.story.append(keeptogether)
647 |
648 | def address_head(self, text):
649 | self.smaller(text)
650 | self.spacer(2 * mm)
651 |
652 | def address(self, obj, prefix=""):
653 | if type(obj) == dict:
654 | data = obj
655 | else:
656 | data = {}
657 | for field in (
658 | "company",
659 | "manner_of_address",
660 | "first_name",
661 | "last_name",
662 | "address",
663 | "zip_code",
664 | "city",
665 | "full_override",
666 | ):
667 | attribute = "%s%s" % (prefix, field)
668 | data[field] = getattr(obj, attribute, u"").strip()
669 |
670 | address = []
671 | if data.get("company", False):
672 | address.append(data["company"])
673 |
674 | title = data.get("manner_of_address", "")
675 | if title:
676 | title += u" "
677 |
678 | if data.get("first_name", False):
679 | address.append(
680 | u"%s%s %s"
681 | % (title, data.get("first_name", ""), data.get("last_name", ""))
682 | )
683 | else:
684 | address.append(u"%s%s" % (title, data.get("last_name", "")))
685 |
686 | address.append(data.get("address"))
687 | address.append(u"%s %s" % (data.get("zip_code", ""), data.get("city", "")))
688 |
689 | if data.get("full_override"):
690 | address = [
691 | l.strip()
692 | for l in data.get("full_override").replace("\r", "").splitlines()
693 | ]
694 |
695 | self.p("\n".join(address))
696 |
--------------------------------------------------------------------------------