-
Notifications
You must be signed in to change notification settings - Fork 48
ENH: Add a rotate command, per discussion
#128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
2a114e9
[Bug] Update minimum python version
hwine b2e5fac
DEV: add lock file
hwine 643585a
DEV: record failing tests at start of work.
hwine 515f065
DEV: support non-project usage of `direnv`
hwine 0d97e3e
DEV: Add initial failing tests
hwine 1cb8661
DEV: cli tests pass
hwine 8944b59
DEV: Add test for default rotation
hwine 6ce6f41
ENH: Add "rotation" value to page meta output
hwine 62dbdb9
DEV: working with minimal tests
hwine cda2503
DEV: add additional test coverage
hwine 0916a67
DEV: Add post change test results
hwine 34c0f9d
DEV: Fix CI errors
hwine 56aa6f9
fixup! DEV: Fix CI errors
hwine 722d8f1
fixup! [Bug] Update minimum python version
hwine 1a6fc7b
fixup! DEV: Add initial failing tests
hwine ab2201b
DEV: correct --help output
hwine c3a71b4
Correct exception handling
hwine e053262
Apply suggestions from code review
hwine efc0e36
Apply suggestions from code review
hwine d4bfd82
Update pdfly/rotate.py
hwine File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,7 @@ celerybeat.pid | |
|
|
||
| # Environments | ||
| .env | ||
| .envrc | ||
| .venv | ||
| env/ | ||
| venv/ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| """ | ||
| Rotate specified pages by the specified amount | ||
|
|
||
| Example: | ||
| pdfly rotate --output output.pdf input.pdf 90 | ||
| Rotate all pages by 90 degrees (clockwise) | ||
|
|
||
| A file not followed by a page range (PGRGS) means all the pages of the file. | ||
|
|
||
| PAGE RANGES are like Python slices. | ||
|
|
||
| Remember, page indices start with zero. | ||
|
|
||
| Page range expression examples: | ||
|
|
||
| : all pages. -1 last page. | ||
| 22 just the 23rd page. :-1 all but the last page. | ||
| 0:3 the first three pages. -2 second-to-last page. | ||
| :3 the first three pages. -2: last two pages. | ||
| 5: from the sixth page onward. -3:-1 third & second to last. | ||
|
|
||
| The third, "stride" or "step" number is also recognized. | ||
|
|
||
| ::2 0 2 4 ... to the end. 3:0:-1 3 2 1 but not 0. | ||
| 1:10:2 1 3 5 7 9 2::-1 2 1 0. | ||
| ::-1 all pages in reverse order. | ||
|
|
||
|
|
||
| """ | ||
|
|
||
| from pathlib import Path | ||
| from typing import Set | ||
|
|
||
| from pypdf import ( | ||
| PageRange, | ||
| PdfReader, | ||
| PdfWriter, | ||
| ) | ||
| from rich.console import Console | ||
|
|
||
|
|
||
| def main( | ||
| filename: Path, | ||
| output: Path, | ||
| degrees: int, | ||
| page_range: str, | ||
| ) -> None: | ||
| try: | ||
| # Set up the streams | ||
| reader = PdfReader(filename) | ||
| pages = list(reader.pages) | ||
| writer = PdfWriter() | ||
|
|
||
| # Convert the page range into a set of page numbers | ||
| pages_to_rotate = convert_range_to_pages(page_range, len(pages)) | ||
|
|
||
| for page_index, page in enumerate(pages): | ||
| if page_index in pages_to_rotate: | ||
| page = page.rotate(degrees) | ||
| writer.add_page(page) | ||
|
|
||
| # Everything looks good! Write the output file. | ||
| with open(output, "wb") as output_fh: | ||
| writer.write(output_fh) | ||
|
|
||
| except Exception as error: | ||
| console = Console() | ||
| console.print(f"Error while rotating {filename}") | ||
| raise error | ||
|
|
||
|
|
||
| def convert_range_to_pages(page_range: str, num_pages: int) -> Set[int]: | ||
| pages_to_rotate = {*range(*PageRange(page_range).indices(num_pages))} | ||
| return pages_to_rotate |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| from pathlib import Path | ||
| from typing import List | ||
| import pytest | ||
| from pypdf import PdfReader | ||
|
|
||
| from .conftest import RESOURCES_ROOT, chdir, run_cli | ||
|
|
||
|
|
||
| def test_rotate_fewer_args(capsys, tmp_path): | ||
| with chdir(tmp_path): | ||
| exit_code = run_cli( | ||
| [ | ||
| "rotate", | ||
| ] | ||
| ) | ||
| assert exit_code == 2 | ||
| captured = capsys.readouterr() | ||
| assert "Missing argument" in captured.err | ||
|
|
||
|
|
||
| def test_rotate_extra_args(capsys, tmp_path): | ||
| with chdir(tmp_path): | ||
| exit_code = run_cli( | ||
| [ | ||
| "rotate", | ||
| "-o", | ||
| "/dev/null", | ||
| str(RESOURCES_ROOT / "box.pdf"), | ||
| "37", | ||
| "extra 1", | ||
| "extra 2", | ||
| ] | ||
| ) | ||
| assert exit_code == 2 | ||
| captured = capsys.readouterr() | ||
| assert "unexpected extra argument" in captured.err | ||
|
|
||
|
|
||
| def get_page_rotations(fname: Path) -> List[int]: | ||
| reader = PdfReader(fname) | ||
| rotations = [] | ||
| for page in reader.pages: | ||
| rotations.append(page.rotation) | ||
| return rotations | ||
|
|
||
|
|
||
| def diff_rotations( | ||
| in_: List[int], out: List[int], degrees: int = 0 | ||
| ) -> List[int]: | ||
| diffs = [] | ||
| for orig, rotated in zip(in_, out): | ||
| diffs.append(rotated - (orig + degrees)) | ||
| return diffs | ||
|
|
||
|
|
||
| def test_rotate_default(capsys, tmp_path): | ||
| in_fname = str(RESOURCES_ROOT / "input8.pdf") | ||
| out_fname = "output8.pdf" | ||
| degrees = 90 | ||
|
|
||
| with chdir(tmp_path): | ||
| print(f"{tmp_path=}") | ||
| exit_code = run_cli( | ||
| [ | ||
| "rotate", | ||
| "-o", | ||
| out_fname, | ||
| in_fname, | ||
| str(degrees), | ||
| ] | ||
| ) | ||
| in_rotations = get_page_rotations(in_fname) | ||
| out_rotations = get_page_rotations(out_fname) | ||
|
|
||
| assert exit_code == 0 | ||
|
|
||
| assert not any(diff_rotations(in_rotations, out_rotations, degrees)) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| # NB "slice" can not be specified as the empty string | ||
| ("degrees", "slice", "expected_diff"), | ||
| [ | ||
| (90, ":", [90, 90, 90, 90, 90, 90, 90, 90]), # every page | ||
| (90, "::2", [90, 0, 90, 0, 90, 0, 90, 0]), # every other, even index | ||
| (90, "1::2", [0, 90, 0, 90, 0, 90, 0, 90]), # every other, odd index | ||
| (90, ":2", [90, 90, 0, 0, 0, 0, 0, 0]), # first 2 | ||
| ( | ||
| -90, | ||
| ":", | ||
| [-90, -90, -90, -90, -90, -90, -90, -90], | ||
| ), # negative degrees works | ||
| ( | ||
| -720, | ||
| ":", | ||
| [-720, -720, -720, -720, -720, -720, -720, -720], | ||
| ), # |degrees| > 360 is also supported | ||
| ], | ||
| ) | ||
| def test_rotate_slices(capsys, tmp_path, degrees, slice, expected_diff): | ||
| in_fname = str(RESOURCES_ROOT / "input8.pdf") | ||
| out_fname = "output.pdf" | ||
| with chdir(tmp_path): | ||
| args = [ | ||
| "rotate", | ||
| "-o", | ||
| f"{out_fname}", | ||
| f"{in_fname}", | ||
| "--", # end options, so negative degree values work | ||
| f"{degrees}", | ||
| f"{slice}", | ||
| ] | ||
| exit_code = run_cli(args) | ||
| captured = capsys.readouterr() | ||
| assert exit_code == 0, captured.err | ||
|
|
||
| in_rotations = get_page_rotations(in_fname) | ||
| out_rotations = get_page_rotations(out_fname) | ||
| actual_diff = diff_rotations(in_rotations, out_rotations) | ||
|
|
||
| assert not any(diff_rotations(actual_diff, expected_diff)) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.