Skip to content
Open
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
24 changes: 24 additions & 0 deletions apps/docs/docs/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ const data = Skia.Data.fromBase64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJA
const image = Skia.Image.MakeImageFromEncoded(data);
```

### MakeImageFromEncodedScaled

When an encoded image is only needed at a smaller size,
`MakeImageFromEncodedScaled` can avoid a full-resolution native decode. The
requested width and optional height form an aspect-preserving bounding box:

```tsx twoslash
import { Skia } from "@shopify/react-native-skia";

declare const data: ReturnType<typeof Skia.Data.fromBytes>;
const thumbnail = Skia.Image.MakeImageFromEncodedScaled(data, 400, 300);
```

Native codecs decode at the closest scale they support, so the returned image
can be slightly larger or smaller than the requested box. A codec without
scaled decoding support returns the source resolution. The method never
upscales beyond that resolution and returns `null` for invalid data or
non-positive dimensions.

CanvasKit does not expose codec-level scaled decoding. On Web, the method
preserves the output semantics by decoding at full resolution and resizing
afterwards, so it does not provide the same CPU and peak-memory benefit as the
native implementation.

### MakeImage

`MakeImage` allows you to create an image by providing pixel data and specifying the format.
Expand Down
81 changes: 81 additions & 0 deletions packages/skia/cpp/api/JsiSkImageFactory.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <algorithm>
#include <cmath>
#include <memory>
#include <utility>

Expand All @@ -11,6 +13,9 @@
#include "JsiSkImage.h"
#include "JsiSkImageInfo.h"

#include "include/codec/SkCodec.h"
#include "include/codec/SkEncodedOrigin.h"

#ifdef SK_GRAPHITE
#include "rnskia/RNDawnContext.h"
#include "rnwgpu/api/GPUTexture.h"
Expand Down Expand Up @@ -41,6 +46,80 @@ class JsiSkImageFactory : public JsiSkHostObject {
runtime, hostObjectInstance, getContext());
}

JSI_HOST_FUNCTION(MakeImageFromEncodedScaled) {
if (count < 2 || !arguments[1].isNumber()) {
return jsi::Value::null();
}

const auto targetWidth = arguments[1].asNumber();
if (!std::isfinite(targetWidth) || targetWidth <= 0) {
return jsi::Value::null();
}

double targetHeight = 0;
if (count > 2 && !arguments[2].isUndefined()) {
if (!arguments[2].isNumber()) {
return jsi::Value::null();
}
targetHeight = arguments[2].asNumber();
if (!std::isfinite(targetHeight) || targetHeight <= 0) {
return jsi::Value::null();
}
}

auto data = JsiSkData::fromValue(runtime, arguments[0]);
auto codec = SkCodec::MakeFromData(data);
if (codec == nullptr) {
return jsi::Value::null();
}

const auto encodedDimensions = codec->dimensions();
if (encodedDimensions.isEmpty()) {
return jsi::Value::null();
}

const bool swapsWidthHeight =
SkEncodedOriginSwapsWidthHeight(codec->getOrigin());
const auto imageWidth = swapsWidthHeight ? encodedDimensions.height()
: encodedDimensions.width();
const auto imageHeight = swapsWidthHeight ? encodedDimensions.width()
: encodedDimensions.height();

auto scale = targetWidth / static_cast<double>(imageWidth);
if (targetHeight > 0) {
scale = std::min(
scale, targetHeight / static_cast<double>(imageHeight));
}
scale = std::min(scale, 1.0);

const auto desiredScale = static_cast<float>(scale);
if (desiredScale <= 0) {
return jsi::Value::null();
}
const auto scaledDimensions =
codec->getScaledDimensions(desiredScale);
if (scaledDimensions.isEmpty()) {
return jsi::Value::null();
}

const auto outputDimensions =
swapsWidthHeight
? SkISize::Make(scaledDimensions.height(),
scaledDimensions.width())
: scaledDimensions;
const auto scaledInfo =
codec->getInfo().makeDimensions(outputDimensions);
auto [image, result] = codec->getImage(scaledInfo);
if (image == nullptr || result != SkCodec::kSuccess) {
return jsi::Value::null();
}

auto hostObjectInstance =
std::make_shared<JsiSkImage>(getContext(), std::move(image));
return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(
runtime, hostObjectInstance, getContext());
}

JSI_HOST_FUNCTION(MakeImageFromNativeBuffer) {
jsi::BigInt pointer = arguments[0].asBigInt(runtime);
const uintptr_t nativeBufferPointer = pointer.asUint64(runtime);
Expand Down Expand Up @@ -184,6 +263,8 @@ class JsiSkImageFactory : public JsiSkHostObject {
std::string getObjectType() const override { return "JsiSkImageFactory"; }

JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImageFromEncoded),
JSI_EXPORT_FUNC(JsiSkImageFactory,
MakeImageFromEncodedScaled),
JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImageFromViewTag),
JSI_EXPORT_FUNC(JsiSkImageFactory,
MakeImageFromNativeBuffer),
Expand Down
46 changes: 45 additions & 1 deletion packages/skia/src/renderer/__tests__/e2e/Image.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";

import { checkImage } from "../../../__tests__/setup";
import { images, loadImage, surface } from "../setup";
import { dataAssets, images, loadImage, surface } from "../setup";
import { Fill, Image as SkiaImage } from "../../components";
import { AlphaType, ColorType } from "../../../skia/types";

Expand Down Expand Up @@ -54,6 +54,50 @@ describe("Image loading from bundles", () => {
]);
});

it("should decode an encoded image at a reduced resolution", async () => {
const dimensions = await surface.eval(
(Skia, { data }) => {
const encoded = Skia.Data.fromBytes(new Uint8Array(data));
const fullSize = Skia.Image.MakeImageFromEncoded(encoded)!;
const scaled = Skia.Image.MakeImageFromEncodedScaled(
encoded,
fullSize.width() / 2,
fullSize.height() / 2
)!;
return {
fullWidth: fullSize.width(),
fullHeight: fullSize.height(),
scaledWidth: scaled.width(),
scaledHeight: scaled.height(),
};
},
{ data: Array.from(dataAssets.img_0) }
);

expect(dimensions.scaledWidth).toBeLessThan(dimensions.fullWidth);
expect(dimensions.scaledHeight).toBeLessThan(dimensions.fullHeight);
expect(dimensions.scaledWidth / dimensions.scaledHeight).toBeCloseTo(
dimensions.fullWidth / dimensions.fullHeight,
1
);
});

it("should reject non-positive scaled decode dimensions", async () => {
const result = await surface.eval(
(Skia, { data }) => {
const encoded = Skia.Data.fromBytes(new Uint8Array(data));
return {
width: Skia.Image.MakeImageFromEncodedScaled(encoded, 0) === null,
height:
Skia.Image.MakeImageFromEncodedScaled(encoded, 100, -1) === null,
};
},
{ data: Array.from(dataAssets.img_0) }
);

expect(result).toEqual({ width: true, height: true });
});

it("should read pixels from a canvas", async () => {
const pixels = await surface.eval(
(Skia, { colorType, alphaType }) => {
Expand Down
23 changes: 23 additions & 0 deletions packages/skia/src/skia/types/Image/ImageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@ export interface ImageFactory {
*/
MakeImageFromEncoded: (encoded: SkData) => SkImage | null;

/**
* Decode an encoded image at a reduced resolution.
*
* The requested dimensions define an aspect-preserving bounding box. Native
* codecs decode at their closest supported scale, which may be slightly
* larger or smaller than the requested box. Codecs without scaled decoding
* support, and requests larger than the source, return a native-resolution
* image.
*
* On Web, CanvasKit does not expose codec-level scaled decoding, so the image
* is decoded at full resolution and then resized to fit the requested box.
*
* @param encoded - Data object containing encoded image bytes.
* @param targetWidth - Positive target width in pixels.
* @param targetHeight - Optional positive target height in pixels.
* @returns A decoded image, or null if the input or dimensions are invalid.
*/
MakeImageFromEncodedScaled: (
encoded: SkData,
targetWidth: number,
targetHeight?: number
) => SkImage | null;

/**
* Return an Image backed by a given native buffer.
* The native buffer must be a valid owning reference.
Expand Down
60 changes: 60 additions & 0 deletions packages/skia/src/skia/web/JsiSkImageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,66 @@ export class JsiSkImageFactory extends Host implements ImageFactory {
return new JsiSkImage(this.CanvasKit, image);
}

MakeImageFromEncodedScaled(
encoded: SkData,
targetWidth: number,
targetHeight?: number
) {
if (
!Number.isFinite(targetWidth) ||
targetWidth <= 0 ||
(targetHeight !== undefined &&
(!Number.isFinite(targetHeight) || targetHeight <= 0))
) {
return null;
}

const image = this.CanvasKit.MakeImageFromEncoded(
JsiSkData.fromValue(encoded)
);
if (image === null) {
return null;
}

const width = image.width();
const height = image.height();
let scale = targetWidth / width;
if (targetHeight !== undefined) {
scale = Math.min(scale, targetHeight / height);
}
scale = Math.min(scale, 1);
if (scale === 1) {
return new JsiSkImage(this.CanvasKit, image);
}

const scaledWidth = Math.max(1, Math.round(width * scale));
const scaledHeight = Math.max(1, Math.round(height * scale));
const surface = this.CanvasKit.MakeSurface(scaledWidth, scaledHeight);
if (surface === null) {
image.delete();
return null;
}

const canvas = surface.getCanvas();
canvas.drawImageRectOptions(
image,
[0, 0, width, height],
[0, 0, scaledWidth, scaledHeight],
this.CanvasKit.FilterMode.Linear,
this.CanvasKit.MipmapMode.None
);
surface.flush();
const scaledImage = surface.makeImageSnapshot();
surface.delete();

if (scaledImage === null) {
image.delete();
return null;
}
image.delete();
return new JsiSkImage(this.CanvasKit, scaledImage);
}

MakeImageFromNativeTextureUnstable() {
return throwNotImplementedOnRNWeb<SkImage>();
}
Expand Down
Loading