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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user