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/
|
||||||
18
PKGBUILD
18
PKGBUILD
|
|
@ -1,28 +1,30 @@
|
||||||
# Maintainer: Stu Leak <leaktechnologies@proton.me>
|
# Maintainer: Stu Leak <leaktechnologies@proton.me>
|
||||||
# Leak Technologies internal packaging
|
|
||||||
|
|
||||||
pkgname=img2pdf
|
pkgname=img2pdf
|
||||||
pkgver=1.0.0
|
pkgver=1.0.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Convert a single image or a folder of images to a PDF (sorted by filename)."
|
pkgdesc="Convert a single image or a folder of images to a PDF (sorted by filename)."
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://git.leaktechnologies.dev/Leak-Technologies/img2pdf"
|
url="https://git.leaktechnologies.dev/stu/img2pdf"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('python' 'python-pillow')
|
depends=('python' 'python-pillow')
|
||||||
makedepends=('python-build' 'python-installer' 'python-setuptools' 'python-wheel')
|
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')
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$srcdir/$pkgname-v$pkgver"
|
cd "$(find "$srcdir" -maxdepth 1 -type d -name "${pkgname}*" | head -n 1)"
|
||||||
python -m build --wheel --no-isolation
|
python -m build --wheel --no-isolation
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
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
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Usage:
|
# Usage:
|
||||||
# makepkg -si
|
# makepkg -si
|
||||||
# img2pdf ./images -o output.pdf
|
# img2pdf ./images -o output.pdf
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "img2pdf"
|
name = "img2pdf"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
description = "Convert a single image or folder of images into a PDF file (sorted by filename)."
|
description = "Convert a single image or folder of images into a PDF (sorted by filename)."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
license = { text = "MIT" }
|
license = "MIT"
|
||||||
|
authors = [{ name = "Stu Leak", email = "leaktechnologies@proton.me" }]
|
||||||
authors = [
|
|
||||||
{ name = "Stu Leak", email = "leaktechnologies@proton.me" }
|
|
||||||
]
|
|
||||||
|
|
||||||
dependencies = ["pillow>=10.0.0"]
|
dependencies = ["pillow>=10.0.0"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|
@ -21,4 +17,7 @@ Homepage = "https://git.leaktechnologies.dev/stu/img2pdf"
|
||||||
Issues = "https://git.leaktechnologies.dev/stu/img2pdf/issues"
|
Issues = "https://git.leaktechnologies.dev/stu/img2pdf/issues"
|
||||||
|
|
||||||
[project.scripts]
|
[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