pdf-helper 0.1.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.
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pdf-helper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple python script that helps with doing simple stuff with PDFs.
|
|
5
|
+
Author: CodeWriter21(Mehrad Pooryoussof)
|
|
6
|
+
Author-email: CodeWriter21(Mehrad Pooryoussof) <CodeWriter21@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: log21>=3.0.0
|
|
9
|
+
Requires-Dist: pypdfium2>=4.30.0
|
|
10
|
+
Requires-Dist: pillow>=11.0.0
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
PDF-Helper
|
|
15
|
+
==========
|
|
16
|
+
|
|
17
|
+
A simple python package that helps with doing simple stuff with PDFs.
|
|
18
|
+
|
|
19
|
+
Features
|
|
20
|
+
--------
|
|
21
|
+
|
|
22
|
+
+ [x] **Merge PDFs**: Merge multiple PDFs into one PDF
|
|
23
|
+
+ [x] **Split PDFs**: Split a PDF into multiple PDFs, each containing a range of pages from
|
|
24
|
+
the original PDF
|
|
25
|
+
+ [x] **Export as image**: Export designated pages from a PDF as image files
|
|
26
|
+
+ [x] **Remove pages**: Remove designated pages from a PDF
|
|
27
|
+
+ [ ] Encrypt a PDF
|
|
28
|
+
+ [ ] Decrypt a PDF
|
|
29
|
+
+ [ ] Add watermark to a PDF
|
|
30
|
+
+ [ ] Extract images from a PDF
|
|
31
|
+
+ [x] **Extract text**: Export text from a PDF file and optionally save it to a text file
|
|
32
|
+
+ [ ] Extract links from a PDF
|
|
33
|
+
+ [x] **Image to PDF**: Export one or multiple images as a PDF file
|
|
34
|
+
|
|
35
|
+
If you want any other feature to be added, feel free to open an [issue](https://github.com/MPCodeWriter21/PDF-To-Image/issues)
|
|
36
|
+
or fork the repo and make a [pull request](https://github.com/MPCodeWriter21/PDF-To-Image/pulls)
|
|
37
|
+
after adding your contribution.
|
|
38
|
+
|
|
39
|
+
Usage
|
|
40
|
+
-----
|
|
41
|
+
|
|
42
|
+
### Installation
|
|
43
|
+
|
|
44
|
+
You can install PDF-Helper via pip:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install pdf-helper
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
And run it using the command line:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pdf-helper <command> [options]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or you can use uvx to run the package without installing it in a specific python environment:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uvx pdf-helper <command> [options]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You can also clone the repository and use `uv run`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/MPCodeWriter21/PDF-Helper.git
|
|
66
|
+
cd PDF-Helper
|
|
67
|
+
uv run pdf-helper <command> [options]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Merge PDFs
|
|
71
|
+
|
|
72
|
+
Merge multiple PDFs into one PDF:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pdf-helper merge -i <input_file_1> <input_file_2>... <input_file_n> -o <output_file>
|
|
76
|
+
|
|
77
|
+
# E.g. Merge PDFs 1, 2 and 3 into a new PDF
|
|
78
|
+
pdf-helper merge -i 1.pdf 2.pdf 3.pdf -o new.pdf
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Split PDFs
|
|
82
|
+
|
|
83
|
+
Split a PDF into multiple PDFs, each containing a range of pages:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pdf-helper split -i <input_file> -o <output_folder> -s <split_point_1>,<split_point_2>
|
|
87
|
+
|
|
88
|
+
# E.g. Split a PDF into three PDFs, one with pages 1-10, the second with pages 11-20 and
|
|
89
|
+
# the third with pages 21-end
|
|
90
|
+
pdf-helper split -i my-pdf.pdf -o my-split-pdfs -s 10,20
|
|
91
|
+
|
|
92
|
+
# E.g. Split a PDF into PDFs each containing one page
|
|
93
|
+
pdf-helper split -i my-pdf.pdf -o my-split-pdfs # No need to specify split points
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Export PDF pages as image files
|
|
97
|
+
|
|
98
|
+
Export PDF pages as image files:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pdf-helper to-image -i <input_file> -o <output_folder> \
|
|
102
|
+
-p <page_number_1>,<page_number_2>,...,<page_number_n> -s <scale_factor>
|
|
103
|
+
|
|
104
|
+
# E.g. Export pages 1, 2, 3 and 6 from a PDF with scale factor 1
|
|
105
|
+
pdf-helper to-image -i 1.pdf -o images -p 1-3,6 -s 1
|
|
106
|
+
|
|
107
|
+
# E.g. Export all pages from a PDF with scale 2
|
|
108
|
+
pdf-helper to-image -i my-pdf.pdf -o my-images
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Remove pages from a PDF
|
|
112
|
+
|
|
113
|
+
Remove pages from a PDF:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pdf-helper remove-pages -i <input_file> -o <output_file> -p <page_number_1>,<page_number_2>,...,<page_number_n>
|
|
117
|
+
|
|
118
|
+
# E.g. Remove pages 1, 2, 3 and 6 from a PDF
|
|
119
|
+
pdf-helper remove-pages -i 1.pdf -o new.pdf -p 1-3,6
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Export text from a PDF
|
|
123
|
+
|
|
124
|
+
To extract text from a PDF file and export them to text files you can do as follows:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pdf-helper extract-text -i <input_file> -o <output_file_name>
|
|
128
|
+
|
|
129
|
+
# E.g. Extract text from a PDF named my-pdf.pdf and save it to my-text.txt
|
|
130
|
+
pdf-helper extract-text -i my-pdf.pdf -o my-text.txt
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Export one or multiple images as a PDF file
|
|
134
|
+
|
|
135
|
+
You simply provide the script with your images, and it will create a PDF file with them:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pdf-helper image-to-pdf -i <image_1> <image_2> <image_3> ... -o <output_file>
|
|
139
|
+
|
|
140
|
+
# E.g. Take 1.png, 2.jpg, and 3.png and create a PDF named 123.pdf and override
|
|
141
|
+
# if already exists
|
|
142
|
+
pdf-helper image-to-pdf -i 1.png 2.jpg 3.png -o 123.pdf -f
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
About
|
|
146
|
+
-----
|
|
147
|
+
|
|
148
|
+
Author: [CodeWriter21](https://github.com/MPCodeWriter21)
|
|
149
|
+
|
|
150
|
+
GitHub: [MPCodeWriter21/PDF-Helper](https://github.com/MPCodeWriter21/PDF-Helper)
|
|
151
|
+
|
|
152
|
+
Donations
|
|
153
|
+
---------
|
|
154
|
+
|
|
155
|
+
Your donations are very welcome: [nowpayments.io](https://nowpayments.io/donation/MehradP21)
|
|
156
|
+
|
|
157
|
+
You can also consider donating a
|
|
158
|
+
[Star](https://github.com/MPCodeWriter21/PDF-Helper) to the repo.
|
|
159
|
+
|
|
160
|
+
License
|
|
161
|
+
-------
|
|
162
|
+
|
|
163
|
+
This project is licensed under the MIT License.
|
|
164
|
+
|
|
165
|
+
See the [LICENSE](LICENSE)
|
|
166
|
+
|
|
167
|
+
References
|
|
168
|
+
----------
|
|
169
|
+
|
|
170
|
+
+ [pypdfium2](https://pypdfium2.readthedocs.io/en/stable/readme.html)
|
|
171
|
+
+ [PILlow](https://pillow.readthedocs.io/en/stable/)
|
|
172
|
+
+ [log21](https://GitHub.com/MPCodeWriter21/log21)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
PDF-Helper
|
|
2
|
+
==========
|
|
3
|
+
|
|
4
|
+
A simple python package that helps with doing simple stuff with PDFs.
|
|
5
|
+
|
|
6
|
+
Features
|
|
7
|
+
--------
|
|
8
|
+
|
|
9
|
+
+ [x] **Merge PDFs**: Merge multiple PDFs into one PDF
|
|
10
|
+
+ [x] **Split PDFs**: Split a PDF into multiple PDFs, each containing a range of pages from
|
|
11
|
+
the original PDF
|
|
12
|
+
+ [x] **Export as image**: Export designated pages from a PDF as image files
|
|
13
|
+
+ [x] **Remove pages**: Remove designated pages from a PDF
|
|
14
|
+
+ [ ] Encrypt a PDF
|
|
15
|
+
+ [ ] Decrypt a PDF
|
|
16
|
+
+ [ ] Add watermark to a PDF
|
|
17
|
+
+ [ ] Extract images from a PDF
|
|
18
|
+
+ [x] **Extract text**: Export text from a PDF file and optionally save it to a text file
|
|
19
|
+
+ [ ] Extract links from a PDF
|
|
20
|
+
+ [x] **Image to PDF**: Export one or multiple images as a PDF file
|
|
21
|
+
|
|
22
|
+
If you want any other feature to be added, feel free to open an [issue](https://github.com/MPCodeWriter21/PDF-To-Image/issues)
|
|
23
|
+
or fork the repo and make a [pull request](https://github.com/MPCodeWriter21/PDF-To-Image/pulls)
|
|
24
|
+
after adding your contribution.
|
|
25
|
+
|
|
26
|
+
Usage
|
|
27
|
+
-----
|
|
28
|
+
|
|
29
|
+
### Installation
|
|
30
|
+
|
|
31
|
+
You can install PDF-Helper via pip:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install pdf-helper
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
And run it using the command line:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pdf-helper <command> [options]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or you can use uvx to run the package without installing it in a specific python environment:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uvx pdf-helper <command> [options]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You can also clone the repository and use `uv run`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/MPCodeWriter21/PDF-Helper.git
|
|
53
|
+
cd PDF-Helper
|
|
54
|
+
uv run pdf-helper <command> [options]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Merge PDFs
|
|
58
|
+
|
|
59
|
+
Merge multiple PDFs into one PDF:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pdf-helper merge -i <input_file_1> <input_file_2>... <input_file_n> -o <output_file>
|
|
63
|
+
|
|
64
|
+
# E.g. Merge PDFs 1, 2 and 3 into a new PDF
|
|
65
|
+
pdf-helper merge -i 1.pdf 2.pdf 3.pdf -o new.pdf
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Split PDFs
|
|
69
|
+
|
|
70
|
+
Split a PDF into multiple PDFs, each containing a range of pages:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pdf-helper split -i <input_file> -o <output_folder> -s <split_point_1>,<split_point_2>
|
|
74
|
+
|
|
75
|
+
# E.g. Split a PDF into three PDFs, one with pages 1-10, the second with pages 11-20 and
|
|
76
|
+
# the third with pages 21-end
|
|
77
|
+
pdf-helper split -i my-pdf.pdf -o my-split-pdfs -s 10,20
|
|
78
|
+
|
|
79
|
+
# E.g. Split a PDF into PDFs each containing one page
|
|
80
|
+
pdf-helper split -i my-pdf.pdf -o my-split-pdfs # No need to specify split points
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Export PDF pages as image files
|
|
84
|
+
|
|
85
|
+
Export PDF pages as image files:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pdf-helper to-image -i <input_file> -o <output_folder> \
|
|
89
|
+
-p <page_number_1>,<page_number_2>,...,<page_number_n> -s <scale_factor>
|
|
90
|
+
|
|
91
|
+
# E.g. Export pages 1, 2, 3 and 6 from a PDF with scale factor 1
|
|
92
|
+
pdf-helper to-image -i 1.pdf -o images -p 1-3,6 -s 1
|
|
93
|
+
|
|
94
|
+
# E.g. Export all pages from a PDF with scale 2
|
|
95
|
+
pdf-helper to-image -i my-pdf.pdf -o my-images
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Remove pages from a PDF
|
|
99
|
+
|
|
100
|
+
Remove pages from a PDF:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pdf-helper remove-pages -i <input_file> -o <output_file> -p <page_number_1>,<page_number_2>,...,<page_number_n>
|
|
104
|
+
|
|
105
|
+
# E.g. Remove pages 1, 2, 3 and 6 from a PDF
|
|
106
|
+
pdf-helper remove-pages -i 1.pdf -o new.pdf -p 1-3,6
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Export text from a PDF
|
|
110
|
+
|
|
111
|
+
To extract text from a PDF file and export them to text files you can do as follows:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pdf-helper extract-text -i <input_file> -o <output_file_name>
|
|
115
|
+
|
|
116
|
+
# E.g. Extract text from a PDF named my-pdf.pdf and save it to my-text.txt
|
|
117
|
+
pdf-helper extract-text -i my-pdf.pdf -o my-text.txt
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Export one or multiple images as a PDF file
|
|
121
|
+
|
|
122
|
+
You simply provide the script with your images, and it will create a PDF file with them:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pdf-helper image-to-pdf -i <image_1> <image_2> <image_3> ... -o <output_file>
|
|
126
|
+
|
|
127
|
+
# E.g. Take 1.png, 2.jpg, and 3.png and create a PDF named 123.pdf and override
|
|
128
|
+
# if already exists
|
|
129
|
+
pdf-helper image-to-pdf -i 1.png 2.jpg 3.png -o 123.pdf -f
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
About
|
|
133
|
+
-----
|
|
134
|
+
|
|
135
|
+
Author: [CodeWriter21](https://github.com/MPCodeWriter21)
|
|
136
|
+
|
|
137
|
+
GitHub: [MPCodeWriter21/PDF-Helper](https://github.com/MPCodeWriter21/PDF-Helper)
|
|
138
|
+
|
|
139
|
+
Donations
|
|
140
|
+
---------
|
|
141
|
+
|
|
142
|
+
Your donations are very welcome: [nowpayments.io](https://nowpayments.io/donation/MehradP21)
|
|
143
|
+
|
|
144
|
+
You can also consider donating a
|
|
145
|
+
[Star](https://github.com/MPCodeWriter21/PDF-Helper) to the repo.
|
|
146
|
+
|
|
147
|
+
License
|
|
148
|
+
-------
|
|
149
|
+
|
|
150
|
+
This project is licensed under the MIT License.
|
|
151
|
+
|
|
152
|
+
See the [LICENSE](LICENSE)
|
|
153
|
+
|
|
154
|
+
References
|
|
155
|
+
----------
|
|
156
|
+
|
|
157
|
+
+ [pypdfium2](https://pypdfium2.readthedocs.io/en/stable/readme.html)
|
|
158
|
+
+ [PILlow](https://pillow.readthedocs.io/en/stable/)
|
|
159
|
+
+ [log21](https://GitHub.com/MPCodeWriter21/log21)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "PDF-Helper"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{name = "CodeWriter21(Mehrad Pooryoussof)", email = "CodeWriter21@gmail.com"}
|
|
6
|
+
]
|
|
7
|
+
description = "A simple python script that helps with doing simple stuff with PDFs."
|
|
8
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
9
|
+
license = {text = "MIT", file = "LICENSE"}
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"log21>=3.0.0",
|
|
13
|
+
"pypdfium2>=4.30.0",
|
|
14
|
+
"Pillow>=11.0.0"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
pdf-helper = "pdf_helper:__main__.main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.8.22,<0.9.0"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[tool.yapf]
|
|
25
|
+
column_limit = 88
|
|
26
|
+
split_before_dot = true
|
|
27
|
+
split_before_first_argument = true
|
|
28
|
+
dedent_closing_brackets = true
|
|
29
|
+
|
|
30
|
+
[tool.isort]
|
|
31
|
+
line_length = 88
|
|
32
|
+
combine_as_imports = true
|
|
33
|
+
length_sort = true
|
|
34
|
+
order_by_type = true
|
|
35
|
+
|
|
36
|
+
[tool.docformatter]
|
|
37
|
+
recursive = true
|
|
38
|
+
wrap-summaries = 88
|
|
39
|
+
wrap-descriptions = 88
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
show-fixes = true
|
|
43
|
+
exclude = ["migrations"]
|
|
44
|
+
target-version = "py39"
|
|
45
|
+
line-length = 88
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
extend-select = ["C4", "SIM", "TCH", "ANN", "N", "B"]
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# PDF-Helper
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional, Sequence, Collection
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import log21
|
|
10
|
+
import pypdfium2 as pdfium
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from pypdfium2 import PdfImage, PdfBitmap, PdfDocument
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'merge_pdfs', 'remove_pages', 'pdf_to_image', 'extract_text', 'image_to_pdf',
|
|
16
|
+
'split_pdf', 'watermark_pdf'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def merge_pdfs(
|
|
21
|
+
input_files: Sequence[str | Path | io.TextIOWrapper],
|
|
22
|
+
output_stream: str | Path | io.BytesIO | io.BufferedWriter
|
|
23
|
+
) -> int:
|
|
24
|
+
"""Merge PDF files.
|
|
25
|
+
|
|
26
|
+
:param input_files: List of PDF files to concatenate.
|
|
27
|
+
:param output_stream: Output stream to write to.
|
|
28
|
+
:return: Number of pages of the merged PDF.
|
|
29
|
+
"""
|
|
30
|
+
writer = PdfDocument.new()
|
|
31
|
+
for input_file in input_files:
|
|
32
|
+
log21.info(f'Adding {input_file}...')
|
|
33
|
+
reader = PdfDocument(input_file)
|
|
34
|
+
writer.import_pages(reader)
|
|
35
|
+
writer.save(output_stream)
|
|
36
|
+
return len(writer)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def remove_pages(
|
|
40
|
+
input_file: str | Path | io.BytesIO | io.TextIOWrapper,
|
|
41
|
+
pages_to_remove: Collection[int],
|
|
42
|
+
output_stream: str | Path | io.BytesIO | io.BufferedWriter
|
|
43
|
+
) -> int:
|
|
44
|
+
"""Remove pages from a PDF file.
|
|
45
|
+
|
|
46
|
+
:param input_file: PDF file to remove pages from.
|
|
47
|
+
:param pages_to_remove: List of pages to remove. A one based collection of indices.
|
|
48
|
+
:param output_stream: Output stream to write to.
|
|
49
|
+
:return: Number of pages removed.
|
|
50
|
+
"""
|
|
51
|
+
writer = PdfDocument.new()
|
|
52
|
+
reader = PdfDocument(input_file)
|
|
53
|
+
pages_to_remove = tuple((i - 1 for i in pages_to_remove))
|
|
54
|
+
pages_to_add = [i for i in range(len(reader)) if i not in pages_to_remove]
|
|
55
|
+
writer.import_pages(reader, pages_to_add)
|
|
56
|
+
writer.save(output_stream, version=reader.get_version())
|
|
57
|
+
return len(reader) - len(pages_to_add)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pdf_to_image(
|
|
61
|
+
input_file: str | Path,
|
|
62
|
+
output_directory: str | Path,
|
|
63
|
+
pages_to_convert: Optional[Collection[int]] = None,
|
|
64
|
+
scale: int = 2
|
|
65
|
+
) -> int:
|
|
66
|
+
"""Convert a PDF file to a series of images.
|
|
67
|
+
|
|
68
|
+
:param input_file: PDF file to convert.
|
|
69
|
+
:param output_directory: Directory to write images to.
|
|
70
|
+
:param scale: Scale of each image.
|
|
71
|
+
:return: Number of pages converted to image
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(input_file, str):
|
|
74
|
+
input_file = Path(input_file)
|
|
75
|
+
if isinstance(output_directory, str):
|
|
76
|
+
output_directory = Path(output_directory)
|
|
77
|
+
if not output_directory.exists():
|
|
78
|
+
output_directory.mkdir(parents=True)
|
|
79
|
+
|
|
80
|
+
pdf = pdfium.PdfDocument(input_file)
|
|
81
|
+
name = input_file.name.rsplit('.', maxsplit=1)[0]
|
|
82
|
+
# Number of digits each number in the filename should have
|
|
83
|
+
length = len(str(len(pdf)))
|
|
84
|
+
if not pages_to_convert:
|
|
85
|
+
for i, page in enumerate(pdf, start=1):
|
|
86
|
+
log21.info(f'Converting page {i}...', end='\r')
|
|
87
|
+
image = page.render(scale=scale).to_pil()
|
|
88
|
+
image.save(output_directory / f'{name}-{i:0>{length}}.png')
|
|
89
|
+
return len(pdf)
|
|
90
|
+
for i, page in enumerate(pdf, start=1):
|
|
91
|
+
if i not in pages_to_convert:
|
|
92
|
+
continue
|
|
93
|
+
log21.info(f'Converting page {i}...', end='\r')
|
|
94
|
+
image = page.render(scale=scale).to_pil()
|
|
95
|
+
image.save(output_directory / f'{name}-{i:0>{length}}.png')
|
|
96
|
+
return len(pages_to_convert)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract_text(
|
|
100
|
+
input_file: str | Path | io.BytesIO | io.TextIOWrapper,
|
|
101
|
+
pages_to_extract_from: Optional[Collection[int]] = None,
|
|
102
|
+
max_number_of_characters: int = -1,
|
|
103
|
+
reverse_lines: bool = False
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Extract text from a PDF file.
|
|
106
|
+
|
|
107
|
+
:param input_file: PDF file to extract text from.
|
|
108
|
+
:param pages_to_extract_from: Pages to extract text from.
|
|
109
|
+
:param max_number_of_characters: Maximum number of characters to extract in total.
|
|
110
|
+
:param reverse_lines: Reverse the characters in each line (Useful for Persian text)
|
|
111
|
+
:return: Extracted text.
|
|
112
|
+
"""
|
|
113
|
+
pdf = pdfium.PdfDocument(input_file)
|
|
114
|
+
text = ''
|
|
115
|
+
if pages_to_extract_from:
|
|
116
|
+
pages_to_extract_from = sorted(pages_to_extract_from)
|
|
117
|
+
if pages_to_extract_from[0] < 1:
|
|
118
|
+
log21.critical('Pages must be >= 1')
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
if pages_to_extract_from[-1] > len(pdf):
|
|
121
|
+
log21.critical(
|
|
122
|
+
f'Page {pages_to_extract_from[-1]} does not exist in `{input_file}`'
|
|
123
|
+
)
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
log21.info(
|
|
126
|
+
f'Extracting text from {len(pages_to_extract_from)} page' +
|
|
127
|
+
('s' if len(pages_to_extract_from) > 1 else '') + f' from `{input_file}`...'
|
|
128
|
+
)
|
|
129
|
+
count = max_number_of_characters
|
|
130
|
+
for i, page in enumerate(pdf):
|
|
131
|
+
i = i + 1
|
|
132
|
+
if i not in pages_to_extract_from:
|
|
133
|
+
continue
|
|
134
|
+
log21.info(f'Extracting text from page {i}...', end='\r')
|
|
135
|
+
text += page.get_textpage().get_text_range(count=count)
|
|
136
|
+
if count == 0:
|
|
137
|
+
break
|
|
138
|
+
if count > 0:
|
|
139
|
+
count = max_number_of_characters - len(text)
|
|
140
|
+
else:
|
|
141
|
+
count = max_number_of_characters
|
|
142
|
+
for page in pdf:
|
|
143
|
+
text += page.get_textpage().get_text_range(count=count)
|
|
144
|
+
if count == 0:
|
|
145
|
+
break
|
|
146
|
+
if count > 0:
|
|
147
|
+
count = max_number_of_characters - len(text)
|
|
148
|
+
text += '\n'
|
|
149
|
+
log21.info('\rDone!')
|
|
150
|
+
|
|
151
|
+
if reverse_lines:
|
|
152
|
+
reversed_text = ''
|
|
153
|
+
log21.info('Reversing the lines...', end='\r')
|
|
154
|
+
for i, line in enumerate(text.split(os.linesep), start=1):
|
|
155
|
+
log21.info('Reversing line %d...', args=(i, ), end='\r')
|
|
156
|
+
reversed_text += ''.join(reversed(line)) + os.linesep
|
|
157
|
+
log21.info('\rReversed every line!')
|
|
158
|
+
text = reversed_text
|
|
159
|
+
|
|
160
|
+
return text
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def image_to_pdf(
|
|
164
|
+
input_files: Sequence[str | bytes | Path | os.PathLike[str] | io.BytesIO],
|
|
165
|
+
output_stream: str | Path | io.BytesIO | io.BufferedWriter
|
|
166
|
+
) -> int:
|
|
167
|
+
"""Convert images to a PDF file.
|
|
168
|
+
|
|
169
|
+
:param input_files: List of images to convert.
|
|
170
|
+
:param output_stream: Output stream to write to.
|
|
171
|
+
:return: Number of pages in the output PDF
|
|
172
|
+
"""
|
|
173
|
+
writer = PdfDocument.new()
|
|
174
|
+
for input_file in input_files:
|
|
175
|
+
log21.info(f'Adding {input_file}...')
|
|
176
|
+
# Open the image file
|
|
177
|
+
image = Image.open(input_file)
|
|
178
|
+
# Create a bitmap from the image
|
|
179
|
+
bitmap = PdfBitmap.from_pil(image)
|
|
180
|
+
# Create a PdfImage object from the bitmap
|
|
181
|
+
pdf_image = PdfImage.new(writer)
|
|
182
|
+
pdf_image.set_bitmap(bitmap)
|
|
183
|
+
matrix = pdfium.PdfMatrix().scale(bitmap.width, bitmap.height)
|
|
184
|
+
pdf_image.set_matrix(matrix)
|
|
185
|
+
# Create a new page and insert the PdfImage object
|
|
186
|
+
page = writer.new_page(bitmap.width, bitmap.height)
|
|
187
|
+
page.insert_obj(pdf_image)
|
|
188
|
+
page.gen_content()
|
|
189
|
+
# Close the objects
|
|
190
|
+
page.close()
|
|
191
|
+
pdf_image.close()
|
|
192
|
+
bitmap.close()
|
|
193
|
+
image.close()
|
|
194
|
+
writer.save(output_stream)
|
|
195
|
+
return len(writer)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def split_pdf(
|
|
199
|
+
input_file: str | Path,
|
|
200
|
+
output_directory: str | Path,
|
|
201
|
+
split_points: Optional[Collection[int]] = None
|
|
202
|
+
) -> int:
|
|
203
|
+
"""Split a PDF file into multiple files.
|
|
204
|
+
|
|
205
|
+
:param input_file: PDF file to split.
|
|
206
|
+
:param output_directory: Directory to write split files to.
|
|
207
|
+
:param split_points: Pages to split. If None, splits every page into a separate
|
|
208
|
+
file.
|
|
209
|
+
:return: Number of pages split.
|
|
210
|
+
"""
|
|
211
|
+
if isinstance(input_file, str):
|
|
212
|
+
input_file = Path(input_file)
|
|
213
|
+
if isinstance(output_directory, str):
|
|
214
|
+
output_directory = Path(output_directory)
|
|
215
|
+
if not output_directory.exists():
|
|
216
|
+
output_directory.mkdir(parents=True)
|
|
217
|
+
|
|
218
|
+
pdf = pdfium.PdfDocument(input_file)
|
|
219
|
+
if not split_points:
|
|
220
|
+
split_points = range(1, len(pdf) - 1)
|
|
221
|
+
split_points = [0] + sorted(set(split_points)) + [len(pdf)]
|
|
222
|
+
|
|
223
|
+
for i in range(len(split_points) - 1):
|
|
224
|
+
start = split_points[i]
|
|
225
|
+
end = split_points[i + 1]
|
|
226
|
+
if start < 0 or end > len(pdf):
|
|
227
|
+
log21.warning(
|
|
228
|
+
f'Split points {start + 1} to {end} are out of bounds for '
|
|
229
|
+
f'input file `{input_file}`.'
|
|
230
|
+
)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
log21.info(f'Splitting pages {start + 1} to {end}...')
|
|
234
|
+
writer = PdfDocument.new()
|
|
235
|
+
writer.import_pages(pdf, range(start, end))
|
|
236
|
+
output_file = output_directory / f'{input_file.stem}_part_{i + 1}.pdf'
|
|
237
|
+
try:
|
|
238
|
+
writer.save(output_file)
|
|
239
|
+
log21.info(f'Saved split file to {output_file}')
|
|
240
|
+
except PermissionError:
|
|
241
|
+
log21.critical(
|
|
242
|
+
f'Cannot write to output file `{output_file}`.\n'
|
|
243
|
+
'Check the file permissions and close any applications that may be '
|
|
244
|
+
'using the file, then try again.'
|
|
245
|
+
)
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
finally:
|
|
248
|
+
writer.close()
|
|
249
|
+
|
|
250
|
+
return len(split_points) - 1
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def watermark_pdf(
|
|
254
|
+
input_file: str | Path | io.BytesIO | io.TextIOWrapper,
|
|
255
|
+
output_file: str | Path,
|
|
256
|
+
watermark_text: str,
|
|
257
|
+
position: str = 'center',
|
|
258
|
+
font_size: int = 36,
|
|
259
|
+
opacity: float = 0.1,
|
|
260
|
+
rotation: float = 45.0
|
|
261
|
+
) -> int:
|
|
262
|
+
"""Split a PDF file into multiple files.
|
|
263
|
+
|
|
264
|
+
:param input_file: PDF file to split.
|
|
265
|
+
:param output_file:
|
|
266
|
+
:param watermark_text: Text to use as watermark.
|
|
267
|
+
:param position: Position of watermark. One of 'center' or '50% 50%', 'top-left' or
|
|
268
|
+
'0% 0%', 'top-right' or '100% 0%', 'bottom-left' or '0% 100%', etc.
|
|
269
|
+
You can also use absolute values like '100 100'.
|
|
270
|
+
Note: The position is relative to the center of the watermark text.
|
|
271
|
+
:param font_size: Font size of watermark text.
|
|
272
|
+
:param opacity: Opacity of watermark text. Between 0.0 and 1.0.
|
|
273
|
+
:param rotation: Rotation of watermark text in degrees.
|
|
274
|
+
:return: Number of pages split.
|
|
275
|
+
"""
|
|
276
|
+
if isinstance(input_file, str):
|
|
277
|
+
input_file = Path(input_file)
|
|
278
|
+
if isinstance(output_file, str):
|
|
279
|
+
output_file = Path(output_file)
|
|
280
|
+
if len(watermark_text) == 0:
|
|
281
|
+
raise ValueError('Watermark text cannot be empty.')
|
|
282
|
+
if font_size < 1:
|
|
283
|
+
raise ValueError('Font size must be at least 1.')
|
|
284
|
+
if opacity < 0.0 or opacity > 1.0:
|
|
285
|
+
raise ValueError('Opacity must be between 0.0 and 1.0.')
|
|
286
|
+
|
|
287
|
+
pdf = pdfium.PdfDocument(input_file)
|
|
288
|
+
watermark = pdfium.PdfDocument.new()
|
|
289
|
+
_page = watermark.new_page(512, 512)
|
|
290
|
+
for i, _page in enumerate(pdf):
|
|
291
|
+
log21.info(f'Adding watermark to page {i + 1}...', end='\r')
|
|
292
|
+
log21.info('\rDone!')
|
|
293
|
+
pdf.save(output_file)
|
|
294
|
+
return len(pdf)
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# yapf: disable
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import importlib.util
|
|
8
|
+
from typing import Optional, Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import log21
|
|
12
|
+
from log21.colors import RED, GREEN, RESET
|
|
13
|
+
|
|
14
|
+
from . import (split_pdf, merge_pdfs, extract_text, image_to_pdf, pdf_to_image,
|
|
15
|
+
remove_pages, watermark_pdf)
|
|
16
|
+
from .utils import parse_pages
|
|
17
|
+
|
|
18
|
+
# yapf: ensable
|
|
19
|
+
|
|
20
|
+
def merge_pdfs_entry_point(
|
|
21
|
+
input_paths: Sequence[Path],
|
|
22
|
+
output_path: Path,
|
|
23
|
+
/,
|
|
24
|
+
force: bool = False,
|
|
25
|
+
verbose: bool = False
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Merge PDF files.
|
|
28
|
+
|
|
29
|
+
:param output_path: Path to write concatenated PDF file to.
|
|
30
|
+
:param input_paths: List of PDF files to concatenate.
|
|
31
|
+
:param force: Force overwrite of output file.
|
|
32
|
+
:param verbose: Print verbose output.
|
|
33
|
+
"""
|
|
34
|
+
if len(input_paths) < 2:
|
|
35
|
+
log21.critical('Must provide at least two input files.')
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
if output_path.exists() and not force:
|
|
38
|
+
log21.critical('Output file already exists.')
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
if output_path.absolute() in (path.absolute() for path in input_paths):
|
|
41
|
+
log21.critical('Input and output files cannot be the same.')
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
if verbose:
|
|
44
|
+
log21.basic_config(level=log21.INFO)
|
|
45
|
+
|
|
46
|
+
for path in input_paths:
|
|
47
|
+
if not path.exists():
|
|
48
|
+
log21.critical(f'Input file `{path}` does not exist.')
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
log21.info(f'Concatenating {len(input_paths)} files to {output_path}')
|
|
52
|
+
try:
|
|
53
|
+
with open(output_path, 'wb') as output_file:
|
|
54
|
+
merge_pdfs(input_paths, output_file)
|
|
55
|
+
except PermissionError:
|
|
56
|
+
log21.critical(
|
|
57
|
+
f'Cannot write to output file `{output_path}`.\n'
|
|
58
|
+
'Check the file permissions and close any applications that may be using '
|
|
59
|
+
'the file, then try again.'
|
|
60
|
+
)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def remove_pages_entry_point(
|
|
65
|
+
input_path: Path,
|
|
66
|
+
output_path: Path,
|
|
67
|
+
pages_to_remove: str,
|
|
68
|
+
/,
|
|
69
|
+
force: bool = False,
|
|
70
|
+
verbose: bool = False
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Remove pages from a PDF file.
|
|
73
|
+
|
|
74
|
+
:param input_path: Path to PDF file to remove pages from.
|
|
75
|
+
:param output_path: Path to write PDF file to.
|
|
76
|
+
:param pages_to_remove: Comma-separated list of pages to remove.
|
|
77
|
+
Example: '1-5,7,9-11'
|
|
78
|
+
:param force: Force overwrite of output file.
|
|
79
|
+
:param verbose: Print verbose output.
|
|
80
|
+
"""
|
|
81
|
+
if not input_path.exists():
|
|
82
|
+
log21.critical(f'Input file `{input_path}` does not exist.')
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
if output_path.exists() and not force:
|
|
85
|
+
log21.critical('Output file already exists.')
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
if input_path.absolute() == output_path.absolute():
|
|
88
|
+
log21.critical('Input and output files cannot be the same.')
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
if verbose:
|
|
91
|
+
log21.basic_config(level=log21.INFO)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
pages_to_remove_ = parse_pages(pages_to_remove)
|
|
95
|
+
except ValueError:
|
|
96
|
+
log21.critical(f'Invalid pages string: `{pages_to_remove}`')
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
log21.info(
|
|
100
|
+
f'Removing {len(pages_to_remove_)} page' + (
|
|
101
|
+
's: ' + ', '.join(str(page) for page in pages_to_remove_[:-1]) +
|
|
102
|
+
' and' if len(pages_to_remove_) > 2 else ':'
|
|
103
|
+
) + f' {pages_to_remove_[-1]} from `{input_path}`'
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
with open(output_path, 'wb') as output_file:
|
|
107
|
+
number_of_removed_pages = remove_pages(
|
|
108
|
+
input_path, pages_to_remove_, output_file
|
|
109
|
+
)
|
|
110
|
+
log21.info(
|
|
111
|
+
f'Removed {number_of_removed_pages} page' +
|
|
112
|
+
('s' if number_of_removed_pages > 1 else '') + '!'
|
|
113
|
+
)
|
|
114
|
+
except PermissionError:
|
|
115
|
+
log21.critical(
|
|
116
|
+
f'Cannot write to output file `{output_path}`.\n'
|
|
117
|
+
'Check the file permissions and close any applications that may be using '
|
|
118
|
+
'the file, then try again.'
|
|
119
|
+
)
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def pdf_to_image_entry_point(
|
|
124
|
+
input_path: Path,
|
|
125
|
+
output_directory: Path,
|
|
126
|
+
/,
|
|
127
|
+
pages_to_convert: Optional[str] = None,
|
|
128
|
+
scale: int = 2,
|
|
129
|
+
force: bool = False,
|
|
130
|
+
verbose: bool = False
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Convert a PDF file to a series of images.
|
|
133
|
+
|
|
134
|
+
:param input_path: Path to PDF file to convert.
|
|
135
|
+
:param output_directory: Path to directory to write images to.
|
|
136
|
+
:param pages_to_convert: Comma-separated list of pages to convert.
|
|
137
|
+
Example: '1-5,7,9-11'
|
|
138
|
+
:param scale: Scale of each image.
|
|
139
|
+
:param force: Force overwrite of output directory.
|
|
140
|
+
:param verbose: Print verbose output.
|
|
141
|
+
"""
|
|
142
|
+
if importlib.util.find_spec('PIL') is None:
|
|
143
|
+
log21.error('PIL must be installed to use this feature.')
|
|
144
|
+
cmd = 'python -m pip install "Pillow>=10.1.0"'
|
|
145
|
+
answer = log21.input(
|
|
146
|
+
f'[{RED}?{RESET}] Do you want to install it? ({GREEN}{cmd}{RESET})'
|
|
147
|
+
)
|
|
148
|
+
if answer.lower().startswith('y'):
|
|
149
|
+
log21.print(f'[{GREEN}+{RESET}] Installing Pillow...')
|
|
150
|
+
os.system(cmd)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
if not input_path.exists():
|
|
153
|
+
log21.critical(f'Input file `{input_path}` does not exist.')
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
if output_directory.exists() and not output_directory.is_dir():
|
|
156
|
+
log21.critical(f'Output path `{output_directory}` is not a directory.')
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
if output_directory.exists() and os.listdir(output_directory) and not force:
|
|
159
|
+
log21.critical(f'Output directory `{output_directory}` already exists.')
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
if verbose:
|
|
162
|
+
log21.basic_config(level=log21.INFO)
|
|
163
|
+
|
|
164
|
+
pages_to_convert_ = None
|
|
165
|
+
if pages_to_convert:
|
|
166
|
+
try:
|
|
167
|
+
pages_to_convert_ = parse_pages(pages_to_convert)
|
|
168
|
+
except ValueError:
|
|
169
|
+
log21.critical(f'Invalid pages string: `{pages_to_convert}`')
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
log21.info(
|
|
172
|
+
f'Converting {len(pages_to_convert_)} page' +
|
|
173
|
+
('s' if len(pages_to_convert_) > 2 else '') +
|
|
174
|
+
f' from `{input_path}` to image...'
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
log21.info(f'Converting `{input_path}` to images...')
|
|
178
|
+
|
|
179
|
+
pdf_to_image(input_path, output_directory, pages_to_convert_, scale)
|
|
180
|
+
log21.info('\rDone!')
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def extract_text_entry_point(
|
|
184
|
+
input_path: Path,
|
|
185
|
+
/,
|
|
186
|
+
output_path: Optional[Path] = None,
|
|
187
|
+
pages_to_extract_from: Optional[str] = None,
|
|
188
|
+
max_number_of_characters: int = -1,
|
|
189
|
+
characters_to_split: int = 0,
|
|
190
|
+
reverse_lines: bool = False,
|
|
191
|
+
force: bool = False,
|
|
192
|
+
verbose: bool = False
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Extract text from a PDF file.
|
|
195
|
+
|
|
196
|
+
:param input_path: Path to PDF file to extract text from.
|
|
197
|
+
:param output_path: Path to write extracted text to. (Writes to stdout if not
|
|
198
|
+
provided)
|
|
199
|
+
:param pages_to_extract_from: Pages to extract text from. Example: '1-5,7,9-11'
|
|
200
|
+
:param max_number_of_characters: Maximum number of characters to extract in total.
|
|
201
|
+
:param characters_to_split: Create a new file if the number of characters in the
|
|
202
|
+
extracted text exceeds this value.
|
|
203
|
+
:param reverse_lines: Reverse the characters in each line (Useful for Persian text)
|
|
204
|
+
:param force: Force overwrite of output file.
|
|
205
|
+
:param verbose: Print verbose output.
|
|
206
|
+
"""
|
|
207
|
+
if not input_path.exists():
|
|
208
|
+
log21.critical(f'Input file `{input_path}` does not exist.')
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
if output_path and output_path.exists() and not force:
|
|
211
|
+
log21.critical(f'Output `{output_path}` already exists.')
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
if verbose:
|
|
214
|
+
log21.basic_config(level=log21.INFO)
|
|
215
|
+
|
|
216
|
+
pages_to_extract_from_ = None
|
|
217
|
+
if pages_to_extract_from:
|
|
218
|
+
try:
|
|
219
|
+
pages_to_extract_from_ = parse_pages(pages_to_extract_from)
|
|
220
|
+
except ValueError:
|
|
221
|
+
log21.critical(f'Invalid pages string: `{pages_to_extract_from}`')
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
log21.info(
|
|
224
|
+
f'Extracting text from {len(pages_to_extract_from_)} page' +
|
|
225
|
+
('s' if len(pages_to_extract_from_) > 2 else '') +
|
|
226
|
+
f' from `{input_path}`...'
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
log21.info(f'Extracting text from `{input_path}`...')
|
|
230
|
+
|
|
231
|
+
text = extract_text(
|
|
232
|
+
input_path, pages_to_extract_from_, max_number_of_characters, reverse_lines
|
|
233
|
+
)
|
|
234
|
+
if output_path:
|
|
235
|
+
if not characters_to_split:
|
|
236
|
+
with output_path.open('w', encoding='utf-8') as file:
|
|
237
|
+
file.write(text)
|
|
238
|
+
return
|
|
239
|
+
i = 1
|
|
240
|
+
stem = output_path.stem
|
|
241
|
+
while len(text) > characters_to_split:
|
|
242
|
+
output_path_ = output_path.with_name(stem + f'_{i}{output_path.suffix}')
|
|
243
|
+
with output_path_.open('w', encoding='utf-8') as file:
|
|
244
|
+
file.write(text[:characters_to_split])
|
|
245
|
+
text = text[characters_to_split:]
|
|
246
|
+
output_path = output_path_
|
|
247
|
+
i += 1
|
|
248
|
+
with output_path.open('w', encoding='utf-8') as file:
|
|
249
|
+
file.write(text)
|
|
250
|
+
else:
|
|
251
|
+
print(text)
|
|
252
|
+
log21.info('\rDone!')
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def image_to_pdf_entry_point(
|
|
256
|
+
input_paths: Sequence[Path],
|
|
257
|
+
output_path: Path,
|
|
258
|
+
/,
|
|
259
|
+
force: bool = False,
|
|
260
|
+
verbose: bool = False
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Convert images to a PDF file.
|
|
263
|
+
|
|
264
|
+
:param input_paths: List of images to convert.
|
|
265
|
+
:param output_path: Path to write PDF file to.
|
|
266
|
+
:param force: Force overwrite of output file.
|
|
267
|
+
:param verbose: Print verbose output.
|
|
268
|
+
"""
|
|
269
|
+
if len(input_paths) < 1:
|
|
270
|
+
log21.critical('Must provide at least one input file.')
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
if output_path.exists() and not force:
|
|
273
|
+
log21.critical('Output file already exists.')
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
if verbose:
|
|
276
|
+
log21.basic_config(level=log21.INFO)
|
|
277
|
+
|
|
278
|
+
for path in input_paths:
|
|
279
|
+
if not path.exists():
|
|
280
|
+
log21.critical(f'Input file `{path}` does not exist.')
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
log21.info(f'Converting {len(input_paths)} images to `{output_path}`...')
|
|
284
|
+
try:
|
|
285
|
+
with open(output_path, 'wb') as output_file:
|
|
286
|
+
image_to_pdf(input_paths, output_file)
|
|
287
|
+
except PermissionError:
|
|
288
|
+
log21.critical(
|
|
289
|
+
f'Cannot write to output file `{output_path}`.\n'
|
|
290
|
+
'Check the file permissions and close any applications that may be using '
|
|
291
|
+
'the file, then try again.'
|
|
292
|
+
)
|
|
293
|
+
sys.exit(1)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def split_pdf_entry_point(
|
|
297
|
+
input_path: Path,
|
|
298
|
+
output_directory: Path,
|
|
299
|
+
/,
|
|
300
|
+
split_points: Optional[str] = None,
|
|
301
|
+
force: bool = False,
|
|
302
|
+
verbose: bool = False
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Split a PDF file into multiple files.
|
|
305
|
+
|
|
306
|
+
:param input_path: Path to PDF file to split.
|
|
307
|
+
:param output_directory: Path to directory to write split files to.
|
|
308
|
+
:param split_points: Comma-separated list of pages to split. Example: '5,7,9'
|
|
309
|
+
:param force: Force overwrite of output directory.
|
|
310
|
+
:param verbose: Print verbose output.
|
|
311
|
+
"""
|
|
312
|
+
if not input_path.exists():
|
|
313
|
+
log21.critical(f'Input file `{input_path}` does not exist.')
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
if output_directory.exists() and not output_directory.is_dir():
|
|
316
|
+
log21.critical(f'Output path `{output_directory}` is not a directory.')
|
|
317
|
+
sys.exit(1)
|
|
318
|
+
if output_directory.exists() and os.listdir(output_directory) and not force:
|
|
319
|
+
log21.critical(f'Output directory `{output_directory}` already exists.')
|
|
320
|
+
sys.exit(1)
|
|
321
|
+
if verbose:
|
|
322
|
+
log21.basic_config(level=log21.INFO)
|
|
323
|
+
|
|
324
|
+
split_points_ = None
|
|
325
|
+
if split_points:
|
|
326
|
+
try:
|
|
327
|
+
split_points_ = parse_pages(split_points)
|
|
328
|
+
except ValueError:
|
|
329
|
+
log21.critical(f'Invalid pages string: `{split_points}`')
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
|
|
332
|
+
log21.info(f'Splitting `{input_path}`...')
|
|
333
|
+
number_of_pdfs = split_pdf(input_path, output_directory, split_points_)
|
|
334
|
+
if number_of_pdfs > 1:
|
|
335
|
+
log21.info(f'Split {input_path} to {number_of_pdfs} PDF files!')
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def watermark_pdf_entry_point(
|
|
339
|
+
input_path: Path,
|
|
340
|
+
output_path: Path,
|
|
341
|
+
watermark_text: str,
|
|
342
|
+
/,
|
|
343
|
+
position: str = 'center',
|
|
344
|
+
font_size: int = 36,
|
|
345
|
+
opacity: float = 0.1,
|
|
346
|
+
rotation: float = 45.0,
|
|
347
|
+
force: bool = False,
|
|
348
|
+
verbose: bool = False
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Add a watermark to a PDF file.
|
|
351
|
+
|
|
352
|
+
:param input_path: Path to PDF file to add watermark to.
|
|
353
|
+
:param output_path: Path to write watermarked PDF file to.
|
|
354
|
+
:param watermark_text: Text to use as watermark.
|
|
355
|
+
:param position: Position of watermark. One of 'center' or '50% 50%', 'top-left' or
|
|
356
|
+
'0% 0%', 'top-right' or '100% 0%', 'bottom-left' or '0% 100%', etc.
|
|
357
|
+
You can also use absolute values like '100 100'.
|
|
358
|
+
Note: The position is relative to the center of the watermark text.
|
|
359
|
+
:param font_size: Font size of watermark text.
|
|
360
|
+
:param opacity: Opacity of watermark text. Between 0.0 and 1.0.
|
|
361
|
+
:param rotation: Rotation of watermark text in degrees.
|
|
362
|
+
:param force: Force overwrite of output file.
|
|
363
|
+
:param verbose: Print verbose output.
|
|
364
|
+
"""
|
|
365
|
+
if not input_path.exists():
|
|
366
|
+
log21.critical(f'Input file `{input_path}` does not exist.')
|
|
367
|
+
sys.exit(1)
|
|
368
|
+
if output_path.exists() and not force:
|
|
369
|
+
log21.critical('Output file already exists.')
|
|
370
|
+
sys.exit(1)
|
|
371
|
+
if input_path.absolute() == output_path.absolute():
|
|
372
|
+
log21.critical('Input and output files cannot be the same.')
|
|
373
|
+
sys.exit(1)
|
|
374
|
+
if verbose:
|
|
375
|
+
log21.basic_config(level=log21.INFO)
|
|
376
|
+
|
|
377
|
+
log21.info(f'Adding watermark to `{input_path}`...')
|
|
378
|
+
try:
|
|
379
|
+
number_of_pages = watermark_pdf(
|
|
380
|
+
input_path, output_path, watermark_text, position, font_size, opacity,
|
|
381
|
+
rotation
|
|
382
|
+
)
|
|
383
|
+
log21.info(
|
|
384
|
+
f'Added watermark to {number_of_pages} page' +
|
|
385
|
+
('s' if number_of_pages > 1 else '') + '!'
|
|
386
|
+
)
|
|
387
|
+
except PermissionError:
|
|
388
|
+
log21.critical(
|
|
389
|
+
f'Cannot write to output file `{output_path}`.\n'
|
|
390
|
+
'Check the file permissions and close any applications that may be using '
|
|
391
|
+
'the file, then try again.'
|
|
392
|
+
)
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
except ValueError as e:
|
|
395
|
+
log21.critical(str(e))
|
|
396
|
+
sys.exit(1)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def main() -> None:
|
|
400
|
+
try:
|
|
401
|
+
if sys.platform == 'win32':
|
|
402
|
+
import msvcrt
|
|
403
|
+
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
|
404
|
+
log21.basic_config(level=log21.ERROR)
|
|
405
|
+
log21.argumentify(
|
|
406
|
+
{
|
|
407
|
+
'merge': merge_pdfs_entry_point,
|
|
408
|
+
'remove-pages': remove_pages_entry_point,
|
|
409
|
+
'to-image': pdf_to_image_entry_point,
|
|
410
|
+
'add-watermark': watermark_pdf_entry_point,
|
|
411
|
+
'extract-text': extract_text_entry_point,
|
|
412
|
+
'image-to-pdf': image_to_pdf_entry_point,
|
|
413
|
+
'split': split_pdf_entry_point
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
except KeyboardInterrupt:
|
|
417
|
+
log21.critical('\nKeyboardInterrupt: Exiting...')
|
|
418
|
+
sys.exit(1)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if __name__ == '__main__':
|
|
422
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
POSITION_PATTERN = re.compile(
|
|
4
|
+
r'^(top|bottom|center|left|right|\d{1,3}%?)\s+(top|bottom|center|left|right|\d{1,3}%?)?$'
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_pages(pages: str) -> list[int]:
|
|
9
|
+
"""Parses the pages string into a list of pages.
|
|
10
|
+
|
|
11
|
+
:param pages: The pages string
|
|
12
|
+
Example: '1-5,7,9-11'
|
|
13
|
+
:return: A list of pages
|
|
14
|
+
Example: [1, 2, 3, 4, 5, 7, 9, 10, 11]
|
|
15
|
+
"""
|
|
16
|
+
pages_ = pages.replace(' ', '')
|
|
17
|
+
pages_ = pages_.split(',')
|
|
18
|
+
pages_ = [x.split('-') for x in pages_]
|
|
19
|
+
pages_ = [x if len(x) == 1 else range(int(x[0]), int(x[1]) + 1) for x in pages_]
|
|
20
|
+
pages_ = [int(x) for y in pages_ for x in y]
|
|
21
|
+
return pages_
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_position(
|
|
25
|
+
position: str, page_width: float, page_height: float, text_width: float,
|
|
26
|
+
text_height: float
|
|
27
|
+
) -> tuple[float, float]:
|
|
28
|
+
"""Parse position string into x and y coordinates.
|
|
29
|
+
|
|
30
|
+
:param position: Position string.
|
|
31
|
+
:param page_width: Width of the page.
|
|
32
|
+
:param page_height: Height of the page.
|
|
33
|
+
:param text_width: Width of the text.
|
|
34
|
+
:param text_height: Height of the text.
|
|
35
|
+
:return: x and y coordinates.
|
|
36
|
+
"""
|
|
37
|
+
position = position.lower().strip()
|
|
38
|
+
match = POSITION_PATTERN.match(position)
|
|
39
|
+
if not match:
|
|
40
|
+
raise ValueError(f'Invalid position string: {position}')
|
|
41
|
+
|
|
42
|
+
groups = match.groups()
|
|
43
|
+
if len(groups) == 1:
|
|
44
|
+
value = groups[0]
|
|
45
|
+
if value == 'center':
|
|
46
|
+
return (page_width - text_width) / 2, (page_height - text_height) / 2
|
|
47
|
+
elif value == 'top':
|
|
48
|
+
return (page_width - text_width) / 2, 0
|
|
49
|
+
elif value == 'bottom':
|
|
50
|
+
return (page_width - text_width) / 2, page_height - text_height
|
|
51
|
+
elif value == 'left':
|
|
52
|
+
return 0, (page_height - text_height) / 2
|
|
53
|
+
elif value == 'right':
|
|
54
|
+
return page_width - text_width, (page_height - text_height) / 2
|
|
55
|
+
elif value.endswith('%'):
|
|
56
|
+
percent = float(value[:-1]) / 100.0
|
|
57
|
+
return (page_width -
|
|
58
|
+
text_width) * percent, (page_height - text_height) * percent
|
|
59
|
+
else:
|
|
60
|
+
absolute = float(value)
|
|
61
|
+
return absolute, absolute
|
|
62
|
+
elif len(groups) == 2:
|
|
63
|
+
x_value, y_value = groups
|
|
64
|
+
if x_value == 'center':
|
|
65
|
+
x = (page_width - text_width) / 2
|
|
66
|
+
elif x_value == 'top' or x_value == 'bottom':
|
|
67
|
+
raise ValueError(f'Invalid x position: {x_value}')
|
|
68
|
+
elif x_value == 'left':
|
|
69
|
+
x = 0
|
|
70
|
+
elif x_value == 'right':
|
|
71
|
+
x = page_width - text_width
|
|
72
|
+
elif x_value.endswith('%'):
|
|
73
|
+
percent = float(x_value[:-1]) / 100.0
|
|
74
|
+
x = (page_width - text_width) * percent
|
|
75
|
+
else:
|
|
76
|
+
x = float(x_value)
|
|
77
|
+
|
|
78
|
+
if y_value == 'center':
|
|
79
|
+
y = (page_height - text_height) / 2
|
|
80
|
+
elif y_value == 'left' or y_value == 'right':
|
|
81
|
+
raise ValueError(f'Invalid y position: {y_value}')
|
|
82
|
+
elif y_value == 'top':
|
|
83
|
+
y = 0
|
|
84
|
+
elif y_value == 'bottom':
|
|
85
|
+
y = page_height - text_height
|
|
86
|
+
elif y_value.endswith('%'):
|
|
87
|
+
percent = float(y_value[:-1]) / 100.0
|
|
88
|
+
y = (page_height - text_height) * percent
|
|
89
|
+
else:
|
|
90
|
+
y = float(y_value)
|
|
91
|
+
|
|
92
|
+
return x, y
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError(f'Invalid position string: {position}')
|