Snazzy ui updates.

This commit is contained in:
Aaron D. Lee
2026-01-01 22:51:53 -05:00
parent ef7478b30a
commit c1beaf3611
4 changed files with 308 additions and 71 deletions

View File

@@ -89,6 +89,7 @@ from stegasoo.qr_utils import (
compress_data, decompress_data, auto_decompress, compress_data, decompress_data, auto_decompress,
is_compressed, can_fit_in_qr, needs_compression, is_compressed, can_fit_in_qr, needs_compression,
generate_qr_code, read_qr_code, extract_key_from_qr, generate_qr_code, read_qr_code, extract_key_from_qr,
detect_and_crop_qr,
has_qr_write, has_qr_read, has_qr_write, has_qr_read,
QR_MAX_BINARY, COMPRESSION_PREFIX QR_MAX_BINARY, COMPRESSION_PREFIX
) )
@@ -368,6 +369,43 @@ def generate_qr_download(token):
return f"Error generating QR code: {e}", 500 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']) @app.route('/generate/download-key', methods=['POST'])
def download_key(): def download_key():
"""Download RSA key as password-protected PEM file.""" """Download RSA key as password-protected PEM file."""

View File

@@ -48,6 +48,59 @@
color: rgba(246, 173, 85, 0.4); color: rgba(246, 173, 85, 0.4);
letter-spacing: 1px; 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;
}
</style> </style>
<div class="row justify-content-center"> <div class="row justify-content-center">
@@ -203,7 +256,12 @@
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i> <i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
<span class="text-muted small">Drop QR image or click to browse</span> <span class="text-muted small">Drop QR image or click to browse</span>
</div> </div>
<img class="drop-zone-preview d-none" id="qrPreview" style="max-height: 80px;"> <!-- Crop animation container -->
<div class="qr-crop-container d-none" id="qrCropContainer">
<img class="qr-original" id="qrOriginal" alt="Original">
<img class="qr-cropped" id="qrCropped" alt="Cropped QR">
<span class="crop-badge badge bg-success"><i class="bi bi-crop me-1"></i>Detected</span>
</div>
</div> </div>
</div> </div>
@@ -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 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) { if (rsaKeyQrInput) {
rsaKeyQrInput.addEventListener('change', function() { rsaKeyQrInput.addEventListener('change', function() {
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
const file = this.files[0]; const file = this.files[0];
// Show image preview if (!file.type.startsWith('image/')) return;
if (file.type.startsWith('image/')) {
// Reset animation state
qrCropContainer.classList.remove('animating');
qrCropContainer.classList.add('d-none');
// Show original image first
const reader = new FileReader(); const reader = new FileReader();
reader.onload = e => { reader.onload = e => {
if (qrPreview) { qrOriginal.src = e.target.result;
qrPreview.src = e.target.result; qrCropContainer.classList.remove('d-none');
qrPreview.classList.remove('d-none');
}
};
reader.readAsDataURL(file);
}
// Extract key from QR // Hide the label
document.querySelector('#qrDropZone .drop-zone-label').classList.add('d-none');
// Now fetch cropped version and animate
const formData = new FormData(); const formData = new FormData();
formData.append('qr_image', file); formData.append('image', file);
fetch('/extract-key-from-qr', { fetch('/qr/crop', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
.then(response => {
if (!response.ok) {
throw new Error('No QR code detected');
}
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(response => response.json())
.then(data => { .then(data => {
if (!data.success) { if (data.success) {
alert('QR decode failed: ' + data.error);
return;
}
// Visual feedback
document.querySelector('#qrDropZone .drop-zone-label').innerHTML = document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-check-circle text-success me-1"></i>RSA Key loaded from QR'; '<i class="bi bi-check-circle text-success me-1"></i>RSA Key loaded';
document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none');
}
}) })
.catch(err => { .catch(err => {
alert('QR decode failed: ' + err); // Crop failed - just show original with error
console.log('QR crop/extract error:', err);
document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-exclamation-triangle text-warning me-1"></i>No QR detected';
document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none');
}); });
};
reader.readAsDataURL(file);
} }
}); });
} }

View File

@@ -52,6 +52,59 @@
color: rgba(246, 173, 85, 0.4); color: rgba(246, 173, 85, 0.4);
letter-spacing: 1px; 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;
}
</style> </style>
<div class="row justify-content-center"> <div class="row justify-content-center">
@@ -302,7 +355,12 @@
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i> <i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
<span class="text-muted small">Drop QR image or click to browse</span> <span class="text-muted small">Drop QR image or click to browse</span>
</div> </div>
<img class="drop-zone-preview d-none" id="qrPreview" style="max-height: 80px;"> <!-- Crop animation container -->
<div class="qr-crop-container d-none" id="qrCropContainer">
<img class="qr-original" id="qrOriginal" alt="Original">
<img class="qr-cropped" id="qrCropped" alt="Cropped QR">
<span class="crop-badge badge bg-success"><i class="bi bi-crop me-1"></i>Detected</span>
</div>
</div> </div>
</div> </div>
@@ -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 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) { if (rsaQrInput) {
rsaQrInput.addEventListener('change', function() { rsaQrInput.addEventListener('change', function() {
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
const file = this.files[0]; const file = this.files[0];
// Show image preview if (!file.type.startsWith('image/')) return;
if (file.type.startsWith('image/')) {
// Reset animation state
qrCropContainer.classList.remove('animating');
qrCropContainer.classList.add('d-none');
// Show original image first
const reader = new FileReader(); const reader = new FileReader();
reader.onload = e => { reader.onload = e => {
if (qrPreview) { qrOriginal.src = e.target.result;
qrPreview.src = e.target.result; qrCropContainer.classList.remove('d-none');
qrPreview.classList.remove('d-none');
}
};
reader.readAsDataURL(file);
}
// Extract key from QR // Hide the label
document.querySelector('#qrDropZone .drop-zone-label').classList.add('d-none');
// Now fetch cropped version and animate
const formData = new FormData(); const formData = new FormData();
formData.append('qr_image', file); formData.append('image', file);
fetch('/extract-key-from-qr', { fetch('/qr/crop', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
.then(response => {
if (!response.ok) {
throw new Error('No QR code detected');
}
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(response => response.json())
.then(data => { .then(data => {
if (!data.success) { if (data.success) {
alert('QR decode failed: ' + data.error);
return;
}
// Visual feedback
document.querySelector('#qrDropZone .drop-zone-label').innerHTML = document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-check-circle text-success me-1"></i>RSA Key loaded from QR'; '<i class="bi bi-check-circle text-success me-1"></i>RSA Key loaded';
document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none');
}
}) })
.catch(err => { .catch(err => {
alert('QR decode failed: ' + err); // Crop failed - just show original with error
console.log('QR crop/extract error:', err);
document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-exclamation-triangle text-warning me-1"></i>No QR detected';
document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none');
}); });
};
reader.readAsDataURL(file);
} }
}); });
} }

View File

@@ -62,11 +62,14 @@
</div> </div>
<div class="mt-2 d-none" id="rsaOptions"> <div class="mt-2 d-none" id="rsaOptions">
<label class="form-label small">Key Size</label> <label class="form-label small">Key Size</label>
<select name="rsa_bits" class="form-select form-select-sm"> <select name="rsa_bits" class="form-select form-select-sm" id="rsaBitsSelect">
<option value="2048" selected>2048 bits (~128 bits entropy)</option> <option value="2048" selected>2048 bits (~128 bits entropy)</option>
<option value="3072">3072 bits (~128 bits entropy)</option> <option value="3072">3072 bits (~128 bits entropy)</option>
<option value="4096">4096 bits (~128 bits entropy)</option> <option value="4096">4096 bits (~128 bits entropy)</option>
</select> </select>
<div class="form-text text-warning d-none" id="rsaQrWarning">
<i class="bi bi-exclamation-triangle me-1"></i>QR code unavailable for keys &gt;3072 bits
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -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 // PIN visibility toggle
let pinHidden = false; let pinHidden = false;
function togglePinVisibility() { function togglePinVisibility() {