Snazzy ui updates.
This commit is contained in:
@@ -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."""
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 >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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user