Files
react-native-svg/apple/Utils/RNSVGFabricConversions.h
Jakub Grzywacz 08e92074b4 feat: filters support FeColorMatrix and FilterImage (#2316)
# Summary

Introducing the long-awaited **Filters** in `react-native-svg` 🎉

### Motivation

This PR is the beginning of bringing support of SVG Filters into
`react-native-svg`.

* **related issues**: This PR series will address the following issues:
#150, #176, #635, #883, #994, #996, #1216
* **feature overview**: This PR is a boilerplate for Filters
   * introducing `Filter` component and `FeColorMatrix` as a start. 
* It also introduces a new subdirectory called
`react-native-svg/filter-image` with a `FilterImage` component.

# Usage

## Filter and Fe...

Filters are compatible with the web familiar standard, so most things
should be compatible out-of-the-box and changes will be limited to using
a capital letter as it's component.

### Example

```tsx
import React from 'react';
import { FeColorMatrix, Filter, Rect, Svg } from 'react-native-svg';

export default () => {
  return (
    <Svg height="300" width="300">
      <Filter id="myFilter">
        <FeColorMatrix type="saturate" values="0.2" />
      </Filter>
      <Rect
        x="0"
        y="0"
        width="300"
        height="300"
        fill="red"
        filter="url(#myFilter)"
      />
    </Svg>
  );
};
```

![image](https://github.com/software-mansion/react-native-svg/assets/39670088/c36fb238-95f4-455d-b0aa-2a7d4038b828)

## Filter Image

`FilterImage` is a new component that is not strictly related to SVG.
Its behavior should be the same as a regular `Image` component from
React Native with one exception - the additional prop `filters`, which
accepts an array of filters to apply to the image.

### Example

```tsx
import React from 'react';
import { StyleSheet } from 'react-native';
import { FilterImage } from 'react-native-svg/filter-image';

const myImage = require('./myImage.jpg');

export default () => {
  return (
    <FilterImage
      style={styles.image}
      source={myImage}
      filters={[
        { name: 'colorMatrix', type: 'saturate', values: 0.2 },
        {
          name: 'colorMatrix',
          type: 'matrix',
          values: [
            0.2, 0.2, 0.2, 0, 0, 0.2, 0.2, 0.2, 0, 0, 0.2, 0.2, 0.2, 0, 0, 0, 0,
            0, 1, 0,
          ],
        },
      ]}
    />
  );
};
const styles = StyleSheet.create({
  image: {
    width: 200,
    height: 200,
  },
});
```


![image](https://github.com/software-mansion/react-native-svg/assets/39670088/666ed89f-68d8-491b-b97f-1eef112b7095)

## Test Plan

**Example App**: Updated the example app with various filter effects,
showcasing real-world usage.

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |         |
| Android |         |

## Checklist

- [x] I have tested this on a device and a simulator
- [x] I added documentation in `README.md` and `USAGE.md`
- [x] I updated the typed files (typescript)
2024-07-11 11:17:35 +02:00

278 lines
11 KiB
Objective-C

#import "RNSVGContextBrush.h"
#import "RNSVGFilterPrimitive.h"
#import "RNSVGGroup.h"
#import "RNSVGLength.h"
#import "RNSVGPainterBrush.h"
#import "RNSVGRenderable.h"
#import "RNSVGSolidColorBrush.h"
#import "RNSVGText.h"
#import "RNSVGVBMOS.h"
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <folly/dynamic.h>
// copied from RCTFollyConvert
static id RNSVGConvertFollyDynamicToId(const folly::dynamic &dyn)
{
// I could imagine an implementation which avoids copies by wrapping the
// dynamic in a derived class of NSDictionary. We can do that if profiling
// implies it will help.
switch (dyn.type()) {
case folly::dynamic::NULLT:
return nil;
case folly::dynamic::BOOL:
return dyn.getBool() ? @YES : @NO;
case folly::dynamic::INT64:
return @(dyn.getInt());
case folly::dynamic::DOUBLE:
return @(dyn.getDouble());
case folly::dynamic::STRING:
return [[NSString alloc] initWithBytes:dyn.c_str() length:dyn.size() encoding:NSUTF8StringEncoding];
case folly::dynamic::ARRAY: {
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:dyn.size()];
for (const auto &elem : dyn) {
id value = RNSVGConvertFollyDynamicToId(elem);
if (value) {
[array addObject:value];
}
}
return array;
}
case folly::dynamic::OBJECT: {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:dyn.size()];
for (const auto &elem : dyn.items()) {
id key = RNSVGConvertFollyDynamicToId(elem.first);
id value = RNSVGConvertFollyDynamicToId(elem.second);
if (key && value) {
dict[key] = value;
}
}
return dict;
}
}
}
template <typename T>
RNSVGBrush *brushFromColorStruct(const T &fillObject)
{
int type = fillObject.type;
switch (type) {
case -1: // empty struct
return nil;
case 0: // solid color
{
// These are probably expensive allocations since it's often the same value.
// We should memoize colors but look ups may be just as expensive.
RNSVGColor *color = RCTUIColorFromSharedColor(fillObject.payload) ?: [RNSVGColor clearColor];
return [[RNSVGSolidColorBrush alloc] initWithColor:color];
}
case 1: // brush
{
NSArray *arr = @[ @(type), RCTNSStringFromString(fillObject.brushRef) ];
return [[RNSVGPainterBrush alloc] initWithArray:arr];
}
case 2: // currentColor
return [[RNSVGBrush alloc] initWithArray:nil];
case 3: // context-fill
return [[RNSVGContextBrush alloc] initFill];
case 4: // context-stroke
return [[RNSVGContextBrush alloc] initStroke];
default:
RCTLogError(@"Unknown brush type: %d", type);
return nil;
}
}
template <typename T>
void setCommonNodeProps(const T &nodeProps, RNSVGNode *node)
{
node.name = RCTNSStringFromStringNilIfEmpty(nodeProps.name);
node.opacity = nodeProps.opacity;
if (nodeProps.matrix.size() == 6) {
node.matrix = CGAffineTransformMake(
nodeProps.matrix.at(0),
nodeProps.matrix.at(1),
nodeProps.matrix.at(2),
nodeProps.matrix.at(3),
nodeProps.matrix.at(4),
nodeProps.matrix.at(5));
}
CATransform3D transform3d = RCTCATransform3DFromTransformMatrix(nodeProps.transform);
CGAffineTransform transform = CATransform3DGetAffineTransform(transform3d);
node.invTransform = CGAffineTransformInvert(transform);
node.transforms = transform;
node.mask = RCTNSStringFromStringNilIfEmpty(nodeProps.mask);
node.markerStart = RCTNSStringFromStringNilIfEmpty(nodeProps.markerStart);
node.markerMid = RCTNSStringFromStringNilIfEmpty(nodeProps.markerMid);
node.markerEnd = RCTNSStringFromStringNilIfEmpty(nodeProps.markerEnd);
node.clipPath = RCTNSStringFromStringNilIfEmpty(nodeProps.clipPath);
node.clipRule = nodeProps.clipRule == 0 ? kRNSVGCGFCRuleEvenodd : kRNSVGCGFCRuleNonzero;
node.responsible = nodeProps.responsible;
// onLayout
node.display = RCTNSStringFromStringNilIfEmpty(nodeProps.display);
std::string pointerEvents = nodeProps.pointerEvents;
NSString *pointerEventsString = RCTNSStringFromStringNilIfEmpty(pointerEvents);
if ([pointerEventsString isEqualToString:@"auto"]) {
node.pointerEvents = RCTPointerEventsUnspecified;
} else if ([pointerEventsString isEqualToString:@"none"]) {
node.pointerEvents = RCTPointerEventsNone;
} else if ([pointerEventsString isEqualToString:@"box-none"]) {
node.pointerEvents = RCTPointerEventsNone;
} else if ([pointerEventsString isEqualToString:@"box-only"]) {
node.pointerEvents = RCTPointerEventsNone;
} else {
node.pointerEvents = RCTPointerEventsUnspecified;
}
node.accessibilityIdentifier = RCTNSStringFromStringNilIfEmpty(nodeProps.testId);
node.isAccessibilityElement = nodeProps.accessible;
node.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(nodeProps.accessibilityLabel);
}
template <typename T>
void setCommonRenderableProps(const T &renderableProps, RNSVGRenderable *renderableNode)
{
setCommonNodeProps(renderableProps, renderableNode);
renderableNode.fill = brushFromColorStruct(renderableProps.fill);
renderableNode.fillOpacity = renderableProps.fillOpacity;
renderableNode.fillRule = renderableProps.fillRule == 0 ? kRNSVGCGFCRuleEvenodd : kRNSVGCGFCRuleNonzero;
renderableNode.stroke = brushFromColorStruct(renderableProps.stroke);
renderableNode.strokeOpacity = renderableProps.strokeOpacity;
id strokeWidth = RNSVGConvertFollyDynamicToId(renderableProps.strokeWidth);
if (strokeWidth != nil) {
renderableNode.strokeWidth = [RCTConvert RNSVGLength:strokeWidth];
}
renderableNode.strokeLinecap = renderableProps.strokeLinecap == 0 ? kCGLineCapButt
: renderableProps.strokeLinecap == 1 ? kCGLineCapRound
: kCGLineCapSquare;
renderableNode.strokeLinejoin = renderableProps.strokeLinejoin == 0 ? kCGLineJoinMiter
: renderableProps.strokeLinejoin == 1 ? kCGLineJoinRound
: kCGLineJoinBevel;
id strokeDasharray = RNSVGConvertFollyDynamicToId(renderableProps.strokeDasharray);
if (strokeDasharray != nil) {
renderableNode.strokeDasharray = [RCTConvert RNSVGLengthArray:strokeDasharray];
}
renderableNode.strokeDashoffset = renderableProps.strokeDashoffset;
renderableNode.strokeMiterlimit = renderableProps.strokeMiterlimit;
renderableNode.vectorEffect = renderableProps.vectorEffect == 0 ? kRNSVGVectorEffectDefault
: renderableProps.vectorEffect == 1 ? kRNSVGVectorEffectNonScalingStroke
: renderableProps.vectorEffect == 2 ? kRNSVGVectorEffectInherit
: kRNSVGVectorEffectUri;
if (renderableProps.propList.size() > 0) {
NSMutableArray<NSString *> *propArray = [NSMutableArray new];
for (auto str : renderableProps.propList) {
[propArray addObject:RCTNSStringFromString(str)];
}
renderableNode.propList = propArray;
}
renderableNode.filter = RCTNSStringFromStringNilIfEmpty(renderableProps.filter);
}
template <typename T>
void setCommonGroupProps(const T &groupProps, RNSVGGroup *groupNode)
{
setCommonRenderableProps(groupProps, groupNode);
id fontSize = RNSVGConvertFollyDynamicToId(groupProps.fontSize);
if (fontSize != nil) {
groupNode.font = @{@"fontSize" : fontSize};
}
id fontWeight = RNSVGConvertFollyDynamicToId(groupProps.fontWeight);
if (fontWeight != nil) {
groupNode.font = @{@"fontWeight" : fontWeight};
}
id font = RNSVGConvertFollyDynamicToId(groupProps.font);
if (font != nil) {
NSDictionary *fontDict = (NSDictionary *)font;
if (groupNode.font == nil || fontDict.count > 0) {
// some of text's rendering logic requires that `font` is not nil so we always set it
// even if to an empty dict
groupNode.font = fontDict;
}
}
}
template <typename T>
void setCommonFilterProps(const T &filterProps, RNSVGFilterPrimitive *filterPrimitiveNode)
{
id x = RNSVGConvertFollyDynamicToId(filterProps.x);
if (x != nil) {
filterPrimitiveNode.x = [RCTConvert RNSVGLength:x];
}
id y = RNSVGConvertFollyDynamicToId(filterProps.y);
if (y != nil) {
filterPrimitiveNode.y = [RCTConvert RNSVGLength:y];
}
id height = RNSVGConvertFollyDynamicToId(filterProps.height);
if (height != nil) {
filterPrimitiveNode.height = [RCTConvert RNSVGLength:height];
}
id width = RNSVGConvertFollyDynamicToId(filterProps.width);
if (width != nil) {
filterPrimitiveNode.width = [RCTConvert RNSVGLength:width];
}
filterPrimitiveNode.result = RCTNSStringFromStringNilIfEmpty(filterProps.result);
}
template <typename T>
void setCommonTextProps(const T &textProps, RNSVGText *textNode)
{
setCommonGroupProps(textProps, textNode);
id deltaX = RNSVGConvertFollyDynamicToId(textProps.dx);
if (deltaX != nil) {
textNode.deltaX = [RCTConvert RNSVGLengthArray:deltaX];
}
id deltaY = RNSVGConvertFollyDynamicToId(textProps.dy);
if (deltaY != nil) {
textNode.deltaY = [RCTConvert RNSVGLengthArray:deltaY];
}
id positionX = RNSVGConvertFollyDynamicToId(textProps.x);
if (positionX != nil) {
textNode.positionX = [RCTConvert RNSVGLengthArray:positionX];
}
id positionY = RNSVGConvertFollyDynamicToId(textProps.y);
if (positionY != nil) {
textNode.positionY = [RCTConvert RNSVGLengthArray:positionY];
}
id rotate = RNSVGConvertFollyDynamicToId(textProps.rotate);
if (rotate != nil) {
textNode.rotate = [RCTConvert RNSVGLengthArray:rotate];
}
id textLength = RNSVGConvertFollyDynamicToId(textProps.textLength);
if (textLength != nil) {
textNode.textLength = [RCTConvert RNSVGLength:textLength];
}
id inlineSize = RNSVGConvertFollyDynamicToId(textProps.inlineSize);
if (inlineSize != nil) {
textNode.inlineSize = [RCTConvert RNSVGLength:inlineSize];
}
id baselineShift = RNSVGConvertFollyDynamicToId(textProps.baselineShift);
if (baselineShift != nil) {
if ([baselineShift isKindOfClass:[NSString class]]) {
NSString *stringValue = (NSString *)baselineShift;
textNode.baselineShift = stringValue;
} else {
textNode.baselineShift = [NSString stringWithFormat:@"%f", [baselineShift doubleValue]];
}
}
textNode.lengthAdjust = RCTNSStringFromStringNilIfEmpty(textProps.lengthAdjust);
textNode.alignmentBaseline = RCTNSStringFromStringNilIfEmpty(textProps.alignmentBaseline);
}
static RNSVGVBMOS intToRNSVGVBMOS(int value)
{
switch (value) {
case 0:
return kRNSVGVBMOSMeet;
case 1:
return kRNSVGVBMOSSlice;
case 2:
return kRNSVGVBMOSNone;
default:
return kRNSVGVBMOSMeet;
}
}