1
0
Fork 0

Adding upstream version 0.1.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-08 07:33:47 +01:00
parent a042d54ff1
commit 00981dc324
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
17 changed files with 2130 additions and 0 deletions

29
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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>`_:
Dont 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:
* Dont 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
View 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
View 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
View 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 youve found a bug in pydyf, its 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, well 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
View 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
View 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 pydyfs 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
View 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
View 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 CourtBouillons 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
View 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 PDFs objects.
self.objects = []
zero_object = Object()
zero_object.generation = 65535
zero_object.free = 'f'
self.add_object(zero_object)
#: PDF :class:`Dictionary` containing the PDFs pages.
self.pages = Dictionary({
'Type': '/Pages',
'Kids': Array([]),
'Count': 0,
})
self.add_object(self.pages)
#: PDF:class:`Dictionary` containing the PDFs 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
View 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
View 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
View 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 dont 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
View 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>'