quicktools-atom 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/PKG-INFO +9 -1
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/pyproject.toml +11 -2
- quicktools_atom-0.4.0/src/quicktools/doctools.py +89 -0
- quicktools_atom-0.4.0/src/quicktools/filetools.py +103 -0
- quicktools_atom-0.4.0/src/quicktools/imagetools.py +128 -0
- quicktools_atom-0.4.0/src/quicktools/pdftools.py +161 -0
- quicktools_atom-0.4.0/src/quicktools/spreadsheettools.py +72 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/PKG-INFO +9 -1
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/SOURCES.txt +6 -0
- quicktools_atom-0.4.0/src/quicktools_atom.egg-info/requires.txt +8 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/README.md +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/setup.cfg +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/__init__.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/calculus.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/cli.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/combinatorics.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/linalg.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/mathtools.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/numbertheory.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/strtools.py +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/dependency_links.txt +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/entry_points.txt +0 -0
- {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quicktools-atom
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Math and string utilities with a bundled CLI, by AtomDev Studios
|
|
5
5
|
Author-email: Samuel Peprah <windscribe.samuel@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
@@ -8,6 +8,14 @@ Project-URL: Repository, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
|
8
8
|
Project-URL: Documentation, https://samuel-peprah-cmd.github.io/quicktools/
|
|
9
9
|
Requires-Python: >=3.9
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: Pillow
|
|
12
|
+
Requires-Dist: python-docx
|
|
13
|
+
Requires-Dist: pypdf
|
|
14
|
+
Requires-Dist: openpyxl
|
|
15
|
+
Requires-Dist: reportlab
|
|
16
|
+
Requires-Dist: PyYAML
|
|
17
|
+
Requires-Dist: pdf2docx
|
|
18
|
+
Requires-Dist: docx2pdf
|
|
11
19
|
|
|
12
20
|
\# quicktools
|
|
13
21
|
|
|
@@ -4,14 +4,23 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quicktools-atom"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Math and string utilities with a bundled CLI, by AtomDev Studios"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Samuel Peprah", email = "windscribe.samuel@gmail.com" }
|
|
13
13
|
]
|
|
14
|
-
dependencies = [
|
|
14
|
+
dependencies = [
|
|
15
|
+
"Pillow",
|
|
16
|
+
"python-docx",
|
|
17
|
+
"pypdf",
|
|
18
|
+
"openpyxl",
|
|
19
|
+
"reportlab",
|
|
20
|
+
"PyYAML",
|
|
21
|
+
"pdf2docx",
|
|
22
|
+
"docx2pdf",
|
|
23
|
+
]
|
|
15
24
|
|
|
16
25
|
[project.urls]
|
|
17
26
|
Homepage = "https://github.com/Samuel-Peprah-cmd/quicktools"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Word document utilities: reading, creating, editing .docx files, and converting to/from PDF."""
|
|
2
|
+
from docx import Document
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def read_docx_text(path: str) -> str:
|
|
6
|
+
"""Extract and return all paragraph text from a .docx file."""
|
|
7
|
+
doc = Document(path)
|
|
8
|
+
return "\n".join(p.text for p in doc.paragraphs)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_docx_from_text(text: str, output_path: str) -> None:
|
|
12
|
+
"""Create a new .docx file, adding each line of text as a separate paragraph."""
|
|
13
|
+
doc = Document()
|
|
14
|
+
for line in text.split("\n"):
|
|
15
|
+
doc.add_paragraph(line)
|
|
16
|
+
doc.save(output_path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_docx_word_count(path: str) -> int:
|
|
20
|
+
"""Count the total number of words across all paragraphs in a .docx file."""
|
|
21
|
+
doc = Document(path)
|
|
22
|
+
return sum(len(p.text.split()) for p in doc.paragraphs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_docx_paragraph_count(path: str) -> int:
|
|
26
|
+
"""Count the number of paragraphs in a .docx file."""
|
|
27
|
+
return len(Document(path).paragraphs)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def replace_text_in_docx(path: str, old: str, new: str, output_path: str) -> None:
|
|
31
|
+
"""Replace all occurrences of `old` with `new` across every paragraph, saving to output_path."""
|
|
32
|
+
doc = Document(path)
|
|
33
|
+
for p in doc.paragraphs:
|
|
34
|
+
if old in p.text:
|
|
35
|
+
for run in p.runs:
|
|
36
|
+
if old in run.text:
|
|
37
|
+
run.text = run.text.replace(old, new)
|
|
38
|
+
doc.save(output_path)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_docx_tables(path: str) -> list[list[list[str]]]:
|
|
42
|
+
"""Extract every table in a .docx file as a list of tables, each a list of rows, each a list of cell strings."""
|
|
43
|
+
doc = Document(path)
|
|
44
|
+
all_tables = []
|
|
45
|
+
for table in doc.tables:
|
|
46
|
+
rows = []
|
|
47
|
+
for row in table.rows:
|
|
48
|
+
rows.append([cell.text for cell in row.cells])
|
|
49
|
+
all_tables.append(rows)
|
|
50
|
+
return all_tables
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def merge_docx_files(paths: list[str], output_path: str) -> None:
|
|
54
|
+
"""Merge multiple .docx files into one, concatenating their paragraphs in order."""
|
|
55
|
+
combined = Document()
|
|
56
|
+
for i, path in enumerate(paths):
|
|
57
|
+
doc = Document(path)
|
|
58
|
+
for p in doc.paragraphs:
|
|
59
|
+
combined.add_paragraph(p.text)
|
|
60
|
+
if i < len(paths) - 1:
|
|
61
|
+
combined.add_page_break()
|
|
62
|
+
combined.save(output_path)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def docx_to_pdf(path: str, output_path: str) -> None:
|
|
66
|
+
"""Convert a .docx file to PDF. Requires Microsoft Word to be installed (Windows/macOS) and
|
|
67
|
+
the optional 'docx2pdf' package (pip install docx2pdf)."""
|
|
68
|
+
try:
|
|
69
|
+
from docx2pdf import convert
|
|
70
|
+
except ImportError:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"docx_to_pdf() requires docx2pdf. Install it with: pip install docx2pdf\n"
|
|
73
|
+
"Note: it also requires Microsoft Word to be installed on this machine."
|
|
74
|
+
)
|
|
75
|
+
convert(path, output_path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def pdf_to_docx(path: str, output_path: str) -> None:
|
|
79
|
+
"""Convert a PDF file to .docx, preserving text layout where possible.
|
|
80
|
+
Requires the optional 'pdf2docx' package (pip install pdf2docx)."""
|
|
81
|
+
try:
|
|
82
|
+
from pdf2docx import Converter
|
|
83
|
+
except ImportError:
|
|
84
|
+
raise ImportError(
|
|
85
|
+
"pdf_to_docx() requires pdf2docx. Install it with: pip install pdf2docx"
|
|
86
|
+
)
|
|
87
|
+
cv = Converter(path)
|
|
88
|
+
cv.convert(output_path)
|
|
89
|
+
cv.close()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""File utilities: reading, writing, hashing, zipping, and inspecting files of any type."""
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
import zipfile
|
|
6
|
+
import mimetypes
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_file_info(path: str) -> dict:
|
|
11
|
+
"""Return basic metadata about a file: size, extension, mime type, last modified time."""
|
|
12
|
+
stat = os.stat(path)
|
|
13
|
+
mime_type, _ = mimetypes.guess_type(path)
|
|
14
|
+
return {
|
|
15
|
+
"size_bytes": stat.st_size,
|
|
16
|
+
"extension": os.path.splitext(path)[1],
|
|
17
|
+
"mime_type": mime_type or "unknown",
|
|
18
|
+
"modified_time": stat.st_mtime,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_text_file(path: str, encoding: str = "utf-8") -> str:
|
|
23
|
+
"""Read and return the full contents of a text file."""
|
|
24
|
+
with open(path, "r", encoding=encoding) as f:
|
|
25
|
+
return f.read()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_text_file(path: str, content: str, encoding: str = "utf-8") -> None:
|
|
29
|
+
"""Write text content to a file, overwriting it if it exists."""
|
|
30
|
+
with open(path, "w", encoding=encoding) as f:
|
|
31
|
+
f.write(content)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_json_file(path: str) -> object:
|
|
35
|
+
"""Read and parse a JSON file, returning the resulting Python object."""
|
|
36
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
37
|
+
return json.load(f)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def write_json_file(path: str, data: object, indent: int = 2) -> None:
|
|
41
|
+
"""Write a Python object to a file as formatted JSON."""
|
|
42
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
43
|
+
json.dump(data, f, indent=indent)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_yaml_file(path: str) -> object:
|
|
47
|
+
"""Read and parse a YAML file, returning the resulting Python object."""
|
|
48
|
+
import yaml
|
|
49
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
50
|
+
return yaml.safe_load(f)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def write_yaml_file(path: str, data: object) -> None:
|
|
54
|
+
"""Write a Python object to a file as YAML."""
|
|
55
|
+
import yaml
|
|
56
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
57
|
+
yaml.safe_dump(data, f)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def compute_file_hash(path: str, algorithm: str = "sha256") -> str:
|
|
61
|
+
"""Compute the hex digest hash of a file's contents (works for any file type)."""
|
|
62
|
+
h = hashlib.new(algorithm)
|
|
63
|
+
with open(path, "rb") as f:
|
|
64
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
65
|
+
h.update(chunk)
|
|
66
|
+
return h.hexdigest()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def zip_files(file_paths: list[str], output_zip_path: str) -> None:
|
|
70
|
+
"""Compress a list of files into a single .zip archive."""
|
|
71
|
+
with zipfile.ZipFile(output_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
72
|
+
for path in file_paths:
|
|
73
|
+
zf.write(path, arcname=os.path.basename(path))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def unzip_file(zip_path: str, output_dir: str) -> list[str]:
|
|
77
|
+
"""Extract a .zip archive into output_dir, returning the list of extracted file names."""
|
|
78
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
79
|
+
zf.extractall(output_dir)
|
|
80
|
+
return zf.namelist()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def list_files_in_directory(directory: str, extension: str | None = None) -> list[str]:
|
|
84
|
+
"""List all files in a directory, optionally filtered by extension (e.g. '.txt')."""
|
|
85
|
+
files = os.listdir(directory)
|
|
86
|
+
if extension:
|
|
87
|
+
files = [f for f in files if f.endswith(extension)]
|
|
88
|
+
return files
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_file_extension(path: str) -> str:
|
|
92
|
+
"""Return the file extension of a path, including the leading dot (e.g. '.pdf')."""
|
|
93
|
+
return os.path.splitext(path)[1]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def copy_file(src: str, dst: str) -> None:
|
|
97
|
+
"""Copy a file from src to dst."""
|
|
98
|
+
shutil.copy2(src, dst)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def delete_file(path: str) -> None:
|
|
102
|
+
"""Delete a file. Raises FileNotFoundError if it doesn't exist."""
|
|
103
|
+
os.remove(path)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Image utilities: format conversion, resizing, editing, and image-to-PDF, powered by Pillow.
|
|
2
|
+
|
|
3
|
+
Supports conversion between PNG, JPEG/JPG, BMP, GIF, WEBP, TIFF, and more —
|
|
4
|
+
the output format is inferred automatically from the output file's extension.
|
|
5
|
+
"""
|
|
6
|
+
from PIL import Image, ImageOps, ImageDraw, ImageFont
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def convert_image(input_path: str, output_path: str) -> None:
|
|
10
|
+
"""Convert an image from one format to another (e.g. PNG -> JPEG, JPEG -> WEBP).
|
|
11
|
+
The target format is inferred from output_path's file extension."""
|
|
12
|
+
img = Image.open(input_path)
|
|
13
|
+
# JPEG doesn't support transparency (RGBA); flatten onto white if needed.
|
|
14
|
+
if output_path.lower().endswith((".jpg", ".jpeg")) and img.mode in ("RGBA", "P"):
|
|
15
|
+
img = img.convert("RGB")
|
|
16
|
+
img.save(output_path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resize_image(input_path: str, output_path: str, width: int, height: int) -> None:
|
|
20
|
+
"""Resize an image to the given width and height."""
|
|
21
|
+
img = Image.open(input_path)
|
|
22
|
+
resized = img.resize((width, height))
|
|
23
|
+
resized.save(output_path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def rotate_image(input_path: str, output_path: str, degrees: float) -> None:
|
|
27
|
+
"""Rotate an image by the given number of degrees (counter-clockwise), expanding the canvas as needed."""
|
|
28
|
+
img = Image.open(input_path)
|
|
29
|
+
rotated = img.rotate(degrees, expand=True)
|
|
30
|
+
rotated.save(output_path)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def flip_image(input_path: str, output_path: str, direction: str = "horizontal") -> None:
|
|
34
|
+
"""Flip an image. direction is 'horizontal' or 'vertical'."""
|
|
35
|
+
img = Image.open(input_path)
|
|
36
|
+
if direction == "horizontal":
|
|
37
|
+
flipped = ImageOps.mirror(img)
|
|
38
|
+
elif direction == "vertical":
|
|
39
|
+
flipped = ImageOps.flip(img)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError("direction must be 'horizontal' or 'vertical'")
|
|
42
|
+
flipped.save(output_path)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def grayscale_image(input_path: str, output_path: str) -> None:
|
|
46
|
+
"""Convert an image to grayscale."""
|
|
47
|
+
img = Image.open(input_path).convert("L")
|
|
48
|
+
img.save(output_path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def crop_image(input_path: str, output_path: str, left: int, top: int, right: int, bottom: int) -> None:
|
|
52
|
+
"""Crop an image to the box defined by (left, top, right, bottom) pixel coordinates."""
|
|
53
|
+
img = Image.open(input_path)
|
|
54
|
+
cropped = img.crop((left, top, right, bottom))
|
|
55
|
+
cropped.save(output_path)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_image_info(path: str) -> dict:
|
|
59
|
+
"""Return basic information about an image: format, dimensions, and color mode."""
|
|
60
|
+
img = Image.open(path)
|
|
61
|
+
return {"format": img.format, "size": img.size, "mode": img.mode}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_thumbnail(input_path: str, output_path: str, max_size: int = 128) -> None:
|
|
65
|
+
"""Create a thumbnail no larger than max_size x max_size, preserving aspect ratio."""
|
|
66
|
+
img = Image.open(input_path)
|
|
67
|
+
img.thumbnail((max_size, max_size))
|
|
68
|
+
img.save(output_path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def compress_image(input_path: str, output_path: str, quality: int = 70) -> None:
|
|
72
|
+
"""Save a (typically JPEG) image at a reduced quality level to shrink file size."""
|
|
73
|
+
img = Image.open(input_path)
|
|
74
|
+
if img.mode in ("RGBA", "P"):
|
|
75
|
+
img = img.convert("RGB")
|
|
76
|
+
img.save(output_path, quality=quality, optimize=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def merge_images_horizontally(paths: list[str], output_path: str) -> None:
|
|
80
|
+
"""Combine multiple images side by side into a single image."""
|
|
81
|
+
images = [Image.open(p) for p in paths]
|
|
82
|
+
total_width = sum(img.width for img in images)
|
|
83
|
+
max_height = max(img.height for img in images)
|
|
84
|
+
combined = Image.new("RGB", (total_width, max_height), "white")
|
|
85
|
+
x_offset = 0
|
|
86
|
+
for img in images:
|
|
87
|
+
combined.paste(img, (x_offset, 0))
|
|
88
|
+
x_offset += img.width
|
|
89
|
+
combined.save(output_path)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def merge_images_vertically(paths: list[str], output_path: str) -> None:
|
|
93
|
+
"""Stack multiple images on top of each other into a single image."""
|
|
94
|
+
images = [Image.open(p) for p in paths]
|
|
95
|
+
max_width = max(img.width for img in images)
|
|
96
|
+
total_height = sum(img.height for img in images)
|
|
97
|
+
combined = Image.new("RGB", (max_width, total_height), "white")
|
|
98
|
+
y_offset = 0
|
|
99
|
+
for img in images:
|
|
100
|
+
combined.paste(img, (0, y_offset))
|
|
101
|
+
y_offset += img.height
|
|
102
|
+
combined.save(output_path)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def add_text_watermark(input_path: str, output_path: str, text: str) -> None:
|
|
106
|
+
"""Overlay a text watermark in the bottom-right corner of an image."""
|
|
107
|
+
img = Image.open(input_path).convert("RGBA")
|
|
108
|
+
overlay = Image.new("RGBA", img.size, (255, 255, 255, 0))
|
|
109
|
+
draw = ImageDraw.Draw(overlay)
|
|
110
|
+
try:
|
|
111
|
+
font = ImageFont.load_default()
|
|
112
|
+
except Exception:
|
|
113
|
+
font = None
|
|
114
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
115
|
+
text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
116
|
+
position = (img.width - text_w - 10, img.height - text_h - 10)
|
|
117
|
+
draw.text(position, text, fill=(255, 255, 255, 180), font=font)
|
|
118
|
+
watermarked = Image.alpha_composite(img, overlay).convert("RGB")
|
|
119
|
+
watermarked.save(output_path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def images_to_pdf(image_paths: list[str], output_pdf_path: str) -> None:
|
|
123
|
+
"""Combine one or more images into a single multi-page PDF file."""
|
|
124
|
+
images = [Image.open(p).convert("RGB") for p in image_paths]
|
|
125
|
+
if not images:
|
|
126
|
+
raise ValueError("At least one image path is required")
|
|
127
|
+
first, rest = images[0], images[1:]
|
|
128
|
+
first.save(output_pdf_path, save_all=True, append_images=rest)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""PDF utilities: extracting, merging, splitting, rotating, and creating PDFs."""
|
|
2
|
+
from pypdf import PdfReader, PdfWriter
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def extract_text_from_pdf(path: str) -> str:
|
|
6
|
+
"""Extract and return all text content from a PDF file."""
|
|
7
|
+
reader = PdfReader(path)
|
|
8
|
+
return "\n".join(page.extract_text() or "" for page in reader.pages)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_pdf_page_count(path: str) -> int:
|
|
12
|
+
"""Return the number of pages in a PDF file."""
|
|
13
|
+
return len(PdfReader(path).pages)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def merge_pdfs(paths: list[str], output_path: str) -> None:
|
|
17
|
+
"""Merge multiple PDF files into a single PDF, in the given order."""
|
|
18
|
+
writer = PdfWriter()
|
|
19
|
+
for path in paths:
|
|
20
|
+
reader = PdfReader(path)
|
|
21
|
+
for page in reader.pages:
|
|
22
|
+
writer.add_page(page)
|
|
23
|
+
with open(output_path, "wb") as f:
|
|
24
|
+
writer.write(f)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def split_pdf_to_pages(path: str, output_dir: str) -> list[str]:
|
|
28
|
+
"""Split a PDF into individual single-page PDF files. Returns the list of output file paths."""
|
|
29
|
+
import os
|
|
30
|
+
reader = PdfReader(path)
|
|
31
|
+
output_paths = []
|
|
32
|
+
for i, page in enumerate(reader.pages):
|
|
33
|
+
writer = PdfWriter()
|
|
34
|
+
writer.add_page(page)
|
|
35
|
+
out_path = os.path.join(output_dir, f"page_{i + 1}.pdf")
|
|
36
|
+
with open(out_path, "wb") as f:
|
|
37
|
+
writer.write(f)
|
|
38
|
+
output_paths.append(out_path)
|
|
39
|
+
return output_paths
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_pdf_pages(path: str, page_numbers: list[int], output_path: str) -> None:
|
|
43
|
+
"""Extract specific pages (1-indexed) from a PDF into a new PDF file."""
|
|
44
|
+
reader = PdfReader(path)
|
|
45
|
+
writer = PdfWriter()
|
|
46
|
+
for num in page_numbers:
|
|
47
|
+
writer.add_page(reader.pages[num - 1])
|
|
48
|
+
with open(output_path, "wb") as f:
|
|
49
|
+
writer.write(f)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rotate_pdf(path: str, degrees: int, output_path: str) -> None:
|
|
53
|
+
"""Rotate every page in a PDF by the given degrees (must be a multiple of 90)."""
|
|
54
|
+
reader = PdfReader(path)
|
|
55
|
+
writer = PdfWriter()
|
|
56
|
+
for page in reader.pages:
|
|
57
|
+
page.rotate(degrees)
|
|
58
|
+
writer.add_page(page)
|
|
59
|
+
with open(output_path, "wb") as f:
|
|
60
|
+
writer.write(f)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_pdf_metadata(path: str) -> dict:
|
|
64
|
+
"""Return the metadata (title, author, creation date, etc.) of a PDF file."""
|
|
65
|
+
reader = PdfReader(path)
|
|
66
|
+
meta = reader.metadata
|
|
67
|
+
return dict(meta) if meta else {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_pdf_from_text(text: str, output_path: str) -> None:
|
|
71
|
+
"""Create a simple PDF file containing the given text, wrapping and paginating automatically."""
|
|
72
|
+
from reportlab.lib.pagesizes import letter
|
|
73
|
+
from reportlab.pdfgen import canvas
|
|
74
|
+
from reportlab.lib.units import inch
|
|
75
|
+
|
|
76
|
+
c = canvas.Canvas(output_path, pagesize=letter)
|
|
77
|
+
width, height = letter
|
|
78
|
+
margin = inch
|
|
79
|
+
max_width_chars = 90
|
|
80
|
+
y = height - margin
|
|
81
|
+
line_height = 14
|
|
82
|
+
|
|
83
|
+
import textwrap
|
|
84
|
+
for paragraph in text.split("\n"):
|
|
85
|
+
wrapped_lines = textwrap.wrap(paragraph, max_width_chars) or [""]
|
|
86
|
+
for line in wrapped_lines:
|
|
87
|
+
if y < margin:
|
|
88
|
+
c.showPage()
|
|
89
|
+
y = height - margin
|
|
90
|
+
c.drawString(margin, y, line)
|
|
91
|
+
y -= line_height
|
|
92
|
+
c.save()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def encrypt_pdf(path: str, password: str, output_path: str) -> None:
|
|
96
|
+
"""Create a password-protected copy of a PDF."""
|
|
97
|
+
reader = PdfReader(path)
|
|
98
|
+
writer = PdfWriter()
|
|
99
|
+
for page in reader.pages:
|
|
100
|
+
writer.add_page(page)
|
|
101
|
+
writer.encrypt(password)
|
|
102
|
+
with open(output_path, "wb") as f:
|
|
103
|
+
writer.write(f)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def decrypt_pdf(path: str, password: str, output_path: str) -> None:
|
|
107
|
+
"""Remove password protection from a PDF, given the correct password."""
|
|
108
|
+
reader = PdfReader(path)
|
|
109
|
+
if reader.is_encrypted:
|
|
110
|
+
reader.decrypt(password)
|
|
111
|
+
writer = PdfWriter()
|
|
112
|
+
for page in reader.pages:
|
|
113
|
+
writer.add_page(page)
|
|
114
|
+
with open(output_path, "wb") as f:
|
|
115
|
+
writer.write(f)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def add_page_numbers(path: str, output_path: str) -> None:
|
|
119
|
+
"""Add page numbers (bottom-center) to every page of a PDF."""
|
|
120
|
+
import io
|
|
121
|
+
from reportlab.pdfgen import canvas
|
|
122
|
+
from reportlab.lib.pagesizes import letter
|
|
123
|
+
|
|
124
|
+
reader = PdfReader(path)
|
|
125
|
+
writer = PdfWriter()
|
|
126
|
+
|
|
127
|
+
for i, page in enumerate(reader.pages):
|
|
128
|
+
packet = io.BytesIO()
|
|
129
|
+
page_width = float(page.mediabox.width)
|
|
130
|
+
c = canvas.Canvas(packet, pagesize=(page_width, float(page.mediabox.height)))
|
|
131
|
+
c.drawCentredString(page_width / 2, 20, str(i + 1))
|
|
132
|
+
c.save()
|
|
133
|
+
packet.seek(0)
|
|
134
|
+
overlay_reader = PdfReader(packet)
|
|
135
|
+
page.merge_page(overlay_reader.pages[0])
|
|
136
|
+
writer.add_page(page)
|
|
137
|
+
|
|
138
|
+
with open(output_path, "wb") as f:
|
|
139
|
+
writer.write(f)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def pdf_to_images(path: str, output_dir: str, dpi: int = 150) -> list[str]:
|
|
143
|
+
"""Render every page of a PDF as a PNG image. Requires the optional 'PyMuPDF' package
|
|
144
|
+
(pip install pymupdf) — raises ImportError with instructions if it's not installed."""
|
|
145
|
+
import os
|
|
146
|
+
try:
|
|
147
|
+
import fitz # PyMuPDF
|
|
148
|
+
except ImportError:
|
|
149
|
+
raise ImportError(
|
|
150
|
+
"pdf_to_images() requires PyMuPDF. Install it with: pip install pymupdf"
|
|
151
|
+
)
|
|
152
|
+
doc = fitz.open(path)
|
|
153
|
+
output_paths = []
|
|
154
|
+
zoom = dpi / 72
|
|
155
|
+
matrix = fitz.Matrix(zoom, zoom)
|
|
156
|
+
for i, page in enumerate(doc):
|
|
157
|
+
pix = page.get_pixmap(matrix=matrix)
|
|
158
|
+
out_path = os.path.join(output_dir, f"page_{i + 1}.png")
|
|
159
|
+
pix.save(out_path)
|
|
160
|
+
output_paths.append(out_path)
|
|
161
|
+
return output_paths
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Spreadsheet utilities: reading, writing, and converting between CSV, Excel, and JSON."""
|
|
2
|
+
import csv
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def read_csv_file(path: str) -> list[dict]:
|
|
7
|
+
"""Read a CSV file and return its rows as a list of dictionaries (keyed by header)."""
|
|
8
|
+
with open(path, "r", encoding="utf-8", newline="") as f:
|
|
9
|
+
return list(csv.DictReader(f))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_csv_file(path: str, data: list[dict]) -> None:
|
|
13
|
+
"""Write a list of dictionaries to a CSV file, using the first row's keys as headers."""
|
|
14
|
+
if not data:
|
|
15
|
+
raise ValueError("data must contain at least one row")
|
|
16
|
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
|
17
|
+
writer = csv.DictWriter(f, fieldnames=list(data[0].keys()))
|
|
18
|
+
writer.writeheader()
|
|
19
|
+
writer.writerows(data)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def csv_to_xlsx(csv_path: str, xlsx_path: str) -> None:
|
|
23
|
+
"""Convert a CSV file into an Excel .xlsx file."""
|
|
24
|
+
from openpyxl import Workbook
|
|
25
|
+
rows = read_csv_file(csv_path)
|
|
26
|
+
wb = Workbook()
|
|
27
|
+
ws = wb.active
|
|
28
|
+
if rows:
|
|
29
|
+
ws.append(list(rows[0].keys()))
|
|
30
|
+
for row in rows:
|
|
31
|
+
ws.append(list(row.values()))
|
|
32
|
+
wb.save(xlsx_path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def xlsx_to_csv(xlsx_path: str, csv_path: str, sheet_name: str | None = None) -> None:
|
|
36
|
+
"""Convert an Excel .xlsx file (a single sheet) into a CSV file."""
|
|
37
|
+
from openpyxl import load_workbook
|
|
38
|
+
wb = load_workbook(xlsx_path)
|
|
39
|
+
ws = wb[sheet_name] if sheet_name else wb.active
|
|
40
|
+
with open(csv_path, "w", encoding="utf-8", newline="") as f:
|
|
41
|
+
writer = csv.writer(f)
|
|
42
|
+
for row in ws.iter_rows(values_only=True):
|
|
43
|
+
writer.writerow(row)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def json_to_csv(json_path: str, csv_path: str) -> None:
|
|
47
|
+
"""Convert a JSON file (a list of flat objects) into a CSV file."""
|
|
48
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
49
|
+
data = json.load(f)
|
|
50
|
+
write_csv_file(csv_path, data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def csv_to_json(csv_path: str, json_path: str) -> None:
|
|
54
|
+
"""Convert a CSV file into a JSON file (a list of objects)."""
|
|
55
|
+
data = read_csv_file(csv_path)
|
|
56
|
+
with open(json_path, "w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(data, f, indent=2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_xlsx_sheet_names(path: str) -> list[str]:
|
|
61
|
+
"""Return the list of sheet names in an Excel workbook."""
|
|
62
|
+
from openpyxl import load_workbook
|
|
63
|
+
wb = load_workbook(path, read_only=True)
|
|
64
|
+
return wb.sheetnames
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def merge_csv_files(paths: list[str], output_path: str) -> None:
|
|
68
|
+
"""Merge multiple CSV files (with the same columns) into a single CSV file."""
|
|
69
|
+
all_rows = []
|
|
70
|
+
for path in paths:
|
|
71
|
+
all_rows.extend(read_csv_file(path))
|
|
72
|
+
write_csv_file(output_path, all_rows)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quicktools-atom
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Math and string utilities with a bundled CLI, by AtomDev Studios
|
|
5
5
|
Author-email: Samuel Peprah <windscribe.samuel@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
@@ -8,6 +8,14 @@ Project-URL: Repository, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
|
8
8
|
Project-URL: Documentation, https://samuel-peprah-cmd.github.io/quicktools/
|
|
9
9
|
Requires-Python: >=3.9
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: Pillow
|
|
12
|
+
Requires-Dist: python-docx
|
|
13
|
+
Requires-Dist: pypdf
|
|
14
|
+
Requires-Dist: openpyxl
|
|
15
|
+
Requires-Dist: reportlab
|
|
16
|
+
Requires-Dist: PyYAML
|
|
17
|
+
Requires-Dist: pdf2docx
|
|
18
|
+
Requires-Dist: docx2pdf
|
|
11
19
|
|
|
12
20
|
\# quicktools
|
|
13
21
|
|
|
@@ -4,12 +4,18 @@ src/quicktools/__init__.py
|
|
|
4
4
|
src/quicktools/calculus.py
|
|
5
5
|
src/quicktools/cli.py
|
|
6
6
|
src/quicktools/combinatorics.py
|
|
7
|
+
src/quicktools/doctools.py
|
|
8
|
+
src/quicktools/filetools.py
|
|
9
|
+
src/quicktools/imagetools.py
|
|
7
10
|
src/quicktools/linalg.py
|
|
8
11
|
src/quicktools/mathtools.py
|
|
9
12
|
src/quicktools/numbertheory.py
|
|
13
|
+
src/quicktools/pdftools.py
|
|
14
|
+
src/quicktools/spreadsheettools.py
|
|
10
15
|
src/quicktools/strtools.py
|
|
11
16
|
src/quicktools_atom.egg-info/PKG-INFO
|
|
12
17
|
src/quicktools_atom.egg-info/SOURCES.txt
|
|
13
18
|
src/quicktools_atom.egg-info/dependency_links.txt
|
|
14
19
|
src/quicktools_atom.egg-info/entry_points.txt
|
|
20
|
+
src/quicktools_atom.egg-info/requires.txt
|
|
15
21
|
src/quicktools_atom.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|