From 73601c579c5083e7d081b0b10c773cc6cb419263 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 3 Feb 2026 18:20:43 -0700 Subject: [PATCH] Convert image uploads into webp files unless it's a gif (closes #11) --- module/api.mjs | 7 +++---- module/apps/ImageApp.mjs | 16 ++++++++++++---- module/config.mjs | 3 +++ module/utils/fs.mjs | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 module/config.mjs diff --git a/module/api.mjs b/module/api.mjs index 5d4dc5e..f74de0f 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -5,11 +5,9 @@ import { ArtistBrowser } from "./apps/ArtistBrowser.mjs"; import { ImageApp } from "./apps/ImageApp.mjs"; // Utils -import { getFileSize, hashFile, lastModifiedAt } from "./utils/fs.mjs"; +import { convertToWebp, getFileSize, hashFile, lastModifiedAt } from "./utils/fs.mjs"; -const { deepFreeze } = foundry.utils; - -export const api = deepFreeze({ +export const api = foundry.utils.deepFreeze({ Apps: { ArtBrowser, ArtistBrowser, @@ -18,6 +16,7 @@ export const api = deepFreeze({ }, utils: { fs: { + convertToWebp, hashFile, lastModifiedAt, getFileSize, diff --git a/module/apps/ImageApp.mjs b/module/apps/ImageApp.mjs index e33294e..4d80b1e 100644 --- a/module/apps/ImageApp.mjs +++ b/module/apps/ImageApp.mjs @@ -1,5 +1,5 @@ import { __ID__, filePath } from "../consts.mjs"; -import { determineFileExtension, getFile, hashFile, lastModifiedAt, uploadFile, uploadJson } from "../utils/fs.mjs"; +import { convertToWebp, determineFileExtension, getFile, hashFile, lastModifiedAt, uploadFile, uploadJson } from "../utils/fs.mjs"; import { DBConnectorMixin } from "./mixins/DBConnector.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -147,7 +147,7 @@ export class ImageApp extends this.#artistCache.push({ value: id, label: artist.name }); }; }; - console.log(this._doc); + const ctx = { meta: { idp: this.id, @@ -170,7 +170,7 @@ export class ImageApp extends #file = null; async #changeImage(event) { /** @type {File} */ - const file = this.#file = event.target.files[0]; + let file = this.#file = event.target.files[0]; // Prevent memory leaks URL.revokeObjectURL(this._doc.path); @@ -185,7 +185,15 @@ export class ImageApp extends }; // Ensure we don't already have the file uploaded - const extension = determineFileExtension(file); + const webp = await convertToWebp(file); + let extension; + if (webp) { + file = this.#file = webp; + extension = `webp`; + } else { + extension = determineFileExtension(file); + }; + const hash = await hashFile(file); const path = `storage/tokens/${hash}.${extension}`; const lastModified = await lastModifiedAt(path); diff --git a/module/config.mjs b/module/config.mjs new file mode 100644 index 0000000..18d44cb --- /dev/null +++ b/module/config.mjs @@ -0,0 +1,3 @@ +export const config = CONFIG.ImageTagger = foundry.utils.deepSeal({ + WEBP_IGNORE: [`image/gif`], +}); diff --git a/module/utils/fs.mjs b/module/utils/fs.mjs index 805a968..088261a 100644 --- a/module/utils/fs.mjs +++ b/module/utils/fs.mjs @@ -1,3 +1,4 @@ +import { config } from "../config.mjs"; import { __ID__, devMode, filePath } from "../consts.mjs"; const { fetchJsonWithTimeout } = foundry.utils; @@ -28,6 +29,41 @@ export async function hashFile(file) { .join(``); }; +/** + * Converts an image file into a webp file unless it is already a webp file or + * cannot be converted to a webp, in which case it doesn't modify the file at all. + * + * @param {File} file The file to convert + * @returns {Promise} + */ +export async function convertToWebp(file) { + if (file.type === `image/webp`) { return null }; + if (config.WEBP_IGNORE.includes(file.type)) { return null }; + + /** @type {HTMLImageElement} */ + const image = document.createElement(`img`); + const url = URL.createObjectURL(file); + image.src = url; + await image.decode(); + + /** @type {HTMLCanvasElement} */ + const canvas = document.createElement(`canvas`); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + canvas.getContext(`2d`).drawImage(image, 0, 0); + + return new Promise((resolve) => { + canvas.toBlob( + (blob) => { + const name = file.name.split(`.`).slice(0, -1).join(`.`); + const webp = new File([blob], `${name}.webp`, { type: blob.type }); + resolve(webp); + }, + `image/webp`, + ); + }); +}; + export async function lastModifiedAt(path) { try { const response = await fetch(filePath(path), { method: `HEAD` });