Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a3e1b5b
changed the default image width handling to be None and have the rend…
Jan 28, 2026
1fa4eac
added the "rich" library and a renderer for it. also added tests.
Jan 28, 2026
33331b7
added missing requirement rich-pixels
Jan 28, 2026
9bfea95
improved the case when embed_images=False
Jan 28, 2026
e11ef7f
minor adjustment to the case when embed_images = False
Jan 28, 2026
323cb78
updated the getting started example with the new functionality
Jan 28, 2026
301ef62
added the possibility to provide a rendering function (instead of sel…
Jan 28, 2026
3f8b532
added embed_images argument to show
Jan 28, 2026
e40e7c3
added new allow_pandoc argument to to_docx()
Jan 28, 2026
88c9214
implemented basic table creating in docx when pandoc is not available.
Jan 28, 2026
2634784
implemented defailt image width handling
Jan 28, 2026
5dfb3c3
updated readme with new functions
Jan 28, 2026
0a4856e
v2.5.5 release candidate. Added rich backend for consoles. Added supp…
Jan 28, 2026
5d83cce
small fix for colab env
Jan 28, 2026
6c7c420
small typo fix
Jan 28, 2026
3521925
bugfix in error handling
Jan 28, 2026
1430067
updated the latex compiler syntax to be in line with all other configs
Jan 28, 2026
b125e37
removed pandoc from text->pdf pipelines
Jan 28, 2026
368f475
added lazy checking of latex compilers
Jan 28, 2026
f83eb6e
fixing configs, pdf engines etc.
Jan 28, 2026
9046828
still debugging
Jan 28, 2026
9ebb06d
tested if the converted file actually exists before raising an error
Jan 28, 2026
ab3cf41
added warning when no pdf engine is installed
Jan 28, 2026
0846b4c
fixing
Jan 28, 2026
04bb836
updated libreoffice path config handling
Jan 28, 2026
f007524
removed ignored tests
Jan 28, 2026
0f14ecf
added badges
Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Tests-No-Optional-Dependencies

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
# Run tests on multiple Python versions to ensure compatibility
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install -e . # Install pydocmaker in editable mode

- name: Run tests
run: |
python -m unittest discover tests -v
97 changes: 46 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
# pydocmaker

<div align="center">
<p align="center">
<img src="https://img.shields.io/github/v/release/TobiasGlaubach/pydocmaker">
<img src="https://img.shields.io/pypi/v/pydocmaker">
<!-- <img src="https://img.shields.io/pypi/dm/pydocmaker"> -->
<img src="https://img.shields.io/github/license/TobiasGlaubach/pydocmaker">
<img src="https://img.shields.io/pypi/pyversions/pydocmaker">
<img src="https://img.shields.io/codecov/c/github/TobiasGlaubach/pydocmaker">
<img src="https://github.com/TobiasGlaubach/pydocmaker/actions/workflows/main.yml/badge.svg">

![Icon](icon.png)
</p>

</div>
<div align="center">

Please find the full documentation at https://pydocmaker.readthedocs.io/en/latest/
![Icon](icon.png)

a minimal python document maker to create reports in the following formats:
</div>

- `pdf`: PDF
- `md`: Markdown
- `html`: HTML
- `json`: JSON
- `docx`:: Word docx
- `textile`: Textile Markup language (with images as attachments)
- `ipynb`: Jupyter/ IPython Notebooks
- `tex`: Latex Documents (with external images)
- `redmine`: Textile Markup language ready for uplaod to Redmine
A minimal easy to use python document maker to create reports in `pdf`, `md`, `html`, `docx`, `tex` and more formats. Written in pure python.


Written in pure python
**NOTE:** some functions will try to call pandoc and fall back if not found.
**NOTE:** exporting PDFs need a latex compiler such as pdflatex, lualatex, xelatex
- **NOTE:** some functions will try to call pandoc and fall back if not found.
- **NOTE:** exporting PDFs need optional dependencies, such as either a latex compiler or Microsoft Word, or Libreoffice.

Full documentation at https://pydocmaker.readthedocs.io/en/latest/

## Installation

Expand Down Expand Up @@ -53,39 +52,61 @@ doc.show()

import pydocmaker as pyd

doc = pyd.Doc() # basic doc where we always append to the end
doc = pyd.Doc() # basic doc. Workd like a list, We always append new content to the end
doc.add('dummy text') # adds raw text

# this is how to add parts to the document
doc.add_pre('this will be shown as preformatted') # preformatted
doc.add_md('This is some *fancy* `markdown` **text**') # markdown
doc.add_tex(r'\textbf{Hello, LaTeX!}') # latex
doc.add_table([['John Doe', "30"]], header=['Name', 'Age'], caption='example table') # table

# this is how to add an image from link
doc.add_image("https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png", caption='', children='', width=0.8)
doc.add_image("https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png", caption='Github Logo')

# this is how to add matplotlib figures to your report
import matplotlib.pyplot as plt
fig = plt.figure()
plt.plot([1,2,3], [6,5,7])
doc.add_image(fig, caption='Example figure', width=0.7)

# show will render and show a doc when in an iPython
# environment such as jupyter or colab, on the terminal
# it will fall back to use rich console instead
doc.show()
```

### Showing Documents in iPython
### "Showing" Documents in iPython/Terminal

the Doc class has a method called show which will detect if it is running in Ipython. If it does it will render the document and show it.
The desired rendering format can be set with the `engine` argument. Markdown, HTML, or PDF is possible.
the `Doc` class has a method called `show` which will detect if its running in `Ipython`. If it does it will render the document and show it.
If not it will fallback to a rich consiole and do its best to show the content on the terminal (on a terminal image support is very limited).
The desired rendering format can be set with the `engine` argument. `rich`, `markdown`, `HTML`, or `PDF` is possible.

In Ipython:
Any environment:
**NOTE**: when rendering with "rich" console image support is very limited, since images will be printed on the console as pixels (with the size being scaled down to the console width)

```python
doc.show()
doc.show('rich')
doc.show('rich', embed_images=False)
```

In `Ipython` (such as Jupyter or Colab) any of the following:

```python
doc.show('md')
```

Or:
```python
doc.show('html')
```
Or (**NOTE**: some IDEs do not support this and instead open a "save" dialog, but in a browser with jupyter this works):

Or:
```python
doc.show('pdf')
```

**NOTE**: some IDEs do not support the PDF option and instead open a "save" dialog, but in a browser with jupyter this works

### Exporting:

Expand Down Expand Up @@ -242,29 +263,3 @@ docx_bts = doc.to_docx("my/path/outfile_w32.docx", template=templatepath, templa
docx_bts = doc.to_docx("my/path/outfile_w32_comp.pdf", template=templatepath, template_params=metadata, use_w32=True, as_pdf=True, compress_images=True)
```



## Document Parts and Schema for them

The basic building blocks for a document are called `document parts` and are always either of type `dict` or type `str` (A string will automatically parsed as a text dict element).

Each document part has a `typ` field which states the type of document part and a `children` field, which can be either `string` or `list`. This way hirachical documents can be build if needed.

The `document-parts` are:
- `text`: holds text as string (`children`) which will inserted directly as raw text
- `markdown`: holds text as string (`children`) which will be rendered by markdown markup language before parsing into the documents
- `image`: holds all needed information to render an image in a report. The image data is saved as a string in base64 encoded format in the `imageblob` field. A `caption` (str) can be given which will be inserted below the image. The filename is given by the `children` field. The relative width can be given by the `width` field (float).
- `verbatim`: holds text as string (`children`) which will be inserted as preformatted text into the documents
- `iter`: a meta `document-part` which holds n sub `document-parts` in the `children` field which will be rendered and inserted into the documents in given order.

An example of the whole schema is given below.

```json
{
"text": {"typ": "text", "children": ""},
"markdown": {"typ": "markdown", "children": ""},
"image": {"typ": "image", "children": "", "imageblob": "", "caption": "", "width": 0.8},
"verbatim": {"typ": "verbatim", "children": ""},
"iter": {"typ": "iter", "children": [] }
}
```
499 changes: 385 additions & 114 deletions docs/s01_getting_started.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/s05_detailed_examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
"doc.add('dummy text')\n",
"doc.add({\"typ\": \"markdown\",\"children\": \"some dummy markdown text!\"})\n",
"doc.add_kw('verbatim', 'this text will be shown preformatted!')\n",
"doc.add_image('https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png', caption='', children='', width=0.8)\n",
"doc.add_image('https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png', caption='', children='')\n",
"\n",
"doc.show()"
]
Expand Down
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ latex
jinja2

docxcompose
docx-mailmerge
docx-mailmerge

rich
rich-pixels
19 changes: 10 additions & 9 deletions src/pydocmaker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
__version__ = '2.5.4'
__version__ = '2.5.5'

from pydocmaker.core import Doc, construct, constr, buildingblocks, print_to_pdf, get_latex_compiler, set_latex_compiler, make_pdf_from_tex, show_pdf
from pydocmaker.core import Doc, construct, constr, buildingblocks, print_to_pdf, make_pdf_from_tex, show_pdf, is_notebook
from pydocmaker.util import upload_report_to_redmine, bcolors, txtcolor, colors_dc

from pydocmaker.backend.ex_docx import DocxFile, DocxFileW32, can_use_libreoffice, can_use_w32_word
from pydocmaker.backend.ex_tex import can_run_pandoc
from pydocmaker.backend.pdf_maker_tex import get_all_installed_latex_compilers, get_latex_compiler

from pydocmaker.backend.pandoc_api import pandoc_convert_file, pandoc_set_allowed

from pydocmaker.templating import DocTemplate, TemplateDirSource, register_new_template_dir, get_registered_template_dirs, get_available_template_ids, test_template_exists, remove_from_template_dir

from latex import escape as tex_escape


from pydocmaker.backend.libreoffice_api import config_libreoffice_path_get, config_libreoffice_path_set
from pydocmaker.core import config_pdf_engine_get, config_pdf_engine_set, config_pdf_engine_scan, config_pdf_engine_test
from pydocmaker.backend.libreoffice_api import config_libreoffice_path_get, config_libreoffice_path_set, config_libreoffice_path_find, config_libreoffice_path_testset
from pydocmaker.core import config_pdf_engine_get, config_pdf_engine_set, config_pdf_engine_scan, config_pdf_engine_test, config_renderer_default_get, config_renderer_default_set
from pydocmaker.backend.pdf_maker_tex import config_latex_compiler_scan, config_latex_compiler_get, config_latex_compiler_set, config_latex_compiler_testset

try:
# tests and caches already if pandoc is installed when import is used, so its faster later when we want to use it (or not)
Expand Down Expand Up @@ -180,13 +181,13 @@ def mk_pre(children=None, index=None, chapter=None, color='', end=None, **kwargs
return Doc().add_pre(children=children, index=index, chapter=chapter, color=color, end=end, **kwargs)[0]


def mk_fig(fig=None, caption='', width=0.8, bbox_inches='tight', children=None, color='', end=None, **kwargs):
def mk_fig(fig=None, caption='', width=None, bbox_inches='tight', children=None, color='', end=None, **kwargs):
"""make an image document part from a pyplot figure type dict from given image input.

Args:
fig (matplotlib figure, optional): the figure which to upload (or the current figure if None). Defaults to None.
caption (str, optional): the caption to give to the image. Defaults to ''.
width (float, optional): The width for the image to have in the document. Defaults to 0.8.
width (float, optional): The width for the image to have in the document None will let the individual formatter determine the width. Defaults to None.
bbox_inches (str, optional): will give better spacing for matplotlib figures.
children (str, optional): A specific name/id to give to the image (will be auto generated if None). Defaults to None.
index (int, optional): The index where to insert the part. If None, appends to the end.
Expand All @@ -199,7 +200,7 @@ def mk_fig(fig=None, caption='', width=0.8, bbox_inches='tight', children=None,
"""
return Doc().add_fig(fig=fig, caption=caption, width=width, bbox_inches=bbox_inches, children=children, color=color, end=end, **kwargs)[0]

def mk_image(image, caption='', width=0.8, children=None, color='', end=None, **kwargs):
def mk_image(image, caption='', width=None, children=None, color='', end=None, **kwargs):
"""Make an image type dict from given image input.

The image can be of type:
Expand All @@ -212,7 +213,7 @@ def mk_image(image, caption='', width=0.8, children=None, color='', end=None, **
Args:
image: The image input, which can be a pyplot figure, a link, a file-like object, a numpy array, or a PIL image.
caption (str, optional): The caption to give to the image. Defaults to ''.
width (float, optional): The width for the image to have in the document. Defaults to 0.8.
width (float, optional): The width for the image to have in the document None will let the individual formatter determine the width. Defaults to None.
children (str, optional): A specific name/id to give to the image (will be auto-generated if None). Defaults to None.
color (str, optional): Any color which can be rendered by HTML or LaTeX. Empty string for default. Defaults to ''.
end (str, optional): If you want to insert a different line ending (than the default) for this element, set this argument to any string. None for default.
Expand Down
11 changes: 7 additions & 4 deletions src/pydocmaker/backend/baseformatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def digest_markdown(self, children='', **kwargs) -> str:
pass

@abc.abstractmethod
def digest_image(self, children='', width=0.8, caption='', imageblob='', **kwargs) -> str:
def digest_image(self, children='', width=None, caption='', imageblob='', **kwargs) -> str:
pass

@abc.abstractmethod
Expand Down Expand Up @@ -137,10 +137,13 @@ def format(self, doc:list) -> str:



def _map_table2mat(self, children=None, **kwargs) -> str:
def _map_table2mat(self, children=None, fun=None, **kwargs) -> str:
if children is None:
children = [[]]

if fun is None:
fun = self.digest

assert isinstance(children, (list, tuple)), f'children must be of type list! but was {type(children)=} {children=}'
header = kwargs.get('header', None)
header = list(header) if header else []
Expand All @@ -157,7 +160,7 @@ def _map_table2mat(self, children=None, **kwargs) -> str:
if n_cols is None:
n_cols = max(len(header), max([len(row) for row in data]))

head = [self.digest(el) for el in header]
head = [fun(el) for el in header]
if len(head) < n_cols:
head += ['']*(n_cols-len(head))

Expand All @@ -167,7 +170,7 @@ def _map_table2mat(self, children=None, **kwargs) -> str:

for irow, row in enumerate(data):
for icol, el in enumerate(row):
mat[irow][icol] = self.digest(el)
mat[irow][icol] = fun(el)

return head, mat

Loading