diff --git a/USAGE.md b/USAGE.md index 6e9b21557..ced94cd87 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1307,6 +1307,7 @@ The following filters have been implemented: - FeFlood - FeGaussianBlur - FeMerge +- FeMorphology - FeOffset Not supported yet: @@ -1320,7 +1321,6 @@ Not supported yet: - FeFuncG - FeFuncR - FeImage -- FeMorphology - FePointLight - FeSpecularLighting - FeSpotLight diff --git a/android/src/main/java/com/horcrux/svg/FeMorphologyView.java b/android/src/main/java/com/horcrux/svg/FeMorphologyView.java new file mode 100644 index 000000000..adb5de697 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/FeMorphologyView.java @@ -0,0 +1,159 @@ +package com.horcrux.svg; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.Color; +import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.ReactContext; +import java.util.ArrayList; +import java.util.HashMap; + +@SuppressLint("ViewConstructor") +class FeMorphologyView extends FilterPrimitiveView { + String mIn1; + FilterProperties.FeMorphologyOperator mOperator; + ArrayList mRadius; + + public FeMorphologyView(ReactContext reactContext) { + super(reactContext); + } + + public void setIn1(String in1) { + this.mIn1 = in1; + invalidate(); + } + + public void setOperator1(String operator1) { + this.mOperator = FilterProperties.FeMorphologyOperator.getEnum(operator1); + invalidate(); + } + + public void setRadius(Dynamic radius) { + this.mRadius = SVGLength.arrayFrom(radius); + invalidate(); + } + + @Override + public Bitmap applyFilter(HashMap resultsMap, Bitmap prevResult) { + Bitmap source = getSource(resultsMap, prevResult, this.mIn1); + int width = source.getWidth(); + int height = source.getHeight(); + Bitmap result = + Bitmap.createBitmap(prevResult.getWidth(), prevResult.getHeight(), Bitmap.Config.ARGB_8888); + + int[] srcPixels = new int[width * height]; + source.getPixels(srcPixels, 0, width, 0, 0, width, height); + int[] dstPixels = this.applyMorphology(srcPixels, width, height); + + result.setPixels(dstPixels, 0, width, 0, 0, width, height); + return result; + } + + private int[] applyMorphology(int[] srcPixels, int width, int height) { + int[] dstPixels = new int[width * height]; + int[] tmpPixels = new int[width * height]; + boolean isErode = this.mOperator == FilterProperties.FeMorphologyOperator.ERODE; + int radiusX = this.getRadiusX(); + int radiusY = this.getRadiusY(); + + if (radiusX == 0 && radiusY == 0) { + return srcPixels; + } + + horizontalPass(srcPixels, tmpPixels, width, height, radiusX, isErode); + verticalPass(tmpPixels, dstPixels, width, height, radiusY, isErode); + + return dstPixels; + } + + private int getRadiusX() { + if (this.mRadius != null && !this.mRadius.isEmpty()) { + return (int) Math.max(0, this.mRadius.get(0).value); + } + + return 0; + } + + private int getRadiusY() { + if (this.mRadius != null) { + if (this.mRadius.size() == 1) { + return this.getRadiusX(); + } + + if (this.mRadius.size() == 2) { + return (int) Math.max(0, this.mRadius.get(1).value); + } + } + + return 0; + } + + private void horizontalPass( + int[] src, int[] dst, int width, int height, int radiusX, boolean isErode) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + PixelRange range = new PixelRange(); + + int startX = Math.max(0, x - radiusX); + int endX = Math.min(width - 1, x + radiusX); + + for (int nx = startX; nx <= endX; nx++) { + int c = src[y * width + nx]; + range.include(c); + } + + dst[y * width + x] = isErode ? range.minColor() : range.maxColor(); + } + } + } + + private void verticalPass( + int[] src, int[] dst, int width, int height, int radiusY, boolean isErode) { + for (int y = 0; y < height; y++) { + int startY = Math.max(0, y - radiusY); + int endY = Math.min(height - 1, y + radiusY); + + for (int x = 0; x < width; x++) { + PixelRange range = new PixelRange(); + + for (int ny = startY; ny <= endY; ny++) { + int c = src[ny * width + x]; + range.include(c); + } + + dst[y * width + x] = isErode ? range.minColor() : range.maxColor(); + } + } + } + + /** Helper to track min and max RGBA values while scanning neighbors. */ + private static class PixelRange { + int minR = 255, minG = 255, minB = 255, minA = 255; + int maxR = 0, maxG = 0, maxB = 0, maxA = 0; + + void include(int color) { + int A = Color.alpha(color); + int R = Color.red(color); + int G = Color.green(color); + int B = Color.blue(color); + + if (R < minR) minR = R; + if (G < minG) minG = G; + if (B < minB) minB = B; + if (A < minA) minA = A; + + if (R > maxR) maxR = R; + if (G > maxG) maxG = G; + if (B > maxB) maxB = B; + if (A > maxA) maxA = A; + } + + int minColor() { + return Color.argb(minA, minR, minG, minB); + } + + int maxColor() { + return Color.argb(maxA, maxR, maxG, maxB); + } + } +} diff --git a/android/src/main/java/com/horcrux/svg/FilterProperties.java b/android/src/main/java/com/horcrux/svg/FilterProperties.java index fbd17c059..002433f14 100644 --- a/android/src/main/java/com/horcrux/svg/FilterProperties.java +++ b/android/src/main/java/com/horcrux/svg/FilterProperties.java @@ -181,4 +181,36 @@ public String toString() { return type; } } + + enum FeMorphologyOperator { + ERODE("erode"), + DILATE("dilate"); + + private final String operator; + + FeMorphologyOperator(String operator) { + this.operator = operator; + } + + static FeMorphologyOperator getEnum(String strVal) { + if (!typeToEnum.containsKey(strVal)) { + throw new IllegalArgumentException("Unknown String Value: " + strVal); + } + return typeToEnum.get(strVal); + } + + private static final Map typeToEnum = new HashMap<>(); + + static { + for (final FeMorphologyOperator en : FeMorphologyOperator.values()) { + typeToEnum.put(en.operator, en); + } + } + + @Nonnull + @Override + public String toString() { + return operator; + } + } } diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index 06095e45f..ebc016a24 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -105,6 +105,8 @@ import com.facebook.react.viewmanagers.RNSVGFeGaussianBlurManagerInterface; import com.facebook.react.viewmanagers.RNSVGFeMergeManagerDelegate; import com.facebook.react.viewmanagers.RNSVGFeMergeManagerInterface; +import com.facebook.react.viewmanagers.RNSVGFeMorphologyManagerDelegate; +import com.facebook.react.viewmanagers.RNSVGFeMorphologyManagerInterface; import com.facebook.react.viewmanagers.RNSVGFeOffsetManagerDelegate; import com.facebook.react.viewmanagers.RNSVGFeOffsetManagerInterface; import com.facebook.react.viewmanagers.RNSVGFilterManagerDelegate; @@ -487,6 +489,7 @@ protected enum SVGClass { RNSVGFeFlood, RNSVGFeGaussianBlur, RNSVGFeMerge, + RNSVGFeMorphology, RNSVGFeOffset, RNSVGMarker, RNSVGForeignObject, @@ -546,6 +549,8 @@ protected VirtualView createViewInstance(@Nonnull ThemedReactContext reactContex return new FeGaussianBlurView(reactContext); case RNSVGFeMerge: return new FeMergeView(reactContext); + case RNSVGFeMorphology: + return new FeMorphologyView(reactContext); case RNSVGFeOffset: return new FeOffsetView(reactContext); case RNSVGMarker: @@ -1651,6 +1656,31 @@ public void setNodes(FeMergeView node, ReadableArray nodes) { } } + static class FeMorphologyManager extends FilterPrimitiveManager + implements RNSVGFeMorphologyManagerInterface { + FeMorphologyManager() { + super(SVGClass.RNSVGFeMorphology); + mDelegate = new RNSVGFeMorphologyManagerDelegate(this); + } + + public static final String REACT_CLASS = "RNSVGFeMorphology"; + + @ReactProp(name = "in1") + public void setIn1(FeMorphologyView node, String in1) { + node.setIn1(in1); + } + + @ReactProp(name = "operator1") + public void setOperator1(FeMorphologyView node, String operator1) { + node.setOperator1(operator1); + } + + @ReactProp(name = "radius") + public void setRadius(FeMorphologyView node, Dynamic radius) { + node.setRadius(radius); + } + } + static class FeOffsetManager extends FilterPrimitiveManager implements RNSVGFeOffsetManagerInterface { FeOffsetManager() { diff --git a/android/src/main/java/com/horcrux/svg/SvgPackage.java b/android/src/main/java/com/horcrux/svg/SvgPackage.java index fe81f02c3..717dd58c4 100644 --- a/android/src/main/java/com/horcrux/svg/SvgPackage.java +++ b/android/src/main/java/com/horcrux/svg/SvgPackage.java @@ -268,6 +268,15 @@ public NativeModule get() { return new FeMergeManager(); } })); + specs.put( + FeMorphologyManager.REACT_CLASS, + ModuleSpec.viewManagerSpec( + new Provider() { + @Override + public NativeModule get() { + return new FeMorphologyManager(); + } + })); specs.put( FeOffsetManager.REACT_CLASS, ModuleSpec.viewManagerSpec( diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeMorphologyManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeMorphologyManagerDelegate.java new file mode 100644 index 000000000..fee125b27 --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeMorphologyManagerDelegate.java @@ -0,0 +1,54 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaDelegate.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; +import androidx.annotation.Nullable; +import com.facebook.react.bridge.DynamicFromObject; +import com.facebook.react.uimanager.BaseViewManager; +import com.facebook.react.uimanager.BaseViewManagerDelegate; +import com.facebook.react.uimanager.LayoutShadowNode; + +public class RNSVGFeMorphologyManagerDelegate & RNSVGFeMorphologyManagerInterface> extends BaseViewManagerDelegate { + public RNSVGFeMorphologyManagerDelegate(U viewManager) { + super(viewManager); + } + @Override + public void setProperty(T view, String propName, @Nullable Object value) { + switch (propName) { + case "x": + mViewManager.setX(view, new DynamicFromObject(value)); + break; + case "y": + mViewManager.setY(view, new DynamicFromObject(value)); + break; + case "width": + mViewManager.setWidth(view, new DynamicFromObject(value)); + break; + case "height": + mViewManager.setHeight(view, new DynamicFromObject(value)); + break; + case "result": + mViewManager.setResult(view, value == null ? null : (String) value); + break; + case "in1": + mViewManager.setIn1(view, value == null ? null : (String) value); + break; + case "operator1": + mViewManager.setOperator1(view, (String) value); + break; + case "radius": + mViewManager.setRadius(view, new DynamicFromObject(value)); + break; + default: + super.setProperty(view, propName, value); + } + } +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeMorphologyManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeMorphologyManagerInterface.java new file mode 100644 index 000000000..6dfe06f23 --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeMorphologyManagerInterface.java @@ -0,0 +1,25 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaInterface.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; +import androidx.annotation.Nullable; +import com.facebook.react.bridge.Dynamic; + +public interface RNSVGFeMorphologyManagerInterface { + void setX(T view, Dynamic value); + void setY(T view, Dynamic value); + void setWidth(T view, Dynamic value); + void setHeight(T view, Dynamic value); + void setResult(T view, @Nullable String value); + void setIn1(T view, @Nullable String value); + void setOperator1(T view, @Nullable String value); + void setRadius(T view, Dynamic value); +} diff --git a/apple/Filters/RNSVGFEMorphology.h b/apple/Filters/RNSVGFEMorphology.h new file mode 100644 index 000000000..318874e00 --- /dev/null +++ b/apple/Filters/RNSVGFEMorphology.h @@ -0,0 +1,10 @@ +#import "RNSVGMorphologyOperator.h" +#import "RNSVGFilterPrimitive.h" + +@interface RNSVGFeMorphology : RNSVGFilterPrimitive + +@property (nonatomic, strong) NSString *in1; +@property (nonatomic, assign) RNSVGMorphologyOperator operator1; +@property (nonatomic, strong) NSArray *radius; + +@end diff --git a/apple/Filters/RNSVGFEMorphology.mm b/apple/Filters/RNSVGFEMorphology.mm new file mode 100644 index 000000000..42ac4d266 --- /dev/null +++ b/apple/Filters/RNSVGFEMorphology.mm @@ -0,0 +1,269 @@ +#import "RNSVGFeMorphology.h" + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import "RNSVGConvert.h" +#import "RNSVGFabricConversions.h" +#endif // RCT_NEW_ARCH_ENABLED + +@implementation RNSVGFeMorphology + +#ifdef RCT_NEW_ARCH_ENABLED +using namespace facebook::react; + +// Needed because of this: https://github.com/facebook/react-native/pull/37274 ++ (void)load +{ + [super load]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + } + return self; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + const auto &newProps = static_cast(*props); + + self.in1 = RCTNSStringFromStringNilIfEmpty(newProps.in1); + self.operator1 = [RNSVGConvert RNSVGMorphologyOperatorFromCppEquivalent:newProps.operator1]; + + id radius = RNSVGConvertFollyDynamicToId(newProps.radius); + if (radius != nil) { + self.radius = [RCTConvert RNSVGLengthArray:radius]; + } + + setCommonFilterProps(newProps, self); + _props = std::static_pointer_cast(props); +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + _in1 = nil; + _operator1 = RNSVGMorphologyOperator::SVG_FEMORPHOLOGY_OPERATOR_ERODE; + _radius = @[[RNSVGLength lengthWithNumber:0]]; +} +#endif // RCT_NEW_ARCH_ENABLED + +- (void)setIn1:(NSString *)in1 +{ + if ([in1 isEqualToString:_in1]) { + return; + } + + _in1 = in1; + [self invalidate]; +} + +- (void)setOperator1:(RNSVGMorphologyOperator)operator1 +{ + if (operator1 == _operator1) { + return; + } + _operator1 = operator1; + [self invalidate]; +} + +- (void)setRadius:(NSArray *)radius +{ + if (radius == _radius) { + return; + } + + _radius = radius; + [self invalidate]; +} + +- (CIImage *)applyFilter:(NSMutableDictionary *)results previousFilterResult:(CIImage *)previous +{ + CIImage *inResults1 = self.in1 ? [results objectForKey:self.in1] : nil; + CIImage *inputImage1 = inResults1 ? inResults1 : previous; + CIContext *ciContext = [CIContext contextWithOptions:nil]; + CGImageRef cgImage = [ciContext createCGImage:inputImage1 fromRect:inputImage1.extent]; + + size_t width = CGImageGetWidth(cgImage), height = CGImageGetHeight(cgImage); + uint32_t *pixels = (uint32_t *)malloc(width * height * sizeof(uint32_t)); + + if (!pixels) { + CGImageRelease(cgImage); + return inputImage1; + } + + // Create RGBA bitmap context + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate( + pixels, + width, + height, + 8, + width * 4, + colorSpace, + (CGBitmapInfo)kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast + ); + CGColorSpaceRelease(colorSpace); + + // Draw CGImage into context (fills pixels buffer) + CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); + CGImageRelease(cgImage); + + [self applyMorphology:pixels width:width height:height]; + + CGImageRef outputCGImage = CGBitmapContextCreateImage(context); + CIImage *outputCIImage = [CIImage imageWithCGImage:outputCGImage]; + + CGImageRelease(outputCGImage); + CGContextRelease(context); + free(pixels); + + return outputCIImage; +} + +- (void)applyMorphology:(uint32_t *)srcPixels width:(int)width height:(int)height +{ + int total = width * height; + uint32_t *tmpPixels = (uint32_t *)malloc(total * sizeof(uint32_t)); + uint32_t *dstPixels = (uint32_t *)malloc(total * sizeof(uint32_t)); + int radiusX = self.getRadiusX; + int radiusY = self.getRadiusY; + bool isErode = self.operator1 == SVG_FEMORPHOLOGY_OPERATOR_ERODE; + + if (radiusX == 0 && radiusY == 0) { + return; + } + + [self horizontalPass:srcPixels dst:tmpPixels width:width height:height radiusX:radiusX isErode:isErode]; + [self verticalPass:tmpPixels dst:dstPixels width:width height:height radiusY:radiusY isErode:isErode]; + + memcpy(srcPixels, dstPixels, total * sizeof(uint32_t)); + + free(tmpPixels); + free(dstPixels); +} + +-(NSInteger) getRadiusX +{ + if (self.radius != nil && self.radius.count == 1) { + return MAX(0, (int) self.radius[0].value); + } + + return 0; +} + +-(NSInteger) getRadiusY +{ + if (self.radius != nil) { + if (self.radius.count == 1) { + return self.getRadiusX; + } + + if (self.radius.count == 2) { + return MAX(0, (int) self.radius[1].value); + } + } + + return 0; +} + +- (void)horizontalPass:(uint32_t *)src dst:(uint32_t *)dst + width:(int)width height:(int)height + radiusX:(int)radiusX isErode:(BOOL)isErode { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + PixelRange range; + PixelRangeInit(&range); + + int startX = MAX(0, x - radiusX); + int endX = MIN(width - 1, x + radiusX); + + for (int nx = startX; nx <= endX; nx++) { + PixelRangeInclude(&range, src[y * width + nx]); + } + + dst[y * width + x] = PixelRangeResult(&range, isErode); + } + } +} + +- (void)verticalPass:(uint32_t *)src dst:(uint32_t *)dst + width:(int)width height:(int)height + radiusY:(int)radiusY isErode:(BOOL)isErode { + for (int y = 0; y < height; y++) { + int startY = MAX(0, y - radiusY); + int endY = MIN(height - 1, y + radiusY); + + for (int x = 0; x < width; x++) { + PixelRange range; + PixelRangeInit(&range); + + for (int ny = startY; ny <= endY; ny++) { + PixelRangeInclude(&range, src[ny * width + x]); + } + + dst[y * width + x] = PixelRangeResult(&range, isErode); + } + } +} + +typedef struct { + int minR, minG, minB, minA; + int maxR, maxG, maxB, maxA; +} PixelRange; + +static inline void PixelRangeInit(PixelRange *range) { + range->minR = range->minG = range->minB = range->minA = 255; + range->maxR = range->maxG = range->maxB = range->maxA = 0; +} + +static inline void PixelRangeInclude(PixelRange *range, uint32_t c) { + int A = (c >> 24) & 0xFF; + int R = (c >> 16) & 0xFF; + int G = (c >> 8) & 0xFF; + int B = (c >> 0) & 0xFF; + + if (R < range->minR) range->minR = R; + if (G < range->minG) range->minG = G; + if (B < range->minB) range->minB = B; + if (A < range->minA) range->minA = A; + + if (R > range->maxR) range->maxR = R; + if (G > range->maxG) range->maxG = G; + if (B > range->maxB) range->maxB = B; + if (A > range->maxA) range->maxA = A; +} + +static inline uint32_t PixelRangeResult(const PixelRange *range, BOOL isErode) { + int R = isErode ? range->minR : range->maxR; + int G = isErode ? range->minG : range->maxG; + int B = isErode ? range->minB : range->maxB; + int A = isErode ? range->minA : range->maxA; + + return ((A & 0xFF) << 24) | + ((R & 0xFF) << 16) | + ((G & 0xFF) << 8) | + ((B & 0xFF)); +} + +#ifdef RCT_NEW_ARCH_ENABLED +Class RNSVGFeMorphologyCls(void) +{ + return RNSVGFeMorphology.class; +} +#endif // RCT_NEW_ARCH_ENABLED + +@end diff --git a/apple/Filters/RNSVGMorphologyOperator.h b/apple/Filters/RNSVGMorphologyOperator.h new file mode 100644 index 000000000..7bf26e6f4 --- /dev/null +++ b/apple/Filters/RNSVGMorphologyOperator.h @@ -0,0 +1,6 @@ +#import + +typedef CF_ENUM(NSInteger, RNSVGMorphologyOperator) { + SVG_FEMORPHOLOGY_OPERATOR_ERODE, + SVG_FEMORPHOLOGY_OPERATOR_DILATE, +}; diff --git a/apple/Utils/RCTConvert+RNSVG.h b/apple/Utils/RCTConvert+RNSVG.h index 5f835292f..54c459d5b 100644 --- a/apple/Utils/RCTConvert+RNSVG.h +++ b/apple/Utils/RCTConvert+RNSVG.h @@ -17,6 +17,7 @@ #import "RNSVGEdgeMode.h" #import "RNSVGLength.h" #import "RNSVGMaskType.h" +#import "RNSVGMorphologyOperator.h" #import "RNSVGPathParser.h" #import "RNSVGUnits.h" #import "RNSVGVBMOS.h" diff --git a/apple/Utils/RCTConvert+RNSVG.mm b/apple/Utils/RCTConvert+RNSVG.mm index b65cae7d6..6d5321c5c 100644 --- a/apple/Utils/RCTConvert+RNSVG.mm +++ b/apple/Utils/RCTConvert+RNSVG.mm @@ -98,6 +98,15 @@ @implementation RCTConvert (RNSVG) SVG_FECOMPOSITE_OPERATOR_UNKNOWN, intValue) +RCT_ENUM_CONVERTER( + RNSVGMorphologyOperator, + (@{ + @"erode" : @(SVG_FEMORPHOLOGY_OPERATOR_ERODE), + @"dilate" : @(SVG_FEMORPHOLOGY_OPERATOR_DILATE), + }), + SVG_FEMORPHOLOGY_OPERATOR_ERODE, + intValue) + + (RNSVGBrush *)RNSVGBrush:(id)json { if (json == nil) { diff --git a/apple/Utils/RNSVGConvert.h b/apple/Utils/RNSVGConvert.h index 320007c43..f87107e46 100644 --- a/apple/Utils/RNSVGConvert.h +++ b/apple/Utils/RNSVGConvert.h @@ -4,6 +4,7 @@ #import "RNSVGColorMatrixType.h" #import "RNSVGCompositeOperator.h" #import "RNSVGEdgeMode.h" +#import "RNSVGMorphologyOperator.h" #import "RNSVGUnits.h" namespace react = facebook::react; @@ -16,6 +17,7 @@ namespace react = facebook::react; + (RNSVGColorMatrixType)RNSVGColorMatrixTypeFromCppEquivalent:(react::RNSVGFeColorMatrixType)type; + (RNSVGCompositeOperator)RNSVGRNSVGCompositeOperatorFromCppEquivalent:(react::RNSVGFeCompositeOperator1)operator1; + (RNSVGEdgeMode)RNSVGEdgeModeFromCppEquivalent:(react::RNSVGFeGaussianBlurEdgeMode)edgeMode; ++ (RNSVGMorphologyOperator)RNSVGMorphologyOperatorFromCppEquivalent:(react::RNSVGFeMorphologyOperator1)operator1; @end diff --git a/apple/Utils/RNSVGConvert.mm b/apple/Utils/RNSVGConvert.mm index 967521491..d578fd7c9 100644 --- a/apple/Utils/RNSVGConvert.mm +++ b/apple/Utils/RNSVGConvert.mm @@ -87,6 +87,18 @@ + (RNSVGEdgeMode)RNSVGEdgeModeFromCppEquivalent:(react::RNSVGFeGaussianBlurEdgeM } } ++ (RNSVGMorphologyOperator)RNSVGMorphologyOperatorFromCppEquivalent:(react::RNSVGFeMorphologyOperator1)operator1 +{ + switch (operator1) { + case react::RNSVGFeMorphologyOperator1::Erode: + return SVG_FEMORPHOLOGY_OPERATOR_ERODE; + case react::RNSVGFeMorphologyOperator1::Dilate: + return SVG_FEMORPHOLOGY_OPERATOR_DILATE; + default: + return SVG_FEMORPHOLOGY_OPERATOR_ERODE; + } +} + @end #endif // RCT_NEW_ARCH_ENABLED diff --git a/apple/ViewManagers/RNSVGFeMorphologyManager.h b/apple/ViewManagers/RNSVGFeMorphologyManager.h new file mode 100644 index 000000000..93d151a6c --- /dev/null +++ b/apple/ViewManagers/RNSVGFeMorphologyManager.h @@ -0,0 +1,5 @@ +#import "RNSVGFilterPrimitiveManager.h" + +@interface RNSVGFeMorphologyManager : RNSVGFilterPrimitiveManager + +@end diff --git a/apple/ViewManagers/RNSVGFeMorphologyManager.mm b/apple/ViewManagers/RNSVGFeMorphologyManager.mm new file mode 100644 index 000000000..5e7469fe1 --- /dev/null +++ b/apple/ViewManagers/RNSVGFeMorphologyManager.mm @@ -0,0 +1,18 @@ +#import "RNSVGFeMorphologyManager.h" +#import "RNSVGMorphologyOperator.h" +#import "RNSVGFeMorphology.h" + +@implementation RNSVGFeMorphologyManager + +RCT_EXPORT_MODULE() + +- (RNSVGFeMorphology *)node +{ + return [RNSVGFeMorphology new]; +} + +RCT_EXPORT_VIEW_PROPERTY(in1, NSString) +RCT_EXPORT_VIEW_PROPERTY(operator1, RNSVGMorphologyOperator) +RCT_EXPORT_VIEW_PROPERTY(radius, NSArray) + +@end diff --git a/apps/common/example/examples/Filters/FeMorphology.tsx b/apps/common/example/examples/Filters/FeMorphology.tsx new file mode 100644 index 000000000..e56e36836 --- /dev/null +++ b/apps/common/example/examples/Filters/FeMorphology.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import {FeMorphology, Filter, G, Rect, Svg, Text} from 'react-native-svg'; + +function MDNExample() { + return ( + + + + + + + + + Normal Text + + + Thinned Text + + + Fattened Text + + + ); +} +MDNExample.title = 'MDN Example'; + +function W3CExample() { + return ( + + + + + + + + + + + + + + + + + Unfiltered + + + Erode radius 3 + + + Erode radius 6 + + + Dilate radius 3 + + + Dilate radius 6 + + + + ); +} +W3CExample.title = 'W3C Example'; + +const icon = ( + + + + + + + + + Dilate + + + Erode + + +); +const samples = [MDNExample, W3CExample]; + +export {icon, samples}; diff --git a/apps/common/example/examples/Filters/index.tsx b/apps/common/example/examples/Filters/index.tsx index 36069573c..f8efb87ea 100644 --- a/apps/common/example/examples/Filters/index.tsx +++ b/apps/common/example/examples/Filters/index.tsx @@ -7,6 +7,7 @@ import * as FeDropShadow from './FeDropShadow'; import * as FeFlood from './FeFlood'; import * as FeGaussianBlur from './FeGaussianBlur'; import * as FeMerge from './FeMerge'; +import * as FeMorphology from './FeMorphology'; import * as FeOffset from './FeOffset'; import * as ReanimatedFeColorMatrix from './ReanimatedFeColorMatrix'; @@ -18,6 +19,7 @@ const samples = { FeFlood, FeGaussianBlur, FeMerge, + FeMorphology, FeOffset, ReanimatedFeColorMatrix, }; diff --git a/common/cpp/react/renderer/components/rnsvg/RNSVGComponentDescriptors.h b/common/cpp/react/renderer/components/rnsvg/RNSVGComponentDescriptors.h index 61c44e7a6..467793eaa 100644 --- a/common/cpp/react/renderer/components/rnsvg/RNSVGComponentDescriptors.h +++ b/common/cpp/react/renderer/components/rnsvg/RNSVGComponentDescriptors.h @@ -26,6 +26,8 @@ using RNSVGFeGaussianBlurComponentDescriptor = ConcreteComponentDescriptor; using RNSVGFeMergeComponentDescriptor = ConcreteComponentDescriptor; +using RNSVGFeMorphologyComponentDescriptor = + ConcreteComponentDescriptor; using RNSVGFeOffsetComponentDescriptor = ConcreteComponentDescriptor; using RNSVGFilterComponentDescriptor = diff --git a/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.cpp b/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.cpp index 70f1abd43..a55e0b715 100644 --- a/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.cpp +++ b/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.cpp @@ -12,6 +12,7 @@ extern const char RNSVGFeCompositeComponentName[] = "RNSVGFeComposite"; extern const char RNSVGFeFloodComponentName[] = "RNSVGFeFlood"; extern const char RNSVGFeGaussianBlurComponentName[] = "RNSVGFeGaussianBlur"; extern const char RNSVGFeMergeComponentName[] = "RNSVGFeMerge"; +extern const char RNSVGFeMorphologyComponentName[] = "RNSVGFeMorphology"; extern const char RNSVGFeOffsetComponentName[] = "RNSVGFeOffset"; extern const char RNSVGFilterComponentName[] = "RNSVGFilter"; extern const char RNSVGForeignObjectComponentName[] = "RNSVGForeignObject"; diff --git a/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.h b/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.h index 71addc842..981d91121 100644 --- a/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.h +++ b/common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.h @@ -91,6 +91,14 @@ JSI_EXPORT extern const char RNSVGFeMergeComponentName[]; using RNSVGFeMergeShadowNode = RNSVGConcreteShadowNode; +JSI_EXPORT extern const char RNSVGFeMorphologyComponentName[]; + +/* + * `ShadowNode` for component. + */ +using RNSVGFeMorphologyShadowNode = + RNSVGConcreteShadowNode; + JSI_EXPORT extern const char RNSVGFeOffsetComponentName[]; /* diff --git a/package.json b/package.json index a058ce201..0947db038 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "RNSVGFeFlood": "RNSVGFeFlood", "RNSVGFeGaussianBlur": "RNSVGFeGaussianBlur", "RNSVGFeMerge": "RNSVGFeMerge", + "RNSVGFeMorphology": "RNSVGFeMorphology", "RNSVGFeOffset": "RNSVGFeOffset", "RNSVGFilter": "RNSVGFilter", "RNSVGForeignObject": "RNSVGForeignObject", diff --git a/react-native.config.js b/react-native.config.js index 96657a69c..3001eaeb7 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -12,6 +12,7 @@ module.exports = { 'RNSVGFeFloodComponentDescriptor', 'RNSVGFeGaussianBlurComponentDescriptor', 'RNSVGFeMergeComponentDescriptor', + 'RNSVGFeMorphologyComponentDescriptor', 'RNSVGFeOffsetComponentDescriptor', 'RNSVGFilterComponentDescriptor', 'RNSVGEllipseComponentDescriptor', diff --git a/src/elements/filters/FeMorphology.tsx b/src/elements/filters/FeMorphology.tsx index d9358f78e..755b30b61 100644 --- a/src/elements/filters/FeMorphology.tsx +++ b/src/elements/filters/FeMorphology.tsx @@ -1,10 +1,18 @@ import { NumberArray } from '../../lib/extract/types'; -import { warnUnimplementedFilter } from '../../lib/util'; +import RNSVGFeMorphology from '../../fabric/FeMorphologyNativeComponent'; import FilterPrimitive from './FilterPrimitive'; +import { + extractFeMorphology, + extractFilter, + extractIn, +} from '../../lib/extract/extractFilter'; +import { NativeMethods } from 'react-native'; + +type Operator = 'erode' | 'dilate'; export interface FeMorphologyProps { in?: string; - operator?: 'erode' | 'dilate'; + operator?: Operator; radius?: NumberArray; } @@ -13,10 +21,20 @@ export default class FeMorphology extends FilterPrimitive { static defaultProps = { ...this.defaultPrimitiveProps, + operator: 'erode', + radius: 0, }; render() { - warnUnimplementedFilter(); - return null; + return ( + + this.refMethod(ref as (FeMorphology & NativeMethods) | null) + } + {...extractFilter(this.props)} + {...extractIn(this.props)} + {...extractFeMorphology(this.props)} + /> + ); } } diff --git a/src/fabric/FeMorphologyNativeComponent.ts b/src/fabric/FeMorphologyNativeComponent.ts new file mode 100644 index 000000000..0a360378d --- /dev/null +++ b/src/fabric/FeMorphologyNativeComponent.ts @@ -0,0 +1,26 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { ViewProps } from './utils'; + +import { NumberArray, NumberProp } from '../lib/extract/types'; +import type { UnsafeMixed } from './codegenUtils'; +import { WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; + +interface FilterPrimitiveCommonProps { + x?: UnsafeMixed; + y?: UnsafeMixed; + width?: UnsafeMixed; + height?: UnsafeMixed; + result?: string; +} + +type OperatorType = 'erode' | 'dilate'; + +export interface NativeProps extends ViewProps, FilterPrimitiveCommonProps { + in1?: string; + operator1?: WithDefault; + radius?: UnsafeMixed; +} + +export default codegenNativeComponent('RNSVGFeMorphology', { + interfaceOnly: true, +}); diff --git a/src/fabric/index.ts b/src/fabric/index.ts index 71147c7da..f3fe19082 100644 --- a/src/fabric/index.ts +++ b/src/fabric/index.ts @@ -27,6 +27,7 @@ import RNSVGFeComposite from './FeCompositeNativeComponent'; import RNSVGFeFlood from './FeFloodNativeComponent'; import RNSVGFeGaussianBlur from './FeGaussianBlurNativeComponent'; import RNSVGFeMerge from './FeMergeNativeComponent'; +import RNSVGFeMorphology from './FeMorphologyNativeComponent'; import RNSVGFeOffset from './FeOffsetNativeComponent'; export { @@ -59,5 +60,6 @@ export { RNSVGFeFlood, RNSVGFeGaussianBlur, RNSVGFeMerge, + RNSVGFeMorphology, RNSVGFeOffset, }; diff --git a/src/lib/extract/extractFilter.ts b/src/lib/extract/extractFilter.ts index e921f6c50..fe890cd9a 100644 --- a/src/lib/extract/extractFilter.ts +++ b/src/lib/extract/extractFilter.ts @@ -6,12 +6,14 @@ import { FeCompositeProps as FeCompositeComponentProps } from '../../elements/fi import { FeFloodProps as FeFloodComponentProps } from '../../elements/filters/FeFlood'; import { FeGaussianBlurProps as FeGaussianBlurComponentProps } from '../../elements/filters/FeGaussianBlur'; import { FeMergeProps as FeMergeComponentProps } from '../../elements/filters/FeMerge'; +import { FeMorphologyProps as FeMorphologyComponentProps } from '../../elements/filters/FeMorphology'; import { NativeProps as FeBlendNativeProps } from '../../fabric/FeBlendNativeComponent'; import { NativeProps as FeColorMatrixNativeProps } from '../../fabric/FeColorMatrixNativeComponent'; import { NativeProps as FeCompositeNativeProps } from '../../fabric/FeCompositeNativeComponent'; import { NativeProps as FeFloodNativeProps } from '../../fabric/FeFloodNativeComponent'; import { NativeProps as FeGaussianBlurNativeProps } from '../../fabric/FeGaussianBlurNativeComponent'; import { NativeProps as FeMergeNativeProps } from '../../fabric/FeMergeNativeComponent'; +import { NativeProps as FeMorphologyNativeProps } from '../../fabric/FeMorphologyNativeComponent'; import extractBrush from './extractBrush'; import extractOpacity from './extractOpacity'; import { NumberProp } from './types'; @@ -180,3 +182,21 @@ export const extractFeMerge = ( return { nodes }; }; + +export const extractFeMorphology = ( + props: FeMorphologyComponentProps +): FeMorphologyNativeProps => { + const extracted: FeMorphologyNativeProps = {}; + + if (props.in) { + extracted.in1 = props.in; + } + if (props.operator) { + extracted.operator1 = props.operator; + } + if (props.radius !== undefined) { + extracted.radius = props.radius; + } + + return extracted; +};