Migrate from jpegio to jpeglib for Python 3.13+ support

- Replace jpegio with jpeglib (jpeglib.to_jpegio compatibility layer)
- Update Python requirement to >=3.11, add 3.13/3.14 classifiers
- AUR: Add install script for user creation and permissions
- AUR: Install frontends to site-packages, create Flask instance dir
- AUR: Use dynamic ${pyver} for systemd WorkingDirectory

Tested: CLI, Web UI (Gunicorn), API (Uvicorn), DCT jpeglib roundtrip

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-10 20:09:52 -05:00
parent 530e5debef
commit 71088989f3
4 changed files with 80 additions and 38 deletions

View File

@@ -1,31 +1,31 @@
# Maintainer: Aaron D. Lee <your-email@example.com> # Maintainer: Aaron D. Lee <your-email@example.com>
pkgname=stegasoo-git pkgname=stegasoo-git
pkgver=4.1.7.r0.g1acb5a3 pkgver=4.2.0.r0.g530e5de
pkgrel=1 pkgrel=1
pkgdesc="Secure steganography with hybrid photo + passphrase + PIN authentication" pkgdesc="Secure steganography with hybrid photo + passphrase + PIN authentication"
arch=('x86_64') arch=('x86_64')
url="https://github.com/adlee-was-taken/stegasoo" url="https://github.com/adlee-was-taken/stegasoo"
license=('MIT') license=('MIT')
# NOTE: Requires Python 3.12 (jpegio not compatible with 3.13 yet) # Python 3.11-3.14 supported (uses jpeglib for modern Python compatibility)
depends=( depends=(
'python312' # AUR package - jpegio requires 3.12 'python>=3.11'
) )
makedepends=( makedepends=(
'git' 'git'
'python312' 'python'
'python-build'
'python-hatchling'
) )
optdepends=( optdepends=(
'zbar: QR code reading from webcam/images' 'zbar: QR code reading from webcam/images'
) )
provides=('stegasoo') provides=('stegasoo')
conflicts=('stegasoo') conflicts=('stegasoo')
install=stegasoo-git.install
source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main") source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main")
sha256sums=('SKIP') sha256sums=('SKIP')
# Python 3.12 from AUR package
_python="/usr/bin/python3.12"
pkgver() { pkgver() {
cd "$pkgname" cd "$pkgname"
git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \ git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \
@@ -34,32 +34,32 @@ pkgver() {
build() { build() {
cd "$pkgname" cd "$pkgname"
python -m build --wheel --no-isolation
echo "Using Python: $_python ($($_python --version))"
# Bootstrap pip for python312
$_python -m ensurepip --user --upgrade
# Install build dependencies
$_python -m pip install --user build hatchling
# Build wheel
$_python -m build --wheel --no-isolation
} }
package() { package() {
cd "$pkgname" cd "$pkgname"
# Detect Python version for site-packages path
local pyver=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
# Install to /opt/stegasoo with dedicated venv # Install to /opt/stegasoo with dedicated venv
install -dm755 "$pkgdir/opt/stegasoo" install -dm755 "$pkgdir/opt/stegasoo"
# Create fresh venv in package # Create fresh venv in package
$_python -m venv "$pkgdir/opt/stegasoo/venv" python -m venv "$pkgdir/opt/stegasoo/venv"
# Install the wheel with all extras # Install the wheel with all extras
local wheel=$(ls dist/*.whl | head -1) local wheel=$(ls dist/*.whl | head -1)
"$pkgdir/opt/stegasoo/venv/bin/pip" install --no-cache-dir "${wheel}[all]" "$pkgdir/opt/stegasoo/venv/bin/pip" install --no-cache-dir "${wheel}[all]"
# Install frontends (not included in wheel)
local site_packages="$pkgdir/opt/stegasoo/venv/lib/python${pyver}/site-packages"
cp -r frontends "$site_packages/"
# Create instance directory for Flask (writable by stegasoo user)
install -dm755 "$pkgdir/opt/stegasoo/venv/var/app-instance"
# Fix shebangs - replace build-time paths with installed paths # Fix shebangs - replace build-time paths with installed paths
find "$pkgdir/opt/stegasoo/venv/bin" -type f -exec \ find "$pkgdir/opt/stegasoo/venv/bin" -type f -exec \
sed -i "s|$pkgdir/opt/stegasoo/venv|/opt/stegasoo/venv|g" {} \; sed -i "s|$pkgdir/opt/stegasoo/venv|/opt/stegasoo/venv|g" {} \;
@@ -86,7 +86,7 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
User=stegasoo User=stegasoo
WorkingDirectory=/opt/stegasoo/venv/lib/python3.12/site-packages/frontends/web WorkingDirectory=/opt/stegasoo/venv/lib/python${pyver}/site-packages/frontends/web
Environment="PATH=/opt/stegasoo/venv/bin" Environment="PATH=/opt/stegasoo/venv/bin"
ExecStart=/opt/stegasoo/venv/bin/gunicorn -b 127.0.0.1:5000 app:app ExecStart=/opt/stegasoo/venv/bin/gunicorn -b 127.0.0.1:5000 app:app
Restart=on-failure Restart=on-failure
@@ -104,7 +104,7 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
User=stegasoo User=stegasoo
WorkingDirectory=/opt/stegasoo/venv/lib/python3.12/site-packages/frontends/api WorkingDirectory=/opt/stegasoo/venv/lib/python${pyver}/site-packages/frontends/api
Environment="PATH=/opt/stegasoo/venv/bin" Environment="PATH=/opt/stegasoo/venv/bin"
ExecStart=/opt/stegasoo/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 ExecStart=/opt/stegasoo/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000
Restart=on-failure Restart=on-failure

40
aur/stegasoo-git.install Normal file
View File

@@ -0,0 +1,40 @@
post_install() {
# Create stegasoo system user if it doesn't exist
if ! getent passwd stegasoo >/dev/null; then
useradd -r -s /usr/bin/nologin -d /opt/stegasoo stegasoo
echo "Created system user 'stegasoo'"
fi
# Set ownership of instance directory for Flask
chown -R stegasoo:stegasoo /opt/stegasoo/venv/var/app-instance 2>/dev/null || true
echo ""
echo "Stegasoo installed successfully!"
echo ""
echo "CLI usage:"
echo " stegasoo --help"
echo ""
echo "To start the web UI:"
echo " sudo systemctl start stegasoo-web"
echo ""
echo "To start the REST API:"
echo " sudo systemctl start stegasoo-api"
echo ""
}
post_upgrade() {
post_install
}
pre_remove() {
# Stop services if running
systemctl stop stegasoo-web 2>/dev/null || true
systemctl stop stegasoo-api 2>/dev/null || true
}
post_remove() {
# Optionally remove the stegasoo user
# userdel stegasoo 2>/dev/null || true
echo "Stegasoo removed. User 'stegasoo' was not removed."
echo "To remove: userdel stegasoo"
}

View File

@@ -8,7 +8,7 @@ version = "4.2.0"
description = "Secure steganography with hybrid photo + passphrase + PIN authentication" description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
requires-python = ">=3.10" requires-python = ">=3.11"
authors = [ authors = [
{ name = "Aaron D. Lee" } { name = "Aaron D. Lee" }
] ]
@@ -29,9 +29,10 @@ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Security :: Cryptography", "Topic :: Security :: Cryptography",
"Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics",
] ]
@@ -48,7 +49,7 @@ dependencies = [
dct = [ dct = [
"numpy>=2.0.0", "numpy>=2.0.0",
"scipy>=1.10.0", "scipy>=1.10.0",
"jpegio>=0.2.0", "jpeglib>=1.0.0",
"reedsolo>=1.7.0", "reedsolo>=1.7.0",
] ]
cli = [ cli = [
@@ -69,7 +70,7 @@ web = [
# Include DCT support for web UI # Include DCT support for web UI
"numpy>=2.0.0", "numpy>=2.0.0",
"scipy>=1.10.0", "scipy>=1.10.0",
"jpegio>=0.2.0", "jpeglib>=1.0.0",
"reedsolo>=1.7.0", "reedsolo>=1.7.0",
] ]
api = [ api = [
@@ -81,7 +82,7 @@ api = [
# Include DCT support for API # Include DCT support for API
"numpy>=2.0.0", "numpy>=2.0.0",
"scipy>=1.10.0", "scipy>=1.10.0",
"jpegio>=0.2.0", "jpeglib>=1.0.0",
"reedsolo>=1.7.0", "reedsolo>=1.7.0",
] ]
all = [ all = [
@@ -120,7 +121,7 @@ addopts = "-v --cov=stegasoo --cov-report=term-missing"
[tool.black] [tool.black]
line-length = 100 line-length = 100
target-version = ["py310", "py311", "py312"] target-version = ["py311", "py312", "py313"]
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
@@ -137,7 +138,7 @@ ignore = ["E501"]
"src/stegasoo/__init__.py" = ["E402"] "src/stegasoo/__init__.py" = ["E402"]
[tool.mypy] [tool.mypy]
python_version = "3.10" python_version = "3.11"
warn_return_any = true warn_return_any = true
warn_unused_configs = true warn_unused_configs = true
ignore_missing_imports = true ignore_missing_imports = true

View File

@@ -12,7 +12,7 @@ Why is this cool?
Two approaches depending on what you want: Two approaches depending on what you want:
1. PNG output: We do our own DCT math via scipy (works on any image) 1. PNG output: We do our own DCT math via scipy (works on any image)
2. JPEG output: We use jpegio to directly tweak the coefficients (chef's kiss) 2. JPEG output: We use jpeglib to directly tweak the coefficients (chef's kiss)
v4.1.0 - The "please stop corrupting my data" release: v4.1.0 - The "please stop corrupting my data" release:
- Reed-Solomon error correction (can fix up to 16 byte errors per chunk) - Reed-Solomon error correction (can fix up to 16 byte errors per chunk)
@@ -24,7 +24,7 @@ v3.2.0-patch2 - The "scipy why are you like this" release:
- Process blocks one at a time with fresh arrays - Process blocks one at a time with fresh arrays
- Yes, it's slower. No, I don't care. Correctness > speed. - Yes, it's slower. No, I don't care. Correctness > speed.
Requires: scipy (PNG mode), optionally jpegio (JPEG mode), reedsolo (error correction) Requires: scipy (PNG mode), optionally jpeglib (JPEG mode), reedsolo (error correction)
""" """
import gc import gc
@@ -55,14 +55,15 @@ except ImportError:
dctn = None dctn = None
idctn = None idctn = None
# Check for jpegio availability (for proper JPEG mode) # Check for jpeglib availability (for proper JPEG mode)
# jpeglib replaces jpegio for Python 3.13+ compatibility
try: try:
import jpegio as jio import jpeglib
HAS_JPEGIO = True HAS_JPEGIO = True # Keep variable name for compatibility
except ImportError: except ImportError:
HAS_JPEGIO = False HAS_JPEGIO = False
jio = None jpeglib = None
# Import custom exceptions # Import custom exceptions
from .exceptions import InvalidMagicBytesError from .exceptions import InvalidMagicBytesError
@@ -742,7 +743,7 @@ def estimate_capacity_comparison(image_data: bytes) -> dict:
}, },
"jpeg_native": { "jpeg_native": {
"available": HAS_JPEGIO, "available": HAS_JPEGIO,
"note": "Uses jpegio for proper JPEG coefficient embedding", "note": "Uses jpeglib for proper JPEG coefficient embedding",
}, },
} }
@@ -1082,7 +1083,7 @@ def _embed_jpegio(
flags = FLAG_COLOR_MODE if color_mode == "color" else 0 flags = FLAG_COLOR_MODE if color_mode == "color" else 0
try: try:
jpeg = jio.read(input_path) jpeg = jpeglib.to_jpegio(jpeglib.read_dct(input_path))
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL] coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
all_positions = _jpegio_get_usable_positions(coef_array) all_positions = _jpegio_get_usable_positions(coef_array)
@@ -1144,7 +1145,7 @@ def _embed_jpegio(
if progress_file: if progress_file:
_write_progress(progress_file, total_bits, total_bits, "saving") _write_progress(progress_file, total_bits, total_bits, "saving")
jio.write(jpeg, output_path) jpeg.write(output_path)
with open(output_path, "rb") as f: with open(output_path, "rb") as f:
stego_bytes = f.read() stego_bytes = f.read()
@@ -1392,7 +1393,7 @@ def _extract_jpegio(
temp_path = _jpegio_bytes_to_file(stego_image, suffix=".jpg") temp_path = _jpegio_bytes_to_file(stego_image, suffix=".jpg")
try: try:
jpeg = jio.read(temp_path) jpeg = jpeglib.to_jpegio(jpeglib.read_dct(temp_path))
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL] coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
all_positions = _jpegio_get_usable_positions(coef_array) all_positions = _jpegio_get_usable_positions(coef_array)