Add per-channel hybrid audio spread spectrum and env feature toggles

Spread spectrum v2: independent per-channel embedding with round-robin
bit distribution, preserving spatial stereo/surround mix. Adaptive chip
tiers (256/512/1024) trade capacity for lossy codec robustness. LFE
channel skipped for 5.1+ layouts. v2 header (20B) with backward-
compatible v0 decode fallback.

Environment toggles (STEGASOO_AUDIO, STEGASOO_VIDEO) gate audio/video
features for minimal builds (e.g. Raspberry Pi image-only). Values:
auto (default, detect deps), 1/true (force on), 0/false (force off).

Web UI fixes: accordion defaults to step 1 on load, chevron arrow
styling, required attribute toggling for audio carrier type switch,
"Images & Mode" renamed to "Reference, Carrier, Mode".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-02-28 11:58:40 -05:00
parent 0248bec813
commit ef5a9ce9cb
41 changed files with 4281 additions and 732 deletions

View File

@@ -24,7 +24,11 @@
border-left: 3px solid #ffe699;
}
.step-accordion .accordion-button::after {
filter: invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(1.2);
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);
@@ -172,19 +176,51 @@
<div class="accordion step-accordion" id="decodeAccordion">
<!-- ================================================================
STEP 1: IMAGES & MODE
STEP 1: CARRIER TYPE (v4.3.0)
================================================================ -->
<div class="accordion-item" id="carrierTypeStep">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#stepCarrierType">
<span class="step-title">
<span class="step-number" id="stepCarrierTypeNumber">1</span>
<i class="bi bi-collection me-1"></i> Carrier Type
</span>
<span class="step-summary" id="stepCarrierTypeSummary"></span>
</button>
</h2>
<div id="stepCarrierType" class="accordion-collapse collapse show" data-bs-parent="#decodeAccordion">
<div class="accordion-body">
<input type="hidden" name="carrier_type" id="carrierTypeInput" value="image">
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="carrier_type_select" id="typeImage" value="image" checked>
<label class="btn btn-outline-secondary" 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 {% if not has_audio %}disabled text-muted{% endif %}" for="typeAudio">
<i class="bi bi-music-note-beamed me-1"></i> Audio
{% if not has_audio %}<small class="d-block" style="font-size: 0.65rem;">(not available)</small>{% endif %}
</label>
</div>
</div>
</div>
</div>
<!-- ================================================================
STEP 2: IMAGES & MODE
================================================================ -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#stepImages">
<button class="accordion-button collapsed" 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> Images & Mode
<span class="step-number" id="stepImagesNumber">2</span>
<i class="bi bi-images me-1"></i> Reference, Carrier, Mode
</span>
<span class="step-summary" id="stepImagesSummary">Select reference & stego</span>
</button>
</h2>
<div id="stepImages" class="accordion-collapse collapse show" data-bs-parent="#decodeAccordion">
<div id="stepImages" class="accordion-collapse collapse" data-bs-parent="#decodeAccordion">
<div class="accordion-body">
<div class="row">
@@ -213,41 +249,74 @@
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-image me-1"></i> Stego Image
</label>
<div class="drop-zone pixel-container" id="stegoDropZone">
<input type="file" name="stego_image" accept="image/*" required id="stegoInput">
<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="stegoPreview">
<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="stegoFileName">image.png</span></div>
<div class="pixel-data-row"><span class="pixel-status-badge">Stego Loaded</span><span class="pixel-data-value" id="stegoFileSize">--</span></div>
<div class="pixel-dimensions" id="stegoDims">-- x -- px</div>
<div id="imageStegoSection">
<label class="form-label">
<i class="bi bi-file-earmark-image me-1"></i> Stego Image
</label>
<div class="drop-zone pixel-container" id="stegoDropZone">
<input type="file" name="stego_image" accept="image/*" required id="stegoInput">
<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="stegoPreview">
<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="stegoFileName">image.png</span></div>
<div class="pixel-data-row"><span class="pixel-status-badge">Stego Loaded</span><span class="pixel-data-value" id="stegoFileSize">--</span></div>
<div class="pixel-dimensions" id="stegoDims">-- x -- px</div>
</div>
</div>
<div class="form-text">Image containing the hidden message</div>
</div>
<!-- Audio Stego (hidden by default) -->
<div class="d-none" id="audioStegoSection">
<label class="form-label">
<i class="bi bi-file-earmark-music me-1"></i> Stego Audio
</label>
<div class="drop-zone pixel-container" id="audioStegoDropZone">
<input type="file" name="stego_image" accept="audio/*" id="audioStegoInput">
<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="audioStegoFileName">audio.wav</span></div>
<div class="pixel-data-row"><span class="pixel-status-badge">Audio Loaded</span><span class="pixel-data-value" id="audioStegoFileSize">--</span></div>
</div>
</div>
<div class="form-text">Audio file containing the hidden message</div>
</div>
<div class="form-text">Image containing the hidden message</div>
</div>
</div>
<!-- Extraction Mode -->
<div class="d-flex gap-2 align-items-center flex-wrap mb-2">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="embed_mode" id="modeAuto" value="auto" checked>
<label class="btn btn-outline-secondary text-nowrap" for="modeAuto"><i class="bi bi-magic me-1"></i>Auto</label>
<input type="radio" class="btn-check" name="embed_mode" id="modeLsb" value="lsb">
<label class="btn btn-outline-secondary text-nowrap" for="modeLsb"><i class="bi bi-grid-3x3-gap me-1"></i>LSB</label>
<input type="radio" class="btn-check" name="embed_mode" id="modeDct" value="dct" {% if not has_dct %}disabled{% endif %}>
<label class="btn btn-outline-secondary text-nowrap" for="modeDct" id="dctModeLabel"><i class="bi bi-soundwave me-1"></i>DCT</label>
<div id="imageModeGroup">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="embed_mode" id="modeAuto" value="auto" checked>
<label class="btn btn-outline-secondary text-nowrap" for="modeAuto"><i class="bi bi-magic me-1"></i>Auto</label>
<input type="radio" class="btn-check" name="embed_mode" id="modeLsb" value="lsb">
<label class="btn btn-outline-secondary text-nowrap" for="modeLsb"><i class="bi bi-grid-3x3-gap me-1"></i>LSB</label>
<input type="radio" class="btn-check" name="embed_mode" id="modeDct" value="dct" {% if not has_dct %}disabled{% endif %}>
<label class="btn btn-outline-secondary text-nowrap" for="modeDct" id="dctModeLabel"><i class="bi bi-soundwave me-1"></i>DCT</label>
</div>
</div>
<!-- Audio Extraction Modes (hidden by default) -->
<div class="d-none" id="audioModeGroup">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="embed_mode" id="modeAudioAuto" value="audio_auto">
<label class="btn btn-outline-secondary text-nowrap" for="modeAudioAuto"><i class="bi bi-magic me-1"></i>Auto</label>
<input type="radio" class="btn-check" name="embed_mode" id="modeAudioLsb" value="audio_lsb">
<label class="btn btn-outline-secondary 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 text-nowrap" for="modeAudioSpread"><i class="bi bi-broadcast me-1"></i>Spread</label>
</div>
</div>
</div>
<div class="form-text" id="modeHint">
@@ -259,13 +328,13 @@
</div>
<!-- ================================================================
STEP 2: SECURITY
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">2</span>
<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>
@@ -425,7 +494,10 @@
const modeHints = {
auto: { icon: 'lightning', text: 'Tries LSB first, then DCT' },
lsb: { icon: 'hdd', text: 'For email and direct transfers' },
dct: { icon: 'phone', text: 'For social media images' }
dct: { icon: 'phone', text: 'For social media images' },
audio_auto: { icon: 'lightning', text: 'Tries LSB first, then Spread Spectrum' },
audio_lsb: { icon: 'grid-3x3-gap', text: 'Direct bit embedding in audio samples' },
audio_spread: { icon: 'broadcast', text: 'Noise-resistant spread spectrum encoding' }
};
document.querySelectorAll('input[name="embed_mode"]').forEach(radio => {
@@ -442,9 +514,14 @@ document.querySelectorAll('input[name="embed_mode"]').forEach(radio => {
// ACCORDION SUMMARY UPDATES
// ============================================================================
const carrierTypeInput = document.getElementById('carrierTypeInput');
function updateImagesSummary() {
const ref = document.getElementById('refPhotoInput')?.files[0];
const stego = document.getElementById('stegoInput')?.files[0];
const isAudio = carrierTypeInput?.value === 'audio';
const stego = isAudio
? document.getElementById('audioStegoInput')?.files[0]
: document.getElementById('stegoInput')?.files[0];
const mode = document.querySelector('input[name="embed_mode"]:checked')?.value?.toUpperCase() || 'AUTO';
const summary = document.getElementById('stepImagesSummary');
const stepNum = document.getElementById('stepImagesNumber');
@@ -460,12 +537,12 @@ function updateImagesSummary() {
summary.textContent = ref ? ref.name.slice(0, 15) : stego.name.slice(0, 15);
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '1';
stepNum.textContent = '2';
} else {
summary.textContent = 'Select reference & stego';
summary.textContent = isAudio ? 'Select reference & audio' : 'Select reference & stego';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '1';
stepNum.textContent = '2';
}
}
@@ -493,19 +570,99 @@ function updateSecuritySummary() {
summary.textContent = 'Passphrase & keys';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '2';
stepNum.textContent = '3';
}
}
// Attach listeners
document.getElementById('refPhotoInput')?.addEventListener('change', updateImagesSummary);
document.getElementById('stegoInput')?.addEventListener('change', updateImagesSummary);
document.getElementById('audioStegoInput')?.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('passphraseInput')?.addEventListener('input', updateSecuritySummary);
document.getElementById('pinInput')?.addEventListener('input', updateSecuritySummary);
document.querySelector('input[name="rsa_key"]')?.addEventListener('change', updateSecuritySummary);
// ============================================================================
// CARRIER TYPE TOGGLE (v4.3.0)
// ============================================================================
const carrierTypeRadios = document.querySelectorAll('input[name="carrier_type_select"]');
const imageStegoSection = document.getElementById('imageStegoSection');
const audioStegoSection = document.getElementById('audioStegoSection');
const imageModeGroup = document.getElementById('imageModeGroup');
const audioModeGroup = document.getElementById('audioModeGroup');
const stepCarrierTypeSummary = document.getElementById('stepCarrierTypeSummary');
carrierTypeRadios.forEach(radio => {
radio.addEventListener('change', function() {
const isAudio = this.value === 'audio';
carrierTypeInput.value = this.value;
// Toggle stego sections
if (imageStegoSection) imageStegoSection.classList.toggle('d-none', isAudio);
if (audioStegoSection) audioStegoSection.classList.toggle('d-none', !isAudio);
// Toggle required attribute so hidden inputs don't block form submission
const imgStego = document.getElementById('stegoInput');
const audStego = document.getElementById('audioStegoInput');
if (imgStego) { if (isAudio) imgStego.removeAttribute('required'); else imgStego.setAttribute('required', ''); }
if (audStego) { if (isAudio) audStego.setAttribute('required', ''); else audStego.removeAttribute('required'); }
// Toggle mode groups
if (imageModeGroup) imageModeGroup.classList.toggle('d-none', isAudio);
if (audioModeGroup) audioModeGroup.classList.toggle('d-none', !isAudio);
// Update summary
if (stepCarrierTypeSummary) {
stepCarrierTypeSummary.textContent = isAudio ? 'Audio' : 'Image';
}
// Select default mode
if (isAudio) {
const audioAuto = document.getElementById('modeAudioAuto');
if (audioAuto) audioAuto.checked = true;
} else {
const autoMode = document.getElementById('modeAuto');
if (autoMode) autoMode.checked = true;
}
// Clear stego file selections
const stegoInput = document.getElementById('stegoInput');
const audioStegoInput = document.getElementById('audioStegoInput');
if (stegoInput) stegoInput.value = '';
if (audioStegoInput) audioStegoInput.value = '';
// Reset previews
document.getElementById('stegoPreview')?.classList.add('d-none');
// Update mode hint
const hint = document.getElementById('modeHint');
if (hint) {
if (isAudio) {
hint.innerHTML = '<i class="bi bi-lightning me-1"></i>Tries LSB first, then Spread Spectrum';
} else {
hint.innerHTML = '<i class="bi bi-lightning me-1"></i>Tries LSB first, then DCT';
}
}
updateImagesSummary();
});
});
// Audio stego file info display
const audioStegoInput = document.getElementById('audioStegoInput');
audioStegoInput?.addEventListener('change', function() {
if (this.files && this.files[0]) {
const file = this.files[0];
document.getElementById('audioStegoFileName').textContent = file.name;
document.getElementById('audioStegoFileSize').textContent = (file.size / 1024).toFixed(1) + ' KB';
updateImagesSummary();
}
});
// ============================================================================
// MODE SWITCHING
// ============================================================================

View File

@@ -24,7 +24,11 @@
border-left: 3px solid #ffe699;
}
.step-accordion .accordion-button::after {
filter: invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(1.2);
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);
@@ -126,19 +130,56 @@
<div class="accordion step-accordion" id="encodeAccordion">
<!-- ================================================================
STEP 1: IMAGES
STEP 1: CARRIER TYPE (v4.3.0)
================================================================ -->
<div class="accordion-item" id="carrierTypeStep">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#stepCarrierType">
<span class="step-title">
<span class="step-number" id="stepCarrierTypeNumber">1</span>
<i class="bi bi-collection me-1"></i> Carrier Type
</span>
<span class="step-summary" id="stepCarrierTypeSummary"></span>
</button>
</h2>
<div id="stepCarrierType" 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="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="carrier_type_select" id="typeImage" value="image" checked>
<label class="btn btn-outline-secondary" 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 {% if not has_audio %}disabled text-muted{% endif %}" for="typeAudio">
<i class="bi bi-music-note-beamed me-1"></i> Audio
{% if not has_audio %}<small class="d-block" style="font-size: 0.65rem;">(not available)</small>{% endif %}
</label>
</div>
{% if not has_audio %}
<div class="form-text text-warning mt-2">
<i class="bi bi-exclamation-triangle me-1"></i>Audio requires numpy and soundfile packages
</div>
{% endif %}
</div>
</div>
</div>
<!-- ================================================================
STEP 2: IMAGES & MODE
================================================================ -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#stepImages">
<button class="accordion-button collapsed" 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> Images & Mode
<span class="step-number" id="stepImagesNumber">2</span>
<i class="bi bi-images me-1"></i> Reference, 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 id="stepImages" class="accordion-collapse collapse" data-bs-parent="#encodeAccordion">
<div class="accordion-body">
<div class="row">
@@ -167,29 +208,51 @@
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-image me-1"></i> Carrier Image
</label>
<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 id="imageCarrierSection">
<label class="form-label">
<i class="bi bi-file-earmark-image me-1"></i> Carrier Image
</label>
<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">Image to hide your message in</div>
</div>
<!-- Audio Carrier (hidden by default, shown when audio type selected) -->
<div class="d-none" id="audioCarrierSection">
<label class="form-label">
<i class="bi bi-file-earmark-music me-1"></i> Carrier Audio
</label>
<div class="drop-zone pixel-container" id="audioCarrierDropZone">
<input type="file" name="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">Audio file to hide your message in</div>
</div>
<div class="form-text">Image to hide your message in</div>
</div>
</div>
@@ -204,30 +267,54 @@
</div>
</div>
<!-- Embedding Mode (compact inline) -->
<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>
<!-- 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>
<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>
<!-- Embedding Mode (compact inline) -->
<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" 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>
@@ -237,13 +324,13 @@
</div>
<!-- ================================================================
STEP 2: PAYLOAD
STEP 3: 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>
<span class="step-number" id="stepPayloadNumber">3</span>
<i class="bi bi-box me-1"></i> Payload
</span>
<span class="step-summary" id="stepPayloadSummary">Message or file to hide</span>
@@ -295,13 +382,13 @@
</div>
<!-- ================================================================
STEP 3: SECURITY
STEP 4: 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>
<span class="step-number" id="stepSecurityNumber">4</span>
<i class="bi bi-shield-lock me-1"></i> Security
</span>
<span class="step-summary" id="stepSecuritySummary">Passphrase & keys</span>
@@ -462,13 +549,131 @@ document.querySelectorAll('input[name="embed_mode"]').forEach(radio => {
});
});
// ============================================================================
// 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');
const stepCarrierTypeSummary = document.getElementById('stepCarrierTypeSummary');
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');
// Update summary
if (stepCarrierTypeSummary) {
stepCarrierTypeSummary.textContent = isAudio ? 'Audio' : 'Image';
}
// Select default mode for the active type
if (isAudio) {
const audioLsb = document.getElementById('modeAudioLsb');
if (audioLsb) audioLsb.checked = true;
} 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;
} else if (lsbRadio) {
lsbRadio.checked = true;
}
}
// 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 carrier = document.getElementById('carrierInput')?.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');
@@ -484,12 +689,12 @@ function updateImagesSummary() {
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';
stepNum.textContent = '2';
} else {
summary.textContent = 'Select reference & carrier';
summary.textContent = isAudio ? 'Select reference & audio' : 'Select reference & carrier';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '1';
stepNum.textContent = '2';
}
}
@@ -515,7 +720,7 @@ function updatePayloadSummary() {
summary.textContent = 'Message or file to hide';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '2';
stepNum.textContent = '3';
}
}
@@ -543,14 +748,16 @@ function updateSecuritySummary() {
summary.textContent = 'Passphrase & keys';
summary.classList.remove('has-content');
stepNum.classList.remove('complete');
stepNum.textContent = '3';
stepNum.textContent = '4';
}
}
// 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);

View File

@@ -12,12 +12,26 @@
</h5>
</div>
<div class="card-body text-center">
{% if carrier_type == 'audio' %}
<!-- Audio Preview -->
<div class="my-4">
<div class="text-center">
<i class="bi bi-music-note-beamed text-success" style="font-size: 4rem;"></i>
<div class="mt-2">
<audio controls src="{{ url_for('encode_file_route', file_id=file_id) }}" class="w-100" style="max-width: 400px;"></audio>
</div>
<div class="mt-2 small text-muted">
<i class="bi bi-music-note-beamed me-1"></i>Encoded Audio Preview
</div>
</div>
</div>
{% else %}
<div class="my-4">
{% if thumbnail_url %}
<!-- Thumbnail of the actual encoded image -->
<div class="encoded-image-thumbnail">
<img src="{{ thumbnail_url }}"
alt="Encoded image thumbnail"
<img src="{{ thumbnail_url }}"
alt="Encoded image thumbnail"
class="img-thumbnail rounded"
style="max-width: 250px; max-height: 250px; object-fit: contain;">
<div class="mt-2 small text-muted">
@@ -29,8 +43,9 @@
<i class="bi bi-file-earmark-image text-success" style="font-size: 4rem;"></i>
{% endif %}
</div>
{% endif %}
<p class="lead mb-4">Your secret has been hidden in the image.</p>
<p class="lead mb-4">Your secret has been hidden in the {{ 'audio file' if carrier_type == 'audio' else 'image' }}.</p>
<div class="mb-3">
<code class="fs-5">{{ filename }}</code>
@@ -38,11 +53,32 @@
<!-- Mode and format badges -->
<div class="mb-4">
{% if embed_mode == 'dct' %}
{% if carrier_type == 'audio' %}
<!-- Audio mode badges -->
{% if embed_mode == 'audio_spread' %}
<span class="badge bg-warning text-dark fs-6">
<i class="bi bi-broadcast me-1"></i>Spread Spectrum
</span>
{% else %}
<span class="badge bg-primary fs-6">
<i class="bi bi-grid-3x3-gap me-1"></i>Audio LSB
</span>
{% endif %}
<span class="badge bg-info fs-6 ms-1">
<i class="bi bi-file-earmark-music me-1"></i>WAV
</span>
<div class="small text-muted mt-2">
{% if embed_mode == 'audio_spread' %}
Spread spectrum embedding in audio samples
{% else %}
LSB embedding in audio samples, WAV output
{% endif %}
</div>
{% elif embed_mode == 'dct' %}
<span class="badge bg-info fs-6">
<i class="bi bi-soundwave me-1"></i>DCT Mode
</span>
<!-- Color mode badge (v3.0.1) -->
{% if color_mode == 'color' %}
<span class="badge bg-success fs-6 ms-1">
@@ -53,7 +89,7 @@
<i class="bi bi-circle-half me-1"></i>Grayscale
</span>
{% endif %}
<!-- Output format badge -->
{% if output_format == 'jpeg' %}
<span class="badge bg-warning text-dark fs-6 ms-1">
@@ -78,7 +114,7 @@
{% endif %}
</div>
{% endif %}
{% else %}
<span class="badge bg-primary fs-6">
<i class="bi bi-grid-3x3-gap me-1"></i>LSB Mode
@@ -114,7 +150,7 @@
<div class="d-grid gap-2">
<a href="{{ url_for('encode_download', file_id=file_id) }}"
class="btn btn-primary btn-lg" id="downloadBtn">
<i class="bi bi-download me-2"></i>Download Image
<i class="bi bi-download me-2"></i>Download {{ 'Audio' if carrier_type == 'audio' else 'Image' }}
</a>
<button type="button" class="btn btn-outline-primary" id="shareBtn" style="display: none;">
@@ -129,6 +165,11 @@
<strong>Important:</strong>
<ul class="mb-0 mt-2">
<li>This file expires in <strong>10 minutes</strong></li>
{% if carrier_type == 'audio' %}
<li>Do <strong>not</strong> re-encode or convert the audio file</li>
<li>WAV format preserves your hidden data losslessly</li>
<li>Sharing via platforms that re-encode audio will destroy the hidden data</li>
{% else %}
<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>
@@ -141,6 +182,7 @@
<li>Color preserved - extraction works on both color and grayscale</li>
{% endif %}
{% endif %}
{% endif %}
{% if channel_mode == 'private' %}
<li><i class="bi bi-shield-lock text-warning me-1"></i>Recipient needs the <strong>same channel key</strong> to decode</li>
{% endif %}
@@ -148,7 +190,7 @@
</div>
<a href="{{ url_for('encode_page') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-repeat me-2"></i>Encode Another Message
<i class="bi bi-arrow-repeat me-2"></i>Encode Another
</a>
</div>
</div>
@@ -162,7 +204,7 @@
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' }}";
const mimeType = "{{ 'audio/wav' if carrier_type == 'audio' else ('image/jpeg' if embed_mode == 'dct' and output_format == 'jpeg' else 'image/png') }}";
if (navigator.share && navigator.canShare) {
// Check if we can share files