import { config } from "../config.mjs"; import { __ID__, devMode, filePath } from "../consts.mjs"; const { fetchJsonWithTimeout } = foundry.utils; /** * Returns a pseudo-hash for a file. If the browser is in a secure * context this will return a true SHA-1 hash of the file, otherwise * a random ID is generated using the character set A-Za-z0-9. * * @param {File} file The file to hash */ export async function hashFile(file) { // Handle fallback when hashing isn't possible if (!window.isSecureContext || !crypto.subtle.digest) { return foundry.utils.randomID(40); }; // Actually hash the file since we can const bytes = await file.arrayBuffer(); const buffer = await crypto.subtle.digest(`SHA-1`, bytes); const intArray = new Uint8Array(buffer); if (Uint8Array.prototype.toHex) { return intArray.toHex(); }; return Array.from(intArray) .map(b => b.toString(16).padStart(2, `0`)) .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`, config.WEBP_QUALITY, ); }); }; export async function lastModifiedAt(path) { try { const response = await fetch(filePath(path), { method: `HEAD` }); return response.headers.get(`Last-Modified`); } catch { return null; }; }; export async function getFileSize(path) { try { const response = await fetch(filePath(path), { method: `HEAD` }); const bytes = response.headers.get(`Content-Length`); if (!bytes) { return null } // round the non-bytes to 2 decimal places const kilobytes = Math.floor(bytes / 10) / 100; const megabytes = Math.floor(bytes / 10_000) / 100; // determine the most appropriate unit to use let friendly = `${bytes} bytes`; if (megabytes > 0.25) { friendly = `${megabytes}MB`; } else if (kilobytes > 0) { friendly = `${kilobytes}KB`; }; return { bytes, kilobytes, megabytes, friendly }; } catch { return null; }; }; export async function getFile(path) { try { return fetchJsonWithTimeout(filePath(path)); } catch { return undefined; }; }; /** * @param {string} path * @param {any} data */ export async function uploadJson(path, filename, data) { const content = JSON.stringify( data, undefined, devMode() ? `\t` : undefined, ); try { const file = new File( [content], filename, { type: `text/plain` }, ); await uploadFile(path, file); } catch {}; }; export async function uploadFile(path, file) { // uploadPersistent adds "storage" into the path automatically if (path.startsWith(`storage/`)) { path = path.slice(8); }; if (path.endsWith(`/`)) { path = path.slice(0, -1); }; const picker = foundry.applications.apps.FilePicker.implementation; try { await picker.uploadPersistent( __ID__, path, file, ); } catch {}; }; export function determineFileExtension(file) { for (const [short, long] of Object.entries(CONST.IMAGE_FILE_EXTENSIONS)) { if (long === file.type) { return short; }; }; return null; };