Refactor packaging structure (src/core) and fix build paths

This commit is contained in:
Stu Leak 2025-11-04 04:56:46 -05:00
parent e1e851da1a
commit 9b8e63aeec
8 changed files with 28 additions and 337 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# build artefacts
pkg/
src/
*.egg-info/
*.tar.gz
*.pkg.tar.*
build/
dist/
__pycache__/
.venv/

View File

@ -1,28 +1,30 @@
# Maintainer: Stu Leak <leaktechnologies@proton.me>
# 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"
cd "$(find "$srcdir" -maxdepth 1 -type d -name "${pkgname}*" | head -n 1)"
python -m build --wheel --no-isolation
}
package() {
cd "$srcdir/$pkgname-v$pkgver"
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

View File

@ -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"]

View File

@ -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,
)

View File

@ -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 <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()

View File

@ -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}"
)

View File

@ -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)

View File

@ -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 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