Initial commit: img2pdf v1.0.0

This commit is contained in:
Stu Leak 2025-11-04 04:24:17 -05:00
commit bb94bf0b91
9 changed files with 428 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Stu Leak / Leak Technologies
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

28
PKGBUILD Normal file
View File

@ -0,0 +1,28 @@
# Maintainer: Stu Leak <leaktechnologies@proton.me>
# Leak Technologies internal packaging
pkgname=img2pdf
pkgver=1.0.0
pkgrel=1
pkgdesc="Convert a single image or a folder of images to a PDF (sorted by filename)."
arch=('any')
url="https://git.leaktechnologies.dev/Leak-Technologies/img2pdf"
license=('MIT')
depends=('python' 'python-pillow')
makedepends=('python-build' 'python-installer' 'python-setuptools' 'python-wheel')
source=("$pkgname-$pkgver.tar.gz::$url/-/archive/v$pkgver/$pkgname-v$pkgver.tar.gz")
sha256sums=('SKIP')
build() {
cd "$srcdir/$pkgname-v$pkgver"
python -m build --wheel --no-isolation
}
package() {
cd "$srcdir/$pkgname-v$pkgver"
python -m installer --destdir="$pkgdir" dist/*.whl
}
# Usage:
# makepkg -si
# img2pdf ./images -o output.pdf

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# Leak Technologies — img2pdf
**Version:** 1.0.0
**Author:** Stu Leak (<leaktechnologies@proton.me>)
**License:** MIT
---
## Overview
`img2pdf` is a lightweight, production-ready command-line utility that converts a single image or a folder of images into a PDF file.
Files are automatically sorted by filename (numerically and alphabetically), ensuring consistent page order.
Built for Arch Linux systems, fully AUR-ready, and modular enough for GUI integration later.
---
## Features
- Converts individual images or entire folders
- Auto-sorts images (natural order)
- Supports JPG, PNG, BMP, TIFF, WEBP
- Clean, colorized CLI output
- Safe overwrite handling (`--overwrite`)
- Optional quiet mode for scripting (`--quiet`)
---
## Usage
```bash
img2pdf <input_path> [-o output.pdf] [--overwrite] [--quiet]

27
pyproject.toml Normal file
View File

@ -0,0 +1,27 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "img2pdf"
version = "1.0.0"
description = "Convert a single image or folder of images into a PDF file (sorted by filename)."
readme = "README.md"
requires-python = ">=3.8"
license = { text = "MIT" }
authors = [
{ name = "Stu Leak", email = "leaktechnologies@proton.me" }
]
dependencies = [
"pillow>=10.0.0"
]
urls = {
"Homepage" = "https://git.leaktechnologies.dev/Leak-Technologies/img2pdf",
"Issues" = "https://git.leaktechnologies.dev/Leak-Technologies/img2pdf/issues"
}
[project.scripts]
img2pdf = "src.__main__:main"

19
src/__init__.py Normal file
View File

@ -0,0 +1,19 @@
"""
Leak Technologies - img2pdf
---------------------------
Modular package initializer for img2pdf.
Provides a simple image-to-PDF conversion interface for importers and external use.
Example:
from src.converter import ImageToPDFConverter
"""
from .converter import ImageToPDFConverter
from .config import (
APP_NAME,
APP_VERSION,
APP_AUTHOR,
APP_DESCRIPTION,
SUPPORTED_EXTENSIONS,
)

93
src/__main__.py Normal file
View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
Leak Technologies - img2pdf
---------------------------
Production-grade CLI tool to convert one or more images into a single PDF file.
Images are automatically sorted by filename before conversion.
Author: Stu Leak <leaktechnologies@proton.me>
Repository: https://git.leaktechnologies.dev/Leak-Technologies/img2pdf
"""
import argparse
import sys
from pathlib import Path
from src.converter import ImageToPDFConverter
from src.config import Colours, banner, DEFAULT_OUTPUT_NAME
# ------------------------------------------------------------
# ARGUMENT PARSER
# ------------------------------------------------------------
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="img2pdf",
description="Convert a single image or a folder of images into a PDF (sorted by filename)."
)
parser.add_argument(
"input",
help="Path to an image file or a directory containing images"
)
parser.add_argument(
"-o", "--output",
default=DEFAULT_OUTPUT_NAME,
help=f"Output PDF filename (default: {DEFAULT_OUTPUT_NAME})"
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Overwrite the output file if it already exists"
)
parser.add_argument(
"-q", "--quiet",
action="store_true",
help="Suppress banner output (for scripting)"
)
return parser.parse_args()
# ------------------------------------------------------------
# MAIN LOGIC
# ------------------------------------------------------------
def main() -> None:
args = parse_args()
if not args.quiet:
print(banner(), "\n")
input_path = Path(args.input)
output_pdf = Path(args.output)
if not input_path.exists():
print(f"{Colours.RED}❌ Error:{Colours.RESET} Input path does not exist — {input_path}")
sys.exit(1)
converter = ImageToPDFConverter()
try:
converter.convert(input_path, output_pdf, overwrite=args.overwrite)
print(f"{Colours.GREEN}✅ Done:{Colours.RESET} {output_pdf}")
sys.exit(0)
except FileExistsError as e:
print(f"{Colours.YELLOW}⚠️ {e}{Colours.RESET}")
sys.exit(1)
except FileNotFoundError as e:
print(f"{Colours.RED}{e}{Colours.RESET}")
sys.exit(1)
except ValueError as e:
print(f"{Colours.RED}❌ Invalid input: {e}{Colours.RESET}")
sys.exit(1)
except Exception as e:
print(f"{Colours.RED}❌ Unexpected error: {e}{Colours.RESET}")
sys.exit(1)
if __name__ == "__main__":
main()

73
src/config.py Normal file
View File

@ -0,0 +1,73 @@
"""
Leak Technologies - img2pdf Configuration
----------------------------------------
Centralized configuration constants for the img2pdf tool.
This file defines:
- Application metadata
- Supported image formats
- Default behavior settings
- CLI color codes for consistent output styling
"""
# ------------------------------------------------------------
# APPLICATION METADATA
# ------------------------------------------------------------
APP_NAME = "img2pdf"
APP_VERSION = "1.0.0"
APP_AUTHOR = "Stu Leak"
APP_EMAIL = "leaktechnologies@proton.me"
APP_LICENSE = "MIT"
APP_REPOSITORY = "https://git.leaktechnologies.dev/Leak-Technologies/img2pdf"
APP_DESCRIPTION = "Convert one or more images into a single PDF, sorted by filename."
# ------------------------------------------------------------
# SUPPORTED FORMATS
# ------------------------------------------------------------
SUPPORTED_EXTENSIONS = (
".jpg",
".jpeg",
".png",
".bmp",
".tiff",
".webp",
)
# ------------------------------------------------------------
# DEFAULT SETTINGS
# ------------------------------------------------------------
DEFAULT_OUTPUT_NAME = "output.pdf"
ALLOW_OVERWRITE_DEFAULT = False
# ------------------------------------------------------------
# TERMINAL COLORS (ANSI)
# ------------------------------------------------------------
class Colours:
RESET = "\033[0m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
BOLD = "\033[1m"
# ------------------------------------------------------------
# UTILITY FUNCTIONS
# ------------------------------------------------------------
def banner() -> str:
"""Return a simple version banner for CLI output."""
return (
f"{Colours.CYAN}{Colours.BOLD}{APP_NAME}{Colours.RESET} "
f"v{APP_VERSION} — Leak Technologies\n"
f"{Colours.YELLOW}{APP_DESCRIPTION}{Colours.RESET}"
)

56
src/converter.py Normal file
View File

@ -0,0 +1,56 @@
"""
img2pdf.converter
-----------------
Handles image-to-PDF conversion logic.
Responsible for:
- Loading and validating image paths
- Sorting images by filename
- Generating and saving the PDF file
"""
from pathlib import Path
from PIL import Image
from src.utils import get_sorted_images, ensure_overwrite_safe
class ImageToPDFConverter:
SUPPORTED_FORMATS = (".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".webp")
def convert(self, input_path: Path, output_pdf: Path, overwrite: bool = False):
"""Convert a single image or a folder of images to a PDF."""
# Ensure overwriting is handled safely
ensure_overwrite_safe(output_pdf, overwrite)
if input_path.is_file():
if not self._is_supported(input_path):
raise ValueError(f"Unsupported image format: {input_path.suffix}")
self._save_single_image(input_path, output_pdf)
print(f"✅ Created PDF from single image: {output_pdf}")
return
if input_path.is_dir():
image_files = get_sorted_images(input_path, self.SUPPORTED_FORMATS)
if not image_files:
raise FileNotFoundError("No valid image files found in directory.")
self._save_multiple_images(image_files, output_pdf)
print(f"✅ Created PDF with {len(image_files)} pages: {output_pdf}")
return
raise FileNotFoundError("Input path must be a valid file or directory.")
# --------------------
# Internal Methods
# --------------------
def _is_supported(self, path: Path) -> bool:
return path.suffix.lower() in self.SUPPORTED_FORMATS
def _save_single_image(self, path: Path, output_pdf: Path):
img = Image.open(path).convert("RGB")
img.save(output_pdf)
def _save_multiple_images(self, files: list[Path], output_pdf: Path):
images = [Image.open(f).convert("RGB") for f in files]
first, rest = images[0], images[1:]
first.save(output_pdf, save_all=True, append_images=rest)

79
src/utils.py Normal file
View File

@ -0,0 +1,79 @@
"""
img2pdf.utils
-------------
Utility functions for the img2pdf tool.
Handles:
- Safe overwrite checks
- Image file filtering and sorting
- General path validation
"""
import os
from pathlib import Path
# ------------------------------------------------------------
# IMAGE FILE DISCOVERY
# ------------------------------------------------------------
def get_sorted_images(directory: Path, valid_extensions: tuple[str, ...]) -> list[Path]:
"""
Return a sorted list of image paths within the given directory,
filtered by supported extensions and sorted naturally by filename.
Example:
1.jpg, 2.jpg, 10.jpg will be sorted as [1, 2, 10]
"""
if not directory.is_dir():
raise NotADirectoryError(f"Not a directory: {directory}")
# Filter and sort by name
files = [
f for f in directory.iterdir()
if f.suffix.lower() in valid_extensions and f.is_file()
]
files.sort(key=lambda x: _natural_sort_key(x.name))
return files
def _natural_sort_key(name: str):
"""
Sort key that ensures numeric filenames sort correctly
(e.g., 2.png before 10.png).
"""
import re
return [int(text) if text.isdigit() else text.lower() for text in re.split(r"([0-9]+)", name)]
# ------------------------------------------------------------
# OVERWRITE PROTECTION
# ------------------------------------------------------------
def ensure_overwrite_safe(path: Path, overwrite: bool = False):
"""
Ensure its safe to write to a file. If overwrite=False and the file exists,
raises FileExistsError.
"""
if path.exists() and not overwrite:
raise FileExistsError(f"Output file '{path}' already exists. Use --overwrite to replace it.")
# Create directories if needed
path.parent.mkdir(parents=True, exist_ok=True)
# ------------------------------------------------------------
# GENERAL UTILITIES
# ------------------------------------------------------------
def human_readable_size(num_bytes: int) -> str:
"""Return a human-readable file size (e.g., 12.4 MB)."""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if num_bytes < 1024.0:
return f"{num_bytes:.1f} {unit}"
num_bytes /= 1024.0
return f"{num_bytes:.1f} PB"
def validate_image_file(path: Path, valid_extensions: tuple[str, ...]) -> bool:
"""Check if a given file path is a valid supported image."""
return path.is_file() and path.suffix.lower() in valid_extensions