←Back

React Native – Watermark Photos when sharing with Expo

Recently I came across a need to add a watermark to photos in my React native + expo app when photos are shared outside of my app. My goal was to make watermarks customizable, reusable and most importantly it needs to play nice with expo & EAS. There are a couple options on npm that allow you to add a watermark to a photo however however when trying to use these pre-existing libraries I noticed the documentation is quite outdated or don’t work with expo go (instead you have to pre-build the app and use metro).

This solution is going to seem quite out-of-the-box thinking as it’s not using a regular photo manipulation library or watermark library. Instead it’s using a screenshot library and using native React native components to make the watermark.

Disclaimer: I like to post about actual working components with full functionality instead of bare-bones examples so some blocks of code are going to be lengthy and do more than what is advertised

Thought Process

Before we start coding I want to explain the thought process for this implementation as it seems unconventional. When the “share” icon is pressed next to a post on my app, we want to render this component with 0 opacity and no pointer events. Once it’s loaded we take a screenshot of the ref then create a temporary image on the device’s cache to be used in expo’s sharing functionality. The view shot dependency we are going to install in the next step automatically handles clearing the cache when the app is closed.

Set up

Set up is extremely simple, all we need to do is install a single dependency for the actual watermarking and a second dependency for sharing it outside of your application

Bash
npx expo install react-native-view-shot expo-sharing

Implementation

Lets create a new component called WaterMarkPhoto.tsx. Our goal for this component is to define a customizable component that sets a sharable local URI for when the user wants to share to social media/ other apps.

TypeScript
import YStack from '@components/layout/YStack';
import { Dimensions, Image, StyleSheet, View } from 'react-native';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Text } from 'react-native';
import ViewShot from 'react-native-view-shot';

const MAX_ASPECT_RATIO = 1.33;
const MIN_ASPECT_RATIO = 0.75;

interface WaterMarkPhotoProps {
  src: string;
  text: string;
  typeText: string;
  sharableURICallback: (uri: string) => void;
  generateSharableImage: boolean;
}

const styles = StyleSheet.create({
  poweredBy: {
    color: 'gray',
    fontSize: 8,
    fontStyle: 'italic',
  },
  waterMarkContainer: {
    backgroundColor: 'white',
    paddingHorizontal: 12,
    paddingVertical: 12,
  },
  viewShotContainer: {
    paddingHorizontal: 6,
    paddingTop: 6,
    margin: 0,
    backgroundColor: 'white',
  },
  overallContainer: {
    position: 'absolute',
    padding: 0,
    margin: 0,
    opacity: 0,
  },
  image: {
    borderRadius: 4,
  },
  caption: {
    color: 'black',
    fontSize: 10,
  },
});

const WatermarkPhoto = ({ src, text, typeText, sharableURICallback, generateSharableImage }: WaterMarkPhotoProps) => {
  const [imageSize, setImageSize] = useState({ width: 0, height: 0, aspectRatio: 0 });
  const captureRef = useRef<ViewShot>(null);

  useEffect(() => {
    if (!generateSharableImage) return;
    Image.getSize(src, (width, height) => {
      const screenWidth = Dimensions.get('window').width;
      const scaleFactor = width / screenWidth;
      const imageHeight = height / scaleFactor;
      const imageWidth = screenWidth;
      const aspectRatio = imageWidth / imageHeight;
      if (aspectRatio > MAX_ASPECT_RATIO) {
        setImageSize({ width: imageWidth, height: imageHeight, aspectRatio: MAX_ASPECT_RATIO });
        return;
      }
      if (aspectRatio < MIN_ASPECT_RATIO) {
        setImageSize({ width: imageWidth, height: imageHeight, aspectRatio: MIN_ASPECT_RATIO });
        return;
      }
      setImageSize({ width: imageWidth, height: imageHeight, aspectRatio });
    });
  }, [src, Dimensions]);

  const capture = useCallback(async () => {
    if (!generateSharableImage) return;
    if (!src) return '';
    try {
      sharableURICallback('');
      const uri = await captureRef.current.capture();
      if (uri) sharableURICallback(uri);
    } catch (error) {
      // handle error...
    }
  }, [src]);

  // prevent rendering if the image is not ready or if the image is not provided
  if (!src) return null;
  if (!imageSize.width) return null;
  if (!imageSize.height) return null;

  if (!generateSharableImage) return null;

  return (
    <View style={styles.overallContainer} pointerEvents="none">
      <ViewShot ref={captureRef} options={{ format: 'jpg', quality: 1 }} style={styles.viewShotContainer}>
        <YStack
          style={{
            padding: 0,
            margin: 0,
          }}
        >
          <Image
            source={{ uri: src }}
            style={[
              styles.image,
              {
                width: imageSize.width,
                aspectRatio: imageSize.aspectRatio,
              },
            ]}
            onLoad={capture}
          />

          <YStack style={styles.waterMarkContainer} justifyContent="space-between" alignItems="center" space={6}>
            <Text style={styles.caption}>
              {typeText} | <Text style={{ fontWeight: 'bold' }}>{text}</Text>
            </Text>
            <Text style={styles.poweredBy}>
              Powered By <Text style={{ fontWeight: 'bold', fontStyle: 'italic' }}>DrivnBye</Text>
            </Text>
          </YStack>
        </YStack>
      </ViewShot>
    </View>
  );
};

export default WatermarkPhoto;

Any changes to the stying within <ViewShot Tag will be reflected in the shared image. Feel free to experiment and play around

Usage

TypeScript
import * as Sharing from 'expo-sharing';
import WaterMarkPhoto from './WaterMarkPhoto'
import { TouchableOpacity, Text } from 'react-native';

const Example = () => {
  const [generate, setGenerate] = useState(false);
  const photo = 'link to a photo';

  const handleShare = async (uri: string) => {
    // share to social media
    const filePath = `file://${uri}`;
    await Sharing.shareAsync(filePath, {
      mimeType: 'image/jpeg', // Android
      UTI: 'image/jpeg', // iOS
      dialogTitle: 'Check out this cool watermarked photo',
    });
    setGenerate(false);
  };

  return (
    <>
      <WatermarkPhoto
        sharableURICallback={handleShare}
        src={photo}
        generateSharableImage={generate}
        text="username"
        typeText="Event"
      />
      <TouchableOpacity onPress={() => setGenerate(true)}>
        <Text>Generate</Text>
      </TouchableOpacity>
    </>
  );
};

Result

This watermark mimics a Polaroid by giving space at the bottom of the image to advertise you app, give the user some context like a username or how the photo was shared. It is also non-intrusive so if the user wants to crop out the watermark they can do so without needing to crop the actual image.

Leave a Reply

Your email address will not be published. Required fields are marked *