fieldwitness/frontends/web/templates/stego/encode.html
Aaron D. Lee 490f9d4a1d Rebrand SooSeF to FieldWitness
Complete project rebrand for better positioning in the press freedom
and digital security space. FieldWitness communicates both field
deployment and evidence testimony — appropriate for the target audience
of journalists, NGOs, and human rights organizations.

Rename mapping:
- soosef → fieldwitness (package, CLI, all imports)
- soosef.stegasoo → fieldwitness.stego
- soosef.verisoo → fieldwitness.attest
- ~/.soosef/ → ~/.fwmetadata/ (innocuous data dir name)
- SOOSEF_DATA_DIR → FIELDWITNESS_DATA_DIR
- SoosefConfig → FieldWitnessConfig
- SoosefError → FieldWitnessError

Also includes:
- License switch from MIT to GPL-3.0
- C2PA bridge module (Phase 0-2 MVP): cert.py, export.py, vendor_assertions.py
- README repositioned to lead with provenance/federation, stego backgrounded
- Threat model skeleton at docs/security/threat-model.md
- Planning docs: docs/planning/c2pa-integration.md, docs/planning/gtm-feasibility.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:05:13 -04:00

891 lines
51 KiB
HTML

{% extends "base.html" %}
{% block title %}Encode Message - Stego{% endblock %}
{% block content %}
<style>
/* Accordion styling */
.step-accordion .accordion-button {
background: rgba(35, 45, 55, 0.8);
color: #fff;
padding: 0.75rem 1rem;
border-left: 3px solid rgba(255, 230, 153, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.3s ease;
}
.step-accordion .accordion-button:hover {
background: rgba(45, 55, 65, 0.9);
border-left-color: rgba(255, 230, 153, 0.5);
}
.step-accordion .accordion-button:not(.collapsed) {
background: linear-gradient(90deg, rgba(255, 230, 153, 0.12) 0%, rgba(40, 50, 60, 0.85) 40%, rgba(40, 50, 60, 0.85) 100%);
color: #fff;
box-shadow: inset 0 1px 0 rgba(255, 230, 153, 0.1);
border-left: 3px solid #ffe699;
}
.step-accordion .accordion-button::after {
filter: brightness(0) invert(1);
opacity: 0.5;
}
.step-accordion .accordion-button:not(.collapsed)::after {
opacity: 0.9;
}
.step-accordion .accordion-body {
background: rgba(30, 40, 50, 0.4);
padding: 1rem;
}
.step-accordion .accordion-item {
border-color: rgba(255,255,255,0.1);
background: transparent;
}
.step-accordion .accordion-item:first-child .accordion-button {
border-radius: 0;
}
.step-accordion .accordion-item:last-child .accordion-button.collapsed {
border-radius: 0;
}
.step-summary {
font-size: 0.8rem;
color: rgba(255,255,255,0.5);
margin-left: auto;
padding-right: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 50%;
}
.step-summary.has-content {
color: rgba(99, 179, 237, 0.8);
}
.step-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.step-number {
background: rgba(246, 173, 85, 0.2);
color: #f6ad55;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: bold;
border: 1px solid rgba(246, 173, 85, 0.3);
}
.step-number.complete {
background: rgba(72, 187, 120, 0.2);
color: #48bb78;
border-color: rgba(72, 187, 120, 0.3);
}
/* Glowing passphrase input */
.passphrase-input {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(99, 179, 237, 0.3) !important;
color: #63b3ed !important;
font-family: 'Courier New', monospace;
font-size: 1.1rem;
letter-spacing: 0.5px;
padding: 12px 16px;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.passphrase-input:focus {
border-color: rgba(99, 179, 237, 0.8) !important;
box-shadow: 0 0 20px rgba(99, 179, 237, 0.4) !important;
}
.passphrase-input::placeholder {
color: rgba(99, 179, 237, 0.4);
}
/* PIN input */
.pin-input-container .form-control {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(246, 173, 85, 0.3) !important;
color: #f6ad55 !important;
font-family: 'Courier New', monospace;
font-size: 1.2rem;
letter-spacing: 3px;
text-align: center;
}
.pin-input-container .form-control:focus {
border-color: rgba(246, 173, 85, 0.8) !important;
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4) !important;
}
/* QR Crop Animation - uses .qr-scan-container from style.css */
</style>
<div class="row justify-content-center">
<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 or File</h5>
</div>
<div class="card-body p-0">
<form method="POST" enctype="multipart/form-data" id="encodeForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="accordion step-accordion" id="encodeAccordion">
<!-- ================================================================
STEP 1: CARRIER & MODE
================================================================ -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#stepImages">
<span class="step-title">
<span class="step-number" id="stepImagesNumber">1</span>
<i class="bi bi-images me-1"></i> Carrier & Mode
</span>
<span class="step-summary" id="stepImagesSummary">Select reference & carrier</span>
</button>
</h2>
<div id="stepImages" class="accordion-collapse collapse show" data-bs-parent="#encodeAccordion">
<div class="accordion-body">
<input type="hidden" name="carrier_type" id="carrierTypeInput" value="image">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-image me-1"></i> Reference Photo
</label>
<div class="drop-zone scan-container" id="refDropZone">
<input type="file" name="reference_photo" accept="image/*" required id="refPhotoInput">
<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</span>
</div>
<img class="drop-zone-preview d-none" id="refPreview">
<div class="scan-overlay"><div class="scan-grid"></div><div class="scan-line"></div></div>
<div class="scan-corners">
<div class="scan-corner tl"></div><div class="scan-corner tr"></div>
<div class="scan-corner bl"></div><div class="scan-corner br"></div>
</div>
<div class="scan-data-panel">
<div class="scan-data-filename"><i class="bi bi-check-circle-fill"></i><span id="refFileName">image.jpg</span></div>
<div class="scan-data-row"><span class="scan-status-badge">Hash Acquired</span><span class="scan-data-value" id="refFileSize">--</span></div>
</div>
</div>
<div class="form-text">Secret photo both parties have</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark me-1"></i> Carrier File
</label>
<div id="imageCarrierSection">
<div class="drop-zone pixel-container" id="carrierDropZone">
<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</span>
</div>
<img class="drop-zone-preview d-none" id="carrierPreview">
<div class="pixel-blocks"></div>
<div class="pixel-scan-line"></div>
<div class="pixel-corners">
<div class="pixel-corner tl"></div><div class="pixel-corner tr"></div>
<div class="pixel-corner bl"></div><div class="pixel-corner br"></div>
</div>
<div class="pixel-data-panel">
<div class="pixel-data-filename"><i class="bi bi-check-circle-fill"></i><span id="carrierFileName">image.jpg</span></div>
<div class="pixel-data-row"><span class="pixel-status-badge">Carrier Loaded</span><span class="pixel-data-value" id="carrierFileSize">--</span></div>
<div class="pixel-dimensions" id="carrierDims">-- x -- px</div>
</div>
</div>
<div class="form-text" id="imageCarrierHint">Image to hide your message in</div>
</div>
<!-- Audio Carrier (hidden by default, shown when audio type selected) -->
<div class="d-none" id="audioCarrierSection">
<div class="drop-zone pixel-container" id="audioCarrierDropZone">
<input type="file" name="audio_carrier" accept="audio/*" id="audioCarrierInput">
<div class="drop-zone-label">
<i class="bi bi-music-note-beamed fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop audio or click</span>
</div>
<div class="pixel-data-panel">
<div class="pixel-data-filename"><i class="bi bi-check-circle-fill"></i><span id="audioCarrierFileName">audio.wav</span></div>
<div class="pixel-data-row"><span class="pixel-status-badge">Audio Loaded</span><span class="pixel-data-value" id="audioCarrierFileSize">--</span></div>
<div class="pixel-dimensions" id="audioCarrierDuration">--:-- duration</div>
</div>
</div>
<div class="form-text" id="audioCarrierHint">Audio file to hide your message in</div>
</div>
</div>
</div>
<!-- Capacity Info -->
<div class="alert alert-info small d-none mb-3" id="capacityPanel">
<div class="d-flex justify-content-between align-items-center">
<span><i class="bi bi-rulers me-1"></i><span id="carrierDimensions">-</span></span>
<span>
<span class="badge bg-warning text-dark me-1" id="dctCapacityBadge">DCT: -</span>
<span class="badge bg-primary" id="lsbCapacityBadge">LSB: -</span>
</span>
</div>
</div>
<!-- Audio Capacity Info (v4.3.0) -->
<div class="alert alert-info small d-none mb-3" id="audioCapacityPanel">
<div class="d-flex justify-content-between align-items-center">
<span><i class="bi bi-music-note-beamed me-1"></i><span id="audioInfo">-</span></span>
<span>
<span class="badge bg-primary me-1" id="lsbAudioCapacityBadge">LSB: -</span>
<span class="badge bg-warning text-dark" id="spreadCapacityBadge">Spread: -</span>
</span>
</div>
</div>
<!-- Mode & Carrier Type toggles (aligned row) -->
<div class="row">
<div class="col-md-6">
<div id="imageModeGroup">
<div class="d-flex gap-2 align-items-center flex-wrap mb-2">
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="embed_mode" id="modeDct" value="dct" {% if has_dct %}checked{% endif %} {% if not has_dct %}disabled{% endif %}>
<label class="btn btn-outline-secondary btn-sm text-nowrap" for="modeDct" id="dctModeLabel"><i class="bi bi-soundwave me-1"></i>DCT</label>
<input type="radio" class="btn-check" name="embed_mode" id="modeLsb" value="lsb" {% if not has_dct %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm text-nowrap" for="modeLsb"><i class="bi bi-grid-3x3-gap me-1"></i>LSB</label>
</div>
<span class="text-muted d-none d-sm-inline">|</span>
<span class="d-flex gap-2 align-items-center" id="outputOptions">
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="dct_color_mode" id="colorMode" value="color" checked>
<label class="btn btn-outline-secondary btn-sm" for="colorMode">Color</label>
<input type="radio" class="btn-check" name="dct_color_mode" id="grayMode" value="grayscale">
<label class="btn btn-outline-secondary btn-sm" for="grayMode" id="grayModeLabel">Gray</label>
</div>
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="dct_output_format" id="jpegFormat" value="jpeg" checked>
<label class="btn btn-outline-secondary btn-sm" for="jpegFormat" id="jpegFormatLabel">JPEG</label>
<input type="radio" class="btn-check" name="dct_output_format" id="pngFormat" value="png">
<label class="btn btn-outline-secondary btn-sm" for="pngFormat">PNG</label>
</div>
</span>
</div>
</div>
<!-- Audio Modes (hidden by default) -->
<div class="d-none" id="audioModeGroup">
<div class="btn-group btn-group-sm mb-2" role="group">
<input type="radio" class="btn-check" name="embed_mode" id="modeAudioLsb" value="audio_lsb">
<label class="btn btn-outline-secondary btn-sm text-nowrap" for="modeAudioLsb"><i class="bi bi-grid-3x3-gap me-1"></i>LSB</label>
<input type="radio" class="btn-check" name="embed_mode" id="modeAudioSpread" value="audio_spread">
<label class="btn btn-outline-secondary btn-sm text-nowrap" for="modeAudioSpread"><i class="bi bi-broadcast me-1"></i>Spread</label>
</div>
</div>
<div class="form-text" id="modeHint">
<i class="bi bi-{% if has_dct %}phone{% else %}hdd{% endif %} me-1"></i>{% if has_dct %}Survives social media compression{% else %}Higher capacity for direct transfers{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center gap-2">
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="carrier_type_select" id="typeImage" value="image" checked>
<label class="btn btn-outline-secondary btn-sm text-nowrap" for="typeImage"><i class="bi bi-image me-1"></i>Image</label>
<input type="radio" class="btn-check" name="carrier_type_select" id="typeAudio" value="audio" {% if not has_audio %}disabled{% endif %}>
<label class="btn btn-outline-secondary btn-sm text-nowrap {% if not has_audio %}disabled text-muted{% endif %}" for="typeAudio"><i class="bi bi-music-note-beamed me-1"></i>Audio</label>
</div>
{% if not has_audio %}
<span class="form-text text-warning mb-0" style="font-size: 0.7rem;"><i class="bi bi-exclamation-triangle me-1"></i>Requires numpy + soundfile</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ================================================================
STEP 2: PAYLOAD
================================================================ -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#stepPayload">
<span class="step-title">
<span class="step-number" id="stepPayloadNumber">2</span>
<i class="bi bi-box me-1"></i> Payload
</span>
<span class="step-summary" id="stepPayloadSummary">Message or file to hide</span>
</button>
</h2>
<div id="stepPayload" class="accordion-collapse collapse" data-bs-parent="#encodeAccordion">
<div class="accordion-body">
<!-- Payload Type Toggle -->
<div class="btn-group w-100 mb-3" 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>
<!-- Text Message -->
<div id="textPayloadSection">
<textarea name="message" class="form-control" rows="4" id="messageInput"
placeholder="Enter your secret message here..."></textarea>
<div class="d-flex justify-content-between form-text">
<span><span id="charCount">0</span> chars</span>
<span id="charPercent" class="text-muted">0%</span>
</div>
</div>
<!-- File Upload -->
<div class="d-none" id="filePayloadSection">
<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 file or click (max {{ max_payload_kb }} KB)</span>
</div>
</div>
<div id="fileInfo" class="d-none mt-2 p-2 bg-dark rounded small">
<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>
</div>
</div>
<!-- ================================================================
STEP 3: SECURITY
================================================================ -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#stepSecurity">
<span class="step-title">
<span class="step-number" id="stepSecurityNumber">3</span>
<i class="bi bi-shield-lock me-1"></i> Security
</span>
<span class="step-summary" id="stepSecuritySummary">Passphrase & keys</span>
</button>
</h2>
<div id="stepSecurity" class="accordion-collapse collapse" data-bs-parent="#encodeAccordion">
<div class="accordion-body">
<!-- Passphrase -->
<div class="mb-3">
<label class="form-label"><i class="bi bi-chat-quote me-1"></i> Passphrase</label>
<input type="text" name="passphrase" class="form-control passphrase-input"
placeholder="e.g., apple forest thunder mountain" required id="passphraseInput">
<div class="form-text">Your passphrase for this message</div>
</div>
<hr class="my-3 opacity-25">
<div class="small text-muted mb-2">Provide at least one: PIN or RSA Key</div>
<div class="row">
<!-- PIN -->
<div class="col-md-6 mb-2">
<div class="security-box h-100">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9">
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
<!-- Channel -->
<div class="col-md-6 mb-2">
<div class="security-box h-100">
<label class="form-label"><i class="bi bi-broadcast me-1"></i> Channel</label>
<select class="form-select form-select-sm" name="channel_key" id="channelSelect">
<option value="auto" selected>Auto{% if channel_configured %} (Server){% endif %}</option>
<option value="none">Public</option>
{% if saved_channel_keys %}
<optgroup label="Saved Keys">
{% for key in saved_channel_keys %}
<option value="{{ key.channel_key }}">{{ key.name }}</option>
{% endfor %}
</optgroup>
{% endif %}
<option value="custom">Custom...</option>
</select>
</div>
</div>
</div>
<!-- Custom Channel Key -->
<div class="mb-3 d-none" id="channelCustomInput">
<div class="security-box">
<label class="form-label"><i class="bi bi-key me-1"></i> Custom Channel Key</label>
<div class="input-group">
<input type="text" name="channel_key_custom" class="form-control form-control-sm font-monospace"
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" id="channelKeyInput">
<button class="btn btn-outline-secondary btn-sm" type="button" id="channelKeyScan" title="Scan QR"><i class="bi bi-camera"></i></button>
<button class="btn btn-outline-secondary btn-sm" type="button" id="channelKeyGenerate" title="Generate"><i class="bi bi-shuffle"></i></button>
</div>
</div>
</div>
<!-- RSA Key -->
<div class="mb-3">
<div class="security-box">
<label class="form-label"><i class="bi bi-file-earmark-lock me-1"></i> RSA Key <span class="text-muted">(optional)</span></label>
<div class="btn-group w-100 mb-2" role="group">
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodFile" value="file" checked>
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodFile"><i class="bi bi-file-earmark me-1"></i>.pem</label>
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodQr" value="qr">
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodQr"><i class="bi bi-qr-code me-1"></i>QR</label>
</div>
<div id="rsaFileSection">
<input type="file" name="rsa_key" class="form-control form-control-sm" accept=".pem">
</div>
<div id="rsaQrSection" class="d-none d-flex flex-column">
<input type="hidden" name="rsa_key_pem" id="rsaKeyPem">
<div class="drop-zone p-2 w-100" id="qrDropZone">
<input type="file" name="rsa_key_qr" accept="image/*" id="rsaQrInput">
<div class="drop-zone-label text-center">
<i class="bi bi-qr-code-scan fs-5 d-block text-muted mb-1"></i>
<span class="text-muted small">Drop QR image</span>
</div>
<div class="qr-scan-container d-none" id="qrCropContainer">
<img class="qr-original" id="qrOriginal" alt="Original">
<img class="qr-cropped" id="qrCropped" alt="Cropped">
</div>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm w-100 mt-2" id="rsaQrWebcam">
<i class="bi bi-camera me-1"></i>Scan with Camera
</button>
</div>
<div class="input-group input-group-sm mt-2">
<input type="password" name="rsa_password" class="form-control" id="rsaPasswordInput" placeholder="Key password (if encrypted)">
<button class="btn btn-outline-secondary" type="button" data-toggle-password="rsaPasswordInput"><i class="bi bi-eye"></i></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="p-3">
<button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn">
<i class="bi bi-lock me-2"></i>Encode
</button>
</div>
</form>
</div>
</div>
<!-- Footer info -->
<div class="row text-center text-muted small mt-3">
<div class="col-4">
<i class="bi bi-shield-check fs-5 d-block mb-1 text-success"></i>
AES-256-GCM
</div>
<div class="col-4">
<i class="bi bi-shuffle fs-5 d-block mb-1 text-info"></i>
Random Pixels
</div>
<div class="col-4">
<i class="bi bi-eye-slash fs-5 d-block mb-1 text-warning"></i>
Covertly Embedded
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/fieldwitness.js') }}"></script>
<script>
// ============================================================================
// MODE HINT - Dynamic text based on selected embedding mode
// ============================================================================
const modeHints = {
dct: { icon: 'phone', text: 'Survives social media compression' },
lsb: { icon: 'hdd', text: 'Higher capacity, outputs Color PNG' },
audio_lsb: { icon: 'soundwave', text: 'Highest capacity, lossless carriers only (WAV/FLAC)' },
audio_spread: { icon: 'broadcast', text: 'Lower capacity, survives lossy conversion (MP3/AAC)' }
};
document.querySelectorAll('input[name="embed_mode"]').forEach(radio => {
radio.addEventListener('change', function() {
const hint = document.getElementById('modeHint');
const data = modeHints[this.value];
if (hint && data) {
hint.innerHTML = `<i class="bi bi-${data.icon} me-1"></i>${data.text}`;
}
});
});
// ============================================================================
// CARRIER TYPE TOGGLE (v4.3.0)
// ============================================================================
const carrierTypeRadios = document.querySelectorAll('input[name="carrier_type_select"]');
const carrierTypeInput = document.getElementById('carrierTypeInput');
const imageCarrierSection = document.getElementById('imageCarrierSection');
const audioCarrierSection = document.getElementById('audioCarrierSection');
const imageModeGroup = document.getElementById('imageModeGroup');
const audioModeGroup = document.getElementById('audioModeGroup');
const capacityPanel = document.getElementById('capacityPanel');
const audioCapacityPanel = document.getElementById('audioCapacityPanel');
carrierTypeRadios.forEach(radio => {
radio.addEventListener('change', function() {
const isAudio = this.value === 'audio';
carrierTypeInput.value = this.value;
// Toggle carrier sections
if (imageCarrierSection) imageCarrierSection.classList.toggle('d-none', isAudio);
if (audioCarrierSection) audioCarrierSection.classList.toggle('d-none', !isAudio);
// Toggle required attribute so hidden inputs don't block form submission
const imgCarrier = document.getElementById('carrierInput');
const audCarrier = document.getElementById('audioCarrierInput');
if (imgCarrier) { if (isAudio) imgCarrier.removeAttribute('required'); else imgCarrier.setAttribute('required', ''); }
if (audCarrier) { if (isAudio) audCarrier.setAttribute('required', ''); else audCarrier.removeAttribute('required'); }
// Toggle mode groups
if (imageModeGroup) imageModeGroup.classList.toggle('d-none', isAudio);
if (audioModeGroup) audioModeGroup.classList.toggle('d-none', !isAudio);
// Toggle capacity panels
if (capacityPanel) capacityPanel.classList.add('d-none');
if (audioCapacityPanel) audioCapacityPanel.classList.add('d-none');
// Select default mode for the active type and update hint
if (isAudio) {
const audioLsb = document.getElementById('modeAudioLsb');
if (audioLsb) { audioLsb.checked = true; audioLsb.dispatchEvent(new Event('change')); }
} else {
// Reset to DCT if available, else LSB
const dctRadio = document.getElementById('modeDct');
const lsbRadio = document.getElementById('modeLsb');
if (dctRadio && !dctRadio.disabled) {
dctRadio.checked = true; dctRadio.dispatchEvent(new Event('change'));
} else if (lsbRadio) {
lsbRadio.checked = true; lsbRadio.dispatchEvent(new Event('change'));
}
}
// Clear carrier file selections
const carrierInput = document.getElementById('carrierInput');
const audioCarrierInput = document.getElementById('audioCarrierInput');
if (carrierInput) carrierInput.value = '';
if (audioCarrierInput) audioCarrierInput.value = '';
// Reset previews
document.getElementById('carrierPreview')?.classList.add('d-none');
// Update step title
const stepImagesTitle = document.querySelector('#stepImages')?.closest('.accordion-item')?.querySelector('.accordion-button .step-title');
if (stepImagesTitle) {
const icon = stepImagesTitle.querySelector('i:not(.step-number i)');
const textNode = stepImagesTitle.childNodes[stepImagesTitle.childNodes.length - 1];
if (icon) {
icon.className = isAudio ? 'bi bi-music-note-beamed me-1' : 'bi bi-images me-1';
}
}
updateImagesSummary();
});
});
// Audio carrier file change handler
const audioCarrierInput = document.getElementById('audioCarrierInput');
audioCarrierInput?.addEventListener('change', function() {
if (this.files && this.files[0]) {
const file = this.files[0];
document.getElementById('audioCarrierFileName').textContent = file.name;
document.getElementById('audioCarrierFileSize').textContent = (file.size / 1024).toFixed(1) + ' KB';
// Fetch audio capacity
const formData = new FormData();
formData.append('carrier', file);
fetch('/api/audio-capacity', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.error) return;
const info = `${data.format || 'Audio'} · ${data.sample_rate}Hz · ${data.channels}ch · ${data.duration}s`;
document.getElementById('audioInfo').textContent = info;
document.getElementById('lsbAudioCapacityBadge').textContent = `LSB: ${(data.lsb_capacity / 1024).toFixed(1)} KB`;
document.getElementById('spreadCapacityBadge').textContent = `Spread: ${(data.spread_capacity / 1024).toFixed(1)} KB`;
document.getElementById('audioCapacityPanel')?.classList.remove('d-none');
if (data.duration) {
document.getElementById('audioCarrierDuration').textContent = data.duration + 's duration';
}
}).catch(() => {});
// Trigger the drop zone animation
const dropZone = document.getElementById('audioCarrierDropZone');
if (dropZone) {
dropZone.classList.add('has-file');
}
updateImagesSummary();
}
});
// ============================================================================
// ACCORDION SUMMARY UPDATES
// ============================================================================
function updateImagesSummary() {
const ref = document.getElementById('refPhotoInput')?.files[0];
const isAudio = carrierTypeInput?.value === 'audio';
const carrier = isAudio
? document.getElementById('audioCarrierInput')?.files[0]
: document.getElementById('carrierInput')?.files[0];
const mode = document.querySelector('input[name="embed_mode"]:checked')?.value?.toUpperCase() || 'LSB';
const summary = document.getElementById('stepImagesSummary');
const stepNum = document.getElementById('stepImagesNumber');
if (ref && carrier) {
const refName = ref.name.length > 12 ? ref.name.slice(0, 10) + '..' : ref.name;
const carName = carrier.name.length > 12 ? carrier.name.slice(0, 10) + '..' : carrier.name;
summary.textContent = `${refName} + ${carName}, ${mode}`;
summary.classList.add('has-content');
stepNum.classList.add('complete');
stepNum.innerHTML = '<i class="bi bi-check"></i>';
} else if (ref || carrier) {
summary.textContent = ref ? ref.name.slice(0, 15) : carrier.name.slice(0, 15);
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '1';
} else {
summary.textContent = isAudio ? 'Select reference & audio' : 'Select reference & carrier';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '1';
}
}
function updatePayloadSummary() {
const isText = document.getElementById('payloadText')?.checked;
const message = document.getElementById('messageInput')?.value || '';
const file = document.getElementById('payloadFileInput')?.files[0];
const summary = document.getElementById('stepPayloadSummary');
const stepNum = document.getElementById('stepPayloadNumber');
if (isText && message.trim()) {
const preview = message.trim().slice(0, 25) + (message.length > 25 ? '...' : '');
summary.textContent = `"${preview}"`;
summary.classList.add('has-content');
stepNum.classList.add('complete');
stepNum.innerHTML = '<i class="bi bi-check"></i>';
} else if (!isText && file) {
summary.textContent = file.name.slice(0, 20);
summary.classList.add('has-content');
stepNum.classList.add('complete');
stepNum.innerHTML = '<i class="bi bi-check"></i>';
} else {
summary.textContent = 'Message or file to hide';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '2';
}
}
function updateSecuritySummary() {
const passphrase = document.getElementById('passphraseInput')?.value || '';
const pin = document.getElementById('pinInput')?.value || '';
const rsaFile = document.querySelector('input[name="rsa_key"]')?.files[0];
const rsaPem = document.getElementById('rsaKeyPem')?.value || '';
const summary = document.getElementById('stepSecuritySummary');
const stepNum = document.getElementById('stepSecurityNumber');
const parts = [];
if (passphrase.trim()) parts.push('passphrase');
if (pin) parts.push('PIN');
if (rsaFile || rsaPem) parts.push('RSA');
if (parts.length > 0) {
summary.textContent = parts.join(' + ');
summary.classList.add('has-content');
if (passphrase.trim() && (pin || rsaFile || rsaPem)) {
stepNum.classList.add('complete');
stepNum.innerHTML = '<i class="bi bi-check"></i>';
}
} else {
summary.textContent = 'Passphrase & keys';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '3';
}
}
// Attach listeners
document.getElementById('refPhotoInput')?.addEventListener('change', updateImagesSummary);
document.getElementById('carrierInput')?.addEventListener('change', updateImagesSummary);
document.getElementById('audioCarrierInput')?.addEventListener('change', updateImagesSummary);
document.querySelectorAll('input[name="embed_mode"]').forEach(r => r.addEventListener('change', updateImagesSummary));
document.querySelectorAll('#audioModeGroup input[name="embed_mode"]').forEach(r => r.addEventListener('change', updateImagesSummary));
document.getElementById('messageInput')?.addEventListener('input', updatePayloadSummary);
document.getElementById('payloadFileInput')?.addEventListener('change', updatePayloadSummary);
document.querySelectorAll('input[name="payload_type"]').forEach(r => r.addEventListener('change', updatePayloadSummary));
document.getElementById('passphraseInput')?.addEventListener('input', updateSecuritySummary);
document.getElementById('pinInput')?.addEventListener('input', updateSecuritySummary);
document.querySelector('input[name="rsa_key"]')?.addEventListener('change', updateSecuritySummary);
// ============================================================================
// 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);
if (isText) {
messageInput.setAttribute('required', '');
payloadFileInput.removeAttribute('required');
} else {
messageInput.removeAttribute('required');
payloadFileInput.setAttribute('required', '');
}
updatePayloadSummary();
}
payloadTextRadio?.addEventListener('change', updatePayloadSection);
payloadFileRadio?.addEventListener('change', updatePayloadSection);
// ============================================================================
// FILE INFO DISPLAY
// ============================================================================
payloadFileInput?.addEventListener('change', function() {
const fileInfo = document.getElementById('fileInfo');
const fileInfoName = document.getElementById('fileInfoName');
const fileInfoSize = document.getElementById('fileInfoSize');
if (this.files && this.files[0]) {
const file = this.files[0];
fileInfo?.classList.remove('d-none');
if (fileInfoName) fileInfoName.textContent = file.name;
if (fileInfoSize) fileInfoSize.textContent = (file.size / 1024).toFixed(1) + ' KB';
const label = document.getElementById('payloadDropLabel');
if (label) label.innerHTML = `<i class="bi bi-check-circle text-success me-1"></i>${file.name}`;
} else {
fileInfo?.classList.add('d-none');
}
});
// ============================================================================
// CHARACTER COUNTER
// ============================================================================
messageInput?.addEventListener('input', function() {
const count = this.value.length;
document.getElementById('charCount').textContent = count.toLocaleString();
document.getElementById('charPercent').textContent = Math.round((count / 250000) * 100) + '%';
});
// ============================================================================
// CAPACITY FETCH
// ============================================================================
const carrierInput = document.getElementById('carrierInput');
carrierInput?.addEventListener('change', function() {
if (this.files && this.files[0]) {
const formData = new FormData();
formData.append('carrier', this.files[0]);
fetch('/api/compare-capacity', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.error) return;
document.getElementById('carrierDimensions').textContent = `${data.width} x ${data.height}`;
document.getElementById('lsbCapacityBadge').textContent = `LSB: ${data.lsb.capacity_kb} KB`;
document.getElementById('dctCapacityBadge').textContent = `DCT: ${data.dct.capacity_kb} KB`;
document.getElementById('capacityPanel')?.classList.remove('d-none');
}).catch(() => {});
}
});
// ============================================================================
// MODE SWITCHING
// ============================================================================
const modeRadios = document.querySelectorAll('input[name="embed_mode"]');
const dctModeLabel = document.getElementById('dctModeLabel');
const grayModeInput = document.getElementById('grayMode');
const grayModeLabel = document.getElementById('grayModeLabel');
const jpegFormatInput = document.getElementById('jpegFormat');
const jpegFormatLabel = document.getElementById('jpegFormatLabel');
const colorModeInput = document.getElementById('colorMode');
const pngFormatInput = document.getElementById('pngFormat');
// Apply disabled styling to DCT if not available
if (document.getElementById('modeDct')?.disabled) {
dctModeLabel?.classList.add('disabled', 'text-muted');
}
function updateOutputOptions(mode) {
const isLsb = mode === 'lsb';
if (isLsb) {
// LSB only supports Color + PNG
colorModeInput.checked = true;
pngFormatInput.checked = true;
grayModeInput.disabled = true;
jpegFormatInput.disabled = true;
grayModeLabel?.classList.add('disabled', 'text-muted');
jpegFormatLabel?.classList.add('disabled', 'text-muted');
} else {
// DCT: reset to defaults (Color + JPEG) and enable all
colorModeInput.checked = true;
jpegFormatInput.checked = true;
grayModeInput.disabled = false;
jpegFormatInput.disabled = false;
grayModeLabel?.classList.remove('disabled', 'text-muted');
jpegFormatLabel?.classList.remove('disabled', 'text-muted');
}
}
modeRadios.forEach(radio => {
radio.addEventListener('change', () => updateOutputOptions(radio.value));
});
// Initialize output options based on initial mode
const initialMode = document.querySelector('input[name="embed_mode"]:checked')?.value || 'lsb';
updateOutputOptions(initialMode);
// ============================================================================
// DUPLICATE FILE CHECK
// ============================================================================
function checkDuplicateFiles() {
const refInput = document.querySelector('input[name="reference_photo"]');
const carInput = document.querySelector('input[name="carrier"]');
if (refInput?.files[0] && carInput?.files[0]) {
if (refInput.files[0].name === carInput.files[0].name && refInput.files[0].size === carInput.files[0].size) {
alert("You cannot use the same image for both Reference and Carrier!");
carInput.value = '';
document.getElementById('carrierPreview')?.classList.add('d-none');
updateImagesSummary();
}
}
}
document.querySelector('input[name="reference_photo"]')?.addEventListener('change', checkDuplicateFiles);
document.querySelector('input[name="carrier"]')?.addEventListener('change', checkDuplicateFiles);
</script>
{% endblock %}