Version 3.1.0 now with experimental DCT support.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user