Disegno Tecnico: BMP convertiti in PNG nel browser, errore upload nel riquadro. Refs #107
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* image_to_png.js - #107: converte nel browser le immagini che il backend non ammette (es. BMP) in PNG.
|
||||
*
|
||||
* Cosa fa: tmfNeedsPng(type) dice se il MIME e' un'immagine fuori da PNG/JPEG/GIF/WebP;
|
||||
* tmfImageToPng(file) la ridisegna su canvas e ritorna un File PNG (null se illeggibile, es. TIFF).
|
||||
* Come si usa: await tmfImageToPng(file) prima del ritaglio e del caricamento.
|
||||
* Dipende da: createImageBitmap e canvas del browser (non servono nei test: il caso "nessuna conversione" e' puro).
|
||||
*/
|
||||
// #107: tipi che il backend ammette gia': non vanno convertiti
|
||||
const TMF_PNG_PASSTHROUGH = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'];
|
||||
|
||||
// #107: true per un'immagine (image/*) non ammessa dal backend, es. image/bmp
|
||||
function tmfNeedsPng(type) {
|
||||
return typeof type === 'string' && type.indexOf('image/') === 0 && TMF_PNG_PASSTHROUGH.indexOf(type) === -1;
|
||||
}
|
||||
|
||||
// #107: converte in PNG con canvas; ritorna il file invariato se non serve, null se il browser non sa decodificarlo
|
||||
async function tmfImageToPng(file) {
|
||||
if (!file || !tmfNeedsPng(file.type)) return file;
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = bitmap.width;
|
||||
canvas.height = bitmap.height;
|
||||
canvas.getContext('2d').drawImage(bitmap, 0, 0);
|
||||
if (bitmap.close) bitmap.close();
|
||||
const blob = await new Promise(function (resolve) { canvas.toBlob(resolve, 'image/png'); });
|
||||
if (!blob) return null;
|
||||
const base = (file.name || 'immagine').replace(/\.[^.]+$/, '');
|
||||
return new File([blob], base + '.png', { type: 'image/png' });
|
||||
} catch (err) {
|
||||
// #107: formato non decodificabile dal browser (es. TIFF)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// #107: export per il test node (nel browser module non esiste)
|
||||
if (typeof module !== 'undefined') module.exports = { tmfNeedsPng, tmfImageToPng };
|
||||
@@ -504,6 +504,7 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/image-crop.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/image_to_png.js') }}"></script>{# #107 #}
|
||||
<script>
|
||||
// #56: stringhe e percorsi per il ritaglio prima dell'upload
|
||||
ImageCrop.configure({
|
||||
@@ -686,6 +687,13 @@ function recipeEditor() {
|
||||
async uploadFile(file) {
|
||||
if (!file) return;
|
||||
|
||||
// #107: BMP e simili (leggibili dal browser, non dal backend) diventano PNG prima dei controlli
|
||||
file = await tmfImageToPng(file);
|
||||
if (!file) {
|
||||
this.errorMessage = {{ _("Formato immagine non leggibile. Usa PNG, JPG o PDF.")|tojson }};
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
|
||||
if (!allowed.includes(file.type)) {
|
||||
|
||||
@@ -879,6 +879,8 @@
|
||||
@change="uploadTaskFile(task.id, $event.target.files[0], task); $event.target.value = ''">
|
||||
</label>
|
||||
</template>
|
||||
<!-- #107: errore di caricamento anche dentro il riquadro, non solo nel banner in cima -->
|
||||
<p x-show="uploadErrorTask === task.id && errorMessage" x-text="errorMessage" class="text-xs text-red-600 dark:text-red-400 mt-2"></p>
|
||||
</div>
|
||||
|
||||
<!-- Task Directive / Description (read-only display) -->
|
||||
@@ -1484,6 +1486,7 @@
|
||||
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';</script>
|
||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||
<script src="{{ url_for('static', filename='js/image-crop.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/image_to_png.js') }}"></script>{# #107 #}
|
||||
<script>
|
||||
// #56: stringhe e percorsi per il ritaglio prima dell'upload
|
||||
ImageCrop.configure({
|
||||
@@ -1530,6 +1533,7 @@ function taskEditor() {
|
||||
showAddTask: false,
|
||||
saving: false,
|
||||
errorMessage: '',
|
||||
uploadErrorTask: null, // #107: id del task il cui upload e' fallito (errore mostrato nel riquadro Disegno Tecnico)
|
||||
successMessage: '',
|
||||
|
||||
// ---- Task CRUD state ----
|
||||
@@ -2415,14 +2419,24 @@ function taskEditor() {
|
||||
async uploadTaskFile(taskId, file, task) {
|
||||
if (!file) return;
|
||||
|
||||
// #107: BMP e simili (leggibili dal browser, non dal backend) diventano PNG prima dei controlli
|
||||
file = await tmfImageToPng(file);
|
||||
if (!file) {
|
||||
this.errorMessage = {{ _("Formato immagine non leggibile. Usa PNG, JPG o PDF.")|tojson }};
|
||||
this.uploadErrorTask = taskId;
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'application/pdf'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
this.errorMessage = {{ _("Formato file non supportato. Usa PNG, JPG o PDF.")|tojson }};
|
||||
this.uploadErrorTask = taskId; // #107
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
this.errorMessage = {{ _("File troppo grande. Dimensione massima: 20MB.")|tojson }};
|
||||
this.uploadErrorTask = taskId; // #107
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2432,6 +2446,7 @@ function taskEditor() {
|
||||
|
||||
this.saving = true;
|
||||
this.errorMessage = '';
|
||||
this.uploadErrorTask = null; // #107: nuovo tentativo, azzera l'errore del riquadro
|
||||
|
||||
try {
|
||||
// 1. Upload file
|
||||
@@ -2448,6 +2463,7 @@ function taskEditor() {
|
||||
const uploadData = await uploadResp.json();
|
||||
if (uploadData.error) {
|
||||
this.errorMessage = uploadData.detail || {{ _("Errore durante il caricamento del file")|tojson }};
|
||||
this.uploadErrorTask = taskId; // #107
|
||||
this.saving = false;
|
||||
return;
|
||||
}
|
||||
@@ -2472,6 +2488,7 @@ function taskEditor() {
|
||||
const updateData = await updateResp.json();
|
||||
if (updateData.error) {
|
||||
this.errorMessage = updateData.detail || {{ _("Errore nel salvataggio del task")|tojson }};
|
||||
this.uploadErrorTask = taskId; // #107
|
||||
this.saving = false;
|
||||
return;
|
||||
}
|
||||
@@ -2487,6 +2504,7 @@ function taskEditor() {
|
||||
} catch (err) {
|
||||
console.error('uploadTaskFile error:', err);
|
||||
this.errorMessage = {{ _("Errore di connessione al server")|tojson }};
|
||||
this.uploadErrorTask = taskId; // #107
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* image_to_png.js, run with plain node (#107): solo la parte pura (niente canvas qui).
|
||||
* Run from src/frontend/flask_app: node tests/js/test_image_to_png.js
|
||||
*/
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const { tmfNeedsPng, tmfImageToPng } = require(
|
||||
path.join(__dirname, '..', '..', 'static', 'js', 'image_to_png.js'),
|
||||
);
|
||||
|
||||
let failures = 0;
|
||||
function check(label, cond) {
|
||||
if (cond) { console.log('ok - ' + label); } else { console.error('FAIL - ' + label); failures++; }
|
||||
}
|
||||
|
||||
check('bmp va convertito', tmfNeedsPng('image/bmp') === true);
|
||||
check('tiff va convertito', tmfNeedsPng('image/tiff') === true);
|
||||
check('png no', tmfNeedsPng('image/png') === false);
|
||||
check('jpeg no', tmfNeedsPng('image/jpeg') === false);
|
||||
check('gif no', tmfNeedsPng('image/gif') === false);
|
||||
check('webp no', tmfNeedsPng('image/webp') === false);
|
||||
check('pdf no', tmfNeedsPng('application/pdf') === false);
|
||||
check('tipo vuoto no', tmfNeedsPng('') === false);
|
||||
|
||||
(async () => {
|
||||
const fake = { name: 'a.png', type: 'image/png' };
|
||||
check('png ritorna lo stesso oggetto', (await tmfImageToPng(fake)) === fake);
|
||||
const pdf = { name: 'a.pdf', type: 'application/pdf' };
|
||||
check('pdf ritorna lo stesso oggetto', (await tmfImageToPng(pdf)) === pdf);
|
||||
if (failures) { console.error(failures + ' failure(s)'); process.exit(1); }
|
||||
console.log('all ok');
|
||||
})();
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Runs the plain-node image_to_png.js test (tests/js/test_image_to_png.js).
|
||||
|
||||
Covers gitea:Adriano/TieMeasureFlow#107.
|
||||
"""
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
FLASK_APP_DIR = Path(__file__).resolve().parent.parent
|
||||
JS_TEST = Path(__file__).resolve().parent / "js" / "test_image_to_png.js"
|
||||
|
||||
|
||||
def test_image_to_png_js_pure_behaviour():
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node non installato: test JS di image_to_png.js saltato")
|
||||
|
||||
result = subprocess.run(
|
||||
[node, str(JS_TEST)],
|
||||
cwd=str(FLASK_APP_DIR),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"tests/js/test_image_to_png.js fallito (exit {result.returncode})\n"
|
||||
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
||||
)
|
||||
@@ -3584,3 +3584,7 @@ msgstr "File not found"
|
||||
#, python-format
|
||||
msgid "Servizio non raggiungibile: %(detail)s"
|
||||
msgstr "Service not reachable: %(detail)s"
|
||||
|
||||
#: templates/maker/task_editor.html templates/maker/recipe_editor.html (#107)
|
||||
msgid "Formato immagine non leggibile. Usa PNG, JPG o PDF."
|
||||
msgstr "Unreadable image format. Use PNG, JPG or PDF."
|
||||
|
||||
@@ -3568,3 +3568,7 @@ msgstr "File non trovato"
|
||||
#, python-format
|
||||
msgid "Servizio non raggiungibile: %(detail)s"
|
||||
msgstr "Servizio non raggiungibile: %(detail)s"
|
||||
|
||||
#: templates/maker/task_editor.html templates/maker/recipe_editor.html (#107)
|
||||
msgid "Formato immagine non leggibile. Usa PNG, JPG o PDF."
|
||||
msgstr "Formato immagine non leggibile. Usa PNG, JPG o PDF."
|
||||
|
||||
Reference in New Issue
Block a user