PR #552 + black all files

This commit is contained in:
Eric Lapouyade 2024-07-21 16:10:44 +02:00
parent f3ba468927
commit a50b52b317
43 changed files with 944 additions and 650 deletions

View File

@ -4,7 +4,7 @@ Created : 2015-03-12
@author: Eric Lapouyade
"""
__version__ = '0.17.0'
__version__ = "0.17.0"
# flake8: noqa
from .inline_image import InlineImage

View File

@ -4,32 +4,41 @@ import os
from .template import DocxTemplate, TemplateError
TEMPLATE_ARG = 'template_path'
JSON_ARG = 'json_path'
OUTPUT_ARG = 'output_filename'
OVERWRITE_ARG = 'overwrite'
QUIET_ARG = 'quiet'
TEMPLATE_ARG = "template_path"
JSON_ARG = "json_path"
OUTPUT_ARG = "output_filename"
OVERWRITE_ARG = "overwrite"
QUIET_ARG = "quiet"
def make_arg_parser():
parser = argparse.ArgumentParser(
usage='python -m docxtpl [-h] [-o] [-q] {} {} {}'.format(TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG),
description='Make docx file from existing template docx and json data.')
parser.add_argument(TEMPLATE_ARG,
type=str,
help='The path to the template docx file.')
parser.add_argument(JSON_ARG,
type=str,
help='The path to the json file with the data.')
parser.add_argument(OUTPUT_ARG,
type=str,
help='The filename to save the generated docx.')
parser.add_argument('-' + OVERWRITE_ARG[0], '--' + OVERWRITE_ARG,
action='store_true',
help='If output file already exists, overwrites without asking for confirmation')
parser.add_argument('-' + QUIET_ARG[0], '--' + QUIET_ARG,
action='store_true',
help='Do not display unnecessary messages')
usage="python -m docxtpl [-h] [-o] [-q] {} {} {}".format(
TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG
),
description="Make docx file from existing template docx and json data.",
)
parser.add_argument(
TEMPLATE_ARG, type=str, help="The path to the template docx file."
)
parser.add_argument(
JSON_ARG, type=str, help="The path to the json file with the data."
)
parser.add_argument(
OUTPUT_ARG, type=str, help="The filename to save the generated docx."
)
parser.add_argument(
"-" + OVERWRITE_ARG[0],
"--" + OVERWRITE_ARG,
action="store_true",
help="If output file already exists, overwrites without asking for confirmation",
)
parser.add_argument(
"-" + QUIET_ARG[0],
"--" + QUIET_ARG,
action="store_true",
help="Do not display unnecessary messages",
)
return parser
@ -43,18 +52,21 @@ def get_args(parser):
if e.code == 0:
raise SystemExit
else:
raise RuntimeError('Correct usage is:\n{parser.usage}'.format(parser=parser))
raise RuntimeError(
"Correct usage is:\n{parser.usage}".format(parser=parser)
)
def is_argument_valid(arg_name, arg_value, overwrite):
# Basic checks for the arguments
if arg_name == TEMPLATE_ARG:
return os.path.isfile(arg_value) and arg_value.endswith('.docx')
return os.path.isfile(arg_value) and arg_value.endswith(".docx")
elif arg_name == JSON_ARG:
return os.path.isfile(arg_value) and arg_value.endswith('.json')
return os.path.isfile(arg_value) and arg_value.endswith(".json")
elif arg_name == OUTPUT_ARG:
return arg_value.endswith('.docx') and check_exists_ask_overwrite(
arg_value, overwrite)
return arg_value.endswith(".docx") and check_exists_ask_overwrite(
arg_value, overwrite
)
elif arg_name in [OVERWRITE_ARG, QUIET_ARG]:
return arg_value in [True, False]
@ -65,13 +77,18 @@ def check_exists_ask_overwrite(arg_value, overwrite):
# confirmed returns True, else raises OSError.
if os.path.exists(arg_value) and not overwrite:
try:
msg = 'File %s already exists, would you like to overwrite the existing file? (y/n)' % arg_value
if input(msg).lower() == 'y':
msg = (
"File %s already exists, would you like to overwrite the existing file? (y/n)"
% arg_value
)
if input(msg).lower() == "y":
return True
else:
raise OSError
except OSError:
raise RuntimeError('File %s already exists, please choose a different name.' % arg_value)
raise RuntimeError(
"File %s already exists, please choose a different name." % arg_value
)
else:
return True
@ -87,7 +104,8 @@ def validate_all_args(parsed_args):
raise RuntimeError(
'The specified {arg_name} "{arg_value}" is not valid.'.format(
arg_name=arg_name, arg_value=arg_value
))
)
)
def get_json_data(json_path):
@ -97,17 +115,18 @@ def get_json_data(json_path):
return json_data
except json.JSONDecodeError as e:
print(
'There was an error on line {e.lineno}, column {e.colno} while trying to parse file {json_path}'.format(
"There was an error on line {e.lineno}, column {e.colno} while trying to parse file {json_path}".format(
e=e, json_path=json_path
))
raise RuntimeError('Failed to get json data.')
)
)
raise RuntimeError("Failed to get json data.")
def make_docxtemplate(template_path):
try:
return DocxTemplate(template_path)
except TemplateError:
raise RuntimeError('Could not create docx template.')
raise RuntimeError("Could not create docx template.")
def render_docx(doc, json_data):
@ -115,7 +134,7 @@ def render_docx(doc, json_data):
doc.render(json_data)
return doc
except TemplateError:
raise RuntimeError('An error ocurred while trying to render the docx')
raise RuntimeError("An error ocurred while trying to render the docx")
def save_file(doc, parsed_args):
@ -123,10 +142,14 @@ def save_file(doc, parsed_args):
output_path = parsed_args[OUTPUT_ARG]
doc.save(output_path)
if not parsed_args[QUIET_ARG]:
print('Document successfully generated and saved at {output_path}'.format(output_path=output_path))
print(
"Document successfully generated and saved at {output_path}".format(
output_path=output_path
)
)
except OSError as e:
print('{e.strerror}. Could not save file {e.filename}.'.format(e=e))
raise RuntimeError('Failed to save file.')
print("{e.strerror}. Could not save file {e.filename}.".format(e=e))
raise RuntimeError("Failed to save file.")
def main():
@ -142,12 +165,12 @@ def main():
doc = render_docx(doc, json_data)
save_file(doc, parsed_args)
except RuntimeError as e:
print('Error: '+e.__str__())
print("Error: " + e.__str__())
return
finally:
if not parsed_args[QUIET_ARG]:
print('Exiting program!')
print("Exiting program!")
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -7,11 +7,13 @@ Created : 2021-07-30
from docx.oxml import OxmlElement, parse_xml
from docx.oxml.ns import qn
class InlineImage(object):
"""Class to generate an inline image
This is much faster than using Subdoc class.
"""
tpl = None
image_descriptor = None
width = None
@ -25,17 +27,21 @@ class InlineImage(object):
def _add_hyperlink(self, run, url, part):
# Create a relationship for the hyperlink
r_id = part.relate_to(url, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', is_external=True)
r_id = part.relate_to(
url,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
is_external=True,
)
# Find the <wp:docPr> and <pic:cNvPr> element
docPr = run.xpath('.//wp:docPr')[0]
cNvPr = run.xpath('.//pic:cNvPr')[0]
docPr = run.xpath(".//wp:docPr")[0]
cNvPr = run.xpath(".//pic:cNvPr")[0]
# Create the <a:hlinkClick> element
hlinkClick1 = OxmlElement('a:hlinkClick')
hlinkClick1.set(qn('r:id'), r_id)
hlinkClick2 = OxmlElement('a:hlinkClick')
hlinkClick2.set(qn('r:id'), r_id)
hlinkClick1 = OxmlElement("a:hlinkClick")
hlinkClick1.set(qn("r:id"), r_id)
hlinkClick2 = OxmlElement("a:hlinkClick")
hlinkClick2.set(qn("r:id"), r_id)
# Insert the <a:hlinkClick> element right after the <wp:docPr> element
docPr.append(hlinkClick1)
@ -51,12 +57,16 @@ class InlineImage(object):
).xml
if self.anchor:
run = parse_xml(pic)
if run.xpath('.//a:blip'):
hyperlink = self._add_hyperlink(run, self.anchor, self.tpl.current_rendering_part)
if run.xpath(".//a:blip"):
hyperlink = self._add_hyperlink(
run, self.anchor, self.tpl.current_rendering_part
)
pic = hyperlink.xml
return '</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>' \
return (
"</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>"
'<w:t xml:space="preserve">' % pic
)
def __unicode__(self):
return self._insert_image()

View File

@ -5,6 +5,7 @@ Created : 2021-07-30
@author: Eric Lapouyade
"""
import six
try:
from html import escape
except ImportError:
@ -19,6 +20,7 @@ class Listing(object):
use {{ mylisting }} in your template and
context={ mylisting:Listing(the_listing_with_newlines) }
"""
def __init__(self, text):
# If not a string : cast to string (ex: int, dict etc...)
if not isinstance(text, (six.text_type, six.binary_type)):

View File

@ -5,6 +5,7 @@ Created : 2021-07-30
@author: Eric Lapouyade
"""
import six
try:
from html import escape
except ImportError:
@ -18,12 +19,15 @@ class RichText(object):
This is much faster than using Subdoc class,
but this only for texts INSIDE an existing paragraph.
"""
def __init__(self, text=None, **text_prop):
self.xml = ''
self.xml = ""
if text:
self.add(text, **text_prop)
def add(self, text,
def add(
self,
text,
style=None,
color=None,
highlight=None,
@ -35,7 +39,8 @@ class RichText(object):
underline=False,
strike=False,
font=None,
url_id=None):
url_id=None,
):
# If a RichText is added
if isinstance(text, RichText):
@ -46,55 +51,65 @@ class RichText(object):
if not isinstance(text, (six.text_type, six.binary_type)):
text = six.text_type(text)
if not isinstance(text, six.text_type):
text = text.decode('utf-8', errors='ignore')
text = text.decode("utf-8", errors="ignore")
text = escape(text)
prop = u''
prop = ""
if style:
prop += u'<w:rStyle w:val="%s"/>' % style
prop += '<w:rStyle w:val="%s"/>' % style
if color:
if color[0] == '#':
if color[0] == "#":
color = color[1:]
prop += u'<w:color w:val="%s"/>' % color
prop += '<w:color w:val="%s"/>' % color
if highlight:
if highlight[0] == '#':
if highlight[0] == "#":
highlight = highlight[1:]
prop += u'<w:shd w:fill="%s"/>' % highlight
prop += '<w:shd w:fill="%s"/>' % highlight
if size:
prop += u'<w:sz w:val="%s"/>' % size
prop += u'<w:szCs w:val="%s"/>' % size
prop += '<w:sz w:val="%s"/>' % size
prop += '<w:szCs w:val="%s"/>' % size
if subscript:
prop += u'<w:vertAlign w:val="subscript"/>'
prop += '<w:vertAlign w:val="subscript"/>'
if superscript:
prop += u'<w:vertAlign w:val="superscript"/>'
prop += '<w:vertAlign w:val="superscript"/>'
if bold:
prop += u'<w:b/>'
prop += "<w:b/>"
if italic:
prop += u'<w:i/>'
prop += "<w:i/>"
if underline:
if underline not in ['single', 'double', 'thick', 'dotted', 'dash', 'dotDash', 'dotDotDash', 'wave']:
underline = 'single'
prop += u'<w:u w:val="%s"/>' % underline
if underline not in [
"single",
"double",
"thick",
"dotted",
"dash",
"dotDash",
"dotDotDash",
"wave",
]:
underline = "single"
prop += '<w:u w:val="%s"/>' % underline
if strike:
prop += u'<w:strike/>'
prop += "<w:strike/>"
if font:
regional_font = u''
if ':' in font:
region, font = font.split(':', 1)
regional_font = u' w:{region}="{font}"'.format(font=font, region=region)
prop += (
u'<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"{regional_font}/>'
.format(font=font, regional_font=regional_font)
regional_font = ""
if ":" in font:
region, font = font.split(":", 1)
regional_font = ' w:{region}="{font}"'.format(font=font, region=region)
prop += '<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"{regional_font}/>'.format(
font=font, regional_font=regional_font
)
xml = u'<w:r>'
xml = "<w:r>"
if prop:
xml += u'<w:rPr>%s</w:rPr>' % prop
xml += u'<w:t xml:space="preserve">%s</w:t></w:r>' % text
xml += "<w:rPr>%s</w:rPr>" % prop
xml += '<w:t xml:space="preserve">%s</w:t></w:r>' % text
if url_id:
xml = (u'<w:hyperlink r:id="%s" w:tgtFrame="_blank">%s</w:hyperlink>'
% (url_id, xml))
xml = '<w:hyperlink r:id="%s" w:tgtFrame="_blank">%s</w:hyperlink>' % (
url_id,
xml,
)
self.xml += xml
def __unicode__(self):

View File

@ -51,22 +51,23 @@ class SubdocComposer(Composer):
def add_diagrams(self, doc, element):
# While waiting docxcompose 1.3.3
dgm_rels = xpath(element, './/dgm:relIds[@r:dm]')
dgm_rels = xpath(element, ".//dgm:relIds[@r:dm]")
for dgm_rel in dgm_rels:
for item, rt_type in (
('dm', RT.DIAGRAM_DATA),
('lo', RT.DIAGRAM_LAYOUT),
('qs', RT.DIAGRAM_QUICK_STYLE),
('cs', RT.DIAGRAM_COLORS)
("dm", RT.DIAGRAM_DATA),
("lo", RT.DIAGRAM_LAYOUT),
("qs", RT.DIAGRAM_QUICK_STYLE),
("cs", RT.DIAGRAM_COLORS),
):
dm_rid = dgm_rel.get('{%s}%s' % (NS['r'], item))
dm_rid = dgm_rel.get("{%s}%s" % (NS["r"], item))
dm_part = doc.part.rels[dm_rid].target_part
new_rid = self.doc.part.relate_to(dm_part, rt_type)
dgm_rel.set('{%s}%s' % (NS['r'], item), new_rid)
dgm_rel.set("{%s}%s" % (NS["r"], item), new_rid)
class Subdoc(object):
"""Class for subdocument to insert into master document"""
def __init__(self, tpl, docpath=None):
self.tpl = tpl
self.docx = tpl.get_docx()
@ -83,8 +84,13 @@ class Subdoc(object):
def _get_xml(self):
if self.subdocx.element.body.sectPr is not None:
self.subdocx.element.body.remove(self.subdocx.element.body.sectPr)
xml = re.sub(r'</?w:body[^>]*>', '', etree.tostring(
self.subdocx.element.body, encoding='unicode', pretty_print=False))
xml = re.sub(
r"</?w:body[^>]*>",
"",
etree.tostring(
self.subdocx.element.body, encoding="unicode", pretty_print=False
),
)
return xml
def __unicode__(self):

View File

@ -18,6 +18,7 @@ import docx.oxml.ns
from docx.opc.constants import RELATIONSHIP_TYPE as REL_TYPE
from jinja2 import Environment, Template, meta
from jinja2.exceptions import TemplateError
try:
from html import escape # noqa: F401
except ImportError:
@ -33,8 +34,12 @@ import zipfile
class DocxTemplate(object):
"""Class for managing docx files as they were jinja2 templates"""
HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
HEADER_URI = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
)
FOOTER_URI = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
)
def __init__(self, template_file: Union[IO[bytes], str, PathLike]) -> None:
self.template_file = template_file
@ -58,10 +63,10 @@ class DocxTemplate(object):
def __getattr__(self, name):
return getattr(self.docx, name)
def xml_to_string(self, xml, encoding='unicode'):
def xml_to_string(self, xml, encoding="unicode"):
# Be careful : pretty_print MUST be set to False, otherwise patch_xml()
# won't work properly
return etree.tostring(xml, encoding='unicode', pretty_print=False)
return etree.tostring(xml, encoding="unicode", pretty_print=False)
def get_docx(self):
self.init_docx()
@ -71,7 +76,7 @@ class DocxTemplate(object):
return self.xml_to_string(self.docx._element.body)
def write_xml(self, filename):
with open(filename, 'w') as fh:
with open(filename, "w") as fh:
fh.write(self.get_xml())
def patch_xml(self, src_xml):
@ -80,112 +85,169 @@ class DocxTemplate(object):
unescape html entities, etc..."""
# replace {<something>{ by {{ ( works with {{ }} {% and %} {# and #})
src_xml = re.sub(r'(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})', '',
src_xml, flags=re.DOTALL)
src_xml = re.sub(
r"(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})",
"",
src_xml,
flags=re.DOTALL,
)
# replace {{<some tags>jinja2 stuff<some other tags>}} by {{jinja2 stuff}}
# same thing with {% ... %} and {# #}
# "jinja2 stuff" could a variable, a 'if' etc... anything jinja2 will understand
def striptags(m):
return re.sub('</w:t>.*?(<w:t>|<w:t [^>]*>)', '',
m.group(0), flags=re.DOTALL)
src_xml = re.sub(r'{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*', striptags,
src_xml, flags=re.DOTALL)
return re.sub(
"</w:t>.*?(<w:t>|<w:t [^>]*>)", "", m.group(0), flags=re.DOTALL
)
src_xml = re.sub(
r"{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*",
striptags,
src_xml,
flags=re.DOTALL,
)
# manage table cell colspan
def colspan(m):
cell_xml = m.group(1) + m.group(3)
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>',
'', cell_xml, flags=re.DOTALL)
cell_xml = re.sub(r'<w:gridSpan[^/]*/>', '', cell_xml, count=1)
return re.sub(r'(<w:tcPr[^>]*>)', r'\1<w:gridSpan w:val="{{%s}}"/>'
% m.group(2), cell_xml)
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)',
colspan, src_xml, flags=re.DOTALL)
cell_xml = re.sub(
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
"",
cell_xml,
flags=re.DOTALL,
)
cell_xml = re.sub(r"<w:gridSpan[^/]*/>", "", cell_xml, count=1)
return re.sub(
r"(<w:tcPr[^>]*>)",
r'\1<w:gridSpan w:val="{{%s}}"/>' % m.group(2),
cell_xml,
)
src_xml = re.sub(
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)",
colspan,
src_xml,
flags=re.DOTALL,
)
# manage table cell background color
def cellbg(m):
cell_xml = m.group(1) + m.group(3)
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>',
'', cell_xml, flags=re.DOTALL)
cell_xml = re.sub(r'<w:shd[^/]*/>', '', cell_xml, count=1)
return re.sub(r'(<w:tcPr[^>]*>)',
r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>'
% m.group(2), cell_xml)
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)',
cellbg, src_xml, flags=re.DOTALL)
cell_xml = re.sub(
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
"",
cell_xml,
flags=re.DOTALL,
)
cell_xml = re.sub(r"<w:shd[^/]*/>", "", cell_xml, count=1)
return re.sub(
r"(<w:tcPr[^>]*>)",
r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>' % m.group(2),
cell_xml,
)
src_xml = re.sub(
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)",
cellbg,
src_xml,
flags=re.DOTALL,
)
# ensure space preservation
src_xml = re.sub(r'<w:t>((?:(?!<w:t>).)*)({{.*?}}|{%.*?%})',
src_xml = re.sub(
r"<w:t>((?:(?!<w:t>).)*)({{.*?}}|{%.*?%})",
r'<w:t xml:space="preserve">\1\2',
src_xml, flags=re.DOTALL)
src_xml = re.sub(r'({{r\s.*?}}|{%r\s.*?%})',
src_xml,
flags=re.DOTALL,
)
src_xml = re.sub(
r"({{r\s.*?}}|{%r\s.*?%})",
r'</w:t></w:r><w:r><w:t xml:space="preserve">\1</w:t></w:r><w:r><w:t xml:space="preserve">',
src_xml, flags=re.DOTALL)
src_xml,
flags=re.DOTALL,
)
# {%- will merge with previous paragraph text
src_xml = re.sub(r'</w:t>(?:(?!</w:t>).)*?{%-', '{%', src_xml, flags=re.DOTALL)
src_xml = re.sub(r"</w:t>(?:(?!</w:t>).)*?{%-", "{%", src_xml, flags=re.DOTALL)
# -%} will merge with next paragraph text
src_xml = re.sub(r'-%}(?:(?!<w:t[ >]|{%|{{).)*?<w:t[^>]*?>', '%}', src_xml, flags=re.DOTALL)
src_xml = re.sub(
r"-%}(?:(?!<w:t[ >]|{%|{{).)*?<w:t[^>]*?>", "%}", src_xml, flags=re.DOTALL
)
for y in ['tr', 'tc', 'p', 'r']:
for y in ["tr", "tc", "p", "r"]:
# replace into xml code the row/paragraph/run containing
# {%y xxx %} or {{y xxx}} template tag
# by {% xxx %} or {{ xx }} without any surrounding <w:y> tags :
# This is mandatory to have jinja2 generating correct xml code
pat = r'<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>' % {'y': y}
src_xml = re.sub(pat, r'\1 \2', src_xml, flags=re.DOTALL)
pat = (
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>"
% {"y": y}
)
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
for y in ['tr', 'tc', 'p']:
for y in ["tr", "tc", "p"]:
# same thing, but for {#y xxx #} (but not where y == 'r', since that
# makes less sense to use comments in that context
pat = r'<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({#)%(y)s ([^}#]*(?:#})).*?</w:%(y)s>' % {'y': y}
src_xml = re.sub(pat, r'\1 \2', src_xml, flags=re.DOTALL)
pat = (
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({#)%(y)s ([^}#]*(?:#})).*?</w:%(y)s>"
% {"y": y}
)
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
# add vMerge
# use {% vm %} to make this table cell and its copies be vertically merged within a {% for %}
def v_merge_tc(m):
def v_merge(m1):
return (
'<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' +
m1.group(1) + # Everything between ``</w:tcPr>`` and ``<w:t>``.
"{% if loop.first %}" +
m1.group(2) + # Everything before ``{% vm %}``.
m1.group(3) + # Everything after ``{% vm %}``.
"{% endif %}" +
m1.group(4) # ``</w:t>``.
'<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>'
+ m1.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
+ "{% if loop.first %}"
+ m1.group(2) # Everything before ``{% vm %}``.
+ m1.group(3) # Everything after ``{% vm %}``.
+ "{% endif %}"
+ m1.group(4) # ``</w:t>``.
)
return re.sub(
r'(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(</w:t>)',
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(</w:t>)",
v_merge,
m.group(), # Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% vm %}`` inside.
flags=re.DOTALL,
)
src_xml = re.sub(r'<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?</w:tc[ >]',
v_merge_tc, src_xml, flags=re.DOTALL)
src_xml = re.sub(
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?</w:tc[ >]",
v_merge_tc,
src_xml,
flags=re.DOTALL,
)
# Use ``{% hm %}`` to make table cell become horizontally merged within
# a ``{% for %}``.
def h_merge_tc(m):
xml_to_patch = m.group() # Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% hm %}`` inside.
xml_to_patch = (
m.group()
) # Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% hm %}`` inside.
def with_gridspan(m1):
return (
m1.group(1) + # ``w:gridSpan w:val="``.
'{{ ' + m1.group(2) + ' * loop.length }}' + # Content of ``w:val``, multiplied by loop length.
m1.group(3) # Closing quotation mark.
m1.group(1) # ``w:gridSpan w:val="``.
+ "{{ "
+ m1.group(2)
+ " * loop.length }}" # Content of ``w:val``, multiplied by loop length.
+ m1.group(3) # Closing quotation mark.
)
def without_gridspan(m2):
return (
'<w:gridSpan w:val="{{ loop.length }}"/>' +
m2.group(1) + # Everything between ``</w:tcPr>`` and ``<w:t>``.
m2.group(2) + # Everything before ``{% hm %}``.
m2.group(3) + # Everything after ``{% hm %}``.
m2.group(4) # ``</w:t>``.
'<w:gridSpan w:val="{{ loop.length }}"/>'
+ m2.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
+ m2.group(2) # Everything before ``{% hm %}``.
+ m2.group(3) # Everything after ``{% hm %}``.
+ m2.group(4) # ``</w:t>``.
)
if re.search(r'w:gridSpan', xml_to_patch):
if re.search(r"w:gridSpan", xml_to_patch):
# Simple case, there's already ``gridSpan``, multiply its value.
xml = re.sub(
@ -195,15 +257,15 @@ class DocxTemplate(object):
flags=re.DOTALL,
)
xml = re.sub(
r'{%\s*hm\s*%}',
'',
r"{%\s*hm\s*%}",
"",
xml, # Patched xml.
flags=re.DOTALL,
)
else:
# There're no ``gridSpan``, add one.
xml = re.sub(
r'(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(</w:t>)',
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(</w:t>)",
without_gridspan,
xml_to_patch,
flags=re.DOTALL,
@ -212,24 +274,31 @@ class DocxTemplate(object):
# Discard every other cell generated in loop.
return "{% if loop.first %}" + xml + "{% endif %}"
src_xml = re.sub(r'<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?</w:tc[ >]',
h_merge_tc, src_xml, flags=re.DOTALL)
src_xml = re.sub(
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?</w:tc[ >]",
h_merge_tc,
src_xml,
flags=re.DOTALL,
)
def clean_tags(m):
return (m.group(0)
return (
m.group(0)
.replace(r"&#8216;", "'")
.replace('&lt;', '<')
.replace('&gt;', '>')
.replace(u'', u'"')
.replace(u'', u'"')
.replace(u"", u"'")
.replace(u"", u"'"))
src_xml = re.sub(r'(?<=\{[\{%])(.*?)(?=[\}%]})', clean_tags, src_xml)
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("", '"')
.replace("", '"')
.replace("", "'")
.replace("", "'")
)
src_xml = re.sub(r"(?<=\{[\{%])(.*?)(?=[\}%]})", clean_tags, src_xml)
return src_xml
def render_xml_part(self, src_xml, part, context, jinja_env=None):
src_xml = re.sub(r'<w:p([ >])', r'\n<w:p\1', src_xml)
src_xml = re.sub(r"<w:p([ >])", r"\n<w:p\1", src_xml)
try:
self.current_rendering_part = part
if jinja_env:
@ -238,34 +307,39 @@ class DocxTemplate(object):
template = Template(src_xml)
dst_xml = template.render(context)
except TemplateError as exc:
if hasattr(exc, 'lineno') and exc.lineno is not None:
if hasattr(exc, "lineno") and exc.lineno is not None:
line_number = max(exc.lineno - 4, 0)
exc.docx_context = map(lambda x: re.sub(r'<[^>]+>', '', x),
src_xml.splitlines()[line_number:(line_number + 7)])
exc.docx_context = map(
lambda x: re.sub(r"<[^>]+>", "", x),
src_xml.splitlines()[line_number : (line_number + 7)],
)
raise exc
dst_xml = re.sub(r'\n<w:p([ >])', r'<w:p\1', dst_xml)
dst_xml = (dst_xml
.replace('{_{', '{{')
.replace('}_}', '}}')
.replace('{_%', '{%')
.replace('%_}', '%}'))
dst_xml = re.sub(r"\n<w:p([ >])", r"<w:p\1", dst_xml)
dst_xml = (
dst_xml.replace("{_{", "{{")
.replace("}_}", "}}")
.replace("{_%", "{%")
.replace("%_}", "%}")
)
dst_xml = self.resolve_listing(dst_xml)
return dst_xml
def render_properties(self, context: Dict[str, Any], jinja_env: Optional[Environment] = None) -> None:
def render_properties(
self, context: Dict[str, Any], jinja_env: Optional[Environment] = None
) -> None:
# List of string attributes of docx.opc.coreprops.CoreProperties which are strings.
# It seems that some attributes cannot be written as strings. Those are commented out.
properties = [
'author',
"author",
# 'category',
'comments',
"comments",
# 'content_status',
'identifier',
"identifier",
# 'keywords',
'language',
"language",
# 'last_modified_by',
'subject',
'title',
"subject",
"title",
# 'version',
]
if jinja_env is None:
@ -280,32 +354,53 @@ class DocxTemplate(object):
def resolve_listing(self, xml):
def resolve_text(run_properties, paragraph_properties, m):
xml = m.group(0).replace('\t', '</w:t></w:r>'
'<w:r>%s<w:tab/></w:r>'
'<w:r>%s<w:t xml:space="preserve">' % (run_properties, run_properties))
xml = xml.replace('\a', '</w:t></w:r></w:p>'
'<w:p>%s<w:r>%s<w:t xml:space="preserve">' % (paragraph_properties, run_properties))
xml = xml.replace('\n', '</w:t><w:br/><w:t xml:space="preserve">')
xml = xml.replace('\f', '</w:t></w:r></w:p>'
xml = m.group(0).replace(
"\t",
"</w:t></w:r>"
"<w:r>%s<w:tab/></w:r>"
'<w:r>%s<w:t xml:space="preserve">' % (run_properties, run_properties),
)
xml = xml.replace(
"\a",
"</w:t></w:r></w:p>"
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
% (paragraph_properties, run_properties),
)
xml = xml.replace("\n", '</w:t><w:br/><w:t xml:space="preserve">')
xml = xml.replace(
"\f",
"</w:t></w:r></w:p>"
'<w:p><w:r><w:br w:type="page"/></w:r></w:p>'
'<w:p>%s<w:r>%s<w:t xml:space="preserve">' % (paragraph_properties, run_properties))
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
% (paragraph_properties, run_properties),
)
return xml
def resolve_run(paragraph_properties, m):
run_properties = re.search(r'<w:rPr>.*?</w:rPr>', m.group(0))
run_properties = run_properties.group(0) if run_properties else ''
return re.sub(r'<w:t(?: [^>]*)?>.*?</w:t>',
lambda x: resolve_text(run_properties, paragraph_properties, x), m.group(0),
flags=re.DOTALL)
run_properties = re.search(r"<w:rPr>.*?</w:rPr>", m.group(0))
run_properties = run_properties.group(0) if run_properties else ""
return re.sub(
r"<w:t(?: [^>]*)?>.*?</w:t>",
lambda x: resolve_text(run_properties, paragraph_properties, x),
m.group(0),
flags=re.DOTALL,
)
def resolve_paragraph(m):
paragraph_properties = re.search(r'<w:pPr>.*?</w:pPr>', m.group(0))
paragraph_properties = paragraph_properties.group(0) if paragraph_properties else ''
return re.sub(r'<w:r(?: [^>]*)?>.*?</w:r>',
paragraph_properties = re.search(r"<w:pPr>.*?</w:pPr>", m.group(0))
paragraph_properties = (
paragraph_properties.group(0) if paragraph_properties else ""
)
return re.sub(
r"<w:r(?: [^>]*)?>.*?</w:r>",
lambda x: resolve_run(paragraph_properties, x),
m.group(0), flags=re.DOTALL)
m.group(0),
flags=re.DOTALL,
)
xml = re.sub(r'<w:p(?: [^>]*)?>.*?</w:p>', resolve_paragraph, xml, flags=re.DOTALL)
xml = re.sub(
r"<w:p(?: [^>]*)?>.*?</w:p>", resolve_paragraph, xml, flags=re.DOTALL
)
return xml
@ -332,7 +427,7 @@ class DocxTemplate(object):
m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"', xml, re.I)
if m:
return m.group(1)
return 'utf-8'
return "utf-8"
def build_headers_footers_xml(self, context, uri, jinja_env=None):
for relKey, part in self.get_headers_footers(uri):
@ -353,7 +448,7 @@ class DocxTemplate(object):
self,
context: Dict[str, Any],
jinja_env: Optional[Environment] = None,
autoescape: bool = False
autoescape: bool = False,
) -> None:
# init template working attributes
self.render_init()
@ -377,14 +472,12 @@ class DocxTemplate(object):
self.map_tree(tree)
# Headers
headers = self.build_headers_footers_xml(context, self.HEADER_URI,
jinja_env)
headers = self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env)
for relKey, xml in headers:
self.map_headers_footers_xml(relKey, xml)
# Footers
footers = self.build_headers_footers_xml(context, self.FOOTER_URI,
jinja_env)
footers = self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env)
for relKey, xml in footers:
self.map_headers_footers_xml(relKey, xml)
@ -399,15 +492,15 @@ class DocxTemplate(object):
parser = etree.XMLParser(recover=True)
tree = etree.fromstring(xml, parser=parser)
# get namespace
ns = '{' + tree.nsmap['w'] + '}'
ns = "{" + tree.nsmap["w"] + "}"
# walk trough xml and find table
for t in tree.iter(ns+'tbl'):
tblGrid = t.find(ns+'tblGrid')
columns = tblGrid.findall(ns+'gridCol')
for t in tree.iter(ns + "tbl"):
tblGrid = t.find(ns + "tblGrid")
columns = tblGrid.findall(ns + "gridCol")
to_add = 0
# walk trough all rows and try to find if there is higher cell count
for r in t.iter(ns+'tr'):
cells = r.findall(ns+'tc')
for r in t.iter(ns + "tr"):
cells = r.findall(ns + "tc")
if (len(columns) + to_add) < len(cells):
to_add = len(cells) - len(columns)
# is necessary to add columns?
@ -417,39 +510,44 @@ class DocxTemplate(object):
width = 0.0
new_average = None
for c in columns:
if not c.get(ns+'w') is None:
width += float(c.get(ns+'w'))
if not c.get(ns + "w") is None:
width += float(c.get(ns + "w"))
# try to keep proportion of table
if width > 0:
old_average = width / len(columns)
new_average = width / (len(columns) + to_add)
# scale the old columns
for c in columns:
c.set(ns+'w', str(int(float(c.get(ns+'w')) *
new_average/old_average)))
c.set(
ns + "w",
str(
int(float(c.get(ns + "w")) * new_average / old_average)
),
)
# add new columns
for i in range(to_add):
etree.SubElement(tblGrid, ns+'gridCol',
{ns+'w': str(int(new_average))})
etree.SubElement(
tblGrid, ns + "gridCol", {ns + "w": str(int(new_average))}
)
# Refetch columns after columns addition.
columns = tblGrid.findall(ns + 'gridCol')
columns = tblGrid.findall(ns + "gridCol")
columns_len = len(columns)
cells_len_max = 0
def get_cell_len(total, cell):
tc_pr = cell.find(ns + 'tcPr')
grid_span = None if tc_pr is None else tc_pr.find(ns + 'gridSpan')
tc_pr = cell.find(ns + "tcPr")
grid_span = None if tc_pr is None else tc_pr.find(ns + "gridSpan")
if grid_span is not None:
return total + int(grid_span.get(ns + 'val'))
return total + int(grid_span.get(ns + "val"))
return total + 1
# Calculate max of table cells to compare with `gridCol`.
for r in t.iter(ns + 'tr'):
cells = r.findall(ns + 'tc')
for r in t.iter(ns + "tr"):
cells = r.findall(ns + "tc")
cells_len = functools.reduce(get_cell_len, cells, 0)
cells_len_max = max(cells_len_max, cells_len)
@ -463,11 +561,11 @@ class DocxTemplate(object):
removed_width = 0.0
for c in columns[-to_remove:]:
removed_width += float(c.get(ns + 'w'))
removed_width += float(c.get(ns + "w"))
tblGrid.remove(c)
columns_left = tblGrid.findall(ns + 'gridCol')
columns_left = tblGrid.findall(ns + "gridCol")
# Distribute `removed_width` across all columns that has
# left after extras removal.
@ -477,15 +575,15 @@ class DocxTemplate(object):
extra_space = int(extra_space)
for c in columns_left:
c.set(ns+'w', str(int(float(c.get(ns+'w')) + extra_space)))
c.set(ns + "w", str(int(float(c.get(ns + "w")) + extra_space)))
return tree
def fix_docpr_ids(self, tree):
# some Ids may have some collisions : so renumbering all of them :
for elt in tree.xpath('//wp:docPr', namespaces=docx.oxml.ns.nsmap):
for elt in tree.xpath("//wp:docPr", namespaces=docx.oxml.ns.nsmap):
self.docx_ids_index += 1
elt.attrib['id'] = str(self.docx_ids_index)
elt.attrib["id"] = str(self.docx_ids_index)
def new_subdoc(self, docpath=None):
self.init_docx()
@ -493,13 +591,13 @@ class DocxTemplate(object):
@staticmethod
def get_file_crc(file_obj):
if hasattr(file_obj, 'read'):
if hasattr(file_obj, "read"):
buf = file_obj.read()
else:
with open(file_obj, 'rb') as fh:
with open(file_obj, "rb") as fh:
buf = fh.read()
crc = (binascii.crc32(buf) & 0xFFFFFFFF)
crc = binascii.crc32(buf) & 0xFFFFFFFF
return crc
def replace_media(self, src_file, dst_file):
@ -522,10 +620,10 @@ class DocxTemplate(object):
"""
crc = self.get_file_crc(src_file)
if hasattr(dst_file, 'read'):
if hasattr(dst_file, "read"):
self.crc_to_new_media[crc] = dst_file.read()
else:
with open(dst_file, 'rb') as fh:
with open(dst_file, "rb") as fh:
self.crc_to_new_media[crc] = fh.read()
def replace_pic(self, embedded_file, dst_file):
@ -543,11 +641,11 @@ class DocxTemplate(object):
for replace_embedded and replace_media)
"""
if hasattr(dst_file, 'read'):
if hasattr(dst_file, "read"):
# NOTE: file extension not checked
self.pics_to_replace[embedded_file] = dst_file.read()
else:
with open(dst_file, 'rb') as fh:
with open(dst_file, "rb") as fh:
self.pics_to_replace[embedded_file] = fh.read()
def replace_embedded(self, src_file, dst_file):
@ -563,7 +661,7 @@ class DocxTemplate(object):
Note2 : it is important to have the source file as it is required to
calculate its CRC to find them in the docx
"""
with open(dst_file, 'rb') as fh:
with open(dst_file, "rb") as fh:
crc = self.get_file_crc(src_file)
self.crc_to_new_embedded[crc] = fh.read()
@ -594,7 +692,7 @@ class DocxTemplate(object):
"word/embeddings/". Note that the file is renamed by MSWord,
so you have to guess a little bit...
"""
with open(dst_file, 'rb') as fh:
with open(dst_file, "rb") as fh:
self.zipname_to_replace[zipname] = fh.read()
def reset_replacements(self):
@ -619,11 +717,9 @@ class DocxTemplate(object):
self.pics_to_replace = {}
def post_processing(self, docx_file):
if (self.crc_to_new_media or
self.crc_to_new_embedded or
self.zipname_to_replace):
if self.crc_to_new_media or self.crc_to_new_embedded or self.zipname_to_replace:
if hasattr(docx_file, 'read'):
if hasattr(docx_file, "read"):
tmp_file = io.BytesIO()
DocxTemplate(docx_file).save(tmp_file)
tmp_file.seek(0)
@ -632,27 +728,31 @@ class DocxTemplate(object):
docx_file.seek(0)
else:
tmp_file = '%s_docxtpl_before_replace_medias' % docx_file
tmp_file = "%s_docxtpl_before_replace_medias" % docx_file
os.rename(docx_file, tmp_file)
with zipfile.ZipFile(tmp_file) as zin:
with zipfile.ZipFile(docx_file, 'w') as zout:
with zipfile.ZipFile(docx_file, "w") as zout:
for item in zin.infolist():
buf = zin.read(item.filename)
if item.filename in self.zipname_to_replace:
zout.writestr(item, self.zipname_to_replace[item.filename])
elif (item.filename.startswith('word/media/') and
item.CRC in self.crc_to_new_media):
elif (
item.filename.startswith("word/media/")
and item.CRC in self.crc_to_new_media
):
zout.writestr(item, self.crc_to_new_media[item.CRC])
elif (item.filename.startswith('word/embeddings/') and
item.CRC in self.crc_to_new_embedded):
elif (
item.filename.startswith("word/embeddings/")
and item.CRC in self.crc_to_new_embedded
):
zout.writestr(item, self.crc_to_new_embedded[item.CRC])
else:
zout.writestr(item, buf)
if not hasattr(tmp_file, 'read'):
if not hasattr(tmp_file, "read"):
os.remove(tmp_file)
if hasattr(docx_file, 'read'):
if hasattr(docx_file, "read"):
docx_file.seek(0)
def pre_processing(self):
@ -677,9 +777,7 @@ class DocxTemplate(object):
# make sure all template images defined by user were replaced
for img_id, replaced in replaced_pics.items():
if not replaced:
raise ValueError(
"Picture %s not found in the docx template" % img_id
)
raise ValueError("Picture %s not found in the docx template" % img_id)
def get_pic_map(self):
return self.pic_map
@ -690,16 +788,17 @@ class DocxTemplate(object):
part_map = {}
gds = et.xpath('//a:graphic/a:graphicData', namespaces=docx.oxml.ns.nsmap)
gds = et.xpath("//a:graphic/a:graphicData", namespaces=docx.oxml.ns.nsmap)
for gd in gds:
rel = None
# Either IMAGE, CHART, SMART_ART, ...
try:
if gd.attrib['uri'] == docx.oxml.ns.nsmap['pic']:
if gd.attrib["uri"] == docx.oxml.ns.nsmap["pic"]:
# Either PICTURE or LINKED_PICTURE image
blip = gd.xpath('pic:pic/pic:blipFill/a:blip',
namespaces=docx.oxml.ns.nsmap)[0]
dest = blip.xpath('@r:embed', namespaces=docx.oxml.ns.nsmap)
blip = gd.xpath(
"pic:pic/pic:blipFill/a:blip", namespaces=docx.oxml.ns.nsmap
)[0]
dest = blip.xpath("@r:embed", namespaces=docx.oxml.ns.nsmap)
if len(dest) > 0:
rel = dest[0]
else:
@ -707,24 +806,29 @@ class DocxTemplate(object):
else:
continue
non_visual_properties = 'pic:pic/pic:nvPicPr/pic:cNvPr/'
filename = gd.xpath('%s@name' % non_visual_properties,
namespaces=docx.oxml.ns.nsmap)[0]
titles = gd.xpath('%s@title' % non_visual_properties,
namespaces=docx.oxml.ns.nsmap)
non_visual_properties = "pic:pic/pic:nvPicPr/pic:cNvPr/"
filename = gd.xpath(
"%s@name" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
)[0]
titles = gd.xpath(
"%s@title" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
)
if titles:
title = titles[0]
else:
title = ""
descriptions = gd.xpath('%s@descr' % non_visual_properties,
namespaces=docx.oxml.ns.nsmap)
descriptions = gd.xpath(
"%s@descr" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
)
if descriptions:
description = descriptions[0]
else:
description = ""
part_map[filename] = (doc_part.rels[rel].target_ref,
doc_part.rels[rel].target_part)
part_map[filename] = (
doc_part.rels[rel].target_ref,
doc_part.rels[rel].target_part,
)
# replace data
for img_id, img_data in six.iteritems(self.pics_to_replace):
@ -741,8 +845,7 @@ class DocxTemplate(object):
def build_url_id(self, url):
self.init_docx()
return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK,
is_external=True)
return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK, is_external=True)
def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> None:
# case where save() is called without doing rendering
@ -754,7 +857,9 @@ class DocxTemplate(object):
self.post_processing(filename)
self.is_saved = True
def get_undeclared_template_variables(self, jinja_env: Optional[Environment] = None) -> Set[str]:
def get_undeclared_template_variables(
self, jinja_env: Optional[Environment] = None
) -> Set[str]:
self.init_docx(reload=False)
xml = self.get_xml()
xml = self.patch_xml(xml)

120
poetry.lock generated
View File

@ -14,6 +14,75 @@ files = [
[package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "black"
version = "24.4.2"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "docxcompose"
version = "1.4.0"
@ -306,6 +375,55 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pycodestyle"
version = "2.12.0"
@ -384,4 +502,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "0a19499992b7770bc844b87288ec61c29b194487b3e99437a9004e66d7965ca8"
content-hash = "43818448bde523eafcedcdaeb6541d8205a5d52eef5cb4d0e1a0563a7134a579"

View File

@ -11,6 +11,7 @@ six = "^1.16.0"
python-docx = "^1.1.2"
docxcompose = "^1.4.0"
jinja2 = "^3.1.4"
black = "^24.4.2"
[tool.poetry.group.dev.dependencies]

View File

@ -1,42 +1,42 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/cellbg_tpl.docx')
tpl = DocxTemplate("templates/cellbg_tpl.docx")
context = {
'alerts': [
"alerts": [
{
'date': '2015-03-10',
'desc': RichText('Very critical alert', color='FF0000', bold=True),
'type': 'CRITICAL',
'bg': 'FF0000',
"date": "2015-03-10",
"desc": RichText("Very critical alert", color="FF0000", bold=True),
"type": "CRITICAL",
"bg": "FF0000",
},
{
'date': '2015-03-11',
'desc': RichText('Just a warning'),
'type': 'WARNING',
'bg': 'FFDD00',
"date": "2015-03-11",
"desc": RichText("Just a warning"),
"type": "WARNING",
"bg": "FFDD00",
},
{
'date': '2015-03-12',
'desc': RichText('Information'),
'type': 'INFO',
'bg': '8888FF',
"date": "2015-03-12",
"desc": RichText("Information"),
"type": "INFO",
"bg": "8888FF",
},
{
'date': '2015-03-13',
'desc': RichText('Debug trace'),
'type': 'DEBUG',
'bg': 'FF00FF',
"date": "2015-03-13",
"desc": RichText("Debug trace"),
"type": "DEBUG",
"bg": "FF00FF",
},
],
}
tpl.render(context)
tpl.save('output/cellbg.docx')
tpl.save("output/cellbg.docx")

View File

@ -1,6 +1,6 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/comments_tpl.docx')
tpl = DocxTemplate("templates/comments_tpl.docx")
tpl.render({})
tpl.save('output/comments.docx')
tpl.save("output/comments.docx")

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: sandeeprah, Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
import jinja2
@ -14,7 +14,7 @@ jinja_env = jinja2.Environment()
# to create new filters, first create functions that accept the value to filter
# as first argument, and filter parameters as next arguments
def my_filterA(value, my_string_arg):
return_value = value + ' ' + my_string_arg
return_value = value + " " + my_string_arg
return return_value
@ -24,12 +24,12 @@ def my_filterB(value, my_float_arg):
# Then, declare them to jinja like this :
jinja_env.filters['my_filterA'] = my_filterA
jinja_env.filters['my_filterB'] = my_filterB
jinja_env.filters["my_filterA"] = my_filterA
jinja_env.filters["my_filterB"] = my_filterB
context = {'base_value_string': ' Hello', 'base_value_float': 1.5}
context = {"base_value_string": " Hello", "base_value_float": 1.5}
tpl = DocxTemplate('templates/custom_jinja_filters_tpl.docx')
tpl = DocxTemplate("templates/custom_jinja_filters_tpl.docx")
tpl.render(context, jinja_env)
tpl.save('output/custom_jinja_filters.docx')
tpl.save("output/custom_jinja_filters.docx")

View File

@ -1,12 +1,10 @@
from docxtpl import DocxTemplate
doctemplate = r'templates/doc_properties_tpl.docx'
doctemplate = r"templates/doc_properties_tpl.docx"
tpl = DocxTemplate(doctemplate)
context = {
'test': 'HelloWorld'
}
context = {"test": "HelloWorld"}
tpl.render(context)
tpl.save("output/doc_properties.docx")

View File

@ -1,15 +1,15 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/dynamic_table_tpl.docx')
tpl = DocxTemplate("templates/dynamic_table_tpl.docx")
context = {
'col_labels': ['fruit', 'vegetable', 'stone', 'thing'],
'tbl_contents': [
{'label': 'yellow', 'cols': ['banana', 'capsicum', 'pyrite', 'taxi']},
{'label': 'red', 'cols': ['apple', 'tomato', 'cinnabar', 'doubledecker']},
{'label': 'green', 'cols': ['guava', 'cucumber', 'aventurine', 'card']},
"col_labels": ["fruit", "vegetable", "stone", "thing"],
"tbl_contents": [
{"label": "yellow", "cols": ["banana", "capsicum", "pyrite", "taxi"]},
{"label": "red", "cols": ["apple", "tomato", "cinnabar", "doubledecker"]},
{"label": "green", "cols": ["guava", "cucumber", "aventurine", "card"]},
],
}
tpl.render(context)
tpl.save('output/dynamic_table.docx')
tpl.save("output/dynamic_table.docx")

View File

@ -1,45 +1,45 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-09-09
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
# rendering the "dynamic embedded docx":
embedded_docx_tpl = DocxTemplate('templates/embedded_embedded_docx_tpl.docx')
embedded_docx_tpl = DocxTemplate("templates/embedded_embedded_docx_tpl.docx")
context = {
'name': 'John Doe',
"name": "John Doe",
}
embedded_docx_tpl.render(context)
embedded_docx_tpl.save('output/embedded_embedded_docx.docx')
embedded_docx_tpl.save("output/embedded_embedded_docx.docx")
# rendering the main document :
tpl = DocxTemplate('templates/embedded_main_tpl.docx')
tpl = DocxTemplate("templates/embedded_main_tpl.docx")
context = {
'name': 'John Doe',
"name": "John Doe",
}
tpl.replace_embedded(
'templates/embedded_dummy.docx', 'templates/embedded_static_docx.docx'
"templates/embedded_dummy.docx", "templates/embedded_static_docx.docx"
)
tpl.replace_embedded(
'templates/embedded_dummy2.docx', 'output/embedded_embedded_docx.docx'
"templates/embedded_dummy2.docx", "output/embedded_embedded_docx.docx"
)
# The zipname is the one you can find when you open docx with WinZip, 7zip (Windows)
# or unzip -l (Linux). The zipname starts with "word/embeddings/".
# Note that the file is renamed by MSWord, so you have to guess a little bit...
tpl.replace_zipname(
'word/embeddings/Feuille_Microsoft_Office_Excel3.xlsx', 'templates/real_Excel.xlsx'
"word/embeddings/Feuille_Microsoft_Office_Excel3.xlsx", "templates/real_Excel.xlsx"
)
tpl.replace_zipname(
'word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx',
'templates/real_PowerPoint.pptx',
"word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx",
"templates/real_PowerPoint.pptx",
)
tpl.render(context)
tpl.save('output/embedded.docx')
tpl.save("output/embedded.docx")

View File

@ -1,19 +1,19 @@
from docxtpl import DocxTemplate, R, Listing
tpl = DocxTemplate('templates/escape_tpl.docx')
tpl = DocxTemplate("templates/escape_tpl.docx")
context = {
'myvar': R(
"myvar": R(
'"less than" must be escaped : <, this can be done with RichText() or R()'
),
'myescvar': 'It can be escaped with a "|e" jinja filter in the template too : < ',
'nlnp': R('Here is a multiple\nlines\nstring\aand some\aother\aparagraphs',
color='#ff00ff'),
'mylisting': Listing(
'the listing\nwith\nsome\nlines\nand special chars : <>& ...'
"myescvar": 'It can be escaped with a "|e" jinja filter in the template too : < ',
"nlnp": R(
"Here is a multiple\nlines\nstring\aand some\aother\aparagraphs",
color="#ff00ff",
),
'page_break': R('\f'),
'new_listing': """
"mylisting": Listing("the listing\nwith\nsome\nlines\nand special chars : <>& ..."),
"page_break": R("\f"),
"new_listing": """
This is a new listing
Now, does not require Listing() Object
Here is a \t tab\a
@ -21,34 +21,34 @@ Here is a new paragraph\a
Here is a page break : \f
That's it
""",
'some_html': (
'HTTP/1.1 200 OK\n'
'Server: Apache-Coyote/1.1\n'
'Cache-Control: no-store\n'
'Expires: Thu, 01 Jan 1970 00:00:00 GMT\n'
'Pragma: no-cache\n'
'Content-Type: text/html;charset=UTF-8\n'
'Content-Language: zh-CN\n'
'Date: Thu, 22 Oct 2020 10:59:40 GMT\n'
'Content-Length: 9866\n'
'\n'
'<html>\n'
'<head>\n'
' <title>Struts Problem Report</title>\n'
' <style>\n'
' \tpre {\n'
'\t \tmargin: 0;\n'
'\t padding: 0;\n'
'\t } '
'\n'
' </style>\n'
'</head>\n'
'<body>\n'
'...\n'
'</body>\n'
'</html>'
"some_html": (
"HTTP/1.1 200 OK\n"
"Server: Apache-Coyote/1.1\n"
"Cache-Control: no-store\n"
"Expires: Thu, 01 Jan 1970 00:00:00 GMT\n"
"Pragma: no-cache\n"
"Content-Type: text/html;charset=UTF-8\n"
"Content-Language: zh-CN\n"
"Date: Thu, 22 Oct 2020 10:59:40 GMT\n"
"Content-Length: 9866\n"
"\n"
"<html>\n"
"<head>\n"
" <title>Struts Problem Report</title>\n"
" <style>\n"
" \tpre {\n"
"\t \tmargin: 0;\n"
"\t padding: 0;\n"
"\t } "
"\n"
" </style>\n"
"</head>\n"
"<body>\n"
"...\n"
"</body>\n"
"</html>"
),
}
tpl.render(context)
tpl.save('output/escape.docx')
tpl.save("output/escape.docx")

View File

@ -12,18 +12,18 @@ from docxtpl import DocxTemplate
XML_RESERVED = """<"&'>"""
tpl = DocxTemplate('templates/escape_tpl_auto.docx')
tpl = DocxTemplate("templates/escape_tpl_auto.docx")
context = {
'nested_dict': {name(text_type(c)): c for c in XML_RESERVED},
'autoescape': 'Escaped "str & ing"!',
'autoescape_unicode': u'This is an escaped <unicode> example \u4f60 & \u6211',
'iteritems': iteritems,
"nested_dict": {name(text_type(c)): c for c in XML_RESERVED},
"autoescape": 'Escaped "str & ing"!',
"autoescape_unicode": "This is an escaped <unicode> example \u4f60 & \u6211",
"iteritems": iteritems,
}
tpl.render(context, autoescape=True)
OUTPUT = 'output'
OUTPUT = "output"
if not os.path.exists(OUTPUT):
os.makedirs(OUTPUT)
tpl.save(OUTPUT + '/escape_auto.docx')
tpl.save(OUTPUT + "/escape_auto.docx")

View File

@ -1,25 +1,25 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/header_footer_tpl.docx')
tpl = DocxTemplate("templates/header_footer_tpl.docx")
sd = tpl.new_subdoc()
p = sd.add_paragraph(
'This is a sub-document to check it does not break header and footer'
"This is a sub-document to check it does not break header and footer"
)
context = {
'title': 'Header and footer test',
'company_name': 'The World Wide company',
'date': '2016-03-17',
'mysubdoc': sd,
"title": "Header and footer test",
"company_name": "The World Wide company",
"date": "2016-03-17",
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('output/header_footer.docx')
tpl.save("output/header_footer.docx")

View File

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/header_footer_entities_tpl.docx')
tpl = DocxTemplate("templates/header_footer_entities_tpl.docx")
context = {
'title': 'Header and footer test',
"title": "Header and footer test",
}
tpl.render(context)
tpl.save('output/header_footer_entities.docx')
tpl.save("output/header_footer_entities.docx")

View File

@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-09-03
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
DEST_FILE = 'output/header_footer_image.docx'
DEST_FILE = "output/header_footer_image.docx"
tpl = DocxTemplate('templates/header_footer_image_tpl.docx')
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
context = {
'mycompany': 'The World Wide company',
"mycompany": "The World Wide company",
}
tpl.replace_media('templates/dummy_pic_for_header.png', 'templates/python.png')
tpl.replace_media("templates/dummy_pic_for_header.png", "templates/python.png")
tpl.render(context)
tpl.save(DEST_FILE)

View File

@ -1,29 +1,29 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2019-05-22
@author: Eric Dufresne
'''
"""
from docxtpl import DocxTemplate
import io
DEST_FILE = 'output/header_footer_image_file_obj.docx'
DEST_FILE2 = 'output/header_footer_image_file_obj2.docx'
DEST_FILE = "output/header_footer_image_file_obj.docx"
DEST_FILE2 = "output/header_footer_image_file_obj2.docx"
tpl = DocxTemplate('templates/header_footer_image_tpl.docx')
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
context = {
'mycompany': 'The World Wide company',
"mycompany": "The World Wide company",
}
dummy_pic = io.BytesIO(open('templates/dummy_pic_for_header.png', 'rb').read())
new_image = io.BytesIO(open('templates/python.png', 'rb').read())
dummy_pic = io.BytesIO(open("templates/dummy_pic_for_header.png", "rb").read())
new_image = io.BytesIO(open("templates/python.png", "rb").read())
tpl.replace_media(dummy_pic, new_image)
tpl.render(context)
tpl.save(DEST_FILE)
tpl = DocxTemplate('templates/header_footer_image_tpl.docx')
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
dummy_pic.seek(0)
new_image.seek(0)
tpl.replace_media(dummy_pic, new_image)
@ -32,5 +32,5 @@ tpl.render(context)
file_obj = io.BytesIO()
tpl.save(file_obj)
file_obj.seek(0)
with open(DEST_FILE2, 'wb') as f:
with open(DEST_FILE2, "wb") as f:
f.write(file_obj.read())

View File

@ -1,24 +1,24 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2021-04-06
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, InlineImage
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
from docx.shared import Mm
tpl = DocxTemplate('templates/header_footer_inline_image_tpl.docx')
tpl = DocxTemplate("templates/header_footer_inline_image_tpl.docx")
context = {
'inline_image': InlineImage(tpl, 'templates/django.png', height=Mm(10)),
'images': [
InlineImage(tpl, 'templates/python.png', height=Mm(10)),
InlineImage(tpl, 'templates/python.png', height=Mm(10)),
InlineImage(tpl, 'templates/python.png', height=Mm(10))
]
"inline_image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
"images": [
InlineImage(tpl, "templates/python.png", height=Mm(10)),
InlineImage(tpl, "templates/python.png", height=Mm(10)),
InlineImage(tpl, "templates/python.png", height=Mm(10)),
],
}
tpl.render(context)
tpl.save('output/header_footer_inline_image.docx')
tpl.save("output/header_footer_inline_image.docx")

View File

@ -1,28 +1,28 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2016-07-19
@author: AhnSeongHyun
Edited : 2016-07-19 by Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/header_footer_tpl_utf8.docx')
tpl = DocxTemplate("templates/header_footer_tpl_utf8.docx")
sd = tpl.new_subdoc()
p = sd.add_paragraph(
u'This is a sub-document to check it does not break header and footer with utf-8 '
u'characters inside the template .docx'
"This is a sub-document to check it does not break header and footer with utf-8 "
"characters inside the template .docx"
)
context = {
'title': u'헤더와 푸터',
'company_name': u'세계적 회사',
'date': u'2016-03-17',
'mysubdoc': sd,
"title": "헤더와 푸터",
"company_name": "세계적 회사",
"date": "2016-03-17",
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('output/header_footer_utf8.docx')
tpl.save("output/header_footer_utf8.docx")

View File

@ -2,6 +2,6 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/horizontal_merge_tpl.docx')
tpl = DocxTemplate("templates/horizontal_merge_tpl.docx")
tpl.render({})
tpl.save('output/horizontal_merge.docx')
tpl.save("output/horizontal_merge.docx")

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-01-14
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, InlineImage
@ -11,37 +11,37 @@ from docxtpl import DocxTemplate, InlineImage
from docx.shared import Mm
import jinja2
tpl = DocxTemplate('templates/inline_image_tpl.docx')
tpl = DocxTemplate("templates/inline_image_tpl.docx")
context = {
'myimage': InlineImage(tpl, 'templates/python_logo.png', width=Mm(20)),
'myimageratio': InlineImage(
tpl, 'templates/python_jpeg.jpg', width=Mm(30), height=Mm(60)
"myimage": InlineImage(tpl, "templates/python_logo.png", width=Mm(20)),
"myimageratio": InlineImage(
tpl, "templates/python_jpeg.jpg", width=Mm(30), height=Mm(60)
),
'frameworks': [
"frameworks": [
{
'image': InlineImage(tpl, 'templates/django.png', height=Mm(10)),
'desc': 'The web framework for perfectionists with deadlines',
"image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
"desc": "The web framework for perfectionists with deadlines",
},
{
'image': InlineImage(tpl, 'templates/zope.png', height=Mm(10)),
'desc': 'Zope is a leading Open Source Application Server and Content Management Framework',
"image": InlineImage(tpl, "templates/zope.png", height=Mm(10)),
"desc": "Zope is a leading Open Source Application Server and Content Management Framework",
},
{
'image': InlineImage(tpl, 'templates/pyramid.png', height=Mm(10)),
'desc': 'Pyramid is a lightweight Python web framework aimed at taking small web apps into big web apps.',
"image": InlineImage(tpl, "templates/pyramid.png", height=Mm(10)),
"desc": "Pyramid is a lightweight Python web framework aimed at taking small web apps into big web apps.",
},
{
'image': InlineImage(tpl, 'templates/bottle.png', height=Mm(10)),
'desc': 'Bottle is a fast, simple and lightweight WSGI micro web-framework for Python',
"image": InlineImage(tpl, "templates/bottle.png", height=Mm(10)),
"desc": "Bottle is a fast, simple and lightweight WSGI micro web-framework for Python",
},
{
'image': InlineImage(tpl, 'templates/tornado.png', height=Mm(10)),
'desc': 'Tornado is a Python web framework and asynchronous networking library.',
"image": InlineImage(tpl, "templates/tornado.png", height=Mm(10)),
"desc": "Tornado is a Python web framework and asynchronous networking library.",
},
],
}
# testing that it works also when autoescape has been forced to True
jinja_env = jinja2.Environment(autoescape=True)
tpl.render(context, jinja_env)
tpl.save('output/inline_image.docx')
tpl.save("output/inline_image.docx")

View File

@ -1,5 +1,5 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/less_cells_after_loop_tpl.docx')
tpl = DocxTemplate("templates/less_cells_after_loop_tpl.docx")
tpl.render({})
tpl.save('output/less_cells_after_loop.docx')
tpl.save("output/less_cells_after_loop.docx")

View File

@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2021-07-30
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/merge_docx_master_tpl.docx')
sd = tpl.new_subdoc('templates/merge_docx_subdoc.docx')
tpl = DocxTemplate("templates/merge_docx_master_tpl.docx")
sd = tpl.new_subdoc("templates/merge_docx_subdoc.docx")
context = {
'mysubdoc': sd,
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('output/merge_docx.docx')
tpl.save("output/merge_docx.docx")

View File

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/merge_paragraph_tpl.docx')
tpl = DocxTemplate("templates/merge_paragraph_tpl.docx")
context = {
'living_in_town': True,
"living_in_town": True,
}
tpl.render(context)
tpl.save('output/merge_paragraph.docx')
tpl.save("output/merge_paragraph.docx")

View File

@ -1,19 +1,25 @@
import os
TEMPLATE_PATH = 'templates/module_execute_tpl.docx'
JSON_PATH = 'templates/module_execute.json'
OUTPUT_FILENAME = 'output/module_execute.docx'
OVERWRITE = '-o'
QUIET = '-q'
TEMPLATE_PATH = "templates/module_execute_tpl.docx"
JSON_PATH = "templates/module_execute.json"
OUTPUT_FILENAME = "output/module_execute.docx"
OVERWRITE = "-o"
QUIET = "-q"
if os.path.exists(OUTPUT_FILENAME):
os.unlink(OUTPUT_FILENAME)
os.chdir(os.path.dirname(__file__))
cmd = 'python -m docxtpl %s %s %s %s %s' % (TEMPLATE_PATH, JSON_PATH, OUTPUT_FILENAME, OVERWRITE, QUIET)
cmd = "python -m docxtpl %s %s %s %s %s" % (
TEMPLATE_PATH,
JSON_PATH,
OUTPUT_FILENAME,
OVERWRITE,
QUIET,
)
print('Executing "%s" ...' % cmd)
os.system(cmd)
if os.path.exists(OUTPUT_FILENAME):
print(' --> File %s has been generated.' % OUTPUT_FILENAME)
print(" --> File %s has been generated." % OUTPUT_FILENAME)

View File

@ -1,40 +1,40 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2021-12-20
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/multi_rendering_tpl.docx')
tpl = DocxTemplate("templates/multi_rendering_tpl.docx")
documents_data = [
{
'dest_file': 'multi_render1.docx',
'context': {
'title': 'Title ONE',
'body': 'This is the body for first document'
}
"dest_file": "multi_render1.docx",
"context": {
"title": "Title ONE",
"body": "This is the body for first document",
},
},
{
'dest_file': 'multi_render2.docx',
'context': {
'title': 'Title TWO',
'body': 'This is the body for second document'
}
"dest_file": "multi_render2.docx",
"context": {
"title": "Title TWO",
"body": "This is the body for second document",
},
},
{
'dest_file': 'multi_render3.docx',
'context': {
'title': 'Title THREE',
'body': 'This is the body for third document'
}
"dest_file": "multi_render3.docx",
"context": {
"title": "Title THREE",
"body": "This is the body for third document",
},
},
]
for document_data in documents_data:
dest_file = document_data['dest_file']
context = document_data['context']
dest_file = document_data["dest_file"]
context = document_data["context"]
tpl.render(context)
tpl.save('output/%s' % dest_file)
tpl.save("output/%s" % dest_file)

View File

@ -1,45 +1,45 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2016-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/nested_for_tpl.docx')
tpl = DocxTemplate("templates/nested_for_tpl.docx")
context = {
'dishes': [
{'name': 'Pizza', 'ingredients': ['bread', 'tomato', 'ham', 'cheese']},
"dishes": [
{"name": "Pizza", "ingredients": ["bread", "tomato", "ham", "cheese"]},
{
'name': 'Hamburger',
'ingredients': ['bread', 'chopped steak', 'cheese', 'sauce'],
"name": "Hamburger",
"ingredients": ["bread", "chopped steak", "cheese", "sauce"],
},
{
'name': 'Apple pie',
'ingredients': ['flour', 'apples', 'suggar', 'quince jelly'],
"name": "Apple pie",
"ingredients": ["flour", "apples", "suggar", "quince jelly"],
},
],
'authors': [
"authors": [
{
'name': 'Saint-Exupery',
'books': [
{'title': 'Le petit prince'},
{'title': "L'aviateur"},
{'title': 'Vol de nuit'},
"name": "Saint-Exupery",
"books": [
{"title": "Le petit prince"},
{"title": "L'aviateur"},
{"title": "Vol de nuit"},
],
},
{
'name': 'Barjavel',
'books': [
{'title': 'Ravage'},
{'title': "La nuit des temps"},
{'title': 'Le grand secret'},
"name": "Barjavel",
"books": [
{"title": "Ravage"},
{"title": "La nuit des temps"},
{"title": "Le grand secret"},
],
},
],
}
tpl.render(context)
tpl.save('output/nested_for.docx')
tpl.save("output/nested_for.docx")

View File

@ -1,26 +1,26 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/order_tpl.docx')
tpl = DocxTemplate("templates/order_tpl.docx")
context = {
'customer_name': 'Eric',
'items': [
{'desc': 'Python interpreters', 'qty': 2, 'price': 'FREE'},
{'desc': 'Django projects', 'qty': 5403, 'price': 'FREE'},
{'desc': 'Guido', 'qty': 1, 'price': '100,000,000.00'},
"customer_name": "Eric",
"items": [
{"desc": "Python interpreters", "qty": 2, "price": "FREE"},
{"desc": "Django projects", "qty": 5403, "price": "FREE"},
{"desc": "Guido", "qty": 1, "price": "100,000,000.00"},
],
'in_europe': True,
'is_paid': False,
'company_name': 'The World Wide company',
'total_price': '100,000,000.00',
"in_europe": True,
"is_paid": False,
"company_name": "The World Wide company",
"total_price": "100,000,000.00",
}
tpl.render(context)
tpl.save('output/order.docx')
tpl.save("output/order.docx")

View File

@ -3,12 +3,12 @@ from docxtpl import DocxTemplate
# With old docxtpl version, "... for spicy ..." was replaced by "... forspicy..."
# This test is for checking that is some cases the spaces are not lost anymore
tpl = DocxTemplate('templates/preserve_spaces_tpl.docx')
tpl = DocxTemplate("templates/preserve_spaces_tpl.docx")
tags = ['tag_1', 'tag_2']
replacement = ['looking', 'too']
tags = ["tag_1", "tag_2"]
replacement = ["looking", "too"]
context = dict(zip(tags, replacement))
tpl.render(context)
tpl.save('output/preserve_spaces.docx')
tpl.save("output/preserve_spaces.docx")

View File

@ -1,18 +1,18 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-09-03
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
DEST_FILE = 'output/replace_picture.docx'
DEST_FILE = "output/replace_picture.docx"
tpl = DocxTemplate('templates/replace_picture_tpl.docx')
tpl = DocxTemplate("templates/replace_picture_tpl.docx")
context = {}
tpl.replace_pic('python_logo.png', 'templates/python.png')
tpl.replace_pic("python_logo.png", "templates/python.png")
tpl.render(context)
tpl.save(DEST_FILE)

View File

@ -1,55 +1,64 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/richtext_tpl.docx')
tpl = DocxTemplate("templates/richtext_tpl.docx")
rt = RichText()
rt.add('a rich text', style='myrichtextstyle')
rt.add(' with ')
rt.add('some italic', italic=True)
rt.add(' and ')
rt.add('some violet', color='#ff00ff')
rt.add(' and ')
rt.add('some striked', strike=True)
rt.add(' and ')
rt.add('some Highlighted', highlight='#ffff00')
rt.add(' and ')
rt.add('some small', size=14)
rt.add(' or ')
rt.add('big', size=60)
rt.add(' text.')
rt.add('\nYou can add an hyperlink, here to ')
rt.add('google', url_id=tpl.build_url_id('http://google.com'))
rt.add('\nEt voilà ! ')
rt.add('\n1st line')
rt.add('\n2nd line')
rt.add('\n3rd line')
rt.add('\aA new paragraph : <cool>\a')
rt.add('--- A page break here (see next page) ---\f')
rt.add("a rich text", style="myrichtextstyle")
rt.add(" with ")
rt.add("some italic", italic=True)
rt.add(" and ")
rt.add("some violet", color="#ff00ff")
rt.add(" and ")
rt.add("some striked", strike=True)
rt.add(" and ")
rt.add("some Highlighted", highlight="#ffff00")
rt.add(" and ")
rt.add("some small", size=14)
rt.add(" or ")
rt.add("big", size=60)
rt.add(" text.")
rt.add("\nYou can add an hyperlink, here to ")
rt.add("google", url_id=tpl.build_url_id("http://google.com"))
rt.add("\nEt voilà ! ")
rt.add("\n1st line")
rt.add("\n2nd line")
rt.add("\n3rd line")
rt.add("\aA new paragraph : <cool>\a")
rt.add("--- A page break here (see next page) ---\f")
for ul in ['single', 'double', 'thick', 'dotted', 'dash', 'dotDash', 'dotDotDash', 'wave']:
rt.add('\nUnderline : ' + ul + ' \n', underline=ul)
rt.add('\nFonts :\n', underline=True)
rt.add('Arial\n', font='Arial')
rt.add('Courier New\n', font='Courier New')
rt.add('Times New Roman\n', font='Times New Roman')
rt.add('\n\nHere some')
rt.add('superscript', superscript=True)
rt.add(' and some')
rt.add('subscript', subscript=True)
for ul in [
"single",
"double",
"thick",
"dotted",
"dash",
"dotDash",
"dotDotDash",
"wave",
]:
rt.add("\nUnderline : " + ul + " \n", underline=ul)
rt.add("\nFonts :\n", underline=True)
rt.add("Arial\n", font="Arial")
rt.add("Courier New\n", font="Courier New")
rt.add("Times New Roman\n", font="Times New Roman")
rt.add("\n\nHere some")
rt.add("superscript", superscript=True)
rt.add(" and some")
rt.add("subscript", subscript=True)
rt_embedded = RichText('an example of ')
rt_embedded = RichText("an example of ")
rt_embedded.add(rt)
context = {
'example': rt_embedded,
"example": rt_embedded,
}
tpl.render(context)
tpl.save('output/richtext.docx')
tpl.save("output/richtext.docx")

View File

@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/richtext_and_if_tpl.docx')
tpl = DocxTemplate("templates/richtext_and_if_tpl.docx")
context = {'foobar': RichText('Foobar!', color='ff0000')}
context = {"foobar": RichText("Foobar!", color="ff0000")}
tpl.render(context)
tpl.save('output/richtext_and_if.docx')
tpl.save("output/richtext_and_if.docx")

View File

@ -6,15 +6,16 @@ Created : 2022-08-03
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/richtext_eastAsia_tpl.docx')
rt = RichText('测试TEST', font='eastAsia:Microsoft YaHei')
ch = RichText('测试TEST', font='eastAsia:微软雅黑')
sun = RichText('测试TEST', font='eastAsia:SimSun')
tpl = DocxTemplate("templates/richtext_eastAsia_tpl.docx")
rt = RichText("测试TEST", font="eastAsia:Microsoft YaHei")
ch = RichText("测试TEST", font="eastAsia:微软雅黑")
sun = RichText("测试TEST", font="eastAsia:SimSun")
context = {
'example': rt,
'Chinese': ch,
'simsun': sun,
"example": rt,
"Chinese": ch,
"simsun": sun,
}
tpl.render(context)
tpl.save('output/richtext_eastAsia.docx')
tpl.save("output/richtext_eastAsia.docx")

View File

@ -3,16 +3,16 @@ import glob
import six
import os
tests = sorted(glob.glob('[A-Za-z]*.py'))
excludes = ['runtests.py']
tests = sorted(glob.glob("[A-Za-z]*.py"))
excludes = ["runtests.py"]
output_dir = os.path.join(os.path.dirname(__file__), 'output')
output_dir = os.path.join(os.path.dirname(__file__), "output")
if not os.path.exists(output_dir):
os.mkdir(output_dir)
for test in tests:
if test not in excludes:
six.print_('%s ...' % test)
subprocess.call(['python', './%s' % test])
six.print_("%s ..." % test)
subprocess.call(["python", "./%s" % test])
six.print_('Done.')
six.print_("Done.")

View File

@ -1,36 +1,36 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
from docx.shared import Inches
tpl = DocxTemplate('templates/subdoc_tpl.docx')
tpl = DocxTemplate("templates/subdoc_tpl.docx")
sd = tpl.new_subdoc()
p = sd.add_paragraph('This is a sub-document inserted into a bigger one')
p = sd.add_paragraph('It has been ')
p.add_run('dynamically').style = 'dynamic'
p.add_run(' generated with python by using ')
p.add_run('python-docx').italic = True
p.add_run(' library')
p = sd.add_paragraph("This is a sub-document inserted into a bigger one")
p = sd.add_paragraph("It has been ")
p.add_run("dynamically").style = "dynamic"
p.add_run(" generated with python by using ")
p.add_run("python-docx").italic = True
p.add_run(" library")
sd.add_heading('Heading, level 1', level=1)
sd.add_paragraph('This is an Intense quote', style='IntenseQuote')
sd.add_heading("Heading, level 1", level=1)
sd.add_paragraph("This is an Intense quote", style="IntenseQuote")
sd.add_paragraph('A picture :')
sd.add_picture('templates/python_logo.png', width=Inches(1.25))
sd.add_paragraph("A picture :")
sd.add_picture("templates/python_logo.png", width=Inches(1.25))
sd.add_paragraph('A Table :')
sd.add_paragraph("A Table :")
table = sd.add_table(rows=1, cols=3)
hdr_cells = table.rows[0].cells
hdr_cells[0].text = 'Qty'
hdr_cells[1].text = 'Id'
hdr_cells[2].text = 'Desc'
recordset = ((1, 101, 'Spam'), (2, 42, 'Eggs'), (3, 631, 'Spam,spam, eggs, and ham'))
hdr_cells[0].text = "Qty"
hdr_cells[1].text = "Id"
hdr_cells[2].text = "Desc"
recordset = ((1, 101, "Spam"), (2, 42, "Eggs"), (3, 631, "Spam,spam, eggs, and ham"))
for item in recordset:
row_cells = table.add_row().cells
row_cells[0].text = str(item[0])
@ -38,8 +38,8 @@ for item in recordset:
row_cells[2].text = item[2]
context = {
'mysubdoc': sd,
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('output/subdoc.docx')
tpl.save("output/subdoc.docx")

View File

@ -2,19 +2,19 @@ from docxtpl import DocxTemplate
from jinja2.exceptions import TemplateError
import six
six.print_('=' * 80)
six.print_("=" * 80)
six.print_("Generating template error for testing (so it is safe to ignore) :")
six.print_('.' * 80)
six.print_("." * 80)
try:
tpl = DocxTemplate('templates/template_error_tpl.docx')
tpl.render({'test_variable': 'test variable value'})
tpl = DocxTemplate("templates/template_error_tpl.docx")
tpl.render({"test_variable": "test variable value"})
except TemplateError as the_error:
six.print_(six.text_type(the_error))
if hasattr(the_error, 'docx_context'):
if hasattr(the_error, "docx_context"):
six.print_("Context:")
for line in the_error.docx_context:
six.print_(line)
tpl.save('output/template_error.docx')
six.print_('.' * 80)
tpl.save("output/template_error.docx")
six.print_("." * 80)
six.print_(" End of TemplateError Test ")
six.print_('=' * 80)
six.print_("=" * 80)

View File

@ -1,23 +1,23 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-10-15
@author: Arthaslixin
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/vertical_merge_tpl.docx')
tpl = DocxTemplate("templates/vertical_merge_tpl.docx")
context = {
'items': [
{'desc': 'Python interpreters', 'qty': 2, 'price': 'FREE'},
{'desc': 'Django projects', 'qty': 5403, 'price': 'FREE'},
{'desc': 'Guido', 'qty': 1, 'price': '100,000,000.00'},
"items": [
{"desc": "Python interpreters", "qty": 2, "price": "FREE"},
{"desc": "Django projects", "qty": 5403, "price": "FREE"},
{"desc": "Guido", "qty": 1, "price": "100,000,000.00"},
],
'total_price': '100,000,000.00',
'category': 'Book',
"total_price": "100,000,000.00",
"category": "Book",
}
tpl.render(context)
tpl.save('output/vertical_merge.docx')
tpl.save("output/vertical_merge.docx")

View File

@ -1,5 +1,5 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/vertical_merge_nested_tpl.docx')
tpl = DocxTemplate("templates/vertical_merge_nested_tpl.docx")
tpl.render({})
tpl.save('output/vertical_merge_nested.docx')
tpl.save("output/vertical_merge_nested.docx")

View File

@ -1,12 +1,12 @@
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/word2016_tpl.docx')
tpl = DocxTemplate("templates/word2016_tpl.docx")
tpl.render(
{
'test_space': ' ',
'test_tabs': 5 * '\t',
'test_space_r': RichText(' '),
'test_tabs_r': RichText(5 * '\t'),
"test_space": " ",
"test_tabs": 5 * "\t",
"test_space_r": RichText(" "),
"test_tabs_r": RichText(5 * "\t"),
}
)
tpl.save('output/word2016.docx')
tpl.save("output/word2016.docx")