diff --git a/frontends/web/app.py b/frontends/web/app.py index db4e294..523ff4e 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -89,6 +89,7 @@ from stegasoo.qr_utils import ( compress_data, decompress_data, auto_decompress, is_compressed, can_fit_in_qr, needs_compression, generate_qr_code, read_qr_code, extract_key_from_qr, + detect_and_crop_qr, has_qr_write, has_qr_read, QR_MAX_BINARY, COMPRESSION_PREFIX ) @@ -368,6 +369,43 @@ def generate_qr_download(token): return f"Error generating QR code: {e}", 500 +@app.route('/qr/crop', methods=['POST']) +def qr_crop(): + """ + Detect and crop QR code from an image. + + Useful for extracting QR codes from photos taken at an angle, + with extra background, etc. Returns the cropped QR as PNG. + """ + if not HAS_QRCODE_READ: + return jsonify({'error': 'QR code reading not available (install pyzbar)'}), 501 + + image_file = request.files.get('image') + if not image_file: + return jsonify({'error': 'No image provided'}), 400 + + try: + image_data = image_file.read() + + # Use the new crop function + cropped = detect_and_crop_qr(image_data) + + if cropped is None: + return jsonify({'error': 'No QR code detected in image'}), 404 + + # Return as downloadable PNG or inline based on query param + as_attachment = request.args.get('download', '').lower() in ('1', 'true', 'yes') + + return send_file( + io.BytesIO(cropped), + mimetype='image/png', + as_attachment=as_attachment, + download_name='cropped_qr.png' + ) + except Exception as e: + return jsonify({'error': f'Error processing image: {e}'}), 500 + + @app.route('/generate/download-key', methods=['POST']) def download_key(): """Download RSA key as password-protected PEM file.""" diff --git a/frontends/web/templates/decode.html b/frontends/web/templates/decode.html index 17279a7..648b3f2 100644 --- a/frontends/web/templates/decode.html +++ b/frontends/web/templates/decode.html @@ -48,6 +48,59 @@ color: rgba(246, 173, 85, 0.4); letter-spacing: 1px; } + +/* QR Crop Animation */ +.qr-crop-container { + position: relative; + overflow: hidden; + border-radius: 8px; + background: rgba(0, 0, 0, 0.3); +} + +.qr-crop-container img { + display: block; + max-height: 120px; + width: auto; + margin: 0 auto; + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.qr-crop-container .qr-original { + opacity: 1; +} + +.qr-crop-container .qr-cropped { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.3); + opacity: 0; + max-height: 100px; +} + +.qr-crop-container.animating .qr-original { + opacity: 0; + transform: scale(1.1); + filter: blur(4px); +} + +.qr-crop-container.animating .qr-cropped { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +.qr-crop-container .crop-badge { + position: absolute; + bottom: 4px; + right: 4px; + font-size: 0.65rem; + opacity: 0; + transition: opacity 0.3s ease 0.4s; +} + +.qr-crop-container.animating .crop-badge { + opacity: 1; +}
@@ -203,7 +256,12 @@ Drop QR image or click to browse
- + +
+ Original + Cropped QR + Detected +
@@ -496,47 +554,82 @@ document.querySelectorAll('.drop-zone').forEach(zone => { } }); -// QR Code RSA Key scanning +// QR Code RSA Key scanning with crop animation const rsaKeyQrInput = document.getElementById('rsaKeyQrInput'); -const qrPreview = document.getElementById('qrPreview'); +const qrCropContainer = document.getElementById('qrCropContainer'); +const qrOriginal = document.getElementById('qrOriginal'); +const qrCropped = document.getElementById('qrCropped'); + if (rsaKeyQrInput) { rsaKeyQrInput.addEventListener('change', function() { if (this.files && this.files[0]) { const file = this.files[0]; - // Show image preview - if (file.type.startsWith('image/')) { - const reader = new FileReader(); - reader.onload = e => { - if (qrPreview) { - qrPreview.src = e.target.result; - qrPreview.classList.remove('d-none'); + if (!file.type.startsWith('image/')) return; + + // Reset animation state + qrCropContainer.classList.remove('animating'); + qrCropContainer.classList.add('d-none'); + + // Show original image first + const reader = new FileReader(); + reader.onload = e => { + qrOriginal.src = e.target.result; + qrCropContainer.classList.remove('d-none'); + + // Hide the label + document.querySelector('#qrDropZone .drop-zone-label').classList.add('d-none'); + + // Now fetch cropped version and animate + const formData = new FormData(); + formData.append('image', file); + + fetch('/qr/crop', { + method: 'POST', + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error('No QR code detected'); } - }; - reader.readAsDataURL(file); - } - - // Extract key from QR - const formData = new FormData(); - formData.append('qr_image', file); - - fetch('/extract-key-from-qr', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (!data.success) { - alert('QR decode failed: ' + data.error); - return; - } - // Visual feedback - document.querySelector('#qrDropZone .drop-zone-label').innerHTML = - 'RSA Key loaded from QR'; - }) - .catch(err => { - alert('QR decode failed: ' + err); - }); + return response.blob(); + }) + .then(blob => { + // Load cropped image + const croppedUrl = URL.createObjectURL(blob); + qrCropped.src = croppedUrl; + + // Wait a moment to show original, then animate + setTimeout(() => { + qrCropContainer.classList.add('animating'); + }, 400); + + // Also verify key extraction works + const keyFormData = new FormData(); + keyFormData.append('qr_image', file); + + return fetch('/extract-key-from-qr', { + method: 'POST', + body: keyFormData + }); + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + document.querySelector('#qrDropZone .drop-zone-label').innerHTML = + 'RSA Key loaded'; + document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none'); + } + }) + .catch(err => { + // Crop failed - just show original with error + console.log('QR crop/extract error:', err); + document.querySelector('#qrDropZone .drop-zone-label').innerHTML = + 'No QR detected'; + document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none'); + }); + }; + reader.readAsDataURL(file); } }); } diff --git a/frontends/web/templates/encode.html b/frontends/web/templates/encode.html index ff77521..47c4e5d 100644 --- a/frontends/web/templates/encode.html +++ b/frontends/web/templates/encode.html @@ -52,6 +52,59 @@ color: rgba(246, 173, 85, 0.4); letter-spacing: 1px; } + +/* QR Crop Animation */ +.qr-crop-container { + position: relative; + overflow: hidden; + border-radius: 8px; + background: rgba(0, 0, 0, 0.3); +} + +.qr-crop-container img { + display: block; + max-height: 120px; + width: auto; + margin: 0 auto; + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.qr-crop-container .qr-original { + opacity: 1; +} + +.qr-crop-container .qr-cropped { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.3); + opacity: 0; + max-height: 100px; +} + +.qr-crop-container.animating .qr-original { + opacity: 0; + transform: scale(1.1); + filter: blur(4px); +} + +.qr-crop-container.animating .qr-cropped { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +.qr-crop-container .crop-badge { + position: absolute; + bottom: 4px; + right: 4px; + font-size: 0.65rem; + opacity: 0; + transition: opacity 0.3s ease 0.4s; +} + +.qr-crop-container.animating .crop-badge { + opacity: 1; +}
@@ -302,7 +355,12 @@ Drop QR image or click to browse
- + +
+ Original + Cropped QR + Detected +
@@ -806,47 +864,82 @@ document.addEventListener('paste', function(e) { } }); -// QR Code RSA Key scanning +// QR Code RSA Key scanning with crop animation const rsaQrInput = document.getElementById('rsaQrInput'); -const qrPreview = document.getElementById('qrPreview'); +const qrCropContainer = document.getElementById('qrCropContainer'); +const qrOriginal = document.getElementById('qrOriginal'); +const qrCropped = document.getElementById('qrCropped'); + if (rsaQrInput) { rsaQrInput.addEventListener('change', function() { if (this.files && this.files[0]) { const file = this.files[0]; - // Show image preview - if (file.type.startsWith('image/')) { - const reader = new FileReader(); - reader.onload = e => { - if (qrPreview) { - qrPreview.src = e.target.result; - qrPreview.classList.remove('d-none'); + if (!file.type.startsWith('image/')) return; + + // Reset animation state + qrCropContainer.classList.remove('animating'); + qrCropContainer.classList.add('d-none'); + + // Show original image first + const reader = new FileReader(); + reader.onload = e => { + qrOriginal.src = e.target.result; + qrCropContainer.classList.remove('d-none'); + + // Hide the label + document.querySelector('#qrDropZone .drop-zone-label').classList.add('d-none'); + + // Now fetch cropped version and animate + const formData = new FormData(); + formData.append('image', file); + + fetch('/qr/crop', { + method: 'POST', + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error('No QR code detected'); } - }; - reader.readAsDataURL(file); - } - - // Extract key from QR - const formData = new FormData(); - formData.append('qr_image', file); - - fetch('/extract-key-from-qr', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (!data.success) { - alert('QR decode failed: ' + data.error); - return; - } - // Visual feedback - document.querySelector('#qrDropZone .drop-zone-label').innerHTML = - 'RSA Key loaded from QR'; - }) - .catch(err => { - alert('QR decode failed: ' + err); - }); + return response.blob(); + }) + .then(blob => { + // Load cropped image + const croppedUrl = URL.createObjectURL(blob); + qrCropped.src = croppedUrl; + + // Wait a moment to show original, then animate + setTimeout(() => { + qrCropContainer.classList.add('animating'); + }, 400); + + // Also verify key extraction works + const keyFormData = new FormData(); + keyFormData.append('qr_image', file); + + return fetch('/extract-key-from-qr', { + method: 'POST', + body: keyFormData + }); + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + document.querySelector('#qrDropZone .drop-zone-label').innerHTML = + 'RSA Key loaded'; + document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none'); + } + }) + .catch(err => { + // Crop failed - just show original with error + console.log('QR crop/extract error:', err); + document.querySelector('#qrDropZone .drop-zone-label').innerHTML = + 'No QR detected'; + document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none'); + }); + }; + reader.readAsDataURL(file); } }); } diff --git a/frontends/web/templates/generate.html b/frontends/web/templates/generate.html index f00830f..3f8d7e5 100644 --- a/frontends/web/templates/generate.html +++ b/frontends/web/templates/generate.html @@ -62,11 +62,14 @@
- +
+ QR code unavailable for keys >3072 bits +
@@ -498,6 +501,16 @@ if (useRsaCheck) { }); } +// RSA key size QR warning +const rsaBitsSelect = document.getElementById('rsaBitsSelect'); +const rsaQrWarning = document.getElementById('rsaQrWarning'); + +if (rsaBitsSelect && rsaQrWarning) { + rsaBitsSelect.addEventListener('change', function() { + rsaQrWarning.classList.toggle('d-none', parseInt(this.value) <= 3072); + }); +} + // PIN visibility toggle let pinHidden = false; function togglePinVisibility() {