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.
Files changed (23) hide show
  1. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/PKG-INFO +9 -1
  2. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/pyproject.toml +11 -2
  3. quicktools_atom-0.4.0/src/quicktools/doctools.py +89 -0
  4. quicktools_atom-0.4.0/src/quicktools/filetools.py +103 -0
  5. quicktools_atom-0.4.0/src/quicktools/imagetools.py +128 -0
  6. quicktools_atom-0.4.0/src/quicktools/pdftools.py +161 -0
  7. quicktools_atom-0.4.0/src/quicktools/spreadsheettools.py +72 -0
  8. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/PKG-INFO +9 -1
  9. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/SOURCES.txt +6 -0
  10. quicktools_atom-0.4.0/src/quicktools_atom.egg-info/requires.txt +8 -0
  11. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/README.md +0 -0
  12. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/setup.cfg +0 -0
  13. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/__init__.py +0 -0
  14. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/calculus.py +0 -0
  15. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/cli.py +0 -0
  16. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/combinatorics.py +0 -0
  17. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/linalg.py +0 -0
  18. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/mathtools.py +0 -0
  19. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/numbertheory.py +0 -0
  20. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools/strtools.py +0 -0
  21. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/dependency_links.txt +0 -0
  22. {quicktools_atom-0.3.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/entry_points.txt +0 -0
  23. {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.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.3.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.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
@@ -0,0 +1,8 @@
1
+ Pillow
2
+ python-docx
3
+ pypdf
4
+ openpyxl
5
+ reportlab
6
+ PyYAML
7
+ pdf2docx
8
+ docx2pdf