Version 3.1.0 now with experimental DCT support.

This commit is contained in:
Aaron D. Lee
2025-12-31 13:11:34 -05:00
parent e4a4a5e074
commit 4eefc946c4
10 changed files with 2520 additions and 299 deletions

View File

@@ -1,19 +1,20 @@
#!/usr/bin/env python3
"""
Stegasoo REST API
Stegasoo REST API (v3.0)
FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding.
NEW in v3.0: LSB and DCT embedding modes.
"""
import io
import sys
import base64
from pathlib import Path
from typing import Optional
from typing import Optional, Literal
from datetime import date
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Query
from fastapi.responses import Response, JSONResponse
from pydantic import BaseModel, Field
@@ -30,6 +31,14 @@ from stegasoo import (
has_argon2,
FilePayload,
MAX_FILE_PAYLOAD_SIZE,
# NEW in v3.0 - Embedding modes
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
has_dct_support,
compare_modes,
will_fit_by_mode,
calculate_capacity_by_mode,
)
from stegasoo.constants import (
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
@@ -55,13 +64,30 @@ except ImportError:
app = FastAPI(
title="Stegasoo API",
description="Secure steganography with hybrid authentication. Supports text messages and file embedding.",
description="""
Secure steganography with hybrid authentication. Supports text messages and file embedding.
## Embedding Modes (v3.0)
- **LSB mode** (default): Spatial LSB embedding, full color output, higher capacity
- **DCT mode**: Frequency domain embedding, grayscale output, ~20% capacity, better stealth
Use the `/modes` endpoint to check availability and `/compare` to compare capacities.
""",
version=__version__,
docs_url="/docs",
redoc_url="/redoc",
)
# ============================================================================
# TYPE ALIASES
# ============================================================================
EmbedModeType = Literal["lsb", "dct"]
ExtractModeType = Literal["auto", "lsb", "dct"]
# ============================================================================
# MODELS
# ============================================================================
@@ -90,6 +116,10 @@ class EncodeRequest(BaseModel):
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None
embed_mode: EmbedModeType = Field(
default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (grayscale, requires scipy)"
)
class EncodeFileRequest(BaseModel):
@@ -104,6 +134,10 @@ class EncodeFileRequest(BaseModel):
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None
embed_mode: EmbedModeType = Field(
default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (grayscale, requires scipy)"
)
class EncodeResponse(BaseModel):
@@ -112,6 +146,7 @@ class EncodeResponse(BaseModel):
capacity_used_percent: float
date_used: str
day_of_week: str
embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'")
class DecodeRequest(BaseModel):
@@ -121,6 +156,10 @@ class DecodeRequest(BaseModel):
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
embed_mode: ExtractModeType = Field(
default="auto",
description="Extraction mode: 'auto' (default), 'lsb', or 'dct'"
)
class DecodeResponse(BaseModel):
@@ -132,20 +171,60 @@ class DecodeResponse(BaseModel):
mime_type: Optional[str] = None # For file
class ModeCapacity(BaseModel):
"""Capacity info for a single mode."""
capacity_bytes: int
capacity_kb: float
available: bool
output_format: str
class ImageInfoResponse(BaseModel):
width: int
height: int
pixels: int
capacity_bytes: int
capacity_kb: int
capacity_bytes: int = Field(description="LSB mode capacity (for backwards compatibility)")
capacity_kb: int = Field(description="LSB mode capacity in KB")
# NEW in v3.0
modes: Optional[dict[str, ModeCapacity]] = Field(
default=None,
description="Capacity by embedding mode (v3.0+)"
)
class CompareModesRequest(BaseModel):
"""Request for comparing embedding modes."""
carrier_image_base64: str
payload_size: Optional[int] = Field(
default=None,
description="Optional payload size to check if it fits"
)
class CompareModesResponse(BaseModel):
"""Response comparing LSB and DCT modes."""
width: int
height: int
lsb: dict
dct: dict
payload_check: Optional[dict] = None
recommendation: str
class ModesResponse(BaseModel):
"""Response showing available embedding modes."""
lsb: dict
dct: dict
class StatusResponse(BaseModel):
version: str
has_argon2: bool
has_qrcode_read: bool
has_dct: bool # NEW in v3.0
day_names: list[str]
max_payload_kb: int
available_modes: list[str] # NEW in v3.0
class QrExtractResponse(BaseModel):
@@ -154,27 +233,165 @@ class QrExtractResponse(BaseModel):
error: Optional[str] = None
class WillFitRequest(BaseModel):
"""Request to check if payload will fit."""
carrier_image_base64: str
payload_size: int
embed_mode: EmbedModeType = "lsb"
class WillFitResponse(BaseModel):
"""Response for will_fit check."""
fits: bool
payload_size: int
capacity: int
usage_percent: float
headroom: int
mode: str
class ErrorResponse(BaseModel):
error: str
detail: Optional[str] = None
# ============================================================================
# ROUTES
# ROUTES - STATUS & INFO
# ============================================================================
@app.get("/", response_model=StatusResponse)
async def root():
"""Get API status and configuration."""
available_modes = ["lsb"]
if has_dct_support():
available_modes.append("dct")
return StatusResponse(
version=__version__,
has_argon2=has_argon2(),
has_qrcode_read=HAS_QR_READ,
has_dct=has_dct_support(),
day_names=list(DAY_NAMES),
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024,
available_modes=available_modes
)
@app.get("/modes", response_model=ModesResponse)
async def api_modes():
"""
Get available embedding modes and their status.
NEW in v3.0: Shows LSB and DCT mode availability.
"""
return ModesResponse(
lsb={
"available": True,
"name": "Spatial LSB",
"description": "Embed in pixel LSBs, outputs PNG/BMP",
"output_format": "PNG (color)",
"capacity_ratio": "100%",
},
dct={
"available": has_dct_support(),
"name": "DCT Domain",
"description": "Embed in DCT coefficients, outputs grayscale PNG",
"output_format": "PNG (grayscale)",
"capacity_ratio": "~20% of LSB",
"requires": "scipy",
}
)
@app.post("/compare", response_model=CompareModesResponse)
async def api_compare_modes(request: CompareModesRequest):
"""
Compare LSB and DCT embedding modes for a carrier image.
NEW in v3.0: Returns capacity for both modes and recommendation.
Optionally checks if a specific payload size would fit.
"""
try:
carrier = base64.b64decode(request.carrier_image_base64)
comparison = compare_modes(carrier)
response = CompareModesResponse(
width=comparison['width'],
height=comparison['height'],
lsb={
"capacity_bytes": comparison['lsb']['capacity_bytes'],
"capacity_kb": round(comparison['lsb']['capacity_kb'], 1),
"available": True,
"output_format": comparison['lsb']['output'],
},
dct={
"capacity_bytes": comparison['dct']['capacity_bytes'],
"capacity_kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'],
"output_format": comparison['dct']['output'],
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
},
recommendation="lsb" if not comparison['dct']['available'] else "dct for stealth, lsb for capacity"
)
if request.payload_size:
fits_lsb = request.payload_size <= comparison['lsb']['capacity_bytes']
fits_dct = request.payload_size <= comparison['dct']['capacity_bytes']
response.payload_check = {
"size_bytes": request.payload_size,
"fits_lsb": fits_lsb,
"fits_dct": fits_dct,
}
# Update recommendation based on payload
if fits_dct and comparison['dct']['available']:
response.recommendation = "dct (payload fits, better stealth)"
elif fits_lsb:
response.recommendation = "lsb (payload too large for dct)"
else:
response.recommendation = "none (payload too large for both modes)"
return response
except Exception as e:
raise HTTPException(500, str(e))
@app.post("/will-fit", response_model=WillFitResponse)
async def api_will_fit(request: WillFitRequest):
"""
Check if a payload of given size will fit in the carrier image.
NEW in v3.0: Supports both LSB and DCT modes.
"""
try:
# Validate mode
if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
carrier = base64.b64decode(request.carrier_image_base64)
result = will_fit_by_mode(request.payload_size, carrier, embed_mode=request.embed_mode)
return WillFitResponse(
fits=result['fits'],
payload_size=result['payload_size'],
capacity=result['capacity'],
usage_percent=round(result['usage_percent'], 1),
headroom=result['headroom'],
mode=request.embed_mode
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, str(e))
# ============================================================================
# ROUTES - QR CODE
# ============================================================================
@app.post("/extract-key-from-qr", response_model=QrExtractResponse)
async def api_extract_key_from_qr(
qr_image: UploadFile = File(..., description="QR code image containing RSA key")
@@ -206,6 +423,10 @@ async def api_extract_key_from_qr(
return QrExtractResponse(success=False, error=str(e))
# ============================================================================
# ROUTES - GENERATE
# ============================================================================
@app.post("/generate", response_model=GenerateResponse)
async def api_generate(request: GenerateRequest):
"""
@@ -243,13 +464,23 @@ async def api_generate(request: GenerateRequest):
raise HTTPException(500, str(e))
# ============================================================================
# ROUTES - ENCODE (JSON)
# ============================================================================
@app.post("/encode", response_model=EncodeResponse)
async def api_encode(request: EncodeRequest):
"""
Encode a text message into an image.
Images must be base64-encoded. Returns base64-encoded stego image.
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
"""
# Validate mode
if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
try:
ref_photo = base64.b64decode(request.reference_photo_base64)
carrier = base64.b64decode(request.carrier_image_base64)
@@ -263,7 +494,8 @@ async def api_encode(request: EncodeRequest):
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
date_str=request.date_str
date_str=request.date_str,
embed_mode=request.embed_mode, # NEW in v3.0
)
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
@@ -274,7 +506,8 @@ async def api_encode(request: EncodeRequest):
filename=result.filename,
capacity_used_percent=result.capacity_percent,
date_used=result.date_used,
day_of_week=day_of_week
day_of_week=day_of_week,
embed_mode=request.embed_mode,
)
except CapacityError as e:
@@ -291,7 +524,13 @@ async def api_encode_file(request: EncodeFileRequest):
Encode a file into an image (JSON with base64).
File data must be base64-encoded.
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
"""
# Validate mode
if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
try:
file_data = base64.b64decode(request.file_data_base64)
ref_photo = base64.b64decode(request.reference_photo_base64)
@@ -312,7 +551,8 @@ async def api_encode_file(request: EncodeFileRequest):
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
date_str=request.date_str
date_str=request.date_str,
embed_mode=request.embed_mode, # NEW in v3.0
)
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
@@ -323,7 +563,8 @@ async def api_encode_file(request: EncodeFileRequest):
filename=result.filename,
capacity_used_percent=result.capacity_percent,
date_used=result.date_used,
day_of_week=day_of_week
day_of_week=day_of_week,
embed_mode=request.embed_mode,
)
except CapacityError as e:
@@ -334,13 +575,24 @@ async def api_encode_file(request: EncodeFileRequest):
raise HTTPException(500, str(e))
# ============================================================================
# ROUTES - DECODE (JSON)
# ============================================================================
@app.post("/decode", response_model=DecodeResponse)
async def api_decode(request: DecodeRequest):
"""
Decode a message or file from a stego image.
Returns payload_type to indicate if result is text or file.
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
With 'auto' (default), tries LSB first then DCT.
"""
# Validate mode
if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
try:
stego = base64.b64decode(request.stego_image_base64)
ref_photo = base64.b64decode(request.reference_photo_base64)
@@ -352,7 +604,8 @@ async def api_decode(request: DecodeRequest):
day_phrase=request.day_phrase,
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password
rsa_password=request.rsa_password,
embed_mode=request.embed_mode, # NEW in v3.0
)
if result.is_file:
@@ -376,6 +629,10 @@ async def api_decode(request: DecodeRequest):
raise HTTPException(500, str(e))
# ============================================================================
# ROUTES - ENCODE/DECODE (MULTIPART)
# ============================================================================
@app.post("/encode/multipart")
async def api_encode_multipart(
day_phrase: str = Form(...),
@@ -387,7 +644,8 @@ async def api_encode_multipart(
rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""),
date_str: str = Form("")
date_str: str = Form(""),
embed_mode: str = Form("lsb"), # NEW in v3.0
):
"""
Encode using multipart form data (file uploads).
@@ -395,7 +653,15 @@ async def api_encode_multipart(
Provide either 'message' (text) or 'payload_file' (binary file).
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns the stego image directly as PNG with metadata headers.
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
"""
# Validate mode
if embed_mode not in ("lsb", "dct"):
raise HTTPException(400, "embed_mode must be 'lsb' or 'dct'")
if embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
try:
ref_data = await reference_photo.read()
carrier_data = await carrier.read()
@@ -443,7 +709,8 @@ async def api_encode_multipart(
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=effective_password,
date_str=date_str if date_str else None
date_str=date_str if date_str else None,
embed_mode=embed_mode, # NEW in v3.0
)
day_of_week = get_day_from_date(result.date_used)
@@ -455,7 +722,8 @@ async def api_encode_multipart(
"Content-Disposition": f"attachment; filename={result.filename}",
"X-Stegasoo-Date": result.date_used,
"X-Stegasoo-Day": day_of_week,
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}"
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
"X-Stegasoo-Embed-Mode": embed_mode, # NEW in v3.0
}
)
@@ -463,6 +731,8 @@ async def api_encode_multipart(
raise HTTPException(400, str(e))
except StegasooError as e:
raise HTTPException(400, str(e))
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, str(e))
@@ -475,14 +745,23 @@ async def api_decode_multipart(
pin: str = Form(""),
rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form("")
rsa_password: str = Form(""),
embed_mode: str = Form("auto"), # NEW in v3.0
):
"""
Decode using multipart form data (file uploads).
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns JSON with payload_type indicating text or file.
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
"""
# Validate mode
if embed_mode not in ("auto", "lsb", "dct"):
raise HTTPException(400, "embed_mode must be 'auto', 'lsb', or 'dct'")
if embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
try:
ref_data = await reference_photo.read()
stego_data = await stego_image.read()
@@ -515,7 +794,8 @@ async def api_decode_multipart(
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=effective_password
rsa_password=effective_password,
embed_mode=embed_mode, # NEW in v3.0
)
if result.is_file:
@@ -535,13 +815,26 @@ async def api_decode_multipart(
raise HTTPException(401, "Decryption failed. Check credentials.")
except StegasooError as e:
raise HTTPException(400, str(e))
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, str(e))
# ============================================================================
# ROUTES - IMAGE INFO
# ============================================================================
@app.post("/image/info", response_model=ImageInfoResponse)
async def api_image_info(image: UploadFile = File(...)):
"""Get information about an image's capacity."""
async def api_image_info(
image: UploadFile = File(...),
include_modes: bool = Query(True, description="Include capacity by mode (v3.0+)")
):
"""
Get information about an image's capacity.
NEW in v3.0: Optionally includes capacity for both LSB and DCT modes.
"""
try:
image_data = await image.read()
@@ -551,7 +844,7 @@ async def api_image_info(image: UploadFile = File(...)):
capacity = calculate_capacity(image_data)
return ImageInfoResponse(
response = ImageInfoResponse(
width=result.details['width'],
height=result.details['height'],
pixels=result.details['pixels'],
@@ -559,6 +852,26 @@ async def api_image_info(image: UploadFile = File(...)):
capacity_kb=capacity // 1024
)
# NEW in v3.0 - include mode comparison
if include_modes:
comparison = compare_modes(image_data)
response.modes = {
"lsb": ModeCapacity(
capacity_bytes=comparison['lsb']['capacity_bytes'],
capacity_kb=round(comparison['lsb']['capacity_kb'], 1),
available=True,
output_format=comparison['lsb']['output'],
),
"dct": ModeCapacity(
capacity_bytes=comparison['dct']['capacity_bytes'],
capacity_kb=round(comparison['dct']['capacity_kb'], 1),
available=comparison['dct']['available'],
output_format=comparison['dct']['output'],
),
}
return response
except HTTPException:
raise
except Exception as e:

View File

@@ -8,6 +8,7 @@ Usage:
stegasoo decode [OPTIONS]
stegasoo verify [OPTIONS]
stegasoo info [OPTIONS]
stegasoo compare [OPTIONS] # NEW in v3.0
"""
import sys
@@ -29,9 +30,16 @@ from stegasoo import (
DAY_NAMES, __version__,
StegasooError, DecryptionError, ExtractionError,
FilePayload,
# New in 2.2.1
will_fit,
strip_image_metadata,
# NEW in v3.0 - Embedding modes
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
has_dct_support,
compare_modes,
will_fit_by_mode,
calculate_capacity_by_mode,
)
# QR Code utilities
@@ -68,6 +76,11 @@ def cli():
• Reference photo (something you have)
• Daily passphrase (something you know)
• Static PIN or RSA key (additional security)
\b
NEW in v3.0 - Embedding Modes:
• LSB mode (default): Full color output, higher capacity
• DCT mode: Grayscale output, ~20% capacity, better stealth
"""
pass
@@ -200,8 +213,10 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
help='Embedding mode: lsb (default, color) or dct (grayscale, requires scipy)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr, key_password, output, date_str, quiet):
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr, key_password, output, date_str, embed_mode, quiet):
"""
Encode a secret message or file into an image.
@@ -212,20 +227,37 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
For binary files, use -e/--embed-file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b
Embedding Modes (v3.0):
--mode lsb Spatial LSB embedding (default)
• Full color output (PNG/BMP)
• Higher capacity (~375 KB/megapixel)
--mode dct DCT domain embedding (requires scipy)
• Grayscale output only
• Lower capacity (~75 KB/megapixel)
• Better resistance to visual analysis
\b
Examples:
# Text message with PIN
# Text message with PIN (LSB mode, default)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
# DCT mode for better stealth
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" --mode dct
# With RSA key file
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem -m "secret"
# With RSA key from QR code image
stegasoo encode -r photo.jpg -c meme.png -p "words" --key-qr keyqr.png -m "secret"
# Embed a binary file
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
raise click.ClickException(
"DCT mode requires scipy. Install with: pip install scipy"
)
# Determine what to encode
payload = None
@@ -277,16 +309,28 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
ref_photo = Path(ref).read_bytes()
carrier_image = Path(carrier).read_bytes()
# Pre-check capacity
fit_check = will_fit(payload, carrier_image)
# Pre-check capacity with selected mode
fit_check = will_fit_by_mode(payload, carrier_image, embed_mode=embed_mode)
if not fit_check['fits']:
# Suggest alternative mode if it would fit
alt_mode = 'lsb' if embed_mode == 'dct' else 'dct'
alt_check = will_fit_by_mode(payload, carrier_image, embed_mode=alt_mode)
suggestion = ""
if alt_mode == 'lsb' and alt_check['fits']:
suggestion = f"\n Tip: Payload would fit in LSB mode (--mode lsb)"
raise click.ClickException(
f"Payload too large for carrier image.\n"
f"Payload too large for {embed_mode.upper()} mode.\n"
f" Payload: {fit_check['payload_size']:,} bytes\n"
f" Capacity: {fit_check['capacity']:,} bytes\n"
f" Shortfall: {-fit_check['headroom']:,} bytes"
f"{suggestion}"
)
if not quiet:
click.echo(f"Mode: {embed_mode.upper()} ({fit_check['usage_percent']:.1f}% capacity)")
result = encode(
message=payload,
reference_photo=ref_photo,
@@ -296,6 +340,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
date_str=date_str,
embed_mode=embed_mode, # NEW in v3.0
)
# Determine output path
@@ -313,6 +358,8 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
click.echo(f" Size: {len(result.stego_image):,} bytes")
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
click.echo(f" Date: {result.date_used}")
if embed_mode == 'dct':
click.secho(f" Note: Output is grayscale (DCT mode)", dim=True)
except StegasooError as e:
raise click.ClickException(str(e))
@@ -335,9 +382,11 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file')
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)')
@click.option('--force', is_flag=True, help='Overwrite existing output file')
def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, quiet, force):
def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed_mode, quiet, force):
"""
Decode a secret message or file from a stego image.
@@ -345,20 +394,32 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, quiet
Automatically detects whether content is text or a file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b
Extraction Modes (v3.0):
--mode auto Auto-detect (default) - tries LSB first, then DCT
--mode lsb Only try LSB extraction
--mode dct Only try DCT extraction (requires scipy)
\b
Examples:
# Decode with PIN
# Decode with PIN (auto-detect mode)
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
# Explicitly specify DCT mode
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 --mode dct
# Decode with RSA key file
stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem
# Decode with RSA key from QR code image
stegasoo decode -r photo.jpg -s stego.png -p "words" --key-qr keyqr.png
# Save output to file
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
raise click.ClickException(
"DCT mode requires scipy. Install with: pip install scipy"
)
# Load key if provided (from .pem file or QR code image)
rsa_key_data = None
rsa_key_from_qr = False
@@ -400,6 +461,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, quiet
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode, # NEW in v3.0
)
if result.is_file:
@@ -459,8 +521,10 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, quiet
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def verify(ref, stego, phrase, pin, key, key_qr, key_password, as_json):
def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_json):
"""
Verify that a stego image can be decoded without extracting the message.
@@ -472,7 +536,15 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, as_json):
stegasoo verify -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
stegasoo verify -r photo.jpg -s stego.png -p "words" -k mykey.pem --json
stegasoo verify -r photo.jpg -s stego.png -p "words" --pin 123456 --mode dct
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
raise click.ClickException(
"DCT mode requires scipy. Install with: pip install scipy"
)
# Load key if provided
rsa_key_data = None
rsa_key_from_qr = False
@@ -511,6 +583,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, as_json):
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode, # NEW in v3.0
)
# Calculate payload size
@@ -576,11 +649,13 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, as_json):
@cli.command()
@click.argument('image', type=click.Path(exists=True))
def info(image):
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def info(image, as_json):
"""
Show information about an image.
Displays dimensions, capacity, and attempts to detect date from filename.
Displays dimensions, capacity for both LSB and DCT modes,
and attempts to detect date from filename.
"""
try:
image_data = Path(image).read_bytes()
@@ -589,21 +664,58 @@ def info(image):
if not result.is_valid:
raise click.ClickException(result.error_message)
capacity = calculate_capacity(image_data)
# Get capacity comparison
comparison = compare_modes(image_data)
# Try to get date from filename
date_str = parse_date_from_filename(image)
day_name = get_day_from_date(date_str) if date_str else None
if as_json:
import json
output = {
"file": image,
"width": result.details['width'],
"height": result.details['height'],
"pixels": result.details['pixels'],
"mode": result.details['mode'],
"format": result.details['format'],
"capacity": {
"lsb": {
"bytes": comparison['lsb']['capacity_bytes'],
"kb": round(comparison['lsb']['capacity_kb'], 1),
},
"dct": {
"bytes": comparison['dct']['capacity_bytes'],
"kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'],
"ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1),
},
},
}
if date_str:
output["embed_date"] = date_str
output["embed_day"] = day_name
click.echo(json.dumps(output, indent=2))
return
click.echo()
click.secho(f"Image: {image}", bold=True)
click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}")
click.echo(f" Pixels: {result.details['pixels']:,}")
click.echo(f" Mode: {result.details['mode']}")
click.echo(f" Format: {result.details['format']}")
click.echo(f" Capacity: ~{capacity:,} bytes ({capacity // 1024} KB)")
click.echo()
click.secho(" Capacity:", bold=True)
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
dct_status = "" if comparison['dct']['available'] else "✗ (scipy not installed)"
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
if date_str:
click.echo()
click.echo(f" Embed date: {date_str} ({day_name})")
click.echo()
@@ -612,6 +724,127 @@ def info(image):
raise click.ClickException(str(e))
# ============================================================================
# COMPARE COMMAND (NEW in v3.0)
# ============================================================================
@cli.command()
@click.argument('image', type=click.Path(exists=True))
@click.option('--payload-size', '-s', type=int, help='Check if specific payload size fits')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def compare(image, payload_size, as_json):
"""
Compare LSB and DCT embedding modes for an image.
Shows capacity for each mode and recommends which to use.
Optionally checks if a specific payload size would fit.
\b
Examples:
stegasoo compare carrier.png
stegasoo compare carrier.png --payload-size 50000
stegasoo compare carrier.png --json
"""
try:
image_data = Path(image).read_bytes()
comparison = compare_modes(image_data)
if as_json:
import json
output = {
"file": image,
"width": comparison['width'],
"height": comparison['height'],
"modes": {
"lsb": {
"capacity_bytes": comparison['lsb']['capacity_bytes'],
"capacity_kb": round(comparison['lsb']['capacity_kb'], 1),
"available": True,
"output_format": comparison['lsb']['output'],
},
"dct": {
"capacity_bytes": comparison['dct']['capacity_bytes'],
"capacity_kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'],
"output_format": comparison['dct']['output'],
"ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1),
},
},
}
if payload_size:
output["payload_check"] = {
"size_bytes": payload_size,
"fits_lsb": payload_size <= comparison['lsb']['capacity_bytes'],
"fits_dct": payload_size <= comparison['dct']['capacity_bytes'],
}
click.echo(json.dumps(output, indent=2))
return
click.echo()
click.secho(f"═══ Mode Comparison: {image} ═══", fg='cyan', bold=True)
click.echo(f" Dimensions: {comparison['width']} × {comparison['height']}")
click.echo()
# LSB mode
click.secho(" ┌─── LSB Mode ───", fg='green')
click.echo(f" │ Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
click.echo(f" │ Output: {comparison['lsb']['output']}")
click.echo(f" │ Status: ✓ Available")
click.echo("")
# DCT mode
click.secho(" ├─── DCT Mode ───", fg='blue')
click.echo(f" │ Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
click.echo(f" │ Output: {comparison['dct']['output']}")
click.echo(f" │ Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
if comparison['dct']['available']:
click.echo(f" │ Status: ✓ Available")
else:
click.secho(f" │ Status: ✗ Requires scipy (pip install scipy)", fg='yellow')
click.echo("")
# Payload check
if payload_size:
click.secho(" ├─── Payload Check ───", fg='magenta')
click.echo(f" │ Size: {payload_size:,} bytes")
fits_lsb = payload_size <= comparison['lsb']['capacity_bytes']
fits_dct = payload_size <= comparison['dct']['capacity_bytes']
lsb_icon = "" if fits_lsb else ""
dct_icon = "" if fits_dct else ""
lsb_color = 'green' if fits_lsb else 'red'
dct_color = 'green' if fits_dct else 'red'
click.echo(f" │ LSB mode: ", nl=False)
click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color)
click.echo(f" │ DCT mode: ", nl=False)
click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color)
click.echo("")
# Recommendation
click.secho(" └─── Recommendation ───", fg='yellow')
if not comparison['dct']['available']:
click.echo(" Use LSB mode (DCT unavailable)")
elif payload_size:
if fits_dct:
click.echo(" DCT mode for better stealth (payload fits both modes)")
elif fits_lsb:
click.echo(" LSB mode (payload too large for DCT)")
else:
click.secho(" ✗ Payload too large for both modes!", fg='red')
else:
click.echo(" LSB for larger payloads, DCT for better stealth")
click.echo()
except Exception as e:
raise click.ClickException(str(e))
# ============================================================================
# STRIP-METADATA COMMAND
# ============================================================================
@@ -656,6 +889,48 @@ def strip_metadata_cmd(image, output, output_format, quiet):
raise click.ClickException(str(e))
# ============================================================================
# MODES COMMAND (NEW in v3.0)
# ============================================================================
@cli.command()
def modes():
"""
Show available embedding modes and their status.
Displays which modes are available and their characteristics.
"""
click.echo()
click.secho("═══ Stegasoo Embedding Modes ═══", fg='cyan', bold=True)
click.echo()
# LSB Mode
click.secho(" LSB Mode (Spatial LSB)", fg='green', bold=True)
click.echo(" Status: ✓ Always available")
click.echo(" Output: PNG/BMP (full color)")
click.echo(" Capacity: ~375 KB per megapixel")
click.echo(" Use case: Larger payloads, color preservation")
click.echo(" CLI flag: --mode lsb (default)")
click.echo()
# DCT Mode
click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True)
if has_dct_support():
click.echo(" Status: ✓ Available")
else:
click.secho(" Status: ✗ Requires scipy", fg='yellow')
click.echo(" Install: pip install scipy")
click.echo(" Output: PNG (grayscale only)")
click.echo(" Capacity: ~75 KB per megapixel (~20% of LSB)")
click.echo(" Use case: Better stealth, smaller messages")
click.echo(" CLI flag: --mode dct")
click.echo()
click.secho(" Tip:", dim=True)
click.echo(" Use 'stegasoo compare <image>' to see capacity for both modes")
click.echo()
# ============================================================================
# MAIN
# ============================================================================

View File

@@ -1,9 +1,11 @@
#!/usr/bin/env python3
"""
Stegasoo Web Frontend
Stegasoo Web Frontend (v3.0.1)
Flask-based web UI for steganography operations.
Supports both text messages and file embedding.
NEW in v3.0: LSB and DCT embedding modes with advanced options.
NEW in v3.0.1: DCT output format selection (PNG or JPEG).
"""
import io
@@ -35,6 +37,13 @@ from stegasoo import (
StegasooError, DecryptionError, CapacityError,
has_argon2,
FilePayload,
# NEW in v3.0 - Embedding modes
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
has_dct_support,
compare_modes,
will_fit_by_mode,
)
from stegasoo.constants import (
__version__,
@@ -102,6 +111,8 @@ def inject_globals():
'temp_file_expiry_minutes': TEMP_FILE_EXPIRY_MINUTES,
'min_pin_length': MIN_PIN_LENGTH,
'max_pin_length': MAX_PIN_LENGTH,
# NEW in v3.0
'has_dct': has_dct_support(),
}
@@ -114,6 +125,7 @@ try:
# Check current limits
print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}")
print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}")
print(f"DCT support available: {has_dct_support()}")
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
@@ -131,7 +143,7 @@ def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes
"""Generate thumbnail from image data."""
try:
with Image.open(io.BytesIO(image_data)) as img:
# Convert to RGB if necessary
# Convert to RGB if necessary (handle grayscale too)
if img.mode in ('RGBA', 'LA', 'P'):
# Create white background for transparent images
background = Image.new('RGB', img.size, (255, 255, 255))
@@ -139,6 +151,9 @@ def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = background
elif img.mode == 'L':
# Convert grayscale to RGB for thumbnail
img = img.convert('RGB')
elif img.mode != 'RGB':
img = img.convert('RGB')
@@ -401,6 +416,85 @@ def extract_key_from_qr_route():
}), 500
# ============================================================================
# NEW in v3.0 - CAPACITY COMPARISON API
# ============================================================================
@app.route('/api/compare-capacity', methods=['POST'])
def api_compare_capacity():
"""
Compare LSB and DCT capacity for an uploaded carrier image.
Returns JSON with capacity info for both modes.
"""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier image provided'}), 400
try:
carrier_data = carrier.read()
comparison = compare_modes(carrier_data)
return jsonify({
'success': True,
'width': comparison['width'],
'height': comparison['height'],
'lsb': {
'capacity_bytes': comparison['lsb']['capacity_bytes'],
'capacity_kb': round(comparison['lsb']['capacity_kb'], 1),
'output': comparison['lsb']['output'],
},
'dct': {
'capacity_bytes': comparison['dct']['capacity_bytes'],
'capacity_kb': round(comparison['dct']['capacity_kb'], 1),
'output': comparison['dct']['output'],
'available': comparison['dct']['available'],
'ratio': round(comparison['dct']['ratio_vs_lsb'], 1),
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/check-fit', methods=['POST'])
def api_check_fit():
"""
Check if a payload will fit in the carrier with selected mode.
Returns JSON with fit status and details.
"""
carrier = request.files.get('carrier')
payload_size = request.form.get('payload_size', type=int)
embed_mode = request.form.get('embed_mode', 'lsb')
if not carrier or payload_size is None:
return jsonify({'error': 'Missing carrier or payload_size'}), 400
if embed_mode not in ('lsb', 'dct'):
return jsonify({'error': 'Invalid embed_mode'}), 400
if embed_mode == 'dct' and not has_dct_support():
return jsonify({'error': 'DCT mode requires scipy'}), 400
try:
carrier_data = carrier.read()
result = will_fit_by_mode(payload_size, carrier_data, embed_mode=embed_mode)
return jsonify({
'success': True,
'fits': result['fits'],
'payload_size': result['payload_size'],
'capacity': result['capacity'],
'usage_percent': round(result['usage_percent'], 1),
'headroom': result['headroom'],
'mode': embed_mode,
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# ============================================================================
# ENCODE
# ============================================================================
@app.route('/encode', methods=['GET', 'POST'])
def encode_page():
day_of_week = get_today_day()
@@ -428,6 +522,21 @@ def encode_page():
rsa_password = request.form.get('rsa_password', '')
payload_type = request.form.get('payload_type', 'text')
# NEW in v3.0 - Embedding mode
embed_mode = request.form.get('embed_mode', 'lsb')
if embed_mode not in ('lsb', 'dct'):
embed_mode = 'lsb'
# NEW in v3.0.1 - DCT output format
dct_output_format = request.form.get('dct_output_format', 'png')
if dct_output_format not in ('png', 'jpeg'):
dct_output_format = 'png'
# Check DCT availability
if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
return render_template('encode.html', day_of_week=day_of_week, has_qrcode_read=HAS_QRCODE_READ)
# Determine payload
if payload_type == 'file' and payload_file and payload_file.filename:
# File payload
@@ -515,7 +624,7 @@ def encode_page():
else:
date_str = datetime.now().strftime('%Y-%m-%d')
# Encode
# Encode with selected mode and output format
encode_result = encode(
message=payload,
reference_photo=ref_data,
@@ -524,16 +633,34 @@ def encode_page():
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=date_str
date_str=date_str,
embed_mode=embed_mode, # NEW in v3.0
dct_output_format=dct_output_format if embed_mode == 'dct' else None, # NEW in v3.0.1
)
# Determine actual output format for filename and storage
if embed_mode == 'dct' and dct_output_format == 'jpeg':
output_ext = '.jpg'
output_mime = 'image/jpeg'
# Modify filename extension if needed
filename = encode_result.filename
if filename.endswith('.png'):
filename = filename[:-4] + '.jpg'
else:
output_ext = '.png'
output_mime = 'image/png'
filename = encode_result.filename
# Store temporarily
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[file_id] = {
'data': encode_result.stego_image,
'filename': encode_result.filename,
'timestamp': time.time()
'filename': filename,
'timestamp': time.time(),
'embed_mode': embed_mode,
'output_format': dct_output_format if embed_mode == 'dct' else 'png',
'mime_type': output_mime,
}
return redirect(url_for('encode_result', file_id=file_id))
@@ -570,7 +697,9 @@ def encode_result(file_id):
return render_template('encode_result.html',
file_id=file_id,
filename=file_info['filename'],
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None,
embed_mode=file_info.get('embed_mode', 'lsb'),
output_format=file_info.get('output_format', 'png'), # NEW in v3.0.1
)
@@ -594,9 +723,11 @@ def encode_download(file_id):
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
mime_type = file_info.get('mime_type', 'image/png')
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
mimetype=mime_type,
as_attachment=True,
download_name=file_info['filename']
)
@@ -609,9 +740,11 @@ def encode_file_route(file_id):
return "Not found", 404
file_info = TEMP_FILES[file_id]
mime_type = file_info.get('mime_type', 'image/png')
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
mimetype=mime_type,
as_attachment=False,
download_name=file_info['filename']
)
@@ -629,6 +762,10 @@ def encode_cleanup(file_id):
return jsonify({'status': 'ok'})
# ============================================================================
# DECODE
# ============================================================================
@app.route('/decode', methods=['GET', 'POST'])
def decode_page():
if request.method == 'POST':
@@ -647,6 +784,16 @@ def decode_page():
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
# NEW in v3.0 - Extraction mode
embed_mode = request.form.get('embed_mode', 'auto')
if embed_mode not in ('auto', 'lsb', 'dct'):
embed_mode = 'auto'
# Check DCT availability
if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Get encoding date from form (detected from filename in JS)
stego_date = request.form.get('stego_date', '').strip()
@@ -700,7 +847,7 @@ def decode_page():
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Decode
# Decode with selected mode
decode_result = decode(
stego_image=stego_data,
reference_photo=ref_data,
@@ -708,7 +855,8 @@ def decode_page():
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=stego_date if stego_date else None
date_str=stego_date if stego_date else None,
embed_mode=embed_mode, # NEW in v3.0
)
if decode_result.is_file:

View File

@@ -181,6 +181,78 @@
</div>
</div>
<!-- ================================================================
ADVANCED OPTIONS (v3.0) - Extraction Mode
================================================================ -->
<div class="mb-4">
<a class="btn btn-sm btn-outline-secondary w-100" data-bs-toggle="collapse" href="#advancedOptionsDec" role="button" aria-expanded="false">
<i class="bi bi-gear me-1"></i> Advanced Options
<i class="bi bi-chevron-down ms-1" id="advancedChevronDec"></i>
</a>
<div class="collapse" id="advancedOptionsDec">
<div class="card card-body mt-2 bg-dark border-secondary">
<!-- Extraction Mode Selection -->
<div class="mb-0">
<label class="form-label">
<i class="bi bi-cpu me-1"></i> Extraction Mode
<span class="badge bg-info ms-1">v3.0</span>
</label>
<div class="row g-2">
<!-- Auto Mode -->
<div class="col-4">
<div class="form-check card p-2 text-center h-100" id="autoModeCard">
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeAuto" value="auto" checked>
<label class="form-check-label w-100" for="modeAuto">
<i class="bi bi-magic text-success fs-4 d-block mb-1"></i>
<strong>Auto</strong>
<div class="small text-muted">Try both</div>
</label>
</div>
</div>
<!-- LSB Mode -->
<div class="col-4">
<div class="form-check card p-2 text-center h-100" id="lsbModeCardDec">
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeLsbDec" value="lsb">
<label class="form-check-label w-100" for="modeLsbDec">
<i class="bi bi-grid-3x3-gap text-primary fs-4 d-block mb-1"></i>
<strong>LSB</strong>
<div class="small text-muted">Spatial only</div>
</label>
</div>
</div>
<!-- DCT Mode -->
<div class="col-4">
<div class="form-check card p-2 text-center h-100 {% if not has_dct %}opacity-50{% endif %}" id="dctModeCardDec">
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeDctDec" value="dct" {% if not has_dct %}disabled{% endif %}>
<label class="form-check-label w-100" for="modeDctDec">
<i class="bi bi-soundwave text-info fs-4 d-block mb-1"></i>
<strong>DCT</strong>
<div class="small text-muted">
{% if has_dct %}Frequency only{% else %}N/A{% endif %}
</div>
</label>
</div>
</div>
</div>
<div class="form-text mt-2">
<i class="bi bi-lightbulb me-1"></i>
<strong>Auto</strong> tries LSB first, then DCT. Use specific mode if you know how it was encoded.
{% if not has_dct %}
<br><span class="text-warning"><i class="bi bi-exclamation-triangle me-1"></i>DCT requires scipy: <code>pip install scipy</code></span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="decodeBtn">
<i class="bi bi-unlock me-2"></i>Decode
</button>
@@ -211,10 +283,14 @@
<i class="bi bi-dot"></i>
Ensure the stego image hasn't been <strong>resized or recompressed</strong>
</li>
<li class="mb-0">
<li class="mb-2">
<i class="bi bi-dot"></i>
If using an RSA key, make sure the <strong>password is correct</strong>
</li>
<li class="mb-0">
<i class="bi bi-dot"></i>
<strong>v3.0:</strong> If auto-detection fails, try specifying <strong>LSB or DCT mode</strong> in Advanced Options
</li>
</ul>
</div>
</div>
@@ -228,7 +304,8 @@
// Form submit loading state
document.getElementById('decodeForm')?.addEventListener('submit', function() {
const btn = document.getElementById('decodeBtn');
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Decoding...';
const selectedMode = document.querySelector('input[name="embed_mode"]:checked')?.value || 'auto';
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Decoding (${selectedMode.toUpperCase()})...`;
btn.disabled = true;
});
@@ -323,6 +400,38 @@ document.getElementById('togglePin')?.addEventListener('click', function() {
}
});
// Mode card highlighting
const autoModeCard = document.getElementById('autoModeCard');
const lsbModeCardDec = document.getElementById('lsbModeCardDec');
const dctModeCardDec = document.getElementById('dctModeCardDec');
const modeAuto = document.getElementById('modeAuto');
const modeLsbDec = document.getElementById('modeLsbDec');
const modeDctDec = document.getElementById('modeDctDec');
function updateModeCardHighlightDec() {
autoModeCard?.classList.toggle('border-success', modeAuto?.checked);
autoModeCard?.classList.toggle('border-2', modeAuto?.checked);
lsbModeCardDec?.classList.toggle('border-primary', modeLsbDec?.checked);
lsbModeCardDec?.classList.toggle('border-2', modeLsbDec?.checked);
dctModeCardDec?.classList.toggle('border-info', modeDctDec?.checked);
dctModeCardDec?.classList.toggle('border-2', modeDctDec?.checked);
}
modeAuto?.addEventListener('change', updateModeCardHighlightDec);
modeLsbDec?.addEventListener('change', updateModeCardHighlightDec);
modeDctDec?.addEventListener('change', updateModeCardHighlightDec);
updateModeCardHighlightDec(); // Initial state
// Advanced options chevron rotation
document.getElementById('advancedOptionsDec')?.addEventListener('show.bs.collapse', function() {
document.getElementById('advancedChevronDec').classList.add('bi-chevron-up');
document.getElementById('advancedChevronDec').classList.remove('bi-chevron-down');
});
document.getElementById('advancedOptionsDec')?.addEventListener('hide.bs.collapse', function() {
document.getElementById('advancedChevronDec').classList.remove('bi-chevron-up');
document.getElementById('advancedChevronDec').classList.add('bi-chevron-down');
});
// Paste from Clipboard
document.addEventListener('paste', function(e) {
if (!document.getElementById('decodeForm')) return;

View File

@@ -36,7 +36,7 @@
<i class="bi bi-file-image me-1"></i> Carrier Image
</label>
<div class="drop-zone" id="carrierDropZone">
<input type="file" name="carrier" accept="image/*" required>
<input type="file" name="carrier" accept="image/*" required id="carrierInput">
<div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop image or click to browse</span>
@@ -49,6 +49,20 @@
</div>
</div>
<!-- Capacity Info Panel (shown when carrier loaded) -->
<div class="alert alert-info small d-none" id="capacityPanel">
<div class="row align-items-center">
<div class="col">
<i class="bi bi-rulers me-1"></i>
<strong>Carrier:</strong> <span id="carrierDimensions">-</span>
</div>
<div class="col-auto">
<span class="badge bg-primary me-1" id="lsbCapacityBadge">LSB: -</span>
<span class="badge bg-secondary" id="dctCapacityBadge">DCT: -</span>
</div>
</div>
</div>
<!-- Payload Type Selector -->
<div class="mb-3">
<label class="form-label">
@@ -179,6 +193,141 @@
</div>
</div>
<!-- ================================================================
ADVANCED OPTIONS (v3.0) - Collapsible Section
================================================================ -->
<div class="mb-4">
<a class="btn btn-sm btn-outline-secondary w-100" data-bs-toggle="collapse" href="#advancedOptions" role="button" aria-expanded="false">
<i class="bi bi-gear me-1"></i> Advanced Options
<i class="bi bi-chevron-down ms-1" id="advancedChevron"></i>
</a>
<div class="collapse" id="advancedOptions">
<div class="card card-body mt-2 bg-dark border-secondary">
<!-- Embedding Mode Selection -->
<div class="mb-3">
<label class="form-label">
<i class="bi bi-cpu me-1"></i> Embedding Mode
<span class="badge bg-info ms-1">v3.0</span>
</label>
<div class="row g-2">
<!-- LSB Mode Card -->
<div class="col-md-6">
<div class="form-check card p-3 h-100 border-primary border-2" id="lsbModeCard">
<input class="form-check-input" type="radio" name="embed_mode" id="modeLsb" value="lsb" checked>
<label class="form-check-label w-100" for="modeLsb">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-grid-3x3-gap text-primary fs-4 me-2"></i>
<strong>LSB Mode</strong>
<span class="badge bg-success ms-auto">Default</span>
</div>
<ul class="small text-muted mb-0 ps-3">
<li>Full color PNG output</li>
<li>Higher capacity (~375 KB/MP)</li>
<li>Faster processing</li>
</ul>
</label>
</div>
</div>
<!-- DCT Mode Card -->
<div class="col-md-6">
<div class="form-check card p-3 h-100 {% if not has_dct %}opacity-50{% endif %}" id="dctModeCard">
<input class="form-check-input" type="radio" name="embed_mode" id="modeDct" value="dct" {% if not has_dct %}disabled{% endif %}>
<label class="form-check-label w-100" for="modeDct">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-soundwave text-info fs-4 me-2"></i>
<strong>DCT Mode</strong>
{% if has_dct %}
<span class="badge bg-info ms-auto">Stealth</span>
{% else %}
<span class="badge bg-secondary ms-auto">Unavailable</span>
{% endif %}
</div>
<ul class="small text-muted mb-0 ps-3">
<li>Grayscale output (PNG/JPEG)</li>
<li>Lower capacity (~75 KB/MP)</li>
<li>Better detection resistance</li>
</ul>
{% if not has_dct %}
<div class="alert alert-warning small mt-2 mb-0 py-1 px-2">
<i class="bi bi-exclamation-triangle me-1"></i>
Requires scipy: <code>pip install scipy</code>
</div>
{% endif %}
</label>
</div>
</div>
</div>
<!-- Mode comparison hint -->
<div class="form-text mt-2" id="modeHint">
<i class="bi bi-lightbulb me-1"></i>
<strong>LSB</strong> is best for most uses.
<strong>DCT</strong> provides better stealth but smaller capacity and grayscale output.
</div>
</div>
<!-- DCT Output Format (shown only when DCT selected) -->
<div class="mb-3 d-none" id="dctOutputFormatGroup">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> DCT Output Format
</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctPngCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png" checked>
<label class="form-check-label w-100" for="dctFormatPng">
<i class="bi bi-file-earmark-image text-success fs-5 d-block"></i>
<strong>PNG</strong>
<div class="small text-muted">Lossless, larger</div>
</label>
</div>
</div>
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctJpegCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg">
<label class="form-check-label w-100" for="dctFormatJpeg">
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i>
<strong>JPEG</strong>
<div class="small text-muted">Smaller, natural</div>
</label>
</div>
</div>
</div>
<div class="form-text mt-2">
<i class="bi bi-info-circle me-1"></i>
<strong>PNG</strong> is 100% reliable. <strong>JPEG</strong> produces smaller, more natural-looking files but uses lossy compression (Q=95).
</div>
</div>
<!-- Capacity Comparison (populated by JS) -->
<div class="d-none" id="modeCapacityComparison">
<div class="alert alert-secondary small mb-0">
<div class="row text-center">
<div class="col-6 border-end">
<div class="text-muted">LSB Capacity</div>
<div class="fs-5 text-primary" id="lsbCapacityDetail">-</div>
</div>
<div class="col-6">
<div class="text-muted">DCT Capacity</div>
<div class="fs-5 text-info" id="dctCapacityDetail">-</div>
</div>
</div>
<div class="text-center mt-2 small text-muted" id="capacityRatio">
DCT is ~20% of LSB capacity
</div>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn">
<i class="bi bi-lock me-2"></i>Encode
</button>
@@ -317,7 +466,15 @@ if (rsaKeyQrInput) {
// Form submit loading state
document.getElementById('encodeForm').addEventListener('submit', function(e) {
const btn = document.getElementById('encodeBtn');
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
const selectedMode = document.querySelector('input[name="embed_mode"]:checked').value;
let modeLabel = selectedMode.toUpperCase();
if (selectedMode === 'dct') {
const outputFormat = document.querySelector('input[name="dct_output_format"]:checked')?.value || 'png';
modeLabel += `${outputFormat.toUpperCase()}`;
}
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Encoding (${modeLabel})...`;
btn.disabled = true;
});
@@ -338,12 +495,147 @@ messageInput.addEventListener('input', function() {
charCount.classList.toggle('text-danger', len > maxChars * 0.95);
});
// ============================================================================
// v3.0 - Capacity Comparison API
// ============================================================================
const carrierInput = document.getElementById('carrierInput');
const capacityPanel = document.getElementById('capacityPanel');
const carrierDimensions = document.getElementById('carrierDimensions');
const lsbCapacityBadge = document.getElementById('lsbCapacityBadge');
const dctCapacityBadge = document.getElementById('dctCapacityBadge');
const lsbCapacityDetail = document.getElementById('lsbCapacityDetail');
const dctCapacityDetail = document.getElementById('dctCapacityDetail');
const modeCapacityComparison = document.getElementById('modeCapacityComparison');
const capacityRatio = document.getElementById('capacityRatio');
let currentCapacity = null;
async function fetchCapacityComparison(file) {
const formData = new FormData();
formData.append('carrier', file);
try {
const response = await fetch('/api/compare-capacity', {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
if (data.success) {
currentCapacity = data;
updateCapacityDisplay(data);
}
}
} catch (err) {
console.error('Capacity comparison failed:', err);
}
}
function updateCapacityDisplay(data) {
// Update top panel
carrierDimensions.textContent = `${data.width} × ${data.height}`;
lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`;
if (data.dct.available) {
dctCapacityBadge.textContent = `DCT: ${data.dct.capacity_kb} KB`;
dctCapacityBadge.classList.remove('bg-secondary');
dctCapacityBadge.classList.add('bg-info');
} else {
dctCapacityBadge.textContent = `DCT: N/A`;
dctCapacityBadge.classList.remove('bg-info');
dctCapacityBadge.classList.add('bg-secondary');
}
capacityPanel.classList.remove('d-none');
// Update advanced options panel
lsbCapacityDetail.textContent = `${data.lsb.capacity_kb} KB`;
dctCapacityDetail.textContent = data.dct.available ? `${data.dct.capacity_kb} KB` : 'N/A';
capacityRatio.textContent = data.dct.available
? `DCT is ${data.dct.ratio}% of LSB capacity`
: 'DCT mode not available';
modeCapacityComparison.classList.remove('d-none');
}
// Listen for carrier file selection
if (carrierInput) {
carrierInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
fetchCapacityComparison(this.files[0]);
} else {
capacityPanel.classList.add('d-none');
modeCapacityComparison.classList.add('d-none');
currentCapacity = null;
}
});
}
// ============================================================================
// Mode card highlighting & DCT output format visibility
// ============================================================================
const lsbModeCard = document.getElementById('lsbModeCard');
const dctModeCard = document.getElementById('dctModeCard');
const modeLsb = document.getElementById('modeLsb');
const modeDct = document.getElementById('modeDct');
const dctOutputFormatGroup = document.getElementById('dctOutputFormatGroup');
const dctPngCard = document.getElementById('dctPngCard');
const dctJpegCard = document.getElementById('dctJpegCard');
const dctFormatPng = document.getElementById('dctFormatPng');
const dctFormatJpeg = document.getElementById('dctFormatJpeg');
function updateModeCardHighlight() {
// Mode cards
lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
lsbModeCard.classList.toggle('border-2', modeLsb.checked);
dctModeCard.classList.toggle('border-info', modeDct.checked);
dctModeCard.classList.toggle('border-2', modeDct.checked);
// Show/hide DCT output format selector
if (dctOutputFormatGroup) {
dctOutputFormatGroup.classList.toggle('d-none', !modeDct.checked);
}
}
function updateDctFormatCardHighlight() {
if (dctPngCard && dctJpegCard) {
dctPngCard.classList.toggle('border-success', dctFormatPng.checked);
dctPngCard.classList.toggle('border-2', dctFormatPng.checked);
dctJpegCard.classList.toggle('border-warning', dctFormatJpeg.checked);
dctJpegCard.classList.toggle('border-2', dctFormatJpeg.checked);
}
}
modeLsb.addEventListener('change', updateModeCardHighlight);
modeDct.addEventListener('change', updateModeCardHighlight);
dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight);
dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight);
updateModeCardHighlight(); // Initial state
updateDctFormatCardHighlight(); // Initial state
// Advanced options chevron rotation
document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() {
document.getElementById('advancedChevron').classList.add('bi-chevron-up');
document.getElementById('advancedChevron').classList.remove('bi-chevron-down');
});
document.getElementById('advancedOptions').addEventListener('hide.bs.collapse', function() {
document.getElementById('advancedChevron').classList.remove('bi-chevron-up');
document.getElementById('advancedChevron').classList.add('bi-chevron-down');
});
// ============================================================================
// Drag & drop with preview for images
// ============================================================================
document.querySelectorAll('.drop-zone').forEach(zone => {
const input = zone.querySelector('input[type="file"]');
const label = zone.querySelector('.drop-zone-label');
const preview = zone.querySelector('.drop-zone-preview');
const isPayloadZone = zone.id === 'payloadDropZone';
const isCarrierZone = zone.id === 'carrierDropZone';
['dragenter', 'dragover'].forEach(evt => {
zone.addEventListener(evt, e => {
@@ -367,6 +659,11 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
if (!isPayloadZone) {
showPreview(e.dataTransfer.files[0]);
}
// Trigger capacity check for carrier
if (isCarrierZone && e.dataTransfer.files[0]) {
fetchCapacityComparison(e.dataTransfer.files[0]);
}
}
});
@@ -420,6 +717,7 @@ function checkDuplicateFiles() {
document.querySelector('#carrierDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>' +
'<span class="text-muted">Drop image or click to browse</span>';
capacityPanel.classList.add('d-none');
}
}
}
@@ -443,6 +741,11 @@ document.addEventListener('paste', function(e) {
targetInput.files = container.files;
targetInput.dispatchEvent(new Event('change'));
// Trigger capacity check if pasted to carrier
if (targetInput === carrierInput) {
fetchCapacityComparison(blob);
}
break;
}
}

View File

@@ -34,6 +34,34 @@
<code class="fs-5">{{ filename }}</code>
</div>
<!-- Mode and format badges (v3.0) -->
<div class="mb-4">
{% if embed_mode == 'dct' %}
<span class="badge bg-info fs-6">
<i class="bi bi-soundwave me-1"></i>DCT Mode
</span>
{% if output_format == 'jpeg' %}
<span class="badge bg-warning text-dark fs-6 ms-1">
<i class="bi bi-file-earmark-richtext me-1"></i>JPEG
</span>
<div class="small text-muted mt-1">Grayscale JPEG, frequency domain embedding (Q=95)</div>
{% else %}
<span class="badge bg-success fs-6 ms-1">
<i class="bi bi-file-earmark-image me-1"></i>PNG
</span>
<div class="small text-muted mt-1">Grayscale PNG, frequency domain embedding (lossless)</div>
{% endif %}
{% else %}
<span class="badge bg-primary fs-6">
<i class="bi bi-grid-3x3-gap me-1"></i>LSB Mode
</span>
<span class="badge bg-success fs-6 ms-1">
<i class="bi bi-file-earmark-image me-1"></i>PNG
</span>
<div class="small text-muted mt-1">Full color PNG, spatial LSB embedding</div>
{% endif %}
</div>
<div class="d-grid gap-2">
<a href="{{ url_for('encode_download', file_id=file_id) }}"
class="btn btn-primary btn-lg" id="downloadBtn">
@@ -53,7 +81,14 @@
<ul class="mb-0 mt-2">
<li>This file expires in <strong>5 minutes</strong></li>
<li>Do <strong>not</strong> resize or recompress the image</li>
{% if embed_mode == 'dct' and output_format == 'jpeg' %}
<li>JPEG format is lossy - avoid re-saving or editing</li>
{% else %}
<li>PNG format preserves your hidden data</li>
{% endif %}
{% if embed_mode == 'dct' %}
<li>Recipient needs <strong>DCT mode</strong> or <strong>Auto</strong> detection to decode</li>
{% endif %}
</ul>
</div>
@@ -72,13 +107,14 @@
const shareBtn = document.getElementById('shareBtn');
const fileUrl = "{{ url_for('encode_file_route', file_id=file_id, _external=True) }}";
const fileName = "{{ filename }}";
const mimeType = "{{ 'image/jpeg' if embed_mode == 'dct' and output_format == 'jpeg' else 'image/png' }}";
if (navigator.share && navigator.canShare) {
// Check if we can share files
fetch(fileUrl)
.then(response => response.blob())
.then(blob => {
const file = new File([blob], fileName, { type: 'image/png' });
const file = new File([blob], fileName, { type: mimeType });
if (navigator.canShare({ files: [file] })) {
shareBtn.style.display = 'block';
shareBtn.addEventListener('click', async () => {
@@ -106,4 +142,4 @@ document.getElementById('downloadBtn').addEventListener('click', function() {
}, 2000);
});
</script>
{% endblock %}
{% endblock %}