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);