Skip to content

Commit 7cd00d4

Browse files
authored
feat: async Transformer class (#9)
1 parent d6cf87a commit 7cd00d4

25 files changed

+1746
-341
lines changed

.cargo/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[target.x86_64-unknown-linux-musl]
2-
rustflags = ["-C", "target-feature=+sse4.2", "-C", "target-feature=-crt-static"]
2+
rustflags = ["-C", "target-feature=-crt-static"]
33
[target.aarch64-unknown-linux-gnu]
44
linker = "aarch64-linux-gnu-gcc"
55
[target.aarch64-unknown-linux-musl]

.github/workflows/CI.yml

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ jobs:
3232
build: |
3333
yarn build -- --features with_simd
3434
target: x86_64-pc-windows-msvc
35-
- host: windows-latest
35+
- host: windows-2022-xl
3636
build: |
37-
yarn build -- --features with_simd
37+
$Env:PATH="C:\Cargo\bin;$Env:PATH"
38+
$Env:LIBAVIF_CROSS_WIN32=1
39+
yarn build -- --features with_simd --target i686-pc-windows-msvc
40+
node -e "console.log(process.arch)"
3841
yarn test
3942
target: i686-pc-windows-msvc
4043
- host: ubuntu-latest
@@ -43,6 +46,7 @@ jobs:
4346
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
4447
build: |
4548
set -e && \
49+
apt install libaom-dev -y && \
4650
yarn build -- --features with_simd --target x86_64-unknown-linux-gnu && \
4751
strip packages/*/*.node
4852
- host: ubuntu-latest
@@ -52,7 +56,11 @@ jobs:
5256
set -e &&
5357
unset RUSTFLAGS &&
5458
unset CC &&
55-
apk add --update --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing --no-cache nasm &&
59+
unset CXX &&
60+
apk add --update --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing --no-cache perl nasm aom-dev &&
61+
export LIB_AOM_STATIC_LIB_PATH=/usr/lib &&
62+
export LIB_AOM_INCLUDE_PATH=/usr/include/aom/aom &&
63+
export LIB_AOM_PKG_CONFIG_PATH=/usr/lib/pkgconfig &&
5664
yarn build -- --features with_simd &&
5765
strip packages/*/*.node
5866
- host: macos-latest
@@ -96,6 +104,7 @@ jobs:
96104
export CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
97105
export CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang++"
98106
export PATH="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin:${PATH}"
107+
export C_INCLUDE_PATH="${ANDROID_NDK_HOME}/sources/android/cpufeatures";
99108
yarn build -- --features oxipng_libdeflater
100109
${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/arm-linux-androideabi-strip packages/*/*.node
101110
- host: ubuntu-latest
@@ -105,6 +114,10 @@ jobs:
105114
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
106115
build: >-
107116
set -e &&
117+
unset RUSTFLAGS &&
118+
unset CC &&
119+
unset CXX &&
120+
apk add --update --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing --no-cache aom-dev perl &&
108121
yarn build -- --target aarch64-unknown-linux-musl --features with_simd &&
109122
/aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip packages/*/*.node
110123
name: stable - ${{ matrix.settings.target }} - node@16
@@ -121,9 +134,10 @@ jobs:
121134
cache: yarn
122135
- name: Setup nasm
123136
uses: ilammy/setup-nasm@v1
124-
if: matrix.settings.target == 'x86_64-pc-windows-msvc' || matrix.settings.target == 'x86_64-apple-darwin'
137+
if: matrix.settings.target == 'x86_64-pc-windows-msvc' || matrix.settings.target == 'x86_64-apple-darwin' || matrix.settings.target == 'i686-pc-windows-msvc'
125138
- name: Install
126139
uses: actions-rs/toolchain@v1
140+
if: matrix.settings.target != 'i686-pc-windows-msvc'
127141
with:
128142
profile: minimal
129143
override: true
@@ -172,8 +186,12 @@ jobs:
172186
run: ${{ matrix.settings.build }}
173187
- name: Build
174188
run: ${{ matrix.settings.build }}
175-
if: ${{ !matrix.settings.docker }}
189+
if: ${{ !matrix.settings.docker && matrix.settings.target != 'i686-pc-windows-msvc' }}
176190
shell: bash
191+
- name: Build
192+
run: ${{ matrix.settings.build }}
193+
if: matrix.settings.target == 'i686-pc-windows-msvc'
194+
shell: powershell
177195
- name: Upload artifact
178196
uses: actions/upload-artifact@v2
179197
with:
@@ -198,7 +216,7 @@ jobs:
198216
usesh: true
199217
mem: 3000
200218
prepare: |
201-
pkg install -y curl nasm node14 python2
219+
pkg install -y curl cmake nasm node14 python2 perl5
202220
curl -qL https://www.npmjs.com/install.sh | sh
203221
npm install -g yarn
204222
curl https://sh.rustup.rs -sSf --output rustup.sh

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ target
22
node_modules
33
*.node
44
Cargo.lock
5+
*.log
56
.swc
67
.DS_Store
78
.pnp.*
@@ -16,3 +17,4 @@ Cargo.lock
1617
optimized*
1718
lib
1819
dist
20+
output-exif.*

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ target
22
node_modules
33
lib
44
.yarn
5+
index.js
6+
index.d.js

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@
22
members = ["./packages/binding"]
33

44
[profile.release]
5-
codegen-units = 1
65
lto = true

bench/bench.mjs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { promises as fs } from 'fs'
2+
import { cpus } from 'os'
3+
import { hrtime } from 'process'
4+
5+
import { from, timer, lastValueFrom, Subject } from 'rxjs'
6+
import { mergeMap, takeUntil } from 'rxjs/operators'
7+
import sharp from 'sharp'
8+
9+
import { ChromaSubsampling, Transformer } from '@napi-rs/image'
10+
11+
// https://github.com/ianare/exif-samples/blob/master/jpg/orientation/portrait_5.jpg
12+
const WITH_EXIF = await fs.readFile('./with-exif.jpg')
13+
14+
const CPU_LENGTH = cpus().length
15+
16+
const DEFAULT_TOTAL_ITERATIONS = 10000
17+
const DEFAULT_MAX_DURATION = 20000
18+
19+
function bench(name, options = {}) {
20+
const suites = []
21+
return {
22+
add(suiteName, suiteFn) {
23+
suites.push({
24+
name: suiteName,
25+
fn: suiteFn,
26+
})
27+
return this
28+
},
29+
run: async () => {
30+
let fastest = {
31+
perf: -1,
32+
name: '',
33+
}
34+
for (const { suiteName, fn: suiteFn } of suites) {
35+
try {
36+
await suiteFn()
37+
} catch (e) {
38+
console.error(`Warming up ${suiteName} failed`)
39+
throw e
40+
}
41+
}
42+
for (const { name: suiteName, fn: suiteFn } of suites) {
43+
const iterations = options.iterations ?? DEFAULT_TOTAL_ITERATIONS
44+
const parallel = options.parallel ?? CPU_LENGTH
45+
const maxDuration = options.maxDuration ?? DEFAULT_MAX_DURATION
46+
const start = hrtime.bigint()
47+
let totalIterations = 0
48+
let finishedIterations = 0
49+
const finish$ = new Subject()
50+
await lastValueFrom(
51+
from({ length: iterations }).pipe(
52+
mergeMap(async () => {
53+
totalIterations++
54+
await suiteFn()
55+
finishedIterations++
56+
if (finishedIterations === totalIterations) {
57+
finish$.next()
58+
finish$.complete()
59+
}
60+
}, parallel),
61+
takeUntil(timer(maxDuration)),
62+
),
63+
)
64+
await lastValueFrom(finish$)
65+
const duration = Number(hrtime.bigint() - start)
66+
const currentPerf = totalIterations / duration
67+
if (currentPerf > fastest.perf) {
68+
fastest = {
69+
perf: currentPerf,
70+
name: suiteName,
71+
}
72+
}
73+
console.info(`${suiteName} ${Math.round(currentPerf * 1e9)} ops/s`)
74+
}
75+
console.info(`In ${name} suite, fastest is ${fastest.name}`)
76+
},
77+
}
78+
}
79+
80+
await bench('webp')
81+
.add('@napi-rs/image', () =>
82+
new Transformer(WITH_EXIF)
83+
.rotate()
84+
.resize(450 / 2)
85+
.webp(75),
86+
)
87+
.add('sharp', () =>
88+
sharp(WITH_EXIF)
89+
.rotate()
90+
.resize(450 / 2)
91+
.webp({ quality: 75 })
92+
.toBuffer(),
93+
)
94+
.run()
95+
96+
bench('avif')
97+
.add('@napi-rs/image', () =>
98+
new Transformer(WITH_EXIF)
99+
.rotate()
100+
.resize(450 / 2)
101+
.avif({ quality: 70, chromaSubsampling: ChromaSubsampling.Yuv420 }),
102+
)
103+
.add('sharp', () =>
104+
sharp(WITH_EXIF)
105+
.rotate()
106+
.resize(450 / 2)
107+
.avif({ quality: 70, chromaSubsampling: '4:2:0' })
108+
.toBuffer(),
109+
)
110+
.run()

example.mjs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
import { readFileSync, writeFileSync } from 'fs'
22

3-
import {
4-
losslessCompressPng,
5-
compressJpeg,
6-
pngQuantize,
7-
losslessEncodeWebp,
8-
encodeAvif,
9-
encodeWebp,
10-
} from '@napi-rs/image'
3+
import { losslessCompressPngSync, compressJpegSync, pngQuantizeSync, Transformer } from '@napi-rs/image'
114

125
const PNG = readFileSync('./un-optimized.png')
136
const JPEG = readFileSync('./un-optimized.jpg')
7+
// https://github.com/ianare/exif-samples/blob/master/jpg/orientation/portrait_5.jpg
8+
const WITH_EXIF = readFileSync('./with-exif.jpg')
149

15-
writeFileSync('optimized-lossless.png', losslessCompressPng(PNG))
10+
writeFileSync('optimized-lossless.png', losslessCompressPngSync(PNG))
1611

17-
writeFileSync('optimized-lossy.png', pngQuantize(PNG))
12+
writeFileSync('optimized-lossy.png', pngQuantizeSync(PNG))
1813

19-
writeFileSync('optimized-lossless.jpg', compressJpeg(readFileSync('./un-optimized.jpg')))
14+
writeFileSync('optimized-lossless.jpg', compressJpegSync(readFileSync('./un-optimized.jpg')))
2015

21-
writeFileSync('optimized-lossless.webp', losslessEncodeWebp(PNG))
16+
writeFileSync('optimized-lossless.webp', new Transformer(PNG).webpLosslessSync())
2217

23-
writeFileSync('optimized-lossy-jpeg.webp', encodeWebp(JPEG, 90))
18+
writeFileSync('optimized-lossy-jpeg.webp', new Transformer(JPEG).webpSync(90))
2419

25-
writeFileSync('optimized-lossy.webp', encodeWebp(PNG, 90))
20+
writeFileSync('optimized-lossy.webp', new Transformer(PNG).webpSync(90))
2621

27-
writeFileSync('optimized.avif', encodeAvif(PNG))
22+
writeFileSync('optimized.avif', new Transformer(PNG).avifSync())
23+
24+
writeFileSync(
25+
'output-exif.webp',
26+
await new Transformer(WITH_EXIF)
27+
.rotate()
28+
.resize(450 / 2)
29+
.webp(75),
30+
)

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
"packages/*"
1212
],
1313
"devDependencies": {
14-
"@napi-rs/cli": "^2.5.0",
14+
"@napi-rs/cli": "^2.6.2",
1515
"@taplo/cli": "^0.3.2",
1616
"@types/node": "^17.0.23",
17+
"@types/sharp": "^0.30.1",
1718
"ava": "^4.1.0",
1819
"lerna": "^4.0.0",
1920
"npm-run-all": "^4.1.5",
20-
"prettier": "^2.6.1",
21+
"prettier": "^2.6.2",
22+
"rxjs": "^7.5.5",
23+
"sharp": "^0.30.3",
2124
"typescript": "^4.6.3"
2225
},
2326
"scripts": {
@@ -47,7 +50,7 @@
4750
}
4851
},
4952
"lint-staged": {
50-
"*.@(js||ts|json|md|yml|yaml)": [
53+
"*.@(js|ts|json|md|yml|yaml)": [
5154
"prettier --write"
5255
],
5356
"*.toml": [

packages/binding/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@ with_simd = ["mozjpeg-sys/nasm_simd_parallel_build"]
1313

1414
[dependencies]
1515
imagequant = "4.0.0"
16+
image = { version = "0.24" }
1617
infer = "0.7"
1718
jpeg-decoder = "0.2"
19+
libavif = { version = "0.10", git = "https://github.com/Brooooooklyn/libavif-rs", branch = "fix-build", default-features = false, features = [
20+
"codec-aom",
21+
] }
1822
libc = "0.2"
1923
libwebp-sys = { version = "0.5", features = ["avx2", "sse41", "neon"] }
2024
lodepng = "3"
2125
napi = { version = "2", default-features = false, features = ["napi3"] }
2226
napi-derive = { version = "2", default-features = false, features = [
2327
"type-def",
2428
] }
29+
num_cpus = "1"
2530
png = "0.17"
26-
ravif = "0.8"
31+
rexif = "0.7"
2732
rgb = "0.8"
2833

2934
[dependencies.oxipng]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { promises as fs } from 'fs'
2+
import { join } from 'path'
3+
import { fileURLToPath } from 'url'
4+
5+
import test from 'ava'
6+
7+
import { Transformer } from '../index.js'
8+
9+
const ROOT_DIR = join(fileURLToPath(import.meta.url), '..', '..', '..', '..')
10+
11+
const PNG = await fs.readFile(join(ROOT_DIR, 'un-optimized.png'))
12+
const JPEG = await fs.readFile(join(ROOT_DIR, 'un-optimized.jpg'))
13+
const WITH_EXIF_JPG = await fs.readFile(join(ROOT_DIR, 'with-exif.jpg'))
14+
15+
test('should be able to get metadata from png', async (t) => {
16+
const decoder = new Transformer(PNG)
17+
const metadata = await decoder.metadata()
18+
t.is(metadata.width, 1052)
19+
t.is(metadata.height, 744)
20+
})
21+
22+
test('should be able to get metadata from jpg', async (t) => {
23+
const decoder = new Transformer(JPEG)
24+
const metadata = await decoder.metadata()
25+
t.is(metadata.width, 1024)
26+
t.is(metadata.height, 678)
27+
})
28+
29+
test('should be able to get exif from jpg', async (t) => {
30+
const decoder = new Transformer(WITH_EXIF_JPG)
31+
const metadata = await decoder.metadata(true)
32+
t.snapshot(metadata.exif)
33+
t.is(metadata.orientation, 5)
34+
t.is(metadata.format, 'jpeg')
35+
})
36+
37+
test('should be able to encode into webp', async (t) => {
38+
const decoder = new Transformer(PNG)
39+
await t.notThrowsAsync(() => decoder.webp(75))
40+
})

0 commit comments

Comments
 (0)