Initial commit
This commit is contained in:
192
fusion_claims/static/src/js/attachment_image_compress.js
Normal file
192
fusion_claims/static/src/js/attachment_image_compress.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/** @odoo-module **/
|
||||
/**
|
||||
* Image compression for file uploads on Odoo.
|
||||
*
|
||||
* Problem: On iPhone, selecting 4+ photos (5-15MB each) causes the
|
||||
* browser tab to crash because Odoo converts each to a base64 data URL
|
||||
* before uploading. 7 photos = 50-100MB of strings in memory.
|
||||
*
|
||||
* Solution: Intercept at the FileUploader level, compress each image
|
||||
* via Canvas BEFORE the data URL conversion. A 5MB photo becomes ~300KB.
|
||||
*
|
||||
* The FileUploader.onFileChange is completely overridden (not wrapped)
|
||||
* to avoid any DataTransfer API issues on iPhone Safari.
|
||||
*/
|
||||
import { AttachmentUploadService } from "@mail/core/common/attachment_upload_service";
|
||||
import { FileUploader } from "@web/views/fields/file_handler";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { getDataURLFromFile } from "@web/core/utils/urls";
|
||||
import { checkFileSize } from "@web/core/utils/files";
|
||||
|
||||
const IMAGE_TYPES = new Set([
|
||||
"image/jpeg", "image/png", "image/webp", "image/bmp",
|
||||
"image/heic", "image/heif",
|
||||
]);
|
||||
const MAX_DIMENSION = 1280; // Conservative for mobile memory
|
||||
const JPEG_QUALITY = 0.80;
|
||||
const SKIP_THRESHOLD = 500 * 1024; // 500KB
|
||||
|
||||
/**
|
||||
* Compress an image File via Canvas API.
|
||||
* Returns the original file if anything fails.
|
||||
*/
|
||||
function compressImageFile(file) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
const cleanup = () => {
|
||||
try { URL.revokeObjectURL(objectUrl); } catch(e) {}
|
||||
try { img.src = ""; } catch(e) {}
|
||||
};
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
resolve(file); // Timeout fallback after 10s
|
||||
}, 10000);
|
||||
img.onload = () => {
|
||||
try {
|
||||
clearTimeout(timeout);
|
||||
let w = img.naturalWidth || img.width;
|
||||
let h = img.naturalHeight || img.height;
|
||||
if (w > MAX_DIMENSION || h > MAX_DIMENSION) {
|
||||
const ratio = Math.min(MAX_DIMENSION / w, MAX_DIMENSION / h);
|
||||
w = Math.round(w * ratio);
|
||||
h = Math.round(h * ratio);
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
canvas.getContext("2d").drawImage(img, 0, 0, w, h);
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
cleanup();
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
if (!blob) { resolve(file); return; }
|
||||
const name = file.name.replace(/\.[^.]+$/, "") + ".jpg";
|
||||
resolve(new File([blob], name, {
|
||||
type: "image/jpeg",
|
||||
lastModified: file.lastModified,
|
||||
}));
|
||||
},
|
||||
"image/jpeg",
|
||||
JPEG_QUALITY
|
||||
);
|
||||
} catch (e) {
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
resolve(file);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
resolve(file);
|
||||
};
|
||||
img.src = objectUrl;
|
||||
} catch (e) {
|
||||
resolve(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Override FileUploader.onFileChange to compress images before
|
||||
* converting to data URLs. This prevents the massive memory spike
|
||||
* that crashes mobile browsers.
|
||||
*
|
||||
* We re-implement onFileChange instead of wrapping it to avoid
|
||||
* DataTransfer API issues on iPhone Safari.
|
||||
*/
|
||||
patch(FileUploader.prototype, {
|
||||
async onFileChange(ev) {
|
||||
const rawFiles = ev.target?.files;
|
||||
if (!rawFiles || !rawFiles.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any file needs compression
|
||||
let hasLargeImages = false;
|
||||
for (const f of rawFiles) {
|
||||
if (IMAGE_TYPES.has(f.type) && f.size > SKIP_THRESHOLD) {
|
||||
hasLargeImages = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No large images -- use standard Odoo behavior
|
||||
if (!hasLargeImages) {
|
||||
return super.onFileChange(ev);
|
||||
}
|
||||
|
||||
// Process files one at a time with compression
|
||||
const files = [...rawFiles].filter((f) => this.validFileType(f));
|
||||
const target = ev.target;
|
||||
|
||||
for (const file of files) {
|
||||
let processedFile = file;
|
||||
|
||||
// Compress large images
|
||||
if (IMAGE_TYPES.has(file.type) && file.size > SKIP_THRESHOLD) {
|
||||
try {
|
||||
processedFile = await compressImageFile(file);
|
||||
} catch (e) {
|
||||
processedFile = file; // fallback to original
|
||||
}
|
||||
}
|
||||
|
||||
// Size check
|
||||
if (this.props.checkSize && !checkFileSize(processedFile.size, this.notification)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.state.isUploading = true;
|
||||
try {
|
||||
const data = await getDataURLFromFile(processedFile);
|
||||
if (!processedFile.size) {
|
||||
this.notification.add(
|
||||
`There was a problem while uploading: ${processedFile.name}`,
|
||||
{ type: "danger" }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await this.props.onUploaded({
|
||||
name: processedFile.name,
|
||||
size: processedFile.size,
|
||||
type: processedFile.type,
|
||||
data: data.split(",")[1],
|
||||
objectUrl:
|
||||
processedFile.type === "application/pdf"
|
||||
? URL.createObjectURL(processedFile)
|
||||
: null,
|
||||
});
|
||||
} catch (e) {
|
||||
// Skip this file on error, continue with others
|
||||
} finally {
|
||||
this.state.isUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input so same file can be re-selected
|
||||
target.value = null;
|
||||
if (this.props.multiUpload && this.props.onUploadComplete) {
|
||||
this.props.onUploadComplete({});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Safety net for drag-drop and paste uploads that bypass FileUploader.
|
||||
*/
|
||||
patch(AttachmentUploadService.prototype, {
|
||||
async upload(thread, composer, file, options) {
|
||||
if (file && IMAGE_TYPES.has(file.type) && file.size > SKIP_THRESHOLD) {
|
||||
try {
|
||||
file = await compressImageFile(file);
|
||||
} catch (e) {
|
||||
// Use original file
|
||||
}
|
||||
}
|
||||
return super.upload(thread, composer, file, options);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user