Skip to content

Commit 02f0b69

Browse files
authored
ENH: Add pagemeta subcommand (#17)
* Remove a Flake8-Plugin * Cleanup ruff * Update README
1 parent 8afa83d commit 02f0b69

14 files changed

Lines changed: 181 additions & 62 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ repos:
3535
rev: 'v0.0.254'
3636
hooks:
3737
- id: ruff
38+
args: [--fix]
3839
- repo: https://github.com/asottile/pyupgrade
3940
rev: v3.3.1
4041
hooks:

README.md

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,42 @@ As `pdfly` is an application, you might want to install it with [`pipx`](https:/
1919

2020
```console
2121
$ pdfly --help
22-
Usage: pdfly [OPTIONS] COMMAND [ARGS]...
23-
24-
pdfly is a pure-python cli application for manipulating PDF files.
25-
26-
Options:
27-
--version
28-
--help Show this message and exit.
29-
30-
Commands:
31-
2-up Create a booklet-style PDF from a single input.
32-
cat Concatenate pages from PDF files into a single PDF file.
33-
compress Compress a PDF.
34-
extract-images Extract images from PDF without resampling or altering.
35-
extract-text Extract text from a PDF file.
36-
meta Show metadata of a PDF file
3722

23+
Usage: pdfly [OPTIONS] COMMAND [ARGS]...
24+
25+
pdfly is a pure-python cli application for manipulating PDF files.
26+
27+
╭─ Options ───────────────────────────────────────────────────────────────────╮
28+
│ --version │
29+
│ --help Show this message and exit. │
30+
╰─────────────────────────────────────────────────────────────────────────────╯
31+
╭─ Commands ──────────────────────────────────────────────────────────────────╮
32+
│ 2-up Create a booklet-style PDF from a single input. │
33+
│ cat Concatenate pages from PDF files into a single PDF file. │
34+
│ compress Compress a PDF. │
35+
│ extract-images Extract images from PDF without resampling or altering. │
36+
│ extract-text Extract text from a PDF file. │
37+
│ meta Show metadata of a PDF file │
38+
│ pagemeta Give details about a single page. │
39+
╰─────────────────────────────────────────────────────────────────────────────╯
3840
```
3941

4042
You can see the help of every subcommand by typing:
4143

4244
```console
4345
$ pdfly 2-up --help
44-
Usage: pdfly 2-up [OPTIONS] PDF OUT
45-
46-
Create a booklet-style PDF from a single input.
47-
48-
Pairs of two pages will be put on one page (left and right)
4946

50-
usage: python 2-up.py input_file output_file
47+
Usage: pdfly 2-up [OPTIONS] PDF OUT
5148

52-
Arguments:
53-
PDF [required]
54-
OUT [required]
49+
Create a booklet-style PDF from a single input.
50+
Pairs of two pages will be put on one page (left and right)
51+
usage: python 2-up.py input_file output_file
5552

56-
Options:
57-
--help Show this message and exit.
53+
╭─ Arguments ─────────────────────────────────────────────────────────────────╮
54+
│ * pdf PATH [default: None] [required] │
55+
│ * out PATH [default: None] [required] │
56+
╰─────────────────────────────────────────────────────────────────────────────╯
57+
╭─ Options ───────────────────────────────────────────────────────────────────╮
58+
│ --help Show this message and exit. │
59+
╰─────────────────────────────────────────────────────────────────────────────╯
5860
```

pdfly/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""pdfly is a command line utility for manipulating PDFs and getting information about them."""
12
from ._version import __version__
23

34
__all__ = [

pdfly/_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
4+
class OutputOptions(Enum):
5+
json = "json"
6+
text = "text"

pdfly/cat.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env python
21
"""
32
Concatenate pages from PDF files into a single PDF file.
43
@@ -23,8 +22,7 @@
2322
1:10:2 1 3 5 7 9 2::-1 2 1 0.
2423
::-1 all pages in reverse order.
2524
26-
EXAMPLES
27-
25+
Examples
2826
pdfcat -o output.pdf head.pdf content.pdf :6 7: tail.pdf -1
2927
3028
Concatenate all of head.pdf, all but page seven of content.pdf,
@@ -53,7 +51,9 @@
5351
from pypdf import PdfMerger, parse_filename_page_ranges
5452

5553

56-
def main(filename: Path, fn_pgrgs: List[str], output: Path, verbose: bool) -> None:
54+
def main(
55+
filename: Path, fn_pgrgs: List[str], output: Path, verbose: bool
56+
) -> None:
5757
fn_pgrgs_l = list(fn_pgrgs)
5858
fn_pgrgs_l.insert(0, str(filename))
5959
filename_page_ranges = parse_filename_page_ranges(fn_pgrgs_l) # type: ignore

pdfly/cli.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
"""
2+
Define how the CLI should behave.
3+
4+
Subcommands are added here.
5+
"""
6+
17
from pathlib import Path
28
from typing import List
39

@@ -7,25 +13,30 @@
713
import pdfly.compress
814
import pdfly.extract_images
915
import pdfly.metadata
16+
import pdfly.pagemeta
1017
import pdfly.up2
1118

1219

1320
def version_callback(value: bool) -> None:
1421
if value:
1522
typer.echo(f"pdfly {pdfly.__version__}")
16-
raise typer.Exit()
23+
raise typer.Exit
1724

1825

1926
entry_point = typer.Typer(
2027
add_completion=False,
21-
help=("pdfly is a pure-python cli application for manipulating PDF files."),
28+
help=(
29+
"pdfly is a pure-python cli application for manipulating PDF files."
30+
),
2231
)
2332

2433

2534
@entry_point.callback() # type: ignore[misc]
2635
def common(
2736
ctx: typer.Context,
28-
version: bool = typer.Option(None, "--version", callback=version_callback), # noqa
37+
version: bool = typer.Option( # noqa: B008
38+
None, "--version", callback=version_callback
39+
),
2940
) -> None:
3041
pass
3142

@@ -68,8 +79,27 @@ def metadata(
6879
pdfly.metadata.main(pdf, output)
6980

7081

82+
@entry_point.command(name="pagemeta") # type: ignore[misc]
83+
def pagemeta(
84+
pdf: Path,
85+
page_index: int,
86+
output: pdfly.metadata.OutputOptions = typer.Option( # noqa
87+
pdfly.metadata.OutputOptions.text.value,
88+
"--output",
89+
"-o",
90+
help="output format",
91+
show_default=True,
92+
),
93+
) -> None:
94+
pdfly.pagemeta.main(
95+
pdf,
96+
page_index,
97+
output,
98+
)
99+
100+
71101
@entry_point.command(name="extract-text") # type: ignore[misc]
72-
def extract_text(pdf: Path):
102+
def extract_text(pdf: Path) -> None:
73103
"""Extract text from a PDF file."""
74104
from pypdf import PdfReader
75105

@@ -79,12 +109,13 @@ def extract_text(pdf: Path):
79109

80110

81111
@entry_point.command(name="compress") # type: ignore[misc]
82-
def compress(pdf: Path, output: Path):
112+
def compress(pdf: Path, output: Path) -> None:
83113
pdfly.compress.main(pdf, output)
84114

85115

86116
up2.__doc__ = pdfly.up2.__doc__
87117
extract_images.__doc__ = pdfly.extract_images.__doc__
88118
cat.__doc__ = pdfly.cat.__doc__
89119
metadata.__doc__ = pdfly.metadata.__doc__
120+
pagemeta.__doc__ = pdfly.pagemeta.__doc__
90121
compress.__doc__ = pdfly.compress.__doc__

pdfly/compress.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pypdf import PdfReader, PdfWriter
77

88

9-
def main(pdf: Path, output: Path):
9+
def main(pdf: Path, output: Path) -> None:
1010
reader = PdfReader(pdf)
1111
writer = PdfWriter()
1212
for page in reader.pages:

pdfly/metadata.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22

33
import stat
44
from datetime import datetime
5-
from enum import Enum
65
from pathlib import Path
7-
from typing import Optional, Set, Tuple
6+
from typing import Optional, Set
87

98
from pydantic import BaseModel
109
from pypdf import PdfReader
1110

11+
from ._utils import OutputOptions
12+
1213

1314
class MetaInfo(BaseModel):
1415
title: Optional[str] = None
1516
producer: Optional[str] = None
1617
author: Optional[str] = None
1718
pages: int
1819
encrypted: bool
19-
page_size: Tuple[float, float] # (width, height)
2020
pdf_file_version: str
2121
page_mode: Optional[str]
2222
page_layout: Optional[str]
@@ -29,23 +29,16 @@ class MetaInfo(BaseModel):
2929
access_time: datetime
3030

3131

32-
class OutputOptions(Enum):
33-
json = "json"
34-
text = "text"
35-
36-
3732
def main(pdf: Path, output: OutputOptions) -> None:
3833
reader = PdfReader(str(pdf))
3934
info = reader.metadata
40-
x1, y1, x2, y2 = reader.pages[0].mediabox
4135

4236
reader.stream.seek(0)
4337
pdf_file_version = reader.stream.read(8).decode("utf-8")
4438
pdf_stat = pdf.stat()
4539
meta = MetaInfo(
4640
pages=len(reader.pages),
4741
encrypted=reader.is_encrypted,
48-
page_size=(x2 - x1, y2 - y1),
4942
page_mode=reader.page_mode,
5043
pdf_file_version=pdf_file_version,
5144
page_layout=reader.page_layout,
@@ -68,17 +61,16 @@ def main(pdf: Path, output: OutputOptions) -> None:
6861
from rich.table import Table
6962

7063
table = Table(title="PDF Data")
71-
table.add_column("Attribute", justify="right", style="cyan", no_wrap=True)
64+
table.add_column(
65+
"Attribute", justify="right", style="cyan", no_wrap=True
66+
)
7267
table.add_column("Value", style="white")
7368

7469
table.add_row("Title", meta.title)
7570
table.add_row("Producer", meta.producer)
7671
table.add_row("Author", meta.author)
7772
table.add_row("Pages", f"{meta.pages:,}")
7873
table.add_row("Encrypted", f"{meta.encrypted}")
79-
table.add_row(
80-
"Page size", f"{meta.page_size[0]} x {meta.page_size[1]} pts (w x h)"
81-
)
8274
table.add_row("PDF File Version", meta.pdf_file_version)
8375
table.add_row("Page Layout", meta.page_layout)
8476
table.add_row("Page Mode", meta.page_mode)
@@ -92,17 +84,26 @@ def main(pdf: Path, output: OutputOptions) -> None:
9284
table.add_row("Fonts (embedded)", ", ".join(sorted(embedded_fonts)))
9385

9486
os_table = Table(title="Operating System Data")
95-
os_table.add_column("Attribute", justify="right", style="cyan", no_wrap=True)
87+
os_table.add_column(
88+
"Attribute", justify="right", style="cyan", no_wrap=True
89+
)
9690
os_table.add_column("Value", style="white")
9791
os_table.add_row("File Name", f"{pdf}")
9892
os_table.add_row("File Permissions", f"{meta.file_permissions}")
9993
os_table.add_row("File Size", f"{meta.file_size:,} bytes")
100-
os_table.add_row("Creation Time", f"{meta.creation_time:%Y-%m-%d %H:%M:%S}")
94+
os_table.add_row(
95+
"Creation Time", f"{meta.creation_time:%Y-%m-%d %H:%M:%S}"
96+
)
10197
os_table.add_row(
10298
"Modification Time", f"{meta.modification_time:%Y-%m-%d %H:%M:%S}"
10399
)
104-
os_table.add_row("Access Time", f"{meta.access_time:%Y-%m-%d %H:%M:%S}")
100+
os_table.add_row(
101+
"Access Time", f"{meta.access_time:%Y-%m-%d %H:%M:%S}"
102+
)
105103

106104
console = Console()
107105
console.print(os_table)
108106
console.print(table)
107+
console.print(
108+
"Use the 'pagemeta' subcommand to get details about a single page"
109+
)

pdfly/pagemeta.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Give details about a single page."""
2+
3+
from pathlib import Path
4+
from typing import Tuple
5+
6+
from pydantic import BaseModel
7+
from pypdf import PdfReader
8+
9+
from ._utils import OutputOptions
10+
11+
12+
class PageMeta(BaseModel):
13+
mediabox: Tuple[int, int, int, int]
14+
cropbox: Tuple[int, int, int, int]
15+
artbox: Tuple[int, int, int, int]
16+
bleedbox: Tuple[int, int, int, int]
17+
annotations: int
18+
19+
20+
def main(pdf: Path, page_index: int, output: OutputOptions) -> None:
21+
reader = PdfReader(pdf)
22+
page = reader.pages[page_index]
23+
meta = PageMeta(
24+
mediabox=page.mediabox,
25+
cropbox=page.cropbox,
26+
artbox=page.artbox,
27+
bleedbox=page.bleedbox,
28+
annotations=len(page.annotations) if page.annotations else 0,
29+
)
30+
31+
if output == OutputOptions.json:
32+
print(meta.json())
33+
else:
34+
from rich.console import Console
35+
from rich.markdown import Markdown
36+
from rich.table import Table
37+
38+
console = Console()
39+
40+
table = Table(title=f"{pdf}, page index {page_index}")
41+
table.add_column(
42+
"Attribute", justify="right", style="cyan", no_wrap=True
43+
)
44+
table.add_column("Value", style="white")
45+
46+
table.add_row(
47+
"mediabox",
48+
f"{meta.mediabox}: "
49+
f"with={meta.mediabox[2] - meta.mediabox[0]} "
50+
f"x height={meta.mediabox[3] - meta.mediabox[1]}",
51+
)
52+
table.add_row(
53+
"cropbox",
54+
f"{meta.cropbox}: "
55+
f"with={meta.cropbox[2] - meta.cropbox[0]} "
56+
f"x height={meta.cropbox[3] - meta.cropbox[1]}",
57+
)
58+
table.add_row(
59+
"artbox",
60+
f"{meta.artbox}: "
61+
f"with={meta.artbox[2] - meta.artbox[0]} "
62+
f"x height={meta.artbox[3] - meta.artbox[1]}",
63+
)
64+
table.add_row(
65+
"bleedbox",
66+
f"{meta.bleedbox}: "
67+
f"with={meta.bleedbox[2] - meta.bleedbox[0]} "
68+
f"x height={meta.bleedbox[3] - meta.bleedbox[1]}",
69+
)
70+
71+
table.add_row("annotations", str(meta.annotations))
72+
73+
console.print(table)
74+
75+
if page.annotations:
76+
console.print(Markdown("**All annotations:**"))
77+
for i, annot in enumerate(page.annotations, start=1):
78+
obj = annot.get_object()
79+
console.print(f"{i}. {obj['/Subtype']} at {obj['/Rect']}")

0 commit comments

Comments
 (0)