forked from chase-moskal/webp-hero
-
Notifications
You must be signed in to change notification settings - Fork 0
/
webp-machine.ts
111 lines (100 loc) · 2.95 KB
/
webp-machine.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import {Webp} from "../libwebp/dist/webp.js"
import {loadBinaryData} from "./load-binary-data.js"
import {detectWebpSupport} from "./detect-webp-support.js"
import {convertDataURIToBinary, isBase64Url} from "./convert-binary-data.js"
import {WebpMachineOptions, PolyfillDocumentOptions, DetectWebpImage} from "./interfaces.js"
const relax = () => new Promise(resolve => requestAnimationFrame(resolve))
export class WebpMachineError extends Error {}
export const defaultDetectWebpImage: DetectWebpImage = (image: HTMLImageElement) =>
/\.webp.*$/i.test(image.src)
/**
* 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: Promise<boolean>
private readonly detectWebpImage: DetectWebpImage
private busy = false
private cache: {[key: string]: string} = {}
constructor({
webp = new Webp(),
webpSupport = detectWebpSupport(),
detectWebpImage = defaultDetectWebpImage
}: WebpMachineOptions = {}) {
this.webp = webp
this.webpSupport = webpSupport
this.detectWebpImage = detectWebpImage
}
/**
* Decode raw webp data into a png data url
*/
async decode(webpData: Uint8Array): Promise<string> {
if (this.busy) throw new WebpMachineError("cannot decode when already busy")
this.busy = true
try {
await relax()
const canvas = document.createElement("canvas")
this.webp.setCanvas(canvas)
this.webp.webpToSdl(webpData, webpData.length)
this.busy = false
return canvas.toDataURL()
}
catch (error) {
this.busy = false
error.name = WebpMachineError.name
error.message = `failed to decode webp image: ${error.message}`
throw error
}
}
/**
* Polyfill the webp format on the given <img> element
*/
async polyfillImage(image: HTMLImageElement): Promise<void> {
if (await this.webpSupport) return
const {src} = image
if (this.detectWebpImage(image)) {
if (this.cache[src]) {
image.src = this.cache[src]
return
}
try {
const webpData = isBase64Url(src)
? convertDataURIToBinary(src)
: await loadBinaryData(src)
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}`
throw error
}
}
}
/**
* Polyfill webp format on the entire web page
*/
async polyfillDocument({
document = window.document
}: PolyfillDocumentOptions = {}): Promise<void> {
if (await this.webpSupport) return null
for (const image of Array.from(document.querySelectorAll("img"))) {
try {
await this.polyfillImage(image)
}
catch (error) {
error.name = WebpMachineError.name
error.message = `webp image polyfill failed for url "${image.src}": ${error}`
throw error
}
}
}
/**
* Manually wipe the cache to save memory
*/
clearCache() {
this.cache = {}
}
}