Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance compatibility of emscripten built codecs with Cloudflare Workers #21

Merged
merged 10 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- run: yarn
- run: yarn build
node-version: 20
- run: npm install
- run: npm run build
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,7 @@ dist
.DS_Store

# C/C++ Files
*.o
*.o

# lockfile
package-lock.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jSquash name is inspired by jQuery and Squoosh. It symbolizes the browser suppor
## Differences with Squoosh

- The codecs and tools are built for both Web and Web Worker environments
- No dynamic code execution, can be run in strict environments that do not allow code evaluation. Like Cloudflare Workers.
- No dynamic code execution, the packages can be run in strict environments that do not allow code evaluation. Like Cloudflare Workers.
- Does not rely on TextEncoder/TextDecoder API (could reduce performance) but allows it to be run in simpler V8 runtimes that only support UTF-8 (Cloudflare Workers, Vercel Edge Functions etc.)

## Packages
Expand Down
3 changes: 2 additions & 1 deletion examples/cloudflare-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
worker.js
worker.js
.wrangler
32 changes: 32 additions & 0 deletions examples/cloudflare-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Cloudflare Worker Example

For this example, we will be using the [Cloudflare Worker](https://workers.cloudflare.com/) platform to upgrade images to WebP.

We can use the latest Wrangler CLI to run the example locally and deploy it to Cloudflare Workers.

## Running the example locally

1. Run `npm install`
2. Run `npm run start` to start the development server

## Usage of jSquash packages in Cloudflare Worker

One caveat is wrangler won't dynamically bundle the WASM modules with the worker.

You will need to ensure you configure the Worker to set these as global variables in the [wrangler.toml](wrangler.toml) file.
```
# wrangler.toml
[wasm_modules]
# Manually specify the path to the WASM module for each codec
JPEG_DEC_WASM = "node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm"
```

The `encode` and `decode` modules both export an `init` function that can be used to manually load the wasm module.

```js
import decode, { init as initJpegDecode } from '@jsquash/jpeg/decode';

const WASM_MODULE = // A WebAssembly.Module object of the compiled wasm binary
await initJpegDecode(WASM_MODULE);
const image = await fetch('./image.jpeg').then(res => res.arrayBuffer()).then(decode);
```
13 changes: 5 additions & 8 deletions examples/cloudflare-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@
"version": "1.0.0",
"main": "worker.js",
"scripts": {
"start": "rollup -c && wrangler dev"
"start": "wrangler dev src/index.js"
},
"dependencies": {
"@jsquash/jpeg": "^1.1.4",
"@jsquash/png": "^2.0.0",
"@jsquash/webp": "^1.1.2"
"@jsquash/jpeg": "file:../../packages/jpeg/dist",
"@jsquash/png": "file:../../packages/png/dist",
"@jsquash/webp": "file:../../packages/webp/dist"
},
"devDependencies": {
"@cloudflare/wrangler": "^1.19.5",
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-replace": "^3.0.0",
"rollup": "^2.60.0"
"wrangler": "^3.1.0"
}
}
16 changes: 0 additions & 16 deletions examples/cloudflare-worker/rollup.config.js

This file was deleted.

33 changes: 21 additions & 12 deletions examples/cloudflare-worker/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,51 @@ import decodeJpeg, { init as initJpegWasm } from '@jsquash/jpeg/decode';
import decodePng, { init as initPngWasm } from '@jsquash/png/decode';
import encodeWebp, { init as initWebpWasm } from '@jsquash/webp/encode';

// Simple Polyfill ImageData Object
globalThis.ImageData = class ImageData {
constructor(data, width, height) {
this.data = data;
this.width = width;
this.height = height;
}
};

const MONTH_IN_SECONDS = 30 * 24 * 60 * 60;
const CDN_CACHE_AGE = 6 * MONTH_IN_SECONDS; // 6 Months

const decodeImage = async (buffer, format) => {
if (format === 'jpeg' || format === 'jpg') {
// @Note, we need to manually initialise the wasm module here
// CF Workers do not support dynamic imports and inject the WASM binary as a global var
initJpegWasm(JPEG_DEC_WASM);
await initJpegWasm(JPEG_DEC_WASM);
return decodeJpeg(buffer);
} else if (format === 'png') {
// @Note, we need to manually initialise the wasm module here
// CF Workers do not support dynamic imports and inject the WASM binary as a global var
initPngWasm(PNG_DEC_WASM);
await initPngWasm(PNG_DEC_WASM);
return decodePng(buffer);
}

throw new Error(`Unsupported format: ${format}`);
}

/**
* This request handler could be used as an Image CDN example. It does the following:
* 1. Check if the image is already cached
* 2. If not, fetch the image from the origin
* 3. If the client supports webp, encode the image to webp
* 4. Cache the image
* 5. Return the image
*/
async function handleRequest(request, ctx) {
const requestUrl = new URL(request.url);
const extension = requestUrl.pathname.split('.').pop();
const isWebpSupported = request.headers.get('accept').includes('image/webp');
const cacheKeyUrl = isWebpSupported ? requestUrl.toString().replace(`.${extension}`, '.webp') : requestUrl.toString();
const cacheKey = new Request(cacheKeyUrl, request);
const cache = caches.default;


const supportedExtensions = ['jpg', 'jpeg', 'png'];
if (!supportedExtensions.includes(extension)) {
return new Response(`<doctype html>
<title>Unsupported image format</title>
<h1>Unsupported image format or missing image path</h1>
<p>Supported formats: ${supportedExtensions.join(', ')}</p>
<p>For this @jSquash Cloudflare Worker example you need to specify the image url as a path, e.g. <a href="/jamie.tokyo/images/compressed/spare-magnets.jpg">https://&lt;worker-url&gt;/jamie.tokyo/images/compressed/spare-magnets.jpg</a></p>
`, { status: 404, headers: { 'Content-Type': 'text/html' } });
}

let response = await cache.match(cacheKey);

if (!response) {
Expand Down
6 changes: 0 additions & 6 deletions examples/cloudflare-worker/wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
name = "upgrade-to-webp"
type = "javascript"
route = ''
zone_id = ''
usage_model = ''
compatibility_flags = []
workers_dev = true
compatibility_date = "2021-11-06"

[wasm_modules]
Expand Down
Loading