Refactor packaging structure (src/core) and fix build paths
This commit is contained in:
parent
e1e851da1a
commit
9b8e63aeec
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# build artefacts
|
||||
pkg/
|
||||
src/
|
||||
*.egg-info/
|
||||
*.tar.gz
|
||||
*.pkg.tar.*
|
||||
build/
|
||||
dist/
|
||||
__pycache__/
|
||||
.venv/
|
||||
14
PKGBUILD
14
PKGBUILD
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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}"
|
||||
)
|
||||
|
|
@ -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)
|
||||
79
src/utils.py
79
src/utils.py
|
|
@ -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
|
||||
Loading…
Reference in New Issue
Block a user