Skip to content

Commit

Permalink
feat: implement filter region (#2441)
Browse files Browse the repository at this point in the history
# Summary

Implement proper handling for filter region according to the specs:
*
[FilterEffectsRegion](https://www.w3.org/TR/SVG11/filters.html#FilterEffectsRegion)
*
[FilterPrimitiveSubRegion](https://www.w3.org/TR/SVG11/filters.html#FilterPrimitiveSubRegion)

enabling user to specify 
* `filterUnits`
* `primitiveUnits`
* `x`
* `y`
* `width`
* `height`

on `Filter` element and the last four on filter primitives.

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    ✅      |
| MacOS   |    ✅     |
| Android |    ✅      |
| Web     |    ✅      |
  • Loading branch information
jakex7 authored Sep 12, 2024
1 parent 967886d commit 85be1d0
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 84 deletions.
6 changes: 4 additions & 2 deletions android/src/main/java/com/horcrux/svg/FeOffsetView.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReactContext;
import java.util.HashMap;
Expand Down Expand Up @@ -42,8 +42,10 @@ public Bitmap applyFilter(HashMap<String, Bitmap> resultsMap, Bitmap prevResult)

float dx = this.mDx != null ? (float) this.relativeOnWidth(this.mDx) : 0;
float dy = this.mDy != null ? (float) this.relativeOnHeight(this.mDy) : 0;
RectF frame = new RectF(0, 0, dx, dy);
this.getSvgView().getCtm().mapRect(frame);

canvas.drawBitmap(source, dx, dy, new Paint());
canvas.drawBitmap(source, frame.width(), frame.height(), null);

return result;
}
Expand Down
14 changes: 6 additions & 8 deletions android/src/main/java/com/horcrux/svg/FilterPrimitiveView.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,31 @@

@SuppressLint("ViewConstructor")
class FilterPrimitiveView extends DefinitionView {
SVGLength mX;
SVGLength mY;
SVGLength mW;
SVGLength mH;
private String mResult;
public final FilterRegion mFilterRegion;

public FilterPrimitiveView(ReactContext reactContext) {
super(reactContext);
mFilterRegion = new FilterRegion();
}

public void setX(Dynamic x) {
mX = SVGLength.from(x);
mFilterRegion.setX(x);
invalidate();
}

public void setY(Dynamic y) {
mY = SVGLength.from(y);
mFilterRegion.setY(y);
invalidate();
}

public void setWidth(Dynamic width) {
mW = SVGLength.from(width);
mFilterRegion.setWidth(width);
invalidate();
}

public void setHeight(Dynamic height) {
mH = SVGLength.from(height);
mFilterRegion.setHeight(height);
invalidate();
}

Expand Down
49 changes: 49 additions & 0 deletions android/src/main/java/com/horcrux/svg/FilterRegion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.horcrux.svg;

import android.graphics.Rect;
import android.graphics.RectF;
import com.facebook.react.bridge.Dynamic;

public class FilterRegion {
SVGLength mX;
SVGLength mY;
SVGLength mW;
SVGLength mH;

public void setX(Dynamic x) {
mX = SVGLength.from(x);
}

public void setY(Dynamic y) {
mY = SVGLength.from(y);
}

public void setWidth(Dynamic width) {
mW = SVGLength.from(width);
}

public void setHeight(Dynamic height) {
mH = SVGLength.from(height);
}

public Rect getCropRect(VirtualView view, FilterProperties.Units units, RectF renderableBounds) {
double x, y, width, height;
if (units == FilterProperties.Units.USER_SPACE_ON_USE) {
x = view.relativeOn(this.mX, view.getSvgView().getCanvasWidth());
y = view.relativeOn(this.mY, view.getSvgView().getCanvasHeight());
width = view.relativeOn(this.mW, view.getSvgView().getCanvasWidth());
height = view.relativeOn(this.mH, view.getSvgView().getCanvasHeight());
return new Rect((int) x, (int) y, (int) (x + width), (int) (y + height));
} else { // FilterProperties.Units.OBJECT_BOUNDING_BOX
x = view.relativeOnFraction(this.mX, renderableBounds.width());
y = view.relativeOnFraction(this.mY, renderableBounds.height());
width = view.relativeOnFraction(this.mW, renderableBounds.width());
height = view.relativeOnFraction(this.mH, renderableBounds.height());
return new Rect(
(int) (renderableBounds.left + x),
(int) (renderableBounds.top + y),
(int) (renderableBounds.left + x + width),
(int) (renderableBounds.top + y + height));
}
}
}
49 changes: 20 additions & 29 deletions android/src/main/java/com/horcrux/svg/FilterView.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import android.view.View;
import com.facebook.react.bridge.Dynamic;
Expand All @@ -14,37 +16,32 @@
class FilterView extends DefinitionView {
private final HashMap<String, Bitmap> mResultsMap = new HashMap<>();

SVGLength mX;
SVGLength mY;
SVGLength mW;
SVGLength mH;

private FilterProperties.Units mFilterUnits;

@SuppressWarnings({"FieldCanBeLocal", "unused"})
private FilterProperties.Units mPrimitiveUnits;
private final FilterRegion mFilterRegion;

public FilterView(ReactContext reactContext) {
super(reactContext);
mFilterRegion = new FilterRegion();
}

public void setX(Dynamic x) {
mX = SVGLength.from(x);
mFilterRegion.setX(x);
invalidate();
}

public void setY(Dynamic y) {
mY = SVGLength.from(y);
mFilterRegion.setY(y);
invalidate();
}

public void setWidth(Dynamic width) {
mW = SVGLength.from(width);
mFilterRegion.setWidth(width);
invalidate();
}

public void setHeight(Dynamic height) {
mH = SVGLength.from(height);
mFilterRegion.setHeight(height);
invalidate();
}

Expand All @@ -68,21 +65,28 @@ void saveDefinition() {
}
}

public Bitmap applyFilter(
Bitmap source, Bitmap background, Rect renderableBounds, Rect canvasBounds) {
public Bitmap applyFilter(Bitmap source, Bitmap background, RectF renderableBounds) {
mResultsMap.clear();
mResultsMap.put("SourceGraphic", source);
mResultsMap.put("SourceAlpha", FilterUtils.applySourceAlphaFilter(source));
mResultsMap.put("BackgroundImage", background);
mResultsMap.put("BackgroundAlpha", FilterUtils.applySourceAlphaFilter(background));

Bitmap res = source;
Bitmap resultBitmap = Bitmap.createBitmap(res.getWidth(), res.getHeight(), res.getConfig());
Canvas canvas = new Canvas(resultBitmap);
Rect cropRect;

for (int i = 0; i < getChildCount(); i++) {
View node = getChildAt(i);
if (node instanceof FilterPrimitiveView) {
FilterPrimitiveView currentFilter = (FilterPrimitiveView) node;
res = currentFilter.applyFilter(mResultsMap, res);
resultBitmap.eraseColor(Color.TRANSPARENT);
cropRect =
currentFilter.mFilterRegion.getCropRect(
currentFilter, this.mPrimitiveUnits, renderableBounds);
canvas.drawBitmap(currentFilter.applyFilter(mResultsMap, res), cropRect, cropRect, null);
res = resultBitmap.copy(Bitmap.Config.ARGB_8888, true);
String resultName = currentFilter.getResult();
if (resultName != null) {
mResultsMap.put(resultName, res);
Expand All @@ -93,21 +97,8 @@ public Bitmap applyFilter(
}

// crop Bitmap to filter coordinates
int x, y, width, height;
if (this.mFilterUnits == FilterProperties.Units.USER_SPACE_ON_USE) {
x = (int) this.relativeOn(this.mX, canvasBounds.width());
y = (int) this.relativeOn(this.mY, canvasBounds.height());
width = (int) this.relativeOn(this.mW, canvasBounds.width());
height = (int) this.relativeOn(this.mH, canvasBounds.height());
} else { // FilterProperties.Units.OBJECT_BOUNDING_BOX
x = (int) this.relativeOnFraction(this.mX, renderableBounds.width());
y = (int) this.relativeOnFraction(this.mY, renderableBounds.height());
width = (int) this.relativeOnFraction(this.mW, renderableBounds.width());
height = (int) this.relativeOnFraction(this.mH, renderableBounds.height());
}
Rect cropRect = new Rect(x, y, x + width, y + height);
Bitmap resultBitmap = Bitmap.createBitmap(res.getWidth(), res.getHeight(), res.getConfig());
Canvas canvas = new Canvas(resultBitmap);
resultBitmap.eraseColor(Color.TRANSPARENT);
cropRect = this.mFilterRegion.getCropRect(this, this.mFilterUnits, renderableBounds);
canvas.drawBitmap(res, cropRect, cropRect, null);
return resultBitmap;
}
Expand Down
22 changes: 14 additions & 8 deletions android/src/main/java/com/horcrux/svg/RenderableView.java
Original file line number Diff line number Diff line change
Expand Up @@ -352,24 +352,30 @@ void render(Canvas canvas, Paint paint, float opacity) {
Paint bitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
canvas.saveLayer(null, bitmapPaint);

Rect canvasBounds = this.getSvgView().getCanvasBounds();
Bitmap backgroundBitmap = this.getSvgView().getCurrentBitmap();

// draw element to self bitmap
Bitmap elementBitmap =
Bitmap.createBitmap(
canvasBounds.width(), canvasBounds.height(), Bitmap.Config.ARGB_8888);
Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
Canvas elementCanvas = new Canvas(elementBitmap);
elementCanvas.setMatrix(canvas.getMatrix());

draw(elementCanvas, paint, opacity);

// get renderableBounds
this.initBounds();
RectF clientRect = this.getClientRect();
if (this instanceof ImageView && clientRect == null) {
return;
}
// apply filters
Bitmap backgroundBitmap = this.getSvgView().getCurrentBitmap();
elementBitmap =
filter.applyFilter(
elementBitmap, backgroundBitmap, elementCanvas.getClipBounds(), canvasBounds);
elementBitmap = filter.applyFilter(elementBitmap, backgroundBitmap, clientRect);

// draw bitmap to canvas
// draw bitmap 1:1 to canvas
int saveCount = canvas.save();
canvas.setMatrix(null);
canvas.drawBitmap(elementBitmap, 0, 0, bitmapPaint);
canvas.restoreToCount(saveCount);
} else {
canvas.saveLayer(null, paint);
draw(canvas, paint, opacity);
Expand Down
12 changes: 12 additions & 0 deletions android/src/main/java/com/horcrux/svg/SvgView.java
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,18 @@ Rect getCanvasBounds() {
return mCanvas.getClipBounds();
}

float getCanvasWidth() {
return mCanvas.getWidth();
}

float getCanvasHeight() {
return mCanvas.getHeight();
}

Matrix getCtm() {
return mCanvas.getMatrix();
}

synchronized void drawChildren(final Canvas canvas) {
mRendered = true;
mCanvas = canvas;
Expand Down
1 change: 1 addition & 0 deletions apple/Elements/RNSVGUse.mm
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
}
CGRect bounds = definedTemplate.clientRect;
self.clientRect = bounds;
self.pathBounds = definedTemplate.pathBounds;

CGAffineTransform current = CGContextGetCTM(context);
CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM);
Expand Down
4 changes: 4 additions & 0 deletions apple/Filters/RNSVGFilter.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#import "RNSVGFilterRegion.h"
#import "RNSVGNode.h"

@interface RNSVGFilter : RNSVGNode
Expand All @@ -14,5 +15,8 @@
renderableBounds:(CGRect)renderableBounds
canvasBounds:(CGRect)canvasBounds
ctm:(CGAffineTransform)ctm;
- (CGContext *)openContext:(CGSize)size;
- (void)endContext:(CGContext *)context;
- (CIImage *)getMaskFromRect:(CGContext *)context rect:(CGRect)rect ctm:(CGAffineTransform)ctm;

@end
70 changes: 64 additions & 6 deletions apple/Filters/RNSVGFilter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ - (void)prepareForRecycle
[super prepareForRecycle];
_x = nil;
_y = nil;
_height = nil;
_width = nil;
_height = nil;
_filterUnits = kRNSVGUnitsObjectBoundingBox;
_primitiveUnits = kRNSVGUnitsUserSpaceOnUse;
}
Expand All @@ -99,14 +99,32 @@ - (CIImage *)applyFilter:(CIImage *)img
[resultsMap setObject:backgroundImg forKey:@"BackgroundImage"];
[resultsMap setObject:applySourceAlphaFilter(backgroundImg) forKey:@"BackgroundAlpha"];

// Setup crop filter
CGRect cropRect;
CIFilter *cropFilter = [CIFilter filterWithName:@"CIBlendWithMask"];
[cropFilter setDefaults];
[cropFilter setValue:nil forKey:@"inputBackgroundImage"];
CGContext *cropContext = [self openContext:canvasBounds.size];
CIImage *mask;

CIImage *result = img;
RNSVGFilterPrimitive *currentFilter;
for (RNSVGNode *node in self.subviews) {
if ([node isKindOfClass:[RNSVGFilterPrimitive class]]) {
currentFilter = (RNSVGFilterPrimitive *)node;
CGImageRef cgResult = [[RNSVGRenderUtils sharedCIContext] createCGImage:[currentFilter applyFilter:resultsMap
previousFilterResult:result
ctm:ctm]
cropRect = [[RNSVGFilterRegion regionWithX:currentFilter.x
y:currentFilter.y
width:currentFilter.width
height:currentFilter.height] getCropRect:currentFilter
units:self.primitiveUnits
renderableBounds:renderableBounds];
mask = [self getMaskFromRect:cropContext rect:cropRect ctm:ctm];
[cropFilter setValue:[currentFilter applyFilter:resultsMap previousFilterResult:result ctm:ctm]
forKey:@"inputImage"];
[cropFilter setValue:mask forKey:@"inputMaskImage"];
CGContextClearRect(cropContext, canvasBounds);

CGImageRef cgResult = [[RNSVGRenderUtils sharedCIContext] createCGImage:[cropFilter valueForKey:@"outputImage"]
fromRect:[result extent]];
result = [CIImage imageWithCGImage:cgResult];
CGImageRelease(cgResult);
Expand All @@ -118,8 +136,48 @@ - (CIImage *)applyFilter:(CIImage *)img
}
}

return result;
// TODO: Crop element to filter's x, y, width, height
cropRect = [[RNSVGFilterRegion regionWithX:self.x y:self.y width:self.width
height:self.height] getCropRect:self
units:self.filterUnits
renderableBounds:renderableBounds];
mask = [self getMaskFromRect:cropContext rect:cropRect ctm:ctm];
[cropFilter setValue:result forKey:@"inputImage"];
[cropFilter setValue:mask forKey:@"inputMaskImage"];
[self endContext:cropContext];
return [cropFilter valueForKey:@"outputImage"];
}

- (CGContext *)openContext:(CGSize)size
{
UIGraphicsBeginImageContextWithOptions(size, NO, 1.0);
CGContextRef cropContext = UIGraphicsGetCurrentContext();
#if TARGET_OS_OSX
CGFloat scale = [RNSVGRenderUtils getScreenScale];
CGContextScaleCTM(cropContext, scale, scale);
#else
CGContextTranslateCTM(cropContext, 0, size.height);
CGContextScaleCTM(cropContext, 1, -1);
#endif
return cropContext;
}

- (void)endContext:(CGContext *)context
{
UIGraphicsEndImageContext();
}

- (CIImage *)getMaskFromRect:(CGContext *)context rect:(CGRect)rect ctm:(CGAffineTransform)ctm
{
CGPathRef path = CGPathCreateWithRect(rect, nil);
path = CGPathCreateCopyByTransformingPath(path, &ctm);

CGContextSetRGBFillColor(context, 255, 255, 255, 255);
CGContextAddPath(context, path);
CGContextFillPath(context);

CGImageRef maskImage = CGBitmapContextCreateImage(context);

return [CIImage imageWithCGImage:maskImage];
}

static CIFilter *sourceAlphaFilter()
Expand Down
Loading

0 comments on commit 85be1d0

Please sign in to comment.