Initial commit: img2pdf v1.0.0
This commit is contained in:
commit
bb94bf0b91
21
LICENSE
Normal file
21
LICENSE
Normal 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
28
PKGBUILD
Normal 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
32
README.md
Normal 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
27
pyproject.toml
Normal 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
19
src/__init__.py
Normal 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
93
src/__main__.py
Normal 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
73
src/config.py
Normal 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
56
src/converter.py
Normal 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
79
src/utils.py
Normal 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 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