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