Skip to content

Commit

Permalink
feat: support jpeg output
Browse files Browse the repository at this point in the history
  • Loading branch information
Brooooooklyn committed May 8, 2021
1 parent 371a022 commit 76adbdc
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 18 deletions.
17 changes: 11 additions & 6 deletions __test__/image-snapshot.ts
Expand Up @@ -2,18 +2,21 @@ import { promises as fs } from 'fs'
import { join } from 'path'

import PNG from '@jimp/png'
import JPEG from '@jimp/jpeg'
import { ExecutionContext } from 'ava'

const png = PNG()
const jpeg = JPEG()

export async function snapshotImage<C>(t: ExecutionContext<C>, context = t.context) {
export async function snapshotImage<C>(t: ExecutionContext<C>, context = t.context, type: 'png' | 'jpeg' = 'png') {
// @ts-expect-error
const { canvas } = context
const image = await canvas.png()
const p = join(__dirname, 'snapshots', `${t.title}.png`)
const image = await (type === 'png' ? canvas.png() : canvas.jpeg(100))
const ext = type === 'png' ? 'png' : 'jpg'
const p = join(__dirname, 'snapshots', `${t.title}.${ext}`)

async function writeFailureImage() {
await fs.writeFile(join(__dirname, 'failure', `${t.title}.png`), image)
await fs.writeFile(join(__dirname, 'failure', `${t.title}.${ext}`), image)
}

let existed = true
Expand All @@ -28,8 +31,10 @@ export async function snapshotImage<C>(t: ExecutionContext<C>, context = t.conte
} else {
const existed = await fs.readFile(p)
t.notThrowsAsync(async () => {
const existedPixels = png.decoders['image/png'](existed).data
const imagePixels = png.decoders['image/png'](image).data
const existedPixels =
type === 'png' ? png.decoders['image/png'](existed).data : jpeg.decoders['image/jpeg'](existed).data
const imagePixels =
type === 'png' ? png.decoders['image/png'](image).data : jpeg.decoders['image/jpeg'](image).data
if (existedPixels.length !== imagePixels.length) {
await writeFailureImage()
throw new Error('Image size is not equal')
Expand Down
33 changes: 32 additions & 1 deletion __test__/index.spec.ts
Expand Up @@ -2,6 +2,8 @@ import ava, { TestInterface } from 'ava'

import { createCanvas, Path2D, Canvas, SKRSContext2D } from '../index'

import { snapshotImage } from './image-snapshot'

const test = ava as TestInterface<{
canvas: Canvas
ctx: SKRSContext2D
Expand Down Expand Up @@ -150,4 +152,33 @@ test('textBaseline state should be ok', (t) => {
t.is(ctx.textBaseline, 'hanging')
})

test.todo('getTransform')
test('getTransform', (t) => {
const { ctx } = t.context
t.deepEqual(ctx.getTransform(), {
a: 1,
b: 0,
c: 0,
d: 1,
e: 0,
f: 0,
})
})

test('stroke-and-filling-jpeg', async (t) => {
const { ctx } = t.context
ctx.lineWidth = 16
ctx.strokeStyle = 'red'

// Stroke on top of fill
ctx.beginPath()
ctx.rect(25, 25, 100, 100)
ctx.fill()
ctx.stroke()

// Fill on top of stroke
ctx.beginPath()
ctx.rect(175, 25, 100, 100)
ctx.stroke()
ctx.fill()
await snapshotImage(t, t.context, 'jpeg')
})
Binary file added __test__/snapshots/stroke-and-filling-jpeg.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion index.d.ts
Expand Up @@ -238,7 +238,7 @@ export interface StrokeOptions {
join?: StrokeJoin
}

export interface SKRSContext2D extends Omit<CanvasRenderingContext2D, 'drawImage' | 'createPattern'> {
export interface SKRSContext2D extends Omit<CanvasRenderingContext2D, 'drawImage' | 'createPattern' | 'getTransform'> {
drawImage(image: Image, dx: number, dy: number): void
drawImage(image: Image, dx: number, dy: number, dw: number, dh: number): void
drawImage(
Expand All @@ -257,11 +257,20 @@ export interface SKRSContext2D extends Omit<CanvasRenderingContext2D, 'drawImage
repeat: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | null,
): CanvasPattern
getContextAttributes(): { alpha: boolean; desynchronized: boolean }
getTransform(): {
a: number
b: number
c: number
d: number
e: number
f: number
}
}

export interface Canvas extends Omit<HTMLCanvasElement, 'getContext'> {
getContext(contextType: '2d', contextAttributes?: { alpha: boolean }): SKRSContext2D
png(): Promise<Buffer>
jpeg(quality: number): Promise<Buffer>
}

export function createCanvas(width: number, height: number): Canvas
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -59,6 +59,7 @@
"devDependencies": {
"@jimp/core": "^0.16.1",
"@jimp/custom": "^0.16.1",
"@jimp/jpeg": "^0.16.1",
"@jimp/png": "^0.16.1",
"@napi-rs/cli": "^1.0.4",
"@octokit/rest": "^18.5.3",
Expand Down
21 changes: 17 additions & 4 deletions skia-c/skia_c.cpp
Expand Up @@ -159,6 +159,18 @@ extern "C"
}
}

void skiac_surface_jpeg_data(skiac_surface *c_surface, skiac_sk_data *data, int quality)
{
auto image = SURFACE_CAST->makeImageSnapshot();
auto jpeg_data = image->encodeToData(SkEncodedImageFormat::kJPEG, quality).release();
if (jpeg_data)
{
data->ptr = const_cast<uint8_t *>(jpeg_data->bytes());
data->size = jpeg_data->size();
data->data = reinterpret_cast<skiac_data *>(jpeg_data);
}
}

int skiac_surface_get_alpha_type(skiac_surface *c_surface)
{
return SURFACE_CAST->imageInfo().alphaType();
Expand Down Expand Up @@ -283,7 +295,7 @@ extern "C"
auto font_collection = c_collection->collection;

TextStyle text_style;
text_style.setFontFamilies({ SkString(font_family) });
text_style.setFontFamilies({SkString(font_family)});
text_style.setFontSize(font_size);
text_style.setForegroundColor(*PAINT_CAST);
text_style.setWordSpacing(0);
Expand Down Expand Up @@ -587,8 +599,9 @@ extern "C"
{
float intervals[] = {on, off};
auto pe = SkDashPathEffect::Make(intervals, 2, phase);
if (!pe) {
return false;
if (!pe)
{
return false;
}
SkStrokeRec rec(SkStrokeRec::InitStyle::kHairline_InitStyle);
if (pe->filterPath(PATH_CAST, *PATH_CAST, &rec, nullptr))
Expand Down Expand Up @@ -1013,7 +1026,7 @@ extern "C"
skiac_font_metrics *skiac_font_metrics_create(const char *font_family, float font_size)
{
TextStyle text_style;
text_style.setFontFamilies({ SkString(font_family) });
text_style.setFontFamilies({SkString(font_family)});
text_style.setFontSize(font_size);
text_style.setWordSpacing(0);
text_style.setHeight(1);
Expand Down
3 changes: 2 additions & 1 deletion skia-c/skia_c.hpp
Expand Up @@ -46,7 +46,7 @@ typedef struct skiac_font_metrics skiac_font_metrics;
struct skiac_typeface
{
sk_sp<SkTypeface> typeface;
skiac_typeface(const char* path)
skiac_typeface(const char *path)
{
typeface = SkTypeface::MakeFromFile(path);
}
Expand Down Expand Up @@ -138,6 +138,7 @@ extern "C"
void skiac_surface_read_pixels(skiac_surface *c_surface, skiac_surface_data *data);
bool skiac_surface_read_pixels_rect(skiac_surface *c_surface, uint8_t *data, int x, int y, int w, int h);
void skiac_surface_png_data(skiac_surface *c_surface, skiac_sk_data *data);
void skiac_surface_jpeg_data(skiac_surface *c_surface, skiac_sk_data *data, int quality);
int skiac_surface_get_alpha_type(skiac_surface *c_surface);
bool skiac_surface_save(skiac_surface *c_surface, const char *path);

Expand Down
10 changes: 6 additions & 4 deletions src/ctx.rs
Expand Up @@ -1835,7 +1835,6 @@ fn get_text_baseline(ctx: CallContext) -> Result<JsString> {

pub enum ContextData {
PNG(SurfaceRef),
#[allow(dead_code)]
JPEG(SurfaceRef, u8),
}

Expand All @@ -1854,9 +1853,12 @@ impl Task for ContextData {
"Get png data from surface failed".to_string(),
)
}),
_ => {
todo!();
}
ContextData::JPEG(surface, quality) => surface.jpeg_data(*quality).ok_or_else(|| {
Error::new(
Status::GenericFailure,
"Get png data from surface failed".to_string(),
)
}),
}
}

Expand Down
16 changes: 15 additions & 1 deletion src/lib.rs
Expand Up @@ -22,7 +22,7 @@ use sk::SurfaceDataRef;
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

#[cfg(windows)]
#[cfg(all(windows, not(debug_assertions)))]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;

Expand All @@ -47,6 +47,7 @@ fn init(mut exports: JsObject, env: Env) -> Result<()> {
&[
Property::new(&env, "getContext")?.with_method(get_context),
Property::new(&env, "png")?.with_method(png),
Property::new(&env, "jpeg")?.with_method(jpeg),
Property::new(&env, "toBuffer")?.with_method(to_buffer),
Property::new(&env, "savePNG")?.with_method(save_png),
],
Expand Down Expand Up @@ -143,6 +144,19 @@ fn png(ctx: CallContext) -> Result<JsObject> {
.map(|p| p.promise_object())
}

#[js_function(1)]
fn jpeg(ctx: CallContext) -> Result<JsObject> {
let quality = ctx.get::<JsNumber>(0)?.get_uint32()? as u8;
let this = ctx.this_unchecked::<JsObject>();
let ctx_js = this.get_named_property::<JsObject>("ctx")?;
let ctx2d = ctx.env.unwrap::<Context>(&ctx_js)?;

ctx
.env
.spawn(ContextData::JPEG(ctx2d.surface.reference(), quality))
.map(|p| p.promise_object())
}

#[js_function]
fn to_buffer(ctx: CallContext) -> Result<JsBuffer> {
let this = ctx.this_unchecked::<JsObject>();
Expand Down
24 changes: 24 additions & 0 deletions src/sk.rs
Expand Up @@ -206,6 +206,12 @@ mod ffi {

pub fn skiac_surface_png_data(surface: *mut skiac_surface, data: *mut skiac_sk_data);

pub fn skiac_surface_jpeg_data(
surface: *mut skiac_surface,
data: *mut skiac_sk_data,
quality: i32,
);

pub fn skiac_surface_get_alpha_type(surface: *mut skiac_surface) -> i32;

pub fn skiac_canvas_clear(canvas: *mut skiac_canvas, color: u32);
Expand Down Expand Up @@ -1325,6 +1331,24 @@ impl SurfaceRef {
}
}
}

#[inline]
pub fn jpeg_data(&self, quality: u8) -> Option<SurfaceDataRef> {
unsafe {
let mut data = ffi::skiac_sk_data {
ptr: ptr::null_mut(),
size: 0,
data: ptr::null_mut(),
};
ffi::skiac_surface_jpeg_data(self.0, &mut data, quality as i32);

if data.ptr.is_null() {
None
} else {
Some(SurfaceDataRef(data))
}
}
}
}

unsafe impl Send for SurfaceRef {}
Expand Down
14 changes: 14 additions & 0 deletions yarn.lock
Expand Up @@ -118,6 +118,15 @@
"@babel/runtime" "^7.7.2"
"@jimp/core" "^0.16.1"

"@jimp/jpeg@^0.16.1":
version "0.16.1"
resolved "https://registry.yarnpkg.com/@jimp/jpeg/-/jpeg-0.16.1.tgz#3b7bb08a4173f2f6d81f3049b251df3ee2ac8175"
integrity sha512-8352zrdlCCLFdZ/J+JjBslDvml+fS3Z8gttdml0We759PnnZGqrnPRhkOEOJbNUlE+dD4ckLeIe6NPxlS/7U+w==
dependencies:
"@babel/runtime" "^7.7.2"
"@jimp/utils" "^0.16.1"
jpeg-js "0.4.2"

"@jimp/png@^0.16.1":
version "0.16.1"
resolved "https://registry.npmjs.org/@jimp/png/-/png-0.16.1.tgz#f24cfc31529900b13a2dd9d4fdb4460c1e4d814e"
Expand Down Expand Up @@ -2340,6 +2349,11 @@ jju@^1.4.0:
resolved "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo=

jpeg-js@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.2.tgz#8b345b1ae4abde64c2da2fe67ea216a114ac279d"
integrity sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw==

js-string-escape@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
Expand Down

1 comment on commit 76adbdc

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 76adbdc Previous: 371a022 Ratio
Draw house#@napi-rs/skia 21.4 ops/sec (±0.64%) 23.931 ops/sec (±1.77%) 1.12
Draw house#node-canvas 20.7 ops/sec (±0.66%) 23.934 ops/sec (±1.78%) 1.16
Draw gradient#@napi-rs/skia 20.5 ops/sec (±1.49%) 24 ops/sec (±1.49%) 1.17
Draw gradient#node-canvas 20 ops/sec (±0.54%) 22 ops/sec (±1.95%) 1.10

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.