Lint cleanup: ruff fixes across entire codebase

- Strip trailing whitespace from all Python files
- Fix import sorting (I001) across all modules
- Convert Optional[X] to X | None syntax (UP045)
- Remove unused imports (F401)
- Convert lambda assignments to def functions (E731)
- Add TYPE_CHECKING import for forward references
- Update pyproject.toml ruff config:
  - Move select/ignore to [tool.ruff.lint] section
  - Add per-file ignores for DCT colorspace naming (N803/N806)
  - Add per-file ignores for __init__.py import structure (E402)
  - Exclude defunct test_routes.py
- Remove frontends/web/test_routes.py (defunct debug snippet)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-02 17:17:38 -05:00
parent d94ee7be90
commit 6b21190f97
36 changed files with 2275 additions and 2383 deletions

View File

@@ -14,12 +14,11 @@ v3.2.0-patch2 Changes:
Requires: scipy (for PNG mode), optionally jpegio (for JPEG mode)
"""
import gc
import hashlib
import io
import struct
import hashlib
import gc
from dataclasses import dataclass
from typing import Optional, Tuple
from enum import Enum
import numpy as np
@@ -103,7 +102,7 @@ class DCTEmbedStats:
color_mode: str = 'grayscale'
@dataclass
@dataclass
class DCTCapacityInfo:
width: int
height: int
@@ -147,19 +146,19 @@ def _safe_dct2(block: np.ndarray) -> np.ndarray:
"""
# Create a brand new array (not a view)
safe_block = np.array(block, dtype=np.float64, copy=True, order='C')
# First DCT on columns (transpose -> DCT rows -> transpose back)
temp = np.zeros_like(safe_block, dtype=np.float64, order='C')
for i in range(BLOCK_SIZE):
col = np.array(safe_block[:, i], dtype=np.float64, copy=True)
temp[:, i] = dct(col, norm='ortho')
# Second DCT on rows
result = np.zeros_like(temp, dtype=np.float64, order='C')
for i in range(BLOCK_SIZE):
row = np.array(temp[i, :], dtype=np.float64, copy=True)
result[i, :] = dct(row, norm='ortho')
return result
@@ -170,19 +169,19 @@ def _safe_idct2(block: np.ndarray) -> np.ndarray:
"""
# Create a brand new array (not a view)
safe_block = np.array(block, dtype=np.float64, copy=True, order='C')
# First IDCT on rows
temp = np.zeros_like(safe_block, dtype=np.float64, order='C')
for i in range(BLOCK_SIZE):
row = np.array(safe_block[i, :], dtype=np.float64, copy=True)
temp[i, :] = idct(row, norm='ortho')
# Second IDCT on columns
result = np.zeros_like(temp, dtype=np.float64, order='C')
for i in range(BLOCK_SIZE):
col = np.array(temp[:, i], dtype=np.float64, copy=True)
result[:, i] = idct(col, norm='ortho')
return result
@@ -200,23 +199,23 @@ def _extract_y_channel(image_data: bytes) -> np.ndarray:
img = Image.open(io.BytesIO(image_data))
if img.mode != 'RGB':
img = img.convert('RGB')
rgb = np.array(img, dtype=np.float64, copy=True, order='C')
Y = 0.299 * rgb[:, :, 0] + 0.587 * rgb[:, :, 1] + 0.114 * rgb[:, :, 2]
return np.array(Y, dtype=np.float64, copy=True, order='C')
def _pad_to_blocks(image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
h, w = image.shape
new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
new_w = ((w + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
if new_h == h and new_w == w:
return np.array(image, dtype=np.float64, copy=True, order='C'), (h, w)
padded = np.zeros((new_h, new_w), dtype=np.float64, order='C')
padded[:h, :w] = image
# Simple edge replication for padding
if new_h > h:
for i in range(h, new_h):
@@ -226,11 +225,11 @@ def _pad_to_blocks(image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
padded[:h, j] = padded[:h, w-1]
if new_h > h and new_w > w:
padded[h:, w:] = padded[h-1, w-1]
return padded, (h, w)
def _unpad_image(image: np.ndarray, original_size: Tuple[int, int]) -> np.ndarray:
def _unpad_image(image: np.ndarray, original_size: tuple[int, int]) -> np.ndarray:
h, w = original_size
return np.array(image[:h, :w], dtype=np.float64, copy=True, order='C')
@@ -263,7 +262,7 @@ def _save_stego_image(image: np.ndarray, output_format: str = OUTPUT_FORMAT_PNG)
img = Image.fromarray(clipped, mode='L')
buffer = io.BytesIO()
if output_format == OUTPUT_FORMAT_JPEG:
img.save(buffer, format='JPEG', quality=JPEG_OUTPUT_QUALITY,
img.save(buffer, format='JPEG', quality=JPEG_OUTPUT_QUALITY,
subsampling=0, optimize=True)
else:
img.save(buffer, format='PNG', optimize=True)
@@ -282,15 +281,15 @@ def _save_color_image(rgb_array: np.ndarray, output_format: str = OUTPUT_FORMAT_
return buffer.getvalue()
def _rgb_to_ycbcr(rgb: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
def _rgb_to_ycbcr(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
R = rgb[:, :, 0].astype(np.float64)
G = rgb[:, :, 1].astype(np.float64)
B = rgb[:, :, 2].astype(np.float64)
Y = np.array(0.299 * R + 0.587 * G + 0.114 * B, dtype=np.float64, copy=True, order='C')
Cb = np.array(128 - 0.168736 * R - 0.331264 * G + 0.5 * B, dtype=np.float64, copy=True, order='C')
Cr = np.array(128 + 0.5 * R - 0.418688 * G - 0.081312 * B, dtype=np.float64, copy=True, order='C')
return Y, Cb, Cr
@@ -298,7 +297,7 @@ def _ycbcr_to_rgb(Y: np.ndarray, Cb: np.ndarray, Cr: np.ndarray) -> np.ndarray:
R = Y + 1.402 * (Cr - 128)
G = Y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128)
B = Y + 1.772 * (Cb - 128)
rgb = np.zeros((Y.shape[0], Y.shape[1], 3), dtype=np.float64, order='C')
rgb[:, :, 0] = R
rgb[:, :, 1] = G
@@ -310,20 +309,20 @@ def _create_header(data_length: int, flags: int = 0) -> bytes:
return struct.pack('>4sBBI', DCT_MAGIC, 1, flags, data_length)
def _parse_header(header_bits: list) -> Tuple[int, int, int]:
def _parse_header(header_bits: list) -> tuple[int, int, int]:
if len(header_bits) < HEADER_SIZE * 8:
raise ValueError("Insufficient header data")
header_bytes = bytes([
sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(HEADER_SIZE)
])
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes)
if magic != DCT_MAGIC:
raise ValueError("Invalid DCT stego magic bytes")
return version, flags, length
@@ -332,8 +331,8 @@ def _parse_header(header_bits: list) -> Tuple[int, int, int]:
# ============================================================================
def _jpegio_bytes_to_file(data: bytes, suffix: str = '.jpg') -> str:
import tempfile
import os
import tempfile
fd, path = tempfile.mkstemp(suffix=suffix)
try:
os.write(fd, data)
@@ -366,7 +365,7 @@ def _jpegio_create_header(data_length: int, flags: int = 0) -> bytes:
return struct.pack('>4sBBI', JPEGIO_MAGIC, 1, flags, data_length)
def _jpegio_parse_header(header_bytes: bytes) -> Tuple[int, int, int]:
def _jpegio_parse_header(header_bytes: bytes) -> tuple[int, int, int]:
if len(header_bytes) < HEADER_SIZE:
raise ValueError("Insufficient header data")
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes[:HEADER_SIZE])
@@ -382,21 +381,21 @@ def _jpegio_parse_header(header_bytes: bytes) -> Tuple[int, int, int]:
def calculate_dct_capacity(image_data: bytes) -> DCTCapacityInfo:
"""Calculate DCT embedding capacity of an image."""
_check_scipy()
# Just get dimensions, don't process anything
img = Image.open(io.BytesIO(image_data))
width, height = img.size
img.close() # Explicitly close
blocks_x = width // BLOCK_SIZE
blocks_y = height // BLOCK_SIZE
total_blocks = blocks_x * blocks_y
bits_per_block = len(DEFAULT_EMBED_POSITIONS)
total_bits = total_blocks * bits_per_block
total_bytes = total_bits // 8
usable_bytes = max(0, total_bytes - HEADER_SIZE)
return DCTCapacityInfo(
width=width,
height=height,
@@ -420,13 +419,13 @@ def estimate_capacity_comparison(image_data: bytes) -> dict:
img = Image.open(io.BytesIO(image_data))
width, height = img.size
img.close()
pixels = width * height
lsb_bytes = (pixels * 3) // 8
blocks = (width // 8) * (height // 8)
dct_bytes = (blocks * 16) // 8 - HEADER_SIZE
return {
'width': width,
'height': height,
@@ -455,17 +454,17 @@ def embed_in_dct(
seed: bytes,
output_format: str = OUTPUT_FORMAT_PNG,
color_mode: str = 'color',
) -> Tuple[bytes, DCTEmbedStats]:
) -> tuple[bytes, DCTEmbedStats]:
"""Embed data using DCT coefficient modification."""
if output_format not in (OUTPUT_FORMAT_PNG, OUTPUT_FORMAT_JPEG):
raise ValueError(f"Invalid output format: {output_format}")
if color_mode not in ('color', 'grayscale'):
color_mode = 'color'
if output_format == OUTPUT_FORMAT_JPEG and HAS_JPEGIO:
return _embed_jpegio(data, carrier_image, seed, color_mode)
_check_scipy()
return _embed_scipy_dct_safe(data, carrier_image, seed, output_format, color_mode)
@@ -476,27 +475,27 @@ def _embed_scipy_dct_safe(
seed: bytes,
output_format: str,
color_mode: str = 'color',
) -> Tuple[bytes, DCTEmbedStats]:
) -> tuple[bytes, DCTEmbedStats]:
"""
Embed using scipy DCT with safe memory handling.
Uses row-by-row 1D DCT operations instead of 2D arrays to avoid
scipy memory corruption issues with large images.
"""
capacity_info = calculate_dct_capacity(carrier_image)
if len(data) > capacity_info.usable_capacity_bytes:
raise ValueError(
f"Data too large ({len(data)} bytes) for carrier "
f"(capacity: {capacity_info.usable_capacity_bytes} bytes)"
)
# Load image
img = Image.open(io.BytesIO(carrier_image))
width, height = img.size
flags = FLAG_COLOR_MODE if color_mode == 'color' else 0
# Prepare payload bits
header = _create_header(len(data), flags)
payload = header + data
@@ -504,41 +503,41 @@ def _embed_scipy_dct_safe(
for byte in payload:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
# Generate block order
num_blocks = capacity_info.total_blocks
block_order = _generate_block_order(num_blocks, seed)
blocks_x = width // BLOCK_SIZE
if color_mode == 'color' and img.mode in ('RGB', 'RGBA'):
if img.mode == 'RGBA':
img = img.convert('RGB')
# Process color image
rgb = np.array(img, dtype=np.float64, copy=True, order='C')
img.close()
Y, Cb, Cr = _rgb_to_ycbcr(rgb)
del rgb
gc.collect()
Y_padded, original_size = _pad_to_blocks(Y)
del Y
gc.collect()
# Embed in Y channel
Y_embedded = _embed_in_channel_safe(Y_padded, bits, block_order, blocks_x)
del Y_padded
gc.collect()
Y_result = _unpad_image(Y_embedded, original_size)
del Y_embedded
gc.collect()
result_rgb = _ycbcr_to_rgb(Y_result, Cb, Cr)
del Y_result, Cb, Cr
gc.collect()
stego_bytes = _save_color_image(result_rgb, output_format)
del result_rgb
gc.collect()
@@ -546,23 +545,23 @@ def _embed_scipy_dct_safe(
# Grayscale mode
image = _to_grayscale(carrier_image)
img.close()
padded, original_size = _pad_to_blocks(image)
del image
gc.collect()
embedded = _embed_in_channel_safe(padded, bits, block_order, blocks_x)
del padded
gc.collect()
result = _unpad_image(embedded, original_size)
del embedded
gc.collect()
stego_bytes = _save_stego_image(result, output_format)
del result
gc.collect()
stats = DCTEmbedStats(
blocks_used=(len(bits) + len(DEFAULT_EMBED_POSITIONS) - 1) // len(DEFAULT_EMBED_POSITIONS),
blocks_available=capacity_info.total_blocks,
@@ -575,7 +574,7 @@ def _embed_scipy_dct_safe(
jpeg_native=False,
color_mode=color_mode,
)
return stego_bytes, stats
@@ -587,78 +586,78 @@ def _embed_in_channel_safe(
) -> np.ndarray:
"""
Embed bits in channel using safe DCT operations.
Processes one block at a time with fresh array allocations.
"""
h, w = channel.shape
# Create result with explicit new memory
result = np.array(channel, dtype=np.float64, copy=True, order='C')
bit_idx = 0
for block_num in block_order:
if bit_idx >= len(bits):
break
by = (block_num // blocks_x) * BLOCK_SIZE
bx = (block_num % blocks_x) * BLOCK_SIZE
# Extract block - create brand new array
block = np.array(
result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE],
dtype=np.float64, copy=True, order='C'
)
# Apply safe DCT (row-by-row)
dct_block = _safe_dct2(block)
# Embed bits
for pos in DEFAULT_EMBED_POSITIONS:
if bit_idx >= len(bits):
break
dct_block[pos[0], pos[1]] = _embed_bit_in_coeff(
float(dct_block[pos[0], pos[1]]),
float(dct_block[pos[0], pos[1]]),
bits[bit_idx]
)
bit_idx += 1
# Apply safe inverse DCT
modified_block = _safe_idct2(dct_block)
# Copy back
result[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE] = modified_block
# Clean up this iteration
del block, dct_block, modified_block
# Force garbage collection
gc.collect()
return result
def _normalize_jpeg_for_jpegio(image_data: bytes) -> bytes:
"""
Normalize a JPEG image to ensure jpegio can process it safely.
JPEGs saved with quality=100 have quantization tables with all values = 1,
which causes jpegio to crash due to huge coefficient magnitudes.
This function detects such images and re-saves them at a safe quality level.
Args:
image_data: Raw JPEG bytes
Returns:
Normalized JPEG bytes (may be unchanged if already safe)
"""
img = Image.open(io.BytesIO(image_data))
# Only process JPEGs
if img.format != 'JPEG':
img.close()
return image_data
# Check quantization tables
needs_normalization = False
if hasattr(img, 'quantization') and img.quantization:
@@ -667,19 +666,19 @@ def _normalize_jpeg_for_jpegio(image_data: bytes) -> bytes:
if max(table) <= JPEGIO_MAX_QUANT_VALUE_THRESHOLD:
needs_normalization = True
break
if not needs_normalization:
img.close()
return image_data
# Re-save at safe quality level
if img.mode != 'RGB':
img = img.convert('RGB')
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=JPEGIO_NORMALIZE_QUALITY, subsampling=0)
img.close()
return buffer.getvalue()
@@ -688,17 +687,17 @@ def _embed_jpegio(
carrier_image: bytes,
seed: bytes,
color_mode: str = 'color',
) -> Tuple[bytes, DCTEmbedStats]:
) -> tuple[bytes, DCTEmbedStats]:
"""Embed using jpegio for proper JPEG coefficient modification."""
import tempfile
import os
import tempfile
# Normalize JPEG to avoid crashes with quality=100 images
carrier_image = _normalize_jpeg_for_jpegio(carrier_image)
img = Image.open(io.BytesIO(carrier_image))
width, height = img.size
if img.format != 'JPEG':
buffer = io.BytesIO()
if img.mode != 'RGB':
@@ -706,54 +705,54 @@ def _embed_jpegio(
img.save(buffer, format='JPEG', quality=95, subsampling=0)
carrier_image = buffer.getvalue()
img.close()
input_path = _jpegio_bytes_to_file(carrier_image, suffix='.jpg')
output_path = tempfile.mktemp(suffix='.jpg')
flags = FLAG_COLOR_MODE if color_mode == 'color' else 0
try:
jpeg = jio.read(input_path)
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
all_positions = _jpegio_get_usable_positions(coef_array)
order = _jpegio_generate_order(len(all_positions), seed)
header = _jpegio_create_header(len(data), flags)
payload = header + data
bits = []
for byte in payload:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
if len(bits) > len(all_positions):
raise ValueError(
f"Payload too large: {len(bits)} bits, "
f"only {len(all_positions)} usable coefficients"
)
coefs_used = 0
for bit_idx, pos_idx in enumerate(order):
if bit_idx >= len(bits):
break
row, col = all_positions[pos_idx]
coef = coef_array[row, col]
if (coef & 1) != bits[bit_idx]:
if coef > 0:
coef_array[row, col] = coef - 1 if (coef & 1) else coef + 1
else:
coef_array[row, col] = coef + 1 if (coef & 1) else coef - 1
coefs_used += 1
jio.write(jpeg, output_path)
with open(output_path, 'rb') as f:
stego_bytes = f.read()
stats = DCTEmbedStats(
blocks_used=coefs_used // 63,
blocks_available=len(all_positions) // 63,
@@ -766,9 +765,9 @@ def _embed_jpegio(
jpeg_native=True,
color_mode=color_mode,
)
return stego_bytes, stats
finally:
for path in [input_path, output_path]:
try:
@@ -782,13 +781,13 @@ def extract_from_dct(stego_image: bytes, seed: bytes) -> bytes:
img = Image.open(io.BytesIO(stego_image))
fmt = img.format
img.close()
if fmt == 'JPEG' and HAS_JPEGIO:
try:
return _extract_jpegio(stego_image, seed)
except ValueError:
pass
_check_scipy()
return _extract_scipy_dct_safe(stego_image, seed)
@@ -798,41 +797,41 @@ def _extract_scipy_dct_safe(stego_image: bytes, seed: bytes) -> bytes:
img = Image.open(io.BytesIO(stego_image))
width, height = img.size
mode = img.mode
if mode in ('RGB', 'RGBA'):
channel = _extract_y_channel(stego_image)
else:
channel = _to_grayscale(stego_image)
img.close()
padded, _ = _pad_to_blocks(channel)
del channel
gc.collect()
h, w = padded.shape
blocks_x = w // BLOCK_SIZE
num_blocks = (h // BLOCK_SIZE) * blocks_x
block_order = _generate_block_order(num_blocks, seed)
all_bits = []
for block_num in block_order:
by = (block_num // blocks_x) * BLOCK_SIZE
bx = (block_num % blocks_x) * BLOCK_SIZE
block = np.array(
padded[by:by+BLOCK_SIZE, bx:bx+BLOCK_SIZE],
dtype=np.float64, copy=True, order='C'
)
dct_block = _safe_dct2(block)
for pos in DEFAULT_EMBED_POSITIONS:
bit = _extract_bit_from_coeff(float(dct_block[pos[0], pos[1]]))
all_bits.append(bit)
del block, dct_block
if len(all_bits) >= HEADER_SIZE * 8:
try:
_, flags, data_length = _parse_header(all_bits[:HEADER_SIZE * 8])
@@ -841,53 +840,53 @@ def _extract_scipy_dct_safe(stego_image: bytes, seed: bytes) -> bytes:
break
except ValueError:
pass
del padded
gc.collect()
_, flags, data_length = _parse_header(all_bits)
data_bits = all_bits[HEADER_SIZE * 8:(HEADER_SIZE + data_length) * 8]
data = bytes([
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(data_length)
])
return data
def _extract_jpegio(stego_image: bytes, seed: bytes) -> bytes:
"""Extract using jpegio for JPEG images."""
import os
# Normalize JPEG to avoid crashes with quality=100 images
# (shouldn't happen with stego images, but be defensive)
stego_image = _normalize_jpeg_for_jpegio(stego_image)
temp_path = _jpegio_bytes_to_file(stego_image, suffix='.jpg')
try:
jpeg = jio.read(temp_path)
coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL]
all_positions = _jpegio_get_usable_positions(coef_array)
order = _jpegio_generate_order(len(all_positions), seed)
header_bits = []
for pos_idx in order[:HEADER_SIZE * 8]:
row, col = all_positions[pos_idx]
coef = coef_array[row, col]
header_bits.append(coef & 1)
header_bytes = bytes([
sum(header_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(HEADER_SIZE)
])
_, flags, data_length = _jpegio_parse_header(header_bytes)
total_bits_needed = (HEADER_SIZE + data_length) * 8
all_bits = []
for bit_idx, pos_idx in enumerate(order):
if bit_idx >= total_bits_needed:
@@ -895,16 +894,16 @@ def _extract_jpegio(stego_image: bytes, seed: bytes) -> bytes:
row, col = all_positions[pos_idx]
coef = coef_array[row, col]
all_bits.append(coef & 1)
data_bits = all_bits[HEADER_SIZE * 8:]
data = bytes([
sum(data_bits[i*8:(i+1)*8][j] << (7-j) for j in range(8))
for i in range(data_length)
])
return data
finally:
try:
os.unlink(temp_path)