-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- PNG is 100% reliable. JPEG produces smaller, more natural-looking files but uses lossy compression (Q=95).
-
-
-
-
-
-
-
-
-
-
-
- DCT is ~20% of LSB capacity
@@ -449,214 +406,124 @@ function updatePayloadSection() {
textSection.classList.toggle('d-none', !isText);
fileSection.classList.toggle('d-none', isText);
- // Update required attribute
if (isText) {
- messageInput.required = true;
- payloadFileInput.required = false;
+ messageInput.setAttribute('required', '');
+ payloadFileInput.removeAttribute('required');
} else {
- messageInput.required = false;
- payloadFileInput.required = true;
+ messageInput.removeAttribute('required');
+ payloadFileInput.setAttribute('required', '');
}
}
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');
-
+// Payload 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];
- fileInfoName.textContent = file.name;
- fileInfoSize.textContent = formatFileSize(file.size);
fileInfo.classList.remove('d-none');
- payloadDropLabel.innerHTML = `
${file.name}`;
+ fileInfoName.textContent = file.name;
+
+ const sizeKB = (file.size / 1024).toFixed(1);
+ fileInfoSize.textContent = sizeKB + ' KB';
+
+ // Update drop zone label
+ const label = document.getElementById('payloadDropLabel');
+ label.innerHTML = `
${file.name}`;
} else {
fileInfo.classList.add('d-none');
- payloadDropLabel.innerHTML = `
Drop any file or click to browseMax {{ max_payload_kb }} KB
`;
}
});
-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 (only for .pem files, not QR)
-const rsaKeyInput = document.getElementById('rsaKeyInput');
-const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
-const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
-
-if (rsaKeyInput) {
- rsaKeyInput.addEventListener('change', function() {
- // Show password field only for .pem files
- rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
- // Clear QR input if file is selected
- if (rsaKeyQrInput && this.files.length) {
- rsaKeyQrInput.value = '';
- }
- });
-}
-
-if (rsaKeyQrInput) {
- rsaKeyQrInput.addEventListener('change', function() {
- // Hide password field for QR codes (they're unencrypted)
- rsaPasswordGroup.classList.add('d-none');
- // Clear file input if QR is selected
- if (rsaKeyInput && this.files.length) {
- rsaKeyInput.value = '';
- }
- });
-}
-
-// Form submit loading state
-document.getElementById('encodeForm').addEventListener('submit', function(e) {
- const btn = document.getElementById('encodeBtn');
- const selectedMode = document.querySelector('input[name="embed_mode"]:checked').value;
- let modeLabel = selectedMode.toUpperCase();
-
- if (selectedMode === 'dct') {
- const colorMode = document.querySelector('input[name="dct_color_mode"]:checked')?.value || 'color';
- const outputFormat = document.querySelector('input[name="dct_output_format"]:checked')?.value || 'png';
- modeLabel += ` (${colorMode}, ${outputFormat.toUpperCase()})`;
- }
-
- btn.innerHTML = `
Encoding (${modeLabel})...`;
- btn.disabled = true;
-});
-
-// Character counter for text
-const charCount = document.getElementById('charCount');
-const charWarning = document.getElementById('charWarning');
-const charPercent = document.getElementById('charPercent');
-const maxChars = 250000;
-
-messageInput.addEventListener('input', function() {
- const len = this.value.length;
- charCount.textContent = len.toLocaleString();
-
- const pct = Math.round((len / maxChars) * 100);
- charPercent.textContent = pct + '%';
-
- charWarning.classList.toggle('d-none', len < maxChars * 0.8);
- charCount.classList.toggle('text-danger', len > maxChars * 0.95);
-});
-
-// ============================================================================
-// v3.0 - Capacity Comparison API
-// ============================================================================
-
-const carrierInput = document.getElementById('carrierInput');
-const capacityPanel = document.getElementById('capacityPanel');
-const carrierDimensions = document.getElementById('carrierDimensions');
-const lsbCapacityBadge = document.getElementById('lsbCapacityBadge');
-const dctCapacityBadge = document.getElementById('dctCapacityBadge');
-const lsbCapacityDetail = document.getElementById('lsbCapacityDetail');
-const dctCapacityDetail = document.getElementById('dctCapacityDetail');
-const modeCapacityComparison = document.getElementById('modeCapacityComparison');
-const capacityRatio = document.getElementById('capacityRatio');
-
-let currentCapacity = null;
-
-async function fetchCapacityComparison(file) {
- const formData = new FormData();
- formData.append('carrier', file);
-
- try {
- const response = await fetch('/api/compare-capacity', {
- method: 'POST',
- body: formData
- });
+// Character counter
+if (messageInput) {
+ messageInput.addEventListener('input', function() {
+ const count = this.value.length;
+ const max = 250000;
+ const percent = Math.round((count / max) * 100);
- if (response.ok) {
- const data = await response.json();
- if (data.success) {
- currentCapacity = data;
- updateCapacityDisplay(data);
- }
- }
- } catch (err) {
- console.error('Capacity comparison failed:', err);
- }
-}
-
-function updateCapacityDisplay(data) {
- // Update top panel
- carrierDimensions.textContent = `${data.width} x ${data.height}`;
- lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`;
-
- if (data.dct.available) {
- dctCapacityBadge.textContent = `DCT: ${data.dct.capacity_kb} KB`;
- dctCapacityBadge.classList.remove('bg-secondary');
- dctCapacityBadge.classList.add('bg-info');
- } else {
- dctCapacityBadge.textContent = `DCT: N/A`;
- dctCapacityBadge.classList.remove('bg-info');
- dctCapacityBadge.classList.add('bg-secondary');
- }
-
- capacityPanel.classList.remove('d-none');
-
- // Update advanced options panel
- lsbCapacityDetail.textContent = `${data.lsb.capacity_kb} KB`;
- dctCapacityDetail.textContent = data.dct.available ? `${data.dct.capacity_kb} KB` : 'N/A';
- capacityRatio.textContent = data.dct.available
- ? `DCT is ${data.dct.ratio}% of LSB capacity`
- : 'DCT mode not available';
- modeCapacityComparison.classList.remove('d-none');
-}
-
-// Listen for carrier file selection
-if (carrierInput) {
- carrierInput.addEventListener('change', function() {
- if (this.files && this.files[0]) {
- fetchCapacityComparison(this.files[0]);
+ document.getElementById('charCount').textContent = count.toLocaleString();
+ document.getElementById('charPercent').textContent = percent + '%';
+
+ const warning = document.getElementById('charWarning');
+ if (percent >= 80) {
+ warning.classList.remove('d-none');
} else {
- capacityPanel.classList.add('d-none');
- modeCapacityComparison.classList.add('d-none');
- currentCapacity = null;
+ warning.classList.add('d-none');
}
});
}
+// Carrier capacity fetching
+const capacityPanel = document.getElementById('capacityPanel');
+const carrierInput = document.getElementById('carrierInput');
+
+carrierInput.addEventListener('change', function() {
+ if (this.files && this.files[0]) {
+ fetchCapacityComparison(this.files[0]);
+ }
+});
+
+function fetchCapacityComparison(file) {
+ const formData = new FormData();
+ formData.append('image', file);
+
+ fetch('/api/capacity', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.error) {
+ console.error('Capacity error:', data.error);
+ return;
+ }
+
+ document.getElementById('carrierDimensions').textContent =
+ `${data.width} × ${data.height} (${(data.pixels / 1000000).toFixed(1)} MP)`;
+ document.getElementById('lsbCapacityBadge').textContent =
+ `LSB: ${data.lsb_capacity_kb} KB`;
+ document.getElementById('dctCapacityBadge').textContent =
+ `DCT: ${data.dct_capacity_kb} KB`;
+
+ capacityPanel.classList.remove('d-none');
+ })
+ .catch(err => console.error('Capacity fetch failed:', err));
+}
+
// ============================================================================
-// Mode card highlighting & DCT options visibility
+// LSB/DCT Mode Switching
// ============================================================================
-const lsbModeCard = document.getElementById('lsbModeCard');
-const dctModeCard = document.getElementById('dctModeCard');
const modeLsb = document.getElementById('modeLsb');
const modeDct = document.getElementById('modeDct');
-const dctOptionsPanel = document.getElementById('dctOptionsPanel');
+const lsbModeCard = document.getElementById('lsbModeCard');
+const dctModeCard = document.getElementById('dctModeCard');
+const advancedOptionsContainer = document.getElementById('advancedOptionsContainer');
-// DCT format cards
-const dctPngCard = document.getElementById('dctPngCard');
-const dctJpegCard = document.getElementById('dctJpegCard');
+// DCT Options elements
const dctFormatPng = document.getElementById('dctFormatPng');
const dctFormatJpeg = document.getElementById('dctFormatJpeg');
-
-// DCT color mode cards
-const dctColorCard = document.getElementById('dctColorCard');
-const dctGrayscaleCard = document.getElementById('dctGrayscaleCard');
const dctColorColor = document.getElementById('dctColorColor');
const dctColorGrayscale = document.getElementById('dctColorGrayscale');
+const dctPngCard = document.getElementById('dctPngCard');
+const dctJpegCard = document.getElementById('dctJpegCard');
+const dctColorCard = document.getElementById('dctColorCard');
+const dctGrayscaleCard = document.getElementById('dctGrayscaleCard');
function updateModeCardHighlight() {
- // Mode cards
lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
lsbModeCard.classList.toggle('border-2', modeLsb.checked);
- dctModeCard.classList.toggle('border-info', modeDct.checked);
+ dctModeCard.classList.toggle('border-warning', modeDct.checked);
dctModeCard.classList.toggle('border-2', modeDct.checked);
- // Show/hide DCT options panel
- if (dctOptionsPanel) {
- dctOptionsPanel.classList.toggle('d-none', !modeDct.checked);
- }
+ // Show/hide DCT options when DCT is selected
+ advancedOptionsContainer.classList.toggle('d-none', !modeDct.checked);
}
function updateDctFormatCardHighlight() {
@@ -689,14 +556,17 @@ updateDctFormatCardHighlight(); // Initial state
updateDctColorCardHighlight(); // Initial state
// Advanced options chevron rotation
-document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() {
- document.getElementById('advancedChevron').classList.add('bi-chevron-up');
- document.getElementById('advancedChevron').classList.remove('bi-chevron-down');
-});
-document.getElementById('advancedOptions').addEventListener('hide.bs.collapse', function() {
- document.getElementById('advancedChevron').classList.remove('bi-chevron-up');
- document.getElementById('advancedChevron').classList.add('bi-chevron-down');
-});
+const advancedOptionsEl = document.getElementById('advancedOptions');
+if (advancedOptionsEl) {
+ advancedOptionsEl.addEventListener('show.bs.collapse', function() {
+ document.getElementById('advancedChevron').classList.add('bi-chevron-up');
+ document.getElementById('advancedChevron').classList.remove('bi-chevron-down');
+ });
+ advancedOptionsEl.addEventListener('hide.bs.collapse', function() {
+ document.getElementById('advancedChevron').classList.remove('bi-chevron-up');
+ document.getElementById('advancedChevron').classList.add('bi-chevron-down');
+ });
+}
// ============================================================================
// Drag & drop with preview for images
@@ -775,6 +645,34 @@ document.getElementById('togglePin').addEventListener('click', function() {
}
});
+// RSA Password Toggle Logic
+document.getElementById('toggleRsaPassword')?.addEventListener('click', function() {
+ const input = document.getElementById('rsaPasswordInput');
+ const icon = this.querySelector('i');
+ if (input.type === 'password') {
+ input.type = 'text';
+ icon.classList.replace('bi-eye', 'bi-eye-slash');
+ } else {
+ input.type = 'password';
+ icon.classList.replace('bi-eye-slash', 'bi-eye');
+ }
+});
+
+// RSA Input Method Toggle (File vs QR)
+const rsaMethodFile = document.getElementById('rsaMethodFile');
+const rsaMethodQr = document.getElementById('rsaMethodQr');
+const rsaFileSection = document.getElementById('rsaFileSection');
+const rsaQrSection = document.getElementById('rsaQrSection');
+
+function updateRsaInputMethod() {
+ const isFile = rsaMethodFile.checked;
+ rsaFileSection.classList.toggle('d-none', !isFile);
+ rsaQrSection.classList.toggle('d-none', isFile);
+}
+
+rsaMethodFile?.addEventListener('change', updateRsaInputMethod);
+rsaMethodQr?.addEventListener('change', updateRsaInputMethod);
+
// Prevent Same File Selection
function checkDuplicateFiles() {
const refInput = document.querySelector('input[name="reference_photo"]');
@@ -822,5 +720,41 @@ document.addEventListener('paste', function(e) {
}
}
});
+
+// QR Code RSA Key scanning
+const rsaQrInput = document.getElementById('rsaQrInput');
+if (rsaQrInput) {
+ rsaQrInput.addEventListener('change', function() {
+ if (this.files && this.files[0]) {
+ const formData = new FormData();
+ formData.append('qr_image', this.files[0]);
+
+ fetch('/api/decode-qr', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.error) {
+ alert('QR decode failed: ' + data.error);
+ return;
+ }
+ document.getElementById('rsaKeyFromQr').value = data.pem_data;
+ document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
+ '
RSA Key loaded from QR';
+ })
+ .catch(err => {
+ alert('QR decode failed: ' + err);
+ });
+ }
+ });
+}
+
+// Form submission with loading state
+document.getElementById('encodeForm').addEventListener('submit', function() {
+ const btn = document.getElementById('encodeBtn');
+ btn.disabled = true;
+ btn.innerHTML = '
Encoding...';
+});
{% endblock %}