Adding upstream version 0.1.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
a042d54ff1
commit
00981dc324
17 changed files with 2130 additions and 0 deletions
29
LICENSE
Normal file
29
LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020, CourtBouillon
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
57
PKG-INFO
Normal file
57
PKG-INFO
Normal file
|
@ -0,0 +1,57 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: pydyf
|
||||
Version: 0.1.1
|
||||
Summary: A low-level PDF generator.
|
||||
Keywords: pdf,generator
|
||||
Author-email: CourtBouillon <contact@courtbouillon.org>
|
||||
Maintainer-email: CourtBouillon <contact@courtbouillon.org>
|
||||
Requires-Python: >=3.6
|
||||
Description-Content-Type: text/x-rst
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Requires-Dist: sphinx ; extra == "doc"
|
||||
Requires-Dist: sphinx_rtd_theme ; extra == "doc"
|
||||
Requires-Dist: pytest ; extra == "test"
|
||||
Requires-Dist: pytest-cov ; extra == "test"
|
||||
Requires-Dist: pytest-flake8 ; extra == "test"
|
||||
Requires-Dist: pytest-isort ; extra == "test"
|
||||
Requires-Dist: coverage[toml] ; extra == "test"
|
||||
Requires-Dist: pillow ; extra == "test"
|
||||
Project-URL: Changelog, https://github.com/CourtBouillon/pydyf/releases
|
||||
Project-URL: Code, https://github.com/CourtBouillon/pydyf
|
||||
Project-URL: Documentation, https://doc.courtbouillon.org/pydyf/
|
||||
Project-URL: Donation, https://opencollective.com/courtbouillon
|
||||
Project-URL: Homepage, https://www.courtbouillon.org/pydyf
|
||||
Project-URL: Issues, https://github.com/CourtBouillon/pydyf/issues
|
||||
Provides-Extra: doc
|
||||
Provides-Extra: test
|
||||
|
||||
pydyf is a low-level PDF generator written in Python and based on PDF
|
||||
specification 1.7.
|
||||
|
||||
* Free software: BSD license
|
||||
* For Python 3.6+, tested on CPython and PyPy
|
||||
* Documentation: https://doc.courtbouillon.org/pydyf
|
||||
* Changelog: https://github.com/CourtBouillon/pydyf/releases
|
||||
* Code, issues, tests: https://github.com/CourtBouillon/pydyf
|
||||
* Code of conduct: https://www.courtbouillon.org/code-of-conduct
|
||||
* Professional support: https://www.courtbouillon.org
|
||||
* Donation: https://opencollective.com/courtbouillon
|
||||
|
||||
Copyrights are retained by their contributors, no copyright assignment is
|
||||
required to contribute to pydyf. Unless explicitly stated otherwise, any
|
||||
contribution intentionally submitted for inclusion is licensed under the BSD
|
||||
3-clause license, without any additional terms or conditions. For full
|
||||
authorship information, see the version control history.
|
||||
|
17
README.rst
Normal file
17
README.rst
Normal file
|
@ -0,0 +1,17 @@
|
|||
pydyf is a low-level PDF generator written in Python and based on PDF
|
||||
specification 1.7.
|
||||
|
||||
* Free software: BSD license
|
||||
* For Python 3.6+, tested on CPython and PyPy
|
||||
* Documentation: https://doc.courtbouillon.org/pydyf
|
||||
* Changelog: https://github.com/CourtBouillon/pydyf/releases
|
||||
* Code, issues, tests: https://github.com/CourtBouillon/pydyf
|
||||
* Code of conduct: https://www.courtbouillon.org/code-of-conduct
|
||||
* Professional support: https://www.courtbouillon.org
|
||||
* Donation: https://opencollective.com/courtbouillon
|
||||
|
||||
Copyrights are retained by their contributors, no copyright assignment is
|
||||
required to contribute to pydyf. Unless explicitly stated otherwise, any
|
||||
contribution intentionally submitted for inclusion is licensed under the BSD
|
||||
3-clause license, without any additional terms or conditions. For full
|
||||
authorship information, see the version control history.
|
19
docs/api_reference.rst
Normal file
19
docs/api_reference.rst
Normal file
|
@ -0,0 +1,19 @@
|
|||
API Reference
|
||||
=============
|
||||
|
||||
.. module:: pydyf
|
||||
|
||||
.. autoclass:: Object
|
||||
:members:
|
||||
|
||||
.. autoclass:: Dictionary
|
||||
|
||||
.. autoclass:: Stream
|
||||
:members:
|
||||
|
||||
.. autoclass:: String
|
||||
|
||||
.. autoclass:: Array
|
||||
|
||||
.. autoclass:: PDF
|
||||
:members:
|
167
docs/changelog.rst
Normal file
167
docs/changelog.rst
Normal file
|
@ -0,0 +1,167 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
|
||||
Version 0.1.1
|
||||
-------------
|
||||
|
||||
Released on 2021-08-22.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* `0f7c8e9 <https://github.com/CourtBouillon/pydyf/commit/0f7c8e9>`_:
|
||||
Fix string encoding
|
||||
|
||||
Contributors:
|
||||
|
||||
* Guillaume Ayoub
|
||||
|
||||
Backers and sponsors:
|
||||
|
||||
* Grip Angebotssoftware
|
||||
* PDF Blocks
|
||||
* SimonSoft
|
||||
* Menutech
|
||||
* Manuel Barkhau
|
||||
* Simon Sapin
|
||||
* KontextWork
|
||||
* René Fritz
|
||||
* Maykin Media
|
||||
* NCC Group
|
||||
* Des images et des mots
|
||||
* Andreas Zettl
|
||||
* Nathalie Gutton
|
||||
* Tom Pohl
|
||||
* Moritz Mahringer
|
||||
* Florian Demmer
|
||||
* Yanal-Yvez Fargialla
|
||||
|
||||
|
||||
Version 0.1.0
|
||||
-------------
|
||||
|
||||
Released on 2021-08-21.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* `#8 <https://github.com/CourtBouillon/pydyf/issues/8>`_:
|
||||
Don’t use sys.stdout.buffer as default write object
|
||||
|
||||
Contributors:
|
||||
|
||||
* Guillaume Ayoub
|
||||
|
||||
Backers and sponsors:
|
||||
|
||||
* Grip Angebotssoftware
|
||||
* PDF Blocks
|
||||
* SimonSoft
|
||||
* Menutech
|
||||
* Manuel Barkhau
|
||||
* Simon Sapin
|
||||
* KontextWork
|
||||
* René Fritz
|
||||
* Maykin Media
|
||||
* NCC Group
|
||||
* Des images et des mots
|
||||
* Andreas Zettl
|
||||
* Nathalie Gutton
|
||||
* Tom Pohl
|
||||
* Moritz Mahringer
|
||||
* Florian Demmer
|
||||
* Yanal-Yvez Fargialla
|
||||
|
||||
|
||||
Version 0.0.3
|
||||
-------------
|
||||
|
||||
Released on 2021-04-22.
|
||||
|
||||
New features:
|
||||
|
||||
* Support text rendering
|
||||
|
||||
Contributors:
|
||||
|
||||
* Guillaume Ayoub
|
||||
|
||||
Backers and sponsors:
|
||||
|
||||
* PDF Blocks
|
||||
* SimonSoft
|
||||
* Menutech
|
||||
* Simon Sapin
|
||||
* Manuel Barkhau
|
||||
* Andreas Zettl
|
||||
* Nathalie Gutton
|
||||
* Tom Pohl
|
||||
* René Fritz
|
||||
* Moritz Mahringer
|
||||
* Florian Demmer
|
||||
* KontextWork
|
||||
* Michele Mostarda
|
||||
|
||||
|
||||
Version 0.0.2
|
||||
-------------
|
||||
|
||||
Released on 2021-03-13.
|
||||
|
||||
New features:
|
||||
|
||||
* Support linecap style
|
||||
* Support line join et miter limit
|
||||
* Add more cubic Bézier curve options
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Don’t include EOL in dictionary length
|
||||
* Add a second binary line in PDF
|
||||
|
||||
Contributors:
|
||||
|
||||
* Guillaume Ayoub
|
||||
* Lucie Anglade
|
||||
* Alexander Schrijver
|
||||
* Kees Cook
|
||||
|
||||
Backers and sponsors:
|
||||
|
||||
* PDF Blocks
|
||||
* SimonSoft
|
||||
* Menutech
|
||||
* Simon Sapin
|
||||
* Manuel Barkhau
|
||||
* Andreas Zettl
|
||||
* Nathalie Gutton
|
||||
* Tom Pohl
|
||||
* René Fritz
|
||||
* Moritz Mahringer
|
||||
* Florian Demmer
|
||||
* KontextWork
|
||||
* Michele Mostarda
|
||||
|
||||
|
||||
Version 0.0.1
|
||||
-------------
|
||||
|
||||
Released on 2020-12-06.
|
||||
|
||||
Initial release.
|
||||
|
||||
Contributors:
|
||||
|
||||
* Guillaume Ayoub
|
||||
* Lucie Anglade
|
||||
|
||||
Backers and sponsors:
|
||||
|
||||
* PDF Blocks
|
||||
* SimonSoft
|
||||
* Menutech
|
||||
* Simon Sapin
|
||||
* Nathalie Gutton
|
||||
* Andreas Zetti
|
||||
* Tom Pohl
|
||||
* Florian Demmer
|
||||
* Moritz Mahringer
|
182
docs/common_use_cases.rst
Normal file
182
docs/common_use_cases.rst
Normal file
|
@ -0,0 +1,182 @@
|
|||
Common Use Cases
|
||||
================
|
||||
|
||||
pydyf has been created for WeasyPrint and many common use cases can thus be
|
||||
found in `its repository`_.
|
||||
|
||||
.. _its repository: https://github.com/Kozea/WeasyPrint
|
||||
|
||||
|
||||
Draw rectangles and lines
|
||||
-------------------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pydyf
|
||||
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
|
||||
# Draw a first rectangle
|
||||
# With the border in dash style
|
||||
# The dash line is 2 points full, 1 point empty
|
||||
# And the dash line begins with 2 full points
|
||||
draw.rectangle(100, 100, 50, 70)
|
||||
draw.set_dash([2, 1], 0)
|
||||
draw.stroke()
|
||||
|
||||
# Draw a second rectangle
|
||||
# The dash is reset to a full line
|
||||
# The line width is set
|
||||
# Move the bottom-left corner to (80, 80)
|
||||
# Fill the rectangle
|
||||
draw.rectangle(50, 50, 20, 40)
|
||||
draw.set_dash([], 0)
|
||||
draw.set_line_width(10)
|
||||
draw.transform(1, 0, 0, 1, 80, 80)
|
||||
draw.fill()
|
||||
|
||||
# Add the stream with the two rectangles into the document
|
||||
document.add_object(draw)
|
||||
|
||||
# Add a page to the document containing the draw
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 200, 200]),
|
||||
}))
|
||||
|
||||
# Write to document.pdf
|
||||
with open('document.pdf', 'wb') as f:
|
||||
document.write(f)
|
||||
|
||||
Add some color
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pydyf
|
||||
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
|
||||
# Set the color for nonstroking and stroking operations
|
||||
# Red for nonstroking an green for stroking
|
||||
draw.set_color_rgb(1.0, 0.0, 0.0)
|
||||
draw.set_color_rgb(0.0, 1.0, 0.0, stroke=True)
|
||||
draw.rectangle(100, 100, 50, 70)
|
||||
draw.set_dash([2, 1], 0)
|
||||
draw.stroke()
|
||||
draw.rectangle(50, 50, 20, 40)
|
||||
draw.set_dash([], 0)
|
||||
draw.set_line_width(10)
|
||||
draw.transform(1, 0, 0, 1, 80, 80)
|
||||
draw.fill()
|
||||
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 200, 200]),
|
||||
}))
|
||||
|
||||
with open('document.pdf', 'wb') as f:
|
||||
document.write(f)
|
||||
|
||||
Display image
|
||||
-------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pydyf
|
||||
|
||||
document = pydyf.PDF()
|
||||
|
||||
extra = Dictionary({
|
||||
'Type': '/XObject',
|
||||
'Subtype': '/Image',
|
||||
'Width': 197,
|
||||
'Height': 101,
|
||||
'ColorSpace': '/DeviceRGB',
|
||||
'BitsPerComponent': 8,
|
||||
'Filter': '/DCTDecode',
|
||||
})
|
||||
|
||||
image = open('logo.jpg', 'rb').read()
|
||||
xobject = pydyf.Stream([image], extra=extra)
|
||||
document.add_object(xobject)
|
||||
|
||||
image = pydyf.Stream()
|
||||
image.push_state()
|
||||
image.transform(100, 0, 0, 100, 100, 100)
|
||||
image.draw_x_object('Im1')
|
||||
image.pop_state()
|
||||
document.add_object(image)
|
||||
|
||||
# Put the image in the resources of the PDF
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 200, 200]),
|
||||
'Resources': pydyf.Dictionary({
|
||||
'ProcSet': pydyf.Array(['/PDF', '/ImageB']),
|
||||
'XObject': pydyf.Dictionary({'Im1': xobject.reference}),
|
||||
}),
|
||||
'Contents': image.reference,
|
||||
}))
|
||||
|
||||
with open('document.pdf', 'wb') as f:
|
||||
document.write(f)
|
||||
|
||||
Display text
|
||||
------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pydyf
|
||||
|
||||
document = pydyf.PDF()
|
||||
|
||||
# Define the font
|
||||
font = pydyf.Dictionary({
|
||||
'Type': '/Font',
|
||||
'Subtype': '/Type1',
|
||||
'Name': '/F1',
|
||||
'BaseFont': '/Helvetica',
|
||||
'Encoding': '/MacRomanEncoding',
|
||||
})
|
||||
|
||||
document.add_object(font)
|
||||
|
||||
# Set the font use for the text
|
||||
# Move to where to display the text
|
||||
# And display it
|
||||
text = pydyf.Stream()
|
||||
text.begin_text()
|
||||
text.set_font_size('F1', 24)
|
||||
text.text_matrix(1, 0, 0, 1, 10, 90)
|
||||
text.show_text(pydyf.String('Hello World'))
|
||||
text.end_text()
|
||||
|
||||
document.add_object(text)
|
||||
|
||||
# Put the font in the resources of the PDF
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 200, 200]),
|
||||
'Contents': text.reference,
|
||||
'Resources': pydyf.Dictionary({
|
||||
'ProcSet': pydyf.Array(['/PDF', '/Text']),
|
||||
'Font': pydyf.Dictionary({'F1': font.reference}),
|
||||
})
|
||||
}))
|
||||
|
||||
with open('document.pdf', 'wb') as f:
|
||||
document.write(f)
|
||||
|
90
docs/conf.py
Normal file
90
docs/conf.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
# pydyf documentation build configuration file.
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pydyf
|
||||
|
||||
# Add current path for css_diagram_role
|
||||
sys.path.append(str(Path(__file__).parent))
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.autosectionlabel']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'pydyf'
|
||||
copyright = 'CourtBouillon and contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = pydyf.__version__
|
||||
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'monokai'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
html_theme_options = {
|
||||
'collapse_navigation': False,
|
||||
}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = []
|
||||
|
||||
# These paths are either relative to html_static_path
|
||||
# or fully qualified paths (eg. https://...)
|
||||
html_css_files = [
|
||||
'https://www.courtbouillon.org/static/docs.css',
|
||||
]
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'pydyf2doc'
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'pydyf', 'pydyf Documentation',
|
||||
['CourtBouillon and contributors'], 1)
|
||||
]
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'pydyf', 'pydyf Documentation',
|
||||
'CourtBouillon', 'pydyf',
|
||||
'A low-level PDF creator',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3/', None),
|
||||
'webencodings': ('https://pythonhosted.org/webencodings/', None),
|
||||
}
|
70
docs/contribute.rst
Normal file
70
docs/contribute.rst
Normal file
|
@ -0,0 +1,70 @@
|
|||
Contribute
|
||||
==========
|
||||
|
||||
You want to add some code to pydyf, launch its tests or improve its
|
||||
documentation? Thank you very much! Here are some tips to help you play with
|
||||
pydyf in good conditions.
|
||||
|
||||
The first step is to clone the repository, create a virtual environment and
|
||||
install pydyf dependencies.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git clone https://github.com/CourtBouillon/pydyf.git
|
||||
cd pydyf
|
||||
python -m venv venv
|
||||
venv/bin/pip install .[doc,test]
|
||||
|
||||
You can then let your terminal in the current directory and launch Python to
|
||||
test your changes. ``import pydyf`` will then import the working directory
|
||||
code, so that you can modify it and test your changes.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
venv/bin/python
|
||||
|
||||
|
||||
Code & Issues
|
||||
-------------
|
||||
|
||||
If you’ve found a bug in pydyf, it’s time to report it, and to fix it if you
|
||||
can!
|
||||
|
||||
You can report bugs and feature requests on GitHub_. If you want to add or
|
||||
fix some code, please fork the repository and create a pull request, we’ll be
|
||||
happy to review your work.
|
||||
|
||||
.. _GitHub: https://github.com/CourtBouillon/pydyf
|
||||
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
Tests are stored in the ``tests`` folder at the top of the repository. They use
|
||||
the pytest_ library.
|
||||
|
||||
Launching tests require to have Ghostscript_ installed and available in
|
||||
``PATH``.
|
||||
|
||||
You can launch tests (with code coverage and lint) using the following command::
|
||||
|
||||
venv/bin/pytest
|
||||
|
||||
.. _pytest: https://docs.pytest.org/
|
||||
.. _Ghostscript: https://www.ghostscript.com/
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation is stored in the ``docs`` folder at the top of the repository. It
|
||||
relies on the Sphinx_ library.
|
||||
|
||||
You can build the documentation using the following command::
|
||||
|
||||
venv/bin/sphinx-build docs docs/_build
|
||||
|
||||
The documentation home page can now be found in the ``docs/_build/index.html``
|
||||
file. You can open this file in a browser to see the final rendering.
|
||||
|
||||
.. _Sphinx: https://www.sphinx-doc.org/
|
36
docs/first_steps.rst
Normal file
36
docs/first_steps.rst
Normal file
|
@ -0,0 +1,36 @@
|
|||
First Steps
|
||||
===========
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
The easiest way to use pydyf is to install it in a Python `virtual
|
||||
environment`_. When your virtual environment is activated, you can then install
|
||||
pydyf with pip_::
|
||||
|
||||
pip install pydyf
|
||||
|
||||
.. _virtual environment: https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/
|
||||
.. _pip: https://pip.pypa.io/
|
||||
|
||||
|
||||
Create a PDF
|
||||
------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pydyf
|
||||
|
||||
document = pydyf.PDF()
|
||||
|
||||
# Add an empty page
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 200, 200]),
|
||||
}))
|
||||
|
||||
# Write to document.pdf
|
||||
with open('document.pdf', 'wb') as f:
|
||||
document.write(f)
|
32
docs/going_further.rst
Normal file
32
docs/going_further.rst
Normal file
|
@ -0,0 +1,32 @@
|
|||
Going Further
|
||||
=============
|
||||
|
||||
|
||||
Why pydyf?
|
||||
-------------
|
||||
|
||||
pydyf has been created to replace Cairo PDF generation in WeasyPrint_.
|
||||
|
||||
Indeed, there are some bugs in WeasyPrint caused by Cairo_ and Cairo has some
|
||||
difficulties to make releases.
|
||||
Also there are features which will be easier to implement while having more
|
||||
control on the PDF generation.
|
||||
|
||||
So we created pydyf.
|
||||
|
||||
.. _WeasyPrint: https://www.courtbouillon.org/weasyprint
|
||||
.. _Cairo: https://www.cairographics.org/
|
||||
|
||||
Why Python?
|
||||
-----------
|
||||
|
||||
Python is a really good language to design a small, OS-agnostic parser. As it
|
||||
is object-oriented, it gives the possibility to follow the specification with
|
||||
high-level classes and a small amount of very simple code.
|
||||
|
||||
And of course, WeasyPrint is written in Python too, giving an obvious reason
|
||||
for this choice.
|
||||
|
||||
Speed is not pydyf’s main goal. Code simplicity, maintainability and
|
||||
flexibility are more important goals for this library, as they give the
|
||||
ability to stay really close to the specification and to fix bugs easily.
|
23
docs/index.rst
Normal file
23
docs/index.rst
Normal file
|
@ -0,0 +1,23 @@
|
|||
pydyf
|
||||
========
|
||||
|
||||
.. currentmodule:: pydyf
|
||||
|
||||
.. include:: ../README.rst
|
||||
|
||||
.. toctree::
|
||||
:caption: Documentation
|
||||
:maxdepth: 3
|
||||
|
||||
first_steps
|
||||
common_use_cases
|
||||
api_reference
|
||||
going_further
|
||||
|
||||
.. toctree::
|
||||
:caption: Extra Information
|
||||
:maxdepth: 3
|
||||
|
||||
changelog
|
||||
contribute
|
||||
support
|
28
docs/support.rst
Normal file
28
docs/support.rst
Normal file
|
@ -0,0 +1,28 @@
|
|||
Support
|
||||
=======
|
||||
|
||||
|
||||
Sponsorship
|
||||
-----------
|
||||
|
||||
With `donations and sponsorship`_, you help the projects to be
|
||||
better. Donations allow the CourtBouillon team to have more time dedicated to
|
||||
add new features, fix bugs, and improve documentation.
|
||||
|
||||
.. _donations and sponsorship: https://opencollective.com/courtbouillon
|
||||
|
||||
|
||||
Professionnal Support
|
||||
---------------------
|
||||
|
||||
You can improve your experience with CourtBouillon’s tools thanks to our
|
||||
professional support. You want bugs fixed as soon as possible? You projects
|
||||
would highly benefit from some new features? You or your team would like to get
|
||||
new skills with one of the technologies we master?
|
||||
|
||||
Please contact us by mail_, by chat_ or by tweet_ to get in touch and find the
|
||||
best way we can help you.
|
||||
|
||||
.. _mail: mailto:contact@courtbouillon.org
|
||||
.. _chat: https://gitter.im/CourtBouillon/pydyf
|
||||
.. _tweet: https://twitter.com/BouillonCourt
|
502
pydyf/__init__.py
Executable file
502
pydyf/__init__.py
Executable file
|
@ -0,0 +1,502 @@
|
|||
"""
|
||||
A low-level PDF generator.
|
||||
|
||||
"""
|
||||
|
||||
import zlib
|
||||
from codecs import BOM_UTF16_BE
|
||||
|
||||
VERSION = __version__ = '0.1.1'
|
||||
|
||||
|
||||
def _to_bytes(item):
|
||||
"""Convert item to bytes."""
|
||||
if isinstance(item, bytes):
|
||||
return item
|
||||
elif isinstance(item, Object):
|
||||
return item.data
|
||||
elif isinstance(item, float):
|
||||
if item.is_integer():
|
||||
return f'{int(item):d}'.encode('ascii')
|
||||
else:
|
||||
return f'{item:f}'.encode('ascii')
|
||||
elif isinstance(item, int):
|
||||
return f'{item:d}'.encode('ascii')
|
||||
return str(item).encode('ascii')
|
||||
|
||||
|
||||
class Object:
|
||||
"""Base class for PDF objects."""
|
||||
def __init__(self):
|
||||
#: Number of the object.
|
||||
self.number = None
|
||||
#: Position in the PDF of the object.
|
||||
self.offset = 0
|
||||
#: Version number of the object, non-negative.
|
||||
self.generation = 0
|
||||
#: Indicate if an object is used (``'n'``), or has been deleted
|
||||
#: and therefore is free (``'f'``).
|
||||
self.free = 'n'
|
||||
|
||||
@property
|
||||
def indirect(self):
|
||||
"""Indirect representation of an object."""
|
||||
return b'\n'.join((
|
||||
str(self.number).encode() + b' ' +
|
||||
str(self.generation).encode() + b' obj',
|
||||
self.data,
|
||||
b'endobj',
|
||||
))
|
||||
|
||||
@property
|
||||
def reference(self):
|
||||
"""Object identifier."""
|
||||
return (
|
||||
str(self.number).encode() + b' ' +
|
||||
str(self.generation).encode() + b' R')
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Data contained in the object. Shall be defined in each subclass."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Dictionary(Object, dict):
|
||||
"""PDF Dictionary object.
|
||||
|
||||
Inherits from :class:`Object` and Python :obj:`dict`.
|
||||
|
||||
"""
|
||||
def __init__(self, values=None):
|
||||
Object.__init__(self)
|
||||
dict.__init__(self, values or {})
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
result = [b'<<']
|
||||
for key, value in self.items():
|
||||
result.append(b'/' + _to_bytes(key) + b' ' + _to_bytes(value))
|
||||
result.append(b'>>')
|
||||
return b'\n'.join(result)
|
||||
|
||||
|
||||
class Stream(Object):
|
||||
"""PDF Stream object.
|
||||
|
||||
Inherits from :class:`Object`.
|
||||
|
||||
"""
|
||||
def __init__(self, stream=None, extra=None, compress=False):
|
||||
super().__init__()
|
||||
#: Python array of data composing stream.
|
||||
self.stream = stream or []
|
||||
#: Metadata containing at least the length of the Stream.
|
||||
self.extra = extra or {}
|
||||
#: Compress the stream data if set to ``True``. Default is ``False``.
|
||||
self.compress = compress
|
||||
|
||||
def begin_text(self):
|
||||
"""Begin a text object."""
|
||||
self.stream.append(b'BT')
|
||||
|
||||
def clip(self, even_odd=False):
|
||||
"""Modify current clipping path by intersecting it with current path.
|
||||
|
||||
Use the nonzero winding number rule to determine which regions lie
|
||||
inside the clipping path by default.
|
||||
|
||||
Use the even-odd rule if ``even_odd`` set to ``True``.
|
||||
|
||||
"""
|
||||
self.stream.append(b'W*' if even_odd else b'W')
|
||||
|
||||
def close(self):
|
||||
"""Close current subpath.
|
||||
|
||||
Append a straight line segment from the current point to the starting
|
||||
point of the subpath.
|
||||
|
||||
"""
|
||||
self.stream.append(b'h')
|
||||
|
||||
def color_space(self, space, stroke=False):
|
||||
"""Set the nonstroking color space.
|
||||
|
||||
If stroke is set to ``True``, set the stroking color space instead.
|
||||
|
||||
"""
|
||||
self.stream.append(
|
||||
b'/' + _to_bytes(space) + b' ' + (b'CS' if stroke else b'cs'))
|
||||
|
||||
def curve_to(self, x1, y1, x2, y2, x3, y3):
|
||||
"""Add cubic Bézier curve to current path.
|
||||
|
||||
The curve shall extend from ``(x3, y3)`` using ``(x1, y1)`` and ``(x2,
|
||||
y2)`` as the Bézier control points.
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
_to_bytes(x1), _to_bytes(y1),
|
||||
_to_bytes(x2), _to_bytes(y2),
|
||||
_to_bytes(x3), _to_bytes(y3), b'c')))
|
||||
|
||||
def curve_start_to(self, x2, y2, x3, y3):
|
||||
"""Add cubic Bézier curve to current path
|
||||
|
||||
The curve shall extend to ``(x3, y3)`` using the current point and
|
||||
``(x2, y2)`` as the Bézier control points.
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
_to_bytes(x2), _to_bytes(y2),
|
||||
_to_bytes(x3), _to_bytes(y3), b'v')))
|
||||
|
||||
def curve_end_to(self, x1, y1, x3, y3):
|
||||
"""Add cubic Bézier curve to current path
|
||||
|
||||
The curve shall extend to ``(x3, y3)`` using `(x1, y1)`` and ``(x3,
|
||||
y3)`` as the Bézier control points.
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
_to_bytes(x1), _to_bytes(y1),
|
||||
_to_bytes(x3), _to_bytes(y3), b'y')))
|
||||
|
||||
def draw_x_object(self, reference):
|
||||
"""Draw object given by reference."""
|
||||
self.stream.append(b'/' + _to_bytes(reference) + b' Do')
|
||||
|
||||
def end(self):
|
||||
"""End path without filling or stroking."""
|
||||
self.stream.append(b'n')
|
||||
|
||||
def end_text(self):
|
||||
"""End text object."""
|
||||
self.stream.append(b'ET')
|
||||
|
||||
def fill(self, even_odd=False):
|
||||
"""Fill path using nonzero winding rule.
|
||||
|
||||
Use even-odd rule if ``even_odd`` is set to ``True``.
|
||||
|
||||
"""
|
||||
self.stream.append(b'f*' if even_odd else b'f')
|
||||
|
||||
def fill_and_stroke(self, even_odd=False):
|
||||
"""Fill and stroke path usign nonzero winding rule.
|
||||
|
||||
Use even-odd rule if ``even_odd`` is set to ``True``.
|
||||
|
||||
"""
|
||||
self.stream.append(b'B*' if even_odd else b'B')
|
||||
|
||||
def fill_stroke_and_close(self, even_odd=False):
|
||||
"""Fill, stroke and close path using nonzero winding rule.
|
||||
|
||||
Use even-odd rule if ``even_odd`` is set to ``True``.
|
||||
|
||||
"""
|
||||
self.stream.append(b'b*' if even_odd else b'b')
|
||||
|
||||
def line_to(self, x, y):
|
||||
"""Add line from current point to point ``(x, y)``."""
|
||||
self.stream.append(b' '.join((_to_bytes(x), _to_bytes(y), b'l')))
|
||||
|
||||
def move_to(self, x, y):
|
||||
"""Begin new subpath by moving current point to ``(x, y)``."""
|
||||
self.stream.append(b' '.join((_to_bytes(x), _to_bytes(y), b'm')))
|
||||
|
||||
def shading(self, name):
|
||||
"""Paint shape and color shading using shading dictionary ``name``."""
|
||||
self.stream.append(b'/' + _to_bytes(name) + b' sh')
|
||||
|
||||
def pop_state(self):
|
||||
"""Restore graphic state."""
|
||||
self.stream.append(b'Q')
|
||||
|
||||
def push_state(self):
|
||||
"""Save graphic state."""
|
||||
self.stream.append(b'q')
|
||||
|
||||
def rectangle(self, x, y, width, height):
|
||||
"""Add rectangle to current path as complete subpath.
|
||||
|
||||
``(x, y)`` is the lower-left corner and width and height the
|
||||
dimensions.
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
_to_bytes(x), _to_bytes(y),
|
||||
_to_bytes(width), _to_bytes(height), b're')))
|
||||
|
||||
def set_color_rgb(self, r, g, b, stroke=False):
|
||||
"""Set RGB color for nonstroking operations.
|
||||
|
||||
Set RGB color for stroking operations instead if ``stroke`` is set to
|
||||
``True``.
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
_to_bytes(r), _to_bytes(g), _to_bytes(b),
|
||||
(b'RG' if stroke else b'rg'))))
|
||||
|
||||
def set_color_special(self, name, stroke=False):
|
||||
"""Set color for nonstroking operations.
|
||||
|
||||
Set color for stroking operation if ``stroke`` is set to ``True``.
|
||||
|
||||
"""
|
||||
self.stream.append(
|
||||
b'/' + _to_bytes(name) + b' ' + (b'SCN' if stroke else b'scn'))
|
||||
|
||||
def set_dash(self, dash_array, dash_phase):
|
||||
"""Set dash line pattern.
|
||||
|
||||
:param dash_array: Dash pattern.
|
||||
:type dash_array: :term:`iterable`
|
||||
:param dash_phase: Start of dash phase.
|
||||
:type dash_phase: :obj:`int`
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
Array(dash_array).data, _to_bytes(dash_phase), b'd')))
|
||||
|
||||
def set_font_size(self, font, size):
|
||||
"""Set font name and size."""
|
||||
self.stream.append(
|
||||
b'/' + _to_bytes(font) + b' ' + _to_bytes(size) + b' Tf')
|
||||
|
||||
def set_text_rendering(self, mode):
|
||||
"""Set text rendering mode."""
|
||||
self.stream.append(_to_bytes(mode) + b' Tr')
|
||||
|
||||
def set_line_cap(self, line_cap):
|
||||
"""Set line cap style."""
|
||||
self.stream.append(_to_bytes(line_cap) + b' J')
|
||||
|
||||
def set_line_join(self, line_join):
|
||||
"""Set line join style."""
|
||||
self.stream.append(_to_bytes(line_join) + b' j')
|
||||
|
||||
def set_line_width(self, width):
|
||||
"""Set line width."""
|
||||
self.stream.append(_to_bytes(width) + b' w')
|
||||
|
||||
def set_miter_limit(self, miter_limit):
|
||||
"""Set miter limit."""
|
||||
self.stream.append(_to_bytes(miter_limit) + b' M')
|
||||
|
||||
def set_state(self, state_name):
|
||||
"""Set specified parameters in graphic state.
|
||||
|
||||
:param state_name: Name of the graphic state.
|
||||
|
||||
"""
|
||||
self.stream.append(b'/' + _to_bytes(state_name) + b' gs')
|
||||
|
||||
def show_text(self, text):
|
||||
"""Show text."""
|
||||
self.stream.append(b'[' + _to_bytes(text) + b'] TJ')
|
||||
|
||||
def stroke(self):
|
||||
"""Stroke path."""
|
||||
self.stream.append(b'S')
|
||||
|
||||
def stroke_and_close(self):
|
||||
"""Stroke and close path."""
|
||||
self.stream.append(b's')
|
||||
|
||||
def text_matrix(self, a, b, c, d, e, f):
|
||||
"""Set text matrix and text line matrix.
|
||||
|
||||
:param a: Top left number in the matrix.
|
||||
:type a: :obj:`int` or :obj:`float`
|
||||
:param b: Top middle number in the matrix.
|
||||
:type b: :obj:`int` or :obj:`float`
|
||||
:param c: Middle left number in the matrix.
|
||||
:type c: :obj:`int` or :obj:`float`
|
||||
:param d: Middle middle number in the matrix.
|
||||
:type d: :obj:`int` or :obj:`float`
|
||||
:param e: Bottom left number in the matrix.
|
||||
:type e: :obj:`int` or :obj:`float`
|
||||
:param f: Bottom middle number in the matrix.
|
||||
:type f: :obj:`int` or :obj:`float`
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
_to_bytes(a), _to_bytes(b), _to_bytes(c),
|
||||
_to_bytes(d), _to_bytes(e), _to_bytes(f), b'Tm')))
|
||||
|
||||
def transform(self, a, b, c, d, e, f):
|
||||
"""Modify current transformation matrix.
|
||||
|
||||
:param a: Top left number in the matrix.
|
||||
:type a: :obj:`int` or :obj:`float`
|
||||
:param b: Top middle number in the matrix.
|
||||
:type b: :obj:`int` or :obj:`float`
|
||||
:param c: Middle left number in the matrix.
|
||||
:type c: :obj:`int` or :obj:`float`
|
||||
:param d: Middle middle number in the matrix.
|
||||
:type d: :obj:`int` or :obj:`float`
|
||||
:param e: Bottom left number in the matrix.
|
||||
:type e: :obj:`int` or :obj:`float`
|
||||
:param f: Bottom middle number in the matrix.
|
||||
:type f: :obj:`int` or :obj:`float`
|
||||
|
||||
"""
|
||||
self.stream.append(b' '.join((
|
||||
_to_bytes(a), _to_bytes(b), _to_bytes(c),
|
||||
_to_bytes(d), _to_bytes(e), _to_bytes(f), b'cm')))
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
stream = b'\n'.join(_to_bytes(item) for item in self.stream)
|
||||
extra = Dictionary(self.extra.copy())
|
||||
if self.compress:
|
||||
extra['Filter'] = '/FlateDecode'
|
||||
compressobj = zlib.compressobj()
|
||||
stream = compressobj.compress(stream)
|
||||
stream += compressobj.flush()
|
||||
extra['Length'] = len(stream)
|
||||
return b'\n'.join((extra.data, b'stream', stream, b'endstream'))
|
||||
|
||||
|
||||
class String(Object):
|
||||
"""PDF String object.
|
||||
|
||||
Inherits from :class:`Object`.
|
||||
|
||||
"""
|
||||
def __init__(self, string=''):
|
||||
super().__init__()
|
||||
#: Unicode string.
|
||||
self.string = string
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
try:
|
||||
return b'(' + _to_bytes(self.string) + b')'
|
||||
except UnicodeEncodeError:
|
||||
encoded = BOM_UTF16_BE + str(self.string).encode('utf-16-be')
|
||||
return b'<' + encoded.hex().encode() + b'>'
|
||||
|
||||
|
||||
class Array(Object, list):
|
||||
"""PDF Array object.
|
||||
|
||||
Inherits from :class:`Object` and Python :obj:`list`.
|
||||
|
||||
"""
|
||||
def __init__(self, array=None):
|
||||
Object.__init__(self)
|
||||
list.__init__(self, array or [])
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
result = [b'[']
|
||||
for child in self:
|
||||
result.append(_to_bytes(child))
|
||||
result.append(b']')
|
||||
return b' '.join(result)
|
||||
|
||||
|
||||
class PDF:
|
||||
"""PDF document."""
|
||||
def __init__(self):
|
||||
#: Python :obj:`list` containing the PDF’s objects.
|
||||
self.objects = []
|
||||
|
||||
zero_object = Object()
|
||||
zero_object.generation = 65535
|
||||
zero_object.free = 'f'
|
||||
self.add_object(zero_object)
|
||||
|
||||
#: PDF :class:`Dictionary` containing the PDF’s pages.
|
||||
self.pages = Dictionary({
|
||||
'Type': '/Pages',
|
||||
'Kids': Array([]),
|
||||
'Count': 0,
|
||||
})
|
||||
self.add_object(self.pages)
|
||||
|
||||
#: PDF :class:`Dictionary` containing the PDF’s metadata.
|
||||
self.info = Dictionary({})
|
||||
self.add_object(self.info)
|
||||
|
||||
#: PDF :class:`Dictionary` containing references to the other objects.
|
||||
self.catalog = Dictionary({
|
||||
'Type': '/Catalog',
|
||||
'Pages': self.pages.reference,
|
||||
})
|
||||
self.add_object(self.catalog)
|
||||
|
||||
#: Current position in the PDF.
|
||||
self.current_position = 0
|
||||
#: Position of the cross reference table.
|
||||
self.xref_position = None
|
||||
|
||||
def add_page(self, page):
|
||||
"""Add page to the PDF.
|
||||
|
||||
:param page: New page.
|
||||
:type page: :class:`Dictionary`
|
||||
|
||||
"""
|
||||
self.pages['Count'] += 1
|
||||
self.add_object(page)
|
||||
self.pages['Kids'].extend([page.number, 0, 'R'])
|
||||
|
||||
def add_object(self, object_):
|
||||
"""Add object to the PDF."""
|
||||
object_.number = len(self.objects)
|
||||
self.objects.append(object_)
|
||||
|
||||
def write_line(self, content, output):
|
||||
"""Write line to output.
|
||||
|
||||
:param content: Content to write.
|
||||
:type content: :obj:`bytes`
|
||||
:param output: Output stream.
|
||||
:type output: binary :term:`file object`
|
||||
|
||||
"""
|
||||
self.current_position += len(content) + 1
|
||||
output.write(content + b'\n')
|
||||
|
||||
def write(self, output):
|
||||
"""Write PDF to output.
|
||||
|
||||
:param output: Output stream.
|
||||
:type output: binary :term:`file object`
|
||||
|
||||
"""
|
||||
# Write header
|
||||
self.write_line(b'%PDF-1.7', output)
|
||||
self.write_line(b'%\xf0\x9f\x96\xa4', output)
|
||||
|
||||
# Write all non-free PDF objects
|
||||
for object_ in self.objects:
|
||||
if object_.free == 'f':
|
||||
continue
|
||||
object_.offset = self.current_position
|
||||
self.write_line(object_.indirect, output)
|
||||
|
||||
# Write cross reference table
|
||||
self.xref_position = self.current_position
|
||||
self.write_line(b'xref', output)
|
||||
self.write_line(f'0 {len(self.objects)}'.encode(), output)
|
||||
for object_ in self.objects:
|
||||
self.write_line(
|
||||
(f'{object_.offset:010} {object_.generation:05} '
|
||||
f'{object_.free} ').encode(), output)
|
||||
|
||||
# Write trailer
|
||||
self.write_line(b'trailer', output)
|
||||
self.write_line(b'<<', output)
|
||||
self.write_line(f'/Size {len(self.objects)}'.encode(), output)
|
||||
self.write_line(b'/Root ' + self.catalog.reference, output)
|
||||
self.write_line(b'/Info ' + self.info.reference, output)
|
||||
self.write_line(b'>>', output)
|
||||
self.write_line(b'startxref', output)
|
||||
self.write_line(f'{self.xref_position}'.encode(), output)
|
||||
self.write_line(b'%%EOF', output)
|
59
pyproject.toml
Normal file
59
pyproject.toml
Normal file
|
@ -0,0 +1,59 @@
|
|||
[build-system]
|
||||
requires = ['flit_core >=3.2,<4']
|
||||
build-backend = 'flit_core.buildapi'
|
||||
|
||||
[project]
|
||||
name = 'pydyf'
|
||||
description = 'A low-level PDF generator.'
|
||||
keywords = ['pdf', 'generator']
|
||||
authors = [{name = 'CourtBouillon', email = 'contact@courtbouillon.org'}]
|
||||
maintainers = [{name = 'CourtBouillon', email = 'contact@courtbouillon.org'}]
|
||||
requires-python = '>=3.6'
|
||||
readme = {file = 'README.rst', content-type = 'text/x-rst'}
|
||||
license = {file = 'LICENSE'}
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
]
|
||||
dynamic = ['version']
|
||||
|
||||
[project.urls]
|
||||
Homepage = 'https://www.courtbouillon.org/pydyf'
|
||||
Documentation = 'https://doc.courtbouillon.org/pydyf/'
|
||||
Code = 'https://github.com/CourtBouillon/pydyf'
|
||||
Issues = 'https://github.com/CourtBouillon/pydyf/issues'
|
||||
Changelog = 'https://github.com/CourtBouillon/pydyf/releases'
|
||||
Donation = 'https://opencollective.com/courtbouillon'
|
||||
|
||||
[project.optional-dependencies]
|
||||
doc = ['sphinx', 'sphinx_rtd_theme']
|
||||
test = ['pytest', 'pytest-cov', 'pytest-flake8', 'pytest-isort', 'coverage[toml]', 'pillow']
|
||||
|
||||
[tool.flit.sdist]
|
||||
exclude = ['.*']
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = '--isort --flake8 --cov --no-cov-on-fail'
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
include = ['tests/*', 'pydyf/*']
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = ['pragma: no cover', 'def __repr__', 'raise NotImplementedError']
|
||||
omit = ['.*']
|
||||
|
||||
[tool.isort]
|
||||
default_section = 'FIRSTPARTY'
|
||||
multi_line_output = 4
|
31
setup.py
Normal file
31
setup.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
# setup.py generated by flit for tools that don't yet use PEP 517
|
||||
|
||||
from distutils.core import setup
|
||||
|
||||
packages = \
|
||||
['pydyf']
|
||||
|
||||
package_data = \
|
||||
{'': ['*']}
|
||||
|
||||
extras_require = \
|
||||
{'doc': ['sphinx', 'sphinx_rtd_theme'],
|
||||
'test': ['pytest',
|
||||
'pytest-cov',
|
||||
'pytest-flake8',
|
||||
'pytest-isort',
|
||||
'coverage[toml]',
|
||||
'pillow']}
|
||||
|
||||
setup(name='pydyf',
|
||||
version='0.1.1',
|
||||
description='A low-level PDF generator.',
|
||||
author=None,
|
||||
author_email='CourtBouillon <contact@courtbouillon.org>',
|
||||
url=None,
|
||||
packages=packages,
|
||||
package_data=package_data,
|
||||
extras_require=extras_require,
|
||||
python_requires='>=3.6',
|
||||
)
|
80
tests/__init__.py
Normal file
80
tests/__init__.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
"""
|
||||
Test suite for pydyf.
|
||||
|
||||
This module adds a PNG export based on GhostScript, that is released under
|
||||
AGPL. As "the end user has the ability to opt out of installing the AGPL
|
||||
version of [Ghostscript] during the install process", and with explicit
|
||||
aggreement from Artifex, it is OK to distribute this code under BSD.
|
||||
|
||||
See https://www.ghostscript.com/license.html.
|
||||
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, run
|
||||
|
||||
from PIL import Image
|
||||
|
||||
PIXELS_BY_CHAR = dict(
|
||||
_=(255, 255, 255), # white
|
||||
R=(255, 0, 0), # red
|
||||
B=(0, 0, 255), # blue
|
||||
G=(0, 255, 0), # lime green
|
||||
K=(0, 0, 0), # black
|
||||
z=None, # any color
|
||||
)
|
||||
|
||||
|
||||
def assert_pixels(document, reference_pixels):
|
||||
"""Test that the rendered document matches the reference pixels."""
|
||||
|
||||
# Transform the PDF document into a list of RGB tuples
|
||||
pdf = io.BytesIO()
|
||||
document.write(pdf)
|
||||
command = [
|
||||
'gs', '-q', '-dNOPAUSE', '-dSAFER', '-sDEVICE=png16m',
|
||||
'-r576', '-dDownScaleFactor=8', '-sOutputFile=-', '-']
|
||||
png = run(command, input=pdf.getvalue(), stdout=PIPE).stdout
|
||||
image = Image.open(io.BytesIO(png))
|
||||
pixels = image.getdata()
|
||||
|
||||
# Transform reference drawings into a list of RGB tuples
|
||||
lines = tuple(
|
||||
line.strip() for line in reference_pixels.splitlines() if line.strip())
|
||||
assert len({len(line) for line in lines}) == 1, (
|
||||
'The lines of reference pixels don’t have the same length')
|
||||
width, height = len(lines[0]), len(lines)
|
||||
assert (width, height) == image.size, (
|
||||
f'Reference size is {width}×{height}, '
|
||||
f'output size is {image.width}×{image.height}')
|
||||
reference_pixels = tuple(
|
||||
PIXELS_BY_CHAR[char] for line in lines for char in line)
|
||||
|
||||
# Compare pixels
|
||||
if pixels != reference_pixels: # pragma: no cover
|
||||
for i, (value, reference) in enumerate(zip(pixels, reference_pixels)):
|
||||
if reference is None:
|
||||
continue
|
||||
if any(value != reference
|
||||
for value, reference in zip(value, reference)):
|
||||
name = os.environ.get('PYTEST_CURRENT_TEST')
|
||||
name = name.split(':')[-1].split(' ')[0]
|
||||
write_png(f'{name}', pixels, width, height)
|
||||
reference_pixels = [
|
||||
pixel or (255, 255, 255) for pixel in reference_pixels]
|
||||
write_png(f'{name}-reference', reference_pixels, width, height)
|
||||
x, y = i % width, i // width
|
||||
assert 0, (
|
||||
f'Pixel ({x}, {y}) in {name}: '
|
||||
f'reference rgba{reference}, got rgba{value}')
|
||||
|
||||
|
||||
def write_png(name, pixels, width, height): # pragma: no cover
|
||||
"""Take a pixel matrix and write a PNG file."""
|
||||
directory = Path(__file__).parent / 'results'
|
||||
directory.mkdir(exist_ok=True)
|
||||
image = Image.new('RGB', (width, height))
|
||||
image.putdata(pixels)
|
||||
image.save(directory / f'{name}.png')
|
708
tests/test_pydyf.py
Normal file
708
tests/test_pydyf.py
Normal file
|
@ -0,0 +1,708 @@
|
|||
import pydyf
|
||||
|
||||
from . import assert_pixels
|
||||
|
||||
|
||||
def test_fill():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.fill()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_stroke():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.set_line_width(2)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
_KKKKKKK__
|
||||
_KKKKKKK__
|
||||
_KK___KK__
|
||||
_KK___KK__
|
||||
_KK___KK__
|
||||
_KK___KK__
|
||||
_KKKKKKK__
|
||||
_KKKKKKK__
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_line_to():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 2)
|
||||
draw.set_line_width(2)
|
||||
draw.line_to(2, 5)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
_KK_______
|
||||
_KK_______
|
||||
_KK_______
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_set_color_rgb_stroke():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.set_line_width(2)
|
||||
draw.set_color_rgb(0, 0, 255, stroke=True)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_set_color_rgb_fill():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.set_color_rgb(255, 0, 0)
|
||||
draw.fill()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__RRRRR___
|
||||
__RRRRR___
|
||||
__RRRRR___
|
||||
__RRRRR___
|
||||
__RRRRR___
|
||||
__RRRRR___
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_set_dash():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 2)
|
||||
draw.set_line_width(2)
|
||||
draw.line_to(2, 6)
|
||||
draw.set_dash([2, 1], 0)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
_KK_______
|
||||
__________
|
||||
_KK_______
|
||||
_KK_______
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_curve_to():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 5)
|
||||
draw.set_line_width(2)
|
||||
draw.curve_to(2, 5, 3, 5, 5, 5)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__KKK_____
|
||||
__KKK_____
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_curve_start_to():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 5)
|
||||
draw.set_line_width(2)
|
||||
draw.curve_start_to(3, 5, 5, 5)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__KKK_____
|
||||
__KKK_____
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_curve_end_to():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 5)
|
||||
draw.set_line_width(2)
|
||||
draw.curve_end_to(3, 5, 5, 5)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__KKK_____
|
||||
__KKK_____
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_transform():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 2)
|
||||
draw.set_line_width(2)
|
||||
draw.line_to(2, 5)
|
||||
draw.transform(1, 0, 0, 1, 1, 1)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__KK______
|
||||
__KK______
|
||||
__KK______
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_set_state():
|
||||
document = pydyf.PDF()
|
||||
|
||||
graphic_state = pydyf.Dictionary({
|
||||
'Type': '/ExtGState',
|
||||
'LW': 2,
|
||||
})
|
||||
document.add_object(graphic_state)
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.set_state('GS')
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
'Resources': pydyf.Dictionary({
|
||||
'ExtGState': pydyf.Dictionary({'GS': graphic_state.reference}),
|
||||
}),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
_KKKKKKK__
|
||||
_KKKKKKK__
|
||||
_KK___KK__
|
||||
_KK___KK__
|
||||
_KK___KK__
|
||||
_KK___KK__
|
||||
_KKKKKKK__
|
||||
_KKKKKKK__
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_fill_and_stroke():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.set_line_width(2)
|
||||
draw.set_color_rgb(0, 0, 255, stroke=True)
|
||||
draw.fill_and_stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
_BBKKKBB__
|
||||
_BBKKKBB__
|
||||
_BBKKKBB__
|
||||
_BBKKKBB__
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_clip():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(3, 3, 5, 6)
|
||||
draw.rectangle(4, 3, 2, 6)
|
||||
draw.clip()
|
||||
draw.end()
|
||||
draw.move_to(0, 5)
|
||||
draw.line_to(10, 5)
|
||||
draw.set_color_rgb(255, 0, 0, stroke=True)
|
||||
draw.set_line_width(2)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
___RRRRR__
|
||||
___RRRRR__
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_clip_even_odd():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(3, 3, 5, 6)
|
||||
draw.rectangle(4, 3, 2, 6)
|
||||
draw.clip(even_odd=True)
|
||||
draw.end()
|
||||
draw.move_to(0, 5)
|
||||
draw.line_to(10, 5)
|
||||
draw.set_color_rgb(255, 0, 0, stroke=True)
|
||||
draw.set_line_width(2)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
___R__RR__
|
||||
___R__RR__
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_close():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 2)
|
||||
draw.line_to(2, 8)
|
||||
draw.line_to(7, 8)
|
||||
draw.line_to(7, 2)
|
||||
draw.close()
|
||||
draw.set_color_rgb(0, 0, 255, stroke=True)
|
||||
draw.set_line_width(2)
|
||||
draw.stroke()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_stroke_and_close():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 2)
|
||||
draw.line_to(2, 8)
|
||||
draw.line_to(7, 8)
|
||||
draw.line_to(7, 2)
|
||||
draw.set_color_rgb(0, 0, 255, stroke=True)
|
||||
draw.set_line_width(2)
|
||||
draw.stroke_and_close()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BB___BB__
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_fill_stroke_and_close():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.move_to(2, 2)
|
||||
draw.line_to(2, 8)
|
||||
draw.line_to(7, 8)
|
||||
draw.line_to(7, 2)
|
||||
draw.set_color_rgb(255, 0, 0)
|
||||
draw.set_color_rgb(0, 0, 255, stroke=True)
|
||||
draw.set_line_width(2)
|
||||
draw.fill_stroke_and_close()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
_BBRRRBB__
|
||||
_BBRRRBB__
|
||||
_BBRRRBB__
|
||||
_BBRRRBB__
|
||||
_BBBBBBB__
|
||||
_BBBBBBB__
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_push_pop_state():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.push_state()
|
||||
draw.rectangle(4, 4, 2, 2)
|
||||
draw.set_color_rgb(255, 0, 0)
|
||||
draw.pop_state()
|
||||
draw.fill()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_types():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2.0, '5', b'6')
|
||||
draw.set_line_width(2.3456)
|
||||
draw.fill()
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_compress():
|
||||
document = pydyf.PDF()
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.fill()
|
||||
assert b'2 2 5 6' in draw.data
|
||||
|
||||
draw = pydyf.Stream(compress=True)
|
||||
draw.rectangle(2, 2, 5, 6)
|
||||
draw.fill()
|
||||
assert b'2 2 5 6' not in draw.data
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
__________
|
||||
__________
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__KKKKK___
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_text():
|
||||
document = pydyf.PDF()
|
||||
|
||||
font = pydyf.Dictionary({
|
||||
'Type': '/Font',
|
||||
'Subtype': '/Type1',
|
||||
'Name': '/F1',
|
||||
'BaseFont': '/Helvetica',
|
||||
'Encoding': '/MacRomanEncoding',
|
||||
})
|
||||
document.add_object(font)
|
||||
|
||||
draw = pydyf.Stream()
|
||||
draw.begin_text()
|
||||
draw.set_font_size('F1', 200)
|
||||
draw.text_matrix(1, 0, 0, 1, -20, 5)
|
||||
draw.show_text(pydyf.String('l'))
|
||||
draw.show_text(pydyf.String('É'))
|
||||
draw.end_text()
|
||||
|
||||
document.add_object(draw)
|
||||
|
||||
document.add_page(pydyf.Dictionary({
|
||||
'Type': '/Page',
|
||||
'Parent': document.pages.reference,
|
||||
'Contents': draw.reference,
|
||||
'MediaBox': pydyf.Array([0, 0, 10, 10]),
|
||||
'Resources': pydyf.Dictionary({
|
||||
'ProcSet': pydyf.Array(['/PDF', '/Text']),
|
||||
'Font': pydyf.Dictionary({'F1': font.reference}),
|
||||
}),
|
||||
}))
|
||||
|
||||
assert_pixels(document, '''
|
||||
KKKKKKKKKK
|
||||
KKKKKKKKKK
|
||||
KKKKKKKKKK
|
||||
KKKKKKKKKK
|
||||
KKKKKKKKKK
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
__________
|
||||
''')
|
||||
|
||||
|
||||
def test_string_encoding():
|
||||
assert pydyf.String('abc').data == b'(abc)'
|
||||
assert pydyf.String('déf').data == b'<feff006400e90066>'
|
||||
assert pydyf.String('♡').data == b'<feff2661>'
|
Loading…
Add table
Reference in a new issue