-
Notifications
You must be signed in to change notification settings - Fork 22
/
webp-machine.ts
189 lines (164 loc) · 5.22 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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<boolean>
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 <img> element with a <canvas> 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<string> {
const canvas = document.createElement("canvas")
await this.decodeToCanvas(canvas, webpData)
return canvas.toDataURL()
}
/**
* Polyfill the webp format on the given <img> element
*/
async polyfillImage(image: HTMLImageElement): Promise<void> {
if (await this.webpSupport)
return
const {src} = image
if (await this.detectWebpImage(image)) {
if (this.cache[src]) {
if (this.useCanvasElements) {
const canvas = WebpMachine.cloneCanvas(<HTMLCanvasElement>this.cache[src])
WebpMachine.replaceImageWithCanvas(image, canvas)
}
else
image.src = <string>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<void> {
for (const image of Array.from(document.querySelectorAll("img")))
await this.polyfillImage(image)
}
/**
* Manually wipe the cache to save memory
*/
clearCache() {
this.cache = {}
}
}