diff --git a/app/javascript/application.js b/app/javascript/application.js index 311b1612..bec3cc0b 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -21,6 +21,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete' import FolderAutocomplete from './elements/folder_autocomplete' import SignatureForm from './elements/signature_form' import SubmitForm from './elements/submit_form' +import ConvertUpload from './elements/convert_upload' import PromptPassword from './elements/prompt_password' import EmailsTextarea from './elements/emails_textarea' import ToggleSubmit from './elements/toggle_submit' @@ -111,6 +112,7 @@ safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete) safeRegisterElement('folder-autocomplete', FolderAutocomplete) safeRegisterElement('signature-form', SignatureForm) safeRegisterElement('submit-form', SubmitForm) +safeRegisterElement('convert-upload', ConvertUpload) safeRegisterElement('prompt-password', PromptPassword) safeRegisterElement('emails-textarea', EmailsTextarea) safeRegisterElement('toggle-cookies', ToggleCookies) diff --git a/app/javascript/elements/convert_upload.js b/app/javascript/elements/convert_upload.js new file mode 100644 index 00000000..4124ad5b --- /dev/null +++ b/app/javascript/elements/convert_upload.js @@ -0,0 +1,73 @@ +export function convertImage (sourceFile, targetType, quality) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + + reader.onload = function (event) { + const img = new Image() + + img.onload = function () { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + canvas.width = img.width + canvas.height = img.height + ctx.drawImage(img, 0, 0) + canvas.toBlob(function (blob) { + const ext = targetType === 'image/jpeg' ? '.jpg' : '.png' + const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType }) + resolve(newFile) + }, targetType, quality) + } + + img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`)) + + img.src = event.target.result + } + reader.onerror = reject + reader.readAsDataURL(sourceFile) + }) +} + +export async function convertImagesInInput (input) { + if (!input.files || input.files.length === 0) return + + const dt = new DataTransfer() + let didConvert = false + + for (const file of Array.from(input.files)) { + let converted = file + + try { + if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml'].includes(file.type)) { + converted = await convertImage(file, 'image/png') + didConvert = true + } else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence'].includes(file.type)) { + converted = await convertImage(file, 'image/jpeg', 0.9) + didConvert = true + } + } catch (e) { + alert(e.message) + } + + dt.items.add(converted) + } + + if (didConvert) { + input.files = dt.files + } +} + +export default class extends HTMLElement { + connectedCallback () { + const input = this.querySelector('input[type="file"]') + const form = input.form + + input.addEventListener('change', async () => { + await convertImagesInInput(input) + + form.querySelector('[type="submit"]')?.setAttribute('disabled', true) + + form.requestSubmit() + }) + } +} diff --git a/app/javascript/elements/dashboard_dropzone.js b/app/javascript/elements/dashboard_dropzone.js index 886aae37..434b33b7 100644 --- a/app/javascript/elements/dashboard_dropzone.js +++ b/app/javascript/elements/dashboard_dropzone.js @@ -1,4 +1,5 @@ import { target, targets, targetable } from '@github/catalyst/lib/targetable' +import { convertImagesInInput } from './convert_upload' const loadingIconHtml = ` @@ -150,12 +151,16 @@ export default targetable(class extends HTMLElement { if (!this.isLoading) this.hideDraghover() } - uploadFiles (files, url) { + async uploadFiles (files, url) { this.isLoading = true this.form.action = url - this.form.querySelector('[type="file"]').files = files + const input = this.form.querySelector('[type="file"]') + + input.files = files + + await convertImagesInInput(input) this.form.querySelector('[type="submit"]').click() } diff --git a/app/javascript/elements/file_dropzone.js b/app/javascript/elements/file_dropzone.js index 12ef253d..fb8c1ae7 100644 --- a/app/javascript/elements/file_dropzone.js +++ b/app/javascript/elements/file_dropzone.js @@ -1,5 +1,6 @@ import { actionable } from '@github/catalyst/lib/actionable' import { target, targetable } from '@github/catalyst/lib/targetable' +import { convertImagesInInput } from './convert_upload' export default actionable(targetable(class extends HTMLElement { static [target.static] = [ @@ -38,17 +39,21 @@ export default actionable(targetable(class extends HTMLElement { this.classList.add('border-base-300', 'hover:bg-base-200/30') } - onDrop (e) { + async onDrop (e) { e.preventDefault() this.input.files = e.dataTransfer.files - this.uploadFiles(e.dataTransfer.files) + await convertImagesInInput(this.input) + + this.uploadFiles(this.input.files) } - onSelectFiles (e) { + async onSelectFiles (e) { e.preventDefault() + await convertImagesInInput(this.input) + this.uploadFiles(this.input.files) } diff --git a/app/javascript/submission_form/dropzone.vue b/app/javascript/submission_form/dropzone.vue index 6cc96866..25790206 100644 --- a/app/javascript/submission_form/dropzone.vue +++ b/app/javascript/submission_form/dropzone.vue @@ -51,6 +51,36 @@