Skip to content

Commit

Permalink
feat: properly implement filter region unit USER_SPACE_ON_USE (#2486)
Browse files Browse the repository at this point in the history
# Summary

After deep dive into the specification, I found out that the default
filter subregion is not equal to `0% 0% 100% 100%`, rather the size of
the parent filter region.

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    ✅      |
| MacOS   |    ✅      |
| Android |    ✅      |
  • Loading branch information
jakex7 authored Oct 14, 2024
1 parent 3aae632 commit 8fed774
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 29 deletions.
29 changes: 17 additions & 12 deletions android/src/main/java/com/horcrux/svg/FilterRegion.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,29 @@ public void setHeight(Dynamic height) {
mH = SVGLength.from(height);
}

private double getRelativeOrDefault(
VirtualView view, SVGLength value, float relativeOn, double defaultValue) {
if (value == null || value.unit == SVGLength.UnitType.UNKNOWN) {
return defaultValue;
}
return view.relativeOn(value, relativeOn);
}

public Rect getCropRect(VirtualView view, FilterProperties.Units units, RectF bounds) {
double x, y, width, height;
if (units == FilterProperties.Units.USER_SPACE_ON_USE) {
x = this.mX == null ? 0 : view.relativeOn(this.mX, view.getSvgView().getCanvasWidth());
y = this.mY == null ? 0 : view.relativeOn(this.mY, view.getSvgView().getCanvasHeight());
width = this.mW == null ? 0 : view.relativeOn(this.mW, view.getSvgView().getCanvasWidth());
height = this.mH == null ? 0 : view.relativeOn(this.mH, view.getSvgView().getCanvasHeight());
return new Rect((int) x, (int) y, (int) (x + width), (int) (y + height));
float canvasWidth = view.getSvgView().getCanvasWidth();
float canvasHeight = view.getSvgView().getCanvasHeight();
x = getRelativeOrDefault(view, mX, canvasWidth, bounds.left);
y = getRelativeOrDefault(view, mY, canvasHeight, bounds.top);
width = getRelativeOrDefault(view, mW, canvasWidth, bounds.width());
height = getRelativeOrDefault(view, mH, canvasHeight, bounds.height());
} else { // FilterProperties.Units.OBJECT_BOUNDING_BOX
x = view.relativeOnFraction(this.mX, bounds.width());
y = view.relativeOnFraction(this.mY, bounds.height());
x = bounds.left + view.relativeOnFraction(this.mX, bounds.width());
y = bounds.top + view.relativeOnFraction(this.mY, bounds.height());
width = view.relativeOnFraction(this.mW, bounds.width());
height = view.relativeOnFraction(this.mH, bounds.height());
return new Rect(
(int) (bounds.left + x),
(int) (bounds.top + y),
(int) (bounds.left + x + width),
(int) (bounds.top + y + height));
}
return new Rect((int) x, (int) y, (int) (x + width), (int) (y + height));
}
}
7 changes: 5 additions & 2 deletions android/src/main/java/com/horcrux/svg/FilterView.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ public Bitmap applyFilter(Bitmap source, Bitmap background, RectF renderableBoun
resultBitmap.eraseColor(Color.TRANSPARENT);
cropRect =
currentFilter.mFilterSubregion.getCropRect(
currentFilter, this.mPrimitiveUnits, renderableBounds);
currentFilter,
this.mPrimitiveUnits,
this.mFilterUnits == FilterProperties.Units.USER_SPACE_ON_USE
? new RectF(filterRegionRect)
: renderableBounds);
canvas.drawBitmap(currentFilter.applyFilter(mResultsMap, res), cropRect, cropRect, null);
res = resultBitmap.copy(Bitmap.Config.ARGB_8888, true);
String resultName = currentFilter.getResult();
Expand All @@ -104,7 +108,6 @@ public Bitmap applyFilter(Bitmap source, Bitmap background, RectF renderableBoun

// crop Bitmap to filter coordinates
resultBitmap.eraseColor(Color.TRANSPARENT);

canvas.drawBitmap(res, filterRegionRect, filterRegionRect, null);
return resultBitmap;
}
Expand Down
11 changes: 6 additions & 5 deletions apple/Filters/RNSVGFilter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,16 @@ - (CIImage *)applyFilter:(CIImage *)img
CGContext *cropContext = [self openContext:canvasBounds.size];
CIImage *mask;

CGRect filterRegionRect = [self.filterRegion getCropRect:self units:self.filterUnits bounds:renderableBounds];
CIImage *result = img;
RNSVGFilterPrimitive *currentFilter;
for (RNSVGNode *node in self.subviews) {
if ([node isKindOfClass:[RNSVGFilterPrimitive class]]) {
currentFilter = (RNSVGFilterPrimitive *)node;
cropRect = [currentFilter.filterSubregion getCropRect:currentFilter
units:self.primitiveUnits
bounds:renderableBounds];
cropRect = [currentFilter.filterSubregion
getCropRect:currentFilter
units:self.primitiveUnits
bounds:self.primitiveUnits == kRNSVGUnitsUserSpaceOnUse ? filterRegionRect : renderableBounds];
mask = [self getMaskFromRect:cropContext rect:cropRect ctm:ctm];
[cropFilter setValue:[currentFilter applyFilter:resultsMap previousFilterResult:result ctm:ctm]
forKey:@"inputImage"];
Expand All @@ -131,8 +133,7 @@ - (CIImage *)applyFilter:(CIImage *)img
}
}

cropRect = [currentFilter.filterSubregion getCropRect:self units:self.filterUnits bounds:renderableBounds];
mask = [self getMaskFromRect:cropContext rect:cropRect ctm:ctm];
mask = [self getMaskFromRect:cropContext rect:filterRegionRect ctm:ctm];
[cropFilter setValue:result forKey:@"inputImage"];
[cropFilter setValue:mask forKey:@"inputMaskImage"];
[self endContext:cropContext];
Expand Down
31 changes: 27 additions & 4 deletions apple/Filters/RNSVGFilterRegion.mm
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ - (void)setHeight:(RNSVGLength *)height
_height = height;
}

+ (CGFloat)getRelativeOrDefault:(RNSVGNode *)node
value:(RNSVGLength *)value
relativeOn:(CGFloat)relativeOn
defaultValue:(CGFloat)defaultValue
{
if (value == nil || value.unit == SVG_LENGTHTYPE_UNKNOWN) {
return defaultValue;
}
return [node relativeOn:value relative:relativeOn];
}

- (CGRect)getCropRect:(RNSVGNode *)node units:(RNSVGUnits)units bounds:(CGRect)bounds
{
CGFloat x, y, width, height;
Expand All @@ -48,10 +59,22 @@ - (CGRect)getCropRect:(RNSVGNode *)node units:(RNSVGUnits)units bounds:(CGRect)b
height = [node relativeOnFraction:self.height relative:bounds.size.height];
return CGRectMake(bounds.origin.x + x, bounds.origin.y + y, width, height);
} else { // kRNSVGUnitsUserSpaceOnUse
x = [node relativeOnWidth:self.x];
y = [node relativeOnHeight:self.y];
width = [node relativeOnWidth:self.width];
height = [node relativeOnHeight:self.height];
x = [RNSVGFilterRegion getRelativeOrDefault:node
value:self.x
relativeOn:[node getCanvasWidth]
defaultValue:bounds.origin.x];
y = [RNSVGFilterRegion getRelativeOrDefault:node
value:self.y
relativeOn:[node getCanvasHeight]
defaultValue:bounds.origin.y];
width = [RNSVGFilterRegion getRelativeOrDefault:node
value:self.width
relativeOn:[node getCanvasWidth]
defaultValue:bounds.size.width];
height = [RNSVGFilterRegion getRelativeOrDefault:node
value:self.height
relativeOn:[node getCanvasHeight]
defaultValue:bounds.size.height];
return CGRectMake(x, y, width, height);
}
}
Expand Down
8 changes: 2 additions & 6 deletions src/elements/filters/FilterPrimitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ export default class FilterPrimitive<P> extends Component<
[x: string]: unknown;
root: (FilterPrimitive<P> & NativeMethods) | null = null;

static defaultPrimitiveProps: React.ComponentProps<typeof FilterPrimitive> = {
x: '0%',
y: '0%',
width: '100%',
height: '100%',
};
static defaultPrimitiveProps: React.ComponentProps<typeof FilterPrimitive> =
{};

refMethod: (instance: (FilterPrimitive<P> & NativeMethods) | null) => void = (
instance: (FilterPrimitive<P> & NativeMethods) | null
Expand Down

0 comments on commit 8fed774

Please sign in to comment.