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,
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."""

View File

@@ -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;
}
</style>
<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>
<span class="text-muted small">Drop QR image or click to browse</span>
</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>
@@ -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 =
'<i class="bi bi-check-circle text-success me-1"></i>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 =
'<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 => {
// 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);
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>
<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>
<span class="text-muted small">Drop QR image or click to browse</span>
</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>
@@ -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 =
'<i class="bi bi-check-circle text-success me-1"></i>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 =
'<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 => {
// 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 class="mt-2 d-none" id="rsaOptions">
<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="3072">3072 bits (~128 bits entropy)</option>
<option value="4096">4096 bits (~128 bits entropy)</option>
</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>
@@ -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() {