From 1fce1f74d273c397f9520d4afdc08c67fbc926df Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Wed, 12 Jun 2024 14:55:49 +0200 Subject: [PATCH 01/12] test: create Test1451 --- TestsExample/App.js | 1 + TestsExample/src/Test1451.tsx | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 TestsExample/src/Test1451.tsx diff --git a/TestsExample/App.js b/TestsExample/App.js index 4b665cbb5..ea8bfab9b 100644 --- a/TestsExample/App.js +++ b/TestsExample/App.js @@ -4,6 +4,7 @@ import React from 'react'; import ColorTest from './src/ColorTest'; import PointerEventsBoxNone from './src/PointerEventsBoxNone'; import Test1374 from './src/Test1374'; +import Test1451 from './src/Test1451'; import Test1718 from './src/Test1718'; import Test1813 from './src/Test1813'; import Test1845 from './src/Test1845'; diff --git a/TestsExample/src/Test1451.tsx b/TestsExample/src/Test1451.tsx new file mode 100644 index 000000000..70f324276 --- /dev/null +++ b/TestsExample/src/Test1451.tsx @@ -0,0 +1,51 @@ +import React, {useEffect, useRef} from 'react'; +import {Animated, View} from 'react-native'; +import {Circle, Mask, Path, Rect, Svg} from 'react-native-svg'; + +const AnimatedCircle = Animated.createAnimatedComponent(Circle as any); + +export default () => { + const zoom = useRef(new Animated.Value(1)).current; + + useEffect(() => { + Animated.loop( + Animated.sequence([ + Animated.timing(zoom, { + toValue: 2, + duration: 2000, + useNativeDriver: true, + }), + Animated.timing(zoom, { + toValue: 1, + duration: 2000, + useNativeDriver: true, + }), + ]), + ).start(); + }); + + return ( + + + + + + + + + + + + + ); +}; From 1690e4aa395b5a633a76bb8b9f5eecdc35e8a634 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Wed, 12 Jun 2024 14:58:08 +0200 Subject: [PATCH 02/12] feature: change android render logic to canvas layers instead of bitmaps --- .../java/com/horcrux/svg/RenderableView.java | 75 ++++++------------- 1 file changed, 23 insertions(+), 52 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index 316b19fba..24a23329d 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -8,8 +8,9 @@ package com.horcrux.svg; -import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.DashPathEffect; import android.graphics.Matrix; import android.graphics.Paint; @@ -329,10 +330,6 @@ public void setPropList(@Nullable ReadableArray propList) { invalidate(); } - private static double saturate(double v) { - return v <= 0 ? 0 : (v >= 1 ? 1 : v); - } - void render(Canvas canvas, Paint paint, float opacity) { MaskView mask = null; if (mMask != null) { @@ -340,60 +337,34 @@ void render(Canvas canvas, Paint paint, float opacity) { mask = (MaskView) root.getDefinedMask(mMask); } if (mask != null) { - Rect clipBounds = canvas.getClipBounds(); - int height = clipBounds.height(); - int width = clipBounds.width(); - - Bitmap maskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Bitmap original = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + // draw element to new layer + canvas.saveLayer(null, paint); + draw(canvas, paint, opacity); - Canvas originalCanvas = new Canvas(original); - Canvas maskCanvas = new Canvas(maskBitmap); - Canvas resultCanvas = new Canvas(result); + // prepare maskPaint with luminanceToAlpha + PorterDuffXfermode and create new layer with it + Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + // luminanceToAlpha conversion TODO: use filters + ColorMatrix luminanceToAlpha = + new ColorMatrix( + new float[] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2125f, 0.7154f, 0.0721f, 0, 0 + }); + maskPaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha)); + maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + canvas.saveLayer(null, maskPaint); - // Clip to mask bounds and render the mask + // clip to mask bounds and render the mask float maskX = (float) relativeOnWidth(mask.mX); float maskY = (float) relativeOnHeight(mask.mY); float maskWidth = (float) relativeOnWidth(mask.mW); float maskHeight = (float) relativeOnHeight(mask.mH); - maskCanvas.clipRect(maskX, maskY, maskWidth + maskX, maskHeight + maskY); - - Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mask.draw(maskCanvas, maskPaint, 1); - - // Apply luminanceToAlpha filter primitive - // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement - int nPixels = width * height; - int[] pixels = new int[nPixels]; - maskBitmap.getPixels(pixels, 0, width, 0, 0, width, height); - - for (int i = 0; i < nPixels; i++) { - int color = pixels[i]; - - int r = (color >> 16) & 0xFF; - int g = (color >> 8) & 0xFF; - int b = color & 0xFF; - int a = color >>> 24; - - double luminance = saturate(((0.299 * r) + (0.587 * g) + (0.144 * b)) / 255); - int alpha = (int) (a * luminance); - int pixel = (alpha << 24); - pixels[i] = pixel; - } - - maskBitmap.setPixels(pixels, 0, width, 0, 0, width, height); - - // Render content of current SVG Renderable to image - draw(originalCanvas, paint, opacity); - - // Blend current element and mask - maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); - resultCanvas.drawBitmap(original, 0, 0, null); - resultCanvas.drawBitmap(maskBitmap, 0, 0, maskPaint); + canvas.clipRect(maskX, maskY, maskWidth + maskX, maskHeight + maskY); + mask.draw(canvas, maskPaint, 1f); - // Render composited result into current render context - canvas.drawBitmap(result, 0, 0, paint); + // close mask layer + canvas.restore(); + // close element layer + canvas.restore(); } else { draw(canvas, paint, opacity); } From 540cf7b4a88af610379bd3b0d582add918194c77 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Wed, 12 Jun 2024 14:59:44 +0200 Subject: [PATCH 03/12] fix: iOS use scale on image and maskImage --- apple/RNSVGRenderable.mm | 120 +++++++++++++-------------------------- 1 file changed, 41 insertions(+), 79 deletions(-) diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index b245afd33..f2fd502ce 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -257,29 +257,23 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect #endif } #endif // TARGET_OS_OSX - NSUInteger iheight = (NSUInteger)height; - NSUInteger iwidth = (NSUInteger)width; - NSUInteger iscale = (NSUInteger)scale; - NSUInteger scaledHeight = iheight * iscale; - NSUInteger scaledWidth = iwidth * iscale; - NSUInteger npixels = scaledHeight * scaledWidth; - CGRect drawBounds = CGRectMake(0, 0, width, height); - - // Allocate pixel buffer and bitmap context for mask - NSUInteger bytesPerPixel = 4; - NSUInteger bitsPerComponent = 8; - NSUInteger bytesPerRow = bytesPerPixel * scaledWidth; - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + // TODO: make it the right way, currently this is a hack to get the correct scale for the mask + scale = scale * self.matrix.a * self.svgView.viewBoxTransform.a; + CGRect scaledRect = CGRectMake(0, 0, rect.size.width * scale, rect.size.height * scale); + + NSUInteger npixels = scaledRect.size.width * scaledRect.size.height; UInt32 *pixels = (UInt32 *)calloc(npixels, sizeof(UInt32)); - CGContextRef bcontext = CGBitmapContextCreate( + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef offscreenContext = CGBitmapContextCreate( pixels, - scaledWidth, - scaledHeight, - bitsPerComponent, - bytesPerRow, + scaledRect.size.width, + scaledRect.size.height, + 8, + 4 * (NSUInteger)scaledRect.size.width, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); - CGContextScaleCTM(bcontext, iscale, iscale); + CGContextScaleCTM(offscreenContext, scale, scale); // Clip to mask bounds and render the mask CGFloat x = [self relativeOn:[_maskNode x] relative:width]; @@ -287,12 +281,14 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect CGFloat w = [self relativeOn:[_maskNode maskwidth] relative:width]; CGFloat h = [self relativeOn:[_maskNode maskheight] relative:height]; CGRect maskBounds = CGRectMake(x, y, w, h); - CGContextClipToRect(bcontext, maskBounds); - [_maskNode renderLayerTo:bcontext rect:rect]; + CGContextClipToRect(offscreenContext, maskBounds); + + [_maskNode renderLayerTo:offscreenContext rect:scaledRect]; // Apply luminanceToAlpha filter primitive // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement UInt32 *currentPixel = pixels; + for (NSUInteger i = 0; i < npixels; i++) { UInt32 color = *currentPixel; @@ -305,68 +301,34 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect currentPixel++; } - // Create mask image and release memory - CGImageRef maskImage = CGBitmapContextCreateImage(bcontext); - CGColorSpaceRelease(colorSpace); - CGContextRelease(bcontext); - free(pixels); + // Create mask image + CGImageRef maskImage = CGBitmapContextCreateImage(offscreenContext); -#if !TARGET_OS_OSX // [macOS] - UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; - format.scale = scale; - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:boundsSize format:format]; - - // Get the content image - UIImage *contentImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { - CGContextTranslateCTM(rendererContext.CGContext, 0.0, height); - CGContextScaleCTM(rendererContext.CGContext, 1.0, -1.0); - [self renderLayerTo:rendererContext.CGContext rect:rect]; - }]; - - // Blend current element and mask - UIImage *blendedImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { - CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeCopy); - CGContextDrawImage(rendererContext.CGContext, drawBounds, maskImage); - CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeSourceIn); - CGContextDrawImage(rendererContext.CGContext, drawBounds, contentImage.CGImage); - }]; - - // Render blended result into current render context - [blendedImage drawInRect:drawBounds]; - - // Render blended result into current render context - CGImageRelease(maskImage); -#else // [macOS - // Render content of current SVG Renderable to image - UIGraphicsBeginImageContextWithOptions(boundsSize, NO, 0.0); - CGContextRef newContext = UIGraphicsGetCurrentContext(); - CGContextTranslateCTM(newContext, 0.0, height); - CGContextScaleCTM(newContext, 1.0, -1.0); - [self renderLayerTo:newContext rect:rect]; - CGImageRef contentImage = CGBitmapContextCreateImage(newContext); - UIGraphicsEndImageContext(); - - // Blend current element and mask - UIGraphicsBeginImageContextWithOptions(boundsSize, NO, 0.0); - newContext = UIGraphicsGetCurrentContext(); - CGContextTranslateCTM(newContext, 0.0, height); - CGContextScaleCTM(newContext, 1.0, -1.0); - - CGContextSetBlendMode(newContext, kCGBlendModeCopy); - CGContextDrawImage(newContext, drawBounds, maskImage); - CGImageRelease(maskImage); + // Clear context and create layer image + CGContextClearRect(offscreenContext, scaledRect); + [self renderLayerTo:offscreenContext rect:scaledRect]; + CGImageRef currentImage = CGBitmapContextCreateImage(offscreenContext); + + // Create new layer + CGLayerRef blendedLayer = CGLayerCreateWithContext(context, scaledRect.size, nil); + CGContextRef blendedLayerContext = CGLayerGetContext(blendedLayer); - CGContextSetBlendMode(newContext, kCGBlendModeSourceIn); - CGContextDrawImage(newContext, drawBounds, contentImage); - CGImageRelease(contentImage); + // Render current element and mask to layer + CGContextSetBlendMode(blendedLayerContext, kCGBlendModeCopy); + CGContextDrawImage(blendedLayerContext, rect, maskImage); + CGContextSetBlendMode(blendedLayerContext, kCGBlendModeSourceIn); + CGContextDrawImage(blendedLayerContext, rect, currentImage); - CGImageRef blendedImage = CGBitmapContextCreateImage(newContext); - UIGraphicsEndImageContext(); + // Render current layer into render context + CGContextDrawLayerAtPoint(context, CGPointMake(0, 0), blendedLayer); - // Render blended result into current render context - CGContextDrawImage(context, drawBounds, blendedImage); - CGImageRelease(blendedImage); -#endif // macOS] + // Release memory + CGColorSpaceRelease(colorSpace); + CGLayerRelease(blendedLayer); + CGImageRelease(maskImage); + CGImageRelease(currentImage); + CGContextRelease(offscreenContext); + free(pixels); } else { [self renderLayerTo:context rect:rect]; } From 8879c02f46f6206ec07803a87f0414e80a7d6930 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Fri, 14 Jun 2024 10:32:30 +0200 Subject: [PATCH 04/12] chore: add comments and extract variables - minor changes after crr --- .../src/main/java/com/horcrux/svg/RenderableView.java | 1 + apple/RNSVGRenderable.mm | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index 24a23329d..cbc320c4c 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -344,6 +344,7 @@ void render(Canvas canvas, Paint paint, float opacity) { // prepare maskPaint with luminanceToAlpha + PorterDuffXfermode and create new layer with it Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // luminanceToAlpha conversion TODO: use filters + // https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEMergeElement:~:text=not%20applicable.%20A-,luminanceToAlpha,-operation%20is%20equivalent ColorMatrix luminanceToAlpha = new ColorMatrix( new float[] { diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index f2fd502ce..61cbf2027 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -258,19 +258,24 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect } #endif // TARGET_OS_OSX // TODO: make it the right way, currently this is a hack to get the correct scale for the mask + // current element scale: screen scale * current element scale * viewBox scale scale = scale * self.matrix.a * self.svgView.viewBoxTransform.a; CGRect scaledRect = CGRectMake(0, 0, rect.size.width * scale, rect.size.height * scale); NSUInteger npixels = scaledRect.size.width * scaledRect.size.height; UInt32 *pixels = (UInt32 *)calloc(npixels, sizeof(UInt32)); + NSUInteger bytesPerPixel = 4; + NSUInteger bitsPerComponent = 8; + NSUInteger bytesPerRow = bytesPerPixel * (NSUInteger)scaledRect.size.width; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef offscreenContext = CGBitmapContextCreate( pixels, scaledRect.size.width, scaledRect.size.height, - 8, - 4 * (NSUInteger)scaledRect.size.width, + bitsPerComponent, + bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextScaleCTM(offscreenContext, scale, scale); From 928a2cc4fee82cc8a57caa018c206a9e061d655a Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Mon, 17 Jun 2024 22:41:39 +0200 Subject: [PATCH 05/12] refactor: rewrite Apple mask to CoreImage alpha filter --- apple/Filters/LuminanceToAlpha.h | 19 ++++ apple/RNSVGRenderable.h | 2 + apple/RNSVGRenderable.mm | 179 ++++++++++++++++--------------- 3 files changed, 114 insertions(+), 86 deletions(-) create mode 100644 apple/Filters/LuminanceToAlpha.h diff --git a/apple/Filters/LuminanceToAlpha.h b/apple/Filters/LuminanceToAlpha.h new file mode 100644 index 000000000..212cf5679 --- /dev/null +++ b/apple/Filters/LuminanceToAlpha.h @@ -0,0 +1,19 @@ +#ifndef LuminanceToAlpha_h +#define LuminanceToAlpha_h + +static CIImage *applyLuminanceToAlphaFilter(CIImage *inputImage) +{ + CIFilter *luminanceToAlpha = [CIFilter filterWithName:@"CIColorMatrix"]; + [luminanceToAlpha setDefaults]; + CGFloat alpha[4] = {0.2125, 0.7154, 0.0721, 0}; + CGFloat zero[4] = {0, 0, 0, 0}; + [luminanceToAlpha setValue:inputImage forKey:@"inputImage"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputRVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputGVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:alpha count:4] forKey:@"inputAVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBiasVector"]; + return [luminanceToAlpha valueForKey:@"outputImage"]; +} + +#endif /* LuminanceToAlpha_h */ diff --git a/apple/RNSVGRenderable.h b/apple/RNSVGRenderable.h index a4f02baab..c78b07da3 100644 --- a/apple/RNSVGRenderable.h +++ b/apple/RNSVGRenderable.h @@ -42,4 +42,6 @@ - (void)resetProperties; ++ (CIContext *)sharedCIContext; + @end diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index 61cbf2027..33a586f7c 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -8,6 +8,7 @@ #import "RNSVGRenderable.h" #import +#import "LuminanceToAlpha.h" #import "RNSVGBezierElement.h" #import "RNSVGClipPath.h" #import "RNSVGMarker.h" @@ -222,9 +223,30 @@ - (void)prepareForRecycle } #endif // RCT_NEW_ARCH_ENABLED -UInt32 saturate(CGFloat value) +static CGImageRef renderToImage(RNSVGRenderable *object, CGSize bounds, CGRect rect, CGRect *clip) { - return value <= 0 ? 0 : value >= 255 ? 255 : (UInt32)value; + UIGraphicsBeginImageContextWithOptions(bounds, NO, 1.0); + CGContextRef cgContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(cgContext, 0.0, bounds.height); + CGContextScaleCTM(cgContext, 1.0, -1.0); + if (clip) { + CGContextClipToRect(cgContext, *clip); + } + [object renderLayerTo:cgContext rect:rect]; + CGImageRef contentImage = CGBitmapContextCreateImage(cgContext); + UIGraphicsEndImageContext(); + return contentImage; +} + ++ (CIContext *)sharedCIContext +{ + static CIContext *sharedCIContext = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedCIContext = [[CIContext alloc] init]; + }); + + return sharedCIContext; } - (void)renderTo:(CGContextRef)context rect:(CGRect)rect @@ -239,101 +261,42 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect [self beginTransparencyLayer:context]; if (self.mask) { - // https://www.w3.org/TR/SVG11/masking.html#MaskElement - RNSVGMask *_maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; CGRect bounds = CGContextGetClipBoundingBox(context); CGSize boundsSize = bounds.size; CGFloat height = boundsSize.height; CGFloat width = boundsSize.width; - CGFloat scale = 0.0; -#if TARGET_OS_OSX - scale = [[NSScreen mainScreen] backingScaleFactor]; -#else - if (@available(iOS 13.0, *)) { - scale = [UITraitCollection currentTraitCollection].displayScale; - } else { -#if !TARGET_OS_VISION - scale = [[UIScreen mainScreen] scale]; -#endif - } -#endif // TARGET_OS_OSX - // TODO: make it the right way, currently this is a hack to get the correct scale for the mask - // current element scale: screen scale * current element scale * viewBox scale - scale = scale * self.matrix.a * self.svgView.viewBoxTransform.a; - CGRect scaledRect = CGRectMake(0, 0, rect.size.width * scale, rect.size.height * scale); - - NSUInteger npixels = scaledRect.size.width * scaledRect.size.height; - UInt32 *pixels = (UInt32 *)calloc(npixels, sizeof(UInt32)); - - NSUInteger bytesPerPixel = 4; - NSUInteger bitsPerComponent = 8; - NSUInteger bytesPerRow = bytesPerPixel * (NSUInteger)scaledRect.size.width; - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef offscreenContext = CGBitmapContextCreate( - pixels, - scaledRect.size.width, - scaledRect.size.height, - bitsPerComponent, - bytesPerRow, - colorSpace, - kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); - CGContextScaleCTM(offscreenContext, scale, scale); + CGRect drawBounds = CGRectMake(0, 0, width, height); + // Render content of current SVG Renderable to image + CGImageRef currentContent = renderToImage(self, boundsSize, rect, nil); + CIImage *contentSrcImage = [CIImage imageWithCGImage:currentContent]; + + // https://www.w3.org/TR/SVG11/masking.html#MaskElement + RNSVGMask *_maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; + // Clip to mask bounds and render the mask CGFloat x = [self relativeOn:[_maskNode x] relative:width]; CGFloat y = [self relativeOn:[_maskNode y] relative:height]; CGFloat w = [self relativeOn:[_maskNode maskwidth] relative:width]; CGFloat h = [self relativeOn:[_maskNode maskheight] relative:height]; CGRect maskBounds = CGRectMake(x, y, w, h); - CGContextClipToRect(offscreenContext, maskBounds); - - [_maskNode renderLayerTo:offscreenContext rect:scaledRect]; - - // Apply luminanceToAlpha filter primitive - // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement - UInt32 *currentPixel = pixels; - - for (NSUInteger i = 0; i < npixels; i++) { - UInt32 color = *currentPixel; - - UInt32 r = color & 0xFF; - UInt32 g = (color >> 8) & 0xFF; - UInt32 b = (color >> 16) & 0xFF; - - CGFloat luma = (CGFloat)(0.299 * r + 0.587 * g + 0.144 * b); - *currentPixel = saturate(luma) << 24; - currentPixel++; - } - - // Create mask image - CGImageRef maskImage = CGBitmapContextCreateImage(offscreenContext); - - // Clear context and create layer image - CGContextClearRect(offscreenContext, scaledRect); - [self renderLayerTo:offscreenContext rect:scaledRect]; - CGImageRef currentImage = CGBitmapContextCreateImage(offscreenContext); - - // Create new layer - CGLayerRef blendedLayer = CGLayerCreateWithContext(context, scaledRect.size, nil); - CGContextRef blendedLayerContext = CGLayerGetContext(blendedLayer); - - // Render current element and mask to layer - CGContextSetBlendMode(blendedLayerContext, kCGBlendModeCopy); - CGContextDrawImage(blendedLayerContext, rect, maskImage); - CGContextSetBlendMode(blendedLayerContext, kCGBlendModeSourceIn); - CGContextDrawImage(blendedLayerContext, rect, currentImage); - - // Render current layer into render context - CGContextDrawLayerAtPoint(context, CGPointMake(0, 0), blendedLayer); - - // Release memory - CGColorSpaceRelease(colorSpace); - CGLayerRelease(blendedLayer); - CGImageRelease(maskImage); - CGImageRelease(currentImage); - CGContextRelease(offscreenContext); - free(pixels); + CGImageRef maskContent = renderToImage(_maskNode, boundsSize, rect, &maskBounds); + CIImage *maskSrcImage = [CIImage imageWithCGImage:maskContent]; + + // Create mask with mask element alpha and mask luminance + CIImage *alphaMask = transformImageIntoAlphaMask(maskSrcImage); + + // Compose source image with alpha mask + CIImage *composite = applyBlendWithAlphaMask(contentSrcImage, alphaMask); + + // Create masked image and release memory + CGImageRef compositeImage = [[RNSVGRenderable sharedCIContext] createCGImage:composite fromRect:drawBounds]; + + // Render composited result into current render context + CGContextDrawImage(context, drawBounds, compositeImage); + CGImageRelease(compositeImage); + CGImageRelease(maskContent); + CGImageRelease(currentContent); } else { [self renderLayerTo:context rect:rect]; } @@ -665,4 +628,48 @@ - (void)resetProperties self.merging = false; } +static CIImage *transparentImage() +{ + static CIImage *transparentImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + CIFilter *transparent = [CIFilter filterWithName:@"CIConstantColorGenerator"]; + [transparent setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.0] forKey:@"inputColor"]; + transparentImage = [transparent valueForKey:@"outputImage"]; + }); + return transparentImage; +} + +static CIImage *blackImage() +{ + static CIImage *blackImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + CIFilter *black = [CIFilter filterWithName:@"CIConstantColorGenerator"]; + [black setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0] forKey:@"inputColor"]; + blackImage = [black valueForKey:@"outputImage"]; + }); + return blackImage; +} + +static CIImage *transformImageIntoAlphaMask(CIImage *inputImage) +{ + CIImage *blackBackground = blackImage(); + CIFilter *layerOverBlack = [CIFilter filterWithName:@"CISourceOverCompositing"]; + [layerOverBlack setValue:blackBackground forKey:@"inputBackgroundImage"]; + [layerOverBlack setValue:inputImage forKey:@"inputImage"]; + return applyLuminanceToAlphaFilter([layerOverBlack valueForKey:@"outputImage"]); +} + +static CIImage *applyBlendWithAlphaMask(CIImage *inputImage, CIImage *inputMaskImage) +{ + CIImage *transparent = transparentImage(); + CIFilter *blendWithAlphaMask = [CIFilter filterWithName:@"CIBlendWithAlphaMask"]; + [blendWithAlphaMask setDefaults]; + [blendWithAlphaMask setValue:inputImage forKey:@"inputImage"]; + [blendWithAlphaMask setValue:transparent forKey:@"inputBackgroundImage"]; + [blendWithAlphaMask setValue:inputMaskImage forKey:@"inputMaskImage"]; + return [blendWithAlphaMask valueForKey:@"outputImage"]; +} + @end From a744e2fca6e586bbe4aa31a4a4d7cf2224363685 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Mon, 17 Jun 2024 22:43:19 +0200 Subject: [PATCH 06/12] fix: android mask --- .../java/com/horcrux/svg/RenderableView.java | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index cbc320c4c..5baaf3a9e 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -332,38 +332,61 @@ public void setPropList(@Nullable ReadableArray propList) { void render(Canvas canvas, Paint paint, float opacity) { MaskView mask = null; + if (mMask != null) { SvgView root = getSvgView(); mask = (MaskView) root.getDefinedMask(mMask); } + if (mask != null) { - // draw element to new layer - canvas.saveLayer(null, paint); - draw(canvas, paint, opacity); + // https://www.w3.org/TR/SVG11/masking.html + // Adding a mask involves several steps + // 1. applying luminanceToAlpha to the mask element + // 2. merging the alpha channel of the element with the alpha channel from the previous step + // 3. applying the result from step 2 to the target element - // prepare maskPaint with luminanceToAlpha + PorterDuffXfermode and create new layer with it - Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - // luminanceToAlpha conversion TODO: use filters + Paint dstInPaint = new Paint(); + dstInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + + // calculate mask bounds + float maskX = (float) relativeOnWidth(mask.mX); + float maskY = (float) relativeOnHeight(mask.mY); + float maskWidth = (float) relativeOnWidth(mask.mW); + float maskHeight = (float) relativeOnHeight(mask.mH); + + // step 3 - combined layer + canvas.saveLayer(null, dstInPaint); + + // step 1 - luminance layer + // prepare maskPaint with luminanceToAlpha // https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEMergeElement:~:text=not%20applicable.%20A-,luminanceToAlpha,-operation%20is%20equivalent + Paint luminancePaint = new Paint(); ColorMatrix luminanceToAlpha = new ColorMatrix( new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2125f, 0.7154f, 0.0721f, 0, 0 }); - maskPaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha)); - maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); - canvas.saveLayer(null, maskPaint); + luminancePaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha)); + canvas.saveLayer(null, luminancePaint); + canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); - // clip to mask bounds and render the mask - float maskX = (float) relativeOnWidth(mask.mX); - float maskY = (float) relativeOnHeight(mask.mY); - float maskWidth = (float) relativeOnWidth(mask.mW); - float maskHeight = (float) relativeOnHeight(mask.mH); - canvas.clipRect(maskX, maskY, maskWidth + maskX, maskHeight + maskY); - mask.draw(canvas, maskPaint, 1f); + mask.draw(canvas, paint, 1f); + + // close luminance layer + canvas.restore(); - // close mask layer + // step 2 - alpha layer + canvas.saveLayer(null, dstInPaint); + canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); + + mask.draw(canvas, paint, 1f); + + // close alpha layer canvas.restore(); + + // close combined layer + canvas.restore(); + // close element layer canvas.restore(); } else { From ac946b46706f07e4f0831c1d62fc4f593a85d743 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Wed, 19 Jun 2024 09:29:50 +0200 Subject: [PATCH 07/12] fix: small changes after cr --- .../java/com/horcrux/svg/RenderableView.java | 19 ++-- apple/RNSVGNode.h | 1 - apple/RNSVGNode.mm | 10 -- apple/RNSVGRenderable.h | 2 + apple/RNSVGRenderable.mm | 94 +++++-------------- apple/Utils/RNSVGMaskUtils.h | 66 +++++++++++++ apple/Utils/RNSVGRenderUtils.h | 19 ++++ 7 files changed, 123 insertions(+), 88 deletions(-) create mode 100644 apple/Utils/RNSVGMaskUtils.h create mode 100644 apple/Utils/RNSVGRenderUtils.h diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index 5baaf3a9e..d88ece419 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -345,16 +345,13 @@ void render(Canvas canvas, Paint paint, float opacity) { // 2. merging the alpha channel of the element with the alpha channel from the previous step // 3. applying the result from step 2 to the target element + canvas.saveLayer(null, paint); + draw(canvas, paint, opacity); + Paint dstInPaint = new Paint(); dstInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); - // calculate mask bounds - float maskX = (float) relativeOnWidth(mask.mX); - float maskY = (float) relativeOnHeight(mask.mY); - float maskWidth = (float) relativeOnWidth(mask.mW); - float maskHeight = (float) relativeOnHeight(mask.mH); - - // step 3 - combined layer + // prepare step 3 - combined layer canvas.saveLayer(null, dstInPaint); // step 1 - luminance layer @@ -368,6 +365,13 @@ void render(Canvas canvas, Paint paint, float opacity) { }); luminancePaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha)); canvas.saveLayer(null, luminancePaint); + + // calculate mask bounds + float maskX = (float) relativeOnWidth(mask.mX); + float maskY = (float) relativeOnHeight(mask.mY); + float maskWidth = (float) relativeOnWidth(mask.mW); + float maskHeight = (float) relativeOnHeight(mask.mH); + // clip to mask bounds canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); mask.draw(canvas, paint, 1f); @@ -377,6 +381,7 @@ void render(Canvas canvas, Paint paint, float opacity) { // step 2 - alpha layer canvas.saveLayer(null, dstInPaint); + // clip to mask bounds canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); mask.draw(canvas, paint, 1f); diff --git a/apple/RNSVGNode.h b/apple/RNSVGNode.h index 3c65e102e..8d64f6bef 100644 --- a/apple/RNSVGNode.h +++ b/apple/RNSVGNode.h @@ -42,7 +42,6 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; @property (nonatomic, assign) CGFloat opacity; @property (nonatomic, assign) RNSVGCGFCRule clipRule; @property (nonatomic, strong) NSString *clipPath; -@property (nonatomic, strong) NSString *mask; @property (nonatomic, strong) NSString *markerStart; @property (nonatomic, strong) NSString *markerMid; @property (nonatomic, strong) NSString *markerEnd; diff --git a/apple/RNSVGNode.mm b/apple/RNSVGNode.mm index 2966a1ec5..bfc09990d 100644 --- a/apple/RNSVGNode.mm +++ b/apple/RNSVGNode.mm @@ -285,15 +285,6 @@ - (void)setClipRule:(RNSVGCGFCRule)clipRule [self invalidate]; } -- (void)setMask:(NSString *)mask -{ - if ([_mask isEqualToString:mask]) { - return; - } - _mask = mask; - [self invalidate]; -} - - (void)setMarkerStart:(NSString *)markerStart { if ([_markerStart isEqualToString:markerStart]) { @@ -623,7 +614,6 @@ - (void)prepareForRecycle _opacity = 0; _clipRule = kRNSVGCGFCRuleEvenodd; _clipPath = nil; - _mask = nil; _markerStart = nil; _markerMid = nil; _markerEnd = nil; diff --git a/apple/RNSVGRenderable.h b/apple/RNSVGRenderable.h index c78b07da3..9a9861e28 100644 --- a/apple/RNSVGRenderable.h +++ b/apple/RNSVGRenderable.h @@ -33,6 +33,8 @@ @property (nonatomic, assign) RNSVGVectorEffect vectorEffect; @property (nonatomic, copy) NSArray *propList; @property (nonatomic, assign) CGPathRef hitArea; +@property (nonatomic, strong) NSString *mask; +@property (nonatomic, strong) NSString *filter; - (void)setHitArea:(CGPathRef)path; diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index 33a586f7c..cf0060c51 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -8,12 +8,13 @@ #import "RNSVGRenderable.h" #import -#import "LuminanceToAlpha.h" #import "RNSVGBezierElement.h" #import "RNSVGClipPath.h" #import "RNSVGMarker.h" #import "RNSVGMarkerPosition.h" #import "RNSVGMask.h" +#import "RNSVGMaskUtils.h" +#import "RNSVGRenderUtils.h" #import "RNSVGVectorEffect.h" #import "RNSVGViewBox.h" @@ -176,6 +177,15 @@ - (void)setPropList:(NSArray *)propList [self invalidate]; } +- (void)setMask:(NSString *)mask +{ + if ([_mask isEqualToString:mask]) { + return; + } + _mask = mask; + [self invalidate]; +} + - (void)dealloc { CGPathRelease(_hitArea); @@ -220,24 +230,10 @@ - (void)prepareForRecycle _strokeDashoffset = 0; _vectorEffect = kRNSVGVectorEffectDefault; _propList = nil; + _mask = nil; } #endif // RCT_NEW_ARCH_ENABLED -static CGImageRef renderToImage(RNSVGRenderable *object, CGSize bounds, CGRect rect, CGRect *clip) -{ - UIGraphicsBeginImageContextWithOptions(bounds, NO, 1.0); - CGContextRef cgContext = UIGraphicsGetCurrentContext(); - CGContextTranslateCTM(cgContext, 0.0, bounds.height); - CGContextScaleCTM(cgContext, 1.0, -1.0); - if (clip) { - CGContextClipToRect(cgContext, *clip); - } - [object renderLayerTo:cgContext rect:rect]; - CGImageRef contentImage = CGBitmapContextCreateImage(cgContext); - UIGraphicsEndImageContext(); - return contentImage; -} - + (CIContext *)sharedCIContext { static CIContext *sharedCIContext = nil; @@ -272,28 +268,30 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect CIImage *contentSrcImage = [CIImage imageWithCGImage:currentContent]; // https://www.w3.org/TR/SVG11/masking.html#MaskElement - RNSVGMask *_maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; - + RNSVGMask *maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; + // Clip to mask bounds and render the mask - CGFloat x = [self relativeOn:[_maskNode x] relative:width]; - CGFloat y = [self relativeOn:[_maskNode y] relative:height]; - CGFloat w = [self relativeOn:[_maskNode maskwidth] relative:width]; - CGFloat h = [self relativeOn:[_maskNode maskheight] relative:height]; + CGFloat x = [self relativeOn:[maskNode x] relative:width]; + CGFloat y = [self relativeOn:[maskNode y] relative:height]; + CGFloat w = [self relativeOn:[maskNode maskwidth] relative:width]; + CGFloat h = [self relativeOn:[maskNode maskheight] relative:height]; CGRect maskBounds = CGRectMake(x, y, w, h); - CGImageRef maskContent = renderToImage(_maskNode, boundsSize, rect, &maskBounds); + CGImageRef maskContent = renderToImage(maskNode, boundsSize, rect, &maskBounds); CIImage *maskSrcImage = [CIImage imageWithCGImage:maskContent]; // Create mask with mask element alpha and mask luminance CIImage *alphaMask = transformImageIntoAlphaMask(maskSrcImage); - + // Compose source image with alpha mask CIImage *composite = applyBlendWithAlphaMask(contentSrcImage, alphaMask); - // Create masked image and release memory + // Create composed CGImage CGImageRef compositeImage = [[RNSVGRenderable sharedCIContext] createCGImage:composite fromRect:drawBounds]; - // Render composited result into current render context + // Render result into context CGContextDrawImage(context, drawBounds, compositeImage); + + // Release memory CGImageRelease(compositeImage); CGImageRelease(maskContent); CGImageRelease(currentContent); @@ -628,48 +626,4 @@ - (void)resetProperties self.merging = false; } -static CIImage *transparentImage() -{ - static CIImage *transparentImage = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - CIFilter *transparent = [CIFilter filterWithName:@"CIConstantColorGenerator"]; - [transparent setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.0] forKey:@"inputColor"]; - transparentImage = [transparent valueForKey:@"outputImage"]; - }); - return transparentImage; -} - -static CIImage *blackImage() -{ - static CIImage *blackImage = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - CIFilter *black = [CIFilter filterWithName:@"CIConstantColorGenerator"]; - [black setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0] forKey:@"inputColor"]; - blackImage = [black valueForKey:@"outputImage"]; - }); - return blackImage; -} - -static CIImage *transformImageIntoAlphaMask(CIImage *inputImage) -{ - CIImage *blackBackground = blackImage(); - CIFilter *layerOverBlack = [CIFilter filterWithName:@"CISourceOverCompositing"]; - [layerOverBlack setValue:blackBackground forKey:@"inputBackgroundImage"]; - [layerOverBlack setValue:inputImage forKey:@"inputImage"]; - return applyLuminanceToAlphaFilter([layerOverBlack valueForKey:@"outputImage"]); -} - -static CIImage *applyBlendWithAlphaMask(CIImage *inputImage, CIImage *inputMaskImage) -{ - CIImage *transparent = transparentImage(); - CIFilter *blendWithAlphaMask = [CIFilter filterWithName:@"CIBlendWithAlphaMask"]; - [blendWithAlphaMask setDefaults]; - [blendWithAlphaMask setValue:inputImage forKey:@"inputImage"]; - [blendWithAlphaMask setValue:transparent forKey:@"inputBackgroundImage"]; - [blendWithAlphaMask setValue:inputMaskImage forKey:@"inputMaskImage"]; - return [blendWithAlphaMask valueForKey:@"outputImage"]; -} - @end diff --git a/apple/Utils/RNSVGMaskUtils.h b/apple/Utils/RNSVGMaskUtils.h new file mode 100644 index 000000000..710cefb78 --- /dev/null +++ b/apple/Utils/RNSVGMaskUtils.h @@ -0,0 +1,66 @@ +#ifndef LuminanceToAlpha_h +#define LuminanceToAlpha_h + +static CIImage *transparentImage() +{ + static CIImage *transparentImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + CIFilter *transparent = [CIFilter filterWithName:@"CIConstantColorGenerator"]; + [transparent setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.0] forKey:@"inputColor"]; + transparentImage = [transparent valueForKey:@"outputImage"]; + }); + return transparentImage; +} + +static CIImage *blackImage() +{ + static CIImage *blackImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + CIFilter *black = [CIFilter filterWithName:@"CIConstantColorGenerator"]; + [black setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0] forKey:@"inputColor"]; + blackImage = [black valueForKey:@"outputImage"]; + }); + return blackImage; +} + +static CIImage *applyLuminanceToAlphaFilter(CIImage *inputImage) +{ + // https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEMergeElement:~:text=not%20applicable.%20A-,luminanceToAlpha,-operation%20is%20equivalent + CIFilter *luminanceToAlpha = [CIFilter filterWithName:@"CIColorMatrix"]; + [luminanceToAlpha setDefaults]; + CGFloat alpha[4] = {0.2125, 0.7154, 0.0721, 0}; + CGFloat zero[4] = {0, 0, 0, 0}; + [luminanceToAlpha setValue:inputImage forKey:@"inputImage"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputRVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputGVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:alpha count:4] forKey:@"inputAVector"]; + [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBiasVector"]; + return [luminanceToAlpha valueForKey:@"outputImage"]; +} + +static CIImage *transformImageIntoAlphaMask(CIImage *inputImage) +{ + // Compose alpha only with luminanceToAlpha results + CIImage *blackBackground = blackImage(); + CIFilter *layerOverBlack = [CIFilter filterWithName:@"CISourceOverCompositing"]; + [layerOverBlack setValue:blackBackground forKey:@"inputBackgroundImage"]; + [layerOverBlack setValue:inputImage forKey:@"inputImage"]; + return applyLuminanceToAlphaFilter([layerOverBlack valueForKey:@"outputImage"]); +} + +static CIImage *applyBlendWithAlphaMask(CIImage *inputImage, CIImage *inputMaskImage) +{ + // Apply mask to the source image with transparent background + CIImage *transparent = transparentImage(); + CIFilter *blendWithAlphaMask = [CIFilter filterWithName:@"CIBlendWithAlphaMask"]; + [blendWithAlphaMask setDefaults]; + [blendWithAlphaMask setValue:inputImage forKey:@"inputImage"]; + [blendWithAlphaMask setValue:transparent forKey:@"inputBackgroundImage"]; + [blendWithAlphaMask setValue:inputMaskImage forKey:@"inputMaskImage"]; + return [blendWithAlphaMask valueForKey:@"outputImage"]; +} + +#endif /* LuminanceToAlpha_h */ diff --git a/apple/Utils/RNSVGRenderUtils.h b/apple/Utils/RNSVGRenderUtils.h new file mode 100644 index 000000000..574be1787 --- /dev/null +++ b/apple/Utils/RNSVGRenderUtils.h @@ -0,0 +1,19 @@ +#ifndef RNSVGRenderUtils_h +#define RNSVGRenderUtils_h + +static CGImageRef renderToImage(RNSVGRenderable *renderable, CGSize bounds, CGRect rect, CGRect *clip) +{ + UIGraphicsBeginImageContextWithOptions(bounds, NO, 1.0); + CGContextRef cgContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(cgContext, 0.0, bounds.height); + CGContextScaleCTM(cgContext, 1.0, -1.0); + if (clip) { + CGContextClipToRect(cgContext, *clip); + } + [renderable renderLayerTo:cgContext rect:rect]; + CGImageRef contentImage = CGBitmapContextCreateImage(cgContext); + UIGraphicsEndImageContext(); + return contentImage; +} + +#endif /* RNSVGRenderUtils_h */ From ee18af9068d28964b5a1315f632c2600daf8f60c Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Sat, 22 Jun 2024 00:38:13 +0200 Subject: [PATCH 08/12] change: revert iOS mask to previous solution but fix scaling issue --- TestsExample/src/Test1451.tsx | 4 +- apple/Filters/LuminanceToAlpha.h | 19 ---- apple/RNSVGRenderable.h | 3 - apple/RNSVGRenderable.mm | 186 +++++++++++++++++++++++-------- apple/Utils/RNSVGRenderUtils.h | 19 ---- 5 files changed, 142 insertions(+), 89 deletions(-) delete mode 100644 apple/Filters/LuminanceToAlpha.h delete mode 100644 apple/Utils/RNSVGRenderUtils.h diff --git a/TestsExample/src/Test1451.tsx b/TestsExample/src/Test1451.tsx index 70f324276..259276d38 100644 --- a/TestsExample/src/Test1451.tsx +++ b/TestsExample/src/Test1451.tsx @@ -11,7 +11,7 @@ export default () => { Animated.loop( Animated.sequence([ Animated.timing(zoom, { - toValue: 2, + toValue: 4, duration: 2000, useNativeDriver: true, }), @@ -27,7 +27,7 @@ export default () => { return ( - + *propList; @property (nonatomic, assign) CGPathRef hitArea; @property (nonatomic, strong) NSString *mask; -@property (nonatomic, strong) NSString *filter; - (void)setHitArea:(CGPathRef)path; @@ -44,6 +43,4 @@ - (void)resetProperties; -+ (CIContext *)sharedCIContext; - @end diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index cf0060c51..a2f957bae 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -13,8 +13,6 @@ #import "RNSVGMarker.h" #import "RNSVGMarkerPosition.h" #import "RNSVGMask.h" -#import "RNSVGMaskUtils.h" -#import "RNSVGRenderUtils.h" #import "RNSVGVectorEffect.h" #import "RNSVGViewBox.h" @@ -230,19 +228,12 @@ - (void)prepareForRecycle _strokeDashoffset = 0; _vectorEffect = kRNSVGVectorEffectDefault; _propList = nil; - _mask = nil; } #endif // RCT_NEW_ARCH_ENABLED -+ (CIContext *)sharedCIContext +UInt32 saturate(CGFloat value) { - static CIContext *sharedCIContext = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedCIContext = [[CIContext alloc] init]; - }); - - return sharedCIContext; + return value <= 0 ? 0 : value >= 255 ? 255 : (UInt32)value; } - (void)renderTo:(CGContextRef)context rect:(CGRect)rect @@ -257,44 +248,147 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect [self beginTransparencyLayer:context]; if (self.mask) { - CGRect bounds = CGContextGetClipBoundingBox(context); - CGSize boundsSize = bounds.size; - CGFloat height = boundsSize.height; - CGFloat width = boundsSize.width; - CGRect drawBounds = CGRectMake(0, 0, width, height); - - // Render content of current SVG Renderable to image - CGImageRef currentContent = renderToImage(self, boundsSize, rect, nil); - CIImage *contentSrcImage = [CIImage imageWithCGImage:currentContent]; - // https://www.w3.org/TR/SVG11/masking.html#MaskElement - RNSVGMask *maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; + RNSVGMask *_maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; + CGFloat height = rect.size.height; + CGFloat width = rect.size.width; + CGFloat scale = 0.0; +#if TARGET_OS_OSX + scale = [[NSScreen mainScreen] backingScaleFactor]; +#else + if (@available(iOS 13.0, *)) { + scale = [UITraitCollection currentTraitCollection].displayScale; + } else { +#if !TARGET_OS_VISION + scale = [[UIScreen mainScreen] scale]; +#endif + } +#endif // TARGET_OS_OSX + NSUInteger iheight = (NSUInteger)height; + NSUInteger iwidth = (NSUInteger)width; + NSUInteger iscale = (NSUInteger)scale; + NSUInteger scaledHeight = iheight * iscale; + NSUInteger scaledWidth = iwidth * iscale; + NSUInteger npixels = scaledHeight * scaledWidth; + CGAffineTransform screenScaleCTM = CGAffineTransformMake(scale, 0, 0, scale, 0, 0); + CGRect scaledRect = CGRectApplyAffineTransform(rect, screenScaleCTM); + // Get current context transformations for offscreenContext + CGAffineTransform currentCTM = CGContextGetCTM(context); + + // Allocate pixel buffer and bitmap context for mask + NSUInteger bytesPerPixel = 4; + NSUInteger bitsPerComponent = 8; + NSUInteger bytesPerRow = bytesPerPixel * scaledWidth; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + UInt32 *pixels = (UInt32 *)calloc(npixels, sizeof(UInt32)); + CGContextRef bcontext = CGBitmapContextCreate( + pixels, + scaledWidth, + scaledHeight, + bitsPerComponent, + bytesPerRow, + colorSpace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + + // CGContextTranslateCTM(bcontext, 0.0, scaledHeight); + // CGContextScaleCTM(bcontext, 1, -1); + CGContextConcatCTM(bcontext, currentCTM); + + // CGContextScaleCTM(bcontext, iscale, iscale); // Clip to mask bounds and render the mask - CGFloat x = [self relativeOn:[maskNode x] relative:width]; - CGFloat y = [self relativeOn:[maskNode y] relative:height]; - CGFloat w = [self relativeOn:[maskNode maskwidth] relative:width]; - CGFloat h = [self relativeOn:[maskNode maskheight] relative:height]; - CGRect maskBounds = CGRectMake(x, y, w, h); - CGImageRef maskContent = renderToImage(maskNode, boundsSize, rect, &maskBounds); - CIImage *maskSrcImage = [CIImage imageWithCGImage:maskContent]; - - // Create mask with mask element alpha and mask luminance - CIImage *alphaMask = transformImageIntoAlphaMask(maskSrcImage); - - // Compose source image with alpha mask - CIImage *composite = applyBlendWithAlphaMask(contentSrcImage, alphaMask); - - // Create composed CGImage - CGImageRef compositeImage = [[RNSVGRenderable sharedCIContext] createCGImage:composite fromRect:drawBounds]; - - // Render result into context - CGContextDrawImage(context, drawBounds, compositeImage); - - // Release memory - CGImageRelease(compositeImage); - CGImageRelease(maskContent); - CGImageRelease(currentContent); + CGFloat x = [self relativeOn:[_maskNode x] relative:width]; + CGFloat y = [self relativeOn:[_maskNode y] relative:height]; + CGFloat w = [self relativeOn:[_maskNode maskwidth] relative:width]; + CGFloat h = [self relativeOn:[_maskNode maskheight] relative:height]; + CGRect maskBounds = CGRectApplyAffineTransform(CGRectMake(x, y, w, h), screenScaleCTM); + CGContextClipToRect(bcontext, maskBounds); + [_maskNode renderLayerTo:bcontext rect:scaledRect]; + + // Apply luminanceToAlpha filter primitive + // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement + UInt32 *currentPixel = pixels; + for (NSUInteger i = 0; i < npixels; i++) { + UInt32 color = *currentPixel; + + UInt32 r = color & 0xFF; + UInt32 g = (color >> 8) & 0xFF; + UInt32 b = (color >> 16) & 0xFF; + + CGFloat luma = (CGFloat)(0.299 * r + 0.587 * g + 0.144 * b); + *currentPixel = saturate(luma) << 24; + currentPixel++; + } + + // Create mask image and release memory + CGImageRef maskImage = CGBitmapContextCreateImage(bcontext); + CGColorSpaceRelease(colorSpace); + CGContextRelease(bcontext); + free(pixels); + +#if !TARGET_OS_OSX // [macOS] + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:rect.size format:format]; + + // Get the content image + UIImage *contentImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { + CGContextConcatCTM( + rendererContext.CGContext, CGAffineTransformInvert(CGContextGetCTM(rendererContext.CGContext))); + CGContextConcatCTM(rendererContext.CGContext, currentCTM); + [self renderLayerTo:rendererContext.CGContext rect:scaledRect]; + }]; + + // Blend current element and mask + UIImage *blendedImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { + CGContextConcatCTM( + rendererContext.CGContext, CGAffineTransformInvert(CGContextGetCTM(rendererContext.CGContext))); + CGContextTranslateCTM(rendererContext.CGContext, 0.0, scaledHeight); + CGContextScaleCTM(rendererContext.CGContext, 1.0, -1.0); + + CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeCopy); + CGContextDrawImage(rendererContext.CGContext, scaledRect, maskImage); + CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeSourceIn); + CGContextDrawImage(rendererContext.CGContext, scaledRect, contentImage.CGImage); + }]; + + // Render blended result into current render context + CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM)); + [blendedImage drawInRect:scaledRect]; + CGContextConcatCTM(context, currentCTM); + + // Render blended result into current render context + CGImageRelease(maskImage); +#else // [macOS + // Render content of current SVG Renderable to image + UIGraphicsBeginImageContextWithOptions(boundsSize, NO, 0.0); + CGContextRef newContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(newContext, 0.0, height); + CGContextScaleCTM(newContext, 1.0, -1.0); + [self renderLayerTo:newContext rect:rect]; + CGImageRef contentImage = CGBitmapContextCreateImage(newContext); + UIGraphicsEndImageContext(); + + // Blend current element and mask + UIGraphicsBeginImageContextWithOptions(boundsSize, NO, 0.0); + newContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(newContext, 0.0, height); + CGContextScaleCTM(newContext, 1.0, -1.0); + + CGContextSetBlendMode(newContext, kCGBlendModeCopy); + CGContextDrawImage(newContext, drawBounds, maskImage); + CGImageRelease(maskImage); + + CGContextSetBlendMode(newContext, kCGBlendModeSourceIn); + CGContextDrawImage(newContext, drawBounds, contentImage); + CGImageRelease(contentImage); + + CGImageRef blendedImage = CGBitmapContextCreateImage(newContext); + UIGraphicsEndImageContext(); + + // Render blended result into current render context + CGContextDrawImage(context, drawBounds, blendedImage); + CGImageRelease(blendedImage); +#endif // macOS] } else { [self renderLayerTo:context rect:rect]; } diff --git a/apple/Utils/RNSVGRenderUtils.h b/apple/Utils/RNSVGRenderUtils.h deleted file mode 100644 index 574be1787..000000000 --- a/apple/Utils/RNSVGRenderUtils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RNSVGRenderUtils_h -#define RNSVGRenderUtils_h - -static CGImageRef renderToImage(RNSVGRenderable *renderable, CGSize bounds, CGRect rect, CGRect *clip) -{ - UIGraphicsBeginImageContextWithOptions(bounds, NO, 1.0); - CGContextRef cgContext = UIGraphicsGetCurrentContext(); - CGContextTranslateCTM(cgContext, 0.0, bounds.height); - CGContextScaleCTM(cgContext, 1.0, -1.0); - if (clip) { - CGContextClipToRect(cgContext, *clip); - } - [renderable renderLayerTo:cgContext rect:rect]; - CGImageRef contentImage = CGBitmapContextCreateImage(cgContext); - UIGraphicsEndImageContext(); - return contentImage; -} - -#endif /* RNSVGRenderUtils_h */ From f28bbb3dcc73bcba50b96312da4c085b757de47d Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Sat, 22 Jun 2024 00:41:14 +0200 Subject: [PATCH 09/12] remove: unused file --- apple/Utils/RNSVGMaskUtils.h | 66 ------------------------------------ 1 file changed, 66 deletions(-) delete mode 100644 apple/Utils/RNSVGMaskUtils.h diff --git a/apple/Utils/RNSVGMaskUtils.h b/apple/Utils/RNSVGMaskUtils.h deleted file mode 100644 index 710cefb78..000000000 --- a/apple/Utils/RNSVGMaskUtils.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef LuminanceToAlpha_h -#define LuminanceToAlpha_h - -static CIImage *transparentImage() -{ - static CIImage *transparentImage = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - CIFilter *transparent = [CIFilter filterWithName:@"CIConstantColorGenerator"]; - [transparent setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.0] forKey:@"inputColor"]; - transparentImage = [transparent valueForKey:@"outputImage"]; - }); - return transparentImage; -} - -static CIImage *blackImage() -{ - static CIImage *blackImage = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - CIFilter *black = [CIFilter filterWithName:@"CIConstantColorGenerator"]; - [black setValue:[CIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0] forKey:@"inputColor"]; - blackImage = [black valueForKey:@"outputImage"]; - }); - return blackImage; -} - -static CIImage *applyLuminanceToAlphaFilter(CIImage *inputImage) -{ - // https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEMergeElement:~:text=not%20applicable.%20A-,luminanceToAlpha,-operation%20is%20equivalent - CIFilter *luminanceToAlpha = [CIFilter filterWithName:@"CIColorMatrix"]; - [luminanceToAlpha setDefaults]; - CGFloat alpha[4] = {0.2125, 0.7154, 0.0721, 0}; - CGFloat zero[4] = {0, 0, 0, 0}; - [luminanceToAlpha setValue:inputImage forKey:@"inputImage"]; - [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputRVector"]; - [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputGVector"]; - [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBVector"]; - [luminanceToAlpha setValue:[CIVector vectorWithValues:alpha count:4] forKey:@"inputAVector"]; - [luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBiasVector"]; - return [luminanceToAlpha valueForKey:@"outputImage"]; -} - -static CIImage *transformImageIntoAlphaMask(CIImage *inputImage) -{ - // Compose alpha only with luminanceToAlpha results - CIImage *blackBackground = blackImage(); - CIFilter *layerOverBlack = [CIFilter filterWithName:@"CISourceOverCompositing"]; - [layerOverBlack setValue:blackBackground forKey:@"inputBackgroundImage"]; - [layerOverBlack setValue:inputImage forKey:@"inputImage"]; - return applyLuminanceToAlphaFilter([layerOverBlack valueForKey:@"outputImage"]); -} - -static CIImage *applyBlendWithAlphaMask(CIImage *inputImage, CIImage *inputMaskImage) -{ - // Apply mask to the source image with transparent background - CIImage *transparent = transparentImage(); - CIFilter *blendWithAlphaMask = [CIFilter filterWithName:@"CIBlendWithAlphaMask"]; - [blendWithAlphaMask setDefaults]; - [blendWithAlphaMask setValue:inputImage forKey:@"inputImage"]; - [blendWithAlphaMask setValue:transparent forKey:@"inputBackgroundImage"]; - [blendWithAlphaMask setValue:inputMaskImage forKey:@"inputMaskImage"]; - return [blendWithAlphaMask valueForKey:@"outputImage"]; -} - -#endif /* LuminanceToAlpha_h */ From 4db4191b07ad2c562b4455f66d24ae5a8cee03b9 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Sat, 22 Jun 2024 01:33:39 +0200 Subject: [PATCH 10/12] revert: mask back to node --- apple/RNSVGNode.h | 1 + apple/RNSVGNode.mm | 9 +++++++++ apple/RNSVGRenderable.h | 1 - apple/RNSVGRenderable.mm | 9 --------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apple/RNSVGNode.h b/apple/RNSVGNode.h index 8d64f6bef..3c65e102e 100644 --- a/apple/RNSVGNode.h +++ b/apple/RNSVGNode.h @@ -42,6 +42,7 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; @property (nonatomic, assign) CGFloat opacity; @property (nonatomic, assign) RNSVGCGFCRule clipRule; @property (nonatomic, strong) NSString *clipPath; +@property (nonatomic, strong) NSString *mask; @property (nonatomic, strong) NSString *markerStart; @property (nonatomic, strong) NSString *markerMid; @property (nonatomic, strong) NSString *markerEnd; diff --git a/apple/RNSVGNode.mm b/apple/RNSVGNode.mm index bfc09990d..73d345933 100644 --- a/apple/RNSVGNode.mm +++ b/apple/RNSVGNode.mm @@ -285,6 +285,15 @@ - (void)setClipRule:(RNSVGCGFCRule)clipRule [self invalidate]; } +- (void)setMask:(NSString *)mask +{ + if ([_mask isEqualToString:mask]) { + return; + } + _mask = mask; + [self invalidate]; +} + - (void)setMarkerStart:(NSString *)markerStart { if ([_markerStart isEqualToString:markerStart]) { diff --git a/apple/RNSVGRenderable.h b/apple/RNSVGRenderable.h index 32c92dd08..a4f02baab 100644 --- a/apple/RNSVGRenderable.h +++ b/apple/RNSVGRenderable.h @@ -33,7 +33,6 @@ @property (nonatomic, assign) RNSVGVectorEffect vectorEffect; @property (nonatomic, copy) NSArray *propList; @property (nonatomic, assign) CGPathRef hitArea; -@property (nonatomic, strong) NSString *mask; - (void)setHitArea:(CGPathRef)path; diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index a2f957bae..c19bd96d8 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -175,15 +175,6 @@ - (void)setPropList:(NSArray *)propList [self invalidate]; } -- (void)setMask:(NSString *)mask -{ - if ([_mask isEqualToString:mask]) { - return; - } - _mask = mask; - [self invalidate]; -} - - (void)dealloc { CGPathRelease(_hitArea); From 929574be20a8bfb39e9f760ef52b403c3dfad218 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Sat, 22 Jun 2024 12:29:52 +0200 Subject: [PATCH 11/12] chore: remove usused line --- apple/RNSVGRenderable.mm | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index c19bd96d8..636bbb1f7 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -280,13 +280,8 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); - - // CGContextTranslateCTM(bcontext, 0.0, scaledHeight); - // CGContextScaleCTM(bcontext, 1, -1); CGContextConcatCTM(bcontext, currentCTM); - // CGContextScaleCTM(bcontext, iscale, iscale); - // Clip to mask bounds and render the mask CGFloat x = [self relativeOn:[_maskNode x] relative:width]; CGFloat y = [self relativeOn:[_maskNode y] relative:height]; From c2fd79f0f27a1d51479027df1964643a4677718b Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Mon, 24 Jun 2024 12:45:00 +0200 Subject: [PATCH 12/12] fix: prepareForRecycle mask --- apple/RNSVGNode.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/apple/RNSVGNode.mm b/apple/RNSVGNode.mm index 73d345933..2966a1ec5 100644 --- a/apple/RNSVGNode.mm +++ b/apple/RNSVGNode.mm @@ -623,6 +623,7 @@ - (void)prepareForRecycle _opacity = 0; _clipRule = kRNSVGCGFCRuleEvenodd; _clipPath = nil; + _mask = nil; _markerStart = nil; _markerMid = nil; _markerEnd = nil;