Files
Odoo-Modules/fusion_claims/static/src/js/attachment_image_compress.js
2026-02-22 01:22:18 -05:00

193 lines
6.8 KiB
JavaScript

/** @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);
},
});