pdfly 0.3.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pdfly-0.3.2 → pdfly-0.4.0}/.github/workflows/create-github-release.yaml +5 -1
- {pdfly-0.3.2 → pdfly-0.4.0}/.github/workflows/github-ci.yaml +2 -2
- {pdfly-0.3.2 → pdfly-0.4.0}/.github/workflows/publish-to-pypi.yaml +4 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/.pre-commit-config.yaml +7 -7
- {pdfly-0.3.2 → pdfly-0.4.0}/.readthedocs.yaml +1 -1
- {pdfly-0.3.2 → pdfly-0.4.0}/CHANGELOG.md +24 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/Makefile +2 -2
- {pdfly-0.3.2 → pdfly-0.4.0}/PKG-INFO +7 -2
- {pdfly-0.3.2 → pdfly-0.4.0}/README.md +3 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/conf.py +1 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/make_release.py +34 -17
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/__init__.py +1 -0
- pdfly-0.4.0/pdfly/_version.py +1 -0
- pdfly-0.4.0/pdfly/booklet.py +159 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/cat.py +38 -14
- pdfly-0.4.0/pdfly/cli.py +318 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/metadata.py +46 -20
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/pagemeta.py +1 -3
- pdfly-0.4.0/pdfly/rm.py +50 -0
- pdfly-0.4.0/pdfly/uncompress.py +52 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/up2.py +3 -1
- pdfly-0.4.0/pdfly/update_offsets.py +290 -0
- pdfly-0.4.0/pdfly/x2pdf.py +51 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pyproject.toml +6 -2
- {pdfly-0.3.2 → pdfly-0.4.0}/requirements/ci.txt +14 -34
- {pdfly-0.3.2 → pdfly-0.4.0}/requirements/dev.txt +58 -71
- {pdfly-0.3.2 → pdfly-0.4.0}/requirements/docs.txt +15 -15
- pdfly-0.4.0/resources/b.pdf +0 -0
- pdfly-0.4.0/resources/c.pdf +0 -0
- pdfly-0.4.0/resources/file-with-fixed-offsets.pdf +52 -0
- pdfly-0.4.0/resources/file-with-invalid-offsets.pdf +52 -0
- pdfly-0.4.0/resources/input8.pdf +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/setup.py +1 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/tests/conftest.py +10 -7
- pdfly-0.4.0/tests/test_booklet.py +121 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/tests/test_cat.py +1 -3
- {pdfly-0.3.2 → pdfly-0.4.0}/tests/test_extract_images.py +0 -2
- pdfly-0.4.0/tests/test_rm.py +229 -0
- pdfly-0.4.0/tests/test_uncompress.py +40 -0
- pdfly-0.4.0/tests/test_up2.py +63 -0
- pdfly-0.4.0/tests/test_update_offsets.py +104 -0
- pdfly-0.4.0/tests/test_x2pdf.py +86 -0
- pdfly-0.3.2/pdfly/_version.py +0 -1
- pdfly-0.3.2/pdfly/cli.py +0 -227
- pdfly-0.3.2/pdfly/x2pdf.py +0 -54
- pdfly-0.3.2/tests/test_x2pdf.py +0 -31
- {pdfly-0.3.2 → pdfly-0.4.0}/.github/dependabot.yml +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/.github/workflows/release.yaml +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/.gitignore +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/.gitmodules +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/.isort.cfg +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/CONTRIBUTORS.md +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/LICENSE +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/Makefile +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/index.rst +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/make.bat +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/meta/project-governance.md +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/user/installation.md +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/user/subcommand-cat.md +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/user/subcommand-meta.md +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/docs/user/subcommand-x2pdf.md +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/mypy.ini +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/__main__.py +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/_utils.py +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/compress.py +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/pdfly/extract_images.py +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/requirements/ci.in +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/requirements/dev.in +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/requirements/docs.in +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/resources/baleines.jpg +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/resources/box.pdf +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/resources/jpeg.pdf +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/resources/pythonknight.png +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/setup.cfg +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/tests/__init__.py +0 -0
- {pdfly-0.3.2 → pdfly-0.4.0}/tests/test_cli.py +0 -0
|
@@ -5,6 +5,10 @@ on:
|
|
|
5
5
|
tags:
|
|
6
6
|
- '*.*.*'
|
|
7
7
|
workflow_dispatch:
|
|
8
|
+
workflow_run:
|
|
9
|
+
workflows: ["Create git tag"]
|
|
10
|
+
types:
|
|
11
|
+
- completed
|
|
8
12
|
|
|
9
13
|
permissions:
|
|
10
14
|
contents: write
|
|
@@ -28,7 +32,7 @@ jobs:
|
|
|
28
32
|
git --no-pager tag -l "${latest_tag}" --format='%(contents:body)' >> "$GITHUB_ENV"
|
|
29
33
|
echo "$EOF" >> "$GITHUB_ENV"
|
|
30
34
|
- name: Create GitHub Release 🚀
|
|
31
|
-
uses: softprops/action-gh-release@
|
|
35
|
+
uses: softprops/action-gh-release@v2
|
|
32
36
|
with:
|
|
33
37
|
tag_name: ${{ env.latest_tag }}
|
|
34
38
|
name: Version ${{ env.latest_tag }}, ${{ env.date }}
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-20.04
|
|
23
23
|
strategy:
|
|
24
24
|
matrix:
|
|
25
|
-
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
|
25
|
+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
26
26
|
|
|
27
27
|
steps:
|
|
28
28
|
- name: Checkout Code
|
|
@@ -73,7 +73,7 @@ jobs:
|
|
|
73
73
|
- name: Test with ruff
|
|
74
74
|
run: |
|
|
75
75
|
echo `ruff --version`
|
|
76
|
-
ruff pdfly/
|
|
76
|
+
ruff check pdfly/
|
|
77
77
|
|
|
78
78
|
package:
|
|
79
79
|
name: Build & verify package
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pre-commit run --all-files
|
|
2
2
|
repos:
|
|
3
3
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
4
|
-
rev:
|
|
4
|
+
rev: v5.0.0
|
|
5
5
|
hooks:
|
|
6
6
|
- id: check-ast
|
|
7
7
|
- id: check-byte-order-marker
|
|
@@ -18,34 +18,34 @@ repos:
|
|
|
18
18
|
- id: check-added-large-files
|
|
19
19
|
args: ['--maxkb=1000']
|
|
20
20
|
- repo: https://github.com/psf/black
|
|
21
|
-
rev:
|
|
21
|
+
rev: 24.10.0
|
|
22
22
|
hooks:
|
|
23
23
|
- id: black
|
|
24
24
|
args: [--target-version, py36]
|
|
25
25
|
- repo: https://github.com/asottile/blacken-docs
|
|
26
|
-
rev: 1.
|
|
26
|
+
rev: 1.19.1
|
|
27
27
|
hooks:
|
|
28
28
|
- id: blacken-docs
|
|
29
29
|
additional_dependencies: [black==22.1.0]
|
|
30
30
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
31
|
-
rev: v0.
|
|
31
|
+
rev: v0.7.4
|
|
32
32
|
hooks:
|
|
33
33
|
- id: ruff
|
|
34
34
|
args: ['--fix']
|
|
35
35
|
exclude: "tests/"
|
|
36
36
|
- repo: https://github.com/asottile/pyupgrade
|
|
37
|
-
rev: v3.
|
|
37
|
+
rev: v3.19.0
|
|
38
38
|
hooks:
|
|
39
39
|
- id: pyupgrade
|
|
40
40
|
args: [--py36-plus]
|
|
41
41
|
- repo: https://github.com/pycqa/flake8
|
|
42
|
-
rev:
|
|
42
|
+
rev: 7.1.1
|
|
43
43
|
hooks:
|
|
44
44
|
- id: flake8
|
|
45
45
|
args: ["--ignore", "E,W,F"]
|
|
46
46
|
|
|
47
47
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
48
|
-
rev: 'v1.
|
|
48
|
+
rev: 'v1.13.0'
|
|
49
49
|
hooks:
|
|
50
50
|
- id: mypy
|
|
51
51
|
files: ^pdfly/.*
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## Version 0.4.0, 2024-12-08
|
|
4
|
+
|
|
5
|
+
### New Features (ENH)
|
|
6
|
+
- New `booklet` command to adjust offsets and lengths ([PR #77](https://github.com/py-pdf/pdfly/pull/77))
|
|
7
|
+
- New `uncompress` command ([PR #75](https://github.com/py-pdf/pdfly/pull/75))
|
|
8
|
+
- New `update-offsets` command to adjust offsets and lengths ([PR #15](https://github.com/py-pdf/pdfly/pull/15))
|
|
9
|
+
- New `rm` command ([PR #59](https://github.com/py-pdf/pdfly/pull/59))
|
|
10
|
+
- `metadata`: now also displaying CreationDate, Creator, Keywords & Subject ([PR #73](https://github.com/py-pdf/pdfly/pull/73))
|
|
11
|
+
- Add warning for out-of-bounds page range in pdfly `cat` command ([PR #58](https://github.com/py-pdf/pdfly/pull/58))
|
|
12
|
+
|
|
13
|
+
### Bug Fixes (BUG)
|
|
14
|
+
- `2-up` command, that only showed one page per sheet, on the left side, with blank space on the right ([PR #78](https://github.com/py-pdf/pdfly/pull/78))
|
|
15
|
+
|
|
16
|
+
[Full Changelog](https://github.com/py-pdf/pdfly/compare/0.3.3...0.4.0)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Version 0.3.3, 2024-04-14
|
|
20
|
+
|
|
21
|
+
### Developer Experience (DEV)
|
|
22
|
+
- Chain workflows
|
|
23
|
+
|
|
24
|
+
[Full Changelog](https://github.com/py-pdf/pdfly/compare/0.3.2...0.3.3)
|
|
25
|
+
|
|
26
|
+
|
|
3
27
|
## Version 0.3.2, 2024-04-14
|
|
4
28
|
|
|
5
29
|
### Developer Experience (DEV)
|
|
@@ -15,10 +15,10 @@ upload:
|
|
|
15
15
|
clean:
|
|
16
16
|
python setup.py clean --all
|
|
17
17
|
pyclean .
|
|
18
|
-
rm -rf
|
|
18
|
+
rm -rf tests/__pycache__ pypdf/__pycache__ Image9.png htmlcov docs/_build dist dont_commit_merged.pdf dont_commit_writer.pdf pypdf.egg-info pypdf_pdfLocation.txt
|
|
19
19
|
|
|
20
20
|
test:
|
|
21
|
-
pytest
|
|
21
|
+
pytest tests --cov --cov-report term-missing -vv --cov-report html --durations=3 --timeout=30
|
|
22
22
|
|
|
23
23
|
mutation-test:
|
|
24
24
|
mutmut run
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: pdfly
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A pure-python CLI application to manipulate PDF files
|
|
5
5
|
Author-email: Martin Thoma <info@martin-thoma.de>
|
|
6
6
|
Maintainer-email: Martin Thoma <info@martin-thoma.de>
|
|
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.7
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.8
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
23
|
Requires-Dist: pypdf>=3.8.2
|
|
22
24
|
Requires-Dist: typer>=0.9.0
|
|
23
25
|
Requires-Dist: pillow
|
|
@@ -62,10 +64,13 @@ $ pdfly --help
|
|
|
62
64
|
│ 2-up Create a booklet-style PDF from a single input. │
|
|
63
65
|
│ cat Concatenate pages from PDF files into a single PDF file. │
|
|
64
66
|
│ compress Compress a PDF. │
|
|
67
|
+
| uncompress Uncompresses a PDF. │
|
|
65
68
|
│ extract-images Extract images from PDF without resampling or altering. │
|
|
66
69
|
│ extract-text Extract text from a PDF file. │
|
|
67
70
|
│ meta Show metadata of a PDF file │
|
|
68
71
|
│ pagemeta Give details about a single page. │
|
|
72
|
+
│ rm Remove pages from PDF files. │
|
|
73
|
+
│ update-offsets Updates offsets and lengths in a simple PDF file. │
|
|
69
74
|
│ x2pdf Convert one or more files to PDF. Each file is a page. │
|
|
70
75
|
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
71
76
|
```
|
|
@@ -34,10 +34,13 @@ $ pdfly --help
|
|
|
34
34
|
│ 2-up Create a booklet-style PDF from a single input. │
|
|
35
35
|
│ cat Concatenate pages from PDF files into a single PDF file. │
|
|
36
36
|
│ compress Compress a PDF. │
|
|
37
|
+
| uncompress Uncompresses a PDF. │
|
|
37
38
|
│ extract-images Extract images from PDF without resampling or altering. │
|
|
38
39
|
│ extract-text Extract text from a PDF file. │
|
|
39
40
|
│ meta Show metadata of a PDF file │
|
|
40
41
|
│ pagemeta Give details about a single page. │
|
|
42
|
+
│ rm Remove pages from PDF files. │
|
|
43
|
+
│ update-offsets Updates offsets and lengths in a simple PDF file. │
|
|
41
44
|
│ x2pdf Convert one or more files to PDF. Each file is a page. │
|
|
42
45
|
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
43
46
|
```
|
|
@@ -5,6 +5,7 @@ This file only contains a selection of the most common options.
|
|
|
5
5
|
For a full list see the documentation:
|
|
6
6
|
https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
# -- Path setup --------------------------------------------------------------
|
|
9
10
|
|
|
10
11
|
# If extensions (or modules to document with autodoc) are in another directory,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Internal tool to update the
|
|
1
|
+
"""Internal tool to update the CHANGELOG."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import subprocess
|
|
@@ -9,6 +9,11 @@ from typing import Any, Dict, List, Tuple
|
|
|
9
9
|
|
|
10
10
|
from rich.prompt import Prompt
|
|
11
11
|
|
|
12
|
+
GH_ORG = "py-pdf"
|
|
13
|
+
GH_PROJECT = "pdfly"
|
|
14
|
+
VERSION_FILE_PATH = "pdfly/_version.py"
|
|
15
|
+
CHANGELOG_FILE_PATH = "CHANGELOG.md"
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
@dataclass(frozen=True)
|
|
14
19
|
class Change:
|
|
@@ -27,6 +32,7 @@ def main(changelog_path: str) -> None:
|
|
|
27
32
|
|
|
28
33
|
Args:
|
|
29
34
|
changelog_path: The location of the CHANGELOG file
|
|
35
|
+
|
|
30
36
|
"""
|
|
31
37
|
changelog = get_changelog(changelog_path)
|
|
32
38
|
git_tag = get_most_recent_git_tag()
|
|
@@ -41,7 +47,7 @@ def main(changelog_path: str) -> None:
|
|
|
41
47
|
|
|
42
48
|
today = datetime.now(tz=timezone.utc)
|
|
43
49
|
header = f"## Version {new_version}, {today:%Y-%m-%d}\n"
|
|
44
|
-
url = f"https://github.com/
|
|
50
|
+
url = f"https://github.com/{GH_ORG}/{GH_PROJECT}/compare/{git_tag}...{new_version}"
|
|
45
51
|
trailer = f"\n[Full Changelog]({url})\n\n"
|
|
46
52
|
new_entry = header + changes + trailer
|
|
47
53
|
print(new_entry)
|
|
@@ -61,9 +67,9 @@ def main(changelog_path: str) -> None:
|
|
|
61
67
|
def print_instructions(new_version: str) -> None:
|
|
62
68
|
"""Print release instructions."""
|
|
63
69
|
print("=" * 80)
|
|
64
|
-
print(f"☑
|
|
65
|
-
print("☑
|
|
66
|
-
print(
|
|
70
|
+
print(f"☑ {VERSION_FILE_PATH} was adjusted to '{new_version}'")
|
|
71
|
+
print(f"☑ {CHANGELOG_FILE_PATH} was adjusted")
|
|
72
|
+
print()
|
|
67
73
|
print("Now run:")
|
|
68
74
|
print(" git commit -eF RELEASE_COMMIT_MSG.md")
|
|
69
75
|
print(f" git tag -s {new_version} -eF RELEASE_TAG_MSG.md")
|
|
@@ -73,7 +79,7 @@ def print_instructions(new_version: str) -> None:
|
|
|
73
79
|
|
|
74
80
|
def adjust_version_py(version: str) -> None:
|
|
75
81
|
"""Adjust the __version__ string."""
|
|
76
|
-
with open(
|
|
82
|
+
with open(VERSION_FILE_PATH, "w") as fp:
|
|
77
83
|
fp.write(f'__version__ = "{version}"\n')
|
|
78
84
|
|
|
79
85
|
|
|
@@ -93,8 +99,7 @@ def get_version_interactive(new_version: str, changes: str) -> str:
|
|
|
93
99
|
|
|
94
100
|
def is_semantic_version(version: str) -> bool:
|
|
95
101
|
"""Check if the given version is a semantic version."""
|
|
96
|
-
#
|
|
97
|
-
# This is meant for pdfly only and we don't make pre-releases
|
|
102
|
+
# This doesn't cover the edge-cases like pre-releases
|
|
98
103
|
if version.count(".") != 2:
|
|
99
104
|
return False
|
|
100
105
|
try:
|
|
@@ -147,6 +152,7 @@ def version_bump(git_tag: str) -> str:
|
|
|
147
152
|
|
|
148
153
|
Returns:
|
|
149
154
|
The new version where the patch version is bumped.
|
|
155
|
+
|
|
150
156
|
"""
|
|
151
157
|
# just assume a patch version change
|
|
152
158
|
major, minor, patch = git_tag.split(".")
|
|
@@ -162,6 +168,7 @@ def get_changelog(changelog_path: str) -> str:
|
|
|
162
168
|
|
|
163
169
|
Returns:
|
|
164
170
|
Data of the CHANGELOG
|
|
171
|
+
|
|
165
172
|
"""
|
|
166
173
|
with open(changelog_path) as fh:
|
|
167
174
|
changelog = fh.read()
|
|
@@ -175,6 +182,7 @@ def write_changelog(new_changelog: str, changelog_path: str) -> None:
|
|
|
175
182
|
Args:
|
|
176
183
|
new_changelog: Contents of the new CHANGELOG
|
|
177
184
|
changelog_path: Path where the CHANGELOG file is
|
|
185
|
+
|
|
178
186
|
"""
|
|
179
187
|
with open(changelog_path, "w") as fh:
|
|
180
188
|
fh.write(new_changelog)
|
|
@@ -189,6 +197,7 @@ def get_formatted_changes(git_tag: str) -> Tuple[str, str]:
|
|
|
189
197
|
|
|
190
198
|
Returns:
|
|
191
199
|
Changes done since git_tag
|
|
200
|
+
|
|
192
201
|
"""
|
|
193
202
|
commits = get_git_commits_since_tag(git_tag)
|
|
194
203
|
|
|
@@ -249,8 +258,11 @@ def get_formatted_changes(git_tag: str) -> Tuple[str, str]:
|
|
|
249
258
|
|
|
250
259
|
if grouped:
|
|
251
260
|
output += "\n### Other\n"
|
|
261
|
+
output_with_user += "\n### Other\n"
|
|
252
262
|
for prefix in grouped:
|
|
253
|
-
|
|
263
|
+
for commit_dict in grouped[prefix]:
|
|
264
|
+
output += f"- {prefix}: {commit_dict['msg']}\n"
|
|
265
|
+
output_with_user += f"- {prefix}: {commit_dict['msg']} by @{commit_dict['author']}\n"
|
|
254
266
|
|
|
255
267
|
return output, output_with_user
|
|
256
268
|
|
|
@@ -261,6 +273,7 @@ def get_most_recent_git_tag() -> str:
|
|
|
261
273
|
|
|
262
274
|
Returns:
|
|
263
275
|
Most recently created git tag.
|
|
276
|
+
|
|
264
277
|
"""
|
|
265
278
|
git_tag = str(
|
|
266
279
|
subprocess.check_output(
|
|
@@ -280,18 +293,18 @@ def get_author_mapping(line_count: int) -> Dict[str, str]:
|
|
|
280
293
|
|
|
281
294
|
Returns:
|
|
282
295
|
A mapping of long commit hashes to author login handles.
|
|
296
|
+
|
|
283
297
|
"""
|
|
284
298
|
per_page = min(line_count, 100)
|
|
285
299
|
page = 1
|
|
286
300
|
mapping: Dict[str, str] = {}
|
|
287
301
|
for _ in range(0, line_count, per_page):
|
|
288
|
-
with urllib.request.urlopen(
|
|
289
|
-
f"https://api.github.com/repos/
|
|
302
|
+
with urllib.request.urlopen(
|
|
303
|
+
f"https://api.github.com/repos/{GH_ORG}/{GH_PROJECT}/commits?per_page={per_page}&page={page}"
|
|
290
304
|
) as response:
|
|
291
305
|
commits = json.loads(response.read())
|
|
292
306
|
page += 1
|
|
293
307
|
for commit in commits:
|
|
294
|
-
print(commit)
|
|
295
308
|
if commit["author"]:
|
|
296
309
|
gh_handle = commit["author"]["login"]
|
|
297
310
|
else:
|
|
@@ -311,8 +324,9 @@ def get_git_commits_since_tag(git_tag: str) -> List[Change]:
|
|
|
311
324
|
|
|
312
325
|
Returns:
|
|
313
326
|
List of all changes since git_tag.
|
|
327
|
+
|
|
314
328
|
"""
|
|
315
|
-
commits =
|
|
329
|
+
commits = (
|
|
316
330
|
subprocess.check_output(
|
|
317
331
|
[
|
|
318
332
|
"git",
|
|
@@ -323,8 +337,10 @@ def get_git_commits_since_tag(git_tag: str) -> List[Change]:
|
|
|
323
337
|
],
|
|
324
338
|
stderr=subprocess.STDOUT,
|
|
325
339
|
)
|
|
326
|
-
|
|
327
|
-
|
|
340
|
+
.decode("UTF-8")
|
|
341
|
+
.strip()
|
|
342
|
+
)
|
|
343
|
+
lines = commits.splitlines()
|
|
328
344
|
authors = get_author_mapping(len(lines))
|
|
329
345
|
return [parse_commit_line(line, authors) for line in lines if line != ""]
|
|
330
346
|
|
|
@@ -341,13 +357,14 @@ def parse_commit_line(line: str, authors: Dict[str, str]) -> Change:
|
|
|
341
357
|
|
|
342
358
|
Raises:
|
|
343
359
|
ValueError: The commit line is not well-structured
|
|
360
|
+
|
|
344
361
|
"""
|
|
345
362
|
parts = line.split(":::")
|
|
346
363
|
if len(parts) != 3:
|
|
347
364
|
raise ValueError(f"Invalid commit line: '{line}'")
|
|
348
365
|
commit_hash, rest, author = parts
|
|
349
366
|
if ":" in rest:
|
|
350
|
-
prefix, message = rest.split(":", 1)
|
|
367
|
+
prefix, message = rest.split(": ", 1)
|
|
351
368
|
else:
|
|
352
369
|
prefix = ""
|
|
353
370
|
message = rest
|
|
@@ -374,4 +391,4 @@ def parse_commit_line(line: str, authors: Dict[str, str]) -> Change:
|
|
|
374
391
|
|
|
375
392
|
|
|
376
393
|
if __name__ == "__main__":
|
|
377
|
-
main(
|
|
394
|
+
main(CHANGELOG_FILE_PATH)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reorder and two-up PDF pages for booklet printing.
|
|
3
|
+
|
|
4
|
+
If the number of pages is not a multiple of four, pages are
|
|
5
|
+
added until it is a multiple of four. This includes a centerfold
|
|
6
|
+
in the middle of the booklet and a single page on the inside
|
|
7
|
+
back cover. The content of those pages are from the
|
|
8
|
+
centerfold-file and blank-page-file files, if specified, otherwise
|
|
9
|
+
they are blank pages.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
pdfly booklet input.pdf output.pdf
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.
|
|
17
|
+
# All rights reserved. This software is available under a BSD license;
|
|
18
|
+
# see https://github.com/py-pdf/pypdf/LICENSE
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import traceback
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Generator, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
from pypdf import (
|
|
26
|
+
PageObject,
|
|
27
|
+
PdfReader,
|
|
28
|
+
PdfWriter,
|
|
29
|
+
)
|
|
30
|
+
from pypdf.generic import RectangleObject
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(
|
|
34
|
+
filename: Path,
|
|
35
|
+
output: Path,
|
|
36
|
+
inside_cover_file: Optional[Path],
|
|
37
|
+
centerfold_file: Optional[Path],
|
|
38
|
+
) -> None:
|
|
39
|
+
try:
|
|
40
|
+
# Set up the streams
|
|
41
|
+
reader = PdfReader(filename)
|
|
42
|
+
pages = list(reader.pages)
|
|
43
|
+
writer = PdfWriter()
|
|
44
|
+
|
|
45
|
+
# Add blank pages to make the number of pages a multiple of 4
|
|
46
|
+
# If the user specified an inside-back-cover file, use it.
|
|
47
|
+
blank_page = PageObject.create_blank_page(
|
|
48
|
+
width=pages[0].mediabox.width, height=pages[0].mediabox.height
|
|
49
|
+
)
|
|
50
|
+
if len(pages) % 2 == 1:
|
|
51
|
+
if inside_cover_file:
|
|
52
|
+
ic_reader_page = fetch_first_page(inside_cover_file)
|
|
53
|
+
pages.insert(-1, ic_reader_page)
|
|
54
|
+
else:
|
|
55
|
+
pages.insert(-1, blank_page)
|
|
56
|
+
if len(pages) % 4 == 2:
|
|
57
|
+
pages.insert(len(pages) // 2, blank_page)
|
|
58
|
+
pages.insert(len(pages) // 2, blank_page)
|
|
59
|
+
requires_centerfold = True
|
|
60
|
+
else:
|
|
61
|
+
requires_centerfold = False
|
|
62
|
+
|
|
63
|
+
# Reorder the pages and place two pages side by side (2-up) on each sheet
|
|
64
|
+
for lhs, rhs in page_iter(len(pages)):
|
|
65
|
+
pages[lhs].merge_translated_page(
|
|
66
|
+
page2=pages[rhs],
|
|
67
|
+
tx=pages[lhs].mediabox.width,
|
|
68
|
+
ty=0,
|
|
69
|
+
expand=True,
|
|
70
|
+
over=True,
|
|
71
|
+
)
|
|
72
|
+
writer.add_page(pages[lhs])
|
|
73
|
+
|
|
74
|
+
# If a centerfold was required, it is already
|
|
75
|
+
# present as a pair of blank pages. If the user
|
|
76
|
+
# specified a centerfold file, use it instead.
|
|
77
|
+
if requires_centerfold and centerfold_file:
|
|
78
|
+
centerfold_page = fetch_first_page(centerfold_file)
|
|
79
|
+
last_page = writer.pages[-1]
|
|
80
|
+
if centerfold_page.rotation != 0:
|
|
81
|
+
centerfold_page.transfer_rotation_to_content()
|
|
82
|
+
if requires_rotate(centerfold_page.mediabox, last_page.mediabox):
|
|
83
|
+
centerfold_page = centerfold_page.rotate(270)
|
|
84
|
+
if centerfold_page.rotation != 0:
|
|
85
|
+
centerfold_page.transfer_rotation_to_content()
|
|
86
|
+
last_page.merge_page(centerfold_page)
|
|
87
|
+
|
|
88
|
+
# Everything looks good! Write the output file.
|
|
89
|
+
with open(output, "wb") as output_fh:
|
|
90
|
+
writer.write(output_fh)
|
|
91
|
+
|
|
92
|
+
except Exception:
|
|
93
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
94
|
+
print(f"Error while reading {filename}", file=sys.stderr)
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def requires_rotate(a: RectangleObject, b: RectangleObject) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Return True if a and b are rotated relative to each other.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
a (RectangleObject): The first rectangle.
|
|
104
|
+
b (RectangleObject): The second rectangle.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
a_portrait = a.height > a.width
|
|
108
|
+
b_portrait = b.height > b.width
|
|
109
|
+
return a_portrait != b_portrait
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def fetch_first_page(filename: Path) -> PageObject:
|
|
113
|
+
"""
|
|
114
|
+
Fetch the first page of a PDF file.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
filename (Path): The path to the PDF file.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
PageObject: The first page of the PDF file.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
return PdfReader(filename).pages[0]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# This function written with inspiration, assistance, and code
|
|
127
|
+
# from claude.ai & Github Copilot
|
|
128
|
+
def page_iter(num_pages: int) -> Generator[Tuple[int, int], None, None]:
|
|
129
|
+
"""
|
|
130
|
+
Generate pairs of page numbers for printing a booklet.
|
|
131
|
+
This function assumes that the total number of pages is divisible by 4.
|
|
132
|
+
It yields tuples of page numbers that should be printed on the same sheet
|
|
133
|
+
of paper to create a booklet.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
num_pages (int): The total number of pages in the document. Must be divisible by 4.
|
|
137
|
+
|
|
138
|
+
Yields:
|
|
139
|
+
Generator[Tuple[int, int], None, None]: Tuples containing pairs of page numbers.
|
|
140
|
+
Each tuple represents the page numbers to be printed on one side of a sheet.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If the number of pages is not divisible by 4.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
if num_pages % 4 != 0:
|
|
147
|
+
raise ValueError("Number of pages must be divisible by 4")
|
|
148
|
+
|
|
149
|
+
for sheet in range(num_pages // 4):
|
|
150
|
+
# Outside the fold
|
|
151
|
+
last_page = num_pages - sheet * 2 - 1
|
|
152
|
+
first_page = sheet * 2
|
|
153
|
+
|
|
154
|
+
# Inside the fold
|
|
155
|
+
second_page = sheet * 2 + 1
|
|
156
|
+
second_to_last_page = num_pages - sheet * 2 - 2
|
|
157
|
+
|
|
158
|
+
yield last_page, first_page
|
|
159
|
+
yield second_page, second_to_last_page
|
|
@@ -37,11 +37,11 @@ Examples
|
|
|
37
37
|
In case you don't want chapter 10 before chapter 2.
|
|
38
38
|
|
|
39
39
|
"""
|
|
40
|
+
|
|
40
41
|
# Copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.
|
|
41
42
|
# All rights reserved. This software is available under a BSD license;
|
|
42
43
|
# see https://github.com/py-pdf/pypdf/LICENSE
|
|
43
44
|
|
|
44
|
-
|
|
45
45
|
import os
|
|
46
46
|
import sys
|
|
47
47
|
import traceback
|
|
@@ -52,7 +52,11 @@ from pypdf import PageRange, PdfReader, PdfWriter, parse_filename_page_ranges
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def main(
|
|
55
|
-
filename: Path,
|
|
55
|
+
filename: Path,
|
|
56
|
+
fn_pgrgs: List[str],
|
|
57
|
+
output: Path,
|
|
58
|
+
verbose: bool,
|
|
59
|
+
inverted_page_selection: bool = False,
|
|
56
60
|
) -> None:
|
|
57
61
|
filename_page_ranges = parse_filepaths_and_pagerange_args(
|
|
58
62
|
filename, fn_pgrgs
|
|
@@ -66,15 +70,35 @@ def main(
|
|
|
66
70
|
writer = PdfWriter()
|
|
67
71
|
in_fs = {}
|
|
68
72
|
try:
|
|
69
|
-
for
|
|
73
|
+
for filepath, page_range in filename_page_ranges: # type: ignore
|
|
70
74
|
if verbose:
|
|
71
|
-
print(
|
|
72
|
-
if
|
|
73
|
-
in_fs[
|
|
74
|
-
|
|
75
|
-
reader = PdfReader(in_fs[
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
print(filepath, page_range, file=sys.stderr)
|
|
76
|
+
if filepath not in in_fs:
|
|
77
|
+
in_fs[filepath] = open(filepath, "rb")
|
|
78
|
+
|
|
79
|
+
reader = PdfReader(in_fs[filepath])
|
|
80
|
+
num_pages = len(reader.pages)
|
|
81
|
+
start, end, step = page_range.indices(num_pages)
|
|
82
|
+
if (
|
|
83
|
+
start < 0
|
|
84
|
+
or end < 0
|
|
85
|
+
or start >= num_pages
|
|
86
|
+
or end > num_pages
|
|
87
|
+
or start > end
|
|
88
|
+
):
|
|
89
|
+
print(
|
|
90
|
+
f"WARNING: Page range {page_range} is out of bounds",
|
|
91
|
+
file=sys.stderr,
|
|
92
|
+
)
|
|
93
|
+
if inverted_page_selection:
|
|
94
|
+
all_page_nums = set(range(len(reader.pages)))
|
|
95
|
+
page_nums = set(range(*page_range.indices(len(reader.pages))))
|
|
96
|
+
inverted_page_nums = all_page_nums - page_nums
|
|
97
|
+
for page_num in inverted_page_nums:
|
|
98
|
+
writer.add_page(reader.pages[page_num])
|
|
99
|
+
else:
|
|
100
|
+
for page_num in range(*page_range.indices(len(reader.pages))):
|
|
101
|
+
writer.add_page(reader.pages[page_num])
|
|
78
102
|
writer.write(output_fh)
|
|
79
103
|
except Exception:
|
|
80
104
|
print(traceback.format_exc(), file=sys.stderr)
|
|
@@ -92,11 +116,11 @@ def parse_filepaths_and_pagerange_args(
|
|
|
92
116
|
fn_pgrgs_l = list(fn_pgrgs)
|
|
93
117
|
fn_pgrgs_l.insert(0, str(filename))
|
|
94
118
|
filename_page_ranges, invalid_filepaths = [], []
|
|
95
|
-
for
|
|
96
|
-
if Path(
|
|
97
|
-
filename_page_ranges.append((
|
|
119
|
+
for filepath, page_range in parse_filename_page_ranges(fn_pgrgs_l): # type: ignore
|
|
120
|
+
if Path(filepath).is_file():
|
|
121
|
+
filename_page_ranges.append((Path(filepath), page_range))
|
|
98
122
|
else:
|
|
99
|
-
invalid_filepaths.append(str(
|
|
123
|
+
invalid_filepaths.append(str(filepath))
|
|
100
124
|
if invalid_filepaths:
|
|
101
125
|
print(
|
|
102
126
|
f"Invalid file path or page range provided: {' '.join(invalid_filepaths)}",
|