kagazkit 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.
- kagazkit-0.1.0/LICENSE +21 -0
- kagazkit-0.1.0/PKG-INFO +104 -0
- kagazkit-0.1.0/README.md +60 -0
- kagazkit-0.1.0/pyproject.toml +55 -0
- kagazkit-0.1.0/setup.cfg +4 -0
- kagazkit-0.1.0/src/kagazkit/__init__.py +0 -0
- kagazkit-0.1.0/src/kagazkit/core/actions.py +192 -0
- kagazkit-0.1.0/src/kagazkit/core/validators.py +129 -0
- kagazkit-0.1.0/src/kagazkit/main.py +18 -0
- kagazkit-0.1.0/src/kagazkit/ui/app.py +91 -0
- kagazkit-0.1.0/src/kagazkit/ui/components/file_list.py +91 -0
- kagazkit-0.1.0/src/kagazkit/ui/pages/image_page.py +110 -0
- kagazkit-0.1.0/src/kagazkit/ui/pages/merge_page.py +109 -0
- kagazkit-0.1.0/src/kagazkit/ui/pages/tools_page.py +70 -0
- kagazkit-0.1.0/src/kagazkit.egg-info/PKG-INFO +104 -0
- kagazkit-0.1.0/src/kagazkit.egg-info/SOURCES.txt +20 -0
- kagazkit-0.1.0/src/kagazkit.egg-info/dependency_links.txt +1 -0
- kagazkit-0.1.0/src/kagazkit.egg-info/entry_points.txt +2 -0
- kagazkit-0.1.0/src/kagazkit.egg-info/requires.txt +5 -0
- kagazkit-0.1.0/src/kagazkit.egg-info/top_level.txt +1 -0
- kagazkit-0.1.0/tests/test_actions.py +103 -0
- kagazkit-0.1.0/tests/test_validators.py +48 -0
kagazkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Farjad Hasan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kagazkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kagazkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: KagazKit - Your Ultimate PDF Toolkit. Merge, Split, Convert, and more.
|
|
5
|
+
Author: Farjad Hasan
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Farjad Hasan
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/farjad-hasan/kagazkit
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/farjad-hasan/kagazkit/issues
|
|
30
|
+
Project-URL: LinkedIn, https://www.linkedin.com/in/farjadh/
|
|
31
|
+
Project-URL: X, https://x.com/im_farjad
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Requires-Python: >=3.9
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: customtkinter==5.2.2
|
|
39
|
+
Requires-Dist: Pillow>=10.0.0
|
|
40
|
+
Requires-Dist: PyPDF2>=3.0.0
|
|
41
|
+
Requires-Dist: tkinterdnd2>=0.3.0
|
|
42
|
+
Requires-Dist: packaging>=23.0
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# KagazKit
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
> **Note**: This project is currently under active development.
|
|
52
|
+
|
|
53
|
+
**KagazKit** (“Kagaz” means paper) is a modern, secure, and professional PDF toolkit built with Python and CustomTkinter. It provides an elegant interface for merging PDFs, converting images to PDFs, splitting, rotating, and more.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Modern UI**: Dark mode support, professional design using CustomTkinter.
|
|
58
|
+
- **Secure**: Validation of file inputs and safe handling of file operations.
|
|
59
|
+
- **Merge PDFs**: Combine multiple PDF files with ease.
|
|
60
|
+
- **Image to PDF**: Convert standard image formats (JPG, PNG) to PDF.
|
|
61
|
+
- **Tools**: Split and Rotate PDFs functionality.
|
|
62
|
+
- **Drag & Drop**: Intuitive file management.
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
1. Clone the repository:
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/farjad-hasan/kagazkit.git
|
|
69
|
+
cd kagazkit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2. Create a virtual environment:
|
|
73
|
+
```bash
|
|
74
|
+
python -m venv venv
|
|
75
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
3. Install dependencies:
|
|
79
|
+
```bash
|
|
80
|
+
pip install -r requirements.txt
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
4. Install the package in editable mode:
|
|
84
|
+
```bash
|
|
85
|
+
pip install -e .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
Run the application:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
kagazkit
|
|
94
|
+
# Or directly via python
|
|
95
|
+
python src/kagazkit/main.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Contributing
|
|
99
|
+
|
|
100
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
kagazkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# KagazKit
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
> **Note**: This project is currently under active development.
|
|
8
|
+
|
|
9
|
+
**KagazKit** (“Kagaz” means paper) is a modern, secure, and professional PDF toolkit built with Python and CustomTkinter. It provides an elegant interface for merging PDFs, converting images to PDFs, splitting, rotating, and more.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Modern UI**: Dark mode support, professional design using CustomTkinter.
|
|
14
|
+
- **Secure**: Validation of file inputs and safe handling of file operations.
|
|
15
|
+
- **Merge PDFs**: Combine multiple PDF files with ease.
|
|
16
|
+
- **Image to PDF**: Convert standard image formats (JPG, PNG) to PDF.
|
|
17
|
+
- **Tools**: Split and Rotate PDFs functionality.
|
|
18
|
+
- **Drag & Drop**: Intuitive file management.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
1. Clone the repository:
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/farjad-hasan/kagazkit.git
|
|
25
|
+
cd kagazkit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. Create a virtual environment:
|
|
29
|
+
```bash
|
|
30
|
+
python -m venv venv
|
|
31
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. Install dependencies:
|
|
35
|
+
```bash
|
|
36
|
+
pip install -r requirements.txt
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. Install the package in editable mode:
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
Run the application:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
kagazkit
|
|
50
|
+
# Or directly via python
|
|
51
|
+
python src/kagazkit/main.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Contributing
|
|
55
|
+
|
|
56
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kagazkit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "KagazKit - Your Ultimate PDF Toolkit. Merge, Split, Convert, and more."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Farjad Hasan" },
|
|
12
|
+
]
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.9"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"customtkinter==5.2.2",
|
|
22
|
+
"Pillow>=10.0.0",
|
|
23
|
+
"PyPDF2>=3.0.0",
|
|
24
|
+
"tkinterdnd2>=0.3.0",
|
|
25
|
+
"packaging>=23.0"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
kagazkit = "kagazkit.main:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
"Homepage" = "https://github.com/farjad-hasan/kagazkit"
|
|
33
|
+
"Bug Tracker" = "https://github.com/farjad-hasan/kagazkit/issues"
|
|
34
|
+
"LinkedIn" = "https://www.linkedin.com/in/farjadh/"
|
|
35
|
+
"X" = "https://x.com/im_farjad"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
minversion = "6.0"
|
|
42
|
+
addopts = "-ra -q"
|
|
43
|
+
testpaths = [
|
|
44
|
+
"tests",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
select = ["E", "F", "I"]
|
|
49
|
+
ignore = ["E501", "F401", "F841"]
|
|
50
|
+
line-length = 88
|
|
51
|
+
target-version = "py39"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.format]
|
|
54
|
+
quote-style = "double"
|
|
55
|
+
indent-style = "space"
|
kagazkit-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core actions module for PDF Master.
|
|
3
|
+
Contains the business logic for merging PDFs and converting images to PDFs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Union
|
|
8
|
+
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from PyPDF2 import PdfMerger, PdfReader, PdfWriter
|
|
11
|
+
|
|
12
|
+
from .validators import FileValidationError, Validator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PDFActionError(Exception):
|
|
16
|
+
"""Custom exception for action failures."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class PDFManager:
|
|
20
|
+
"""Manager class for PDF operations."""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def merge_pdfs(file_paths: List[Union[str, Path]], output_path: Union[str, Path]) -> Path:
|
|
24
|
+
"""
|
|
25
|
+
Merges multiple PDFs into a single file.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
file_paths: List of paths to the PDF files to merge.
|
|
29
|
+
output_path: Path where the merged PDF should be saved.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Path to the output file.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
PDFActionError: If merging fails.
|
|
36
|
+
FileValidationError: If input files are invalid.
|
|
37
|
+
"""
|
|
38
|
+
# Validate inputs
|
|
39
|
+
try:
|
|
40
|
+
validated_paths = Validator.validate_paths(file_paths, file_type='pdf')
|
|
41
|
+
except FileValidationError as e:
|
|
42
|
+
raise PDFActionError(f"Validation failed: {e}")
|
|
43
|
+
|
|
44
|
+
merger = PdfMerger()
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
for path in validated_paths:
|
|
48
|
+
merger.append(str(path))
|
|
49
|
+
|
|
50
|
+
output_path = Path(output_path)
|
|
51
|
+
# Ensure output directory exists (though UI should handle this)
|
|
52
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
merger.write(str(output_path))
|
|
55
|
+
merger.close()
|
|
56
|
+
return output_path
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
try:
|
|
60
|
+
merger.close()
|
|
61
|
+
except:
|
|
62
|
+
pass
|
|
63
|
+
raise PDFActionError(f"Failed to merge PDFs: {e}")
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def convert_images_to_pdf(image_paths: List[Union[str, Path]], output_path: Union[str, Path]) -> Path:
|
|
67
|
+
"""
|
|
68
|
+
Converts a list of images to a single PDF.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
image_paths: List of paths to image files.
|
|
72
|
+
output_path: Path for the output PDF.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Path to the output file.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
PDFActionError: If conversion fails.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
validated_paths = Validator.validate_paths(image_paths, file_type='image')
|
|
82
|
+
except FileValidationError as e:
|
|
83
|
+
raise PDFActionError(f"Validation failed: {e}")
|
|
84
|
+
|
|
85
|
+
if not validated_paths:
|
|
86
|
+
raise PDFActionError("No valid images provided.")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Open images and convert to RGB (required for PDF)
|
|
90
|
+
images = []
|
|
91
|
+
for path in validated_paths:
|
|
92
|
+
img = Image.open(path)
|
|
93
|
+
if img.mode != 'RGB':
|
|
94
|
+
img = img.convert('RGB')
|
|
95
|
+
images.append(img)
|
|
96
|
+
|
|
97
|
+
# Save first image and append the rest
|
|
98
|
+
first_image = images[0]
|
|
99
|
+
rest_images = images[1:]
|
|
100
|
+
|
|
101
|
+
output_path = Path(output_path)
|
|
102
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
first_image.save(
|
|
105
|
+
str(output_path),
|
|
106
|
+
"PDF",
|
|
107
|
+
resolution=100.0,
|
|
108
|
+
save_all=True,
|
|
109
|
+
append_images=rest_images
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Close images to free resources
|
|
113
|
+
for img in images:
|
|
114
|
+
img.close()
|
|
115
|
+
|
|
116
|
+
return output_path
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
raise PDFActionError(f"Failed to convert images to PDF: {e}")
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def split_pdf(file_path: Union[str, Path], output_dir: Union[str, Path]) -> List[Path]:
|
|
123
|
+
"""
|
|
124
|
+
Splits a PDF into individual pages.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
file_path: Path to the source PDF.
|
|
128
|
+
output_dir: Directory to save the pages.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of paths to the generated files.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
Validator.validate_pdf(file_path)
|
|
135
|
+
except FileValidationError as e:
|
|
136
|
+
raise PDFActionError(f"Validation failed: {e}")
|
|
137
|
+
|
|
138
|
+
src_path = Path(file_path)
|
|
139
|
+
out_dir = Path(output_dir)
|
|
140
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
|
|
142
|
+
generated_files = []
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
reader = PdfReader(src_path)
|
|
146
|
+
base_name = src_path.stem
|
|
147
|
+
|
|
148
|
+
for i, page in enumerate(reader.pages):
|
|
149
|
+
writer = PdfWriter()
|
|
150
|
+
writer.add_page(page)
|
|
151
|
+
|
|
152
|
+
out_filename = out_dir / f"{base_name}_page_{i+1}.pdf"
|
|
153
|
+
with open(out_filename, "wb") as out_file:
|
|
154
|
+
writer.write(out_file)
|
|
155
|
+
|
|
156
|
+
generated_files.append(out_filename)
|
|
157
|
+
|
|
158
|
+
return generated_files
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
raise PDFActionError(f"Failed to split PDF: {e}")
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def rotate_pdf(file_path: Union[str, Path], output_path: Union[str, Path], rotation: int) -> Path:
|
|
165
|
+
"""
|
|
166
|
+
Rotates all pages of a PDF.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
file_path: Path to source PDF.
|
|
170
|
+
output_path: Path to save result.
|
|
171
|
+
rotation: Degrees to rotate (90, 180, 270).
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
Validator.validate_pdf(file_path)
|
|
175
|
+
except FileValidationError as e:
|
|
176
|
+
raise PDFActionError(f"Validation failed: {e}")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
reader = PdfReader(file_path)
|
|
180
|
+
writer = PdfWriter()
|
|
181
|
+
|
|
182
|
+
for page in reader.pages:
|
|
183
|
+
page.rotate(rotation)
|
|
184
|
+
writer.add_page(page)
|
|
185
|
+
|
|
186
|
+
output_path = Path(output_path)
|
|
187
|
+
with open(output_path, "wb") as f:
|
|
188
|
+
writer.write(f)
|
|
189
|
+
|
|
190
|
+
return output_path
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise PDFActionError(f"Failed to rotate PDF: {e}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input validation module for PDF Master.
|
|
3
|
+
Handles security checks for file inputs, ensuring only valid and safe files are processed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileValidationError(Exception):
|
|
11
|
+
"""Custom exception for file validation failures."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class Validator:
|
|
15
|
+
"""Validator class for file security checks."""
|
|
16
|
+
|
|
17
|
+
# Magic numbers for file type verification
|
|
18
|
+
MAGIC_NUMBERS = {
|
|
19
|
+
'pdf': b'%PDF',
|
|
20
|
+
'png': b'\x89PNG\r\n\x1a\n',
|
|
21
|
+
'jpg': b'\xff\xd8\xff',
|
|
22
|
+
'jpeg': b'\xff\xd8\xff',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def validate_file(file_path: Union[str, Path]) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Validates a single file exists and is a file.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
file_path: Path to the file.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
True if valid.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
FileValidationError: If file does not exist or is not a file.
|
|
38
|
+
"""
|
|
39
|
+
path = Path(file_path)
|
|
40
|
+
if not path.exists():
|
|
41
|
+
raise FileValidationError(f"File not found: {file_path}")
|
|
42
|
+
if not path.is_file():
|
|
43
|
+
raise FileValidationError(f"Path is not a file: {file_path}")
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def validate_pdf(cls, file_path: Union[str, Path]) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Validates that a file is a valid PDF using magic numbers.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
file_path: Path to the PDF file.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if valid.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
FileValidationError: If not a valid PDF.
|
|
59
|
+
"""
|
|
60
|
+
cls.validate_file(file_path)
|
|
61
|
+
try:
|
|
62
|
+
with open(file_path, 'rb') as f:
|
|
63
|
+
header = f.read(4)
|
|
64
|
+
if not header.startswith(cls.MAGIC_NUMBERS['pdf']):
|
|
65
|
+
raise FileValidationError(f"Invalid PDF file header: {file_path}")
|
|
66
|
+
except OSError as e:
|
|
67
|
+
raise FileValidationError(f"Error reading file {file_path}: {e}")
|
|
68
|
+
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def validate_image(cls, file_path: Union[str, Path]) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Validates that a file is a supported image (PNG/JPG) using magic numbers.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
file_path: Path to the image file.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if valid.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
FileValidationError: If not a valid image.
|
|
84
|
+
"""
|
|
85
|
+
cls.validate_file(file_path)
|
|
86
|
+
path = Path(file_path)
|
|
87
|
+
ext = path.suffix.lower().lstrip('.')
|
|
88
|
+
|
|
89
|
+
if ext not in ['png', 'jpg', 'jpeg']:
|
|
90
|
+
raise FileValidationError(f"Unsupported image extension: {ext}")
|
|
91
|
+
|
|
92
|
+
magic = cls.MAGIC_NUMBERS.get(ext)
|
|
93
|
+
if not magic:
|
|
94
|
+
# Should be covered by extension check, but for safety
|
|
95
|
+
raise FileValidationError(f"Unsupported image type: {ext}")
|
|
96
|
+
|
|
97
|
+
read_len = len(magic)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
with open(file_path, 'rb') as f:
|
|
101
|
+
header = f.read(read_len)
|
|
102
|
+
if not header.startswith(magic):
|
|
103
|
+
raise FileValidationError(f"Invalid {ext.upper()} file header: {file_path}")
|
|
104
|
+
except OSError as e:
|
|
105
|
+
raise FileValidationError(f"Error reading file {file_path}: {e}")
|
|
106
|
+
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def validate_paths(cls, paths: List[Union[str, Path]], file_type: str = 'pdf') -> List[Path]:
|
|
111
|
+
"""
|
|
112
|
+
Validates a list of file paths.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
paths: List of file paths.
|
|
116
|
+
file_type: Type validation to apply ('pdf' or 'image').
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of validated Path objects.
|
|
120
|
+
"""
|
|
121
|
+
validated_paths = []
|
|
122
|
+
for p in paths:
|
|
123
|
+
path_obj = Path(p)
|
|
124
|
+
if file_type == 'pdf':
|
|
125
|
+
cls.validate_pdf(path_obj)
|
|
126
|
+
elif file_type == 'image':
|
|
127
|
+
cls.validate_image(path_obj)
|
|
128
|
+
validated_paths.append(path_obj)
|
|
129
|
+
return validated_paths
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for PDF Master application.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Ensure src is in pythonpath
|
|
8
|
+
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
9
|
+
|
|
10
|
+
from kagazkit.ui.app import PDFMasterApp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
app = PDFMasterApp()
|
|
15
|
+
app.mainloop()
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main application window module.
|
|
3
|
+
"""
|
|
4
|
+
import customtkinter as ctk
|
|
5
|
+
from tkinterdnd2 import TkinterDnD
|
|
6
|
+
|
|
7
|
+
from .pages.image_page import ImagePage
|
|
8
|
+
from .pages.merge_page import MergePage
|
|
9
|
+
from .pages.tools_page import ToolsPage
|
|
10
|
+
|
|
11
|
+
# Set theme
|
|
12
|
+
ctk.set_appearance_mode("System")
|
|
13
|
+
ctk.set_default_color_theme("blue")
|
|
14
|
+
|
|
15
|
+
class PDFMasterApp(ctk.CTk, TkinterDnD.DnDWrapper):
|
|
16
|
+
"""
|
|
17
|
+
Main application class for PDF Master.
|
|
18
|
+
Inherits from CTk for modern UI and DnDWrapper for Drag and Drop support.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.TkdndVersion = TkinterDnD._require(self)
|
|
24
|
+
|
|
25
|
+
self.title("KagazKit")
|
|
26
|
+
self.geometry("900x600")
|
|
27
|
+
|
|
28
|
+
# Configure grid layout (1x2)
|
|
29
|
+
self.grid_rowconfigure(0, weight=1)
|
|
30
|
+
self.grid_columnconfigure(1, weight=1)
|
|
31
|
+
|
|
32
|
+
# Create Sidebar
|
|
33
|
+
self.sidebar_frame = ctk.CTkFrame(self, width=140, corner_radius=0)
|
|
34
|
+
self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
|
|
35
|
+
self.sidebar_frame.grid_rowconfigure(5, weight=1)
|
|
36
|
+
|
|
37
|
+
self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="KagazKit", font=ctk.CTkFont(size=20, weight="bold"))
|
|
38
|
+
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
|
|
39
|
+
|
|
40
|
+
self.sidebar_button_merge = ctk.CTkButton(self.sidebar_frame, text="Merge PDFs", command=self.show_merge_page)
|
|
41
|
+
self.sidebar_button_merge.grid(row=1, column=0, padx=20, pady=10)
|
|
42
|
+
|
|
43
|
+
self.sidebar_button_convert = ctk.CTkButton(self.sidebar_frame, text="Image to PDF", command=self.show_image_page)
|
|
44
|
+
self.sidebar_button_convert.grid(row=2, column=0, padx=20, pady=10)
|
|
45
|
+
|
|
46
|
+
self.sidebar_button_tools = ctk.CTkButton(self.sidebar_frame, text="Tools", command=self.show_tools_page)
|
|
47
|
+
self.sidebar_button_tools.grid(row=3, column=0, padx=20, pady=10)
|
|
48
|
+
|
|
49
|
+
# Pages - Container
|
|
50
|
+
self.pages_frame = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
|
|
51
|
+
self.pages_frame.grid(row=0, column=1, sticky="nsew", padx=0, pady=0)
|
|
52
|
+
self.pages_frame.grid_rowconfigure(0, weight=1)
|
|
53
|
+
self.pages_frame.grid_columnconfigure(0, weight=1)
|
|
54
|
+
|
|
55
|
+
self.merge_page = MergePage(self.pages_frame)
|
|
56
|
+
self.image_page = ImagePage(self.pages_frame)
|
|
57
|
+
self.tools_page = ToolsPage(self.pages_frame)
|
|
58
|
+
|
|
59
|
+
# Show default page
|
|
60
|
+
self.show_merge_page()
|
|
61
|
+
|
|
62
|
+
def show_merge_page(self):
|
|
63
|
+
self.select_frame(self.sidebar_button_merge)
|
|
64
|
+
self.image_page.grid_forget()
|
|
65
|
+
self.tools_page.grid_forget()
|
|
66
|
+
self.merge_page.grid(row=0, column=0, sticky="nsew")
|
|
67
|
+
|
|
68
|
+
def show_image_page(self):
|
|
69
|
+
self.select_frame(self.sidebar_button_convert)
|
|
70
|
+
self.merge_page.grid_forget()
|
|
71
|
+
self.tools_page.grid_forget()
|
|
72
|
+
self.image_page.grid(row=0, column=0, sticky="nsew")
|
|
73
|
+
|
|
74
|
+
def show_tools_page(self):
|
|
75
|
+
self.select_frame(self.sidebar_button_tools)
|
|
76
|
+
self.merge_page.grid_forget()
|
|
77
|
+
self.image_page.grid_forget()
|
|
78
|
+
self.tools_page.grid(row=0, column=0, sticky="nsew")
|
|
79
|
+
|
|
80
|
+
def select_frame(self, button):
|
|
81
|
+
# Reset all buttons
|
|
82
|
+
self.sidebar_button_merge.configure(fg_color=["#3B8ED0", "#1F6AA5"]) # Default blue
|
|
83
|
+
self.sidebar_button_convert.configure(fg_color=["#3B8ED0", "#1F6AA5"])
|
|
84
|
+
self.sidebar_button_tools.configure(fg_color=["#3B8ED0", "#1F6AA5"])
|
|
85
|
+
|
|
86
|
+
# Highlight selected
|
|
87
|
+
button.configure(fg_color=["#36719F", "#144870"])
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
app = PDFMasterApp()
|
|
91
|
+
app.mainloop()
|