From 9b8e63aeecc9fccbe437a8bd56c0d21b3068ab80 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Tue, 4 Nov 2025 04:56:46 -0500 Subject: [PATCH] Refactor packaging structure (src/core) and fix build paths --- .gitignore | 10 ++++++ PKGBUILD | 18 +++++----- pyproject.toml | 17 +++++---- src/__init__.py | 19 ---------- src/__main__.py | 93 ------------------------------------------------ src/config.py | 73 ------------------------------------- src/converter.py | 56 ----------------------------- src/utils.py | 79 ---------------------------------------- 8 files changed, 28 insertions(+), 337 deletions(-) create mode 100644 .gitignore delete mode 100644 src/__init__.py delete mode 100644 src/__main__.py delete mode 100644 src/config.py delete mode 100644 src/converter.py delete mode 100644 src/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b0638a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# build artefacts +pkg/ +src/ +*.egg-info/ +*.tar.gz +*.pkg.tar.* +build/ +dist/ +__pycache__/ +.venv/ diff --git a/PKGBUILD b/PKGBUILD index 0b732b8..811a41c 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,28 +1,30 @@ # Maintainer: Stu Leak -# Leak Technologies internal packaging pkgname=img2pdf -pkgver=1.0.0 +pkgver=1.0.1 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" +url="https://git.leaktechnologies.dev/stu/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") + +# Gitea tag archive (works after you push tag v$pkgver) +source=("$pkgname-$pkgver.tar.gz::https://git.leaktechnologies.dev/stu/img2pdf/archive/v$pkgver.tar.gz") sha256sums=('SKIP') build() { - cd "$srcdir/$pkgname-v$pkgver" - python -m build --wheel --no-isolation + cd "$(find "$srcdir" -maxdepth 1 -type d -name "${pkgname}*" | head -n 1)" + python -m build --wheel --no-isolation } package() { - cd "$srcdir/$pkgname-v$pkgver" - python -m installer --destdir="$pkgdir" dist/*.whl + cd "$(find "$srcdir" -maxdepth 1 -type d -name "${pkgname}*" | head -n 1)" + python -m installer --destdir="$pkgdir" dist/*.whl } + # Usage: # makepkg -si # img2pdf ./images -o output.pdf diff --git a/pyproject.toml b/pyproject.toml index fc4ec17..25e0992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,16 +4,12 @@ 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)." +version = "1.0.1" +description = "Convert a single image or folder of images into a PDF (sorted by filename)." readme = "README.md" requires-python = ">=3.8" -license = { text = "MIT" } - -authors = [ - { name = "Stu Leak", email = "leaktechnologies@proton.me" } -] - +license = "MIT" +authors = [{ name = "Stu Leak", email = "leaktechnologies@proton.me" }] dependencies = ["pillow>=10.0.0"] [project.urls] @@ -21,4 +17,7 @@ Homepage = "https://git.leaktechnologies.dev/stu/img2pdf" Issues = "https://git.leaktechnologies.dev/stu/img2pdf/issues" [project.scripts] -img2pdf = "src.__main__:main" +img2pdf = "core.__main__:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index 1ae7163..0000000 --- a/src/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -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, -) diff --git a/src/__main__.py b/src/__main__.py deleted file mode 100644 index cdd63cb..0000000 --- a/src/__main__.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/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 -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() diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 04575bd..0000000 --- a/src/config.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -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}" - ) diff --git a/src/converter.py b/src/converter.py deleted file mode 100644 index 4cb728e..0000000 --- a/src/converter.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -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) diff --git a/src/utils.py b/src/utils.py deleted file mode 100644 index fe9a1ed..0000000 --- a/src/utils.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -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 it’s 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