Added file support and increased file limits.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
Stegasoo REST API
|
||||
|
||||
FastAPI-based REST API for steganography operations.
|
||||
Designed for integration with other services and automation.
|
||||
Supports both text messages and file embedding.
|
||||
"""
|
||||
|
||||
import io
|
||||
@@ -28,6 +28,8 @@ from stegasoo import (
|
||||
DAY_NAMES, __version__,
|
||||
StegasooError, DecryptionError, CapacityError,
|
||||
has_argon2,
|
||||
FilePayload,
|
||||
MAX_FILE_PAYLOAD_SIZE,
|
||||
)
|
||||
from stegasoo.constants import (
|
||||
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
||||
@@ -42,7 +44,7 @@ from stegasoo.constants import (
|
||||
|
||||
app = FastAPI(
|
||||
title="Stegasoo API",
|
||||
description="Secure steganography with hybrid authentication",
|
||||
description="Secure steganography with hybrid authentication. Supports text messages and file embedding.",
|
||||
version=__version__,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
@@ -79,6 +81,20 @@ class EncodeRequest(BaseModel):
|
||||
date_str: Optional[str] = None
|
||||
|
||||
|
||||
class EncodeFileRequest(BaseModel):
|
||||
"""Request for embedding a file (base64-encoded)."""
|
||||
file_data_base64: str
|
||||
filename: str
|
||||
mime_type: Optional[str] = None
|
||||
reference_photo_base64: str
|
||||
carrier_image_base64: str
|
||||
day_phrase: str
|
||||
pin: str = ""
|
||||
rsa_key_base64: Optional[str] = None
|
||||
rsa_password: Optional[str] = None
|
||||
date_str: Optional[str] = None
|
||||
|
||||
|
||||
class EncodeResponse(BaseModel):
|
||||
stego_image_base64: str
|
||||
filename: str
|
||||
@@ -97,7 +113,12 @@ class DecodeRequest(BaseModel):
|
||||
|
||||
|
||||
class DecodeResponse(BaseModel):
|
||||
message: str
|
||||
"""Response for decode - can be text or file."""
|
||||
payload_type: str # 'text' or 'file'
|
||||
message: Optional[str] = None # For text
|
||||
file_data_base64: Optional[str] = None # For file (base64-encoded)
|
||||
filename: Optional[str] = None # For file
|
||||
mime_type: Optional[str] = None # For file
|
||||
|
||||
|
||||
class ImageInfoResponse(BaseModel):
|
||||
@@ -112,6 +133,7 @@ class StatusResponse(BaseModel):
|
||||
version: str
|
||||
has_argon2: bool
|
||||
day_names: list[str]
|
||||
max_payload_kb: int
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
@@ -129,7 +151,8 @@ async def root():
|
||||
return StatusResponse(
|
||||
version=__version__,
|
||||
has_argon2=has_argon2(),
|
||||
day_names=list(DAY_NAMES)
|
||||
day_names=list(DAY_NAMES),
|
||||
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
|
||||
)
|
||||
|
||||
|
||||
@@ -173,7 +196,7 @@ async def api_generate(request: GenerateRequest):
|
||||
@app.post("/encode", response_model=EncodeResponse)
|
||||
async def api_encode(request: EncodeRequest):
|
||||
"""
|
||||
Encode a secret message into an image.
|
||||
Encode a text message into an image.
|
||||
|
||||
Images must be base64-encoded. Returns base64-encoded stego image.
|
||||
"""
|
||||
@@ -194,8 +217,55 @@ async def api_encode(request: EncodeRequest):
|
||||
)
|
||||
|
||||
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
||||
day_of_week = get_day_from_date(result.date_used)
|
||||
|
||||
# Get day of week from the date used
|
||||
return EncodeResponse(
|
||||
stego_image_base64=stego_b64,
|
||||
filename=result.filename,
|
||||
capacity_used_percent=result.capacity_percent,
|
||||
date_used=result.date_used,
|
||||
day_of_week=day_of_week
|
||||
)
|
||||
|
||||
except CapacityError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
except StegasooError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/encode/file", response_model=EncodeResponse)
|
||||
async def api_encode_file(request: EncodeFileRequest):
|
||||
"""
|
||||
Encode a file into an image (JSON with base64).
|
||||
|
||||
File data must be base64-encoded.
|
||||
"""
|
||||
try:
|
||||
file_data = base64.b64decode(request.file_data_base64)
|
||||
ref_photo = base64.b64decode(request.reference_photo_base64)
|
||||
carrier = base64.b64decode(request.carrier_image_base64)
|
||||
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
|
||||
|
||||
payload = FilePayload(
|
||||
data=file_data,
|
||||
filename=request.filename,
|
||||
mime_type=request.mime_type
|
||||
)
|
||||
|
||||
result = encode(
|
||||
message=payload,
|
||||
reference_photo=ref_photo,
|
||||
carrier_image=carrier,
|
||||
day_phrase=request.day_phrase,
|
||||
pin=request.pin,
|
||||
rsa_key_data=rsa_key,
|
||||
rsa_password=request.rsa_password,
|
||||
date_str=request.date_str
|
||||
)
|
||||
|
||||
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
||||
day_of_week = get_day_from_date(result.date_used)
|
||||
|
||||
return EncodeResponse(
|
||||
@@ -217,16 +287,16 @@ async def api_encode(request: EncodeRequest):
|
||||
@app.post("/decode", response_model=DecodeResponse)
|
||||
async def api_decode(request: DecodeRequest):
|
||||
"""
|
||||
Decode a secret message from a stego image.
|
||||
Decode a message or file from a stego image.
|
||||
|
||||
Images must be base64-encoded.
|
||||
Returns payload_type to indicate if result is text or file.
|
||||
"""
|
||||
try:
|
||||
stego = base64.b64decode(request.stego_image_base64)
|
||||
ref_photo = base64.b64decode(request.reference_photo_base64)
|
||||
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
|
||||
|
||||
message = decode(
|
||||
result = decode(
|
||||
stego_image=stego,
|
||||
reference_photo=ref_photo,
|
||||
day_phrase=request.day_phrase,
|
||||
@@ -235,7 +305,18 @@ async def api_decode(request: DecodeRequest):
|
||||
rsa_password=request.rsa_password
|
||||
)
|
||||
|
||||
return DecodeResponse(message=message)
|
||||
if result.is_file:
|
||||
return DecodeResponse(
|
||||
payload_type='file',
|
||||
file_data_base64=base64.b64encode(result.file_data).decode('utf-8'),
|
||||
filename=result.filename,
|
||||
mime_type=result.mime_type
|
||||
)
|
||||
else:
|
||||
return DecodeResponse(
|
||||
payload_type='text',
|
||||
message=result.message
|
||||
)
|
||||
|
||||
except DecryptionError as e:
|
||||
raise HTTPException(401, "Decryption failed. Check credentials.")
|
||||
@@ -247,10 +328,11 @@ async def api_decode(request: DecodeRequest):
|
||||
|
||||
@app.post("/encode/multipart")
|
||||
async def api_encode_multipart(
|
||||
message: str = Form(...),
|
||||
day_phrase: str = Form(...),
|
||||
reference_photo: UploadFile = File(...),
|
||||
carrier: UploadFile = File(...),
|
||||
message: str = Form(""),
|
||||
payload_file: Optional[UploadFile] = File(None),
|
||||
pin: str = Form(""),
|
||||
rsa_key: Optional[UploadFile] = File(None),
|
||||
rsa_password: str = Form(""),
|
||||
@@ -259,6 +341,7 @@ async def api_encode_multipart(
|
||||
"""
|
||||
Encode using multipart form data (file uploads).
|
||||
|
||||
Provide either 'message' (text) or 'payload_file' (binary file).
|
||||
Returns the stego image directly as PNG with metadata headers.
|
||||
"""
|
||||
try:
|
||||
@@ -266,8 +349,21 @@ async def api_encode_multipart(
|
||||
carrier_data = await carrier.read()
|
||||
rsa_key_data = await rsa_key.read() if rsa_key else None
|
||||
|
||||
# Determine payload
|
||||
if payload_file and payload_file.filename:
|
||||
file_data = await payload_file.read()
|
||||
payload = FilePayload(
|
||||
data=file_data,
|
||||
filename=payload_file.filename,
|
||||
mime_type=payload_file.content_type
|
||||
)
|
||||
elif message:
|
||||
payload = message
|
||||
else:
|
||||
raise HTTPException(400, "Must provide either 'message' or 'payload_file'")
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
message=payload,
|
||||
reference_photo=ref_data,
|
||||
carrier_image=carrier_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -277,7 +373,6 @@ async def api_encode_multipart(
|
||||
date_str=date_str if date_str else None
|
||||
)
|
||||
|
||||
# Get day of week from the date used
|
||||
day_of_week = get_day_from_date(result.date_used)
|
||||
|
||||
return Response(
|
||||
@@ -310,13 +405,15 @@ async def api_decode_multipart(
|
||||
):
|
||||
"""
|
||||
Decode using multipart form data (file uploads).
|
||||
|
||||
Returns JSON with payload_type indicating text or file.
|
||||
"""
|
||||
try:
|
||||
ref_data = await reference_photo.read()
|
||||
stego_data = await stego_image.read()
|
||||
rsa_key_data = await rsa_key.read() if rsa_key else None
|
||||
|
||||
message = decode(
|
||||
result = decode(
|
||||
stego_image=stego_data,
|
||||
reference_photo=ref_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -325,7 +422,18 @@ async def api_decode_multipart(
|
||||
rsa_password=rsa_password if rsa_password else None
|
||||
)
|
||||
|
||||
return DecodeResponse(message=message)
|
||||
if result.is_file:
|
||||
return DecodeResponse(
|
||||
payload_type='file',
|
||||
file_data_base64=base64.b64encode(result.file_data).decode('utf-8'),
|
||||
filename=result.filename,
|
||||
mime_type=result.mime_type
|
||||
)
|
||||
else:
|
||||
return DecodeResponse(
|
||||
payload_type='text',
|
||||
message=result.message
|
||||
)
|
||||
|
||||
except DecryptionError:
|
||||
raise HTTPException(401, "Decryption failed. Check credentials.")
|
||||
|
||||
@@ -20,12 +20,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
||||
|
||||
import stegasoo
|
||||
from stegasoo import (
|
||||
encode, decode, generate_credentials,
|
||||
encode, encode_file, decode,
|
||||
generate_credentials,
|
||||
export_rsa_key_pem, load_rsa_key,
|
||||
validate_image, calculate_capacity,
|
||||
get_day_from_date, parse_date_from_filename,
|
||||
DAY_NAMES, __version__,
|
||||
StegasooError, DecryptionError, ExtractionError,
|
||||
FilePayload,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +44,7 @@ def cli():
|
||||
"""
|
||||
Stegasoo - Secure steganography with hybrid authentication.
|
||||
|
||||
Hide encrypted messages in images using a combination of:
|
||||
Hide encrypted messages or files in images using a combination of:
|
||||
|
||||
\b
|
||||
• Reference photo (something you have)
|
||||
@@ -170,8 +172,9 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
||||
@cli.command()
|
||||
@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo')
|
||||
@click.option('--carrier', '-c', required=True, type=click.Path(exists=True), help='Carrier image')
|
||||
@click.option('--message', '-m', help='Message to encode (or use stdin)')
|
||||
@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read message from file')
|
||||
@click.option('--message', '-m', help='Text message to encode')
|
||||
@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read text message from file')
|
||||
@click.option('--embed-file', '-e', type=click.Path(exists=True), help='Embed a file (binary)')
|
||||
@click.option('--phrase', '-p', required=True, help='Day phrase')
|
||||
@click.option('--pin', help='Static PIN')
|
||||
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
|
||||
@@ -179,28 +182,46 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
||||
@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('--quiet', '-q', is_flag=True, help='Suppress output except errors')
|
||||
def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_password, output, date_str, quiet):
|
||||
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_password, output, date_str, quiet):
|
||||
"""
|
||||
Encode a secret message into an image.
|
||||
Encode a secret message or file into an image.
|
||||
|
||||
Requires a reference photo, carrier image, and day phrase.
|
||||
Must provide either --pin or --key (or both).
|
||||
|
||||
For text messages, use -m or -f or pipe via stdin.
|
||||
For binary files, use -e/--embed-file.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
# Text message
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
|
||||
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "word1 word2 word3" --pin 123456
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem --key-password "pass"
|
||||
|
||||
# Text from file
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -f message.txt
|
||||
|
||||
# Embed a binary file (PDF, ZIP, etc.)
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf
|
||||
|
||||
# Pipe text
|
||||
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456
|
||||
"""
|
||||
# Get message
|
||||
if message:
|
||||
msg = message
|
||||
# Determine what to encode
|
||||
payload = None
|
||||
|
||||
if embed_file:
|
||||
# Binary file embedding
|
||||
payload = FilePayload.from_file(embed_file)
|
||||
if not quiet:
|
||||
click.echo(f"Embedding file: {payload.filename} ({len(payload.data):,} bytes)")
|
||||
elif message:
|
||||
payload = message
|
||||
elif message_file:
|
||||
msg = Path(message_file).read_text()
|
||||
payload = Path(message_file).read_text()
|
||||
elif not sys.stdin.isatty():
|
||||
msg = sys.stdin.read()
|
||||
payload = sys.stdin.read()
|
||||
else:
|
||||
raise click.UsageError("Must provide message via -m, -f, or stdin")
|
||||
raise click.UsageError("Must provide message via -m, -f, -e, or stdin")
|
||||
|
||||
# Load key if provided
|
||||
rsa_key_data = None
|
||||
@@ -216,7 +237,7 @@ def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_passwo
|
||||
carrier_image = Path(carrier).read_bytes()
|
||||
|
||||
result = encode(
|
||||
message=msg,
|
||||
message=payload,
|
||||
reference_photo=ref_photo,
|
||||
carrier_image=carrier_image,
|
||||
day_phrase=phrase,
|
||||
@@ -259,19 +280,26 @@ def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_passwo
|
||||
@click.option('--pin', help='Static PIN')
|
||||
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
|
||||
@click.option('--key-password', help='RSA key password')
|
||||
@click.option('--output', '-o', type=click.Path(), help='Save message to file')
|
||||
@click.option('--quiet', '-q', is_flag=True, help='Output only the message')
|
||||
def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet):
|
||||
@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file')
|
||||
@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_password, output, quiet, force):
|
||||
"""
|
||||
Decode a secret message from a stego image.
|
||||
Decode a secret message or file from a stego image.
|
||||
|
||||
Must use the same credentials that were used for encoding.
|
||||
Automatically detects whether content is text or a file.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
# Decode and print text
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem --key-password "pass"
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o message.txt
|
||||
|
||||
# Decode and save (auto-detect type)
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt
|
||||
|
||||
# Quiet mode for piping text
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q | less
|
||||
"""
|
||||
# Load key if provided
|
||||
rsa_key_data = None
|
||||
@@ -286,7 +314,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet):
|
||||
ref_photo = Path(ref).read_bytes()
|
||||
stego_image = Path(stego).read_bytes()
|
||||
|
||||
message = decode(
|
||||
result = decode(
|
||||
stego_image=stego_image,
|
||||
reference_photo=ref_photo,
|
||||
day_phrase=phrase,
|
||||
@@ -295,18 +323,42 @@ def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet):
|
||||
rsa_password=key_password,
|
||||
)
|
||||
|
||||
if output:
|
||||
Path(output).write_text(message)
|
||||
if not quiet:
|
||||
click.secho(f"✓ Decoded successfully!", fg='green')
|
||||
click.echo(f" Saved to: {output}")
|
||||
else:
|
||||
if quiet:
|
||||
click.echo(message)
|
||||
if result.is_file:
|
||||
# File content
|
||||
if output:
|
||||
out_path = Path(output)
|
||||
elif result.filename:
|
||||
out_path = Path(result.filename)
|
||||
else:
|
||||
click.secho("✓ Decoded successfully!", fg='green')
|
||||
click.echo()
|
||||
click.echo(message)
|
||||
out_path = Path("decoded_file")
|
||||
|
||||
if out_path.exists() and not force:
|
||||
raise click.ClickException(
|
||||
f"Output file '{out_path}' exists. Use --force to overwrite."
|
||||
)
|
||||
|
||||
out_path.write_bytes(result.file_data)
|
||||
|
||||
if not quiet:
|
||||
click.secho("✓ Decoded file successfully!", fg='green')
|
||||
click.echo(f" Saved to: {out_path}")
|
||||
click.echo(f" Size: {len(result.file_data):,} bytes")
|
||||
if result.mime_type:
|
||||
click.echo(f" Type: {result.mime_type}")
|
||||
else:
|
||||
# Text content
|
||||
if output:
|
||||
Path(output).write_text(result.message)
|
||||
if not quiet:
|
||||
click.secho("✓ Decoded successfully!", fg='green')
|
||||
click.echo(f" Saved to: {output}")
|
||||
else:
|
||||
if quiet:
|
||||
click.echo(result.message)
|
||||
else:
|
||||
click.secho("✓ Decoded successfully!", fg='green')
|
||||
click.echo()
|
||||
click.echo(result.message)
|
||||
|
||||
except (DecryptionError, ExtractionError) as e:
|
||||
raise click.ClickException(f"Decryption failed: {e}")
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
Stegasoo Web Frontend
|
||||
|
||||
Flask-based web UI for steganography operations.
|
||||
This is a thin wrapper around the stegasoo library.
|
||||
Supports both text messages and file embedding.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import secrets
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
@@ -27,14 +28,17 @@ from stegasoo import (
|
||||
export_rsa_key_pem, load_rsa_key,
|
||||
validate_pin, validate_message, validate_image,
|
||||
validate_rsa_key, validate_security_factors,
|
||||
validate_file_payload,
|
||||
get_today_day, generate_filename,
|
||||
DAY_NAMES, __version__,
|
||||
StegasooError, DecryptionError, CapacityError,
|
||||
has_argon2,
|
||||
FilePayload,
|
||||
MAX_FILE_PAYLOAD_SIZE,
|
||||
)
|
||||
from stegasoo.constants import (
|
||||
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
||||
VALID_RSA_SIZES,
|
||||
VALID_RSA_SIZES, MAX_FILE_SIZE,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +48,7 @@ from stegasoo.constants import (
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = secrets.token_hex(32)
|
||||
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5MB max upload
|
||||
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 10MB max upload
|
||||
|
||||
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
|
||||
TEMP_FILES: dict[str, dict] = {}
|
||||
@@ -67,6 +71,16 @@ def allowed_image(filename: str) -> bool:
|
||||
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
|
||||
|
||||
|
||||
def format_size(size_bytes: int) -> str:
|
||||
"""Format file size for display."""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ROUTES
|
||||
# ============================================================================
|
||||
@@ -164,6 +178,7 @@ def download_key():
|
||||
@app.route('/encode', methods=['GET', 'POST'])
|
||||
def encode_page():
|
||||
day_of_week = get_today_day()
|
||||
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
@@ -171,30 +186,50 @@ def encode_page():
|
||||
ref_photo = request.files.get('reference_photo')
|
||||
carrier = request.files.get('carrier')
|
||||
rsa_key_file = request.files.get('rsa_key')
|
||||
payload_file = request.files.get('payload_file')
|
||||
|
||||
if not ref_photo or not carrier:
|
||||
flash('Both reference photo and carrier image are required', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
|
||||
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Get form data
|
||||
message = request.form.get('message', '')
|
||||
day_phrase = request.form.get('day_phrase', '')
|
||||
pin = request.form.get('pin', '').strip()
|
||||
rsa_password = request.form.get('rsa_password', '')
|
||||
payload_type = request.form.get('payload_type', 'text')
|
||||
|
||||
# Validate message
|
||||
result = validate_message(message)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
# Determine payload
|
||||
if payload_type == 'file' and payload_file and payload_file.filename:
|
||||
# File payload
|
||||
file_data = payload_file.read()
|
||||
|
||||
result = validate_file_payload(file_data, payload_file.filename)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(payload_file.filename)
|
||||
payload = FilePayload(
|
||||
data=file_data,
|
||||
filename=payload_file.filename,
|
||||
mime_type=mime_type
|
||||
)
|
||||
else:
|
||||
# Text message
|
||||
result = validate_message(message)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
payload = message
|
||||
|
||||
if not day_phrase:
|
||||
flash('Day phrase is required', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Read files
|
||||
ref_data = ref_photo.read()
|
||||
@@ -205,27 +240,27 @@ def encode_page():
|
||||
result = validate_security_factors(pin, rsa_key_data)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Validate PIN if provided
|
||||
if pin:
|
||||
result = validate_pin(pin)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Validate RSA key if provided
|
||||
if rsa_key_data:
|
||||
result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Validate carrier image
|
||||
result = validate_image(carrier_data, "Carrier image")
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Get date
|
||||
client_date = request.form.get('client_date', '').strip()
|
||||
@@ -236,7 +271,7 @@ def encode_page():
|
||||
|
||||
# Encode
|
||||
encode_result = encode(
|
||||
message=message,
|
||||
message=payload,
|
||||
reference_photo=ref_data,
|
||||
carrier_image=carrier_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -259,15 +294,15 @@ def encode_page():
|
||||
|
||||
except CapacityError as e:
|
||||
flash(str(e), 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
except StegasooError as e:
|
||||
flash(str(e), 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
except Exception as e:
|
||||
flash(f'Error: {e}', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
|
||||
@app.route('/encode/result/<file_id>')
|
||||
@@ -299,7 +334,7 @@ def encode_download(file_id):
|
||||
|
||||
|
||||
@app.route('/encode/file/<file_id>')
|
||||
def encode_file(file_id):
|
||||
def encode_file_route(file_id):
|
||||
"""Serve file for Web Share API."""
|
||||
if file_id not in TEMP_FILES:
|
||||
return "Not found", 404
|
||||
@@ -368,7 +403,7 @@ def decode_page():
|
||||
return render_template('decode.html')
|
||||
|
||||
# Decode
|
||||
message = decode(
|
||||
decode_result = decode(
|
||||
stego_image=stego_data,
|
||||
reference_photo=ref_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -377,7 +412,29 @@ def decode_page():
|
||||
rsa_password=rsa_password if rsa_password else None
|
||||
)
|
||||
|
||||
return render_template('decode.html', decoded_message=message)
|
||||
if decode_result.is_file:
|
||||
# File content - store temporarily for download
|
||||
file_id = secrets.token_urlsafe(16)
|
||||
cleanup_temp_files()
|
||||
|
||||
filename = decode_result.filename or 'decoded_file'
|
||||
TEMP_FILES[file_id] = {
|
||||
'data': decode_result.file_data,
|
||||
'filename': filename,
|
||||
'mime_type': decode_result.mime_type,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
return render_template('decode.html',
|
||||
decoded_file=True,
|
||||
file_id=file_id,
|
||||
filename=filename,
|
||||
file_size=format_size(len(decode_result.file_data)),
|
||||
mime_type=decode_result.mime_type
|
||||
)
|
||||
else:
|
||||
# Text content
|
||||
return render_template('decode.html', decoded_message=decode_result.message)
|
||||
|
||||
except DecryptionError:
|
||||
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
|
||||
@@ -392,9 +449,30 @@ def decode_page():
|
||||
return render_template('decode.html')
|
||||
|
||||
|
||||
@app.route('/decode/download/<file_id>')
|
||||
def decode_download(file_id):
|
||||
"""Download decoded file."""
|
||||
if file_id not in TEMP_FILES:
|
||||
flash('File expired or not found.', 'error')
|
||||
return redirect(url_for('decode_page'))
|
||||
|
||||
file_info = TEMP_FILES[file_id]
|
||||
mime_type = file_info.get('mime_type', 'application/octet-stream')
|
||||
|
||||
return send_file(
|
||||
io.BytesIO(file_info['data']),
|
||||
mimetype=mime_type,
|
||||
as_attachment=True,
|
||||
download_name=file_info['filename']
|
||||
)
|
||||
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
return render_template('about.html', has_argon2=has_argon2())
|
||||
return render_template('about.html',
|
||||
has_argon2=has_argon2(),
|
||||
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-unlock-fill me-2"></i>Decode Secret Message</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-unlock-fill me-2"></i>Decode Secret Message or File</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if decoded_message %}
|
||||
<!-- Text Message Result -->
|
||||
<div class="alert alert-success">
|
||||
<h6><i class="bi bi-check-circle me-2"></i>Message Decrypted Successfully!</h6>
|
||||
</div>
|
||||
@@ -23,17 +24,40 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
i<!--then<div class="mb-4">
|
||||
<label class="form-label text-muted">Decoded Message:</label>
|
||||
<div class="alert-message">{{ decoded_message }}</div>
|
||||
</div>-->
|
||||
<a href="/decode" class="btn btn-outline-light w-100 mt-3">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Decode Another
|
||||
</a>
|
||||
|
||||
{% elif decoded_file %}
|
||||
<!-- File Result -->
|
||||
<div class="alert alert-success">
|
||||
<h6><i class="bi bi-check-circle me-2"></i>File Decrypted Successfully!</h6>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<i class="bi bi-file-earmark-check text-success" style="font-size: 4rem;"></i>
|
||||
<h5 class="mt-3">{{ filename }}</h5>
|
||||
<p class="text-muted mb-1">{{ file_size }}</p>
|
||||
{% if mime_type %}
|
||||
<small class="text-muted">Type: {{ mime_type }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('decode_download', file_id=file_id) }}" class="btn btn-primary btn-lg w-100 mb-3">
|
||||
<i class="bi bi-download me-2"></i>Download File
|
||||
</a>
|
||||
|
||||
<div class="alert alert-warning small">
|
||||
<i class="bi bi-clock me-1"></i>
|
||||
<strong>File expires in 5 minutes.</strong> Download now.
|
||||
</div>
|
||||
|
||||
<a href="/decode" class="btn btn-outline-light w-100">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Decode Another Message
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Decode Another
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
|
||||
<!-- Decode Form -->
|
||||
<form method="POST" enctype="multipart/form-data" id="decodeForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
@@ -66,7 +90,7 @@
|
||||
<img class="drop-zone-preview d-none">
|
||||
</div>
|
||||
<div class="form-text">
|
||||
The image containing the hidden message
|
||||
The image containing the hidden message/file
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,7 +154,7 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="decodeBtn">
|
||||
<i class="bi bi-unlock me-2"></i>Decode Message
|
||||
<i class="bi bi-unlock me-2"></i>Decode
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -138,6 +162,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not decoded_message and not decoded_file %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted mb-3"><i class="bi bi-question-circle me-2"></i>Troubleshooting</h6>
|
||||
@@ -165,6 +190,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -209,7 +235,7 @@ function updateDayLabel(dayName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1. PIN Toggle
|
||||
// PIN Toggle
|
||||
document.getElementById('togglePin')?.addEventListener('click', function() {
|
||||
const input = document.getElementById('pinInput');
|
||||
const icon = this.querySelector('i');
|
||||
@@ -222,9 +248,8 @@ document.getElementById('togglePin')?.addEventListener('click', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Paste from Clipboard
|
||||
// Paste from Clipboard
|
||||
document.addEventListener('paste', function(e) {
|
||||
// Only run if the form exists (we are not on the success page)
|
||||
if (!document.getElementById('decodeForm')) return;
|
||||
|
||||
const items = e.clipboardData.items;
|
||||
@@ -232,7 +257,6 @@ document.addEventListener('paste', function(e) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
const blob = items[i].getAsFile();
|
||||
|
||||
// Priority for Decode: Fill Stego Image first (most common), then Reference
|
||||
const stegoInput = document.querySelector('input[name="stego_image"]');
|
||||
const refInput = document.querySelector('input[name="reference_photo"]');
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-lock-fill me-2"></i>Encode Secret Message</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-lock-fill me-2"></i>Encode Secret Message or File</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data" id="encodeForm">
|
||||
@@ -49,15 +49,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payload Type Selector -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-box me-1"></i> What to Encode
|
||||
</label>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="payload_type" id="payloadText" value="text" checked>
|
||||
<label class="btn btn-outline-primary" for="payloadText">
|
||||
<i class="bi bi-chat-left-text me-1"></i> Text Message
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="payload_type" id="payloadFile" value="file">
|
||||
<label class="btn btn-outline-primary" for="payloadFile">
|
||||
<i class="bi bi-file-earmark me-1"></i> File
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Message Input -->
|
||||
<div class="mb-3" id="textPayloadSection">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-chat-left-text me-1"></i> Secret Message
|
||||
</label>
|
||||
<textarea name="message" class="form-control" rows="4" id="messageInput"
|
||||
placeholder="Enter your secret message here..." required></textarea>
|
||||
placeholder="Enter your secret message here..."></textarea>
|
||||
<div class="d-flex justify-content-between form-text">
|
||||
<span>
|
||||
<span id="charCount">0</span> / 50,000 characters
|
||||
<span id="charCount">0</span> / 250,000 characters
|
||||
<span id="charWarning" class="text-warning d-none ms-2">
|
||||
<i class="bi bi-exclamation-triangle"></i> Getting long!
|
||||
</span>
|
||||
@@ -66,6 +85,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Input -->
|
||||
<div class="mb-3 d-none" id="filePayloadSection">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark me-1"></i> File to Embed
|
||||
</label>
|
||||
<div class="drop-zone" id="payloadDropZone">
|
||||
<input type="file" name="payload_file" id="payloadFileInput">
|
||||
<div class="drop-zone-label" id="payloadDropLabel">
|
||||
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
|
||||
<span class="text-muted">Drop any file or click to browse</span>
|
||||
<div class="small text-muted mt-1">Max {{ max_payload_kb }} KB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Supports any file type: PDF, ZIP, documents, etc.
|
||||
</div>
|
||||
<div id="fileInfo" class="d-none mt-2 p-2 bg-dark rounded">
|
||||
<i class="bi bi-file-earmark-check text-success me-2"></i>
|
||||
<span id="fileInfoName"></span>
|
||||
<span class="text-muted">(<span id="fileInfoSize"></span>)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" id="dayPhraseLabel">
|
||||
<i class="bi bi-chat-quote me-1"></i> {{ day_of_week }}'s Phrase
|
||||
@@ -121,7 +163,7 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn">
|
||||
<i class="bi bi-lock me-2"></i>Encode Message
|
||||
<i class="bi bi-lock me-2"></i>Encode
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -146,8 +188,8 @@
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Limits:</strong>
|
||||
Carrier image max ~4 megapixels (2000×2000).
|
||||
Files max 5MB each.
|
||||
Message max 50KB.
|
||||
Files max 10MB upload.
|
||||
Payload max {{ max_payload_kb }} KB.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,6 +219,57 @@ if (dateInput) {
|
||||
dateInput.value = localDate;
|
||||
}
|
||||
|
||||
// Payload type switching
|
||||
const payloadTextRadio = document.getElementById('payloadText');
|
||||
const payloadFileRadio = document.getElementById('payloadFile');
|
||||
const textSection = document.getElementById('textPayloadSection');
|
||||
const fileSection = document.getElementById('filePayloadSection');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const payloadFileInput = document.getElementById('payloadFileInput');
|
||||
|
||||
function updatePayloadSection() {
|
||||
const isText = payloadTextRadio.checked;
|
||||
textSection.classList.toggle('d-none', !isText);
|
||||
fileSection.classList.toggle('d-none', isText);
|
||||
|
||||
// Update required attribute
|
||||
if (isText) {
|
||||
messageInput.required = true;
|
||||
payloadFileInput.required = false;
|
||||
} else {
|
||||
messageInput.required = false;
|
||||
payloadFileInput.required = true;
|
||||
}
|
||||
}
|
||||
|
||||
payloadTextRadio.addEventListener('change', updatePayloadSection);
|
||||
payloadFileRadio.addEventListener('change', updatePayloadSection);
|
||||
|
||||
// File payload info display
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const fileInfoName = document.getElementById('fileInfoName');
|
||||
const fileInfoSize = document.getElementById('fileInfoSize');
|
||||
const payloadDropLabel = document.getElementById('payloadDropLabel');
|
||||
|
||||
payloadFileInput.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const file = this.files[0];
|
||||
fileInfoName.textContent = file.name;
|
||||
fileInfoSize.textContent = formatFileSize(file.size);
|
||||
fileInfo.classList.remove('d-none');
|
||||
payloadDropLabel.innerHTML = `<i class="bi bi-check-circle text-success fs-3 d-block mb-2"></i><span>${file.name}</span>`;
|
||||
} else {
|
||||
fileInfo.classList.add('d-none');
|
||||
payloadDropLabel.innerHTML = `<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i><span class="text-muted">Drop any file or click to browse</span><div class="small text-muted mt-1">Max {{ max_payload_kb }} KB</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// Show RSA password field when key is selected
|
||||
const rsaKeyInput = document.getElementById('rsaKeyInput');
|
||||
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
|
||||
@@ -192,12 +285,11 @@ document.getElementById('encodeForm').addEventListener('submit', function(e) {
|
||||
btn.disabled = true;
|
||||
});
|
||||
|
||||
// Character counter
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
// Character counter for text
|
||||
const charCount = document.getElementById('charCount');
|
||||
const charWarning = document.getElementById('charWarning');
|
||||
const charPercent = document.getElementById('charPercent');
|
||||
const maxChars = 50000;
|
||||
const maxChars = 250000;
|
||||
|
||||
messageInput.addEventListener('input', function() {
|
||||
const len = this.value.length;
|
||||
@@ -210,11 +302,12 @@ messageInput.addEventListener('input', function() {
|
||||
charCount.classList.toggle('text-danger', len > maxChars * 0.95);
|
||||
});
|
||||
|
||||
// Drag & drop with preview
|
||||
// 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';
|
||||
|
||||
['dragenter', 'dragover'].forEach(evt => {
|
||||
zone.addEventListener(evt, e => {
|
||||
@@ -233,29 +326,38 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
|
||||
zone.addEventListener('drop', e => {
|
||||
if (e.dataTransfer.files.length) {
|
||||
input.files = e.dataTransfer.files;
|
||||
showPreview(e.dataTransfer.files[0]);
|
||||
input.dispatchEvent(new Event('change'));
|
||||
|
||||
if (!isPayloadZone) {
|
||||
showPreview(e.dataTransfer.files[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
showPreview(this.files[0]);
|
||||
}
|
||||
});
|
||||
if (!isPayloadZone) {
|
||||
input.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
showPreview(this.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showPreview(file) {
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
preview.src = e.target.result;
|
||||
preview.classList.remove('d-none');
|
||||
if (preview) {
|
||||
preview.src = e.target.result;
|
||||
preview.classList.remove('d-none');
|
||||
}
|
||||
label.innerHTML = '<i class="bi bi-check-circle text-success me-1"></i>' + file.name;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
// 1. PIN Toggle Logic
|
||||
|
||||
// PIN Toggle Logic
|
||||
document.getElementById('togglePin').addEventListener('click', function() {
|
||||
const input = document.getElementById('pinInput');
|
||||
const icon = this.querySelector('i');
|
||||
@@ -268,17 +370,16 @@ document.getElementById('togglePin').addEventListener('click', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Prevent Same File Selection
|
||||
// Prevent Same File Selection
|
||||
function checkDuplicateFiles() {
|
||||
const refInput = document.querySelector('input[name="reference_photo"]');
|
||||
const carInput = document.querySelector('input[name="carrier"]');
|
||||
|
||||
if (refInput.files[0] && carInput.files[0]) {
|
||||
// Compare name and size as a proxy for identical files
|
||||
if (refInput.files[0].name === carInput.files[0].name &&
|
||||
refInput.files[0].size === carInput.files[0].size) {
|
||||
alert("Security Warning: You cannot use the same image for both Reference and Carrier!");
|
||||
carInput.value = ''; // Clear carrier
|
||||
carInput.value = '';
|
||||
document.getElementById('carrierPreview').classList.add('d-none');
|
||||
document.querySelector('#carrierDropZone .drop-zone-label').innerHTML =
|
||||
'<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>' +
|
||||
@@ -289,14 +390,13 @@ function checkDuplicateFiles() {
|
||||
document.querySelector('input[name="reference_photo"]').addEventListener('change', checkDuplicateFiles);
|
||||
document.querySelector('input[name="carrier"]').addEventListener('change', checkDuplicateFiles);
|
||||
|
||||
// 3. Paste from Clipboard
|
||||
// Paste from Clipboard
|
||||
document.addEventListener('paste', function(e) {
|
||||
const items = e.clipboardData.items;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
const blob = items[i].getAsFile();
|
||||
|
||||
// Priority: Fill Carrier first, if empty. Else fill Reference.
|
||||
const carrierInput = document.querySelector('input[name="carrier"]');
|
||||
const refInput = document.querySelector('input[name="reference_photo"]');
|
||||
|
||||
@@ -307,7 +407,7 @@ document.addEventListener('paste', function(e) {
|
||||
targetInput.files = container.files;
|
||||
|
||||
targetInput.dispatchEvent(new Event('change'));
|
||||
break; // Only paste one image at a time
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user