Skip to content

Commit 713ca40

Browse files
authored
fix: image decode should be async (#872)
* fix: image decode should be async * set bitmap immutable
1 parent 1666a33 commit 713ca40

File tree

13 files changed

+346
-133
lines changed

13 files changed

+346
-133
lines changed

.cargo/config.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,5 @@ rustflags = ["-C", "target-cpu=cortex-a57"]
1111
rustflags = ["-C", "target-cpu=cortex-a7"]
1212

1313
[target.aarch64-unknown-linux-musl]
14-
linker = "aarch64-linux-musl-gcc"
15-
rustflags = [
16-
"-C",
17-
"target-cpu=cortex-a57",
18-
]
14+
linker = "aarch64-linux-musl-gcc"
15+
rustflags = ["-C", "target-cpu=cortex-a57"]

.taplo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ exclude = ["node_modules/**/*.toml", "skia/**/*.toml"]
55
align_entries = true
66
indent_tables = true
77
reorder_keys = true
8+
column_width = 180

Cargo.toml

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,23 @@ version = "0.1.0"
88
crate-type = ["cdylib"]
99

1010
[dependencies]
11-
anyhow = "1"
12-
base64 = "0.22"
13-
cssparser = "0.29"
14-
infer = "0.16"
15-
libavif = { version = "0.14", default-features = false, features = [
16-
"codec-aom",
17-
] }
18-
napi = { version = "2", default-features = false, features = [
19-
"napi3",
20-
"serde-json",
21-
] }
22-
napi-derive = { version = "2", default-features = false }
23-
nom = "7"
24-
num_cpus = "1"
25-
once_cell = "1"
26-
regex = "1"
27-
rgb = "0.8"
28-
serde = "1"
11+
anyhow = "1"
12+
base64 = "0.22"
13+
base64-simd = "0.8"
14+
cssparser = "0.29"
15+
infer = "0.16"
16+
libavif = { version = "0.14", default-features = false, features = ["codec-aom"] }
17+
napi = { version = "3.0.0-alpha.8", default-features = false, features = ["napi3", "serde-json"] }
18+
napi-derive = { version = "3.0.0-alpha.7", default-features = false }
19+
nom = "7"
20+
num_cpus = "1"
21+
once_cell = "1"
22+
regex = "1"
23+
rgb = "0.8"
24+
serde = "1"
2925
serde_derive = "1"
30-
serde_json = "1"
31-
thiserror = "1"
26+
serde_json = "1"
27+
thiserror = "1"
3228

3329
[target.'cfg(not(target_os = "linux"))'.dependencies]
3430
mimalloc = "0.1"
@@ -41,6 +37,6 @@ cc = "1"
4137
napi-build = "2"
4238

4339
[profile.release]
44-
lto = true
4540
codegen-units = 1
46-
strip = "symbols"
41+
lto = true
42+
strip = "symbols"

__test__/draw.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,12 @@ test('createPattern-no-transform', async (t) => {
240240
const { ctx } = t.context
241241
const imageSrc = await promises.readFile(join(__dirname, 'canvas_createpattern.png'))
242242
const image = new Image()
243+
const { promise, resolve } = Promise.withResolvers<void>()
244+
image.onload = () => {
245+
resolve()
246+
}
243247
image.src = imageSrc
248+
await promise
244249
const pattern = ctx.createPattern(image, 'repeat')
245250
ctx.fillStyle = pattern
246251
ctx.fillRect(0, 0, 300, 300)
@@ -262,7 +267,12 @@ test('createPattern-with-transform', async (t) => {
262267
const { ctx } = t.context
263268
const imageSrc = await promises.readFile(join(__dirname, 'canvas_createpattern.png'))
264269
const image = new Image()
270+
const { promise, resolve } = Promise.withResolvers<void>()
271+
image.onload = () => {
272+
resolve()
273+
}
265274
image.src = imageSrc
275+
await promise
266276
const pattern = ctx.createPattern(image, 'repeat')
267277
const matrix = new DOMMatrix()
268278
pattern.setTransform(matrix.rotate(-45).scale(1.5))
@@ -308,7 +318,12 @@ test('drawImage', async (t) => {
308318
const filePath = './javascript.png'
309319
const file = await promises.readFile(join(__dirname, filePath))
310320
const image = new Image()
321+
const { promise, resolve } = Promise.withResolvers<void>()
322+
image.onload = () => {
323+
resolve()
324+
}
311325
image.src = file
326+
await promise
312327
ctx.drawImage(image, 0, 0)
313328
await snapshotImage(t)
314329
})
@@ -318,7 +333,12 @@ test('drawImage-svg', async (t) => {
318333
const filePath = './mountain.svg'
319334
const file = await promises.readFile(join(__dirname, filePath))
320335
const image = new Image()
336+
const { promise, resolve } = Promise.withResolvers<void>()
337+
image.onload = () => {
338+
resolve()
339+
}
321340
image.src = file
341+
await promise
322342
ctx.drawImage(image, 0, 0)
323343
await snapshotImage(t)
324344
})
@@ -328,7 +348,12 @@ test('drawImage-svg-with-only-viewBox', async (t) => {
328348
const filePath = './only-viewbox.svg'
329349
const file = await promises.readFile(join(__dirname, filePath))
330350
const image = new Image()
351+
const { promise, resolve } = Promise.withResolvers<void>()
352+
image.onload = () => {
353+
resolve()
354+
}
331355
image.src = file
356+
await promise
332357
ctx.drawImage(image, 0, 0)
333358
await snapshotImage(t)
334359
})
@@ -338,7 +363,12 @@ test('drawImage-svg-resize', async (t) => {
338363
const filePath = './resize.svg'
339364
const file = await promises.readFile(join(__dirname, filePath))
340365
const image = new Image()
366+
const { promise, resolve } = Promise.withResolvers<void>()
367+
image.onload = () => {
368+
resolve()
369+
}
341370
image.src = file
371+
await promise
342372
image.width = 100
343373
image.height = 100
344374
ctx.drawImage(image, 0, 0)
@@ -360,7 +390,15 @@ test('drawImage-svg without width height should be empty image', async (t) => {
360390
const filePath = './mountain.svg'
361391
const svgContent = (await promises.readFile(join(__dirname, filePath))).toString('utf-8')
362392
const image = new Image()
393+
const { promise, resolve, reject } = Promise.withResolvers<void>()
394+
image.onload = () => {
395+
resolve()
396+
}
397+
image.onerror = (err) => {
398+
reject(err)
399+
}
363400
image.src = Buffer.from(svgContent.replace('width="128"', '').replace('height="128"', ''))
401+
await promise
364402
ctx.drawImage(image, 0, 0)
365403
const output = await canvas.encode('png')
366404
const outputData = png.decoders['image/png'](output)
@@ -372,7 +410,12 @@ test('draw-image-svg-noto-emoji', async (t) => {
372410
const filePath = './notoemoji-person.svg'
373411
const file = await promises.readFile(join(__dirname, filePath))
374412
const image = new Image()
413+
const { promise, resolve } = Promise.withResolvers<void>()
414+
image.onload = () => {
415+
resolve()
416+
}
375417
image.src = file
418+
await promise
376419
ctx.drawImage(image, 0, 0)
377420
await snapshotImage(t)
378421
})

__test__/filter.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ const test = ava as TestFn<{
1414

1515
const FIREFOX = readFileSync(join(__dirname, 'fixtures', 'firefox-logo.svg'))
1616
const FIREFOX_IMAGE = new Image(200, 206.433)
17+
const { promise: firefoxImageLoad, resolve } = Promise.withResolvers<void>()
18+
FIREFOX_IMAGE.onload = () => {
19+
resolve()
20+
}
1721
FIREFOX_IMAGE.src = FIREFOX
1822

19-
test.beforeEach((t) => {
23+
test.beforeEach(async (t) => {
2024
const canvas = createCanvas(300, 300)
2125
t.context.canvas = canvas
2226
t.context.ctx = canvas.getContext('2d')
27+
await firefoxImageLoad
2328
})
2429

2530
test('filter-blur', async (t) => {
@@ -33,7 +38,12 @@ test('filter-brightness', async (t) => {
3338
const { ctx } = t.context
3439
ctx.filter = 'brightness(2)'
3540
const image = new Image()
41+
const { promise, resolve } = Promise.withResolvers<void>()
42+
image.onload = () => {
43+
resolve()
44+
}
3645
image.src = await fs.readFile(join(__dirname, 'fixtures', 'filter-brightness.jpg'))
46+
await promise
3747
ctx.drawImage(image, 0, 0)
3848
await snapshotImage(t)
3949
})
@@ -42,7 +52,12 @@ test('filter-contrast', async (t) => {
4252
const { ctx } = t.context
4353
ctx.filter = 'contrast(200%)'
4454
const image = new Image()
55+
const { promise, resolve } = Promise.withResolvers<void>()
56+
image.onload = () => {
57+
resolve()
58+
}
4559
image.src = await fs.readFile(join(__dirname, 'fixtures', 'filter-contrast.jpeg'))
60+
await promise
4661
ctx.drawImage(image, 0, 0)
4762
await snapshotImage(t)
4863
})
@@ -122,6 +137,11 @@ test('filter-save-restore', async (t) => {
122137

123138
async function createImage(name: string) {
124139
const i = new Image()
140+
const { promise, resolve } = Promise.withResolvers<void>()
141+
i.onload = () => {
142+
resolve()
143+
}
125144
i.src = await fs.readFile(join(__dirname, 'fixtures', name))
145+
await promise
126146
return i
127147
}

__test__/image-snapshot.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export async function snapshotImage<C>(
4747
if (existedPixels.length !== imagePixels.length) {
4848
await writeFailureImage()
4949
t.fail('Image size is not equal')
50-
return
5150
}
5251
let diffCount = 0
5352
imagePixels.forEach((u8, index) => {
@@ -58,7 +57,6 @@ export async function snapshotImage<C>(
5857
if (diffCount / existedPixels.length > differentRatio / 100) {
5958
await writeFailureImage()
6059
t.fail(`Image bytes is not equal, different ratio is ${((diffCount / existedPixels.length) * 100).toFixed(2)}%`)
61-
return
6260
}
6361
t.pass('Image pixels is equal')
6462
}

__test__/image.spec.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,29 @@ test('should be able to create Image', (t) => {
1616

1717
test('should be able to set src with buffer', async (t) => {
1818
const file = await loadImageFile()
19-
t.notThrows(() => {
19+
await t.notThrowsAsync(async () => {
2020
const image = new Image()
21+
const { promise, resolve, reject } = Promise.withResolvers<void>()
22+
image.onload = () => {
23+
resolve()
24+
}
25+
image.onerror = (err) => {
26+
reject(err)
27+
}
2128
image.src = file
29+
await promise
2230
})
2331
})
2432

2533
test('width and height state should be ok', async (t) => {
2634
const file = await loadImageFile()
2735
const image = new Image()
36+
const { promise, resolve } = Promise.withResolvers<void>()
37+
image.onload = () => {
38+
resolve()
39+
}
2840
image.src = file
41+
await promise
2942
t.is(image.width, 300)
3043
t.is(image.height, 320)
3144
t.is(image.naturalWidth, 300)
@@ -36,8 +49,13 @@ test('width and height state should be ok', async (t) => {
3649
test('complete state should be ok', async (t) => {
3750
const file = await loadImageFile()
3851
const image = new Image()
52+
const { promise, resolve } = Promise.withResolvers<void>()
53+
image.onload = () => {
54+
resolve()
55+
}
3956
t.is(image.complete, false)
4057
image.src = file
58+
await promise
4159
t.is(image.complete, true)
4260
})
4361

@@ -51,15 +69,25 @@ test('alt state should be ok', (t) => {
5169
test('with-exif image width and height should be correct', async (t) => {
5270
const file = await fs.readFile(join(__dirname, 'fixtures', 'with-exif.jpg'))
5371
const image = new Image()
72+
const { promise, resolve } = Promise.withResolvers<void>()
73+
image.onload = () => {
74+
resolve()
75+
}
5476
image.src = file
77+
await promise
5578
t.is(image.width, 450)
5679
t.is(image.height, 600)
5780
})
5881

5982
test('draw-image-exif', async (t) => {
6083
const file = await fs.readFile(join(__dirname, 'fixtures', 'with-exif.jpg'))
6184
const image = new Image()
85+
const { promise, resolve } = Promise.withResolvers<void>()
86+
image.onload = () => {
87+
resolve()
88+
}
6289
image.src = file
90+
await promise
6391
const canvas = createCanvas(800, 800)
6492
const ctx = canvas.getContext('2d')
6593
ctx.drawImage(image, 0, 0)
@@ -86,7 +114,12 @@ test('properties should be readonly', (t) => {
86114

87115
test('svg-transparent-background', async (t) => {
88116
const image = new Image()
117+
const { promise, resolve } = Promise.withResolvers<void>()
118+
image.onload = () => {
119+
resolve()
120+
}
89121
image.src = await fs.readFile(join(__dirname, '..', 'example', 'resize-svg.svg'))
122+
await promise
90123

91124
const w = 1000
92125
const h = 1000
@@ -117,11 +150,9 @@ test('load invalid image should not throw if onerror is provided', async (t) =>
117150
() =>
118151
new Promise<void>((resolve) => {
119152
const image = new Image()
120-
image.onload = () => {
121-
resolve()
122-
}
123153
image.onerror = (err) => {
124154
t.is(err.message, 'Unsupported image type')
155+
resolve()
125156
}
126157
image.src = broken
127158
}),

__test__/regression.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,12 @@ test('draw-svg-with-text', async (t) => {
186186
ctx.fillText(Title, 80, 100)
187187

188188
const Arrow = new Image()
189+
const { promise, resolve } = Promise.withResolvers<void>()
190+
Arrow.onload = () => {
191+
resolve()
192+
}
189193
Arrow.src = await fs.readFile(join(__dirname, 'image-og.svg'))
194+
await promise
190195
ctx.drawImage(Arrow, 80, 60)
191196
await snapshotImage(t, { ctx, canvas }, 'png', process.arch === 'x64' && process.platform !== 'darwin' ? 0.15 : 0.3)
192197
})

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"canvaskit-wasm": "^0.39.1",
8585
"colorette": "^2.0.20",
8686
"conventional-changelog-cli": "^5.0.0",
87+
"core-js": "^3.38.0",
8788
"echarts": "^5.4.3",
8889
"husky": "^9.0.10",
8990
"lint-staged": "^15.2.1",
@@ -109,7 +110,8 @@
109110
},
110111
"ava": {
111112
"require": [
112-
"@swc-node/register"
113+
"@swc-node/register",
114+
"core-js/proposals/promise-with-resolvers.js"
113115
],
114116
"extensions": [
115117
"ts"

skia-c/skia_c.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,9 +1511,11 @@ extern "C"
15111511
canvas->setMatrix(matrix);
15121512
auto image = SkImages::RasterFromBitmap(*bitmap);
15131513
canvas->drawImage(image, 0, 0);
1514+
oriented_bitmap->setImmutable();
15141515
bitmap_info->bitmap = reinterpret_cast<skiac_bitmap *>(oriented_bitmap);
15151516
delete bitmap;
15161517
} else {
1518+
bitmap->setImmutable();
15171519
bitmap_info->bitmap = reinterpret_cast<skiac_bitmap *>(bitmap);
15181520
}
15191521
bitmap_info->width = width;

0 commit comments

Comments
 (0)