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

feat: add wasm build #81

Merged
merged 9 commits into from
Feb 14, 2024
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
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ rustflags = ["-C", "target-feature=+crc"]
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
rustflags = ["-C", "target-feature=-crt-static"]
[target.wasm32-wasi-preview1-threads]
rustflags = ["-C", "target-feature=+simd128"]
65 changes: 46 additions & 19 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,11 @@ jobs:
export LIB_AOM_PKG_CONFIG_PATH=/usr/lib/pkgconfig &&
yarn workspace @napi-rs/image build --target x86_64-unknown-linux-musl --features with_simd &&
chmod -R 777 target
- host: macos-14
- host: macos-latest
target: aarch64-apple-darwin
setup: |
brew install meson llvm
brew install meson
build: |
export cc=clang
export cxx=clang++
MACOSX_DEPLOYMENT_TARGET=11.0 yarn workspace @napi-rs/image build --target aarch64-apple-darwin --features with_simd
- host: ubuntu-latest
target: aarch64-unknown-linux-gnu
Expand All @@ -110,9 +108,8 @@ jobs:
target: armv7-unknown-linux-gnueabihf
setup: |
sudo apt-get update
sudo apt-get install meson gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
build: |
yarn workspace @napi-rs/image build --target armv7-unknown-linux-gnueabihf --features oxipng_libdeflater --zig --zig-link-only
sudo apt-get install meson gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
build: CC="arm-linux-gnueabihf-gcc" yarn workspace @napi-rs/image build --target armv7-unknown-linux-gnueabihf --features oxipng_libdeflater --use-napi-cross
- host: ubuntu-latest
target: aarch64-linux-android
build: |
Expand All @@ -121,7 +118,6 @@ jobs:
yarn workspace @napi-rs/image build --target aarch64-linux-android --features with_simd
- host: ubuntu-latest
target: aarch64-unknown-linux-musl
downloadTarget: aarch64-unknown-linux-musl
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
build: >-
set -e &&
Expand All @@ -131,6 +127,15 @@ jobs:
apk add --update --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing --no-cache aom-dev perl meson &&
yarn workspace @napi-rs/image build --target aarch64-unknown-linux-musl --features with_simd &&
chmod -R 777 target
- host: macos-14
target: wasm32-wasi-preview1-threads
setup: |
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0-macos.tar.gz
tar -xvf wasi-sdk-21.0-macos.tar.gz
build: |
export WASI_SDK_PATH="$(pwd)/wasi-sdk-21.0"
yarn workspace @napi-rs/image build --target wasm32-wasi-preview1-threads

name: stable - ${{ matrix.settings.target }} - node@20
runs-on: ${{ matrix.settings.host }}
env:
Expand Down Expand Up @@ -159,10 +164,11 @@ jobs:
~/.cargo/git/db/
.cargo-cache
.xwin
~/.napi-rs
target/
key: ${{ matrix.settings.target }}-cargo-cache
- uses: goto-bus-stop/setup-zig@v2
if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }}
if: ${{ contains(matrix.settings.target, 'musl') }}
with:
version: 0.11.0
- name: Setup toolchain
Expand All @@ -186,7 +192,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.settings.target }}
path: packages/*/*.node
path: |
packages/*/*.node
packages/*/*.wasm
if-no-files-found: error
build-freebsd:
runs-on: macos-12
Expand Down Expand Up @@ -366,7 +374,6 @@ jobs:
fail-fast: false
matrix:
node:
- '18'
- '20'
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -394,10 +401,7 @@ jobs:
with:
image: node:${{ matrix.node }}-slim
options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build'
run: |
set -e
yarn test
ls -la
run: yarn test
test-linux-arm-gnueabihf-binding:
name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }}
needs:
Expand Down Expand Up @@ -434,10 +438,32 @@ jobs:
with:
image: node:${{ matrix.node }}-bullseye-slim
options: '--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build'
run: |
set -e
yarn test
ls -la
run: yarn test
test-wasi-on-nodejs:
name: Test wasi on Node.js
runs-on: macos-14
needs:
- build
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: bindings-wasm32-wasi-preview1-threads
path: artifacts
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: Move artifacts
run: yarn artifacts
shell: bash
- name: List packages
run: ls -R packages
shell: bash
- name: Run tests
run: yarn test packages/binding/__test__/transformer.spec.mjs -s
env:
NAPI_RS_FORCE_WASI: '1'

publish:
name: Publish
runs-on: ubuntu-latest
Expand All @@ -448,6 +474,7 @@ jobs:
- test-linux-x64-musl-binding
- test-linux-aarch64-gnu-binding
- test-linux-arm-gnueabihf-binding
- test-wasi-on-nodejs
steps:
- uses: actions/checkout@v4
- name: Setup node
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ output-exif.*
nasa-small.*
output-overlay-png.png
output-debian.jpeg
*.wasm
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ resolver = "2"
[profile.release]
lto = 'fat'
codegen-units = 1
strip = 'symbols'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"website"
],
"devDependencies": {
"@napi-rs/cli": "^2.18.0",
"@napi-rs/cli": "^3.0.0-alpha.38",
"@taplo/cli": "^0.7.0",
"@types/node": "^20.11.16",
"@types/sharp": "^0.31.1",
Expand Down
13 changes: 10 additions & 3 deletions packages/binding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ image = { version = "0.24", default-features = false, features = [
"openexr",
] }
jpeg-decoder = "0.3"
libavif = { version = "0.12", default-features = false, features = [
"codec-aom",
] }
libc = "0.2"
lodepng = "3"
napi = { version = "2", default-features = false, features = ["napi3"] }
Expand Down Expand Up @@ -67,5 +64,15 @@ libwebp-sys = { version = "0.9", default-features = false, features = ["std", "p
[target.'cfg(all(target_os = "macos", target_arch = "x86_64"))'.dependencies]
libwebp-sys = { version = "0.9", default-features = false, features = ["std", "parallel"] }

[target.'cfg(target_family = "wasm")'.dependencies]
libavif = { git="https://github.com/njaard/libavif-rs.git", rev = "678bd68", version = "0.12", default-features = false, features = [
"codec-rav1e",
] }

[target.'cfg(not(target_family = "wasm"))'.dependencies]
libavif = { git="https://github.com/njaard/libavif-rs.git", rev = "678bd68", version = "0.12", default-features = false, features = [
"codec-aom",
] }

[build-dependencies]
napi-build = "2"
12 changes: 8 additions & 4 deletions packages/binding/__test__/optimize.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { promises as fs } from 'fs'
import { join } from 'path'
import { fileURLToPath } from 'url'
import { promises as fs } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'

import test from 'ava'

import { losslessCompressPng, pngQuantize, compressJpeg, Transformer } from '../index.js'

const ROOT_DIR = join(fileURLToPath(import.meta.url), '..', '..', '..', '..')

const PNG = await fs.readFile(join(ROOT_DIR, 'un-optimized.png'))
const PNG = await fs.readFile(join(ROOT_DIR, 'nasa-4928x3279.png'))
const JPEG = await fs.readFile(join(ROOT_DIR, 'un-optimized.jpg'))

test('should be able to lossless optimize png image', async (t) => {
if (process.env.NAPI_RS_FORCE_WASI) {
t.pass()
return
}
const dest = await losslessCompressPng(PNG)
t.true(dest.length < PNG.length)
})
Expand Down
16 changes: 10 additions & 6 deletions packages/binding/__test__/transformer.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { promises as fs } from 'fs'
import { join } from 'path'
import { fileURLToPath } from 'url'
import { promises as fs } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'

import test from 'ava'
import { decode } from 'blurhash'
Expand Down Expand Up @@ -43,6 +43,10 @@ test('should be able to encode into webp', async (t) => {
})

test('should be able to decode from avif', async (t) => {
if (process.env.NAPI_RS_FORCE_WASI) {
t.pass()
return
}
const decoder = new Transformer(PNG)
const AVIF = await decoder.avif()
const avifDecoder = new Transformer(AVIF)
Expand All @@ -51,16 +55,16 @@ test('should be able to decode from avif', async (t) => {

test('should be able to decode from webp', async (t) => {
const decoder = new Transformer(PNG)
const WEBP = await decoder.webpLossless()
const WEBP = await decoder.webp()
const webpDecoder = new Transformer(WEBP)
await t.notThrowsAsync(() => webpDecoder.png())
})

test('should be able to create transformer from raw rgba pixels', async (t) => {
const pixels = decode('LEHV6nWB2yk8pyo0adR*.7kCMdnj', 32, 32)
await t.notThrowsAsync(() => Transformer.fromRgbaPixels(pixels, 32, 32).webpLossless())
await t.notThrowsAsync(() => Transformer.fromRgbaPixels(pixels, 32, 32).webp())
})

test('should be able to create transformer from SVG', async (t) => {
await t.notThrowsAsync(() => Transformer.fromSvg(SVG).png())
await t.notThrowsAsync(() => Transformer.fromSvg(SVG).jpeg())
})
1 change: 1 addition & 0 deletions packages/binding/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@napi-rs/image-wasm32-wasi'
105 changes: 105 additions & 0 deletions packages/binding/image.wasi-browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync,
getDefaultContext as __emnapiGetDefaultContext,
WASI as __WASI,
} from '@napi-rs/wasm-runtime'
import { Volume as __Volume, createFsFromVolume as __createFsFromVolume } from '@napi-rs/wasm-runtime/fs'

import __wasmUrl from './image.wasm32-wasi.wasm?url'

const __fs = __createFsFromVolume(
__Volume.fromJSON({
'/': null,
}),
)

const __wasi = new __WASI({
version: 'preview1',
fs: __fs,
})

const __emnapiContext = __emnapiGetDefaultContext()

const __sharedMemory = new WebAssembly.Memory({
initial: 1024,
maximum: 10240,
shared: true,
})

const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())

const {
instance: __napiInstance,
module: __wasiModule,
napiModule: __napiModule,
} = __emnapiInstantiateNapiModuleSync(__wasmFile, {
context: __emnapiContext,
asyncWorkPoolSize: 4,
wasi: __wasi,
onCreateWorker() {
return new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
type: 'module',
})
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
__napi_rs_initialize_modules(instance)
},
})

function __napi_rs_initialize_modules(__napiInstance) {
__napiInstance.exports['__napi_register__AvifConfig_struct_0']?.()
__napiInstance.exports['__napi_register__ChromaSubsampling_1']?.()
__napiInstance.exports['__napi_register__FastResizeFilter_2']?.()
__napiInstance.exports['__napi_register__ResizeFit_3']?.()
__napiInstance.exports['__napi_register__FastResizeOptions_struct_4']?.()
__napiInstance.exports['__napi_register__JpegCompressOptions_struct_5']?.()
__napiInstance.exports['__napi_register__compress_jpeg_sync_6']?.()
__napiInstance.exports['__napi_register__CompressJpegTask_impl_7']?.()
__napiInstance.exports['__napi_register__compress_jpeg_8']?.()
__napiInstance.exports['__napi_register__CompressionType_9']?.()
__napiInstance.exports['__napi_register__FilterType_10']?.()
__napiInstance.exports['__napi_register__PngEncodeOptions_struct_11']?.()
__napiInstance.exports['__napi_register__PngRowFilter_12']?.()
__napiInstance.exports['__napi_register__PNGLosslessOptions_struct_13']?.()
__napiInstance.exports['__napi_register__lossless_compress_png_sync_14']?.()
__napiInstance.exports['__napi_register__LosslessPngTask_impl_15']?.()
__napiInstance.exports['__napi_register__lossless_compress_png_16']?.()
__napiInstance.exports['__napi_register__PngQuantOptions_struct_17']?.()
__napiInstance.exports['__napi_register__png_quantize_sync_18']?.()
__napiInstance.exports['__napi_register__PngQuantTask_impl_19']?.()
__napiInstance.exports['__napi_register__png_quantize_20']?.()
__napiInstance.exports['__napi_register__Orientation_21']?.()
__napiInstance.exports['__napi_register__ResizeFilterType_22']?.()
__napiInstance.exports['__napi_register__JsColorType_23']?.()
__napiInstance.exports['__napi_register__Metadata_struct_24']?.()
__napiInstance.exports['__napi_register__MetadataTask_impl_25']?.()
__napiInstance.exports['__napi_register__ResizeOptions_struct_26']?.()
__napiInstance.exports['__napi_register__EncodeTask_impl_27']?.()
__napiInstance.exports['__napi_register__Transformer_struct_28']?.()
__napiInstance.exports['__napi_register__Transformer_impl_70']?.()
}
export const Transformer = __napiModule.exports.Transformer
export const ChromaSubsampling = __napiModule.exports.ChromaSubsampling
export const CompressionType = __napiModule.exports.CompressionType
export const compressJpeg = __napiModule.exports.compressJpeg
export const compressJpegSync = __napiModule.exports.compressJpegSync
export const FastResizeFilter = __napiModule.exports.FastResizeFilter
export const FilterType = __napiModule.exports.FilterType
export const JsColorType = __napiModule.exports.JsColorType
export const losslessCompressPng = __napiModule.exports.losslessCompressPng
export const losslessCompressPngSync = __napiModule.exports.losslessCompressPngSync
export const Orientation = __napiModule.exports.Orientation
export const pngQuantize = __napiModule.exports.pngQuantize
export const pngQuantizeSync = __napiModule.exports.pngQuantizeSync
export const PngRowFilter = __napiModule.exports.PngRowFilter
export const ResizeFilterType = __napiModule.exports.ResizeFilterType
export const ResizeFit = __napiModule.exports.ResizeFit
Loading