├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.rst ├── LICENSE ├── Makefile ├── README.rst ├── dependabot.yml ├── docs ├── Makefile ├── api.rst ├── changelog.rst ├── conf.py ├── examples.rst ├── index.rst ├── requirements.txt └── tutorial.rst ├── examples ├── book.py └── multicolumn.py ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── resources ├── Core14_AFMs │ ├── Courier-Bold.afm │ ├── Courier-BoldOblique.afm │ ├── Courier-Oblique.afm │ ├── Courier.afm │ ├── Helvetica-Bold.afm │ ├── Helvetica-BoldOblique.afm │ ├── Helvetica-Oblique.afm │ ├── Helvetica.afm │ ├── MustRead.html │ ├── Symbol.afm │ ├── Times-Bold.afm │ ├── Times-BoldItalic.afm │ ├── Times-Italic.afm │ ├── Times-Roman.afm │ └── ZapfDingbats.afm ├── books │ └── the-great-gatsby.txt ├── fonts │ ├── CrimsonText-Bold.ttf │ ├── CrimsonText-BoldItalic.ttf │ ├── CrimsonText-Italic.ttf │ ├── CrimsonText-License.txt │ ├── CrimsonText-Regular.ttf │ ├── DejaVuLicense.txt │ ├── DejaVuSansCondensed-Bold.ttf │ ├── DejaVuSansCondensed-BoldOblique.ttf │ ├── DejaVuSansCondensed-Oblique.ttf │ └── DejaVuSansCondensed.ttf ├── optimal_vs_firstfit.py ├── sample.py └── scripts │ └── parse_afm.py ├── sample.png ├── src └── pdfje │ ├── __init__.py │ ├── atoms.py │ ├── common.py │ ├── compat.py │ ├── document.py │ ├── draw.py │ ├── fonts │ ├── __init__.py │ ├── builtins.py │ ├── common.py │ └── embed.py │ ├── layout │ ├── __init__.py │ ├── common.py │ ├── pages.py │ ├── paragraph.py │ └── rule.py │ ├── page.py │ ├── py.typed │ ├── resources.py │ ├── style.py │ ├── typeset │ ├── __init__.py │ ├── firstfit.py │ ├── hyphens.py │ ├── knuth_plass.py │ ├── layout.py │ ├── optimum.py │ ├── parse.py │ ├── state.py │ └── words.py │ ├── units.py │ └── vendor │ ├── __init__.py │ ├── get_kerning_pairs.py │ └── hyphenate.py ├── tests ├── __init__.py ├── common.py ├── conftest.py ├── layout │ ├── __init__.py │ ├── test_common.py │ ├── test_paragraph.py │ └── test_rule.py ├── test_atoms.py ├── test_common.py ├── test_document.py ├── test_draw.py ├── test_fonts.py ├── test_page.py ├── test_style.py ├── test_units.py └── typeset │ ├── __init__.py │ ├── test_firstfit.py │ ├── test_hyphens.py │ ├── test_knuth_plass.py │ ├── test_optimum.py │ ├── test_parse.py │ ├── test_state.py │ └── test_words.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "**" 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install "tox<5" tox-gh-actions "poetry>=1.7,<1.8" 29 | - name: Test with tox 30 | run: tox 31 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | pip-wheel-metadata/ 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | /.idea 104 | 105 | .vim 106 | 107 | ### macOS ### 108 | # General 109 | .DS_Store 110 | .AppleDouble 111 | .LSOverride 112 | 113 | # Icon must end with two \r 114 | Icon 115 | 116 | # Thumbnails 117 | ._* 118 | 119 | # Files that might appear in the root of a volume 120 | .DocumentRevisions-V100 121 | .fseventsd 122 | .Spotlight-V100 123 | .TemporaryItems 124 | .Trashes 125 | .VolumeIcon.icns 126 | .com.apple.timemachine.donotpresent 127 | 128 | # Directories potentially created on remote AFP share 129 | .AppleDB 130 | .AppleDesktop 131 | Network Trash Folder 132 | Temporary Items 133 | .apdisk 134 | 135 | .envrc 136 | *.csv 137 | *.pdf 138 | output/ 139 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | builder: html 5 | configuration: docs/conf.py 6 | fail_on_warning: true 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | python: 14 | install: 15 | - requirements: docs/requirements.txt 16 | - method: pip 17 | path: . 18 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.6.1 (2023-11-13) 5 | ------------------ 6 | 7 | - 🐍 Official Python 3.12 compatibility 8 | 9 | 0.6.0 (2023-08-15) 10 | ------------------ 11 | 12 | **Added** 13 | 14 | - 🧮 Paragraphs can be optimally typeset using the Knuth-Plass line 15 | breaking algorithm. Use the ``optimal`` argument for this. 16 | - 🛟 Paragraphs support automatically avoiding orphaned lines with 17 | ``avoid_orphans`` argument. 18 | 19 | **Breaking** 20 | 21 | - 📊 In the rare case that a paragraphs contains different text sizes, 22 | all lines now rendered with the same leading. 23 | This is more consistent and allows for faster layouting. 24 | 25 | **Fixed** 26 | 27 | - 🐍 Fix compatibility with Python 3.8 and 3.9 28 | 29 | 0.5.0 (2023-05-07) 30 | ------------------ 31 | 32 | **Breaking** 33 | 34 | - 🪆 Expose most classes from submodules instead of root 35 | (e.g. ``pdfje.Rect`` becomes ``pdfje.draw.Rect``). 36 | The new locations can be found in the API documentation. 37 | - 🏷️ ``Rule`` ``padding`` attribute renamed to ``margin``. 38 | 39 | **Added** 40 | 41 | - 📰 Support for horizontal alignment and justification of text. 42 | - 🫸 Support for indenting the first line of a paragraph. 43 | - ✂️ Automatic hyphenation of text. 44 | 45 | 0.4.0 (2023-04-10) 46 | ------------------ 47 | 48 | A big release with lots of new features and improvements. 49 | Most importantly, the page layout engine is now complete and 50 | can be used to create multi-page/column documents. 51 | 52 | **Added** 53 | 54 | - 📖 Automatic layout of multi-style text into lines, columns, and pages 55 | - 🔬 Automatic kerning for supported fonts 56 | - 🖌️ Support for drawing basic shapes 57 | - 🎨 Additional text styling options 58 | - 📦 Make fonttools dependency optional 59 | - 📏 Horizontal rule element 60 | 61 | **Documentation** 62 | 63 | - 🧑‍🏫 Add a tutorial and examples 64 | - 📋 Polished docstrings in public API 65 | 66 | **Performance** 67 | 68 | - ⛳️ Document pages and fonts are now written in one efficient pass 69 | 70 | **Breaking** 71 | 72 | - 🌅 Drop Python 3.7 support 73 | 74 | 0.3.0 (2022-12-02) 75 | ------------------ 76 | 77 | **Added** 78 | 79 | - 🍰 Documents can be created directly from string input 80 | - 🪜 Support for explicit newlines in text 81 | - 📢 ``Document.write()`` supports paths, file-like objects and iterator output 82 | - ✅ Improved PDF spec compliance 83 | 84 | **Changed** 85 | 86 | - 📚 Text is now positioned automatically within a page 87 | 88 | 0.2.0 (2022-12-01) 89 | ------------------ 90 | 91 | **Added** 92 | 93 | - 🖌️ Different builtin fonts can be selected 94 | - 📥 Truetype fonts can be embedded 95 | - 🌏 Support for non-ASCII text 96 | - 📐 Pages can be rotated 97 | - 🤏 Compression is applied to keep filesize small 98 | 99 | 0.1.0 (2022-11-02) 100 | ------------------ 101 | 102 | **Added** 103 | 104 | - 💬 Support basic ASCII text on different pages 105 | 106 | 0.0.1 (2022-10-28) 107 | ------------------ 108 | 109 | **Added** 110 | 111 | - 🌱 Write a valid, minimal, empty PDF file 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 - 2023 Arie Bovenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: init 2 | init: 3 | poetry install 4 | pip install -r docs/requirements.txt 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf .coverage .hypothesis .mypy_cache .pytest_cache .tox *.egg-info 9 | rm -rf dist 10 | find . | grep -E "(__pycache__|docs_.*$$|\.pyc|\.pyo$$)" | xargs rm -rf 11 | 12 | .PHONY: isort 13 | isort: 14 | isort . 15 | 16 | .PHONY: format 17 | format: 18 | black . 19 | 20 | .PHONY: fix 21 | fix: isort format 22 | 23 | .PHONY: lint 24 | lint: 25 | flake8 . 26 | 27 | .PHONY: mypy 28 | mypy: 29 | mypy --pretty --strict src examples/ 30 | mypy --pretty tests/ 31 | 32 | .PHONY: test 33 | test: 34 | pytest --cov=pdfje 35 | 36 | .PHONY: docs 37 | docs: 38 | @touch docs/api.rst 39 | make -C docs/ html 40 | 41 | .PHONY: publish 42 | publish: 43 | rm -rf dist/* 44 | poetry build 45 | twine upload dist/* 46 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 🌷 pdfje 2 | ======== 3 | 4 | .. image:: https://img.shields.io/pypi/v/pdfje.svg?style=flat-square&color=blue 5 | :target: https://pypi.python.org/pypi/pdfje 6 | 7 | .. image:: https://img.shields.io/pypi/pyversions/pdfje.svg?style=flat-square 8 | :target: https://pypi.python.org/pypi/pdfje 9 | 10 | .. image:: https://img.shields.io/pypi/l/pdfje.svg?style=flat-square&color=blue 11 | :target: https://pypi.python.org/pypi/pdfje 12 | 13 | .. image:: https://img.shields.io/badge/mypy-strict-forestgreen?style=flat-square 14 | :target: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict 15 | 16 | .. image:: https://img.shields.io/badge/coverage-99%25-forestgreen?style=flat-square 17 | :target: https://github.com/ariebovenberg/pdfje 18 | 19 | .. image:: https://img.shields.io/github/actions/workflow/status/ariebovenberg/pdfje/tests.yml?branch=main&style=flat-square 20 | :target: https://github.com/ariebovenberg/pdfje 21 | 22 | .. image:: https://img.shields.io/readthedocs/pdfje.svg?style=flat-square 23 | :target: http://pdfje.readthedocs.io/ 24 | 25 | .. 26 | 27 | **pdf·je** [`🔉 `_ PDF·yuh] (noun) Dutch for 'small PDF' 28 | 29 | **Write beautiful PDFs in declarative Python.** 30 | 31 | Features 32 | -------- 33 | 34 | What makes **pdfje** stand out from the other PDF writers? Here are some of the highlights: 35 | 36 | 🧩 Declarative API 37 | ~~~~~~~~~~~~~~~~~~ 38 | 39 | In most PDF writers, you create empty objects and 40 | then mutate them with methods like ``addText()``, 41 | all while changing the state with methods like ``setFont()``. 42 | **Pdfje** is different. You describe the document you want to write, 43 | and pdfje takes care of the details. No state to manage, no mutations. 44 | This makes your code easier to reuse and reason about. 45 | 46 | .. code-block:: python 47 | 48 | from pdfje import Document 49 | Document("Olá Mundo!").write("hello.pdf") 50 | 51 | See `the tutorial `_ 52 | for a complete overview of features, including: 53 | 54 | - Styling text including font, size, and color 55 | - Automatic layout of text into one or more columns 56 | - Builtin and embedded fonts 57 | - Drawing basic shapes 58 | 59 | See the roadmap_ for supported features. 60 | 61 | 📖 Decent typography 62 | ~~~~~~~~~~~~~~~~~~~~ 63 | 64 | Legibility counts. Good typography is a key part of that. 65 | **Pdfje** supports several features to make your documents look great: 66 | 67 | - Visually pleasing linebreaks, using the `same basic principles as LaTeX `_ 68 | - Automatic `kerning `_ using available font metrics 69 | - Avoiding `widows and orphans `_ by moving 70 | lines between columns or pages. 71 | 72 | .. image:: https://github.com/ariebovenberg/pdfje/raw/main/sample.png 73 | :alt: Sample document with two columns of text 74 | 75 | 🎈 Small footprint 76 | ~~~~~~~~~~~~~~~~~~ 77 | 78 | The PDF format supports many features, but most of the time you only need a few. 79 | Why install many dependencies — just to write a simple document? 80 | Not only is **pdfje** pure-Python, it allows you to 81 | install only the dependencies you need. 82 | 83 | .. code-block:: bash 84 | 85 | pip install pdfje # no dependencies 86 | pip install pdfje[fonts, hyphens] # embedded fonts and improved hyphenation 87 | 88 | .. _roadmap: 89 | 90 | Roadmap 91 | ------- 92 | 93 | **Pdfje** has basic functionality, 94 | but is not yet feature-complete. 95 | Until the 1.0 version, the API may change with minor releases. 96 | 97 | Features: 98 | 99 | ✅ = implemented, 🚧 = may be planned, ❌ = not planned 100 | 101 | - Typesetting 102 | - ✅ Automatic kerning 103 | - ✅ Wrapping text into lines, columns, and pages 104 | - ✅ Page sizes 105 | - ✅ Centering text 106 | - ✅ Justification 107 | - ✅ Hyphenation 108 | - ✅ Move lines between columns/pages to avoid widows/orphans 109 | - ✅ Tex-style line breaking 110 | - 🚧 Headings (which stick to their paragraphs) 111 | - 🚧 Indentation 112 | - 🚧 Keeping layout elements together 113 | - 🚧 Loosening paragraphs to avoid orphans/widows 114 | - 🚧 Broader unicode support in text wrapping 115 | - Drawing operations 116 | - ✅ Lines 117 | - ✅ Rectangles 118 | - ✅ Circles, ellipses 119 | - 🚧 Arbitrary paths, fills, and strokes 120 | - Text styling 121 | - ✅ Font and size 122 | - ✅ Embedded fonts 123 | - ✅ Colors 124 | - ✅ Bold, italic 125 | - 🚧 Underline and strikethrough 126 | - 🚧 Superscript and subscript 127 | - ❌ Complex fill patterns 128 | - 🚧 Images 129 | - 🚧 Bookmarks and links 130 | - 🚧 Tables 131 | - 🚧 Bullet/numbered lists 132 | - 🚧 Inline markup with Markdown (Commonmark/MyST) 133 | - ❌ Emoji 134 | - ❌ Tables of contents 135 | - ❌ Forms 136 | - ❌ Annotations 137 | 138 | Versioning and compatibility policy 139 | ----------------------------------- 140 | 141 | **Pdfje** follows semantic versioning. 142 | Until the 1.0 version, the API may change with minor releases. 143 | Breaking changes will be announced in the changelog. 144 | Since the API is fully typed, your typechecker and/or IDE 145 | will help you adjust to any API changes. 146 | 147 | License 148 | ------- 149 | 150 | This library is licensed under the terms of the MIT license. 151 | It also includes short scripts from other projects (see ``pdfje/vendor``), 152 | which are either also MIT licensed, or in the public domain. 153 | 154 | Contributing 155 | ------------ 156 | 157 | Here are some useful tips for developing in the ``pdfje`` codebase itself: 158 | 159 | - Install dependencies with ``poetry install``. 160 | - To write output files during tests, use ``pytest --output-path=`` 161 | - To also run more comprehensive but 'slow' tests, use ``pytest --runslow`` 162 | 163 | Acknowledgements 164 | ---------------- 165 | 166 | **pdfje** is inspired by the following projects. 167 | If you're looking for a PDF writer, you may want to check them out as well: 168 | 169 | - `python-typesetting `_ 170 | - `fpdf2 `_ 171 | - `ReportLab `_ 172 | - `WeasyPrint `_ 173 | - `borb `_ 174 | - `wkhtmltopdf `_ 175 | - `pydyf `_ 176 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Quiz 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API reference 4 | ============= 5 | 6 | Unless otherwise noted, all classes are immutable. 7 | 8 | pdfje 9 | ----- 10 | 11 | .. automodule:: pdfje 12 | :members: 13 | 14 | pdfje.style 15 | ----------- 16 | 17 | .. automodule:: pdfje.style 18 | :members: 19 | 20 | .. autodata:: pdfje.style.bold 21 | .. autodata:: pdfje.style.italic 22 | .. autodata:: pdfje.style.regular 23 | 24 | pdfje.layout 25 | ------------ 26 | 27 | .. automodule:: pdfje.layout 28 | :members: 29 | 30 | pdfje.draw 31 | ---------- 32 | 33 | .. automodule:: pdfje.draw 34 | :members: 35 | 36 | pdfje.fonts 37 | ----------- 38 | 39 | .. autodata:: pdfje.fonts.helvetica 40 | .. autodata:: pdfje.fonts.times_roman 41 | .. autodata:: pdfje.fonts.courier 42 | .. autodata:: pdfje.fonts.symbol 43 | .. autodata:: pdfje.fonts.zapf_dingbats 44 | 45 | .. automodule:: pdfje.fonts 46 | :members: 47 | 48 | 49 | pdfje.units 50 | ----------- 51 | 52 | .. automodule:: pdfje.units 53 | :members: 54 | 55 | 56 | **Page sizes** 57 | 58 | Below are common page sizes. 59 | Because the page size is a :class:`~pdfje.XY` object, you can use 60 | ``x`` and ``y`` attributes to get the width and height of a page size. 61 | The landscape variants can be obtained by calling :meth:`~pdfje.XY.flip`. 62 | 63 | .. code-block:: python 64 | 65 | from pdfje.units import A4 66 | 67 | A4.x # 595 68 | A4.y # 842 69 | A4.flip() # XY(842, 595) -- the landscape variant 70 | A4 / 2 # XY(297.5, 421) -- point at the center of the page 71 | 72 | .. autodata:: pdfje.units.A0 73 | .. autodata:: pdfje.units.A1 74 | .. autodata:: pdfje.units.A2 75 | .. autodata:: pdfje.units.A3 76 | .. autodata:: pdfje.units.A4 77 | .. autodata:: pdfje.units.A5 78 | .. autodata:: pdfje.units.A6 79 | .. autodata:: pdfje.units.letter 80 | .. autodata:: pdfje.units.legal 81 | .. autodata:: pdfje.units.tabloid 82 | .. autodata:: pdfje.units.ledger 83 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jun 13 22:58:12 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | from __future__ import annotations 17 | 18 | # -- Project information ----------------------------------------------------- 19 | import importlib.metadata 20 | 21 | metadata = importlib.metadata.metadata("pdfje") 22 | 23 | project = metadata["Name"] 24 | author = metadata["Author"] 25 | version = metadata["Version"] 26 | release = metadata["Version"] 27 | 28 | 29 | # -- General configuration ------------------------------------------------ 30 | 31 | extensions = [ 32 | "sphinx.ext.autodoc", 33 | "sphinx.ext.intersphinx", 34 | "sphinx.ext.napoleon", 35 | "sphinx.ext.viewcode", 36 | "sphinx_toolbox.collapse", 37 | "sphinx_autodoc_typehints", 38 | ] 39 | templates_path = ["_templates"] 40 | source_suffix = ".rst" 41 | 42 | master_doc = "index" 43 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 44 | 45 | # -- Options for HTML output ---------------------------------------------- 46 | 47 | autodoc_member_order = "bysource" 48 | html_theme = "furo" 49 | highlight_language = "python3" 50 | pygments_style = "default" 51 | intersphinx_mapping = { 52 | "python": ("https://docs.python.org/3", None), 53 | "pyphen": ("https://doc.courtbouillon.org/pyphen/stable/", None), 54 | } 55 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | Examples 4 | ======== 5 | 6 | The code for the examples can be found in the ``examples/`` directory. 7 | 8 | 📚 A book 9 | ~~~~~~~~~ 10 | 11 | This example shows: 12 | 13 | - Creating single pages and autogenerated ones 14 | - Page numbering 15 | - Simple graphics 16 | - Custom font 17 | 18 | .. collapse:: Source code (click to expand) 19 | 20 | .. literalinclude :: ../examples/book.py 21 | 22 | .. _multi-column: 23 | 24 | 📰 Multiple columns 25 | ~~~~~~~~~~~~~~~~~~~ 26 | 27 | This example shows the flexibility of the layout engine. 28 | 29 | .. collapse:: Source code (click to expand) 30 | 31 | .. literalinclude :: ../examples/multicolumn.py 32 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Contents 4 | ======== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | tutorial.rst 10 | examples.rst 11 | api.rst 12 | changelog.rst 13 | 14 | Indices and tables 15 | ================== 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx<8.3 2 | furo~=2024.8.6 3 | sphinx-toolbox~=4.0.0 4 | sphinx-autodoc-typehints~=3.2 5 | -------------------------------------------------------------------------------- /examples/book.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from pathlib import Path 5 | from typing import Iterable, Sequence 6 | 7 | from pdfje import XY, AutoPage, Document, Page 8 | from pdfje.draw import Ellipse, Rect, Text 9 | from pdfje.fonts import TrueType 10 | from pdfje.layout import Paragraph, Rule 11 | from pdfje.style import Style 12 | from pdfje.units import inch, mm 13 | 14 | 15 | def main() -> None: 16 | "Generate a PDF with the content of The Great Gatsby" 17 | Document( 18 | [TITLE_PAGE] 19 | + [AutoPage(blocks, template=create_page) for blocks in chapters()], 20 | style=CRIMSON, 21 | ).write("book.pdf") 22 | 23 | 24 | def create_page(num: int) -> Page: 25 | # Add a page number at the bottom of the base page 26 | return BASEPAGE.add( 27 | Text( 28 | (PAGESIZE.x / 2, mm(20)), str(num), Style(size=10), align="center" 29 | ) 30 | ) 31 | 32 | 33 | PAGESIZE = XY(inch(5), inch(8)) 34 | BASEPAGE = Page( 35 | [ 36 | # The title in small text at the top of the page 37 | Text( 38 | (PAGESIZE.x / 2, PAGESIZE.y - mm(10)), 39 | "The Great Gatsby", 40 | Style(size=10, italic=True), 41 | align="center", 42 | ), 43 | ], 44 | size=PAGESIZE, 45 | margin=(mm(20), mm(20), mm(25)), 46 | ) 47 | 48 | HEADING = Style(size=20, bold=True, line_spacing=3.5) 49 | 50 | TITLE_PAGE = Page( 51 | [ 52 | # Some nice shapes 53 | Rect( 54 | (PAGESIZE.x / 2 - 200, 275), # use page dimensions to center it 55 | width=400, 56 | height=150, 57 | fill="#99aaff", 58 | stroke=None, 59 | ), 60 | Ellipse((PAGESIZE.x / 2, 350), 300, 100, fill="#22d388"), 61 | # The title and author on top of the shapes 62 | Text( 63 | (PAGESIZE.x / 2, 380), 64 | "The Great Gatsby", 65 | Style(size=30, bold=True), 66 | align="center", 67 | ), 68 | Text( 69 | (PAGESIZE.x / 2, 335), 70 | "F. Scott Fitzgerald", 71 | Style(size=14, italic=True), 72 | align="center", 73 | ), 74 | ], 75 | size=PAGESIZE, 76 | ) 77 | CRIMSON = TrueType( 78 | Path(__file__).parent / "../resources/fonts/CrimsonText-Regular.ttf", 79 | Path(__file__).parent / "../resources/fonts/CrimsonText-Bold.ttf", 80 | Path(__file__).parent / "../resources/fonts/CrimsonText-Italic.ttf", 81 | Path(__file__).parent / "../resources/fonts/CrimsonText-BoldItalic.ttf", 82 | ) 83 | 84 | 85 | _CHAPTER_NUMERALS = set("I II III IV V VI VII VIII IX X".split()) 86 | 87 | 88 | def chapters() -> Iterable[Sequence[Paragraph | Rule]]: 89 | "Book content grouped by chapters" 90 | buffer: list[Paragraph | Rule] = [Paragraph("Chapter I\n", HEADING)] 91 | indent = 0 92 | for p in PARAGRAPHS: 93 | if p.strip() in _CHAPTER_NUMERALS: 94 | yield buffer 95 | buffer = [Paragraph(f"Chapter {p.strip()}\n", HEADING)] 96 | indent = 0 97 | elif p.startswith("------"): 98 | buffer.append(Rule("#aaaaaa", (20, 10, 10))) 99 | else: 100 | buffer.append( 101 | Paragraph( 102 | p, Style(line_spacing=1.2), align="justify", indent=indent 103 | ) 104 | ) 105 | indent = 15 106 | yield buffer 107 | 108 | 109 | PARAGRAPHS = [ 110 | m.replace("\n", " ") 111 | for m in re.split( 112 | r"\n\n", 113 | ( 114 | Path(__file__).parent / "../resources/books/the-great-gatsby.txt" 115 | ).read_text()[1374:-18415], 116 | ) 117 | ] 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /examples/multicolumn.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pdfje import AutoPage, Column, Document, Page 4 | from pdfje.fonts import times_roman 5 | from pdfje.layout import Paragraph 6 | from pdfje.style import Style, italic 7 | from pdfje.units import A3, A4, A6, inch, mm 8 | 9 | 10 | def main() -> None: 11 | "Generate a PDF with differently styled text layed out in various columns" 12 | Document( 13 | [ 14 | AutoPage( 15 | # Repeat the same text in different styles 16 | [Paragraph(LOREM_IPSUM, s) for s in STYLES * 3], 17 | # Cycle through the three page templates 18 | template=lambda i: TEMPLATES[i % 3], 19 | ) 20 | ] 21 | ).write("multicolumn.pdf") 22 | 23 | 24 | STYLES = [Style(size=10), "#225588" | italic, Style(size=15, font=times_roman)] 25 | TEMPLATES = [ 26 | # A one-column page 27 | Page(size=A6, margin=mm(15)), 28 | # A two-column page 29 | Page( 30 | columns=[ 31 | Column( 32 | (inch(1), inch(1)), 33 | width=(A4.x / 2) - inch(1.25), 34 | height=A4.y - inch(2), 35 | ), 36 | Column( 37 | (A4.x / 2 + inch(0.25), inch(1)), 38 | width=(A4.x / 2) - inch(1.25), 39 | height=A4.y - inch(2), 40 | ), 41 | ] 42 | ), 43 | # A page with three arbitrary columns 44 | Page( 45 | size=A3.flip(), 46 | columns=[ 47 | Column( 48 | (inch(1), inch(1)), 49 | width=(A3.y / 4), 50 | height=A3.x - inch(2), 51 | ), 52 | Column( 53 | (A3.y / 4 + inch(1.5), inch(5)), 54 | width=(A3.y / 2) - inch(1.25), 55 | height=A3.x - inch(8), 56 | ), 57 | Column( 58 | ((A3.y * 0.8) + inch(0.25), inch(4)), 59 | width=(A3.y / 10), 60 | height=inch(5), 61 | ), 62 | ], 63 | ), 64 | ] 65 | 66 | 67 | LOREM_IPSUM = """\ 68 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 69 | Integer sed aliquet justo. Donec eu ultricies velit, porta pharetra massa. \ 70 | Ut non augue a urna iaculis vulputate ut sit amet sem. \ 71 | Nullam lectus felis, rhoncus sed convallis a, egestas semper risus. \ 72 | Fusce gravida metus non vulputate vestibulum. \ 73 | Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere \ 74 | cubilia curae; Donec placerat suscipit velit. \ 75 | Mauris tincidunt lorem a eros eleifend tincidunt. \ 76 | Maecenas faucibus imperdiet massa quis pretium. Integer in lobortis nisi. \ 77 | Mauris at odio nec sem volutpat aliquam. Aliquam erat volutpat. \ 78 | 79 | Fusce at vehicula justo. Vestibulum eget viverra velit. \ 80 | Vivamus et nisi pulvinar, elementum lorem nec, volutpat leo. \ 81 | Aliquam erat volutpat. Sed tristique quis arcu vitae vehicula. \ 82 | Morbi egestas vel diam eget dapibus. Donec sit amet lorem turpis. \ 83 | Maecenas ultrices nunc vitae enim scelerisque tempus. \ 84 | Maecenas aliquet dui non hendrerit viverra. \ 85 | Aliquam fringilla, est sit amet gravida convallis, elit ipsum efficitur orci, \ 86 | eget convallis neque nunc nec lorem. Nam nisl sem, \ 87 | tristique a ultrices sed, finibus id enim. 88 | 89 | Etiam vel dolor ultricies, gravida felis in, vestibulum magna. \ 90 | In diam ex, elementum ut massa a, facilisis sollicitudin lacus. \ 91 | Integer lacus ante, ullamcorper ac mauris eget, rutrum facilisis velit. \ 92 | Mauris eu enim efficitur, malesuada ipsum nec, sodales enim. \ 93 | Nam ac tortor velit. Suspendisse ut leo a felis aliquam dapibus ut a justo. \ 94 | Vestibulum sed commodo tortor. Sed vitae enim ipsum. \ 95 | Duis pellentesque dui et ipsum suscipit, in semper odio dictum. \ 96 | 97 | Sed in fermentum leo. Donec maximus suscipit metus. \ 98 | Nulla convallis tortor mollis urna maximus mattis. \ 99 | Sed aliquet leo ac sem aliquam, et ultricies mauris maximus. \ 100 | Cras orci ex, fermentum nec purus non, molestie venenatis odio. \ 101 | Etiam vitae sollicitudin nisl. Sed a ullamcorper velit. \ 102 | 103 | Aliquam congue aliquet eros scelerisque hendrerit. Vestibulum quis ante ex. \ 104 | Fusce venenatis mauris dolor, nec mattis libero pharetra feugiat. \ 105 | Pellentesque habitant morbi tristique senectus et netus et malesuada \ 106 | fames ac turpis egestas. Cras vitae nisl molestie augue finibus lobortis. \ 107 | In hac habitasse platea dictumst. Maecenas rutrum interdum urna, \ 108 | ut finibus tortor facilisis ac. Donec in fringilla mi. \ 109 | Sed molestie accumsan nisi at mattis. \ 110 | Integer eget orci nec urna finibus porta. \ 111 | Sed eu dui vel lacus pulvinar blandit sed a urna. \ 112 | Quisque lacus arcu, mattis vel rhoncus hendrerit, dapibus sed massa. \ 113 | Vivamus sed massa est. In hac habitasse platea dictumst. \ 114 | Nullam volutpat sapien quis tincidunt sagittis. \ 115 | """ 116 | 117 | if __name__ == "__main__": 118 | main() 119 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disallow_untyped_defs = True 3 | warn_redundant_casts = True 4 | warn_unused_ignores = True 5 | warn_unreachable = True 6 | enable_error_code = redundant-expr 7 | 8 | [mypy-tests.*] 9 | check_untyped_defs = True 10 | disallow_untyped_defs = False 11 | warn_unreachable = True 12 | 13 | [mypy-fontTools.*] 14 | ignore_missing_imports = True 15 | 16 | [mypy-hypothesis.*] 17 | ignore_missing_imports = True 18 | 19 | [mypy-pyphen.*] 20 | ignore_missing_imports = True 21 | 22 | [mypy-pdfje.vendor.*] 23 | ignore_errors = True 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pdfje" 3 | version = "0.6.1" 4 | description = "Write beautiful PDFs in declarative Python" 5 | authors = ["Arie Bovenberg "] 6 | license = "MIT" 7 | classifiers = [ 8 | "Programming Language :: Python :: 3.8", 9 | "Programming Language :: Python :: 3.9", 10 | "Programming Language :: Python :: 3.10", 11 | "Programming Language :: Python :: 3.11", 12 | "Programming Language :: Python :: 3.12", 13 | ] 14 | packages = [ 15 | { include = "pdfje", from = "src" }, 16 | ] 17 | documentation = "https://pdfje.readthedocs.io" 18 | readme = "README.rst" 19 | include = ["CHANGELOG.rst", "README.rst"] 20 | repository = "https://github.com/ariebovenberg/pdfje" 21 | keywords = ["pdf"] 22 | 23 | [tool.poetry.dependencies] 24 | python = ">=3.8.1,<4.0" 25 | fonttools = {version="^4.38.0", optional=true} 26 | pyphen = {version=">=0.13.0", optional=true} 27 | 28 | [tool.poetry.extras] 29 | fonts = ["fonttools"] 30 | hyphens = ["pyphen"] 31 | 32 | [tool.poetry.group.test.dependencies] 33 | pytest = ">=7.0.1,<9.0.0" 34 | pytest-cov = ">=4,<6" 35 | pytest-benchmark = "^4.0.0" 36 | hypothesis = "^6.68.2" 37 | 38 | [tool.poetry.group.typecheck.dependencies] 39 | mypy = "^1.0.0" 40 | 41 | [tool.poetry.group.linting.dependencies] 42 | black = "^24" 43 | flake8 = ">=6,<8" 44 | isort = "^5.7.0" 45 | slotscheck = ">=0.17,<0.20" 46 | 47 | 48 | [tool.black] 49 | line-length = 79 50 | include = '\.pyi?$' 51 | exclude = ''' 52 | /( 53 | \.eggs 54 | | \.git 55 | | \.mypy_cache 56 | | \.tox 57 | | \.venv 58 | | _build 59 | | build 60 | | dist 61 | )/ 62 | ''' 63 | 64 | [tool.isort] 65 | line_length = 79 66 | profile = 'black' 67 | add_imports = ['from __future__ import annotations'] 68 | 69 | [tool.slotscheck] 70 | strict-imports = true 71 | require-superclass = true 72 | require-subclass = true 73 | exclude-modules = "^pdfje\\.vendor.*" 74 | 75 | [build-system] 76 | requires = ["poetry-core>=1.1.0"] 77 | build-backend = "poetry.core.masonry.api" 78 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --benchmark-disable 3 | markers = 4 | slow: marks tests as slow (deselect with '-m "not slow"') 5 | -------------------------------------------------------------------------------- /resources/Core14_AFMs/MustRead.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Core 14 AFM Files - ReadMe 7 | 8 | 9 | 10 | or 11 | 12 | 13 | 14 | 15 | 16 |
This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and distributed for any purpose and without charge, with or without modification, provided that all copyright notices are retained; that the AFM files are not distributed without this file; that all modifications to this file or any of the AFM files are prominently noted in the modified file(s); and that this paragraph is not modified. Adobe Systems has no responsibility or obligation to support the use of the AFM files. Col
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/Core14_AFMs/Symbol.afm: -------------------------------------------------------------------------------- 1 | StartFontMetrics 4.1 2 | Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All rights reserved. 3 | Comment Creation Date: Thu May 1 15:12:25 1997 4 | Comment UniqueID 43064 5 | Comment VMusage 30820 39997 6 | FontName Symbol 7 | FullName Symbol 8 | FamilyName Symbol 9 | Weight Medium 10 | ItalicAngle 0 11 | IsFixedPitch false 12 | CharacterSet Special 13 | FontBBox -180 -293 1090 1010 14 | UnderlinePosition -100 15 | UnderlineThickness 50 16 | Version 001.008 17 | Notice Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All rights reserved. 18 | EncodingScheme FontSpecific 19 | StdHW 92 20 | StdVW 85 21 | StartCharMetrics 190 22 | C 32 ; WX 250 ; N space ; B 0 0 0 0 ; 23 | C 33 ; WX 333 ; N exclam ; B 128 -17 240 672 ; 24 | C 34 ; WX 713 ; N universal ; B 31 0 681 705 ; 25 | C 35 ; WX 500 ; N numbersign ; B 20 -16 481 673 ; 26 | C 36 ; WX 549 ; N existential ; B 25 0 478 707 ; 27 | C 37 ; WX 833 ; N percent ; B 63 -36 771 655 ; 28 | C 38 ; WX 778 ; N ampersand ; B 41 -18 750 661 ; 29 | C 39 ; WX 439 ; N suchthat ; B 48 -17 414 500 ; 30 | C 40 ; WX 333 ; N parenleft ; B 53 -191 300 673 ; 31 | C 41 ; WX 333 ; N parenright ; B 30 -191 277 673 ; 32 | C 42 ; WX 500 ; N asteriskmath ; B 65 134 427 551 ; 33 | C 43 ; WX 549 ; N plus ; B 10 0 539 533 ; 34 | C 44 ; WX 250 ; N comma ; B 56 -152 194 104 ; 35 | C 45 ; WX 549 ; N minus ; B 11 233 535 288 ; 36 | C 46 ; WX 250 ; N period ; B 69 -17 181 95 ; 37 | C 47 ; WX 278 ; N slash ; B 0 -18 254 646 ; 38 | C 48 ; WX 500 ; N zero ; B 24 -14 476 685 ; 39 | C 49 ; WX 500 ; N one ; B 117 0 390 673 ; 40 | C 50 ; WX 500 ; N two ; B 25 0 475 685 ; 41 | C 51 ; WX 500 ; N three ; B 43 -14 435 685 ; 42 | C 52 ; WX 500 ; N four ; B 15 0 469 685 ; 43 | C 53 ; WX 500 ; N five ; B 32 -14 445 690 ; 44 | C 54 ; WX 500 ; N six ; B 34 -14 468 685 ; 45 | C 55 ; WX 500 ; N seven ; B 24 -16 448 673 ; 46 | C 56 ; WX 500 ; N eight ; B 56 -14 445 685 ; 47 | C 57 ; WX 500 ; N nine ; B 30 -18 459 685 ; 48 | C 58 ; WX 278 ; N colon ; B 81 -17 193 460 ; 49 | C 59 ; WX 278 ; N semicolon ; B 83 -152 221 460 ; 50 | C 60 ; WX 549 ; N less ; B 26 0 523 522 ; 51 | C 61 ; WX 549 ; N equal ; B 11 141 537 390 ; 52 | C 62 ; WX 549 ; N greater ; B 26 0 523 522 ; 53 | C 63 ; WX 444 ; N question ; B 70 -17 412 686 ; 54 | C 64 ; WX 549 ; N congruent ; B 11 0 537 475 ; 55 | C 65 ; WX 722 ; N Alpha ; B 4 0 684 673 ; 56 | C 66 ; WX 667 ; N Beta ; B 29 0 592 673 ; 57 | C 67 ; WX 722 ; N Chi ; B -9 0 704 673 ; 58 | C 68 ; WX 612 ; N Delta ; B 6 0 608 688 ; 59 | C 69 ; WX 611 ; N Epsilon ; B 32 0 617 673 ; 60 | C 70 ; WX 763 ; N Phi ; B 26 0 741 673 ; 61 | C 71 ; WX 603 ; N Gamma ; B 24 0 609 673 ; 62 | C 72 ; WX 722 ; N Eta ; B 39 0 729 673 ; 63 | C 73 ; WX 333 ; N Iota ; B 32 0 316 673 ; 64 | C 74 ; WX 631 ; N theta1 ; B 18 -18 623 689 ; 65 | C 75 ; WX 722 ; N Kappa ; B 35 0 722 673 ; 66 | C 76 ; WX 686 ; N Lambda ; B 6 0 680 688 ; 67 | C 77 ; WX 889 ; N Mu ; B 28 0 887 673 ; 68 | C 78 ; WX 722 ; N Nu ; B 29 -8 720 673 ; 69 | C 79 ; WX 722 ; N Omicron ; B 41 -17 715 685 ; 70 | C 80 ; WX 768 ; N Pi ; B 25 0 745 673 ; 71 | C 81 ; WX 741 ; N Theta ; B 41 -17 715 685 ; 72 | C 82 ; WX 556 ; N Rho ; B 28 0 563 673 ; 73 | C 83 ; WX 592 ; N Sigma ; B 5 0 589 673 ; 74 | C 84 ; WX 611 ; N Tau ; B 33 0 607 673 ; 75 | C 85 ; WX 690 ; N Upsilon ; B -8 0 694 673 ; 76 | C 86 ; WX 439 ; N sigma1 ; B 40 -233 436 500 ; 77 | C 87 ; WX 768 ; N Omega ; B 34 0 736 688 ; 78 | C 88 ; WX 645 ; N Xi ; B 40 0 599 673 ; 79 | C 89 ; WX 795 ; N Psi ; B 15 0 781 684 ; 80 | C 90 ; WX 611 ; N Zeta ; B 44 0 636 673 ; 81 | C 91 ; WX 333 ; N bracketleft ; B 86 -155 299 674 ; 82 | C 92 ; WX 863 ; N therefore ; B 163 0 701 487 ; 83 | C 93 ; WX 333 ; N bracketright ; B 33 -155 246 674 ; 84 | C 94 ; WX 658 ; N perpendicular ; B 15 0 652 674 ; 85 | C 95 ; WX 500 ; N underscore ; B -2 -125 502 -75 ; 86 | C 96 ; WX 500 ; N radicalex ; B 480 881 1090 917 ; 87 | C 97 ; WX 631 ; N alpha ; B 41 -18 622 500 ; 88 | C 98 ; WX 549 ; N beta ; B 61 -223 515 741 ; 89 | C 99 ; WX 549 ; N chi ; B 12 -231 522 499 ; 90 | C 100 ; WX 494 ; N delta ; B 40 -19 481 740 ; 91 | C 101 ; WX 439 ; N epsilon ; B 22 -19 427 502 ; 92 | C 102 ; WX 521 ; N phi ; B 28 -224 492 673 ; 93 | C 103 ; WX 411 ; N gamma ; B 5 -225 484 499 ; 94 | C 104 ; WX 603 ; N eta ; B 0 -202 527 514 ; 95 | C 105 ; WX 329 ; N iota ; B 0 -17 301 503 ; 96 | C 106 ; WX 603 ; N phi1 ; B 36 -224 587 499 ; 97 | C 107 ; WX 549 ; N kappa ; B 33 0 558 501 ; 98 | C 108 ; WX 549 ; N lambda ; B 24 -17 548 739 ; 99 | C 109 ; WX 576 ; N mu ; B 33 -223 567 500 ; 100 | C 110 ; WX 521 ; N nu ; B -9 -16 475 507 ; 101 | C 111 ; WX 549 ; N omicron ; B 35 -19 501 499 ; 102 | C 112 ; WX 549 ; N pi ; B 10 -19 530 487 ; 103 | C 113 ; WX 521 ; N theta ; B 43 -17 485 690 ; 104 | C 114 ; WX 549 ; N rho ; B 50 -230 490 499 ; 105 | C 115 ; WX 603 ; N sigma ; B 30 -21 588 500 ; 106 | C 116 ; WX 439 ; N tau ; B 10 -19 418 500 ; 107 | C 117 ; WX 576 ; N upsilon ; B 7 -18 535 507 ; 108 | C 118 ; WX 713 ; N omega1 ; B 12 -18 671 583 ; 109 | C 119 ; WX 686 ; N omega ; B 42 -17 684 500 ; 110 | C 120 ; WX 493 ; N xi ; B 27 -224 469 766 ; 111 | C 121 ; WX 686 ; N psi ; B 12 -228 701 500 ; 112 | C 122 ; WX 494 ; N zeta ; B 60 -225 467 756 ; 113 | C 123 ; WX 480 ; N braceleft ; B 58 -183 397 673 ; 114 | C 124 ; WX 200 ; N bar ; B 65 -293 135 707 ; 115 | C 125 ; WX 480 ; N braceright ; B 79 -183 418 673 ; 116 | C 126 ; WX 549 ; N similar ; B 17 203 529 307 ; 117 | C 160 ; WX 750 ; N Euro ; B 20 -12 714 685 ; 118 | C 161 ; WX 620 ; N Upsilon1 ; B -2 0 610 685 ; 119 | C 162 ; WX 247 ; N minute ; B 27 459 228 735 ; 120 | C 163 ; WX 549 ; N lessequal ; B 29 0 526 639 ; 121 | C 164 ; WX 167 ; N fraction ; B -180 -12 340 677 ; 122 | C 165 ; WX 713 ; N infinity ; B 26 124 688 404 ; 123 | C 166 ; WX 500 ; N florin ; B 2 -193 494 686 ; 124 | C 167 ; WX 753 ; N club ; B 86 -26 660 533 ; 125 | C 168 ; WX 753 ; N diamond ; B 142 -36 600 550 ; 126 | C 169 ; WX 753 ; N heart ; B 117 -33 631 532 ; 127 | C 170 ; WX 753 ; N spade ; B 113 -36 629 548 ; 128 | C 171 ; WX 1042 ; N arrowboth ; B 24 -15 1024 511 ; 129 | C 172 ; WX 987 ; N arrowleft ; B 32 -15 942 511 ; 130 | C 173 ; WX 603 ; N arrowup ; B 45 0 571 910 ; 131 | C 174 ; WX 987 ; N arrowright ; B 49 -15 959 511 ; 132 | C 175 ; WX 603 ; N arrowdown ; B 45 -22 571 888 ; 133 | C 176 ; WX 400 ; N degree ; B 50 385 350 685 ; 134 | C 177 ; WX 549 ; N plusminus ; B 10 0 539 645 ; 135 | C 178 ; WX 411 ; N second ; B 20 459 413 737 ; 136 | C 179 ; WX 549 ; N greaterequal ; B 29 0 526 639 ; 137 | C 180 ; WX 549 ; N multiply ; B 17 8 533 524 ; 138 | C 181 ; WX 713 ; N proportional ; B 27 123 639 404 ; 139 | C 182 ; WX 494 ; N partialdiff ; B 26 -20 462 746 ; 140 | C 183 ; WX 460 ; N bullet ; B 50 113 410 473 ; 141 | C 184 ; WX 549 ; N divide ; B 10 71 536 456 ; 142 | C 185 ; WX 549 ; N notequal ; B 15 -25 540 549 ; 143 | C 186 ; WX 549 ; N equivalence ; B 14 82 538 443 ; 144 | C 187 ; WX 549 ; N approxequal ; B 14 135 527 394 ; 145 | C 188 ; WX 1000 ; N ellipsis ; B 111 -17 889 95 ; 146 | C 189 ; WX 603 ; N arrowvertex ; B 280 -120 336 1010 ; 147 | C 190 ; WX 1000 ; N arrowhorizex ; B -60 220 1050 276 ; 148 | C 191 ; WX 658 ; N carriagereturn ; B 15 -16 602 629 ; 149 | C 192 ; WX 823 ; N aleph ; B 175 -18 661 658 ; 150 | C 193 ; WX 686 ; N Ifraktur ; B 10 -53 578 740 ; 151 | C 194 ; WX 795 ; N Rfraktur ; B 26 -15 759 734 ; 152 | C 195 ; WX 987 ; N weierstrass ; B 159 -211 870 573 ; 153 | C 196 ; WX 768 ; N circlemultiply ; B 43 -17 733 673 ; 154 | C 197 ; WX 768 ; N circleplus ; B 43 -15 733 675 ; 155 | C 198 ; WX 823 ; N emptyset ; B 39 -24 781 719 ; 156 | C 199 ; WX 768 ; N intersection ; B 40 0 732 509 ; 157 | C 200 ; WX 768 ; N union ; B 40 -17 732 492 ; 158 | C 201 ; WX 713 ; N propersuperset ; B 20 0 673 470 ; 159 | C 202 ; WX 713 ; N reflexsuperset ; B 20 -125 673 470 ; 160 | C 203 ; WX 713 ; N notsubset ; B 36 -70 690 540 ; 161 | C 204 ; WX 713 ; N propersubset ; B 37 0 690 470 ; 162 | C 205 ; WX 713 ; N reflexsubset ; B 37 -125 690 470 ; 163 | C 206 ; WX 713 ; N element ; B 45 0 505 468 ; 164 | C 207 ; WX 713 ; N notelement ; B 45 -58 505 555 ; 165 | C 208 ; WX 768 ; N angle ; B 26 0 738 673 ; 166 | C 209 ; WX 713 ; N gradient ; B 36 -19 681 718 ; 167 | C 210 ; WX 790 ; N registerserif ; B 50 -17 740 673 ; 168 | C 211 ; WX 790 ; N copyrightserif ; B 51 -15 741 675 ; 169 | C 212 ; WX 890 ; N trademarkserif ; B 18 293 855 673 ; 170 | C 213 ; WX 823 ; N product ; B 25 -101 803 751 ; 171 | C 214 ; WX 549 ; N radical ; B 10 -38 515 917 ; 172 | C 215 ; WX 250 ; N dotmath ; B 69 210 169 310 ; 173 | C 216 ; WX 713 ; N logicalnot ; B 15 0 680 288 ; 174 | C 217 ; WX 603 ; N logicaland ; B 23 0 583 454 ; 175 | C 218 ; WX 603 ; N logicalor ; B 30 0 578 477 ; 176 | C 219 ; WX 1042 ; N arrowdblboth ; B 27 -20 1023 510 ; 177 | C 220 ; WX 987 ; N arrowdblleft ; B 30 -15 939 513 ; 178 | C 221 ; WX 603 ; N arrowdblup ; B 39 2 567 911 ; 179 | C 222 ; WX 987 ; N arrowdblright ; B 45 -20 954 508 ; 180 | C 223 ; WX 603 ; N arrowdbldown ; B 44 -19 572 890 ; 181 | C 224 ; WX 494 ; N lozenge ; B 18 0 466 745 ; 182 | C 225 ; WX 329 ; N angleleft ; B 25 -198 306 746 ; 183 | C 226 ; WX 790 ; N registersans ; B 50 -20 740 670 ; 184 | C 227 ; WX 790 ; N copyrightsans ; B 49 -15 739 675 ; 185 | C 228 ; WX 786 ; N trademarksans ; B 5 293 725 673 ; 186 | C 229 ; WX 713 ; N summation ; B 14 -108 695 752 ; 187 | C 230 ; WX 384 ; N parenlefttp ; B 24 -293 436 926 ; 188 | C 231 ; WX 384 ; N parenleftex ; B 24 -85 108 925 ; 189 | C 232 ; WX 384 ; N parenleftbt ; B 24 -293 436 926 ; 190 | C 233 ; WX 384 ; N bracketlefttp ; B 0 -80 349 926 ; 191 | C 234 ; WX 384 ; N bracketleftex ; B 0 -79 77 925 ; 192 | C 235 ; WX 384 ; N bracketleftbt ; B 0 -80 349 926 ; 193 | C 236 ; WX 494 ; N bracelefttp ; B 209 -85 445 925 ; 194 | C 237 ; WX 494 ; N braceleftmid ; B 20 -85 284 935 ; 195 | C 238 ; WX 494 ; N braceleftbt ; B 209 -75 445 935 ; 196 | C 239 ; WX 494 ; N braceex ; B 209 -85 284 935 ; 197 | C 241 ; WX 329 ; N angleright ; B 21 -198 302 746 ; 198 | C 242 ; WX 274 ; N integral ; B 2 -107 291 916 ; 199 | C 243 ; WX 686 ; N integraltp ; B 308 -88 675 920 ; 200 | C 244 ; WX 686 ; N integralex ; B 308 -88 378 975 ; 201 | C 245 ; WX 686 ; N integralbt ; B 11 -87 378 921 ; 202 | C 246 ; WX 384 ; N parenrighttp ; B 54 -293 466 926 ; 203 | C 247 ; WX 384 ; N parenrightex ; B 382 -85 466 925 ; 204 | C 248 ; WX 384 ; N parenrightbt ; B 54 -293 466 926 ; 205 | C 249 ; WX 384 ; N bracketrighttp ; B 22 -80 371 926 ; 206 | C 250 ; WX 384 ; N bracketrightex ; B 294 -79 371 925 ; 207 | C 251 ; WX 384 ; N bracketrightbt ; B 22 -80 371 926 ; 208 | C 252 ; WX 494 ; N bracerighttp ; B 48 -85 284 925 ; 209 | C 253 ; WX 494 ; N bracerightmid ; B 209 -85 473 935 ; 210 | C 254 ; WX 494 ; N bracerightbt ; B 48 -75 284 935 ; 211 | C -1 ; WX 790 ; N apple ; B 56 -3 733 808 ; 212 | EndCharMetrics 213 | EndFontMetrics 214 | -------------------------------------------------------------------------------- /resources/Core14_AFMs/ZapfDingbats.afm: -------------------------------------------------------------------------------- 1 | StartFontMetrics 4.1 2 | Comment Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. All Rights Reserved. 3 | Comment Creation Date: Thu May 1 15:14:13 1997 4 | Comment UniqueID 43082 5 | Comment VMusage 45775 55535 6 | FontName ZapfDingbats 7 | FullName ITC Zapf Dingbats 8 | FamilyName ZapfDingbats 9 | Weight Medium 10 | ItalicAngle 0 11 | IsFixedPitch false 12 | CharacterSet Special 13 | FontBBox -1 -143 981 820 14 | UnderlinePosition -100 15 | UnderlineThickness 50 16 | Version 002.000 17 | Notice Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. All Rights Reserved.ITC Zapf Dingbats is a registered trademark of International Typeface Corporation. 18 | EncodingScheme FontSpecific 19 | StdHW 28 20 | StdVW 90 21 | StartCharMetrics 202 22 | C 32 ; WX 278 ; N space ; B 0 0 0 0 ; 23 | C 33 ; WX 974 ; N a1 ; B 35 72 939 621 ; 24 | C 34 ; WX 961 ; N a2 ; B 35 81 927 611 ; 25 | C 35 ; WX 974 ; N a202 ; B 35 72 939 621 ; 26 | C 36 ; WX 980 ; N a3 ; B 35 0 945 692 ; 27 | C 37 ; WX 719 ; N a4 ; B 34 139 685 566 ; 28 | C 38 ; WX 789 ; N a5 ; B 35 -14 755 705 ; 29 | C 39 ; WX 790 ; N a119 ; B 35 -14 755 705 ; 30 | C 40 ; WX 791 ; N a118 ; B 35 -13 761 705 ; 31 | C 41 ; WX 690 ; N a117 ; B 34 138 655 553 ; 32 | C 42 ; WX 960 ; N a11 ; B 35 123 925 568 ; 33 | C 43 ; WX 939 ; N a12 ; B 35 134 904 559 ; 34 | C 44 ; WX 549 ; N a13 ; B 29 -11 516 705 ; 35 | C 45 ; WX 855 ; N a14 ; B 34 59 820 632 ; 36 | C 46 ; WX 911 ; N a15 ; B 35 50 876 642 ; 37 | C 47 ; WX 933 ; N a16 ; B 35 139 899 550 ; 38 | C 48 ; WX 911 ; N a105 ; B 35 50 876 642 ; 39 | C 49 ; WX 945 ; N a17 ; B 35 139 909 553 ; 40 | C 50 ; WX 974 ; N a18 ; B 35 104 938 587 ; 41 | C 51 ; WX 755 ; N a19 ; B 34 -13 721 705 ; 42 | C 52 ; WX 846 ; N a20 ; B 36 -14 811 705 ; 43 | C 53 ; WX 762 ; N a21 ; B 35 0 727 692 ; 44 | C 54 ; WX 761 ; N a22 ; B 35 0 727 692 ; 45 | C 55 ; WX 571 ; N a23 ; B -1 -68 571 661 ; 46 | C 56 ; WX 677 ; N a24 ; B 36 -13 642 705 ; 47 | C 57 ; WX 763 ; N a25 ; B 35 0 728 692 ; 48 | C 58 ; WX 760 ; N a26 ; B 35 0 726 692 ; 49 | C 59 ; WX 759 ; N a27 ; B 35 0 725 692 ; 50 | C 60 ; WX 754 ; N a28 ; B 35 0 720 692 ; 51 | C 61 ; WX 494 ; N a6 ; B 35 0 460 692 ; 52 | C 62 ; WX 552 ; N a7 ; B 35 0 517 692 ; 53 | C 63 ; WX 537 ; N a8 ; B 35 0 503 692 ; 54 | C 64 ; WX 577 ; N a9 ; B 35 96 542 596 ; 55 | C 65 ; WX 692 ; N a10 ; B 35 -14 657 705 ; 56 | C 66 ; WX 786 ; N a29 ; B 35 -14 751 705 ; 57 | C 67 ; WX 788 ; N a30 ; B 35 -14 752 705 ; 58 | C 68 ; WX 788 ; N a31 ; B 35 -14 753 705 ; 59 | C 69 ; WX 790 ; N a32 ; B 35 -14 756 705 ; 60 | C 70 ; WX 793 ; N a33 ; B 35 -13 759 705 ; 61 | C 71 ; WX 794 ; N a34 ; B 35 -13 759 705 ; 62 | C 72 ; WX 816 ; N a35 ; B 35 -14 782 705 ; 63 | C 73 ; WX 823 ; N a36 ; B 35 -14 787 705 ; 64 | C 74 ; WX 789 ; N a37 ; B 35 -14 754 705 ; 65 | C 75 ; WX 841 ; N a38 ; B 35 -14 807 705 ; 66 | C 76 ; WX 823 ; N a39 ; B 35 -14 789 705 ; 67 | C 77 ; WX 833 ; N a40 ; B 35 -14 798 705 ; 68 | C 78 ; WX 816 ; N a41 ; B 35 -13 782 705 ; 69 | C 79 ; WX 831 ; N a42 ; B 35 -14 796 705 ; 70 | C 80 ; WX 923 ; N a43 ; B 35 -14 888 705 ; 71 | C 81 ; WX 744 ; N a44 ; B 35 0 710 692 ; 72 | C 82 ; WX 723 ; N a45 ; B 35 0 688 692 ; 73 | C 83 ; WX 749 ; N a46 ; B 35 0 714 692 ; 74 | C 84 ; WX 790 ; N a47 ; B 34 -14 756 705 ; 75 | C 85 ; WX 792 ; N a48 ; B 35 -14 758 705 ; 76 | C 86 ; WX 695 ; N a49 ; B 35 -14 661 706 ; 77 | C 87 ; WX 776 ; N a50 ; B 35 -6 741 699 ; 78 | C 88 ; WX 768 ; N a51 ; B 35 -7 734 699 ; 79 | C 89 ; WX 792 ; N a52 ; B 35 -14 757 705 ; 80 | C 90 ; WX 759 ; N a53 ; B 35 0 725 692 ; 81 | C 91 ; WX 707 ; N a54 ; B 35 -13 672 704 ; 82 | C 92 ; WX 708 ; N a55 ; B 35 -14 672 705 ; 83 | C 93 ; WX 682 ; N a56 ; B 35 -14 647 705 ; 84 | C 94 ; WX 701 ; N a57 ; B 35 -14 666 705 ; 85 | C 95 ; WX 826 ; N a58 ; B 35 -14 791 705 ; 86 | C 96 ; WX 815 ; N a59 ; B 35 -14 780 705 ; 87 | C 97 ; WX 789 ; N a60 ; B 35 -14 754 705 ; 88 | C 98 ; WX 789 ; N a61 ; B 35 -14 754 705 ; 89 | C 99 ; WX 707 ; N a62 ; B 34 -14 673 705 ; 90 | C 100 ; WX 687 ; N a63 ; B 36 0 651 692 ; 91 | C 101 ; WX 696 ; N a64 ; B 35 0 661 691 ; 92 | C 102 ; WX 689 ; N a65 ; B 35 0 655 692 ; 93 | C 103 ; WX 786 ; N a66 ; B 34 -14 751 705 ; 94 | C 104 ; WX 787 ; N a67 ; B 35 -14 752 705 ; 95 | C 105 ; WX 713 ; N a68 ; B 35 -14 678 705 ; 96 | C 106 ; WX 791 ; N a69 ; B 35 -14 756 705 ; 97 | C 107 ; WX 785 ; N a70 ; B 36 -14 751 705 ; 98 | C 108 ; WX 791 ; N a71 ; B 35 -14 757 705 ; 99 | C 109 ; WX 873 ; N a72 ; B 35 -14 838 705 ; 100 | C 110 ; WX 761 ; N a73 ; B 35 0 726 692 ; 101 | C 111 ; WX 762 ; N a74 ; B 35 0 727 692 ; 102 | C 112 ; WX 762 ; N a203 ; B 35 0 727 692 ; 103 | C 113 ; WX 759 ; N a75 ; B 35 0 725 692 ; 104 | C 114 ; WX 759 ; N a204 ; B 35 0 725 692 ; 105 | C 115 ; WX 892 ; N a76 ; B 35 0 858 705 ; 106 | C 116 ; WX 892 ; N a77 ; B 35 -14 858 692 ; 107 | C 117 ; WX 788 ; N a78 ; B 35 -14 754 705 ; 108 | C 118 ; WX 784 ; N a79 ; B 35 -14 749 705 ; 109 | C 119 ; WX 438 ; N a81 ; B 35 -14 403 705 ; 110 | C 120 ; WX 138 ; N a82 ; B 35 0 104 692 ; 111 | C 121 ; WX 277 ; N a83 ; B 35 0 242 692 ; 112 | C 122 ; WX 415 ; N a84 ; B 35 0 380 692 ; 113 | C 123 ; WX 392 ; N a97 ; B 35 263 357 705 ; 114 | C 124 ; WX 392 ; N a98 ; B 34 263 357 705 ; 115 | C 125 ; WX 668 ; N a99 ; B 35 263 633 705 ; 116 | C 126 ; WX 668 ; N a100 ; B 36 263 634 705 ; 117 | C 128 ; WX 390 ; N a89 ; B 35 -14 356 705 ; 118 | C 129 ; WX 390 ; N a90 ; B 35 -14 355 705 ; 119 | C 130 ; WX 317 ; N a93 ; B 35 0 283 692 ; 120 | C 131 ; WX 317 ; N a94 ; B 35 0 283 692 ; 121 | C 132 ; WX 276 ; N a91 ; B 35 0 242 692 ; 122 | C 133 ; WX 276 ; N a92 ; B 35 0 242 692 ; 123 | C 134 ; WX 509 ; N a205 ; B 35 0 475 692 ; 124 | C 135 ; WX 509 ; N a85 ; B 35 0 475 692 ; 125 | C 136 ; WX 410 ; N a206 ; B 35 0 375 692 ; 126 | C 137 ; WX 410 ; N a86 ; B 35 0 375 692 ; 127 | C 138 ; WX 234 ; N a87 ; B 35 -14 199 705 ; 128 | C 139 ; WX 234 ; N a88 ; B 35 -14 199 705 ; 129 | C 140 ; WX 334 ; N a95 ; B 35 0 299 692 ; 130 | C 141 ; WX 334 ; N a96 ; B 35 0 299 692 ; 131 | C 161 ; WX 732 ; N a101 ; B 35 -143 697 806 ; 132 | C 162 ; WX 544 ; N a102 ; B 56 -14 488 706 ; 133 | C 163 ; WX 544 ; N a103 ; B 34 -14 508 705 ; 134 | C 164 ; WX 910 ; N a104 ; B 35 40 875 651 ; 135 | C 165 ; WX 667 ; N a106 ; B 35 -14 633 705 ; 136 | C 166 ; WX 760 ; N a107 ; B 35 -14 726 705 ; 137 | C 167 ; WX 760 ; N a108 ; B 0 121 758 569 ; 138 | C 168 ; WX 776 ; N a112 ; B 35 0 741 705 ; 139 | C 169 ; WX 595 ; N a111 ; B 34 -14 560 705 ; 140 | C 170 ; WX 694 ; N a110 ; B 35 -14 659 705 ; 141 | C 171 ; WX 626 ; N a109 ; B 34 0 591 705 ; 142 | C 172 ; WX 788 ; N a120 ; B 35 -14 754 705 ; 143 | C 173 ; WX 788 ; N a121 ; B 35 -14 754 705 ; 144 | C 174 ; WX 788 ; N a122 ; B 35 -14 754 705 ; 145 | C 175 ; WX 788 ; N a123 ; B 35 -14 754 705 ; 146 | C 176 ; WX 788 ; N a124 ; B 35 -14 754 705 ; 147 | C 177 ; WX 788 ; N a125 ; B 35 -14 754 705 ; 148 | C 178 ; WX 788 ; N a126 ; B 35 -14 754 705 ; 149 | C 179 ; WX 788 ; N a127 ; B 35 -14 754 705 ; 150 | C 180 ; WX 788 ; N a128 ; B 35 -14 754 705 ; 151 | C 181 ; WX 788 ; N a129 ; B 35 -14 754 705 ; 152 | C 182 ; WX 788 ; N a130 ; B 35 -14 754 705 ; 153 | C 183 ; WX 788 ; N a131 ; B 35 -14 754 705 ; 154 | C 184 ; WX 788 ; N a132 ; B 35 -14 754 705 ; 155 | C 185 ; WX 788 ; N a133 ; B 35 -14 754 705 ; 156 | C 186 ; WX 788 ; N a134 ; B 35 -14 754 705 ; 157 | C 187 ; WX 788 ; N a135 ; B 35 -14 754 705 ; 158 | C 188 ; WX 788 ; N a136 ; B 35 -14 754 705 ; 159 | C 189 ; WX 788 ; N a137 ; B 35 -14 754 705 ; 160 | C 190 ; WX 788 ; N a138 ; B 35 -14 754 705 ; 161 | C 191 ; WX 788 ; N a139 ; B 35 -14 754 705 ; 162 | C 192 ; WX 788 ; N a140 ; B 35 -14 754 705 ; 163 | C 193 ; WX 788 ; N a141 ; B 35 -14 754 705 ; 164 | C 194 ; WX 788 ; N a142 ; B 35 -14 754 705 ; 165 | C 195 ; WX 788 ; N a143 ; B 35 -14 754 705 ; 166 | C 196 ; WX 788 ; N a144 ; B 35 -14 754 705 ; 167 | C 197 ; WX 788 ; N a145 ; B 35 -14 754 705 ; 168 | C 198 ; WX 788 ; N a146 ; B 35 -14 754 705 ; 169 | C 199 ; WX 788 ; N a147 ; B 35 -14 754 705 ; 170 | C 200 ; WX 788 ; N a148 ; B 35 -14 754 705 ; 171 | C 201 ; WX 788 ; N a149 ; B 35 -14 754 705 ; 172 | C 202 ; WX 788 ; N a150 ; B 35 -14 754 705 ; 173 | C 203 ; WX 788 ; N a151 ; B 35 -14 754 705 ; 174 | C 204 ; WX 788 ; N a152 ; B 35 -14 754 705 ; 175 | C 205 ; WX 788 ; N a153 ; B 35 -14 754 705 ; 176 | C 206 ; WX 788 ; N a154 ; B 35 -14 754 705 ; 177 | C 207 ; WX 788 ; N a155 ; B 35 -14 754 705 ; 178 | C 208 ; WX 788 ; N a156 ; B 35 -14 754 705 ; 179 | C 209 ; WX 788 ; N a157 ; B 35 -14 754 705 ; 180 | C 210 ; WX 788 ; N a158 ; B 35 -14 754 705 ; 181 | C 211 ; WX 788 ; N a159 ; B 35 -14 754 705 ; 182 | C 212 ; WX 894 ; N a160 ; B 35 58 860 634 ; 183 | C 213 ; WX 838 ; N a161 ; B 35 152 803 540 ; 184 | C 214 ; WX 1016 ; N a163 ; B 34 152 981 540 ; 185 | C 215 ; WX 458 ; N a164 ; B 35 -127 422 820 ; 186 | C 216 ; WX 748 ; N a196 ; B 35 94 698 597 ; 187 | C 217 ; WX 924 ; N a165 ; B 35 140 890 552 ; 188 | C 218 ; WX 748 ; N a192 ; B 35 94 698 597 ; 189 | C 219 ; WX 918 ; N a166 ; B 35 166 884 526 ; 190 | C 220 ; WX 927 ; N a167 ; B 35 32 892 660 ; 191 | C 221 ; WX 928 ; N a168 ; B 35 129 891 562 ; 192 | C 222 ; WX 928 ; N a169 ; B 35 128 893 563 ; 193 | C 223 ; WX 834 ; N a170 ; B 35 155 799 537 ; 194 | C 224 ; WX 873 ; N a171 ; B 35 93 838 599 ; 195 | C 225 ; WX 828 ; N a172 ; B 35 104 791 588 ; 196 | C 226 ; WX 924 ; N a173 ; B 35 98 889 594 ; 197 | C 227 ; WX 924 ; N a162 ; B 35 98 889 594 ; 198 | C 228 ; WX 917 ; N a174 ; B 35 0 882 692 ; 199 | C 229 ; WX 930 ; N a175 ; B 35 84 896 608 ; 200 | C 230 ; WX 931 ; N a176 ; B 35 84 896 608 ; 201 | C 231 ; WX 463 ; N a177 ; B 35 -99 429 791 ; 202 | C 232 ; WX 883 ; N a178 ; B 35 71 848 623 ; 203 | C 233 ; WX 836 ; N a179 ; B 35 44 802 648 ; 204 | C 234 ; WX 836 ; N a193 ; B 35 44 802 648 ; 205 | C 235 ; WX 867 ; N a180 ; B 35 101 832 591 ; 206 | C 236 ; WX 867 ; N a199 ; B 35 101 832 591 ; 207 | C 237 ; WX 696 ; N a181 ; B 35 44 661 648 ; 208 | C 238 ; WX 696 ; N a200 ; B 35 44 661 648 ; 209 | C 239 ; WX 874 ; N a182 ; B 35 77 840 619 ; 210 | C 241 ; WX 874 ; N a201 ; B 35 73 840 615 ; 211 | C 242 ; WX 760 ; N a183 ; B 35 0 725 692 ; 212 | C 243 ; WX 946 ; N a184 ; B 35 160 911 533 ; 213 | C 244 ; WX 771 ; N a197 ; B 34 37 736 655 ; 214 | C 245 ; WX 865 ; N a185 ; B 35 207 830 481 ; 215 | C 246 ; WX 771 ; N a194 ; B 34 37 736 655 ; 216 | C 247 ; WX 888 ; N a198 ; B 34 -19 853 712 ; 217 | C 248 ; WX 967 ; N a186 ; B 35 124 932 568 ; 218 | C 249 ; WX 888 ; N a195 ; B 34 -19 853 712 ; 219 | C 250 ; WX 831 ; N a187 ; B 35 113 796 579 ; 220 | C 251 ; WX 873 ; N a188 ; B 36 118 838 578 ; 221 | C 252 ; WX 927 ; N a189 ; B 35 150 891 542 ; 222 | C 253 ; WX 970 ; N a190 ; B 35 76 931 616 ; 223 | C 254 ; WX 918 ; N a191 ; B 34 99 884 593 ; 224 | EndCharMetrics 225 | EndFontMetrics 226 | -------------------------------------------------------------------------------- /resources/fonts/CrimsonText-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/CrimsonText-Bold.ttf -------------------------------------------------------------------------------- /resources/fonts/CrimsonText-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/CrimsonText-BoldItalic.ttf -------------------------------------------------------------------------------- /resources/fonts/CrimsonText-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/CrimsonText-Italic.ttf -------------------------------------------------------------------------------- /resources/fonts/CrimsonText-License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010 The Crimson Text Project Authors (https://github.com/googlefonts/Crimson) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /resources/fonts/CrimsonText-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/CrimsonText-Regular.ttf -------------------------------------------------------------------------------- /resources/fonts/DejaVuLicense.txt: -------------------------------------------------------------------------------- 1 | Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. 2 | Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) 3 | 4 | 5 | Bitstream Vera Fonts Copyright 6 | ------------------------------ 7 | 8 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is 9 | a trademark of Bitstream, Inc. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of the fonts accompanying this license ("Fonts") and associated 13 | documentation files (the "Font Software"), to reproduce and distribute the 14 | Font Software, including without limitation the rights to use, copy, merge, 15 | publish, distribute, and/or sell copies of the Font Software, and to permit 16 | persons to whom the Font Software is furnished to do so, subject to the 17 | following conditions: 18 | 19 | The above copyright and trademark notices and this permission notice shall 20 | be included in all copies of one or more of the Font Software typefaces. 21 | 22 | The Font Software may be modified, altered, or added to, and in particular 23 | the designs of glyphs or characters in the Fonts may be modified and 24 | additional glyphs or characters may be added to the Fonts, only if the fonts 25 | are renamed to names not containing either the words "Bitstream" or the word 26 | "Vera". 27 | 28 | This License becomes null and void to the extent applicable to Fonts or Font 29 | Software that has been modified and is distributed under the "Bitstream 30 | Vera" names. 31 | 32 | The Font Software may be sold as part of a larger software package but no 33 | copy of one or more of the Font Software typefaces may be sold by itself. 34 | 35 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 36 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, 38 | TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME 39 | FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING 40 | ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 41 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 42 | THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE 43 | FONT SOFTWARE. 44 | 45 | Except as contained in this notice, the names of Gnome, the Gnome 46 | Foundation, and Bitstream Inc., shall not be used in advertising or 47 | otherwise to promote the sale, use or other dealings in this Font Software 48 | without prior written authorization from the Gnome Foundation or Bitstream 49 | Inc., respectively. For further information, contact: fonts at gnome dot 50 | org. 51 | 52 | Arev Fonts Copyright 53 | ------------------------------ 54 | 55 | Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. 56 | 57 | Permission is hereby granted, free of charge, to any person obtaining 58 | a copy of the fonts accompanying this license ("Fonts") and 59 | associated documentation files (the "Font Software"), to reproduce 60 | and distribute the modifications to the Bitstream Vera Font Software, 61 | including without limitation the rights to use, copy, merge, publish, 62 | distribute, and/or sell copies of the Font Software, and to permit 63 | persons to whom the Font Software is furnished to do so, subject to 64 | the following conditions: 65 | 66 | The above copyright and trademark notices and this permission notice 67 | shall be included in all copies of one or more of the Font Software 68 | typefaces. 69 | 70 | The Font Software may be modified, altered, or added to, and in 71 | particular the designs of glyphs or characters in the Fonts may be 72 | modified and additional glyphs or characters may be added to the 73 | Fonts, only if the fonts are renamed to names not containing either 74 | the words "Tavmjong Bah" or the word "Arev". 75 | 76 | This License becomes null and void to the extent applicable to Fonts 77 | or Font Software that has been modified and is distributed under the 78 | "Tavmjong Bah Arev" names. 79 | 80 | The Font Software may be sold as part of a larger software package but 81 | no copy of one or more of the Font Software typefaces may be sold by 82 | itself. 83 | 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL 88 | TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | 94 | Except as contained in this notice, the name of Tavmjong Bah shall not 95 | be used in advertising or otherwise to promote the sale, use or other 96 | dealings in this Font Software without prior written authorization 97 | from Tavmjong Bah. For further information, contact: tavmjong @ free 98 | . fr. 99 | 100 | TeX Gyre DJV Math 101 | ----------------- 102 | Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. 103 | 104 | Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski 105 | (on behalf of TeX users groups) are in public domain. 106 | 107 | Letters imported from Euler Fraktur from AMSfonts are (c) American 108 | Mathematical Society (see below). 109 | Bitstream Vera Fonts Copyright 110 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera 111 | is a trademark of Bitstream, Inc. 112 | 113 | Permission is hereby granted, free of charge, to any person obtaining a copy 114 | of the fonts accompanying this license (“Fonts”) and associated 115 | documentation 116 | files (the “Font Software”), to reproduce and distribute the Font Software, 117 | including without limitation the rights to use, copy, merge, publish, 118 | distribute, 119 | and/or sell copies of the Font Software, and to permit persons to whom 120 | the Font Software is furnished to do so, subject to the following 121 | conditions: 122 | 123 | The above copyright and trademark notices and this permission notice 124 | shall be 125 | included in all copies of one or more of the Font Software typefaces. 126 | 127 | The Font Software may be modified, altered, or added to, and in particular 128 | the designs of glyphs or characters in the Fonts may be modified and 129 | additional 130 | glyphs or characters may be added to the Fonts, only if the fonts are 131 | renamed 132 | to names not containing either the words “Bitstream” or the word “Vera”. 133 | 134 | This License becomes null and void to the extent applicable to Fonts or 135 | Font Software 136 | that has been modified and is distributed under the “Bitstream Vera” 137 | names. 138 | 139 | The Font Software may be sold as part of a larger software package but 140 | no copy 141 | of one or more of the Font Software typefaces may be sold by itself. 142 | 143 | THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS 144 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, 145 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, 146 | TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME 147 | FOUNDATION 148 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, 149 | SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN 150 | ACTION 151 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR 152 | INABILITY TO USE 153 | THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 154 | Except as contained in this notice, the names of GNOME, the GNOME 155 | Foundation, 156 | and Bitstream Inc., shall not be used in advertising or otherwise to promote 157 | the sale, use or other dealings in this Font Software without prior written 158 | authorization from the GNOME Foundation or Bitstream Inc., respectively. 159 | For further information, contact: fonts at gnome dot org. 160 | 161 | AMSFonts (v. 2.2) copyright 162 | 163 | The PostScript Type 1 implementation of the AMSFonts produced by and 164 | previously distributed by Blue Sky Research and Y&Y, Inc. are now freely 165 | available for general use. This has been accomplished through the 166 | cooperation 167 | of a consortium of scientific publishers with Blue Sky Research and Y&Y. 168 | Members of this consortium include: 169 | 170 | Elsevier Science IBM Corporation Society for Industrial and Applied 171 | Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS) 172 | 173 | In order to assure the authenticity of these fonts, copyright will be 174 | held by 175 | the American Mathematical Society. This is not meant to restrict in any way 176 | the legitimate use of the fonts, such as (but not limited to) electronic 177 | distribution of documents containing these fonts, inclusion of these fonts 178 | into other public domain or commercial font collections or computer 179 | applications, use of the outline data to create derivative fonts and/or 180 | faces, etc. However, the AMS does require that the AMS copyright notice be 181 | removed from any derivative versions of the fonts which have been altered in 182 | any way. In addition, to ensure the fidelity of TeX documents using Computer 183 | Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, 184 | has requested that any alterations which yield different font metrics be 185 | given a different name. 186 | 187 | $Id$ 188 | -------------------------------------------------------------------------------- /resources/fonts/DejaVuSansCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/DejaVuSansCondensed-Bold.ttf -------------------------------------------------------------------------------- /resources/fonts/DejaVuSansCondensed-BoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/DejaVuSansCondensed-BoldOblique.ttf -------------------------------------------------------------------------------- /resources/fonts/DejaVuSansCondensed-Oblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/DejaVuSansCondensed-Oblique.ttf -------------------------------------------------------------------------------- /resources/fonts/DejaVuSansCondensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/resources/fonts/DejaVuSansCondensed.ttf -------------------------------------------------------------------------------- /resources/optimal_vs_firstfit.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import replace 4 | from pathlib import Path 5 | 6 | from pdfje import XY, AutoPage, Column, Document, Page 7 | from pdfje.draw import Text 8 | from pdfje.fonts import TrueType 9 | from pdfje.layout import Paragraph 10 | from pdfje.layout.paragraph import LinebreakParams 11 | from pdfje.style import Span, Style, italic 12 | from pdfje.units import inch, mm 13 | 14 | 15 | def main() -> None: 16 | Document( 17 | [ 18 | AutoPage( 19 | [*content, *(replace(p, optimal=False) for p in content)], 20 | template=TEMPLATE, 21 | ) 22 | ], 23 | style=CRIMSON, 24 | ).write("optimal-vs-firstfit.pdf") 25 | 26 | 27 | PAGESIZE = XY(inch(10), inch(8)) 28 | MARGIN = mm(16) 29 | TEMPLATE = Page( 30 | [ 31 | # The title in small text at the top of the page 32 | Text( 33 | (PAGESIZE.x / 4, PAGESIZE.y - mm(5)), 34 | "Optimal", 35 | Style(size=12, bold=True), 36 | align="center", 37 | ), 38 | Text( 39 | (PAGESIZE.x * 0.75, PAGESIZE.y - mm(5)), 40 | "Fast", 41 | Style(size=12, bold=True), 42 | align="center", 43 | ), 44 | ], 45 | size=PAGESIZE, 46 | columns=[ 47 | Column( 48 | (MARGIN, MARGIN), 49 | PAGESIZE.x / 2 - MARGIN * 2, 50 | PAGESIZE.y - MARGIN * 2, 51 | ), 52 | Column( 53 | (PAGESIZE.x / 2 + MARGIN, MARGIN), 54 | PAGESIZE.x / 2 - MARGIN * 2, 55 | PAGESIZE.y - MARGIN * 2, 56 | ), 57 | ], 58 | ) 59 | 60 | CRIMSON = TrueType( 61 | Path(__file__).parent / "../resources/fonts/CrimsonText-Regular.ttf", 62 | Path(__file__).parent / "../resources/fonts/CrimsonText-Bold.ttf", 63 | Path(__file__).parent / "../resources/fonts/CrimsonText-Italic.ttf", 64 | Path(__file__).parent / "../resources/fonts/CrimsonText-BoldItalic.ttf", 65 | ) 66 | 67 | 68 | def flatten_newlines(txt: str) -> str: 69 | return "\n".join(s.replace("\n", " ") for s in txt.split("\n\n")) 70 | 71 | 72 | # Extract from https://www.gutenberg.org/ebooks/1661 73 | content = [ 74 | Paragraph( 75 | [ 76 | flatten_newlines( 77 | """\ 78 | “To the man who loves art for its own sake,” remarked Sherlock 79 | Holmes, tossing aside the advertisement sheet of""" 80 | ), 81 | Span(" The Daily Telegraph", italic), 82 | flatten_newlines( 83 | """, “it is 84 | frequently in its least important and lowliest manifestations that the 85 | keenest pleasure is to be derived. It is pleasant to me to observe, 86 | Watson, that you have so far grasped this truth that in these little 87 | records of our cases which you have been good enough to draw up, and, I 88 | am bound to say, occasionally to embellish, you have given prominence 89 | not so much to the many """ 90 | ), 91 | Span("causes célèbres", italic), 92 | flatten_newlines( 93 | """ and sensational trials in 94 | which I have figured but rather to those incidents which may have been 95 | trivial in themselves, but which have given room for those faculties of 96 | deduction and of logical synthesis which I have made my special 97 | province.”""" 98 | ), 99 | ], 100 | align="justify", 101 | indent=0, 102 | optimal=LinebreakParams( 103 | tolerance=1, 104 | hyphen_penalty=0, 105 | ), 106 | avoid_orphans=False, 107 | ), 108 | Paragraph( 109 | [ 110 | flatten_newlines( 111 | """\ 112 | “And yet,” said I, smiling, “I cannot quite hold myself absolved from 113 | the charge of sensationalism which has been urged against my records.” 114 | 115 | “You have erred, perhaps,” he observed, taking up a glowing cinder with 116 | the tongs and lighting with it the long cherry-wood pipe which was wont 117 | to replace his clay when he was in a disputatious rather than a 118 | meditative mood—“you have erred perhaps in attempting to put colour and 119 | life into each of your statements instead of confining yourself to the 120 | task of placing upon record that severe reasoning from cause to effect 121 | which is really the only notable feature about the thing.” 122 | 123 | “It seems to me that I have done you full justice in the matter,” I 124 | remarked with some coldness, for I was repelled by the egotism which I 125 | had more than once observed to be a strong factor in my friend’s 126 | singular character. 127 | 128 | 129 | 130 | 131 | """ 132 | ), 133 | ], 134 | align="justify", 135 | indent=18, 136 | optimal=LinebreakParams( 137 | tolerance=3, 138 | hyphen_penalty=1000, 139 | ), 140 | avoid_orphans=False, 141 | ), 142 | ] 143 | 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /resources/sample.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from pdfje import XY, AutoPage, Column, Document, Page 6 | from pdfje.draw import Text 7 | from pdfje.fonts import TrueType 8 | from pdfje.layout import Paragraph 9 | from pdfje.layout.paragraph import LinebreakParams 10 | from pdfje.style import Span, Style, italic 11 | from pdfje.units import inch, mm 12 | 13 | 14 | def main() -> None: 15 | Document([AutoPage(chapter, template=TEMPLATE)], style=CRIMSON).write( 16 | "sample.pdf" 17 | ) 18 | 19 | 20 | PAGESIZE = XY(inch(8), inch(3.6)) 21 | TEMPLATE = Page( 22 | [ 23 | # The title in small text at the top of the page 24 | Text( 25 | (PAGESIZE.x / 2, PAGESIZE.y - mm(5)), 26 | "The Adventures of Sherlock Holmes", 27 | Style(size=10, italic=True), 28 | align="center", 29 | ), 30 | ], 31 | size=PAGESIZE, 32 | columns=[ 33 | Column( 34 | (mm(15), mm(15)), 35 | PAGESIZE.x / 2 - mm(30), 36 | PAGESIZE.y - mm(30), 37 | ), 38 | Column( 39 | (PAGESIZE.x / 2 + mm(15), mm(15)), 40 | PAGESIZE.x / 2 - mm(30), 41 | PAGESIZE.y - mm(30), 42 | ), 43 | ], 44 | ) 45 | 46 | CRIMSON = TrueType( 47 | Path(__file__).parent / "../resources/fonts/CrimsonText-Regular.ttf", 48 | Path(__file__).parent / "../resources/fonts/CrimsonText-Bold.ttf", 49 | Path(__file__).parent / "../resources/fonts/CrimsonText-Italic.ttf", 50 | Path(__file__).parent / "../resources/fonts/CrimsonText-BoldItalic.ttf", 51 | ) 52 | 53 | 54 | def flatten_newlines(txt: str) -> str: 55 | return "\n".join(s.replace("\n", " ") for s in txt.split("\n\n")) 56 | 57 | 58 | # Extract from https://www.gutenberg.org/ebooks/1661 59 | chapter = Paragraph( 60 | [ 61 | flatten_newlines( 62 | """“To the man who loves art for its own sake,” remarked Sherlock 63 | Holmes, tossing aside the advertisement sheet of""" 64 | ), 65 | Span(" The Daily Telegraph", italic), 66 | flatten_newlines( 67 | """, “it is 68 | frequently in its least important and lowliest manifestations that the 69 | keenest pleasure is to be derived. It is pleasant to me to observe, 70 | Watson, that you have so far grasped this truth that in these little 71 | records of our cases which you have been good enough to draw up, and, I 72 | am bound to say, occasionally to embellish, you have given prominence 73 | not so much to the many """ 74 | ), 75 | Span("causes célèbres", italic), 76 | flatten_newlines( 77 | """ and sensational trials in 78 | which I have figured but rather to those incidents which may have been 79 | trivial in themselves, but which have given room for those faculties of 80 | deduction and of logical synthesis which I have made my special 81 | province.” 82 | 83 | “And yet,” said I, smiling, “I cannot quite hold myself absolved from 84 | the charge of sensationalism which has been urged against my records.” 85 | 86 | “You have erred, perhaps,” he observed, taking up a glowing cinder with 87 | the tongs and lighting with it the long cherry-wood pipe which was wont 88 | to replace his clay when he was in a disputatious rather than a 89 | meditative mood—“you have erred perhaps in attempting to put colour and 90 | life into each of your statements instead of confining yourself to the 91 | task of placing upon record that severe reasoning from cause to effect 92 | which is really the only notable feature about the thing.” 93 | 94 | “It seems to me that I have done you full justice in the matter,” I 95 | remarked with some coldness, for I was repelled by the egotism which I 96 | had more than once observed to be a strong factor in my friend’s 97 | singular character. 98 | 99 | “No, it is not selfishness or conceit,” said he, answering, as was his 100 | wont, my thoughts rather than my words. “If I claim full justice for my 101 | art, it is because it is an impersonal thing—a thing beyond myself. 102 | Crime is common. Logic is rare. Therefore it is upon the logic rather 103 | than upon the crime that you should dwell. You have degraded what 104 | should have been a course of lectures into a series of tales.” 105 | 106 | It was a cold morning of the early spring, and we sat after breakfast 107 | on either side of a cheery fire in the old room at Baker Street. A 108 | thick fog rolled down between the lines of dun-coloured houses, and the 109 | opposing windows loomed like dark, shapeless blurs through the heavy 110 | yellow wreaths. Our gas was lit and shone on the white cloth and 111 | glimmer of china and metal, for the table had not been cleared yet. 112 | Sherlock Holmes had been silent all the morning, dipping continuously 113 | into the advertisement columns of a succession of papers until at last, 114 | having apparently given up his search, he had emerged in no very sweet 115 | temper to lecture me upon my literary shortcomings. 116 | 117 | “At the same time,” he remarked after a pause, during which he had sat 118 | puffing at his long pipe and gazing down into the fire, “you can hardly 119 | be open to a charge of sensationalism, for out of these cases which you 120 | have been so kind as to interest yourself in, a fair proportion do not 121 | treat of crime, in its legal sense, at all. The small matter in which I 122 | endeavoured to help the King of Bohemia, the singular experience of 123 | Miss Mary Sutherland, the problem connected with the man with the 124 | twisted lip, and the incident of the noble bachelor, were all matters 125 | which are outside the pale of the law. But in avoiding the sensational, 126 | I fear that you may have bordered on the trivial.” 127 | 128 | “The end may have been so,” I answered, “but the methods I hold to have 129 | been novel and of interest.” 130 | 131 | “Pshaw, my dear fellow, what do the public, the great unobservant 132 | public, who could hardly tell a weaver by his tooth or a compositor by 133 | his left thumb, care about the finer shades of analysis and deduction! 134 | But, indeed, if you are trivial, I cannot blame you, for the days of 135 | the great cases are past. Man, or at least criminal man, has lost all 136 | enterprise and originality. As to my own little practice, it seems to 137 | be degenerating into an agency for recovering lost lead pencils and 138 | giving advice to young ladies from boarding-schools. I think that I 139 | have touched bottom at last, however. This note I had this morning 140 | marks my zero-point, I fancy. Read it!” He tossed a crumpled letter 141 | across to me. 142 | """ 143 | ), 144 | ], 145 | align="justify", 146 | indent=18, 147 | optimal=LinebreakParams( 148 | tolerance=1, 149 | hyphen_penalty=1000, 150 | ), 151 | avoid_orphans=False, 152 | ) 153 | 154 | 155 | if __name__ == "__main__": 156 | main() 157 | -------------------------------------------------------------------------------- /resources/scripts/parse_afm.py: -------------------------------------------------------------------------------- 1 | """Script to extract font metrics from .afm files. 2 | 3 | Usage: `python parse_afm.py ` 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | import sys 9 | 10 | from fontTools.afmLib import AFM 11 | 12 | 13 | def print_widths() -> None: 14 | f = AFM(sys.argv[1]) 15 | widths = { 16 | ord(char.strip() or char): f[name][1] for name, char in NAMES.items() 17 | } 18 | for k, v in sorted(widths.items()): 19 | print(f"{k}: {v},") 20 | 21 | 22 | def print_kern() -> None: 23 | f = AFM(sys.argv[1]) 24 | kern = { 25 | (NAMES[a], NAMES[b]): value 26 | for (a, b), value in f._kerning.items() 27 | # Ignore characters we can't encode cp1252 anyway 28 | if a in NAMES and b in NAMES 29 | } 30 | for k, v in sorted(kern.items()): 31 | print(f"{k}: {v},") 32 | 33 | 34 | NAMES = { 35 | "A": "A", 36 | "AE": "Æ", 37 | "Aacute": "Á", 38 | "Acircumflex": "Â", 39 | "Adieresis": "Ä", 40 | "Agrave": "À", 41 | "Aring": "Å", 42 | "Atilde": "Ã", 43 | "B": "B", 44 | "C": "C", 45 | "Ccedilla": "Ç", 46 | "D": "D", 47 | "E": "E", 48 | "Eacute": "É", 49 | "Ecircumflex": "Ê", 50 | "Edieresis": "Ë", 51 | "Egrave": "È", 52 | "Eth": "Ð", 53 | "Euro": "€", 54 | "F": "F", 55 | "G": "G", 56 | "H": "H", 57 | "I": "I", 58 | "Iacute": "Í", 59 | "Icircumflex": "Î", 60 | "Idieresis": "Ï", 61 | "Igrave": "Ì", 62 | "J": "J", 63 | "K": "K", 64 | "L": "L", 65 | "Lslash": "Ł", 66 | "M": "M", 67 | "N": "N", 68 | "Ntilde": "Ñ", 69 | "O": "O", 70 | "OE": "Œ", 71 | "Oacute": "Ó", 72 | "Ocircumflex": "Ô", 73 | "Odieresis": "Ö", 74 | "Ograve": "Ò", 75 | "Oslash": "Ø", 76 | "Otilde": "Õ", 77 | "P": "P", 78 | "Q": "Q", 79 | "R": "R", 80 | "S": "S", 81 | "Scaron": "Š", 82 | "T": "T", 83 | "Thorn": "Þ", 84 | "U": "U", 85 | "Uacute": "Ú", 86 | "Ucircumflex": "Û", 87 | "Udieresis": "Ü", 88 | "Ugrave": "Ù", 89 | "V": "V", 90 | "W": "W", 91 | "X": "X", 92 | "Y": "Y", 93 | "Yacute": "Ý", 94 | "Ydieresis": "Ÿ", 95 | "Z": "Z", 96 | "Zcaron": "Ž", 97 | "a": "a", 98 | "aacute": "á", 99 | "acircumflex": "â", 100 | "acute": " ́", 101 | "adieresis": "ä", 102 | "ae": "æ", 103 | "agrave": "à", 104 | "ampersand": "&", 105 | "aring": "å", 106 | "asciicircum": "^", 107 | "asciitilde": "~", 108 | "asterisk": "*", 109 | "at": "@", 110 | "atilde": "ã", 111 | "b": "b", 112 | "backslash": "\\", 113 | "bar": "|", 114 | "braceleft": "{", 115 | "braceright": "}", 116 | "bracketleft": "[", 117 | "bracketright": "]", 118 | "breve": " ̆", 119 | "brokenbar": "¦", 120 | "bullet": "•", 121 | "c": "c", 122 | "caron": "ˇ", 123 | "ccedilla": "ç", 124 | "cedilla": " ̧", 125 | "cent": "¢", 126 | "circumflex": "ˆ", 127 | "colon": ":", 128 | "comma": ",", 129 | "copyright": "©", 130 | "currency": "¤", 131 | "d": "d", 132 | "dagger": "†", 133 | "daggerdbl": "‡", 134 | "degree": "°", 135 | "dieresis": " ̈", 136 | "divide": "÷", 137 | "dollar": "$", 138 | "dotaccent": " ̇", 139 | "dotlessi": "ı", 140 | "e": "e", 141 | "eacute": "é", 142 | "ecircumflex": "ê", 143 | "edieresis": "ë", 144 | "egrave": "è", 145 | "eight": "8", 146 | "ellipsis": "…", 147 | "emdash": "—", 148 | "endash": "–", 149 | "equal": "=", 150 | "eth": "ð", 151 | "exclam": "!", 152 | "exclamdown": "¡", 153 | "f": "f", 154 | "fi": "fi", 155 | "five": "5", 156 | "fl": "fl", 157 | "florin": "ƒ", 158 | "four": "4", 159 | "fraction": "⁄", 160 | "g": "g", 161 | "germandbls": "ß", 162 | "grave": "`", 163 | "greater": ">", 164 | "guillemotleft": "«", 165 | "guillemotright": "»", 166 | "guilsinglleft": "‹", 167 | "guilsinglright": "›", 168 | "h": "h", 169 | "hungarumlaut": " ̋", 170 | "hyphen": "-", 171 | "i": "i", 172 | "iacute": "í", 173 | "icircumflex": "î", 174 | "idieresis": "ï", 175 | "igrave": "ì", 176 | "j": "j", 177 | "k": "k", 178 | "l": "l", 179 | "less": "<", 180 | "logicalnot": "¬", 181 | "lslash": "ł", 182 | "m": "m", 183 | "macron": " ̄", 184 | "minus": "−", 185 | "mu": "μ", 186 | "multiply": "×", 187 | "n": "n", 188 | "nine": "9", 189 | "ntilde": "ñ", 190 | "numbersign": "#", 191 | "o": "o", 192 | "oacute": "ó", 193 | "ocircumflex": "ô", 194 | "odieresis": "ö", 195 | "oe": "œ", 196 | "ogonek": " ̨", 197 | "ograve": "ò", 198 | "one": "1", 199 | "onehalf": "½", 200 | "onequarter": "¼", 201 | "onesuperior": "¹", 202 | "ordfeminine": "ª", 203 | "ordmasculine": "º", 204 | "oslash": "ø", 205 | "otilde": "õ", 206 | "p": "p", 207 | "paragraph": "¶", 208 | "parenleft": "(", 209 | "parenright": ")", 210 | "percent": "%", 211 | "period": ".", 212 | "periodcentered": "·", 213 | "perthousand": "‰", 214 | "plus": "+", 215 | "plusminus": "±", 216 | "q": "q", 217 | "question": "?", 218 | "questiondown": "¿", 219 | "quotedbl": '"', 220 | "quotedblbase": "„", 221 | "quotedblleft": "“", 222 | "quotedblright": "”", 223 | "quoteleft": "‘", 224 | "quoteright": "’", 225 | "quotesinglbase": "‚", 226 | "quotesingle": "'", 227 | "r": "r", 228 | "registered": "®", 229 | "ring": " ̊", 230 | "s": "s", 231 | "scaron": "š", 232 | "section": "§", 233 | "semicolon": ";", 234 | "seven": "7", 235 | "six": "6", 236 | "slash": "/", 237 | "space": " ", 238 | "sterling": "£", 239 | "t": "t", 240 | "thorn": "þ", 241 | "three": "3", 242 | "threequarters": "¾", 243 | "threesuperior": "³", 244 | "tilde": " ̃", 245 | "trademark": "™", 246 | "two": "2", 247 | "twosuperior": "²", 248 | "u": "u", 249 | "uacute": "ú", 250 | "ucircumflex": "û", 251 | "udieresis": "ü", 252 | "ugrave": "ù", 253 | "underscore": "_", 254 | "v": "v", 255 | "w": "w", 256 | "x": "x", 257 | "y": "y", 258 | "yacute": "ý", 259 | "ydieresis": "ÿ", 260 | "yen": "¥", 261 | "z": "z", 262 | "zcaron": "ž", 263 | "zero": "0", 264 | } 265 | 266 | ZAPF_NAMES = {} 267 | 268 | 269 | # sanity checks 270 | assert len(set(NAMES.values())) == len(NAMES) 271 | # for char in NAMES.values(): 272 | # char.encode("cp1252") 273 | 274 | 275 | if __name__ == "__main__": 276 | # print_widths() 277 | print_kern() 278 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ariebovenberg/pdfje/7d3aa4cee44d9d5b40ca462bd42297427bb0072b/sample.png -------------------------------------------------------------------------------- /src/pdfje/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .common import RGB, XY, black, blue, cyan, lime, magenta, red, yellow 4 | from .document import Document 5 | from .layout.pages import AutoPage 6 | from .page import Column, Page 7 | 8 | __version__ = __import__("importlib.metadata").metadata.version(__name__) 9 | 10 | __all__ = [ 11 | # document & pages 12 | "Document", 13 | "Page", 14 | "Column", 15 | "AutoPage", 16 | # helpers 17 | "red", 18 | "lime", 19 | "blue", 20 | "black", 21 | "yellow", 22 | "magenta", 23 | "cyan", 24 | # common 25 | "RGB", 26 | "XY", 27 | ] 28 | -------------------------------------------------------------------------------- /src/pdfje/atoms.py: -------------------------------------------------------------------------------- 1 | """Low-level PDF objects and operations""" 2 | 3 | from __future__ import annotations 4 | 5 | import abc 6 | import re 7 | from binascii import hexlify 8 | from dataclasses import dataclass 9 | from functools import partial 10 | from itertools import accumulate, chain, repeat, starmap 11 | from math import isfinite 12 | from secrets import token_bytes 13 | from typing import Collection, Iterable, Iterator, Sequence, Tuple 14 | from zlib import compress 15 | 16 | from .common import add_slots, setattr_frozen 17 | 18 | # PDF 32000-1:2008 (7.5.2) specifies that the header must be followed 19 | # immediately by a comment line containing at least 4 binary characters 20 | # of size 0x80 or greater. 21 | _HEADER = b"%PDF-1.7\n%\x80\x80\x80\x80 generated by pdfje\n" 22 | _FIRST_OFFSET = len(_HEADER) 23 | OBJ_ID_XREF, OBJ_ID_CATALOG, OBJ_ID_PAGETREE, OBJ_ID_RESOURCES = 0, 1, 2, 3 24 | ASCII = bytes 25 | Byte = int # 0-255 26 | # ID of an object in the PDF file. Always >=0 and unique within a file. 27 | ObjectID = int 28 | 29 | 30 | class Atom(abc.ABC): 31 | __slots__ = () 32 | 33 | @abc.abstractmethod 34 | def write(self) -> Iterable[bytes]: 35 | raise NotImplementedError() 36 | 37 | 38 | Object = Tuple[ObjectID, Atom] 39 | 40 | 41 | @add_slots 42 | @dataclass(frozen=True) 43 | class Bool(Atom): 44 | value: bool 45 | 46 | def write(self) -> Iterable[bytes]: 47 | raise NotImplementedError() 48 | 49 | 50 | @add_slots 51 | @dataclass(frozen=True) 52 | class Name(Atom): 53 | value: ASCII 54 | 55 | def write(self) -> Iterable[bytes]: 56 | return (b"/", self.value) 57 | 58 | 59 | def _sanitize_name_char(c: Byte) -> bytes: 60 | # PDF32000-1:2008 (7.3.5) says ASCII from 33-126 is OK, except "#" (0x23) 61 | # which is the escape character. 62 | if c == 0x23: 63 | return b"#23" 64 | elif 33 <= c <= 126: 65 | return c.to_bytes(1, "big") 66 | # We decide to keep spaces, but remove the rest 67 | elif c == 0x20: 68 | return b"#20" 69 | else: 70 | return b"" 71 | 72 | 73 | def sanitize_name(s: bytes) -> bytes: 74 | return b"".join(map(_sanitize_name_char, s)) 75 | 76 | 77 | @add_slots 78 | @dataclass(frozen=True) 79 | class Int(Atom): 80 | value: int 81 | 82 | def write(self) -> Iterable[bytes]: 83 | return (b"%i" % self.value,) 84 | 85 | 86 | @add_slots 87 | @dataclass(frozen=True) 88 | class Real(Atom): 89 | value: float 90 | 91 | def write(self) -> Iterable[bytes]: 92 | assert isfinite(self.value), "NaN and Inf not supported" 93 | return (b"%g" % self.value,) 94 | 95 | 96 | # NOTE: this name avoids confusion with typing.LiteralString 97 | @add_slots 98 | @dataclass(frozen=True) 99 | class LiteralStr(Atom): 100 | "See PDF32000-1:2008 (7.3.4.2)" 101 | value: bytes 102 | 103 | # FUTURE: support UTF-16BOM, but in a way that makes it explicit that 104 | # this does not support usage in text streams. 105 | # How to prevent accidential UTF-16BOM? 106 | def write(self) -> Iterable[bytes]: 107 | return (b"(", _escape(self.value), b")") 108 | 109 | 110 | @add_slots 111 | @dataclass(frozen=True) 112 | class HexString(Atom): 113 | value: bytes 114 | 115 | def write(self) -> Iterable[bytes]: 116 | return (b"<", hexlify(self.value), b">") 117 | 118 | 119 | @add_slots 120 | @dataclass(frozen=True) 121 | class Array(Atom): 122 | items: Iterable[Atom] 123 | 124 | def write(self) -> Iterable[bytes]: 125 | yield b"[" 126 | for i in self.items: 127 | yield from i.write() 128 | yield b" " 129 | yield b"]" 130 | 131 | 132 | @add_slots 133 | @dataclass(frozen=True, init=False) 134 | class Dictionary(Atom): 135 | content: Collection[tuple[ASCII, Atom]] 136 | 137 | def __init__(self, *content: tuple[ASCII, Atom]) -> None: 138 | setattr_frozen(self, "content", content) 139 | 140 | def write(self) -> Iterable[bytes]: 141 | yield from _write_dict(self.content) 142 | 143 | 144 | def _write_dict(d: Iterable[tuple[ASCII, Atom]]) -> Iterable[bytes]: 145 | yield b"<<\n" 146 | for key, value in d: 147 | yield b"/" 148 | yield key 149 | yield b" " 150 | yield from value.write() 151 | yield b"\n" 152 | yield b">>" 153 | 154 | 155 | @add_slots 156 | @dataclass(frozen=True) 157 | class Stream(Atom): 158 | content: Iterable[bytes] 159 | meta: Collection[tuple[ASCII, Atom]] = () 160 | 161 | def write(self) -> Iterable[bytes]: 162 | content = compress(b"".join(self.content)) 163 | yield from _write_dict( 164 | chain( 165 | self.meta, 166 | [ 167 | (b"Length", Int(len(content))), 168 | (b"Filter", Name(b"FlateDecode")), 169 | ], 170 | ) 171 | ) 172 | yield b"\nstream\n" 173 | yield content 174 | yield b"\nendstream" 175 | 176 | 177 | @add_slots 178 | @dataclass(frozen=True) 179 | class Ref(Atom): 180 | target: ObjectID 181 | 182 | def write(self) -> Iterable[bytes]: 183 | return (b"%i 0 R" % self.target,) 184 | 185 | 186 | def _write_obj(i: ObjectID, o: Atom) -> Iterable[bytes]: 187 | yield b"%i 0 obj\n" % i 188 | yield from o.write() 189 | yield b"\nendobj\n" 190 | 191 | 192 | # PDF32000:1-2008 (14.4) says the file ID must be unique byte string 193 | # identifying the file. They recommend MD5 hashing the contents, 194 | # file location, and time. 195 | # We'll take a shortcut here and just use random, which will do fine. 196 | # We use `secrets` to ensure this randomness cannot be manipulated. 197 | _generate_file_id = partial(token_bytes, 16) 198 | 199 | 200 | def _write_trailer( 201 | # offsets must be already sorted continuously ascending by object ID from 1 202 | offsets: Sequence[int], 203 | xref_offset: int, 204 | ) -> Iterable[bytes]: 205 | yield b"xref\n%i %i\n0000000000 65535 f \n" % ( 206 | OBJ_ID_XREF, 207 | len(offsets) + 1, 208 | ) 209 | yield from map(b"%010i 00000 n \n".__mod__, offsets) 210 | yield b"trailer\n" 211 | yield from _write_dict( 212 | ( 213 | (b"Root", Ref(OBJ_ID_CATALOG)), 214 | (b"Size", Int(len(offsets) + 1)), 215 | (b"ID", Array(repeat(HexString(_generate_file_id()), 2))), 216 | ) 217 | ) 218 | yield b"\nstartxref\n%i\n%%%%EOF\n" % xref_offset 219 | 220 | 221 | def write( 222 | # object IDs must ascend continuously from 4 onwards, 223 | # with the last three items being catalog, pagetree, 224 | # and resources (ids 1, 2, 3 respectively). 225 | objs: Iterable[Object], 226 | ) -> Iterator[bytes]: 227 | yield _HEADER 228 | offsets = [_FIRST_OFFSET] 229 | for chunk in map(b"".join, starmap(_write_obj, objs)): 230 | offsets.append(len(chunk)) 231 | yield chunk 232 | ( 233 | *offsets, 234 | catalog_offset, 235 | pagetree_offset, 236 | resources_offset, 237 | xref_offset, 238 | ) = accumulate(offsets) 239 | yield from _write_trailer( 240 | [catalog_offset, pagetree_offset, resources_offset] + offsets, 241 | xref_offset, 242 | ) 243 | 244 | 245 | _STRING_ESCAPES = { 246 | b"\\": b"\\\\", 247 | b"\n": b"\\n", 248 | b"\r": b"\\r", 249 | b"\t": b"\\t", 250 | b"\b": b"\\b", 251 | b"\f": b"\\f", 252 | b"(": b"\\(", 253 | b")": b"\\)", 254 | } 255 | 256 | 257 | def _replace_with_escape(m: re.Match[bytes]) -> bytes: 258 | return _STRING_ESCAPES[m.group()] 259 | 260 | 261 | _escape = partial( 262 | re.compile(b"(%b)" % b"|".join(map(re.escape, _STRING_ESCAPES))).sub, 263 | _replace_with_escape, 264 | ) 265 | -------------------------------------------------------------------------------- /src/pdfje/compat.py: -------------------------------------------------------------------------------- 1 | "Compatibility layer for various Python versions" 2 | from __future__ import annotations 3 | 4 | import sys 5 | from itertools import tee 6 | from typing import TYPE_CHECKING, Callable, Iterable, Iterator, TypeVar 7 | 8 | __all__ = ["pairwise", "cache"] 9 | 10 | 11 | if sys.version_info < (3, 10) or TYPE_CHECKING: # pragma: no cover 12 | T = TypeVar("T") 13 | 14 | def pairwise(i: Iterable[T]) -> Iterator[tuple[T, T]]: 15 | a, b = tee(i) 16 | next(b, None) 17 | return zip(a, b) 18 | 19 | else: 20 | from itertools import pairwise 21 | 22 | 23 | if sys.version_info < (3, 9) or TYPE_CHECKING: # pragma: no cover 24 | from functools import lru_cache 25 | 26 | _Tcall = TypeVar("_Tcall", bound=Callable[..., object]) 27 | 28 | def cache(func: _Tcall) -> _Tcall: 29 | return lru_cache(maxsize=None)(func) # type: ignore 30 | 31 | else: 32 | from functools import cache 33 | -------------------------------------------------------------------------------- /src/pdfje/document.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from dataclasses import dataclass 5 | from itertools import count, islice 6 | from pathlib import Path 7 | from typing import IO, Iterable, Iterator, final, overload 8 | 9 | from . import atoms 10 | from .atoms import OBJ_ID_PAGETREE, OBJ_ID_RESOURCES 11 | from .common import add_slots, flatten, setattr_frozen 12 | from .layout import Block, Paragraph 13 | from .layout.pages import AutoPage 14 | from .page import Page 15 | from .resources import Resources 16 | from .style import Style, StyleFull, StyleLike 17 | 18 | _OBJ_ID_FIRST_PAGE: atoms.ObjectID = OBJ_ID_RESOURCES + 1 19 | _OBJS_PER_PAGE = 2 20 | 21 | 22 | @final 23 | @add_slots 24 | @dataclass(frozen=True, init=False) 25 | class Document: 26 | """a PDF Document 27 | 28 | Parameters 29 | ---------- 30 | 31 | content 32 | The content of the document. 33 | style 34 | Change the default style of the document. 35 | 36 | Examples 37 | -------- 38 | 39 | Below are some examples of creating documents. 40 | 41 | >>> Document() # the minimal PDF -- one empty page 42 | >>> Document("hello world") # a document with pages of text 43 | >>> Document([ # document with explicit pages 44 | ... Page(...), 45 | ... AutoPage([LOREM_IPSUM, ZEN_OF_PYTHON]), 46 | ... Page(), 47 | ... ]) 48 | 49 | 50 | note 51 | ---- 52 | A document must contain at least one page to be valid 53 | """ 54 | 55 | pages: Iterable[Page | AutoPage] 56 | style: Style 57 | 58 | def __init__( 59 | self, 60 | content: Iterable[Page | AutoPage] | str | Block | None = None, 61 | style: StyleLike = Style.EMPTY, 62 | ) -> None: 63 | if content is None: 64 | content = [Page()] 65 | elif isinstance(content, str): 66 | content = [AutoPage([Paragraph(content)])] 67 | elif isinstance(content, Block): 68 | content = [AutoPage([content])] 69 | 70 | setattr_frozen(self, "pages", content) 71 | setattr_frozen(self, "style", Style.parse(style)) 72 | 73 | @overload 74 | def write(self) -> Iterator[bytes]: ... 75 | 76 | @overload 77 | def write(self, target: os.PathLike[str] | str | IO[bytes]) -> None: ... 78 | 79 | def write( # type: ignore[return] 80 | self, target: os.PathLike[str] | str | IO[bytes] | None = None 81 | ) -> Iterator[bytes] | None: 82 | """Write the document to a given target. If no target is given, 83 | outputs the binary PDF content iteratively. See examples below. 84 | 85 | Parameters 86 | ---------- 87 | target: ~os.PathLike | str | ~typing.IO[bytes] | None 88 | The target to write to. If not given, the PDF content is returned 89 | as an iterator. 90 | 91 | Returns 92 | ------- 93 | ~typing.Iterator[bytes] | None 94 | 95 | Examples 96 | -------- 97 | 98 | String, :class:`~pathlib.Path`, or :class:`~os.PathLike` target: 99 | 100 | >>> doc.write("myfolder/foo.pdf") 101 | >>> doc.write(Path.home() / "documents/foo.pdf") 102 | 103 | Files and file-like objects: 104 | 105 | >>> with open("my/file.pdf", 'wb') as f: 106 | ... doc.write(f) 107 | >>> doc.write(b:= BytesIO()) 108 | 109 | Iterator output is useful for streaming PDF contents. Below is 110 | an example of an HTTP request using the ``httpx`` library. 111 | 112 | >>> httpx.post("https://mysite.foo/upload", content=doc.write(), 113 | ... headers={"Content-Type": "application/pdf"}) 114 | """ 115 | if target is None: 116 | return self._write_iter() 117 | elif isinstance(target, (str, os.PathLike)): 118 | self._write_to_path(Path(os.fspath(target))) 119 | else: # i.e. IO[bytes] 120 | target.writelines(self._write_iter()) 121 | 122 | def _write_iter(self) -> Iterator[bytes]: 123 | return atoms.write(_doc_objects(self.pages, self.style.setdefault())) 124 | 125 | def _write_to_path(self, p: Path) -> None: 126 | with p.open("wb") as wfile: 127 | wfile.writelines(self.write()) 128 | 129 | 130 | def _doc_objects( 131 | items: Iterable[Page | AutoPage], style: StyleFull 132 | ) -> Iterator[atoms.Object]: 133 | res = Resources() 134 | obj_id = pagenum = 0 135 | # FUTURE: the scoping of `pagenum` is a bit tricky here. Find a better 136 | # way to do this -- or add a specific test. 137 | for pagenum, obj_id, page in zip( 138 | count(1), 139 | count(_OBJ_ID_FIRST_PAGE, step=_OBJS_PER_PAGE), 140 | flatten(p.render(res, style, pagenum + 1) for p in items), 141 | ): 142 | yield from page.to_atoms(obj_id) 143 | 144 | if not pagenum: 145 | raise RuntimeError( 146 | "Cannot write PDF document without at least one page" 147 | ) 148 | first_font_id = obj_id + _OBJS_PER_PAGE 149 | 150 | yield from res.to_objects(first_font_id) 151 | yield from _write_headers( 152 | (obj_id - _OBJ_ID_FIRST_PAGE) // _OBJS_PER_PAGE + 1, 153 | res.to_atoms(first_font_id), 154 | ) 155 | 156 | 157 | _CATALOG_OBJ = ( 158 | atoms.OBJ_ID_CATALOG, 159 | atoms.Dictionary( 160 | (b"Type", atoms.Name(b"Catalog")), 161 | (b"Pages", atoms.Ref(OBJ_ID_PAGETREE)), 162 | ), 163 | ) 164 | 165 | 166 | def _write_headers( 167 | num_pages: int, resources: atoms.Dictionary 168 | ) -> Iterable[atoms.Object]: 169 | yield _CATALOG_OBJ 170 | yield ( 171 | OBJ_ID_PAGETREE, 172 | atoms.Dictionary( 173 | (b"Type", atoms.Name(b"Pages")), 174 | ( 175 | b"Kids", 176 | atoms.Array( 177 | map( 178 | atoms.Ref, 179 | islice( 180 | count(_OBJ_ID_FIRST_PAGE, step=_OBJS_PER_PAGE), 181 | num_pages, 182 | ), 183 | ) 184 | ), 185 | ), 186 | (b"Count", atoms.Int(num_pages)), 187 | ), 188 | ) 189 | yield OBJ_ID_RESOURCES, resources 190 | -------------------------------------------------------------------------------- /src/pdfje/fonts/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .builtins import courier, helvetica, symbol, times_roman, zapf_dingbats 4 | from .common import BuiltinTypeface, TrueType 5 | 6 | __all__ = [ 7 | "helvetica", 8 | "times_roman", 9 | "courier", 10 | "symbol", 11 | "zapf_dingbats", 12 | "BuiltinTypeface", 13 | "TrueType", 14 | ] 15 | -------------------------------------------------------------------------------- /src/pdfje/fonts/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | from dataclasses import dataclass, field 5 | from itertools import chain, count 6 | from pathlib import Path 7 | from typing import TYPE_CHECKING, Iterable, Tuple, Union, final 8 | 9 | from .. import atoms 10 | from ..atoms import ASCII 11 | from ..common import ( 12 | Char, 13 | Func, 14 | Pos, 15 | Pt, 16 | add_slots, 17 | fix_abstract_properties, 18 | setattr_frozen, 19 | ) 20 | from ..compat import pairwise 21 | 22 | FontID = bytes # unique, internal identifier assigned to a font within a PDF 23 | GlyphPt = float # length unit in glyph space 24 | TEXTSPACE_TO_GLYPHSPACE = 1000 # See PDF32000-1:2008 (9.7.3) 25 | 26 | 27 | @fix_abstract_properties 28 | class Font(abc.ABC): 29 | """A specific font within a typeface""" 30 | 31 | __slots__ = () 32 | 33 | @property 34 | @abc.abstractmethod 35 | def id(self) -> FontID: ... 36 | 37 | # It's worth caching this value, as it is used often 38 | @property 39 | @abc.abstractmethod 40 | def spacewidth(self) -> GlyphPt: ... 41 | 42 | @property 43 | @abc.abstractmethod 44 | def encoding_width(self) -> int: 45 | """The number of bytes assigned to each character when encoding""" 46 | 47 | @abc.abstractmethod 48 | def encode(self, s: str, /) -> bytes: ... 49 | 50 | @abc.abstractmethod 51 | def width(self, s: str, /) -> Pt: 52 | """The total width of the given string (excluding kerning)""" 53 | 54 | @staticmethod 55 | @abc.abstractmethod 56 | def charwidth(c: Char, /) -> GlyphPt: ... 57 | 58 | @abc.abstractmethod 59 | def kern(self, s: str, /, prev: Char | None) -> Iterable[Kern]: ... 60 | 61 | @abc.abstractmethod 62 | def charkern(self, a: Char, b: Char, /) -> GlyphPt: ... 63 | 64 | 65 | @final 66 | @add_slots 67 | @dataclass(frozen=True, init=False) 68 | class TrueType: 69 | """A TrueType font to be embedded in a PDF 70 | 71 | Parameters 72 | ---------- 73 | regular 74 | The regular (i.e. non-bold, non-italic) .ttf file 75 | bold 76 | The bold .ttf file 77 | italic 78 | The italic .ttf file 79 | bold_italic 80 | The bold italic .ttf file 81 | 82 | """ 83 | 84 | regular: Path 85 | bold: Path 86 | italic: Path 87 | bold_italic: Path 88 | 89 | def __init__( 90 | self, 91 | regular: Path | str, 92 | bold: Path | str, 93 | italic: Path | str, 94 | bold_italic: Path | str, 95 | ) -> None: 96 | setattr_frozen(self, "regular", Path(regular)) 97 | setattr_frozen(self, "bold", Path(bold)) 98 | setattr_frozen(self, "italic", Path(italic)) 99 | setattr_frozen(self, "bold_italic", Path(bold_italic)) 100 | 101 | # This method cannot be defined in the class body, as it would cause a 102 | # circular import. The implementation is patched into the class 103 | # in the `style` module. 104 | if TYPE_CHECKING: # pragma: no cover 105 | from ..common import HexColor 106 | from ..style import Style, StyleLike 107 | 108 | def __or__(self, _: StyleLike, /) -> Style: ... 109 | 110 | def __ror__(self, _: HexColor, /) -> Style: ... 111 | 112 | def font(self, bold: bool, italic: bool) -> Path: 113 | if bold: 114 | return self.bold_italic if italic else self.bold 115 | else: 116 | return self.italic if italic else self.regular 117 | 118 | 119 | @final 120 | @add_slots 121 | @dataclass(frozen=True, repr=False) 122 | class BuiltinTypeface: 123 | """A typeface that is built into the PDF renderer.""" 124 | 125 | regular: BuiltinFont 126 | bold: BuiltinFont 127 | italic: BuiltinFont 128 | bold_italic: BuiltinFont 129 | 130 | # This method cannot be defined in the class body, as it would cause a 131 | # circular import. The implementation is patched into the class 132 | # in the `style` module. 133 | if TYPE_CHECKING: # pragma: no cover 134 | from ..common import HexColor 135 | from ..style import Style, StyleLike 136 | 137 | def __or__(self, _: StyleLike, /) -> Style: ... 138 | 139 | def __ror__(self, _: HexColor, /) -> Style: ... 140 | 141 | def __repr__(self) -> str: 142 | return f"{self.__class__.__name__}({self.regular.name.decode()})" 143 | 144 | def font(self, bold: bool, italic: bool) -> BuiltinFont: 145 | if bold: 146 | return self.bold_italic if italic else self.bold 147 | else: 148 | return self.italic if italic else self.regular 149 | 150 | 151 | Typeface = Union[BuiltinTypeface, TrueType] 152 | 153 | 154 | @final 155 | @add_slots 156 | @dataclass(frozen=True, eq=False) 157 | class BuiltinFont(Font): 158 | name: ASCII 159 | id: FontID 160 | charwidth: Func[Char, GlyphPt] = field(repr=False) 161 | kerning: KerningTable | None = field(repr=False) 162 | spacewidth: Pt = field(init=False, repr=False) 163 | 164 | encoding_width = 1 165 | 166 | def __post_init__(self) -> None: 167 | setattr_frozen(self, "spacewidth", self.charwidth(" ")) 168 | 169 | def width(self, s: str) -> Pt: 170 | return sum(map(self.charwidth, s)) / TEXTSPACE_TO_GLYPHSPACE 171 | 172 | @staticmethod 173 | def encode(s: str) -> bytes: 174 | # FUTURE: normalize unicode to allow better unicode representation 175 | return s.encode("cp1252", errors="replace") 176 | 177 | def kern(self, s: str, /, prev: Char | None) -> Iterable[Kern]: 178 | return kern(self.kerning, s, prev) if self.kerning else () 179 | 180 | def charkern(self, a: Char, b: Char) -> GlyphPt: 181 | return self.kerning((a, b)) if self.kerning else 0 182 | 183 | def to_resource(self) -> atoms.Dictionary: 184 | return atoms.Dictionary( 185 | (b"Type", atoms.Name(b"Font")), 186 | (b"Subtype", atoms.Name(b"Type1")), 187 | (b"BaseFont", atoms.Name(self.name)), 188 | (b"Encoding", atoms.Name(b"WinAnsiEncoding")), 189 | ) 190 | 191 | 192 | KerningTable = Func[Tuple[Char, Char], GlyphPt] 193 | Kern = Tuple[Pos, GlyphPt] 194 | 195 | 196 | def kern( 197 | table: KerningTable, 198 | s: str, 199 | prev: Char | None, 200 | ) -> Iterable[Kern]: 201 | for i, pair in zip( 202 | count(not prev), 203 | pairwise(chain(prev, s) if prev else s), 204 | ): 205 | if space := table(pair): 206 | yield (i, space) 207 | -------------------------------------------------------------------------------- /src/pdfje/layout/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .common import Block 4 | from .paragraph import LinebreakParams, Paragraph 5 | from .rule import Rule 6 | 7 | __all__ = ["Block", "Paragraph", "Rule", "LinebreakParams"] 8 | -------------------------------------------------------------------------------- /src/pdfje/layout/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | from dataclasses import dataclass 5 | from itertools import islice, tee 6 | from typing import Callable, Iterator, Sequence 7 | 8 | from ..common import ( 9 | XY, 10 | Streamable, 11 | add_slots, 12 | fix_abstract_properties, 13 | flatten, 14 | peek, 15 | prepend, 16 | ) 17 | from ..page import Column, Page 18 | from ..resources import Resources 19 | from ..style import StyleFull 20 | from ..units import Pt 21 | 22 | __all__ = [ 23 | "Block", 24 | ] 25 | 26 | 27 | class Block(abc.ABC): 28 | """Base class for block elements that can be laid out in a column 29 | by :class:`~pdfje.AutoPage`. 30 | """ 31 | 32 | __slots__ = () 33 | 34 | # Fill the given columns with this block's content. It may consume as many 35 | # columns as it needs to determine how to render itself. It should only 36 | # yield columns that are actually filled -- which may be fewer than it 37 | # consumed (e.g. if it needed to look ahead). 38 | # 39 | # Why not a generator? Because a block may need to consume multiple 40 | # columns to render itself, before starting to yield completed columns 41 | @abc.abstractmethod 42 | def into_columns( 43 | self, res: Resources, style: StyleFull, cs: Iterator[ColumnFill], / 44 | ) -> Iterator[ColumnFill]: ... 45 | 46 | 47 | @fix_abstract_properties 48 | class Shaped(abc.ABC): 49 | __slots__ = () 50 | 51 | # FUTURE: remove width from this interface. It can be set 52 | # on this object itself. 53 | @abc.abstractmethod 54 | def render(self, pos: XY, width: Pt) -> Streamable: ... 55 | 56 | @property 57 | @abc.abstractmethod 58 | def height(self) -> Pt: ... 59 | 60 | 61 | @add_slots 62 | @dataclass(frozen=True) 63 | class ColumnFill(Streamable): 64 | box: Column 65 | blocks: Sequence[tuple[XY, Shaped]] 66 | height_free: Pt 67 | 68 | @staticmethod 69 | def new(col: Column) -> ColumnFill: 70 | return ColumnFill(col, [], col.height) 71 | 72 | def add(self, s: Shaped) -> ColumnFill: 73 | return ColumnFill( 74 | self.box, 75 | (*self.blocks, (self.cursor(), s)), 76 | self.height_free - s.height, 77 | ) 78 | 79 | def cursor(self) -> XY: 80 | return self.box.origin.add_y(self.height_free) 81 | 82 | def __iter__(self) -> Iterator[bytes]: 83 | for loc, s in self.blocks: 84 | yield from s.render(loc, self.box.width) 85 | 86 | 87 | _ColumnFiller = Callable[[Iterator[ColumnFill]], Iterator[ColumnFill]] 88 | 89 | 90 | @add_slots 91 | @dataclass(frozen=True) 92 | class PageFill: 93 | base: Page 94 | todo: Sequence[ColumnFill] # in the order they will be filled 95 | done: Sequence[ColumnFill] # most recently filled last 96 | 97 | def reopen_most_recent_column(self) -> PageFill: 98 | return PageFill(self.base, (self.done[-1], *self.todo), self.done[:-1]) 99 | 100 | @staticmethod 101 | def new(page: Page) -> PageFill: 102 | return PageFill(page, list(map(ColumnFill.new, page.columns)), ()) 103 | 104 | 105 | def fill_pages( 106 | doc: Iterator[PageFill], f: _ColumnFiller 107 | ) -> tuple[Iterator[PageFill], Sequence[PageFill]]: 108 | trunk, branch = tee(doc) 109 | return _fill_into( # pragma: no branch 110 | f(flatten(p.todo for p in branch)), trunk 111 | ) 112 | 113 | 114 | def _fill_into( 115 | filled: Iterator[ColumnFill], doc: Iterator[PageFill] 116 | ) -> tuple[Iterator[PageFill], Sequence[PageFill]]: 117 | try: 118 | _, filled = peek(filled) 119 | except StopIteration: 120 | return doc, [] # no content to add 121 | 122 | completed: list[PageFill] = [] 123 | for page in doc: # pragma: no branch 124 | page_cols = list(islice(filled, len(page.todo))) 125 | completed.append( 126 | PageFill( 127 | page.base, 128 | page.todo[len(page_cols) :], # noqa 129 | (*page.done, *page_cols), 130 | ) 131 | ) 132 | try: 133 | _, filled = peek(filled) 134 | except StopIteration: 135 | break # no more content -- wrap things up 136 | 137 | return prepend(completed.pop().reopen_most_recent_column(), doc), completed 138 | -------------------------------------------------------------------------------- /src/pdfje/layout/pages.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from functools import partial 5 | from itertools import chain, count 6 | from typing import Callable, Iterable, Iterator, final 7 | 8 | from ..common import add_slots, always, flatten, setattr_frozen 9 | from ..page import Page, RenderedPage 10 | from ..resources import Resources 11 | from ..style import StyleFull 12 | from .common import Block, PageFill, fill_pages 13 | from .paragraph import Paragraph 14 | 15 | 16 | @final 17 | @add_slots 18 | @dataclass(frozen=True, init=False) 19 | class AutoPage: 20 | """Automatically lays out content on multiple pages. 21 | 22 | Parameters 23 | ---------- 24 | content: ~typing.Iterable[~pdfje.Block | str] | ~pdfje.Block | str 25 | The content to lay out on the pages. Can be parsed from single string 26 | or block. 27 | template: ~pdfje.Page | ~typing.Callable[[int], ~pdfje.Page] 28 | A page to use as a template for the layout. If a callable is given, 29 | it is called with the page number as the only argument to generate 30 | the page. Defaults to the default :class:`Page`. 31 | 32 | """ 33 | 34 | content: Iterable[str | Block] 35 | template: Callable[[int], Page] 36 | 37 | def __init__( 38 | self, 39 | content: str | Block | Iterable[Block | str], 40 | template: Page | Callable[[int], Page] = always(Page()), 41 | ) -> None: 42 | if isinstance(content, str): 43 | content = [Paragraph(content)] 44 | elif isinstance(content, Block): 45 | content = [content] 46 | setattr_frozen(self, "content", content) 47 | 48 | if isinstance(template, Page): 49 | template = always(template) 50 | setattr_frozen(self, "template", template) 51 | 52 | def render( 53 | self, r: Resources, s: StyleFull, pnum: int, / 54 | ) -> Iterator[RenderedPage]: 55 | pages: Iterator[PageFill] = map( 56 | PageFill.new, map(self.template, count(pnum)) 57 | ) 58 | for block in map(_as_block, self.content): 59 | pages, filled = fill_pages( 60 | pages, partial(block.into_columns, r, s) 61 | ) 62 | for p in filled: 63 | yield p.base.fill(r, s, flatten(p.done)) 64 | 65 | last = next(pages) 66 | yield last.base.fill(r, s, flatten(chain(last.done, last.todo))) 67 | 68 | 69 | def _as_block(b: str | Block) -> Block: 70 | return Paragraph(b) if isinstance(b, str) else b 71 | -------------------------------------------------------------------------------- /src/pdfje/layout/paragraph.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from functools import partial 5 | from itertools import tee 6 | from typing import ( 7 | ClassVar, 8 | Iterable, 9 | Iterator, 10 | Literal, 11 | Protocol, 12 | Sequence, 13 | cast, 14 | final, 15 | ) 16 | 17 | from ..common import XY, Align, Pt, add_slots, advance, prepend, setattr_frozen 18 | from ..resources import Resources 19 | from ..style import Span, Style, StyledMixin, StyleFull, StyleLike 20 | from ..typeset import firstfit, optimum 21 | from ..typeset.layout import ShapedText 22 | from ..typeset.parse import into_words 23 | from ..typeset.state import Passage, State, max_lead, splitlines 24 | from ..typeset.words import WordLike, indent_first 25 | from .common import Block, ColumnFill 26 | 27 | 28 | @add_slots 29 | @dataclass(frozen=True) 30 | class LinebreakParams: 31 | """Parameters for tweaking the optimum-fit algorithm. 32 | 33 | Parameters 34 | ---------- 35 | tolerance 36 | The tolerance for the stretch of each line. 37 | If no feasible solution is found, the tolerance is increased until 38 | there is. 39 | Increase the tolerance if you want to avoid hyphenation 40 | at the cost of more stretching and longer runtime. 41 | hyphen_penalty 42 | The penalty for hyphenating a word. If increasing this value does 43 | not result in fewer hyphens, try increasing the tolerance. 44 | consecutive_hyphen_penalty 45 | The penalty for placing hyphens on consecutive lines. If increasing 46 | this value does not appear to work, try increasing the tolerance. 47 | fitness_diff_penalty 48 | The penalty for very tight and very loose lines following each other. 49 | """ 50 | 51 | tolerance: float = 1 52 | hyphen_penalty: float = 1000 53 | consecutive_hyphen_penalty: float = 1000 54 | fitness_diff_penalty: float = 1000 55 | 56 | DEFAULT: ClassVar["LinebreakParams"] 57 | 58 | 59 | LinebreakParams.DEFAULT = LinebreakParams() 60 | 61 | 62 | @final 63 | @add_slots 64 | @dataclass(frozen=True, init=False) 65 | class Paragraph(Block, StyledMixin): 66 | """A :class:`Block` that renders a paragraph of text. 67 | 68 | Parameters 69 | ---------- 70 | content 71 | The text to render. Can be a string, or a nested :class:`~pdfje.Span`. 72 | style 73 | The style to render the text with. 74 | See :ref:`tutorial