Skip to content

Commit

Permalink
Merge pull request #1952 from NikitaDudin/feature/webp
Browse files Browse the repository at this point in the history
Add support of WEBP format conversion
  • Loading branch information
wcandillon authored Nov 20, 2023
2 parents b816245 + 7fefeff commit 9c1975d
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 11 deletions.
17 changes: 16 additions & 1 deletion package/cpp/api/JsiSkImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "include/codec/SkEncodedImageFormat.h"
#include "include/encode/SkJpegEncoder.h"
#include "include/encode/SkPngEncoder.h"
#include "include/encode/SkWebpEncoder.h"

#pragma clang diagnostic pop

Expand Down Expand Up @@ -78,20 +79,34 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject<SkImage> {
count >= 1 ? static_cast<SkEncodedImageFormat>(arguments[0].asNumber())
: SkEncodedImageFormat::kPNG;

auto quality = count == 2 ? arguments[1].asNumber() : 100.0;
auto quality = (count >= 2 && arguments[1].isNumber())
? arguments[1].asNumber()
: 100.0;
auto image = getObject();
if (image->isTextureBacked()) {
image = image->makeNonTextureImage();
}
sk_sp<SkData> data;

if (format == SkEncodedImageFormat::kJPEG) {
SkJpegEncoder::Options options;
options.fQuality = quality;
data = SkJpegEncoder::Encode(nullptr, image.get(), options);
} else if (format == SkEncodedImageFormat::kWEBP) {
SkWebpEncoder::Options options;
if (quality >= 100) {
options.fCompression = SkWebpEncoder::Compression::kLossless;
options.fQuality = 75; // This is effort to compress
} else {
options.fCompression = SkWebpEncoder::Compression::kLossy;
options.fQuality = quality;
}
data = SkWebpEncoder::Encode(nullptr, image.get(), options);
} else {
SkPngEncoder::Options options;
data = SkPngEncoder::Encode(nullptr, image.get(), options);
}

return data;
}

Expand Down
255 changes: 255 additions & 0 deletions package/src/renderer/__tests__/e2e/ImageEncoding.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { surface } from "../setup";

const MAGIC_BYTES = {
JPEG: "/9j/4",
PNG: "iVBORw0KGgo",
WEBP: "UklGR",
};

const IMAGE_FORMAT = {
JPEG: 3, // ImageFormat.JPEG,
PNG: 4, // ImageFormat.PNG,
WEBP: 6, // ImageFormat.WEBP,
};

const IMAGE_INFO_BASE = {
alphaType: 1, // AlphaType.Opaque,
colorType: 4, // ColorType.RGBA_8888,
};

describe("Image Encoding", () => {
it("SkImage.encodeToBase64: check WEBP format by magic bytes sequence UklGR...", async () => {
const result = await surface.eval(
(Skia, ctx) => {
const width = 1;
const height = 1;
const bytesPerPixel = 4;
const bytes = new Uint8Array(width * height * bytesPerPixel);
bytes.fill(255);
const data = Skia.Data.fromBytes(bytes);
const imageInfo = {
...ctx.imageInfoBase,
width,
height,
};

return Skia.Image.MakeImage(imageInfo, data, width * bytesPerPixel)!
.encodeToBase64(ctx.format)
.slice(0, ctx.cutIndex);
},
{
cutIndex: MAGIC_BYTES.WEBP.length,
imageInfoBase: IMAGE_INFO_BASE,
format: IMAGE_FORMAT.WEBP,
}
);

expect(result).toEqual(MAGIC_BYTES.WEBP);
});

it("SkImage.encodeToBase64: check PNG format by magic bytes sequence iVBORw0KGgo...", async () => {
const result = await surface.eval(
(Skia, ctx) => {
const width = 1;
const height = 1;
const bytesPerPixel = 4;
const bytes = new Uint8Array(width * height * bytesPerPixel);
bytes.fill(255);
const data = Skia.Data.fromBytes(bytes);
const imageInfo = {
...ctx.imageInfoBase,
width,
height,
};

return Skia.Image.MakeImage(imageInfo, data, width * bytesPerPixel)!
.encodeToBase64(ctx.format)
.slice(0, ctx.cutIndex);
},
{
cutIndex: MAGIC_BYTES.PNG.length,
imageInfoBase: IMAGE_INFO_BASE,
format: IMAGE_FORMAT.PNG,
}
);

expect(result).toEqual(MAGIC_BYTES.PNG);
});

it("SkImage.encodeToBase64: check JPEG format by magic bytes sequence /9j/4...", async () => {
const result = await surface.eval(
(Skia, ctx) => {
const width = 1;
const height = 1;
const bytesPerPixel = 4;
const bytes = new Uint8Array(width * height * bytesPerPixel);
bytes.fill(255);
const data = Skia.Data.fromBytes(bytes);
const imageInfo = {
...ctx.imageInfoBase,
width,
height,
};

return Skia.Image.MakeImage(imageInfo, data, width * bytesPerPixel)!
.encodeToBase64(ctx.format)
.slice(0, ctx.cutIndex);
},
{
cutIndex: MAGIC_BYTES.JPEG.length,
imageInfoBase: IMAGE_INFO_BASE,
format: IMAGE_FORMAT.JPEG,
}
);

expect(result).toEqual(MAGIC_BYTES.JPEG);
});

it("SkImage.encodeToBase64: JPEG checking of the quality argument work", async () => {
const result = await surface.eval(
(Skia, ctx) => {
const width = 1024;
const height = 1024;
const bytesPerPixel = 4;
const bytes = new Uint8Array(width * height * bytesPerPixel);
bytes.fill(255);
let i = 0;
for (let x = 0; x < width * bytesPerPixel; x++) {
for (let y = 0; y < height; y++) {
bytes[i++] = (x * y) % 255;
}
}
const data = Skia.Data.fromBytes(bytes);
const imageInfo = {
...ctx.imageInfoBase,
width,
height,
};
const image = Skia.Image.MakeImage(
imageInfo,
data,
width * bytesPerPixel
)!;
const minQuality = image.encodeToBase64(ctx.format, 1e-8).length;
const midQuality = image.encodeToBase64(ctx.format, 50).length;
const defaultQuality = image.encodeToBase64(ctx.format).length;
const maxQuality = image.encodeToBase64(ctx.format, 100).length;

return {
minQuality,
midQuality,
defaultQuality,
maxQuality,
};
},
{ imageInfoBase: IMAGE_INFO_BASE, format: IMAGE_FORMAT.JPEG }
);

expect(result.minQuality).toBeLessThan(result.maxQuality);
expect(result.minQuality).toBeLessThan(result.defaultQuality);
expect(result.minQuality).toBeLessThan(result.midQuality);
expect(result.midQuality).toBeLessThan(result.maxQuality);
expect(result.defaultQuality).toEqual(result.maxQuality);
});

it("SkImage.encodeToBase64: PNG checking. The quality argument doesn't work", async () => {
const result = await surface.eval(
(Skia, ctx) => {
const width = 1024;
const height = 1024;
const bytesPerPixel = 4;
const bytes = new Uint8Array(width * height * bytesPerPixel);
bytes.fill(255);
let i = 0;
for (let x = 0; x < width * bytesPerPixel; x++) {
for (let y = 0; y < height; y++) {
bytes[i++] = (x * y) % 255;
}
}
const data = Skia.Data.fromBytes(bytes);
const imageInfo = {
...ctx.imageInfoBase,
width,
height,
};
const image = Skia.Image.MakeImage(
imageInfo,
data,
width * bytesPerPixel
)!;
const minQuality = image.encodeToBase64(ctx.format, 1e-8).length;
const midQuality = image.encodeToBase64(ctx.format, 50).length;
const defaultQuality = image.encodeToBase64(ctx.format).length;
const maxQuality = image.encodeToBase64(ctx.format, 100).length;

return {
minQuality,
midQuality,
defaultQuality,
maxQuality,
};
},
{ imageInfoBase: IMAGE_INFO_BASE, format: IMAGE_FORMAT.PNG }
);

expect(result.minQuality).toEqual(result.midQuality);
expect(result.minQuality).toEqual(result.defaultQuality);
expect(result.minQuality).toEqual(result.maxQuality);
});

it("SkImage.encodeToBase64: WEBP checking of the quality argument work", async () => {
const result = await surface.eval(
(Skia, ctx) => {
const width = 1024;
const height = 1024;
const bytesPerPixel = 4;
const bytes = new Uint8Array(width * height * bytesPerPixel);
let i = 0;
for (let x = 0; x < width * bytesPerPixel; x++) {
for (let y = 0; y < height; y++) {
bytes[i++] = (x * y) % 255;
}
}
const data = Skia.Data.fromBytes(bytes);
const imageInfo = {
...ctx.imageInfoBase,
width,
height,
};
const image = Skia.Image.MakeImage(
imageInfo,
data,
width * bytesPerPixel
)!;
const minQualityLossy = image.encodeToBase64(ctx.format, 1e-8).length;
const midQualityLossy = image.encodeToBase64(ctx.format, 50).length;
const maxQualityLossy = image.encodeToBase64(
ctx.format,
100 - 1e-8
).length;
const defaultQualityLossless = image.encodeToBase64(
ctx.format,
undefined
).length;
const maxQualityLossless = image.encodeToBase64(ctx.format, 100).length;

return {
minQualityLossy,
midQualityLossy,
maxQualityLossy,
defaultQualityLossless,
maxQualityLossless,
};
},
{ imageInfoBase: IMAGE_INFO_BASE, format: IMAGE_FORMAT.WEBP }
);

expect(result.minQualityLossy).toBeLessThan(result.midQualityLossy);
expect(result.minQualityLossy).toBeLessThan(result.maxQualityLossy);
expect(result.midQualityLossy).toBeLessThan(result.maxQualityLossy);
expect(result.minQualityLossy).not.toEqual(result.maxQualityLossless);
expect(result.midQualityLossy).not.toEqual(result.maxQualityLossless);
expect(result.maxQualityLossy).not.toEqual(result.maxQualityLossless);
expect(result.defaultQualityLossless).toEqual(result.maxQualityLossless);
});
});
20 changes: 10 additions & 10 deletions package/src/skia/web/JsiSkImage.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type {
CanvasKit,
Image,
ImageInfo as CKImageInfo,
Image,
} from "canvaskit-wasm";

import {
type ImageFormat,
type ImageInfo,
type FilterMode,
type MipmapMode,
type SkImage,
type SkMatrix,
type SkShader,
type TileMode,
import type {
FilterMode,
MipmapMode,
SkImage,
SkMatrix,
SkShader,
TileMode,
ImageFormat,
ImageInfo,
} from "../types";

import { ckEnum, getCkEnum, HostObject } from "./Host";
Expand Down

0 comments on commit 9c1975d

Please sign in to comment.