import {Webp} from "../libwebp/dist/webp.js" import {loadBinaryData} from "./load-binary-data.js" import {getMimeType} from "./utils/get-mime-type.js" import {parseDataUrl} from "./utils/parse-data-url.js" import {detectWebpSupport} from "./detect-webp-support.js" import {convertDataURIToBinary, isBase64Url} from "./convert-binary-data.js" import {WebpMachineOptions, PolyfillDocumentOptions, DetectWebpImage} from "./interfaces.js" import { detectCanvasReadingSupport } from "./detect-canvas-reading-support.js" const relax = () => new Promise(resolve => setTimeout(resolve, 0)) export class WebpMachineError extends Error {} /** * detect a webp image by its extension * @deprecated please use `improvedWebpImageDetector` instead, but note it returns a promise */ export const defaultDetectWebpImage: DetectWebpImage = (image: HTMLImageElement) => /\.webp.*$/i.test(image.src) export async function improvedWebpImageDetector(image: HTMLImageElement) { const dataUrl = parseDataUrl(image.src) const type = dataUrl ? dataUrl.format : await getMimeType(image.src) return type.indexOf("image/webp") !== -1 } /** * Webp Machine * - decode and polyfill webp images * - can only decode images one-at-a-time (otherwise will throw busy error) */ export class WebpMachine { private readonly webp: Webp private readonly webpSupport: boolean | Promise private readonly detectWebpImage: DetectWebpImage private busy = false private cache: {[key: string]: string | HTMLCanvasElement} = {} private useCanvasElements: boolean constructor({ webp = new Webp(), webpSupport = detectWebpSupport(), useCanvasElements = !detectCanvasReadingSupport(), detectWebpImage = improvedWebpImageDetector, }: WebpMachineOptions = {}) { this.webp = webp this.webpSupport = webpSupport this.useCanvasElements = useCanvasElements this.detectWebpImage = detectWebpImage } /** * Replace an element with a element */ static replaceImageWithCanvas(image: HTMLImageElement, canvas: HTMLCanvasElement) { canvas.className = image.className canvas.style.cssText = window.getComputedStyle(image).cssText canvas.style.pointerEvents = canvas.style.pointerEvents || "none" const imageWidth = image.getAttribute("width") const imageHeight = image.getAttribute("height") canvas.style.width = image.style.width || (imageWidth ? `${imageWidth}px` : "auto") canvas.style.height = image.style.height || (imageHeight ? `${imageHeight}px` : "auto") const parent = image.parentElement parent.replaceChild(canvas, image) } /** * Make a copy of a canvas element (useful for caching) */ static cloneCanvas(oldCanvas: HTMLCanvasElement) { const newCanvas = document.createElement("canvas") newCanvas.className = oldCanvas.className newCanvas.width = oldCanvas.width newCanvas.height = oldCanvas.height newCanvas.style.cssText = window.getComputedStyle(oldCanvas).cssText const context = newCanvas.getContext("2d") context.drawImage(oldCanvas, 0, 0) return newCanvas } /** * Paint a webp image onto a canvas element */ async decodeToCanvas(canvas: HTMLCanvasElement, webpData: Uint8Array) { if (this.busy) throw new WebpMachineError("cannot decode when already busy") this.busy = true try { await relax() this.webp.setCanvas(canvas) this.webp.webpToSdl(webpData, webpData.length) } catch (error) { error.name = WebpMachineError.name error.message = `failed to decode webp image: ${error.message}` throw error } finally { this.busy = false } } /** * Decode raw webp data into a png data url */ async decode(webpData: Uint8Array): Promise { const canvas = document.createElement("canvas") await this.decodeToCanvas(canvas, webpData) return canvas.toDataURL() } /** * Polyfill the webp format on the given element */ async polyfillImage(image: HTMLImageElement): Promise { if (await this.webpSupport) return const {src} = image if (await this.detectWebpImage(image)) { if (this.cache[src]) { if (this.useCanvasElements) { const canvas = WebpMachine.cloneCanvas(this.cache[src]) WebpMachine.replaceImageWithCanvas(image, canvas) } else image.src = this.cache[src] } else { try { const webpData = isBase64Url(src) ? convertDataURIToBinary(src) : await loadBinaryData(src) if (this.useCanvasElements) { const canvas = document.createElement("canvas") await this.decodeToCanvas(canvas, webpData) WebpMachine.replaceImageWithCanvas(image, canvas) this.cache[src] = canvas } else { const pngData = await this.decode(webpData) image.src = this.cache[src] = pngData } } catch (error) { error.name = WebpMachineError.name error.message = `failed to polyfill image "${src}": ${error.message}` console.error(error) } } } } /** * Polyfill webp format on the entire web page */ async polyfillDocument({ document = window.document }: PolyfillDocumentOptions = {}): Promise { for (const image of Array.from(document.querySelectorAll("img"))) await this.polyfillImage(image) } /** * Manually wipe the cache to save memory */ clearCache() { this.cache = {} } }