├── pdf_reports ├── version.py ├── __init__.py ├── css │ ├── style.css │ └── egf-logo.svg ├── tools.py └── pdf_reports.py ├── docs ├── requirements.txt ├── makehtml.sh ├── favicon.png ├── _static │ ├── images │ │ ├── title.png │ │ ├── geometry2.png │ │ ├── html_logo.png │ │ ├── seigaiha.png │ │ ├── confectionary.png │ │ ├── pw_maze_black.png │ │ ├── pw_maze_dark.png │ │ ├── pw_maze_white.png │ │ └── pw_maze_black (copy).png │ └── css │ │ └── main.css ├── README.md ├── ref.rst ├── examples │ ├── standard_example.rst │ └── with_plots_and_tables.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── screenshot.png ├── examples ├── example_reportwriter │ ├── style.css │ ├── example_reportwriter.pdf │ ├── template.pug │ └── example_reportwriter.py ├── basic_example │ ├── example.pdf │ ├── example.py │ └── example_template.pug ├── example_scss_preload │ ├── example.pdf │ ├── template.pug │ ├── style.scss │ └── example_scss_preload.py └── example_with_plot_and_tables │ ├── with_plots_and_tables.pdf │ ├── with_plots_and_tables.pug │ └── with_plots_and_tables.py ├── tests ├── data │ ├── style.scss │ ├── template_rw.pug │ ├── with_plots_and_tables.pug │ └── example_template.pug ├── test_tools.py └── test_basics.py ├── .github └── workflows │ ├── document.yml │ ├── build.yml │ └── publish.yml ├── pyproject.toml ├── LICENSE ├── pypi-readme.rst ├── .gitignore └── README.rst /pdf_reports/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.9" 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | sphinxcontrib-mermaid 3 | -------------------------------------------------------------------------------- /docs/makehtml.sh: -------------------------------------------------------------------------------- 1 | make html 2 | firefox ../../built_docs/html/index.html 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/screenshot.png -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/favicon.png -------------------------------------------------------------------------------- /examples/example_reportwriter/style.css: -------------------------------------------------------------------------------- 1 | .main-paragraph { 2 | text-align: center; 3 | color: gray; 4 | } -------------------------------------------------------------------------------- /docs/_static/images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/title.png -------------------------------------------------------------------------------- /docs/_static/images/geometry2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/geometry2.png -------------------------------------------------------------------------------- /docs/_static/images/html_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/html_logo.png -------------------------------------------------------------------------------- /docs/_static/images/seigaiha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/seigaiha.png -------------------------------------------------------------------------------- /examples/basic_example/example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/examples/basic_example/example.pdf -------------------------------------------------------------------------------- /docs/_static/images/confectionary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/confectionary.png -------------------------------------------------------------------------------- /docs/_static/images/pw_maze_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/pw_maze_black.png -------------------------------------------------------------------------------- /docs/_static/images/pw_maze_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/pw_maze_dark.png -------------------------------------------------------------------------------- /docs/_static/images/pw_maze_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/pw_maze_white.png -------------------------------------------------------------------------------- /examples/example_scss_preload/example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/examples/example_scss_preload/example.pdf -------------------------------------------------------------------------------- /docs/_static/images/pw_maze_black (copy).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/docs/_static/images/pw_maze_black (copy).png -------------------------------------------------------------------------------- /examples/example_scss_preload/template.pug: -------------------------------------------------------------------------------- 1 | h1 {{ title }} 2 | h3 This is version {{ version }} 3 | 4 | p.main-paragraph This report was generated by {{ my_name }} -------------------------------------------------------------------------------- /tests/data/style.scss: -------------------------------------------------------------------------------- 1 | $primary-color: gray; 2 | 3 | body { 4 | .main-paragraph { 5 | text-align: center; 6 | color: $primary-color; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/data/template_rw.pug: -------------------------------------------------------------------------------- 1 | h1 {{ title }} 2 | h3 This is version {{ version }} 3 | 4 | p.main-paragraph This report was generated by {{ my_name }} at {{ my_organization }} -------------------------------------------------------------------------------- /examples/example_reportwriter/example_reportwriter.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/examples/example_reportwriter/example_reportwriter.pdf -------------------------------------------------------------------------------- /examples/example_reportwriter/template.pug: -------------------------------------------------------------------------------- 1 | h1 {{ title }} 2 | h3 This is version {{ version }} 3 | 4 | p.main-paragraph This report was generated by {{ my_name }} at {{ my_organization }} -------------------------------------------------------------------------------- /examples/example_scss_preload/style.scss: -------------------------------------------------------------------------------- 1 | $primary-color: gray; 2 | 3 | body { 4 | .main-paragraph { 5 | text-align: center; 6 | color: $primary-color; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/example_with_plot_and_tables/with_plots_and_tables.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/pdf_reports/HEAD/examples/example_with_plot_and_tables/with_plots_and_tables.pdf -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the sources of the documentation. 2 | 3 | To be able to compile the source, install the dependencies with 4 | :: 5 | sudo pip install sphinx sphinx_rtd_theme numpydoc sphinxcontrib-mermaid 6 | -------------------------------------------------------------------------------- /examples/example_scss_preload/example_scss_preload.py: -------------------------------------------------------------------------------- 1 | from pdf_reports import pug_to_html, write_report, preload_stylesheet 2 | 3 | css = preload_stylesheet('style.scss') 4 | html = pug_to_html("template.pug", title="My report", my_name='Zulko') 5 | write_report(html, "example.pdf", extra_stylesheets=[css]) -------------------------------------------------------------------------------- /docs/ref.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | PDF Reports Reference manual 4 | ============================ 5 | 6 | Core functions 7 | ~~~~~~~~~~~~~~ 8 | 9 | .. automodule:: pdf_reports.pdf_reports 10 | :members: 11 | 12 | Tools 13 | ~~~~~ 14 | 15 | .. automodule:: pdf_reports.tools 16 | :members: 17 | -------------------------------------------------------------------------------- /examples/basic_example/example.py: -------------------------------------------------------------------------------- 1 | """Basic PDF report generation from a Pug template with pdf_reports. 2 | 3 | A HTML page is generated from a template and rendered as a local PDF file. 4 | """ 5 | 6 | from pdf_reports import pug_to_html, write_report 7 | 8 | html = pug_to_html("example_template.pug", title="My report") 9 | write_report(html, "example.pdf") 10 | -------------------------------------------------------------------------------- /pdf_reports/__init__.py: -------------------------------------------------------------------------------- 1 | from .pdf_reports import ( 2 | pug_to_html, 3 | write_report, 4 | ReportWriter, 5 | GLOBALS, 6 | preload_stylesheet, 7 | ) 8 | from .tools import ( 9 | dataframe_to_html, 10 | style_table_rows, 11 | add_css_class, 12 | figure_data, 13 | JupyterPDF, 14 | ) 15 | 16 | from .version import __version__ 17 | -------------------------------------------------------------------------------- /examples/example_with_plot_and_tables/with_plots_and_tables.pug: -------------------------------------------------------------------------------- 1 | h1 Example document with plot and table 2 | 3 | #sidebar: p I am an example PDF generated with PDF Reports 4 | 5 | h3 Our Data 6 | 7 | {{ pdf_tools.dataframe_to_html(dataframe) }} 8 | 9 | 10 | h3 Age versus height 11 | 12 | center 13 | - var figure = dataframe.plot("Age", "Height (cm)") 14 | img(src="{{ pdf_tools.figure_data(figure, (6, 4)) }}") 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/example_reportwriter/example_reportwriter.py: -------------------------------------------------------------------------------- 1 | from pdf_reports import ReportWriter 2 | 3 | # DEFINE A WRITER WITH DEFAULT TEMPLATE AND VALUES 4 | report_writer = ReportWriter( 5 | default_stylesheets=["style.css"], 6 | default_template="template.pug", 7 | title="My default title", 8 | version="0.1.2" 9 | ) 10 | 11 | # THEN LATER IN YOUR CODE: 12 | html = report_writer.pug_to_html(my_name="Zulko", my_organization="EGF") 13 | report_writer.write_report(html, "example_reportwriter.pdf") -------------------------------------------------------------------------------- /.github/workflows/document.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | document: 10 | runs-on: ubuntu-24.04 11 | environment: 12 | name: github-pages 13 | url: ${{ steps.deployment.outputs.page_url }} 14 | permissions: 15 | pages: write 16 | id-token: write 17 | steps: 18 | - id: deployment 19 | uses: sphinx-notes/pages@v3 20 | with: 21 | python_version: 3.12 22 | documentation_path: ./docs 23 | requirements_path: ./docs/requirements.txt 24 | -------------------------------------------------------------------------------- /tests/data/with_plots_and_tables.pug: -------------------------------------------------------------------------------- 1 | h1 Example document with plot and table 2 | 3 | #sidebar: p I am an example PDF generated with PDF Reports 4 | 5 | h3 Our Data 6 | 7 | {{ pdf_tools.dataframe_to_html(dataframe) }} 8 | 9 | 10 | h3 Age versus height 11 | 12 | center 13 | - var figure = dataframe.plot("Age", "Height (cm)") 14 | img(src="{{ pdf_tools.figure_data(figure, (6, 4)) }}") 15 | 16 | h3 Age versus height (SVG version) 17 | 18 | center 19 | - var figure = dataframe.plot("Age", "Height (cm)") 20 | img(src="{{ pdf_tools.figure_data(figure, (6, 4), fmt='svg') }}") 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/_static/css/main.css: -------------------------------------------------------------------------------- 1 | body, h1, h2, h3 { 2 | font-family: Raleway; 3 | } 4 | 5 | .wy-nav-content-wrap { 6 | background: none 7 | } 8 | 9 | .wy-nav-side { 10 | background-image: url("../images/pw_maze_dark.png"); 11 | } 12 | 13 | .wy-menu-vertical a { 14 | color: black 15 | } 16 | 17 | .wy-menu-vertical a:hover { 18 | background-color: #d0d0e7 19 | } 20 | 21 | .wy-side-nav-search, .wy-nav-top { 22 | background-color: #d0d0e7; 23 | background-image: url("../images/pw_maze_dark.png"); 24 | } 25 | 26 | .edgeLabel { 27 | background-color: #fcfcfc; 28 | } 29 | 30 | 31 | .section { 32 | opacity: 1.0 !important; 33 | } 34 | -------------------------------------------------------------------------------- /examples/example_with_plot_and_tables/with_plots_and_tables.py: -------------------------------------------------------------------------------- 1 | """PDF report generation from a Pug template embedding code to generate 2 | plots and tables. 3 | 4 | A HTML page is generated from a template and rendered as a local PDF file. 5 | """ 6 | 7 | import pandas 8 | import matplotlib.pyplot as plt 9 | from pdf_reports import pug_to_html, write_report 10 | 11 | dataframe = pandas.DataFrame.from_records({ 12 | "Name": ["Anna", "Bob", "Claire", "Denis"], 13 | "Age": [12,22,33,44], 14 | "Height (cm)": [140, 175, 173, 185] 15 | }, columns=["Name", "Age", "Height (cm)"]) 16 | 17 | html = pug_to_html("with_plots_and_tables.pug", dataframe=dataframe) 18 | write_report(html, "with_plots_and_tables.pdf") 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pdf_reports" 3 | version = "0.3.9" 4 | license = "MIT" 5 | authors = [{ name = "Zulko" }] 6 | description = "Create nice-looking PDF reports from HTML content." 7 | readme = "pypi-readme.rst" 8 | keywords = ["PDF", "report", "web", "jinja", "weasyprint"] 9 | dependencies = [ 10 | "pypugjs", 11 | "jinja2", 12 | "weasyprint", 13 | "beautifulsoup4", 14 | "pandas", 15 | "Markdown", 16 | "backports.functools-lru-cache", 17 | ] 18 | 19 | [project.urls] 20 | Homepage = "https://github.com/Edinburgh-Genome-Foundry/pdf_reports" 21 | 22 | [build-system] 23 | requires = ["setuptools"] 24 | build-backend = "setuptools.build_meta" 25 | 26 | [tool.setuptools.packages.find] 27 | exclude = ["docs*", "examples*", "tests*"] 28 | 29 | [tool.setuptools.package-data] 30 | pdf_reports = ["css/*"] 31 | -------------------------------------------------------------------------------- /docs/examples/standard_example.rst: -------------------------------------------------------------------------------- 1 | .. non_unique_kmers_minimization: 2 | 3 | Standard example 4 | ---------------- 5 | 6 | Python code 7 | ~~~~~~~~~~~ 8 | 9 | .. literalinclude:: ../../examples/basic_example/example.py 10 | 11 | Pug template 12 | ~~~~~~~~~~~~ 13 | 14 | .. literalinclude:: ../../examples/basic_example/example_template.pug 15 | 16 | Result 17 | ~~~~~~ 18 | 19 | `PDF link `_ 20 | 21 | .. raw:: html 22 | 23 |
24 | 25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /docs/examples/with_plots_and_tables.rst: -------------------------------------------------------------------------------- 1 | .. non_unique_kmers_minimization: 2 | 3 | Report with plots and tables 4 | ---------------------------- 5 | 6 | Python code 7 | ~~~~~~~~~~~ 8 | 9 | .. literalinclude:: ../../examples/example_with_plot_and_tables/with_plots_and_tables.py 10 | 11 | Pug template 12 | ~~~~~~~~~~~~ 13 | 14 | .. literalinclude:: ../../examples/example_with_plot_and_tables/with_plots_and_tables.pug 15 | 16 | Result 17 | ~~~~~~ 18 | 19 | `PDF link `_ 20 | 21 | .. raw:: html 22 | 23 |
24 | 25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Edinburgh Genome Foundry, University of Edinburgh 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 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | .. toctree:: 4 | :hidden: 5 | :maxdepth: 3 6 | 7 | self 8 | 9 | .. toctree:: 10 | :hidden: 11 | :caption: Reference 12 | :maxdepth: 3 13 | 14 | ref 15 | 16 | 17 | 18 | .. toctree:: 19 | :caption: Examples 20 | :maxdepth: 1 21 | 22 | examples/standard_example 23 | examples/with_plots_and_tables 24 | 25 | 26 | .. raw:: html 27 | 28 | 31 | 35 | 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-24.04 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: "3.12" 15 | - name: Install dependencies 16 | # Install dependencies for WeasyPrint: https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#ubuntu-20-04 17 | run: | 18 | python -m pip install --upgrade pip 19 | sudo apt-get -y install python3-cffi python3-brotli libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 20 | pip install --upgrade pytest pytest-cov coveralls 21 | pip install pypdf matplotlib libsass 22 | - name: Install 23 | run: | 24 | pip install -e . 25 | - name: Test with pytest 26 | run: | 27 | python -m pytest --cov pdf_reports --cov-report term-missing 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@v2 30 | continue-on-error: true 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | env: 34 | COVERALLS_SERVICE_NAME: github 35 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | import pdf_reports.tools as tools 2 | import pandas 3 | 4 | dataframe = pandas.DataFrame.from_records( 5 | { 6 | "Name": ["Anna", "Bob", "Claire", "Denis"], 7 | "Age": [12, 22, 33, 44], 8 | "Height (cm)": [140, 175, 173, 185], 9 | }, 10 | columns=["Name", "Age", "Height (cm)"], 11 | ) 12 | 13 | 14 | def test_tr_modifier(): 15 | html = tools.dataframe_to_html(dataframe) 16 | 17 | def tr_modifier(tr): 18 | if "Anna" in tr.text: 19 | tools.add_css_class(tr, "is-anna") 20 | 21 | new_html = tools.style_table_rows(html, tr_modifier) 22 | assert "is-anna" in new_html 23 | 24 | def tr_modifier(tr): 25 | if "Emma" in tr.text: 26 | tools.add_css_class(tr, "is-emma") 27 | 28 | new_html = tools.style_table_rows(html, tr_modifier) 29 | assert "is-emma" not in new_html 30 | 31 | 32 | def test_JupyterPDF(): 33 | pdf = tools.JupyterPDF("some_url.pdf") 34 | assert len(pdf._repr_html_()) > 40 35 | 36 | 37 | def test_now(): 38 | now = tools.now(fmt="%Y-%m-%d %H:%M") 39 | assert now[4] == "-" and now[13] == ":" and len(now) == 16 40 | 41 | 42 | def test_wrap(): 43 | text = "abcde" 44 | assert tools.wrap(text, col_width=4) == "abcd\ne" 45 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-24.04 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.12" 17 | - name: Install dependencies 18 | # Install dependencies for WeasyPrint: https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#ubuntu-20-04 19 | run: | 20 | sudo apt-get -y install python3-cffi python3-brotli libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 21 | pip install --upgrade pytest 22 | pip install pypdf matplotlib libsass 23 | - name: Install 24 | run: | 25 | pip install -e . 26 | - name: Test 27 | run: | 28 | python -m pytest 29 | deploy: 30 | runs-on: ubuntu-24.04 31 | needs: [test] 32 | permissions: 33 | id-token: write 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Set up Python 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: '3.12' 40 | cache: pip 41 | cache-dependency-path: '**/pyproject.toml' 42 | - name: Install dependencies 43 | run: | 44 | pip install setuptools wheel build 45 | - name: Build 46 | run: | 47 | python -m build 48 | - name: Publish 49 | uses: pypa/gh-action-pypi-publish@release/v1 50 | -------------------------------------------------------------------------------- /pypi-readme.rst: -------------------------------------------------------------------------------- 1 | PDF Reports 2 | =========== 3 | 4 | *PDF Reports* (complete documentation `here `_) is a Python library to create nice-looking PDF reports from HTML or `Pug `_ templates. It features modern-looking components (via the `Semantic UI `_ framework) and provides routines to embed tables or plots in the documents. 5 | 6 | 7 | Example of use 8 | -------------- 9 | 10 | Your Pug template file ``template.pug`` may look like this (see a `full example `_): 11 | 12 | .. code:: pug 13 | 14 | #sidebar I am the text in the sidebar. 15 | 16 | h1 {{ title }} 17 | 18 | .ui.piled.segment 19 | p Oh hi there ! I am some text in a cool box. 20 | 21 | Your Python code will be as follows: 22 | 23 | .. code:: python 24 | 25 | from pdf_reports import pug_to_html, write_report 26 | html = pug_to_html("template.pug", title="My report") 27 | write_report(html, "example.pdf") 28 | 29 | 30 | And your final result may look like this (`PDF file `_): 31 | 32 | .. image:: https://github.com/Edinburgh-Genome-Foundry/pdf_reports/raw/master/screenshot.png 33 | 34 | Infos 35 | ----- 36 | 37 | **PIP installation:** 38 | 39 | .. code:: bash 40 | 41 | pip install pdf_reports 42 | 43 | **Web documentation:** ``_ 44 | 45 | **Github Page:** ``_ 46 | 47 | **Live demo:** ``_ 48 | 49 | **License:** MIT 50 | 51 | Copyright 2018 Edinburgh Genome Foundry, University of Edinburgh 52 | -------------------------------------------------------------------------------- /tests/data/example_template.pug: -------------------------------------------------------------------------------- 1 | img(style="width:400px; display:block; margin:0 auto;" 2 | src="file:///{{ egf_logo_url }}") 3 | 4 | #sidebar: p {{ sidebar_text }} 5 | 6 | h1 {{ title }} 7 | 8 | p. 9 | Hiya I am some text. 10 | 11 | .ui.container 12 | .ui.icon.message.yellow.block-center 13 | i.exclamation.circle.icon 14 | .content 15 | .header The header 16 | p. 17 | the content 18 | 19 | .ui.piled.segment 20 | p bla bla 21 | 22 | 23 | h4 Read this table ! 24 | 25 | table.ui.celled.table 26 | thead 27 | tr 28 | th File 29 | th description 30 | tbody 31 | tr 32 | td No Name Specified 33 | td Unknown 34 | td.negative None 35 | tr.positive 36 | td Jimmy 37 | td 38 | i.exclamation.circle.icon 39 | | Approved 40 | td None 41 | tr 42 | td Jamie 43 | td Unknown 44 | td.positive 45 | i.exclamation.circle.icon 46 | | Requires call 47 | tr.negative 48 | td Jill 49 | td Unknown 50 | td None 51 | 52 | h3 Another paragraph 53 | 54 | p Bla bla bla bla bla bla bla bla bla bla 55 | 56 | h2 Another table just to get 2 pages 57 | 58 | table.ui.celled.table 59 | thead 60 | tr 61 | th Name 62 | th Status 63 | th Notes 64 | tbody 65 | tr 66 | td No Name Specified 67 | td Unknown 68 | td.negative None 69 | tr.positive 70 | td Jimmy 71 | td 72 | i.exclamation.circle.icon 73 | | Approved 74 | td None 75 | tr 76 | td Jamie 77 | td Unknown 78 | td.positive 79 | i.exclamation.circle.icon 80 | | Requires call 81 | tr.negative 82 | td Jill 83 | td Unknown 84 | td None 85 | tr.negative 86 | td John 87 | td Unknown 88 | td None 89 | -------------------------------------------------------------------------------- /pdf_reports/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #048a8b; 3 | } 4 | 5 | @page { 6 | margin: 1cm 0 2cm 0cm; 7 | @bottom-center { 8 | content: "Page " counter(page) " of " counter(pages); 9 | font-family: 'Lato'; 10 | } 11 | } 12 | html { 13 | padding-left: 1cm; 14 | padding-right: 1cm; 15 | font-size: 1.2em; 16 | } 17 | 18 | h1 { 19 | text-align: center; 20 | } 21 | 22 | body { 23 | margin-left: 1cm; 24 | } 25 | 26 | #title { 27 | font-weight: bold; 28 | font-size: 2em; 29 | } 30 | 31 | .summary { 32 | background-color: lighten(--primary-color, 33%); 33 | } 34 | 35 | h2 { 36 | font-size: 2.2em; 37 | /* page-break-before: always; */ 38 | } 39 | 40 | .ui.piled.segment { 41 | font-size: 1em; 42 | } 43 | 44 | p { 45 | font-size: 14px; 46 | line-height: 1.5em; 47 | text-align: justify; 48 | } 49 | 50 | #sidebar { 51 | position: fixed; 52 | left: 0; 53 | width: 29.7cm; 54 | height: 0.25in; 55 | bottom: -2.7cm; 56 | border-left: 0.25in solid transparent; 57 | border-top: 0.25in solid #8bc8c8; 58 | border-right: 0.25in solid transparent; 59 | -webkit-transform-origin: 0 0; 60 | transform-origin: 0 0; 61 | transform: rotate(-90deg); 62 | } 63 | 64 | #sidebar p { 65 | text-align: center; 66 | /* color: #cbe7e7; */ 67 | color: white; 68 | opacity: 0.6; 69 | margin-top: -1.7em; 70 | } 71 | 72 | .block-center { 73 | display: block; 74 | margin: 0 auto; 75 | } 76 | 77 | .message, figure { 78 | page-break-inside: avoid; 79 | } 80 | 81 | .message { 82 | width: 17.5cm !important; 83 | margin-top: 2em !important; 84 | } 85 | 86 | .ui.label.head-label { 87 | font-size: 0.8em !important; 88 | float: right; 89 | } 90 | 91 | .logos img { 92 | width: 250px; 93 | margin: 30px 94 | } 95 | 96 | hr { 97 | border: 0; 98 | height: 1px; 99 | background: #333; 100 | background-image: linear-gradient(to right, #ccc, #333, #ccc); 101 | } 102 | 103 | table thead th { 104 | text-align: center !important; 105 | } 106 | -------------------------------------------------------------------------------- /examples/basic_example/example_template.pug: -------------------------------------------------------------------------------- 1 | img(style="width:400px; display:block; margin:0 auto;" 2 | src="file:///{{ egf_logo_url }}") 3 | 4 | #sidebar: p Hey, I am a sidebar text ! I make your document look much more serious. 5 | 6 | 7 | h1 {{ title }} 8 | 9 | 10 | .ui.piled.segment: p. 11 | Oh hi there ! I am some text in a cool box. What you are looking at is a PDF 12 | report made with a library called 13 | #[a(href='https://github.com/Edinburgh-Genome-Foundry/pdf_reports') PDF reports] 14 | (how original...) This library uses the Semantic UI CSS framework - hence the 15 | cool box. 16 | 17 | :markdown 18 | You can install `pdf_reports` with Python's PIP: 19 | 20 | ``` 21 | pip install pdf_reports 22 | ``` 23 | 24 | There are **many** things you can do with Pug and [Semantic 25 | UI](https://semantic-ui.com/) and we will cram all of them in this one page so 26 | that it will make a nice screenshot. By the way, this paragraph is written in 27 | [Markdown](https://en.wikipedia.org/wiki/Markdown) ! 28 | 29 | 30 | .ui.container 31 | .ui.icon.message.yellow.block-center 32 | i.exclamation.circle.icon 33 | .content 34 | .header This is an important message, as per the exclamation mark. 35 | p. 36 | You can add content to your message explaining why it is important and 37 | what the reader can do to de-importantize the situation. 38 | 39 | 40 | :markdown 41 | ## I am a section title 42 | 43 | Maybe the author should think of more original content for the titles. 44 | Anyways, here is a colorful table for you, straight from the Semantic UI 45 | examples (yeah that's lazyness). If you are filling up a template with Python, 46 | you can use Panda's `dataframe.to_html(classes=['ui', 'celled', 'table'])` to 47 | create such tables: 48 | 49 | 50 | table.ui.celled.table 51 | thead 52 | tr 53 | th File 54 | th description 55 | tbody 56 | tr 57 | td No Name Specified 58 | td Unknown 59 | td.negative None 60 | tr.positive 61 | td Jimmy 62 | td 63 | i.exclamation.circle.icon 64 | | Approved 65 | td None 66 | tr 67 | td Jamie 68 | td Unknown 69 | td.positive 70 | i.exclamation.circle.icon 71 | | Requires call 72 | tr.negative 73 | td Jill 74 | td Unknown 75 | td None 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /tests/test_basics.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pdf_reports import pug_to_html, write_report, preload_stylesheet, ReportWriter 3 | import pandas 4 | import matplotlib 5 | 6 | matplotlib.use("Agg") 7 | import matplotlib.pyplot as plt 8 | from pypdf import PdfReader 9 | 10 | 11 | def test_pug_to_html(): 12 | html = pug_to_html(string="p {{ var }}", var="Bla") 13 | assert html == "

Bla

" 14 | 15 | 16 | def test_write_pdf(): 17 | data = write_report("

Test

") 18 | assert len(data) > 100 19 | 20 | 21 | def test_basics(tmpdir): 22 | template_path = os.path.join("tests", "data", "example_template.pug") 23 | assert os.path.exists(template_path) 24 | pdf_path = os.path.join(str(tmpdir), "test.pdf") 25 | html = pug_to_html(template_path, title="Summary of your order") 26 | write_report(html, pdf_path) 27 | with open(pdf_path, "rb") as f: 28 | reader = PdfReader(f) 29 | assert len(reader.pages) == 2 30 | 31 | pdf_data = write_report(html) 32 | assert len(pdf_data) > 10000 33 | 34 | 35 | def test_with_plots_and_tables(tmpdir): 36 | template_path = os.path.join("tests", "data", "with_plots_and_tables.pug") 37 | assert os.path.exists(template_path) 38 | 39 | dataframe = pandas.DataFrame.from_records( 40 | { 41 | "Name": ["Anna", "Bob", "Claire", "Denis"], 42 | "Age": [12, 22, 33, 44], 43 | "Height (cm)": [140, 175, 173, 185], 44 | }, 45 | columns=["Name", "Age", "Height (cm)"], 46 | ) 47 | 48 | pdf_path = os.path.join(str(tmpdir), "test.pdf") 49 | html = pug_to_html(template_path, dataframe=dataframe) 50 | write_report(html, pdf_path) 51 | with open(pdf_path, "rb") as f: 52 | reader = PdfReader(f) 53 | assert len(reader.pages) == 2 54 | 55 | pdf_data = write_report(html) 56 | assert len(pdf_data) > 10000 57 | 58 | 59 | def test_preload_stylesheet(tmpdir): 60 | css = preload_stylesheet(os.path.join("tests", "data", "style.scss")) 61 | html = pug_to_html(string="p {{ var }}", var="Bla") 62 | pdf_path = os.path.join(str(tmpdir), "test.pdf") 63 | write_report(html, pdf_path, extra_stylesheets=[css]) 64 | 65 | 66 | def test_ReportWriter(tmpdir): 67 | report_writer = ReportWriter( 68 | default_template=os.path.join("tests", "data", "template_rw.pug"), 69 | title="My default title", 70 | version="0.1.2", 71 | ) 72 | html = report_writer.pug_to_html(my_name="Zulko", my_organization="EGF") 73 | report_writer.write_report(html, os.path.join(str(tmpdir), "test_rw.pdf")) 74 | -------------------------------------------------------------------------------- /pdf_reports/tools.py: -------------------------------------------------------------------------------- 1 | """Utilities for report generation. 2 | 3 | The module contains in particular routines the creation of tables, plots, etc. 4 | inside the templates. Functions in this module are available from inside the 5 | templates under the domain name ``pdf_tools``. For instance 6 | ``pdf_tools.dataframe_to_html()``. 7 | """ 8 | 9 | from bs4 import BeautifulSoup 10 | import base64 11 | import pandas 12 | from io import BytesIO 13 | import datetime 14 | import textwrap 15 | 16 | 17 | def dataframe_to_html( 18 | dataframe, 19 | extra_classes=(), 20 | index=False, 21 | header=True, 22 | use_default_classes=True, 23 | escape_html=False, 24 | ): 25 | """Return a HTML version of a dataframe with Semantic UI CSS style classes. 26 | 27 | By default, it applies the following Semantic UI classes: 28 | 'ui', 'compact', 'celled', 'striped', 'table', 'groups'. 29 | 30 | Parameters 31 | ---------- 32 | dataframe : DataFrame 33 | The pandas dataframe to convert to HTML. 34 | extra_classes : list of str 35 | Classes to add to the default, which are 'ui', 'compact', 'celled', 36 | 'striped', 'table', 'groups', selected to create nicely-formatted 37 | Semantic UI tables. For instance, 'definition' can be added to add 38 | special emphasis on the first column. See Semantic UI documentation. 39 | index : bool 40 | Whether to display the dataframe's index. 41 | header : bool 42 | Whether to display the dataframe's headers. 43 | escape_html : bool 44 | Whether the content of the dataframe should be html-escaped. Leave to 45 | False if your dataframe contains images or any kind of HTML formatting. 46 | """ 47 | default_classes = () 48 | if use_default_classes: 49 | default_classes = ( 50 | "ui", 51 | "compact", 52 | "celled", 53 | "striped", 54 | "table", 55 | "groups", 56 | ) 57 | classes = default_classes + tuple(extra_classes) 58 | current_colwidth = pandas.get_option("display.max_colwidth") 59 | # pandas.set_option("display.max_colwidth", -1) 60 | result = dataframe.to_html( 61 | classes=classes, index=index, header=header, escape=escape_html 62 | ) 63 | pandas.set_option("display.max_colwidth", current_colwidth) 64 | return result 65 | 66 | 67 | def style_table_rows(table_html, tr_modifier): 68 | """Return a new HTML string of the table, with rows modified. 69 | 70 | Parameters 71 | ---------- 72 | table_html : str 73 | A string "...
" of an HTML table. 74 | tr_modifier : function 75 | A function that takes a BeautifulSoup `tr` element as argument and changes its attributes inplace. 76 | For instance, modifications can be made with `tr.text = new_text`, or with the `add_css_class` method. 77 | """ 78 | soup = BeautifulSoup(table_html, "html.parser") 79 | for tr in soup.find_all("tr"): 80 | tr_modifier(tr) 81 | return str(soup) 82 | 83 | 84 | def add_css_class(element, cls): 85 | """Add a given class to the given BeautifulSoup HTML element.""" 86 | attrs = element.attrs 87 | new_class = (attrs["class"] + " " if "class" in attrs else "") + cls 88 | element.attrs["class"] = new_class 89 | 90 | 91 | class JupyterPDF(object): 92 | """Class to display PDFs in a Jupyter / IPython notebook. 93 | 94 | Just write this at the end of a code Cell to get in-browser PDF preview: 95 | 96 | >>> from pdf_reports import JupyterPDF 97 | >>> JupyterPDF("path_to_some.pdf") 98 | 99 | Credits to StackOverflow's Jakob: https://stackoverflow.com/a/19470377 100 | """ 101 | 102 | def __init__(self, url, width=600, height=800): 103 | self.url = url 104 | self.width = width 105 | self.height = height 106 | 107 | def _repr_html_(self): 108 | return """ 109 |
110 | 112 |
113 | """.format( 114 | self=self 115 | ) 116 | 117 | 118 | def now(fmt="%Y-%m-%d %H:%M"): 119 | now = datetime.datetime.now() 120 | if fmt is not None: 121 | now = now.strftime(fmt) 122 | return now 123 | 124 | 125 | def figure_data(fig, size=None, fmt="png", bbox_inches="tight", **kwargs): 126 | """Return a HTML-embeddable string of the figure data. 127 | 128 | The string can be embedded in an image tag as ``. 129 | 130 | Parameters 131 | ---------- 132 | fig : Matplotlib figure or axis 133 | A Matplotlib figure. A Matplotlib "ax" can also be provided, at which 134 | case the whole `ax.figure` will be displayed (i.e., all axes in the 135 | same figure). 136 | size : tuple 137 | Size or resolution (width, height) of the final figure image, in inches. 138 | fmt : str 139 | Image format, for instance "png", "svg", "jpeg". SVG is vectorial (non 140 | pixelated) but sometimes more difficult to work with inside HTML/PDF 141 | documents. 142 | bbox_inches : str 143 | Keeping this option to "tight" will ensure that your plot's delimitation 144 | is optimal. 145 | **kwargs 146 | Any other option of Matplotlib's figure.savefig() method. 147 | """ 148 | if fig.__class__.__name__ == "Axes": 149 | fig = fig.figure 150 | output = BytesIO() 151 | original_size = fig.get_size_inches() 152 | if size is not None: 153 | fig.set_size_inches((int(size[0]), int(size[1]))) 154 | fig.savefig(output, format=fmt, bbox_inches=bbox_inches, **kwargs) 155 | fig.set_size_inches(original_size) 156 | data = output.getvalue() 157 | # This block prevented rendering SVG in Python 3.9 / newer dependencies. 158 | # Apparently not needed anymore even for Python 3.6. In here for future reference. 159 | # if fmt == "svg": 160 | # svg_txt = data.decode() 161 | # svg_txt = "\n".join(svg_txt.split("\n")[4:]) 162 | # svg_txt = "".join(svg_txt.split("\n")) 163 | # content = base64.b64encode(svg_txt.encode("utf-8")) 164 | # else: 165 | # content = base64.b64encode(data) 166 | content = base64.b64encode(data) 167 | result = b"data:image/%s+xml;base64,%s" % (fmt.encode("utf-8"), content) 168 | return result.decode("utf-8") 169 | 170 | 171 | def wrap(text, col_width): 172 | return "\n".join(textwrap.wrap(text, col_width)) 173 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -E -a 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | PDFBUILDDIR = /tmp 10 | PDF = manual.pdf 11 | 12 | # User-friendly check for sphinx-build 13 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 14 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 15 | endif 16 | 17 | # Internal variables. 18 | PAPEROPT_a4 = -D latex_paper_size=a4 19 | PAPEROPT_letter = -D latex_paper_size=letter 20 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | # the i18n builder cannot share the environment and doctrees with the others 22 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 23 | 24 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 25 | 26 | help: 27 | @echo "Please use \`make ' where is one of" 28 | @echo " html to make standalone HTML files" 29 | @echo " dirhtml to make HTML files named index.html in directories" 30 | @echo " singlehtml to make a single large HTML file" 31 | @echo " pickle to make pickle files" 32 | @echo " json to make JSON files" 33 | @echo " htmlhelp to make HTML files and a HTML help project" 34 | @echo " qthelp to make HTML files and a qthelp project" 35 | @echo " devhelp to make HTML files and a Devhelp project" 36 | @echo " epub to make an epub" 37 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 38 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 39 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 40 | @echo " text to make text files" 41 | @echo " man to make manual pages" 42 | @echo " texinfo to make Texinfo files" 43 | @echo " info to make Texinfo files and run them through makeinfo" 44 | @echo " gettext to make PO message catalogs" 45 | @echo " changes to make an overview of all changed/added/deprecated items" 46 | @echo " xml to make Docutils-native XML files" 47 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 48 | @echo " linkcheck to check all external links for integrity" 49 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelppdf_reports.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelppdf_reports.qhc" 93 | 94 | devhelp: 95 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 96 | @echo 97 | @echo "Build finished." 98 | @echo "To view the help file:" 99 | @echo "# mkdir -p $$HOME/.local/share/devhelppdf_reports" 100 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelppdf_reports" 101 | @echo "# devhelp" 102 | 103 | epub: 104 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 105 | @echo 106 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 107 | 108 | latex: 109 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 110 | @echo 111 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 112 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 113 | "(use \`make latexpdf' here to do that automatically)." 114 | 115 | latexpdf: 116 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(PDFBUILDDIR)/latex 117 | @echo "Running LaTeX files through pdflatex..." 118 | $(MAKE) -C $(PDFBUILDDIR)/latex all-pdf 119 | cp $(PDFBUILDDIR)/latex/*.pdf $(PDF) 120 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 121 | 122 | latexpdfja: 123 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 124 | @echo "Running LaTeX files through platex and dvipdfmx..." 125 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 126 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 127 | 128 | text: 129 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 130 | @echo 131 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 132 | 133 | man: 134 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 135 | @echo 136 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 137 | 138 | texinfo: 139 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 140 | @echo 141 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 142 | @echo "Run \`make' in that directory to run these through makeinfo" \ 143 | "(use \`make info' here to do that automatically)." 144 | 145 | info: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo "Running Texinfo files through makeinfo..." 148 | make -C $(BUILDDIR)/texinfo info 149 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 150 | 151 | gettext: 152 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 153 | @echo 154 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 155 | 156 | changes: 157 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 158 | @echo 159 | @echo "The overview file is in $(BUILDDIR)/changes." 160 | 161 | linkcheck: 162 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 163 | @echo 164 | @echo "Link check complete; look for any errors in the above output " \ 165 | "or in $(BUILDDIR)/linkcheck/output.txt." 166 | 167 | doctest: 168 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 169 | @echo "Testing of doctests in the sources finished, look at the " \ 170 | "results in $(BUILDDIR)/doctest/output.txt." 171 | 172 | xml: 173 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 174 | @echo 175 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 176 | 177 | pseudoxml: 178 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 179 | @echo 180 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 181 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PDF Reports.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PDF Reports.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /pdf_reports/pdf_reports.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | try: 4 | from weasyprint import HTML, CSS 5 | except (ImportError, OSError) as err: 6 | message = ( 7 | "[PDF Reports] ERROR: The Weasyprint library did not load" 8 | "properly! You will not be able to generate PDF reports until" 9 | "you fix this issue.\n" 10 | ) 11 | 12 | if "pango" in str(err): 13 | message += ( 14 | "\nMaybe you haven't installed the Pango dependency? " 15 | "See PDF Reports install instructions in documentation.\n" 16 | ) 17 | if "cairo" in str(err): 18 | message += "\nMaybe you haven't installed the Cairo dependency?\n" 19 | 20 | message += ( 21 | "\nIn any other case the weasyprint docs may be able to help:\n\n" 22 | "http://weasyprint.readthedocs.io/en/stable/install.html#windows\n\n" 23 | "The original import error was %s" % (str(err)) 24 | ) 25 | 26 | def HTML(*args, **kwargs): 27 | """%s""" % message 28 | raise ImportError(message) 29 | 30 | CSS = HTML 31 | 32 | try: 33 | import sass 34 | 35 | LIBSASS_AVAILABLE = True 36 | except ImportError: 37 | LIBSASS_AVAILABLE = False 38 | sass = None 39 | 40 | import jinja2 41 | import tempfile 42 | import warnings 43 | from io import BytesIO 44 | from . import tools 45 | 46 | 47 | try: 48 | from functools import lru_cache 49 | except ImportError: 50 | from backports.functools_lru_cache import lru_cache 51 | 52 | THIS_PATH = os.path.dirname(os.path.realpath(__file__)) 53 | SEMANTIC_UI_CSS = os.path.join(THIS_PATH, "css", "semantic.min.css") 54 | STYLESHEET = os.path.join(THIS_PATH, "css", "style.css") 55 | EGF_LOGO_URL = os.path.join(THIS_PATH, "css", "egf-logo.svg") 56 | 57 | GLOBALS = { 58 | "egf_logo_url": EGF_LOGO_URL, 59 | "list": list, 60 | "len": len, 61 | "zip": zip, 62 | "enumerate": enumerate, 63 | "pdf_tools": tools, 64 | } 65 | 66 | 67 | @lru_cache(maxsize=1) 68 | def get_semantic_ui_CSS(): 69 | with warnings.catch_warnings(): 70 | css = CSS(filename=SEMANTIC_UI_CSS) 71 | return css 72 | 73 | 74 | def pug_to_html(path=None, string=None, **context): 75 | """Convert a Pug template, as file or string, to html. 76 | 77 | Parameters 78 | ---------- 79 | path : str, optional 80 | Path to a .pug template file. The `string` parameter can be provided instead. 81 | string : str, optional 82 | A string of a Pug template. The `path` parameter can be provided instead. 83 | **context : 84 | Keyword arguments indicating the variables to use in the Pug template 85 | (if it contains variables). For instance `title='My title'`. 86 | """ 87 | default = {k: v for (k, v) in GLOBALS.items()} 88 | default.update(context) 89 | context = default 90 | if string is not None: 91 | template_path = tempfile.mktemp(suffix=".pug") 92 | with open(template_path, "w+") as f: 93 | f.write(string) 94 | if path is None: 95 | path = template_path 96 | else: 97 | template_path = path 98 | basepath, filename = os.path.split(template_path) 99 | env = jinja2.Environment( 100 | loader=jinja2.FileSystemLoader(basepath if basepath else "."), 101 | extensions=["pypugjs.ext.jinja.PyPugJSExtension"], 102 | ) 103 | 104 | template = env.get_template(filename) 105 | return template.render(context) 106 | 107 | 108 | def write_report( 109 | html, target=None, base_url=None, use_default_styling=True, extra_stylesheets=() 110 | ): 111 | """Write the provided HTML in a PDF file. 112 | 113 | Parameters 114 | ---------- 115 | html : str 116 | A HTML string. 117 | target : str or file-like object, optional 118 | A PDF file path or file-like object, or None for returning the raw bytes of the PDF. 119 | base_url : str 120 | The base path from which relative paths in the HTML template start. 121 | use_default_styling : bool 122 | Setting this parameter to False, your PDF will have no styling at all by default. 123 | This means no Semantic UI, which can speed up the rendering. 124 | extra_stylesheets : list of str 125 | List of paths to other ".css" files used to define new styles or overwrite default styles. 126 | """ 127 | weasy_html = HTML(string=html, base_url=base_url) 128 | if use_default_styling: 129 | extra_stylesheets = tuple(extra_stylesheets) 130 | stylesheets = ( 131 | get_semantic_ui_CSS(), 132 | STYLESHEET, 133 | ) + extra_stylesheets 134 | else: 135 | stylesheets = extra_stylesheets 136 | if target in [None, "@memory"]: 137 | with BytesIO() as buffer: 138 | weasy_html.write_pdf(buffer, stylesheets=stylesheets) 139 | pdf_data = buffer.getvalue() 140 | return pdf_data 141 | else: 142 | weasy_html.write_pdf(target, stylesheets=stylesheets) 143 | 144 | 145 | class ReportWriter: 146 | def __init__( 147 | self, 148 | default_stylesheets=(), 149 | default_template=None, 150 | use_default_styling=True, 151 | default_base_url=None, 152 | **default_context 153 | ): 154 | 155 | self.default_template = default_template 156 | self.default_context = default_context if default_context else {} 157 | self.default_stylesheets = default_stylesheets 158 | self.use_default_styling = use_default_styling 159 | self.default_base_url = default_base_url 160 | 161 | def pug_to_html(self, path=None, string=None, **context): 162 | """See pdf_reports.pug_to_html.""" 163 | if (path is None) and (string is None): 164 | path = self.default_template 165 | for k in self.default_context: 166 | if k not in context: 167 | context[k] = self.default_context[k] 168 | return pug_to_html(path=path, string=string, **context) 169 | 170 | def write_report(self, html, target=None, extra_stylesheets=(), base_url=None): 171 | return write_report( 172 | html, 173 | target=target, 174 | extra_stylesheets=list(self.default_stylesheets) + list(extra_stylesheets), 175 | base_url=base_url if base_url else self.default_base_url, 176 | use_default_styling=self.use_default_styling, 177 | ) 178 | 179 | 180 | def preload_stylesheet(path, is_scss="auto"): 181 | """Preload a stylesheet as a WeasyPrint CSS object once and for all. 182 | 183 | Returns a weasyprint.CSS object which can be provided as-is in a list of 184 | default_stylesheets or extra_stylesheets. 185 | 186 | Preloading stylesheets can save a lot of time for large CSS frameworks 187 | that are used several times. It prevents weasyprint from parsing the CSS 188 | every time. 189 | 190 | If the path ends with .scss or .sass and is_scss is set to "auto", 191 | is_scss will be set to True. 192 | 193 | If is_scss is true, the file is compiled using python-libsass ( 194 | which must be installed). 195 | 196 | Note: if you already have a string, directly use ``sass.compile(s)`` to 197 | compile the string. 198 | """ 199 | if is_scss == "auto" and isinstance(path, str): 200 | is_scss = path.lower().endswith((".scss", ".sass")) 201 | if hasattr(path, "read"): 202 | string = path.read() 203 | else: 204 | with open(path, "r") as f: 205 | string = f.read() 206 | if is_scss: 207 | if not LIBSASS_AVAILABLE: 208 | raise ImportError( 209 | "Cannot read scss files without python-libsass installed. " 210 | "Either install the library or provide a CSS file, or set " 211 | "is_scss to False" 212 | ) 213 | string = sass.compile(string=string) 214 | return CSS(string=string) 215 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. raw:: html 2 | 3 |

4 | PDF Reports Logo 5 |

6 |

7 | 8 | 9 | .. image:: https://github.com/Edinburgh-Genome-Foundry/pdf_reports/actions/workflows/build.yml/badge.svg 10 | :target: https://github.com/Edinburgh-Genome-Foundry/pdf_reports/actions/workflows/build.yml 11 | :alt: GitHub CI build status 12 | .. image:: https://coveralls.io/repos/github/Edinburgh-Genome-Foundry/pdf_reports/badge.svg?branch=master 13 | :target: https://coveralls.io/github/Edinburgh-Genome-Foundry/pdf_reports?branch=master 14 | 15 | 16 | 17 | *PDF Reports* (complete documentation `here `_) is a Python library to create nice-looking PDF reports from HTML or `Pug `_ templates. It features modern-looking components (via the `Semantic UI `_ framework) and provides routines to embed tables or plots in the documents. 18 | 19 | Note that only Python 3.x is officially supported, although with the right version of weasyprint the library can also run on 2.x. 20 | 21 | 22 | Example of use 23 | -------------- 24 | 25 | Your Pug template file ``template.pug`` may look like this (see a `full example `_): 26 | 27 | .. code:: pug 28 | 29 | #sidebar I am the text in the sidebar. 30 | 31 | h1 {{ title }} 32 | 33 | .ui.piled.segment 34 | p Oh hi there ! I am some text in a cool box. 35 | 36 | Your Python code will be as follows: 37 | 38 | .. code:: python 39 | 40 | from pdf_reports import pug_to_html, write_report 41 | html = pug_to_html("template.pug", title="My report") 42 | write_report(html, "example.pdf") 43 | 44 | And your final result may look like this (`PDF file `_): 45 | 46 | .. image:: https://github.com/Edinburgh-Genome-Foundry/pdf_reports/raw/master/screenshot.png 47 | 48 | See also `this example `_ embedding some python code in the template to 49 | create figures and tables on the flight. 50 | 51 | 52 | Other features 53 | -------------- 54 | 55 | 56 | Preloading CSS and SCSS 57 | ~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | PDF Reports provides a ``preload_stylesheet`` method which can be used to load 60 | and parse a CSS file. It also works with SCSS files (which will automatically 61 | be compiled to CSS) but this requires ``libsass`` installed (for instance via 62 | ``pip install libsass``). Here is an example: 63 | 64 | .. code:: python 65 | 66 | from pdf_reports import pug_to_html, write_report, preload_stylesheet 67 | 68 | css = preload_stylesheet('style.scss') 69 | html = pug_to_html("template.pug", title="My report", my_name='Zulko') 70 | write_report(html, "example.pdf", extra_stylesheets=[css]) 71 | 72 | 73 | Using a ReportWriter 74 | ~~~~~~~~~~~~~~~~~~~~ 75 | 76 | The ReportWriter class allows to define default templates, styles, and variable 77 | names. It can be used to avoid repeating yourself across your application: 78 | 79 | .. code:: python 80 | 81 | from pdf_reports import ReportWriter 82 | 83 | # DEFINE A WRITER WITH DEFAULT TEMPLATE AND VALUES 84 | report_writer = ReportWriter( 85 | default_stylesheets=["style.css"], 86 | default_template="template.pug", 87 | title="My default title", 88 | version="0.1.2" 89 | ) 90 | 91 | # THEN LATER IN YOUR CODE: 92 | html = report_writer.pug_to_html(my_name="Zulko", my_organization="EGF") 93 | report_writer.write_report(html, "example_reportwriter.pdf") 94 | 95 | 96 | Markdown support 97 | ~~~~~~~~~~~~~~~~ 98 | 99 | As a feature of PyPugJS, markdown is supported in the Pug templates. 100 | 101 | .. code:: pug 102 | 103 | div 104 | :markdown 105 | This is some markdown text. Here is a [link](http://example.com/). 106 | 107 | - this is a bullet point list 108 | - Second item 109 | - Etc. 110 | 111 | 112 | PDF tools 113 | ~~~~~~~~~ 114 | 115 | Some useful functions for generating reports are available from inside the 116 | Pug templates under ``pdf_tools``. For instance, ``pdf_tools.figure_data()`` 117 | to embed matplotlib images, or ``pdf_tools.dataframe_to_html()`` 118 | to turn Pandas dataframes into HTML, and style them nicely with Semantic UI. 119 | Have a look at the docs, or this 120 | `example `_. 121 | 122 | 123 | JupyterPDF 124 | ~~~~~~~~~~ 125 | 126 | The ``JupyterPDF`` class eases report templates writing by embedding PDF files 127 | in Jupyter notebooks (using the browser's interactive PDF viewer). 128 | 129 | .. code:: python 130 | 131 | from pdf_reports import JupyterPDF 132 | 133 | # Build your PDF 134 | 135 | # At the end of the notebook cell: 136 | JupyterPDF("path_to_your.pdf") 137 | 138 | 139 | Notes 140 | ----- 141 | 142 | The core of the library consists of just a few lines of Python, using `pypugjs `_ to parse Pug templates, optionally including stylesheets from the Semantic UI CSS framework, and finally calling `weasyprint `_ for PDF generation. Please refer to the Weasyprint documentation for the customization of templates. For instance, to customize the page margins and numbering the Weasyprint way, add this to your SCSS code: 143 | 144 | .. code:: scss 145 | 146 | @page { 147 | margin: 1cm 0 2cm 0cm; 148 | @bottom-center { 149 | content: "Page " counter(page) " / " counter(pages); 150 | font-family: 'Lato'; 151 | } 152 | } 153 | 154 | 155 | Using Semantic UI implies that (1) the Lato font family should be installed on your machine, otherwise the results will look less good, and (2) the first time that ``write_pdf`` is called in a Python session, if using the default Semantic UI style, the parsing of the CSS will add a 3-second overhead to the function calls (but there will be no overhead for the next calls in that session). 156 | 157 | 158 | Installation 159 | ------------ 160 | 161 | You can install the library via PIP: 162 | 163 | .. code:: 164 | 165 | pip install pdf_reports 166 | 167 | Alternatively, you can unzip the sources in a folder and type: 168 | 169 | .. code:: 170 | 171 | python setup.py install 172 | 173 | **Note:** the package depends on the WeasyPrint Python package. If there are any issues, 174 | see installation instructions in the `WeasyPrint documentation `_. 175 | 176 | If Pango is not available in your conda environment (``which pango-view``), but otherwise it's installed, then install it with ``conda install anaconda::pango`` 177 | 178 | 179 | PDF Reports has been tested on Ubuntu 22.04. If you have an older GNU/Linux distribution (e.g. Ubuntu 18.04) which 180 | doesn't have the latest Pango that is required by the latest WeasyPrint, then installing an older WeasyPrint (<=52) may help: ``pip install weasyprint==52`` 181 | 182 | **Note: on some Debian systems** you may need to first install ``libffi-dev`` (``apt install libffi-dev``). The package name may be ``libffi-devel`` on some systems. 183 | 184 | **Note: on macOS,** you may need to first install pango with: ``brew install pango`` 185 | 186 | 187 | License = MIT 188 | ------------- 189 | 190 | This open-source software project was originally written at the `Edinburgh Genome Foundry `_ by `Zulko `_ 191 | and `released on Github `_ under the MIT licence (Copyright 2018 Edinburgh Genome Foundry, University of Edinburgh). Everyone is welcome to contribute ! 192 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PDF Reports documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Jul 13 14:47:48 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | import pdf_reports 16 | 17 | sys.path.insert(0, os.path.abspath("../pdf_reports/")) 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = [ 32 | "sphinx.ext.autodoc", 33 | "sphinx.ext.todo", 34 | "sphinx.ext.viewcode", 35 | "sphinx.ext.napoleon", 36 | # "numpydoc", 37 | "sphinxcontrib.mermaid", 38 | ] 39 | napoleon_numpy_docstring = True 40 | numpydoc_show_class_members = False 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # The suffix of source filenames. 45 | source_suffix = [".rst"] 46 | 47 | # The encoding of source files. 48 | # source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = "index" 52 | 53 | # General information about the project. 54 | project = "PDF Reports" 55 | copyright = "2018 Edinburgh Genome Foundry, University of Edinburgh" 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = pdf_reports.__version__ 63 | # The full version, including alpha/beta/rc tags. 64 | release = pdf_reports.__version__ 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # language = None 69 | 70 | # There are two options for replacing |today|: either, you set today to some 71 | # non-false value, then it is used: 72 | # today = '' 73 | # Else, today_fmt is used as the format for a strftime call. 74 | # today_fmt = '%B %d, %Y' 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | exclude_patterns = ["_build"] 79 | 80 | # The reST default role (used for this markup: `text`) to use for all documents. 81 | # default_role = None 82 | 83 | # If true, '()' will be appended to :func: etc. cross-reference text. 84 | # add_function_parentheses = True 85 | 86 | # If true, the current module name will be prepended to all description 87 | # unit titles (such as .. function::). 88 | # add_module_names = True 89 | 90 | # If true, sectionauthor and moduleauthor directives will be shown in the 91 | # output. They are ignored by default. 92 | # show_authors = False 93 | 94 | # A list of ignored prefixes for module index sorting. 95 | # modindex_common_prefix = [] 96 | 97 | # If true, keep warnings as "system message" paragraphs in the built documents. 98 | # keep_warnings = False 99 | 100 | 101 | # -- Options for HTML output --------------------------------------------------- 102 | 103 | # The theme to use for HTML and HTML Help pages. See the documentation for 104 | # a list of builtin themes. 105 | 106 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 107 | 108 | if not on_rtd: # only import and set the theme if we're building docs locally 109 | import sphinx_rtd_theme 110 | 111 | html_theme = "sphinx_rtd_theme" 112 | 113 | def setup(app): 114 | app.add_css_file("css/main.css") 115 | 116 | else: 117 | html_context = { 118 | "css_files": [ 119 | "https://media.readthedocs.org/css/sphinx_rtd_theme.css", 120 | "https://media.readthedocs.org/css/readthedocs-doc-embed.css", 121 | "_static/css/main.css", 122 | ], 123 | } 124 | # sys.path.append(os.path.abspath('_themes')) 125 | # Theme options are theme-specific and customize the look and feel of a theme 126 | # further. For a list of options available for each theme, see the 127 | # documentation. 128 | # html_theme_options = {} 129 | 130 | # Add any paths that contain custom themes here, relative to this directory. 131 | # html_theme_path = [] 132 | 133 | # The name for this set of Sphinx documents. If None, it defaults to 134 | # " v documentation". 135 | # html_title = None 136 | 137 | # A shorter title for the navigation bar. Default is the same as html_title. 138 | # html_short_title = None 139 | 140 | # The name of an image file (relative to this directory) to place at the top 141 | # of the sidebar. 142 | html_logo = "_static/images/html_logo.png" 143 | 144 | # The name of an image file (within the static path) to use as favicon of the 145 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 146 | # pixels large. 147 | # html_favicon = None 148 | 149 | # Add any paths that contain custom static files (such as style sheets) here, 150 | # relative to this directory. They are copied after the builtin static files, 151 | # so a file named "default.css" will overwrite the builtin "default.css". 152 | html_static_path = ["_static"] 153 | 154 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 155 | # using the given strftime format. 156 | # html_last_updated_fmt = '%b %d, %Y' 157 | 158 | # If true, SmartyPants will be used to convert quotes and dashes to 159 | # typographically correct entities. 160 | # html_use_smartypants = True 161 | 162 | # Custom sidebar templates, maps document names to template names. 163 | # html_sidebars = {} 164 | 165 | # Additional templates that should be rendered to pages, maps page names to 166 | # template names. 167 | # html_additional_pages = {} 168 | 169 | # If false, no module index is generated. 170 | # html_domain_indices = True 171 | 172 | # If false, no index is generated. 173 | # html_use_index = True 174 | 175 | # If true, the index is split into individual pages for each letter. 176 | # html_split_index = False 177 | 178 | # If true, links to the reST sources are added to the pages. 179 | # html_show_sourcelink = True 180 | 181 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 182 | # html_show_sphinx = True 183 | 184 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 185 | # html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages will 188 | # contain a tag referring to it. The value of this option must be the 189 | # base URL from which the finished HTML is served. 190 | # html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | # html_file_suffix = None 194 | 195 | # Output file base name for HTML help builder. 196 | htmlhelp_basename = "PDF Reportsdoc" 197 | 198 | 199 | # -- Options for LaTeX output -------------------------------------------------- 200 | 201 | latex_elements = { 202 | # The paper size ('letterpaper' or 'a4paper'). 203 | #'papersize': 'letterpaper', 204 | # The font size ('10pt', '11pt' or '12pt'). 205 | #'pointsize': '10pt', 206 | # Additional stuff for the LaTeX preamble. 207 | #'preamble': '', 208 | } 209 | 210 | # Grouping the document tree into LaTeX files. List of tuples 211 | # (source start file, target name, title, author, documentclass [howto/manual]). 212 | latex_documents = [ 213 | ("index", "PDF Reports.tex", "PDF Reports Documentation", "Zulko", "manual"), 214 | ] 215 | 216 | # The name of an image file (relative to this directory) to place at the top of 217 | # the title page. 218 | # latex_logo = None 219 | 220 | # For "manual" documents, if this is true, then toplevel headings are parts, 221 | # not chapters. 222 | # latex_use_parts = False 223 | 224 | # If true, show page references after internal links. 225 | # latex_show_pagerefs = False 226 | 227 | # If true, show URL addresses after external links. 228 | # latex_show_urls = False 229 | 230 | # Documents to append as an appendix to all manuals. 231 | # latex_appendices = [] 232 | 233 | # If false, no module index is generated. 234 | # latex_domain_indices = True 235 | 236 | 237 | # -- Options for manual page output -------------------------------------------- 238 | 239 | # One entry per manual page. List of tuples 240 | # (source start file, name, description, authors, manual section). 241 | man_pages = [("index", "PDF Reports", "PACKAGE_NAME Documentation", ["Zulko"], 1)] 242 | 243 | # If true, show URL addresses after external links. 244 | # man_show_urls = False 245 | 246 | 247 | # -- Options for Texinfo output ------------------------------------------------ 248 | 249 | # Grouping the document tree into Texinfo files. List of tuples 250 | # (source start file, target name, title, author, 251 | # dir menu entry, description, category) 252 | texinfo_documents = [ 253 | ( 254 | "index", 255 | "PDF Reports", 256 | "PDF Reports Documentation", 257 | "Zulko", 258 | "PDF Reports", 259 | "One line description of project.", 260 | "Miscellaneous", 261 | ), 262 | ] 263 | 264 | # Documents to append as an appendix to all manuals. 265 | # texinfo_appendices = [] 266 | 267 | # If false, no module index is generated. 268 | # texinfo_domain_indices = True 269 | 270 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 271 | # texinfo_show_urls = 'footnote' 272 | 273 | # If true, do not generate a @detailmenu in the "Top" node's menu. 274 | # texinfo_no_detailmenu = False 275 | 276 | 277 | # -- Options for Epub output --------------------------------------------------- 278 | 279 | # Bibliographic Dublin Core info. 280 | epub_title = "PDF Reports" 281 | epub_author = "Zulko" 282 | epub_publisher = "Zulko" 283 | epub_copyright = "2016, Zulko" 284 | 285 | # The language of the text. It defaults to the language option 286 | # or en if the language is not set. 287 | # epub_language = '' 288 | 289 | # The scheme of the identifier. Typical schemes are ISBN or URL. 290 | # epub_scheme = '' 291 | 292 | # The unique identifier of the text. This can be a ISBN number 293 | # or the project homepage. 294 | # epub_identifier = '' 295 | 296 | # A unique identification for the text. 297 | # epub_uid = '' 298 | 299 | # A tuple containing the cover image and cover page html template filenames. 300 | # epub_cover = () 301 | 302 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 303 | # epub_guide = () 304 | 305 | # HTML files that should be inserted before the pages created by sphinx. 306 | # The format is a list of tuples containing the path and title. 307 | # epub_pre_files = [] 308 | 309 | # HTML files shat should be inserted after the pages created by sphinx. 310 | # The format is a list of tuples containing the path and title. 311 | # epub_post_files = [] 312 | 313 | # A list of files that should not be packed into the epub file. 314 | # epub_exclude_files = [] 315 | 316 | # The depth of the table of contents in toc.ncx. 317 | # epub_tocdepth = 3 318 | 319 | # Allow duplicate toc entries. 320 | # epub_tocdup = True 321 | 322 | # Fix unsupported image types using the PIL. 323 | # epub_fix_images = False 324 | 325 | # Scale large images. 326 | # epub_max_image_width = 0 327 | 328 | # If 'no', URL addresses will not be shown. 329 | # epub_show_urls = 'inline' 330 | 331 | # If false, no index is generated. 332 | # epub_use_index = True 333 | 334 | # autodoc_member_order = 'bysource' 335 | -------------------------------------------------------------------------------- /pdf_reports/css/egf-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 17 | 25 | 31 | 38 | 45 | 52 | 59 | 65 | 72 | 78 | 84 | 87 | 90 | 96 | 101 | 106 | 111 | 116 | 122 | 127 | 131 | 134 | 136 | 137 | 138 | 181 | 182 | 183 | 184 | --------------------------------------------------------------------------------