From 1c518c98960629dd9fc81d8ec89b3229f68bf108 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sat, 2 Feb 2019 02:11:42 +0200 Subject: [PATCH] Allow int32ARGBColor and use it to represent colors instead of rgbaArray int32ARGBColor is 0xaarrggbb format to allow no processing Optimize default fill handling Improve gradient input validation Simplify gradient extraction Use a single array to represent gradient, with two numbers per stop Reuse transform props in extractProps, short circuit identity transform. [android] Refactor ImageView, fix mLoading --- .../src/main/java/com/horcrux/svg/Brush.java | 16 ++- .../main/java/com/horcrux/svg/ImageView.java | 100 ++++++++++-------- .../java/com/horcrux/svg/RenderableView.java | 19 ++-- index.d.ts | 10 +- ios/Utils/RCTConvert+RNSVG.m | 38 ++++--- lib/Matrix2D.js | 24 +++-- lib/extract/extractBrush.js | 45 ++++---- lib/extract/extractColor.js | 61 +++++++++-- lib/extract/extractFill.js | 12 ++- lib/extract/extractGradient.js | 27 ++--- lib/extract/extractProps.js | 9 +- lib/extract/extractTransform.js | 2 +- 12 files changed, 228 insertions(+), 135 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/Brush.java b/android/src/main/java/com/horcrux/svg/Brush.java index dc3ca2dc..4ca247d8 100644 --- a/android/src/main/java/com/horcrux/svg/Brush.java +++ b/android/src/main/java/com/horcrux/svg/Brush.java @@ -65,15 +65,13 @@ class Brush { } private static void parseGradientStops(ReadableArray value, int stopsCount, float[] stops, int[] stopsColors, float opacity) { - int startStops = value.size() - stopsCount; for (int i = 0; i < stopsCount; i++) { - stops[i] = (float) value.getDouble(startStops + i); - stopsColors[i] = Color.argb( - (int) (value.getDouble(i * 4 + 3) * 255 * opacity), - (int) (value.getDouble(i * 4) * 255), - (int) (value.getDouble(i * 4 + 1) * 255), - (int) (value.getDouble(i * 4 + 2) * 255)); - + int stopIndex = i * 2; + stops[i] = (float) value.getDouble(stopIndex); + int color = value.getInt(stopIndex + 1); + int alpha = color >>> 24; + int combined = Math.round((float)alpha * opacity); + stopsColors[i] = combined << 24 | (color & 0x00ffffff); } } @@ -157,7 +155,7 @@ class Brush { return; } - int stopsCount = mColors.size() / 5; + int stopsCount = mColors.size() / 2; int[] stopsColors = new int[stopsCount]; float[] stops = new float[stopsCount]; parseGradientStops(mColors, stopsCount, stops, stopsColors, opacity); diff --git a/android/src/main/java/com/horcrux/svg/ImageView.java b/android/src/main/java/com/horcrux/svg/ImageView.java index f24ae2be..0b6382f0 100644 --- a/android/src/main/java/com/horcrux/svg/ImageView.java +++ b/android/src/main/java/com/horcrux/svg/ImageView.java @@ -23,11 +23,11 @@ import com.facebook.common.logging.FLog; import com.facebook.common.references.CloseableReference; import com.facebook.datasource.DataSource; import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.core.ImagePipeline; import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; import com.facebook.imagepipeline.image.CloseableBitmap; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableMap; @@ -121,13 +121,15 @@ class ImageView extends RenderableView { @Override void draw(final Canvas canvas, final Paint paint, final float opacity) { if (!mLoading.get()) { - final ImageSource imageSource = new ImageSource(mContext, uriString); + ImagePipeline imagePipeline = Fresco.getImagePipeline(); + ImageSource imageSource = new ImageSource(mContext, uriString); + ImageRequest request = ImageRequest.fromUri(imageSource.getUri()); + boolean inMemoryCache = imagePipeline.isInBitmapMemoryCache(request); - final ImageRequest request = ImageRequestBuilder.newBuilderWithSource(imageSource.getUri()).build(); - if (Fresco.getImagePipeline().isInBitmapMemoryCache(request)) { - tryRender(request, canvas, paint, opacity * mOpacity); + if (inMemoryCache) { + tryRenderFromBitmapCache(imagePipeline, request, canvas, paint, opacity * mOpacity); } else { - loadBitmap(request); + loadBitmap(imagePipeline, request); } } } @@ -139,29 +141,29 @@ class ImageView extends RenderableView { return path; } - private void loadBitmap(ImageRequest request) { + private void loadBitmap(final ImagePipeline imagePipeline, final ImageRequest request) { + mLoading.set(true); final DataSource> dataSource - = Fresco.getImagePipeline().fetchDecodedImage(request, mContext); - dataSource.subscribe(new BaseBitmapDataSubscriber() { - @Override - public void onNewResultImpl(Bitmap bitmap) { - mLoading.set(false); - SvgView view = getSvgView(); - if (view != null) { - view.invalidate(); - } - } + = imagePipeline.fetchDecodedImage(request, mContext); + BaseBitmapDataSubscriber subscriber = new BaseBitmapDataSubscriber() { + @Override + public void onNewResultImpl(Bitmap bitmap) { + mLoading.set(false); + SvgView view = getSvgView(); + if (view != null) { + view.invalidate(); + } + } - @Override - public void onFailureImpl(DataSource dataSource) { - // No cleanup required here. - // TODO: more details about this failure - mLoading.set(false); - FLog.w(ReactConstants.TAG, dataSource.getFailureCause(), "RNSVG: fetchDecodedImage failed!"); - } - }, - UiThreadImmediateExecutorService.getInstance() - ); + @Override + public void onFailureImpl(DataSource dataSource) { + // No cleanup required here. + // TODO: more details about this failure + mLoading.set(false); + FLog.w(ReactConstants.TAG, dataSource.getFailureCause(), "RNSVG: fetchDecodedImage failed!"); + } + }; + dataSource.subscribe(subscriber, UiThreadImmediateExecutorService.getInstance()); } @Nonnull @@ -177,7 +179,7 @@ class ImageView extends RenderableView { h = mImageHeight * mScale; } - return new RectF((float)x, (float)y, (float)(x + w), (float)(y + h)); + return new RectF((float) x, (float) y, (float) (x + w), (float) (y + h)); } private void doRender(Canvas canvas, Paint paint, Bitmap bitmap, float opacity) { @@ -212,27 +214,37 @@ class ImageView extends RenderableView { this.setClientRect(vbRect); } - private void tryRender(ImageRequest request, Canvas canvas, Paint paint, float opacity) { + private void tryRenderFromBitmapCache(ImagePipeline imagePipeline, ImageRequest request, Canvas canvas, Paint paint, float opacity) { final DataSource> dataSource - = Fresco.getImagePipeline().fetchImageFromBitmapCache(request, mContext); + = imagePipeline.fetchImageFromBitmapCache(request, mContext); try { final CloseableReference imageReference = dataSource.getResult(); - if (imageReference != null) { - try { - if (imageReference.get() instanceof CloseableBitmap) { - final Bitmap bitmap = ((CloseableBitmap) imageReference.get()).getUnderlyingBitmap(); - - if (bitmap != null) { - doRender(canvas, paint, bitmap, opacity); - } - } - } catch (Exception e) { - throw new IllegalStateException(e); - } finally { - CloseableReference.closeSafely(imageReference); - } + if (imageReference == null) { + return; } + + try { + CloseableImage closeableImage = imageReference.get(); + if (!(closeableImage instanceof CloseableBitmap)) { + return; + } + + CloseableBitmap closeableBitmap = (CloseableBitmap) closeableImage; + final Bitmap bitmap = closeableBitmap.getUnderlyingBitmap(); + + if (bitmap == null) { + return; + } + + doRender(canvas, paint, bitmap, opacity); + + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + CloseableReference.closeSafely(imageReference); + } + } catch (Exception e) { throw new IllegalStateException(e); } finally { diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index 11f3f49a..d0f54d4b 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -403,12 +403,19 @@ abstract public class RenderableView extends VirtualView { int colorType = colors.getInt(0); switch (colorType) { case 0: - // solid color - paint.setARGB( - (int) (colors.size() > 4 ? colors.getDouble(4) * opacity * 255 : opacity * 255), - (int) (colors.getDouble(1) * 255), - (int) (colors.getDouble(2) * 255), - (int) (colors.getDouble(3) * 255)); + if (colors.size() == 2) { + int color = colors.getInt(1); + int alpha = color >>> 24; + int combined = Math.round((float)alpha * opacity); + paint.setColor(combined << 24 | (color & 0x00ffffff)); + } else { + // solid color + paint.setARGB( + (int) (colors.size() > 4 ? colors.getDouble(4) * opacity * 255 : opacity * 255), + (int) (colors.getDouble(1) * 255), + (int) (colors.getDouble(2) * 255), + (int) (colors.getDouble(3) * 255)); + } break; case 1: { Brush brush = getSvgView().getDefinedBrush(colors.getString(1)); diff --git a/index.d.ts b/index.d.ts index 100b86bd..ec02938e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -87,8 +87,12 @@ export interface ResponderProps extends ReactNative.GestureResponderHandlers { // rgbaArray = [r, g, b, a] type rgbaArray = ReadonlyArray +// argb values inside range 0x00 to 0xff inclusive +// int32ARGBColor = 0xaarrggbb +type int32ARGBColor = number + export interface FillProps { - fill?: rgbaArray | string, + fill?: int32ARGBColor | rgbaArray | string, fillOpacity?: NumberProp, fillRule?: FillRule, } @@ -103,7 +107,7 @@ export interface DefinitionProps { } export interface StrokeProps { - stroke?: rgbaArray | string, + stroke?: int32ARGBColor | rgbaArray | string, strokeWidth?: NumberProp, strokeOpacity?: NumberProp, strokeDasharray?: ReadonlyArray | NumberProp, @@ -296,7 +300,7 @@ export interface RectProps extends CommonPathProps { export const Rect: React.ComponentClass; export interface StopProps { - stopColor?: rgbaArray | string, + stopColor?: int32ARGBColor | rgbaArray | string, stopOpacity?: NumberProp, offset?: NumberProp, } diff --git a/ios/Utils/RCTConvert+RNSVG.m b/ios/Utils/RCTConvert+RNSVG.m index 37b099bc..912b41a0 100644 --- a/ios/Utils/RCTConvert+RNSVG.m +++ b/ios/Utils/RCTConvert+RNSVG.m @@ -125,6 +125,9 @@ RCT_ENUM_CONVERTER(RNSVGUnits, (@{ + (CGColorRef)RNSVGCGColor:(id)json offset:(NSUInteger)offset { NSArray *arr = [self NSArray:json]; + if (arr.count == offset + 1) { + return [self CGColor:[arr objectAtIndex:offset]]; + } if (arr.count < offset + 4) { RCTLogError(@"Too few elements in array (expected at least %zd): %@", (ssize_t)(4 + offset), arr); return nil; @@ -134,25 +137,36 @@ RCT_ENUM_CONVERTER(RNSVGUnits, (@{ + (CGGradientRef)RNSVGCGGradient:(id)json { - NSArray *arr = [self NSNumberArray:json]; - NSUInteger count = arr.count; - if (count < 5) { - RCTLogError(@"Too few elements in array (expected at least %zd): %@", 5l, arr); - return nil; - } - - CGFloat colorsAndOffsets[count]; + NSArray *arr = [self NSArray:json]; + NSUInteger count = arr.count / 2; + NSUInteger values = count * 5; + NSUInteger offsetIndex = values - count; + CGFloat colorsAndOffsets[values]; for (NSUInteger i = 0; i < count; i++) { - colorsAndOffsets[i] = (CGFloat)[arr[i] doubleValue]; + NSUInteger stopIndex = i * 2; + CGFloat offset = (CGFloat)[arr[stopIndex] doubleValue]; + NSUInteger argb = [self NSUInteger:arr[stopIndex + 1]]; + + CGFloat a = ((argb >> 24) & 0xFF) / 255.0; + CGFloat r = ((argb >> 16) & 0xFF) / 255.0; + CGFloat g = ((argb >> 8) & 0xFF) / 255.0; + CGFloat b = (argb & 0xFF) / 255.0; + + NSUInteger colorIndex = i * 4; + colorsAndOffsets[colorIndex] = r; + colorsAndOffsets[colorIndex + 1] = g; + colorsAndOffsets[colorIndex + 2] = b; + colorsAndOffsets[colorIndex + 3] = a; + + colorsAndOffsets[offsetIndex + i] = offset; } - size_t stops = count / 5; CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); CGGradientRef gradient = CGGradientCreateWithColorComponents( rgb, colorsAndOffsets, - colorsAndOffsets + stops * 4, - stops + colorsAndOffsets + offsetIndex, + count ); CGColorSpaceRelease(rgb); return (CGGradientRef)CFAutorelease(gradient); diff --git a/lib/Matrix2D.js b/lib/Matrix2D.js index a61effa1..763e607a 100644 --- a/lib/Matrix2D.js +++ b/lib/Matrix2D.js @@ -69,13 +69,14 @@ export default class Matrix2D { /** * Reset current matrix to an identity matrix. * @method reset - * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ reset = function() { + if (this.hasInitialState) { + return; + } this.a = this.d = 1; this.b = this.c = this.tx = this.ty = 0; this.hasInitialState = true; - return this; }; /** @@ -97,7 +98,6 @@ export default class Matrix2D { * @param {Number} d * @param {Number} tx * @param {Number} ty - * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ append = function(a, b, c, d, tx, ty) { if (this.hasInitialState) { @@ -108,7 +108,7 @@ export default class Matrix2D { this.d = d; this.tx = tx; this.ty = ty; - return this; + return; } const a1 = this.a; const b1 = this.b; @@ -122,7 +122,6 @@ export default class Matrix2D { } this.tx = a1 * tx + c1 * ty + this.tx; this.ty = b1 * tx + d1 * ty + this.ty; - return this; }; /** @@ -141,7 +140,6 @@ export default class Matrix2D { * @param {Number} skewY * @param {Number} regX Optional. * @param {Number} regY Optional. - * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ appendTransform = function( x, @@ -154,6 +152,19 @@ export default class Matrix2D { regX, regY, ) { + if ( + x === 0 && + y === 0 && + scaleX === 1 && + scaleY === 1 && + rotation === 0 && + skewX === 0 && + skewY === 0 && + regX === 0 && + regY === 0 + ) { + return; + } let cos, sin; if (rotation % 360) { const r = rotation * DEG_TO_RAD; @@ -182,6 +193,5 @@ export default class Matrix2D { this.tx -= regX * this.a + regY * this.c; this.ty -= regX * this.b + regY * this.d; } - return this; }; } diff --git a/lib/extract/extractBrush.js b/lib/extract/extractBrush.js index c0e6550b..2b913650 100644 --- a/lib/extract/extractBrush.js +++ b/lib/extract/extractBrush.js @@ -2,30 +2,33 @@ import extractColor from './extractColor'; const urlIdPattern = /^url\(#(.+?)\)$/; -export default function extractBrush(colorOrBrush) { - if (!colorOrBrush || colorOrBrush === 'none') { +const currentColorBrush = [2]; + +export default function extractBrush(color) { + if (typeof color === 'number') { + if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) { + return [0, color]; + } + } + + if (!color || color === 'none') { return null; } - if (colorOrBrush === 'currentColor') { - return [2]; + if (color === 'currentColor') { + return currentColorBrush; } - try { - const matched = typeof colorOrBrush === 'string' && colorOrBrush.match(urlIdPattern); - // brush - if (matched) { - return [1, matched[1]]; - } else { - // solid color - const color = extractColor(colorOrBrush); - const r = color[0]; - const g = color[1]; - const b = color[2]; - const a = color[3]; - return [0, r, g, b, a === undefined ? 1 : a]; - } - } catch (err) { - console.warn(`"${colorOrBrush}" is not a valid color or brush`); - return null; + + const brush = typeof color === 'string' && color.match(urlIdPattern); + if (brush) { + return [1, brush[1]]; } + + const int32ARGBColor = extractColor(color); + if (typeof int32ARGBColor === 'number') { + return [0, int32ARGBColor]; + } + + console.warn(`"${color}" is not a valid color or brush`); + return null; } diff --git a/lib/extract/extractColor.js b/lib/extract/extractColor.js index aed8dab7..f8856b72 100644 --- a/lib/extract/extractColor.js +++ b/lib/extract/extractColor.js @@ -1,3 +1,5 @@ +import { Platform } from 'react-native'; + export const colorNames = { aliceblue: [240, 248, 255], antiquewhite: [250, 235, 215], @@ -151,10 +153,20 @@ export const colorNames = { for (const name in colorNames) { if (colorNames.hasOwnProperty(name)) { const color = colorNames[name]; - for (let i = 0; i < 3; i++) { - color[i] = color[i] / 255; + const r = color[0]; + const g = color[1]; + const b = color[2]; + + let int32Color = (0xff000000 | (r << 16) | (g << 8) | b) >>> 0; + + if (Platform.OS === 'android') { + // Android use 32 bit *signed* integer to represent the color + // We utilize the fact that bitwise operations in JS also operates on + // signed 32 bit integers, so that we can use those to convert from + // *unsigned* to *signed* 32bit int that way. + int32Color = int32Color | 0x0; } - Object.freeze(color); + colorNames[name] = int32Color; } } Object.freeze(colorNames); @@ -390,11 +402,7 @@ function rgbFromHwbString(string) { return hwbToRgb([h, w, b, a]); } -export default function extractColor(string) { - if (typeof string !== 'string') { - return string; - } - +function colorFromString(string) { const prefix = string.substring(0, 3).toLowerCase(); switch (prefix) { @@ -406,3 +414,40 @@ export default function extractColor(string) { return rgbFromString(string); } } + +// Returns 0xaarrggbb or null +export default function extractColor(color) { + if (typeof color === 'number') { + if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) { + return color; + } + return null; + } + + const parsedColor = + typeof color === 'string' ? colorFromString(color) : color; + if (!Array.isArray(parsedColor)) { + return parsedColor; + } + + const r = parsedColor[0]; + const g = parsedColor[1]; + const b = parsedColor[2]; + const a = parsedColor[3]; + + let int32Color = + ((a === undefined ? 0xff000000 : Math.round(a * 255) << 24) | + (Math.round(r * 255) << 16) | + (Math.round(g * 255) << 8) | + Math.round(b * 255)) >>> + 0; + + if (Platform.OS === 'android') { + // Android use 32 bit *signed* integer to represent the color + // We utilize the fact that bitwise operations in JS also operates on + // signed 32 bit integers, so that we can use those to convert from + // *unsigned* to *signed* 32bit int that way. + int32Color = int32Color | 0x0; + } + return int32Color; +} diff --git a/lib/extract/extractFill.js b/lib/extract/extractFill.js index 0c4781d9..e5bbcd71 100644 --- a/lib/extract/extractFill.js +++ b/lib/extract/extractFill.js @@ -1,5 +1,6 @@ import extractBrush from './extractBrush'; import extractOpacity from './extractOpacity'; +import { colorNames } from './extractColor'; const fillRules = { evenodd: 0, @@ -9,6 +10,9 @@ const fillRules = { const fillProps = ['fill', 'fillOpacity', 'fillRule']; const numFillProps = fillProps.length; +// default fill is black +const defaultFill = [0, colorNames.black]; + export default function extractFill(props, styleProperties) { for (let i = 0; i < numFillProps; i++) { const name = fillProps[i]; @@ -17,10 +21,10 @@ export default function extractFill(props, styleProperties) { } } + const { fill, fillRule, fillOpacity } = props; return { - // default fill is black - fill: extractBrush(props.fill || '#000'), - fillOpacity: extractOpacity(props.fillOpacity), - fillRule: fillRules[props.fillRule] === 0 ? 0 : 1, + fill: !fill && typeof fill !== 'number' ? defaultFill : extractBrush(fill), + fillRule: fillRules[fillRule] === 0 ? 0 : 1, + fillOpacity: extractOpacity(fillOpacity), }; } diff --git a/lib/extract/extractGradient.js b/lib/extract/extractGradient.js index b2d01ea3..f6b42b86 100644 --- a/lib/extract/extractGradient.js +++ b/lib/extract/extractGradient.js @@ -46,34 +46,29 @@ export default function extractGradient(props, parent) { props: { offset, stopColor, stopOpacity }, } = childArray[i]; const offsetNumber = percentToFloat(offset); - if (stopColor && !isNaN(offsetNumber)) { - const color = extractColor(stopColor); - if (!color) { - console.warn(`"${stopColor}" is not a valid color`); - continue; - } - const r = color[0]; - const g = color[1]; - const b = color[2]; - const a = extractOpacity(stopOpacity); - stops.push([offsetNumber, r, g, b, a]); + const color = stopColor && extractColor(stopColor); + if (typeof color !== 'number' || isNaN(offsetNumber)) { + console.warn( + `"${stopColor}" is not a valid color or "${offset}" is not a valid offset`, + ); + continue; } + const alpha = Math.round(extractOpacity(stopOpacity) * 255); + stops.push([offsetNumber, (color & 0x00ffffff) | (alpha << 24)]); } stops.sort(offsetComparator); - const colors = []; - const offsets = []; + const gradient = []; const k = stops.length; for (let j = 0; j < k; j++) { const s = stops[j]; - offsets.push(s[0]); - colors.push(s[1], s[2], s[3], s[4]); + gradient.push(s[0], s[1]); } return { name: id, + gradient, children: childArray, - gradient: colors.concat(offsets), gradientUnits: units[gradientUnits] || 0, gradientTransform: extractTransform( gradientTransform || transform || props, diff --git a/lib/extract/extractProps.js b/lib/extract/extractProps.js index d91e14f3..33cfa006 100644 --- a/lib/extract/extractProps.js +++ b/lib/extract/extractProps.js @@ -1,6 +1,6 @@ import extractFill from './extractFill'; import extractStroke from './extractStroke'; -import extractTransform, { props2transform } from './extractTransform'; +import { transformToMatrix, props2transform } from './extractTransform'; import extractClipPath from './extractClipPath'; import extractResponder from './extractResponder'; import extractOpacity from './extractOpacity'; @@ -17,13 +17,14 @@ export function propsAndStyles(props) { export default function extractProps(props, ref) { const { opacity, onLayout, id, clipPath, mask } = props; const styleProperties = []; - + const transformProps = props2transform(props); + const matrix = transformToMatrix(transformProps, props.transform); const extractedProps = { + matrix, onLayout, + ...transformProps, propList: styleProperties, opacity: extractOpacity(opacity), - matrix: extractTransform(props), - ...props2transform(props), ...extractResponder(props, ref), ...extractFill(props, styleProperties), ...extractStroke(props, styleProperties), diff --git a/lib/extract/extractTransform.js b/lib/extract/extractTransform.js index e391a347..770cb41f 100644 --- a/lib/extract/extractTransform.js +++ b/lib/extract/extractTransform.js @@ -17,7 +17,7 @@ function appendTransform(props) { ); } -function transformToMatrix(props, transform) { +export function transformToMatrix(props, transform) { pooledMatrix.reset(); appendTransform(props);