├── .gitignore ├── LICENSE ├── Makefile ├── README.rst ├── VERSION ├── docs └── static │ └── images │ ├── sample_input.png │ └── sample_output.png ├── pptx_templater ├── __init__.py ├── core.py ├── file_handler.py ├── slides.py └── text_parser.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── fixtures │ └── test_presentation_layout.pptx ├── test_conversion.py ├── test_outputs │ └── PLACEHOLDER └── test_parser.py └── versioning.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *egg-info 2 | .idea/ 3 | .pytest_cache/ 4 | __pycache__/ 5 | build/ 6 | dist/ 7 | venv/ 8 | 9 | # pptx 10 | .~lock* 11 | *updated.pptx 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kwlo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | upload: 2 | rm -rf build/ dist/ 3 | ./versioning.sh 4 | mv VERSION.new VERSION 5 | python3 setup.py sdist bdist_wheel 6 | twine upload dist/* 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-pptx-templater 2 | ===================== 3 | 4 | python-pptx-templater is a tool to create highly customizable PowerPoint presentation using the jinja template languages. 5 | User specifies the layouts and placeholders and the template will render the presentation. 6 | 7 | Example 8 | ------- 9 | 10 | Input 11 | 12 | .. image:: https://raw.githubusercontent.com/kwlo/python-pptx-templater/master/docs/static/images/sample_input.png 13 | 14 | Using Template JSON: 15 | 16 | .. code-block:: text 17 | 18 | { 19 | 'slides': [ 20 | { 21 | 'layoutSlideNum': 0, 22 | 'text': { 23 | 'name': 'Paul' 24 | } 25 | }, 26 | { 27 | 'layoutSlideNum': 0, 28 | 'text': { 29 | 'name': 'Joe' 30 | } 31 | }, 32 | { 33 | 'layoutSlideNum': 1, 34 | 'text': { 35 | 'dog': { 36 | 'name': 'John Cena' 37 | } 38 | } 39 | }, 40 | ] 41 | } 42 | 43 | Output 44 | 45 | .. image:: https://raw.githubusercontent.com/kwlo/python-pptx-templater/master/docs/static/images/sample_output.png 46 | 47 | Install 48 | ------- 49 | 50 | .. code-block:: text 51 | 52 | pip install python-pptx-templater 53 | 54 | 55 | Usage 56 | ----- 57 | 58 | .. code-block:: text 59 | 60 | from pptx_templater.core import convert 61 | 62 | 63 | def test_conversion(): 64 | currpwd = os.path.dirname(os.path.abspath(__file__)) 65 | srcpath = f'{currpwd}/fixtures/test_presentation_layout.pptx' 66 | destpath = f'{currpwd}/test_outputs/updated.pptx' 67 | 68 | j = { 69 | 'slides': [ 70 | { 71 | 'layoutSlideNum': 0, 72 | 'text': { 73 | 'name': 'Paul' 74 | } 75 | }, 76 | { 77 | 'layoutSlideNum': 0, 78 | 'text': { 79 | 'name': 'Joe' 80 | } 81 | }, 82 | { 83 | 'layoutSlideNum': 1, 84 | 'text': { 85 | 'dog': { 86 | 'name': 'John Cena' 87 | } 88 | } 89 | }, 90 | ] 91 | } 92 | 93 | convert(srcpath, destpath, j) 94 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.15 2 | -------------------------------------------------------------------------------- /docs/static/images/sample_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwlo/python-pptx-templater/e23c83f8712edec642faab06709c3fcc4c0dfbd0/docs/static/images/sample_input.png -------------------------------------------------------------------------------- /docs/static/images/sample_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwlo/python-pptx-templater/e23c83f8712edec642faab06709c3fcc4c0dfbd0/docs/static/images/sample_output.png -------------------------------------------------------------------------------- /pptx_templater/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | name = 'python-pptx-templater' 4 | -------------------------------------------------------------------------------- /pptx_templater/core.py: -------------------------------------------------------------------------------- 1 | from pptx_templater.file_handler import create 2 | from pptx_templater.slides import duplicate_slide, update_text 3 | 4 | 5 | def _parse_request(source, target, jsonbody): 6 | for slide in jsonbody.get('slides', []): 7 | duplicate_slide(source, target, slide['layoutSlideNum']) 8 | update_text(target.slides[-1], **slide['text']) 9 | 10 | 11 | def convert(srcpath, destpath, jsonbody): 12 | source, target = create(srcpath) 13 | 14 | _parse_request(source, target, jsonbody) 15 | 16 | target.save(destpath) 17 | -------------------------------------------------------------------------------- /pptx_templater/file_handler.py: -------------------------------------------------------------------------------- 1 | from pptx import Presentation 2 | 3 | from pptx_templater.slides import clear 4 | 5 | 6 | def create(srcpath): 7 | with open(srcpath, 'rb') as f: 8 | source = Presentation(f) 9 | dest = Presentation(f) 10 | 11 | # Clear all slides in destination 12 | clear(dest) 13 | 14 | return source, dest 15 | -------------------------------------------------------------------------------- /pptx_templater/slides.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | # from pptx.parts.chart import ChartPart 4 | # from pptx.parts.embeddedpackage import EmbeddedXlsxPart 5 | from pptx_templater.text_parser import render 6 | 7 | 8 | def update_text(slide, **kwargs): 9 | for shape in slide.shapes: 10 | if not shape.has_text_frame: 11 | continue 12 | for paragraph in shape.text_frame.paragraphs: 13 | for run in paragraph.runs: 14 | run.text = render(run.text, **kwargs) 15 | 16 | 17 | def clear(presentation): 18 | slides = reversed([(idx, sld.rId) for idx, sld in enumerate(presentation.slides._sldIdLst)]) 19 | 20 | for (idx, rId) in slides: 21 | presentation.part.drop_rel(rId) 22 | del presentation.slides._sldIdLst[idx] 23 | 24 | 25 | def _get_blank_slide_layout(pres): 26 | layout_items_count = [len(layout.placeholders) 27 | for layout in pres.slide_layouts] 28 | min_items = min(layout_items_count) 29 | blank_layout_id = layout_items_count.index(min_items) 30 | return pres.slide_layouts[blank_layout_id] 31 | 32 | 33 | def duplicate_slide(source, target, index): 34 | """Duplicate the slide with the given index in pres. 35 | 36 | Adds slide to the end of the presentation""" 37 | source_slide = source.slides[index] 38 | blank_slide_layout = _get_blank_slide_layout(target) 39 | dest = target.slides.add_slide(blank_slide_layout) 40 | 41 | for shape in source_slide.shapes: 42 | newel = deepcopy(shape.element) 43 | dest.shapes._spTree.insert_element_before(newel, 'p:extLst') 44 | ''' 45 | for key, value in source_slide.part.rels.items(): 46 | # Make sure we don't copy a notesSlide relation as that won't exist 47 | if "notesSlide" not in value.reltype: 48 | _target = value._target 49 | # if the relationship was a chart, we need to duplicate the embedded chart part and xlsx 50 | if "chart" in value.reltype: 51 | partname = _target.package.next_partname( 52 | ChartPart.partname_template) 53 | xlsx_blob = _target.chart_workbook.xlsx_part.blob 54 | _target = ChartPart(partname, _target.content_type, 55 | deepcopy(_target._element), package=_target.package) 56 | 57 | _target.chart_workbook.xlsx_part = EmbeddedXlsxPart.new( 58 | xlsx_blob, _target.package) 59 | 60 | dest.part.rels.add_relationship(value.reltype, 61 | _target, 62 | value.rId) 63 | ''' -------------------------------------------------------------------------------- /pptx_templater/text_parser.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Template 2 | 3 | 4 | def render(text, **kwargs): 5 | template = Template(text) 6 | return template.render(**kwargs) 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2==2.10.3 2 | python-pptx==0.6.18 3 | 4 | # dev-dependencies 5 | pytest==5.2.2 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | with io.open("README.rst") as f: 7 | readme = f.read() 8 | 9 | with io.open("VERSION", "rt", encoding="utf8") as f: 10 | version = f.read() 11 | 12 | setup( 13 | name="python-pptx-templater", 14 | version=version, 15 | url="", 16 | project_urls={ 17 | "Documentation": "https://github.com/kwlo/python-pptx-templater", 18 | "Code": "https://github.com/kwlo/python-pptx-templater", 19 | "Issue tracker": "https://github.com/kwlo/python-pptx-templater/issues", 20 | }, 21 | license="MIT License", 22 | author="kwlo", 23 | author_email="kwlo@github.com", 24 | maintainer="kwlo", 25 | maintainer_email="kwlo@github.com", 26 | description="Create customizable PowerPoint Presentation (.pptx) using a predefined layout template", 27 | long_description=readme, 28 | long_description_content_type="text/x-rst", 29 | classifiers=[ 30 | "Development Status :: 5 - Production/Stable", 31 | "Environment :: Console", 32 | "Environment :: Web Environment", 33 | "Intended Audience :: Developers", 34 | "Operating System :: OS Independent", 35 | "Programming Language :: Python", 36 | "Programming Language :: Python :: 3", 37 | "Programming Language :: Python :: 3.5", 38 | "Programming Language :: Python :: 3.6", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: Implementation :: CPython", 41 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 42 | "Topic :: Software Development :: Libraries :: Python Modules", 43 | "Topic :: Text Processing :: Markup :: HTML", 44 | "Topic :: Office/Business :: Office Suites", 45 | ], 46 | packages=find_packages(), 47 | python_requires="!=2.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", 48 | install_requires=["jinja2>=2.10.3", "python-pptx>=0.6.18"], 49 | ) 50 | -------------------------------------------------------------------------------- /tests/fixtures/test_presentation_layout.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwlo/python-pptx-templater/e23c83f8712edec642faab06709c3fcc4c0dfbd0/tests/fixtures/test_presentation_layout.pptx -------------------------------------------------------------------------------- /tests/test_conversion.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pptx_templater.core import convert 4 | 5 | 6 | def test_conversion(): 7 | currpwd = os.path.dirname(os.path.abspath(__file__)) 8 | srcpath = f'{currpwd}/fixtures/test_presentation_layout.pptx' 9 | destpath = f'{currpwd}/test_outputs/updated.pptx' 10 | 11 | j = { 12 | 'slides': [ 13 | { 14 | 'layoutSlideNum': 0, 15 | 'text': { 16 | 'name': 'Paul' 17 | } 18 | }, 19 | { 20 | 'layoutSlideNum': 0, 21 | 'text': { 22 | 'name': 'Joe' 23 | } 24 | }, 25 | { 26 | 'layoutSlideNum': 1, 27 | 'text': { 28 | 'dog': { 29 | 'name': 'John Cena' 30 | } 31 | } 32 | }, 33 | ] 34 | } 35 | 36 | convert(srcpath, destpath, j) 37 | -------------------------------------------------------------------------------- /tests/test_outputs/PLACEHOLDER: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwlo/python-pptx-templater/e23c83f8712edec642faab06709c3fcc4c0dfbd0/tests/test_outputs/PLACEHOLDER -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | from pptx_templater.text_parser import render 2 | 3 | 4 | def test_render(): 5 | assert render('Hello {{ name }}!', name='John Connor') == 'Hello John Connor!' 6 | assert render('Hello Arnold!', name='Connor') == 'Hello Arnold!' 7 | assert render( 8 | 'Hello {{ nested.name }}!', 9 | nested={'name': 'Sarah Connor'} 10 | ) == 'Hello Sarah Connor!' 11 | assert render( 12 | 'Hello {{ nested.name }}!', 13 | **{ 14 | 'nested': { 15 | 'name': 'T101' 16 | } 17 | }) == 'Hello T101!' 18 | -------------------------------------------------------------------------------- /versioning.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat VERSION | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}' > VERSION.new 4 | --------------------------------------------------------------------------------