Skip to content

Commit

Permalink
feature: decode animated WebP (#2)
Browse files Browse the repository at this point in the history
* feat: supports decode animated webp

* test: unit tests for decodeAnimation

* doc: perf readme
  • Loading branch information
nieyuyao committed Apr 12, 2024
1 parent 18fe517 commit b624d2e
Show file tree
Hide file tree
Showing 22 changed files with 365 additions and 146 deletions.
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ on:

jobs:
release:
if: false
runs-on: ubuntu-latest
environment: release

Expand Down
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ EMSDK_INCLUDE_DIR = emsdk/upstream/emscripten/cache/sysroot/include

.PHONY: clean

webp-wasm.js: webp.o decode.o encode.o version.o ${CODEC_DIR}/libwebp.a ${CODEC_DIR}/libsharpyuv.a ${CODEC_DIR}/libsharpyuv.a ${CODEC_DIR}/libwebpmux.a
webp-wasm.js: webp.o decode.o encode.o version.o ${CODEC_DIR}/libwebp.a ${CODEC_DIR}/libsharpyuv.a ${CODEC_DIR}/libwebpmux.a ${CODEC_DIR}/libwebpdemux.a
emsdk/upstream/emscripten/emcc \
-lembind \
-s EXPORT_ES6=$(EXPORT_ES6) \
Expand All @@ -23,19 +23,19 @@ webp-wasm.js: webp.o decode.o encode.o version.o ${CODEC_DIR}/libwebp.a ${CODEC_
-v \
$<

%/libwebp.a %/libsharpyuv.a %/libwebpmux.a: %/Makefile
%/libwebp.a %/libsharpyuv.a %/libwebpmux.a %/libwebpdemux.a: %/Makefile
$(MAKE) -C $(@D)

$(CODEC_DIR)/Makefile: ${CODEC_DIR}/CMakeLists.txt
emsdk/upstream/emscripten/emcmake cmake \
-DCMAKE_DISABLE_FIND_PACKAGE_Threads=1 \
-DWEBP_BUILD_CWEBP=0 \
-DWEBP_BUILD_DWEBP=0 \
-DWEBP_BUILD_CWEBP=ON \
-DWEBP_BUILD_DWEBP=ON \
-DWEBP_BUILD_GIF2WEBP=0 \
-DWEBP_BUILD_IMG2WEBP=0 \
-DWEBP_BUILD_VWEBP=0 \
-DWEBP_BUILD_WEBPINFO=0 \
-DWEBP_BUILD_EXTRAS=0 \
-DWEBP_BUILD_WEBPINFO=0 \
-B $(@D) \
$(<D)

Expand Down
57 changes: 51 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# webp.wasm

webp.wasm is a pure Webassembly / Javascript port of libwebp. The library supports encoding animated webp.
webp.wasm is a pure Webassembly / Javascript port of libwebp. The library supports encoding animated WebP.

![CI](https://github.com/nieyuyao/webp-wasm/workflows/CI/badge.svg)
![latest tag](https://badgen.net/github/release/nieyuyao/webp-wasm)
Expand Down Expand Up @@ -175,7 +175,7 @@ fr.onload = () => {
canvas.style.height = `${result.height}px`
canvas.width = result.width
canvas.height = result.height
ctx.putImageData(result, 0, 0)
ctx.putImageData(new ImageData(new Uint8ClampedArray(result.data)), 0, 0)
}
// read webp file
fr.readAsArrayBuffer(file)
Expand Down Expand Up @@ -207,19 +207,64 @@ fr.readAsArrayBuffer(file)
...
```

#### decodeAnimation

Decoding animated WebP image. Returns an array of frames.

`function decodeAnimation(data: Uint8Array, hasAlpha: boolean): Promise<Nullable<DecodedWebPAnimationFrame[]>>`

##### Example

```javascript
...
const fr = new FileReader()
fr.onload = () => {
if (!fr.result) {
return
}
webpData = fr.result as Uint8Array
const result = await decodeRGBA(webpData)
// draw imageData
...
}
// webp file
fr.readAsArrayBuffer(file)
...
```

#### DecodedWebPAnimationFrame

The object have the following properties:

- DecodedWebPAnimationFrame.width: `number`

The frame image width.

- DecodedWebPAnimationFrame.height: `number`

The frame image height.

- DecodedWebPAnimationFrame.duration: `number`

The frame display duration.

- DecodedWebPAnimationFrame.data: `Uint8Array`

Raw data in pixels.

#### WebPDecodedImageData

The object have the following properties:

- WebPDecodedImageData.width: number
- WebPDecodedImageData.width: `number`

The image width in pixels.

- WebPDecodedImageData.height: number
- WebPDecodedImageData.height: `number`

The image height in pixels.

- WebPDecodedImageData.data: Uint8Array
- WebPDecodedImageData.data: `Uint8Array`

Raw data in pixels.

Expand All @@ -228,7 +273,7 @@ Raw data in pixels.
## Playing Examples

```shell
npm run dev
npm run build-wasm:dev && npm run dev
```

## Building
Expand Down
2 changes: 1 addition & 1 deletion example/Decode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const drawWebp = async () => {
canvas.style.height = `${result.height}px`
canvas.width = result.width
canvas.height = result.height
ctx.putImageData(new ImageData(result.data as unknown as Uint8ClampedArray, result.width), 0, 0)
ctx.putImageData(new ImageData(new Uint8ClampedArray(result.data), result.width), 0, 0)
}
onMounted(async () => {
Expand Down
82 changes: 82 additions & 0 deletions example/DecodeAnimation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { decoderVersion, decodeAnimation } from '../src/'
const canvasRef = ref<HTMLCanvasElement | null>(null)
let isUploaded = false
let webpData: Uint8Array | null = null
let timeoutId = 0
const onChange = (event) => {
const files = (event.target as HTMLInputElement).files
if (!files?.length) {
return
}
isUploaded = true
const file = files[0]
const fr = new FileReader()
fr.onload = () => {
if (!fr.result) {
return
}
webpData = fr.result as Uint8Array
}
fr.readAsArrayBuffer(file)
}
const drawWebp = async () => {
if (!isUploaded) {
alert('Please upload one webp image')
return
}
window.clearTimeout(timeoutId)
const canvas = canvasRef.value
if (!canvas || !webpData) {
return
}
const frames = await decodeAnimation(webpData, true)
if (!frames?.length) {
return
}
const { width, height } = frames[0]
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')!
const loopFrame = (i: number) => {
const frame = frames[i]
ctx.putImageData(new ImageData(new Uint8ClampedArray(frame.data), frame.width), 0, 0)
timeoutId = window.setTimeout(() => {
loopFrame((i + 1) % frames.length)
}, frame.duration)
}
loopFrame(0)
}
onMounted(async () => {
console.log('decoder version is', await decoderVersion())
})
onMounted(() => {
webpData = null
window.clearTimeout(timeoutId)
})
</script>

<template>
<div>
<input type="file" @change="onChange" accept=".webp" />
<button @click="drawWebp">Decode</button>
<canvas ref="canvasRef" />
</div>
</template>

<style scoped>
canvas {
display: block;
}
button {
margin-top: 12px;
display: block;
}
</style>
2 changes: 1 addition & 1 deletion example/Encode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const downloadWebp = async () => {
// buf[j] = pixel
// j++
// })
const result = await encode(imgData as unknown as Uint8Array, canvas.width, canvas.height, true, { lossless: 1, quality: 100 })
const result = await encode((imgData.data) as unknown as Uint8Array, canvas.width, canvas.height, true, { lossless: 1, quality: 100 })
const blob = new Blob([result!], {type: 'image/webp'})
const blobURL = URL.createObjectURL(blob);
const a = document.createElement('a')
Expand Down
10 changes: 6 additions & 4 deletions example/EncodeAnimation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { encodeAnimation } from '../src/'
let rafId = 0
const canvasRef = ref<HTMLCanvasElement | null>(null)
let duration = 20 // ms
const duration = 1000 // ms
let delta = 0
let lastTime = Date.now()
let frames: any[] = []
Expand All @@ -22,13 +22,14 @@ const drawBall = (x = 0) => {
stop()
return
}
const now = Date.now()
if (delta <= duration) {
const now = Date.now()
delta += now - lastTime
lastTime = now
lastTime = now
rafId = requestAnimationFrame(() => drawBall(x))
return
}
lastTime = now
const ctx = canvasRef.value.getContext('2d')!
ctx.clearRect(0, 0, 100, 100)
ctx.fillStyle = 'red'
Expand All @@ -37,9 +38,10 @@ const drawBall = (x = 0) => {
ctx.closePath()
ctx.fill()
x += 10
delta = 0
frames.push({
data: ctx.getImageData(0, 0, 100, 100).data,
duration: 20
duration
})
rafId = requestAnimationFrame(() => drawBall(x))
}
Expand Down
5 changes: 5 additions & 0 deletions example/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const routesArray: Array<RouteRecordRaw> = [
name: 'Decode',
component: () => import('./Decode.vue')
},
{
path: '/decode-animation',
name: 'DecodeAnimation',
component: () => import('./DecodeAnimation.vue')
},
]


Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"build-wasm:dev": "bash ./build.sh -dev",
"build-wasm:esm": "bash ./build.sh -es6",
"build-wasm:cjs": "bash ./build.sh",
"build": "npm run build:esm",
"build:esm": "npm run build-wasm:esm && tsc -m ES6 --outDir ./dist/esm && cp -r src/*.d.ts dist",
"build:cjs": "npm run build-wasm:cjs && tsc -m commonjs --outDir ./dist/cjs && cp -r src/*.d.ts dist",
"test": "node --experimental-wasm-modules --experimental-vm-modules ./node_modules/jest/bin/jest.js",
Expand Down
46 changes: 28 additions & 18 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import type { WebPConfig, Nullable, WebPAnimationFrame, WebPDecodedImageData } from './types'
import type {
WebPConfig,
Nullable,
WebPAnimationFrame,
WebPDecodedImageData,
DecodedWebPAnimationFrame,
} from './types'
export declare const encoderVersion: () => Promise<string>
export declare const encodeRGB: (
rgb: Uint8Array,
width: number,
height: number,
quality?: number
rgb: Uint8Array,
width: number,
height: number,
quality?: number
) => Promise<Nullable<Uint8Array>>
export declare const encodeRGBA: (
rgba: Uint8Array,
width: number,
height: number,
quality?: number
rgba: Uint8Array,
width: number,
height: number,
quality?: number
) => Promise<Nullable<Uint8Array>>
export declare const encode: (
data: Uint8Array,
width: number,
height: number,
hasAlpha: boolean,
config: Partial<WebPConfig>
data: Uint8Array,
width: number,
height: number,
hasAlpha: boolean,
config: Partial<WebPConfig>
) => Promise<Nullable<Uint8Array>>
export declare const encodeAnimation: (
width: number,
height: number,
hasAlpha: boolean,
frames: WebPAnimationFrame[]
width: number,
height: number,
hasAlpha: boolean,
frames: WebPAnimationFrame[]
) => Promise<Nullable<Uint8Array>>
export declare const decoderVersion: () => Promise<string>
export declare const decodeRGB: (data: Uint8Array) => Promise<Nullable<WebPDecodedImageData>>
export declare const decodeRGBA: (data: Uint8Array) => Promise<Nullable<WebPDecodedImageData>>
export declare const decodeAnimation: (
data: Uint8Array,
hasAlpha: boolean
) => Promise<Nullable<DecodedWebPAnimationFrame[]>>
Loading

0 comments on commit b624d2e

Please sign in to comment.