Shopify / react-native-skia

High-performance React Native Graphics using Skia
https://shopify.github.io/react-native-skia
MIT License
6.97k stars 450 forks source link

load image from filesystem #452

Closed CanRau closed 2 years ago

CanRau commented 2 years ago

Hey amazing people, I'm really impressed by RN Skia and my opportunity to finally get more into shaders and stuff 😍

Unfortunately I couldn't find a way to load an image from the file system into Skia.

So I take pictures with RN Vision Camera which gives me a temp path like the following /data/user/0/com.example.app/cache/mrousavy6579149888239818134.jpg, which I pass 1 to many to another screen, in a normal RN Image component I load it by prepending file://. I also tried base64 encoded FileSystem.readAsStringAsync prepended with data:image/jpeg;base64,

Though I'm not sure how to combine that with RN-Skias useImage hook in an async way 🤯

Tried to search the repo for examples and inspiration, but couldn't get anything off of it so far, what am I missing?

wobsoriano commented 2 years ago
import { Skia } from '@shopify/react-native-skia'

Skia.Data.fromBase64(base64: string): Data

Skia.Data.fromBytes(bytes: Uint8Array): Data

Skia.Data.fromURI(uri: string): Promise<Data>

https://github.com/Shopify/react-native-skia/blob/main/package/src/skia/Data/Data.ts

CanRau commented 2 years ago

Thanks for the quick response, took me a little to get from there to find MakeImageFromEncoded

const imageData = await Skia.Data.fromURI(`file://${photo.path}`);
const image = Skia.MakeImageFromEncoded(imageData)

it's not erroring anymore but my screen stays blank, tried to map as well as just render the first (just in case)

import React, { useEffect, useState } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { PhotoFile } from "react-native-vision-camera";
import {
  Skia,
  Canvas,
  Image,
  SkImage,
} from "@shopify/react-native-skia";
import { RootStackScreenProps } from "../types";

type Image = PhotoFile & {
  image: SkImage | null;
};

export const SkiaScreen = ({ navigation, route }: RootStackScreenProps<"Skia">) => {
  const { photos } = route.params;
  const [images, setImages] = useState<Array<Image>>([]);

  const readImages = async (): Promise<Array<Image>> => {
    const imgs: Array<Image> = [];
    for (const photo of photos) {
      const imageData = await Skia.Data.fromURI(`file://${photo.path}`);
      imgs.push({ ...photo, image: Skia.MakeImageFromEncoded(imageData) });
    }
    return imgs;
  };

  useEffect(() => {
    readImages()
      .then((imgs) => setImages(imgs))
      .catch(() => console.log("errored"));
  }, []);

  if (photos?.length < 1) {
    if (navigation.canGoBack()) {
      navigation.goBack();
    }
    navigation.navigate("Camera");
    return null;
  }

  return (
<SafeAreaView>
  <Canvas style={{ flex: 1 }}>
    {images.length > 0 && images[0].image !== null && (
      <Image
        key={images[0].path}
        image={images[0].image}
        width={images[0].image.width()}
        height={images[0].image.height()}
        fit="contain"
        x={0}
        y={0}
      />
    )}
      {/* {images.map((img) => {
        if (!img.image) return null;
        return (
          <Image
            key={img.path}
            image={img.image}
            width={img.width}
            height={img.height}
            fit="scaleDown"
            x={0}
            y={0}
          />
        );
      })} */}
    </Canvas>
  </SafeAreaView>
  );
};

img.image is the SkImage, the rest is PhotoFile by RN Vision Camera which contains width, height, path and other metadata.

any obvious mistake I'm making?

CanRau commented 2 years ago

Interestingly the above code works when removing the SafeAreaView though the images.map doesn't work either way even when setting fit="contain" instead of scaleDown 🤔

wobsoriano commented 2 years ago

Can you return an empty tag instead of null in your map function and see what happens?

Also width and height are functions

{images.map((img) => {
  if (!img.image) return <></>;

  return (
    <Image
      key={img.path}
      image={img.image}
      width={img.width()}
      height={img.height()}
      fit="scaleDown"
      x={0}
      y={0}
    />
  );
})}
CanRau commented 2 years ago

Returning <></> didn't change anything

Yea in the single image code I'm using it like this images[0].image.width() the map code is accessing width={img.width} which comes from PhotoFile so img.image is the SkImage

I was actually sure I tried img.image.width() as well but it looks like I didn't as it's now working, I didn't think those would make such a difference, also I was sure I tried to enter manual numbers, which didn't work (if I did) and now that also works. 🤷🏻‍♂️

So thanks for your time and sorry for the confusion, though I still don't understand why some manually entered numbers work and others don't which seems to be the reason width={img.width} doesn't work.

Edit: I thought, like with react-native Image, I could enter whatever sizes to "scale" the images, an alternative I just found would be a <Group> around the image with a scale transform I guess.

Edit2: Might be better in a new issue? I'm still not able to wrap my <Canvas> in a RN <View> or import { SafeAreaView } from "react-native-safe-area-context"; The rest of the content works, the canvas just vanishes it seems

wcandillon commented 2 years ago

I would be interested to investigate the issue, is there a minimal reproducible example you could provide us with?

CanRau commented 2 years ago

I was trying to prepare a repro but so far I can't reproduce, even in my actual codebase, not sure what went wrong 🤷🏻‍♂️ I'll keep observing but for now looks like I'm good. Thanks a lot for your efforts 🙏🥰. Reopening if it happens again.

azashi commented 1 year ago

Prefixing file:// in case of iOS worked for me.

 const imgData = await Skia.Data.fromURI(
        `${Platform.OS === "android" ? "" : "file://"}${_uri}`
      );

      const image = Skia.Image.MakeImageFromEncoded(imgData);

      setSnapShot(image);
PiusLucky commented 5 months ago

Just adding this here if anyone runs into this in the future.

Solution >>> Set a default width and height to the canvas

  const width = 256;
  const height = 256;
  const image = useImage("https://picsum.photos/200/300"); // Doesn't matter, you could load the image anywhere

   <Canvas style={{ width, height }}>
      <Image
        image={image}
        fit="fitWidth"
        x={0}
        y={0}
        width={image?.width()}
        height={image?.height()}
      />
  </Canvas>