quicktools-atom 0.2.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 (31) hide show
  1. quicktools_atom-0.4.0/PKG-INFO +59 -0
  2. quicktools_atom-0.4.0/pyproject.toml +34 -0
  3. quicktools_atom-0.4.0/src/quicktools/__init__.py +88 -0
  4. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/src/quicktools/calculus.py +51 -1
  5. quicktools_atom-0.4.0/src/quicktools/combinatorics.py +88 -0
  6. quicktools_atom-0.4.0/src/quicktools/doctools.py +89 -0
  7. quicktools_atom-0.4.0/src/quicktools/filetools.py +103 -0
  8. quicktools_atom-0.4.0/src/quicktools/imagetools.py +128 -0
  9. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/src/quicktools/linalg.py +90 -1
  10. quicktools_atom-0.4.0/src/quicktools/mathtools.py +128 -0
  11. quicktools_atom-0.4.0/src/quicktools/numbertheory.py +149 -0
  12. quicktools_atom-0.4.0/src/quicktools/pdftools.py +161 -0
  13. quicktools_atom-0.4.0/src/quicktools/spreadsheettools.py +72 -0
  14. quicktools_atom-0.4.0/src/quicktools/strtools.py +236 -0
  15. quicktools_atom-0.4.0/src/quicktools_atom.egg-info/PKG-INFO +59 -0
  16. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/SOURCES.txt +6 -0
  17. quicktools_atom-0.4.0/src/quicktools_atom.egg-info/requires.txt +8 -0
  18. quicktools_atom-0.2.0/PKG-INFO +0 -47
  19. quicktools_atom-0.2.0/pyproject.toml +0 -17
  20. quicktools_atom-0.2.0/src/quicktools/__init__.py +0 -3
  21. quicktools_atom-0.2.0/src/quicktools/combinatorics.py +0 -36
  22. quicktools_atom-0.2.0/src/quicktools/mathtools.py +0 -50
  23. quicktools_atom-0.2.0/src/quicktools/numbertheory.py +0 -61
  24. quicktools_atom-0.2.0/src/quicktools/strtools.py +0 -106
  25. quicktools_atom-0.2.0/src/quicktools_atom.egg-info/PKG-INFO +0 -47
  26. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/README.md +0 -0
  27. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/setup.cfg +0 -0
  28. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/src/quicktools/cli.py +0 -0
  29. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/dependency_links.txt +0 -0
  30. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/entry_points.txt +0 -0
  31. {quicktools_atom-0.2.0 → quicktools_atom-0.4.0}/src/quicktools_atom.egg-info/top_level.txt +0 -0
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: quicktools-atom
3
+ Version: 0.4.0
4
+ Summary: Math and string utilities with a bundled CLI, by AtomDev Studios
5
+ Author-email: Samuel Peprah <windscribe.samuel@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Samuel-Peprah-cmd/quicktools
7
+ Project-URL: Repository, https://github.com/Samuel-Peprah-cmd/quicktools
8
+ Project-URL: Documentation, https://samuel-peprah-cmd.github.io/quicktools/
9
+ Requires-Python: >=3.9
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
19
+
20
+ \# quicktools
21
+
22
+
23
+
24
+ A small toolkit with math and string utilities, plus a bundled CLI.
25
+
26
+
27
+
28
+ \## Install
29
+
30
+
31
+
32
+ &#x20; pip install quicktools-atom
33
+
34
+
35
+
36
+ \## Usage
37
+
38
+
39
+
40
+ &#x20; from quicktools import mathtools, strtools
41
+
42
+
43
+
44
+ &#x20; mathtools.is\_prime(97)
45
+
46
+ &#x20; strtools.slugify("Hello World")
47
+
48
+
49
+
50
+ \## CLI
51
+
52
+
53
+
54
+ &#x20; quicktools is-prime 97
55
+
56
+ &#x20; quicktools stats 1 2 3 4 5
57
+
58
+ &#x20; quicktools slugify "Hello World"
59
+
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quicktools-atom"
7
+ version = "0.4.0"
8
+ description = "Math and string utilities with a bundled CLI, by AtomDev Studios"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [
12
+ { name = "Samuel Peprah", email = "windscribe.samuel@gmail.com" }
13
+ ]
14
+ dependencies = [
15
+ "Pillow",
16
+ "python-docx",
17
+ "pypdf",
18
+ "openpyxl",
19
+ "reportlab",
20
+ "PyYAML",
21
+ "pdf2docx",
22
+ "docx2pdf",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/Samuel-Peprah-cmd/quicktools"
27
+ Repository = "https://github.com/Samuel-Peprah-cmd/quicktools"
28
+ Documentation = "https://samuel-peprah-cmd.github.io/quicktools/"
29
+
30
+ [project.scripts]
31
+ quicktools = "quicktools.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
@@ -0,0 +1,88 @@
1
+ """
2
+ <p align="center">
3
+ <img src="assets/AtomDev_Studios.png" alt="AtomDev Studios" width="320" style="border-radius: 8px;">
4
+ </p>
5
+
6
+ # quicktools
7
+
8
+ A math and string-manipulation toolkit covering linear algebra, calculus,
9
+ number theory, combinatorics, and text processing — built by **Samuel Peprah**
10
+ under **AtomDev Studios**.
11
+
12
+ ---
13
+
14
+ ## About Samuel Peprah
15
+
16
+ <p align="center">
17
+ <img src="assets/sam.png" alt="Samuel Peprah" width="160" style="border-radius: 50%; object-fit: cover;">
18
+ </p>
19
+
20
+ Samuel Peprah is a software developer, web application developer, and
21
+ Occupational Therapy student at the University of Ghana. Alongside his
22
+ healthcare education, he is also studying Computer Science at the University
23
+ of the People, combining knowledge from both fields to develop practical
24
+ technology solutions.
25
+
26
+ His interests span full-stack web development, software engineering,
27
+ automation, artificial intelligence, cybersecurity, and health technology.
28
+ He enjoys building scalable applications that solve real-world problems and
29
+ continuously explores new technologies to improve his skills.
30
+
31
+ Samuel has developed educational platforms, business management systems,
32
+ attendance systems, healthcare applications, and AI-powered solutions. He is
33
+ passionate about creating software that is modern, efficient, secure, and
34
+ user-friendly.
35
+
36
+ ## About AtomDev Studios
37
+
38
+ AtomDev Studios is the personal software development brand founded by Samuel
39
+ Peprah. The studio specializes in designing and developing modern websites,
40
+ web applications, business management systems, educational platforms, and
41
+ custom software solutions for individuals, startups, schools, and
42
+ organizations.
43
+
44
+ The mission of AtomDev Studios is to transform ideas into reliable, scalable,
45
+ and innovative digital products that solve real-world problems while
46
+ delivering exceptional user experiences.
47
+
48
+ ### Areas of Expertise
49
+
50
+ - Full-Stack Web Development
51
+ - Custom Web Applications
52
+ - REST API Development
53
+ - Database Design & Management
54
+ - Educational Technology Platforms
55
+ - Healthcare Technology Solutions
56
+ - AI Integration & Automation
57
+ - UI/UX Design
58
+ - Cloud Deployment & Hosting
59
+ - Website Maintenance & Support
60
+
61
+ ### Technologies
62
+
63
+ Python • Flask • JavaScript • HTML • CSS • Tailwind CSS • Bootstrap •
64
+ PostgreSQL • SQLite • Git • GitHub • Cloudflare • Docker
65
+
66
+ ### Connect
67
+
68
+ - LinkedIn: [samuel-peprah-a63598347](https://www.linkedin.com/in/samuel-peprah-a63598347)
69
+ - X (Twitter): [@legend_consult](https://x.com/legend_consult)
70
+ - TikTok: [@legendinlearning](https://www.tiktok.com/@legendinlearning)
71
+ - Instagram: [@ksapeprah](https://www.instagram.com/ksapeprah)
72
+ - GitHub: [Samuel-Peprah-cmd](https://github.com/Samuel-Peprah-cmd)
73
+
74
+ > "Building innovative software solutions that bridge technology with real-world impact."
75
+
76
+ ---
77
+
78
+ ## Modules
79
+
80
+ - `mathtools` — primes, GCD/LCM, mean, median, standard deviation
81
+ - `strtools` — palindromes, slugify, edit distance, anagrams, ciphers
82
+ - `linalg` — matrix operations, determinants, inverses, linear systems
83
+ - `calculus` — derivatives, integrals, Taylor series, Newton's method
84
+ - `numbertheory` — extended GCD, modular inverse, sieve, prime factorization
85
+ - `combinatorics` — permutations, combinations, Catalan numbers, Fibonacci
86
+ """
87
+
88
+ __version__ = "0.2.0"
@@ -62,4 +62,54 @@ def newtons_method(f: Callable[[float], float], x0: float, tol: float = 1e-10, m
62
62
  if dfx == 0:
63
63
  raise ZeroDivisionError("Derivative is zero; Newton's method failed")
64
64
  x = x - fx / dfx
65
- return x
65
+ return x
66
+
67
+ def bisection_method(f, a: float, b: float, tol: float = 1e-10, max_iter: int = 200) -> float:
68
+ """Find a root of f within [a, b] using the bisection method. Requires f(a) and f(b) to have opposite signs."""
69
+ if f(a) * f(b) > 0:
70
+ raise ValueError("f(a) and f(b) must have opposite signs")
71
+ for _ in range(max_iter):
72
+ mid = (a + b) / 2
73
+ if abs(f(mid)) < tol or (b - a) / 2 < tol:
74
+ return mid
75
+ if f(a) * f(mid) < 0:
76
+ b = mid
77
+ else:
78
+ a = mid
79
+ return (a + b) / 2
80
+
81
+
82
+ def secant_method(f, x0: float, x1: float, tol: float = 1e-10, max_iter: int = 200) -> float:
83
+ """Find a root of f near x0 and x1 using the secant method (no derivative required)."""
84
+ for _ in range(max_iter):
85
+ fx0, fx1 = f(x0), f(x1)
86
+ if fx1 - fx0 == 0:
87
+ raise ZeroDivisionError("Secant method failed: division by zero")
88
+ x2 = x1 - fx1 * (x1 - x0) / (fx1 - fx0)
89
+ if abs(x2 - x1) < tol:
90
+ return x2
91
+ x0, x1 = x1, x2
92
+ return x1
93
+
94
+
95
+ def partial_derivative(f, point: list[float], index: int, h: float = 1e-6) -> float:
96
+ """Approximate the partial derivative of a multivariable function f at `point`,
97
+ with respect to the variable at position `index`."""
98
+ point_plus = list(point)
99
+ point_minus = list(point)
100
+ point_plus[index] += h
101
+ point_minus[index] -= h
102
+ return (f(*point_plus) - f(*point_minus)) / (2 * h)
103
+
104
+
105
+ def arc_length(f, a: float, b: float, n: int = 1000) -> float:
106
+ """Approximate the arc length of y = f(x) from x=a to x=b."""
107
+ def integrand(x: float) -> float:
108
+ return math.sqrt(1 + derivative(f, x) ** 2)
109
+ return integral_simpson(integrand, a, b, n)
110
+
111
+
112
+ def limit_approximation(f, x: float, h: float = 1e-6) -> float:
113
+ """Approximate the limit of f(t) as t approaches x, by averaging values just before and after x
114
+ (useful for functions undefined exactly at x, e.g. removable discontinuities)."""
115
+ return (f(x - h) + f(x + h)) / 2
@@ -0,0 +1,88 @@
1
+ """Combinatorics: permutations, combinations, and counting sequences."""
2
+ import math
3
+
4
+
5
+ def permutations_count(n: int, r: int) -> int:
6
+ """Number of ways to arrange r items out of n, order matters: nPr."""
7
+ if r > n or r < 0:
8
+ return 0
9
+ return math.factorial(n) // math.factorial(n - r)
10
+
11
+
12
+ def combinations_count(n: int, r: int) -> int:
13
+ """Number of ways to choose r items out of n, order doesn't matter: nCr."""
14
+ if r > n or r < 0:
15
+ return 0
16
+ return math.factorial(n) // (math.factorial(r) * math.factorial(n - r))
17
+
18
+
19
+ def catalan_number(n: int) -> int:
20
+ """The nth Catalan number — counts things like valid bracket sequences and binary tree shapes."""
21
+ return combinations_count(2 * n, n) // (n + 1)
22
+
23
+
24
+ def fibonacci(n: int) -> int:
25
+ """The nth Fibonacci number (0-indexed: fibonacci(0) == 0)."""
26
+ if n < 0:
27
+ raise ValueError("n must be non-negative")
28
+ a, b = 0, 1
29
+ for _ in range(n):
30
+ a, b = b, a + b
31
+ return a
32
+
33
+
34
+ def binomial_expansion_coeffs(n: int) -> list[int]:
35
+ """Coefficients of (x + y)^n, i.e. row n of Pascal's triangle."""
36
+ return [combinations_count(n, k) for k in range(n + 1)]
37
+
38
+ def multinomial_coefficient(n: int, ks: list[int]) -> int:
39
+ """Number of ways to divide n items into groups of sizes ks (which must sum to n)."""
40
+ if sum(ks) != n:
41
+ raise ValueError("The group sizes must sum to n")
42
+ result = math.factorial(n)
43
+ for k in ks:
44
+ result //= math.factorial(k)
45
+ return result
46
+
47
+
48
+ def stirling_second_kind(n: int, k: int) -> int:
49
+ """Number of ways to partition a set of n objects into k non-empty subsets."""
50
+ if k == 0:
51
+ return 1 if n == 0 else 0
52
+ if k > n or n < 0:
53
+ return 0
54
+ if k == n:
55
+ return 1
56
+ total = 0
57
+ for j in range(k + 1):
58
+ sign = (-1) ** (k - j)
59
+ total += sign * combinations_count(k, j) * (j ** n)
60
+ return total // math.factorial(k)
61
+
62
+
63
+ def derangements_count(n: int) -> int:
64
+ """Number of permutations of n items where no item ends up in its original position."""
65
+ if n == 0:
66
+ return 1
67
+ if n == 1:
68
+ return 0
69
+ a, b = 1, 0
70
+ for i in range(2, n + 1):
71
+ a, b = b, (i - 1) * (a + b)
72
+ return b
73
+
74
+
75
+ def integer_partitions_count(n: int) -> int:
76
+ """Number of ways to write n as a sum of positive integers, order not mattering."""
77
+ if n < 0:
78
+ return 0
79
+ partitions = [1] + [0] * n
80
+ for k in range(1, n + 1):
81
+ for i in range(k, n + 1):
82
+ partitions[i] += partitions[i - k]
83
+ return partitions[n]
84
+
85
+
86
+ def combinations_with_replacement_count(n: int, r: int) -> int:
87
+ """Number of ways to choose r items from n types, with repetition allowed and order not mattering."""
88
+ return combinations_count(n + r - 1, r)
@@ -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)