Skip to content

Commit

Permalink
Merge pull request #71 from jamsinclair/add-qoi-codec
Browse files Browse the repository at this point in the history
Add the "Quite Ok Image Format" codec
  • Loading branch information
jamsinclair authored Nov 2, 2024
2 parents cda239a + 2574536 commit 968368e
Show file tree
Hide file tree
Showing 28 changed files with 683 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jSquash name is inspired by jQuery and Squoosh. It symbolizes the browser suppor
- [@jSquash/jxl](/packages/jxl) - An encoder and decoder for JPEG XL images using the [libjxl](https://github.com/libjxl/libjxl) library
- [@jSquash/oxipng](/packages/oxipng) - A PNG image optimiser using [Oxipng](https://github.com/shssoichiro/oxipng)
- [@jSquash/png](/packages/png) - An encoder and decoder for PNG images using the [rust PNG crate](https://docs.rs/png/0.11.0/png/)
- [@jSquash/qoi](/packages/qoi) - An encoder and decoder for the "Quite Ok Image Format" using the [official library](https://github.com/phoboslab/qoi)
- [@jSquash/resize](/packages/resize) - An image resizer tool using rust [resize](https://github.com/PistonDevelopers/resize), [hqx](https://github.com/CryZe/wasmboy-rs/tree/master/hqx) and [magic-kernel](https://github.com/SevInf/magic-kernel-rust) libraries. Supports both downscaling and upscaling.
- [@jSquash/webp](/packages/webp) - An encoder and decoder for WebP images using [libwebp](https://github.com/webmproject/libwebp)
- ...more to come
Expand Down
6 changes: 6 additions & 0 deletions packages/qoi/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.cpp
Makefile
node_modules
codec/*package.json
*.d.ts.map
tsconfig.tsbuildinfo
7 changes: 7 additions & 0 deletions packages/qoi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## @jsquash/qoi@1.0.0

### Adds

- Initial release of the QOI codec
95 changes: 95 additions & 0 deletions packages/qoi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# @jsquash/qoi

[![npm version](https://badge.fury.io/js/@jsquash%2Fqoi.svg)](https://badge.fury.io/js/@jsquash%2Fqoi)

An easy experience for encoding and decoding the "Quite Ok Image Format" in the browser. Powered by WebAssembly ⚡️.

Uses the [official QOI](https://github.com/phoboslab/qoi) library.

A [jSquash](https://github.com/jamsinclair/jSquash) package. Codecs and supporting code derived from the [Squoosh](https://github.com/GoogleChromeLabs/squoosh) app.

## Installation

```shell
npm install --save @jsquash/qoi
# Or your favourite package manager alternative
```

## Usage

Note: You will need to either manually include the wasm files from the codec directory or use a bundler like WebPack or Rollup to include them in your app/server.

### decode(data: ArrayBuffer): Promise<ImageData>

Decodes QOI binary ArrayBuffer to raw RGB image data.

#### data
Type: `ArrayBuffer`

#### Example
```js
import { decode } from '@jsquash/qoi';

const formEl = document.querySelector('form');
const formData = new FormData(formEl);
// Assuming user selected an input qoi file
const imageData = await decode(await formData.get('image').arrayBuffer());
```

### encode(data: ImageData): Promise<ArrayBuffer>

Encodes raw RGB image data to QOI format and resolves to an ArrayBuffer of binary data.

#### data
Type: `ImageData`

#### Example
```js
import { encode } from '@jsquash/qoi';

async function loadImage(src) {
const img = document.createElement('img');
img.src = src;
await new Promise(resolve => img.onload = resolve);
const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [img.width, img.height];
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}

const rawImageData = await loadImage('/example.png');
const qoiBuffer = await encode(rawImageData);
```

## Manual WASM initialisation (not recommended)

In most situations there is no need to manually initialise the provided WebAssembly modules.
The generated glue code takes care of this and supports most web bundlers.

One situation where this arises is when using the modules in Cloudflare Workers ([See the README for more info](/README.md#usage-in-cloudflare-workers)).

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 initQOIDecode } from '@jsquash/qoi/decode';

initQOIDecode(WASM_MODULE); // The `WASM_MODULE` variable will need to be sourced by yourself and passed as an ArrayBuffer.
const image = await fetch('./image.qoi').then(res => res.arrayBuffer()).then(decode);
```

You can also pass custom options to the `init` function to customise the behaviour of the module. See the [Emscripten documentation](https://emscripten.org/docs/api_reference/module.html#Module) for more information.

```js
import decode, { init as initQOIDecode } from '@jsquash/qoi/decode';

initQOIDecode(null, {
// Customise the path to load the wasm file
locateFile: (path, prefix) => `https://example.com/${prefix}/${path}`,
});
const image = await fetch('./image.qoi').then(res => res.arrayBuffer()).then(decode);
```

## Known Issues

See [jSquash Project README](https://github.com/jamsinclair/jSquash#known-issues)
21 changes: 21 additions & 0 deletions packages/qoi/codec/LICENSE.codec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Dominic Szablewski

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
46 changes: 46 additions & 0 deletions packages/qoi/codec/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
CODEC_URL = https://github.com/phoboslab/qoi/archive/8d35d93cdca85d2868246c2a8a80a1e2c16ba2a8.tar.gz

CODEC_DIR = node_modules/qoi
CODEC_BUILD_DIR:= $(CODEC_DIR)/build
ENVIRONMENT = web,worker

PRE_JS = pre.js
OUT_JS = enc/qoi_enc.js dec/qoi_dec.js
OUT_WASM := $(OUT_JS:.js=.wasm)

.PHONY: all clean

all: $(OUT_JS)

$(filter enc/%,$(OUT_JS)): enc/qoi_enc.o
$(filter dec/%,$(OUT_JS)): dec/qoi_dec.o

# ALL .js FILES
$(OUT_JS):
$(LD) \
$(LDFLAGS) \
--pre-js $(PRE_JS) \
--bind \
-s ENVIRONMENT=$(ENVIRONMENT) \
-s EXPORT_ES6=1 \
-s DYNAMIC_EXECUTION=0 \
-s MODULARIZE=1 \
-o $@ \
$+

# ALL .o FILES
%.o: %.cpp $(CODEC_DIR)
$(CXX) -c \
$(CXXFLAGS) \
-I $(CODEC_DIR) \
-o $@ \
$<

# CREATE DIRECTORY
$(CODEC_DIR):
mkdir -p $(CODEC_DIR)
curl -sL $(CODEC_URL) | tar xz --strip 1 -C $(CODEC_DIR)

clean:
$(RM) $(OUT_JS) $(OUT_WASM)
$(MAKE) -C $(CODEC_DIR) clean
5 changes: 5 additions & 0 deletions packages/qoi/codec/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# QOI

- Source: <https://github.com/phoboslab/qoi>
- Version: N/A
- License: MIT
30 changes: 30 additions & 0 deletions packages/qoi/codec/dec/qoi_dec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>

#define QOI_IMPLEMENTATION
#include "qoi.h"

using namespace emscripten;

thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");

val decode(std::string qoiimage) {
qoi_desc desc;
uint8_t* rgba = (uint8_t*)qoi_decode(qoiimage.c_str(), qoiimage.length(), &desc, 4);

// Resultant width and height stored in descriptor
int decodedWidth = desc.width;
int decodedHeight = desc.height;

val result = ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(4 * decodedWidth * decodedHeight, rgba)),
decodedWidth, decodedHeight);
free(rgba);

return result;
}

EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
}
7 changes: 7 additions & 0 deletions packages/qoi/codec/dec/qoi_dec.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface QOIModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null;
}

declare var moduleFactory: EmscriptenWasm.ModuleFactory<QOIModule>;

export default moduleFactory;
15 changes: 15 additions & 0 deletions packages/qoi/codec/dec/qoi_dec.js

Large diffs are not rendered by default.

Binary file added packages/qoi/codec/dec/qoi_dec.wasm
Binary file not shown.
32 changes: 32 additions & 0 deletions packages/qoi/codec/enc/qoi_enc.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>

#define QOI_IMPLEMENTATION
#include "qoi.h"

using namespace emscripten;

thread_local const val Uint8Array = val::global("Uint8Array");

val encode(std::string buffer, int width, int height) {
int compressedSizeInBytes;
qoi_desc desc;
desc.width = width;
desc.height = height;
desc.channels = 4;
desc.colorspace = QOI_SRGB;

uint8_t* encodedData = (uint8_t*)qoi_encode(buffer.c_str(), &desc, &compressedSizeInBytes);
if (encodedData == NULL)
return val::null();

auto js_result =
Uint8Array.new_(typed_memory_view(compressedSizeInBytes, (const uint8_t*)encodedData));
free(encodedData);

return js_result;
}

EMSCRIPTEN_BINDINGS(my_module) {
function("encode", &encode);
}
11 changes: 11 additions & 0 deletions packages/qoi/codec/enc/qoi_enc.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface QOIModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number
): Uint8Array;
}

declare var moduleFactory: EmscriptenWasm.ModuleFactory<QOIModule>;

export default moduleFactory;
15 changes: 15 additions & 0 deletions packages/qoi/codec/enc/qoi_enc.js

Large diffs are not rendered by default.

Binary file added packages/qoi/codec/enc/qoi_enc.wasm
Binary file not shown.
6 changes: 6 additions & 0 deletions packages/qoi/codec/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"scripts": {
"build": "EMSDK_VERSION=3.1.57 ../../../tools/build-cpp.sh"
},
"type": "module"
}
24 changes: 24 additions & 0 deletions packages/qoi/codec/pre.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const isServiceWorker = globalThis.ServiceWorkerGlobalScope !== undefined;
const isRunningInCloudFlareWorkers = isServiceWorker && typeof self !== 'undefined' && globalThis.caches && globalThis.caches.default !== undefined;
const isRunningInNode = typeof process === 'object' && process.release && process.release.name === 'node';

if (isRunningInCloudFlareWorkers || isRunningInNode) {
if (!globalThis.ImageData) {
// Simple Polyfill for ImageData Object
globalThis.ImageData = class ImageData {
constructor(data, width, height) {
this.data = data;
this.width = width;
this.height = height;
}
};
}

if (import.meta.url === undefined) {
import.meta.url = 'https://localhost';
}

if (typeof self !== 'undefined' && self.location === undefined) {
self.location = { href: '' };
}
}
44 changes: 44 additions & 0 deletions packages/qoi/decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Notice: I (Jamie Sinclair) have copied this code from the original and modified
* to align with the jSquash project structure.
*/

import type { QOIModule } from './codec/dec/qoi_dec.js';
import { initEmscriptenModule } from './utils.js';

import qoi_dec from './codec/dec/qoi_dec.js';

let emscriptenModule: Promise<QOIModule>;

export async function init(
module?: WebAssembly.Module,
moduleOptionOverrides?: Partial<EmscriptenWasm.ModuleOpts>,
): Promise<void> {
emscriptenModule = initEmscriptenModule(
qoi_dec,
module,
moduleOptionOverrides,
);
}

export default async function decode(buffer: ArrayBuffer): Promise<ImageData> {
if (!emscriptenModule) await init();

const module = await emscriptenModule;
const result = module.decode(buffer);
if (!result) throw new Error('Decoding error');
return result;
}
Loading

0 comments on commit 968368e

Please sign in to comment.