From 7d80ac3c93d58cfce714feb8ab02dc0e2befa233 Mon Sep 17 00:00:00 2001 From: Jonas Geiler Date: Sun, 20 Oct 2024 19:42:11 +0200 Subject: [PATCH] Improve media filtering by using OffscreenCanvas, if supported Also improve browser support by testing for each feature instead of checking the user agent. Also improve error handling by using promises. Fixes #2939 Fixes #4194 --- .../assets/js/components/ComposeModal.vue | 98 +++++++++++++------ 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/resources/assets/js/components/ComposeModal.vue b/resources/assets/js/components/ComposeModal.vue index 3994d79d2..6036185f4 100644 --- a/resources/assets/js/components/ComposeModal.vue +++ b/resources/assets/js/components/ComposeModal.vue @@ -1766,57 +1766,91 @@ export default { applyFilterToMedia() { // this is where the magic happens - var ua = navigator.userAgent.toLowerCase(); - if(ua.indexOf('firefox') == -1 && ua.indexOf('chrome') == -1) { - this.isPosting = false; - swal('Oops!', 'Your browser does not support the filter feature.', 'error'); - this.page = 3; - return; - } - let count = this.media.filter(m => m.filter_class).length; if(count) { this.page = 'filteringMedia'; this.filteringRemainingCount = count; this.$nextTick(() => { this.isFilteringMedia = true; - this.media.forEach((media, idx) => this.applyFilterToMediaSave(media, idx)); + Promise.all(this.media.map(media => { + return this.applyFilterToMediaSave(media); + })).catch(err => { + console.error(err); + swal('Oops!', 'An error occurred while applying filters to your media. Please refresh the page and try again. If the problem persist, please try a different web browser.', 'error'); + }); }) } else { this.page = 3; } }, - applyFilterToMediaSave(media, idx) { + async applyFilterToMediaSave(media) { if(!media.filter_class) { return; } - let self = this; - let data = null; - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - let image = document.createElement('img'); + // Load image + const image = document.createElement('img'); image.src = media.url; - image.addEventListener('load', e => { + await new Promise((resolve, reject) => { + image.addEventListener('load', () => resolve()); + image.addEventListener('error', () => { + reject(new Error('Failed to load image')); + }); + }); + + // Create canvas + let canvas; + let usingOffscreenCanvas = false; + if('OffscreenCanvas' in window) { + canvas = new OffscreenCanvas(image.width, image.height); + usingOffscreenCanvas = true; + } else { + canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; - ctx.filter = App.util.filterCss[media.filter_class]; - ctx.drawImage(image, 0, 0, image.width, image.height); - ctx.save(); - canvas.toBlob(function(blob) { - data = new FormData(); - data.append('file', blob); - data.append('id', media.id); - axios.post('/api/compose/v0/media/update', data) - .then(res => { - self.media[idx].is_filtered = true; - self.updateFilteringMedia(); - }).catch(err => { - }); - }, media.mime, 0.9); - }); - ctx.clearRect(0, 0, image.width, image.height); + } + + // Draw image with filter to canvas + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get canvas context'); + } + if (!('filter' in ctx)) { + throw new Error('Canvas filter not supported'); + } + ctx.filter = App.util.filterCss[media.filter_class]; + ctx.drawImage(image, 0, 0, image.width, image.height); + ctx.save(); + + // Convert canvas to blob + let blob; + if(usingOffscreenCanvas) { + blob = await canvas.convertToBlob({ + type: media.mime, + quality: 1, + }); + } else { + blob = await new Promise(resolve => { + canvas.toBlob(blob => { + if(blob) { + resolve(blob); + } else { + reject( + new Error('Failed to convert canvas to blob'), + ); + } + }, media.mime, 1); + }); + } + + // Upload blob / Update media + const data = new FormData(); + data.append('file', blob); + data.append('id', media.id); + await axios.post('/api/compose/v0/media/update', data); + media.is_filtered = true; + this.updateFilteringMedia(); }, updateFilteringMedia() {