diff options
-rw-r--r-- | .github/workflows/doconfly.yml | 2 | ||||
-rw-r--r-- | .github/workflows/tests.yml | 8 | ||||
-rw-r--r-- | docs/changelog.rst | 82 | ||||
-rw-r--r-- | docs/common_use_cases.rst | 84 | ||||
-rw-r--r-- | docs/conf.py | 3 | ||||
-rwxr-xr-x | pydyf/__init__.py | 53 | ||||
-rw-r--r-- | pyproject.toml | 2 |
7 files changed, 222 insertions, 12 deletions
diff --git a/.github/workflows/doconfly.yml b/.github/workflows/doconfly.yml index a720a66..c256435 100644 --- a/.github/workflows/doconfly.yml +++ b/.github/workflows/doconfly.yml @@ -2,7 +2,7 @@ name: doconfly on: push: branches: - - master + - main tags: - "*" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89003af..8de9881 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,15 +8,15 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.11'] + python-version: ['3.12'] include: - os: ubuntu-latest - python-version: '3.7' + python-version: '3.8' - os: ubuntu-latest python-version: 'pypy-3.8' steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Ghostscript (Ubuntu) diff --git a/docs/changelog.rst b/docs/changelog.rst index e116ac8..661a696 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,88 @@ Changelog ========= +Version 0.9.0 +------------- + +Released on 2024-02-26. + +Dependencies: + +* Python 3.12 is supported and tested +* Python 3.8+ is now needed, Python 3.7 is not supported anymore + +New features: + +* Add inline images support + +Performance: + +* Simplify `_to_bytes()` + +Documentation: + +* Add sample to create a PDF with metadata + +Contributors: + +* Panagiotis H.M. Issaris +* Guillaume Ayoub +* Lucie Anglade + +Backers and sponsors: + +* Spacinov +* Kobalt +* Grip Angebotssoftware +* Manuel Barkhau +* SimonSoft +* Menutech +* KontextWork +* René Fritz +* Simon Sapin +* Arcanite +* TrainingSparkle +* Healthchecks.io +* Hammerbacher +* Docraptor +* Yanal-Yvez Fargialla +* Morntag +* NBCO + + +Version 0.8.0 +------------- + +Released on 2023-09-25. + +New features: + +* Add text rise operator + +Backers and sponsors: + +* Spacinov +* Kobalt +* Grip Angebotssoftware +* Manuel Barkhau +* SimonSoft +* Menutech +* KontextWork +* NCC Group +* René Fritz +* Nicola Auchmuty +* Syslifters +* Hammerbacher +* TrainingSparkle +* Daniel Kucharski +* Healthchecks.io +* Yanal-Yvez Fargialla +* WakaTime +* Paheko +* Synapsium +* DocRaptor + + Version 0.7.0 ------------- diff --git a/docs/common_use_cases.rst b/docs/common_use_cases.rst index b045fa6..d8343b3 100644 --- a/docs/common_use_cases.rst +++ b/docs/common_use_cases.rst @@ -179,3 +179,87 @@ Display text with open('document.pdf', 'wb') as f: document.write(f) + + +Add metadata +------------ + +.. code-block:: python + + import datetime + + import pydyf + + document = pydyf.PDF() + document.info['Author'] = pydyf.String('Jane Doe') + document.info['Creator'] = pydyf.String('pydyf') + document.info['Keywords'] = pydyf.String('some keywords') + document.info['Producer'] = pydyf.String('The producer') + document.info['Subject'] = pydyf.String('An example PDF') + document.info['Title'] = pydyf.String('A PDF containing metadata') + now = datetime.datetime.now() + document.info['CreationDate'] = pydyf.String(now.strftime('D:%Y%m%d%H%M%S')) + + document.add_page( + pydyf.Dictionary( + { + 'Type': '/Page', + 'Parent': document.pages.reference, + 'MediaBox': pydyf.Array([0, 0, 200, 200]), + } + ) + ) + + # 550 bytes PDF + with open('metadata.pdf', 'wb') as f: + document.write(f) + + +Display inline QR-code image +---------------------------- + +.. code-block:: python + + import pydyf + import qrcode + + # Create a QR code image + image = qrcode.make('Some data here') + raw_data = image.tobytes() + width = image.size[0] + height = image.size[1] + + document = pydyf.PDF() + stream = pydyf.Stream(compress=True) + stream.push_state() + x = 0 + y = 0 + stream.transform(width, 0, 0, height, x, y) + # Add the 1-bit grayscale image inline in the PDF + stream.inline_image(width, height, 'Gray', 1, raw_data) + stream.pop_state() + document.add_object(stream) + + # 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, 400, 400]), + 'Resources': pydyf.Dictionary( + { + 'ProcSet': pydyf.Array( + ['/PDF', '/ImageB', '/ImageC', '/ImageI'] + ), + } + ), + 'Contents': stream.reference, + } + ) + ) + + # 909 bytes PDF + with open('qrcode.pdf', 'wb') as f: + document.write(f, compress=True) + diff --git a/docs/conf.py b/docs/conf.py index a08c175..c868316 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,9 @@ html_theme_options = { 'collapse_navigation': False, } +# Favicon URL +html_favicon = 'https://www.courtbouillon.org/static/images/favicon.png' + # 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". diff --git a/pydyf/__init__.py b/pydyf/__init__.py index 3d0b122..d8e1d7b 100755 --- a/pydyf/__init__.py +++ b/pydyf/__init__.py @@ -3,28 +3,27 @@ A low-level PDF generator. """ +import base64 import re import zlib from codecs import BOM_UTF16_BE from hashlib import md5 from math import ceil, log -VERSION = __version__ = '0.7.0' +VERSION = __version__ = '0.9.0' 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') + return str(int(item)).encode('ascii') else: return f'{item:f}'.rstrip('0').encode('ascii') - elif isinstance(item, int): - return f'{item:d}'.encode('ascii') + elif isinstance(item, Object): + return item.data return str(item).encode('ascii') @@ -280,6 +279,10 @@ class Stream(Object): """Set text rendering mode.""" self.stream.append(_to_bytes(mode) + b' Tr') + def set_text_rise(self, height): + """Set text rise.""" + self.stream.append(_to_bytes(height) + b' Ts') + def set_line_cap(self, line_cap): """Set line cap style.""" self.stream.append(_to_bytes(line_cap) + b' J') @@ -362,6 +365,44 @@ class Stream(Object): _to_bytes(a), _to_bytes(b), _to_bytes(c), _to_bytes(d), _to_bytes(e), _to_bytes(f), b'cm'))) + def inline_image(self, width, height, color_space, bpc, raw_data): + """Add an inline image. + + :param width: The width of the image. + :type width: :obj:`int` + :param height: The height of the image. + :type height: :obj:`int` + :param colorspace: The color space of the image, f.e. RGB, Gray. + :type colorspace: :obj:`str` + :param bpc: The bits per component. 1 for BW, 8 for grayscale. + :type bpc: :obj:`int` + :param raw_data: The raw pixel data. + + """ + if self.compress: + data = zlib.compress(raw_data) + else: + data = raw_data + enc_data = base64.a85encode(data) + self.stream.append( + b' '.join( + ( + b'BI', + b'/W', _to_bytes(width), + b'/H', _to_bytes(height), + b'/BPC', _to_bytes(bpc), + b'/CS', + b'/Device' + color_space.encode(), + b'/F', + b'[/A85 /Fl]' if self.compress else b'/A85', + b'/L', _to_bytes(len(enc_data) + 2), + b'ID', + enc_data + b'~>', + b'EI', + ) + ) + ) + @property def data(self): stream = b'\n'.join(_to_bytes(item) for item in self.stream) diff --git a/pyproject.toml b/pyproject.toml index e26cbfe..2c17097 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,11 +19,11 @@ classifiers = [ 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ] |