diff --git a/.eslintrc b/.eslintrc index c739b3fc..9fa0ac25 100644 --- a/.eslintrc +++ b/.eslintrc @@ -83,7 +83,7 @@ "curly": 1, // specify curly brace conventions for all control statements "default-case": 0, // require default case in switch statements (off by default) "dot-notation": 1, // encourages use of dot notation whenever possible - "eqeqeq": 1, // require the use of === and !== + "eqeqeq": 0, // require the use of === and !== "guard-for-in": 0, // make sure for-in loops have an if statement (off by default) "no-alert": 0, // disallow the use of alert, confirm, and prompt "no-caller": 1, // disallow use of arguments.caller or arguments.callee diff --git a/README.md b/README.md index fd513fdc..df10776c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Version](https://img.shields.io/npm/v/react-native-svg.svg)](https://www.npmjs.com/package/react-native-svg) [![NPM](https://img.shields.io/npm/dm/react-native-svg.svg)](https://www.npmjs.com/package/react-native-svg) -`react-native-svg` provides SVG support to React Native on iOS and Android. +`react-native-svg` provides SVG support to React Native on iOS and Android, and a compatibility layer for the web. [Check out the demo](https://snack.expo.io/@msand/react-native-svg-example) @@ -70,15 +70,15 @@ # NOTICE: -Due to breaking changes in react-native, the version given in the left column +Due to breaking changes in react-native, the version given in the left column (and higher versions) of react-native-svg only supports the react-native version in the right column (and higher versions, if possible). -It is recommended to use the version of react given in the peer dependencies +It is recommended to use the version of react given in the peer dependencies of the react-native version you are using. The latest version of react-native-svg should always work in a clean react-native project. - + | react-native-svg | react-native | |------------------|--------------| | 3.2.0 | 0.29 | @@ -91,12 +91,16 @@ The latest version of react-native-svg should always work in a clean react-nativ | 5.3.0 | 0.46 | | 5.4.1 | 0.47 | | 5.5.1 | >=0.50 | -| 6.0.0 | >=0.50 | -| 7.0.0 | >=0.57.4 | -| 8.0.0 | >=0.57.4 | +| >=6 | >=0.50 | +| >=7 | >=0.57.4 | +| >=8 | >=0.57.4 | +| >=9 | >=0.57.4 | -Or, include [this PR](https://github.com/facebook/react-native/pull/17842) manually for v7+ stability on android for older RN ( [included in 0.57-stable](https://github.com/facebook/react-native/commit/d9f5319cf0d9828b29d0e350284b22ce29985042) and newer) +Or, include [this PR](https://github.com/facebook/react-native/pull/17842) manually for v7+ stability on android for older RN ( [included in 0.57-stable](https://github.com/facebook/react-native/commit/d9f5319cf0d9828b29d0e350284b22ce29985042) and newer). +The latest version of v6, v7, v8 and v9 should all work in the latest react-native version. + +v7 and newer requires the patch for making android thread safe, to get native animation support. #### Manually @@ -181,7 +185,7 @@ react-native link ``` Make a reproduction of the problem in `App.js` - + ```bash react-native run-ios react-native run-android @@ -193,6 +197,8 @@ Verify that it is still an issue with the latest version. If so, open a new issu react-native info ``` +If you suspect that you've found a spec conformance bug, then you can test using your component in a react-native-web project by forking this codesandbox, to see how different browsers render the same content: https://codesandbox.io/s/pypn6mn3y7 + ### Usage Here's a simple example. To render output like this: @@ -291,9 +297,10 @@ export default () => ( Try [react-native-svg-transformer](https://github.com/kristerkari/react-native-svg-transformer) to get compile time conversion and cached transformations. https://github.com/kristerkari/react-native-svg-transformer#installation-and-configuration -https://github.com/kristerkari/react-native-svg-transformer#for-react-native-v057-or-newer +https://github.com/kristerkari/react-native-svg-transformer#for-react-native-v057-or-newer--expo-sdk-v3100-or-newer + +`metro.config.js` -rn-cli.config.js ```js const { getDefaultConfig } = require("metro-config"); @@ -395,6 +402,33 @@ originY | 0 | Transform originY coordinates for the current obj ``` +Colors set in the Svg element are inherited by its children: + +```html + + + + +``` + +![Pencil](https://raw.githubusercontent.com/react-native-community/react-native-svg/master/screenShoots/pencil.png) + + Code explanation: + + * The fill prop defines the color inside the object. + * The stroke prop defines the color of the line drawn around the object. + * The color prop is a bit special in the sense that it won't color anything by itself, but define a kind of color variable that can be used by children elements. In this example we're defining a "green" color in the Svg element and using it in the second Path element via stroke="currentColor". The "currentColor" is what refers to that "green" value, and it can be used in other props that accept colors too, e.g. fill="currentColor". + ### Rect The element is used to create a rectangle and variations of a rectangle shape: @@ -575,7 +609,7 @@ The following commands are available for path data: * A = elliptical Arc * Z = closepath -`Note:` All of the commands above can also be expressed with lower letters. Capital letters means absolutely positioned, lower cases means relatively positioned. +`Note:` All of the commands above can also be expressed with lower letters. Capital letters means absolutely positioned, lower cases means relatively positioned. See [Path document of SVG](https://www.w3.org/TR/SVG/paths.html) to know parameters for each command. ```html mFontContext = new ArrayList<>(); + final ArrayList mFontContext = new ArrayList<>(); // Unique input attribute lists (only added if node sets a value) private final ArrayList mXsContext = new ArrayList<>(); diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index d0f54d4b..84c29d08 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -56,6 +56,14 @@ abstract public class RenderableView extends VirtualView { private static final int FILL_RULE_EVENODD = 0; static final int FILL_RULE_NONZERO = 1; + // vectorEffect + static final int VECTOR_EFFECT_DEFAULT = 0; + static final int VECTOR_EFFECT_NON_SCALING_STROKE = 1; + static final int VECTOR_EFFECT_INHERIT = 2; + static final int VECTOR_EFFECT_URI = 3; + + public int vectorEffect = VECTOR_EFFECT_DEFAULT; + /* Used in mergeProperties, keep public */ @@ -86,6 +94,12 @@ abstract public class RenderableView extends VirtualView { private static final Pattern regex = Pattern.compile("[0-9.-]+"); + @ReactProp(name = "vectorEffect", defaultInt = VECTOR_EFFECT_DEFAULT) + public void setVectorEffect(int vectorEffect) { + this.vectorEffect = vectorEffect; + invalidate(); + } + @ReactProp(name = "fill") public void setFill(@Nullable Dynamic fill) { if (fill == null || fill.isNull()) { @@ -322,7 +336,14 @@ abstract public class RenderableView extends VirtualView { mPath = getPath(canvas, paint); mPath.setFillType(fillRule); } + boolean nonScalingStroke = vectorEffect == VECTOR_EFFECT_NON_SCALING_STROKE; Path path = mPath; + if (nonScalingStroke) { + Path scaled = new Path(); + mPath.transform(canvas.getMatrix(), scaled); + canvas.setMatrix(null); + path = scaled; + } RectF clientRect = new RectF(); path.computeBounds(clientRect, true); diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index 34d31c18..c69810ac 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -13,10 +13,11 @@ import android.graphics.Matrix; import android.view.View; import android.view.ViewGroup; -import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.JavaOnlyMap; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.MatrixMathHelper; @@ -43,6 +44,7 @@ import static com.facebook.react.uimanager.ViewProps.*; import static com.horcrux.svg.RenderableView.CAP_ROUND; import static com.horcrux.svg.RenderableView.FILL_RULE_NONZERO; import static com.horcrux.svg.RenderableView.JOIN_ROUND; +import static com.horcrux.svg.RenderableView.VECTOR_EFFECT_DEFAULT; /** * ViewManager for all RNSVG views @@ -176,7 +178,9 @@ class RenderableViewManager extends ViewGroupManager { } private static void decomposeMatrix() { - Assertions.assertCondition(sTransformDecompositionArray.length == 16); + if (sTransformDecompositionArray.length != 16) { + throw new AssertionError(); + } // output values final double[] perspective = sMatrixDecompositionContext.perspective; @@ -314,8 +318,12 @@ class RenderableViewManager extends ViewGroupManager { float scale = DisplayMetricsHolder.getScreenDisplayMetrics().density; // The following converts the matrix's perspective to a camera distance - // such that the camera perspective looks the same on Android and iOS - float normalizedCameraDistance = scale * cameraDistance * CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER; + // such that the camera perspective looks the same on Android and iOS. + // The native Android implementation removed the screen density from the + // calculation, so squaring and a normalization value of + // sqrt(5) produces an exact replica with iOS. + // For more information, see https://github.com/facebook/react-native/pull/18302 + float normalizedCameraDistance = scale * scale * cameraDistance * CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER; view.setCameraDistance(normalizedCameraDistance); } @@ -345,6 +353,22 @@ class RenderableViewManager extends ViewGroupManager { public void setFont(GroupView node, @Nullable ReadableMap font) { node.setFont(font); } + + @ReactProp(name = "fontSize") + public void setFontSize(GroupView node, Dynamic fontSize) { + JavaOnlyMap map = new JavaOnlyMap(); + switch (fontSize.getType()) { + case Number: + map.putDouble("fontSize", fontSize.asDouble()); + break; + case String: + map.putString("fontSize", fontSize.asString()); + break; + default: + return; + } + node.setFont(map); + } } @@ -991,17 +1015,26 @@ class RenderableViewManager extends ViewGroupManager { node.setStrokeLinejoin(strokeLinejoin); } + @ReactProp(name = "vectorEffect", defaultInt = VECTOR_EFFECT_DEFAULT) + public void setVectorEffect(RenderableView node, int vectorEffect) { + node.setVectorEffect(vectorEffect); + } + @ReactProp(name = "matrix") public void setMatrix(VirtualView node, Dynamic matrixArray) { node.setMatrix(matrixArray); } @ReactProp(name = "transform") - public void setTransform(VirtualView node, ReadableArray matrix) { - if (matrix == null) { + public void setTransform(VirtualView node, Dynamic matrix) { + if (matrix.getType() != ReadableType.Array) { + return; + } + ReadableArray ma = matrix.asArray(); + if (ma == null) { resetTransformProperty(node); } else { - setTransformProperty(node, matrix); + setTransformProperty(node, ma); } Matrix m = node.getMatrix(); node.mTransform = m; @@ -1028,6 +1061,9 @@ class RenderableViewManager extends ViewGroupManager { if (view!= null) { view.invalidate(); } + if (node instanceof TextView) { + ((TextView)node).getTextContainer().clearChildCache(); + } } @Override diff --git a/android/src/main/java/com/horcrux/svg/SvgView.java b/android/src/main/java/com/horcrux/svg/SvgView.java index 90d855db..74c97d3c 100644 --- a/android/src/main/java/com/horcrux/svg/SvgView.java +++ b/android/src/main/java/com/horcrux/svg/SvgView.java @@ -101,8 +101,19 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC if (mBitmap == null) { mBitmap = drawOutput(); } - if (mBitmap != null) + if (mBitmap != null) { canvas.drawBitmap(mBitmap, 0, 0, null); + if (toDataUrlTask != null) { + toDataUrlTask.run(); + toDataUrlTask = null; + } + } + } + + private Runnable toDataUrlTask = null; + + void setToDataUrlTask(Runnable task) { + toDataUrlTask = task; } @Override @@ -138,6 +149,10 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC private boolean mRendered = false; int mTintColor = 0; + boolean isRendered() { + return mRendered; + } + private void clearChildCache() { if (!mRendered) { return; @@ -238,7 +253,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC return mCanvas.getClipBounds(); } - void drawChildren(final Canvas canvas) { + synchronized void drawChildren(final Canvas canvas) { mRendered = true; mCanvas = canvas; if (mAlign != null) { @@ -298,7 +313,27 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC getHeight(), Bitmap.Config.ARGB_8888); + clearChildCache(); drawChildren(new Canvas(bitmap)); + clearChildCache(); + this.invalidate(); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + bitmap.recycle(); + byte[] bitmapBytes = stream.toByteArray(); + return Base64.encodeToString(bitmapBytes, Base64.DEFAULT); + } + + String toDataURL(int width, int height) { + Bitmap bitmap = Bitmap.createBitmap( + width, + height, + Bitmap.Config.ARGB_8888); + + clearChildCache(); + drawChildren(new Canvas(bitmap)); + clearChildCache(); + this.invalidate(); ByteArrayOutputStream stream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); bitmap.recycle(); diff --git a/android/src/main/java/com/horcrux/svg/SvgViewManager.java b/android/src/main/java/com/horcrux/svg/SvgViewManager.java index 1ef27013..862e22dd 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewManager.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewManager.java @@ -28,9 +28,19 @@ class SvgViewManager extends ReactViewManager { private static final String REACT_CLASS = "RNSVGSvgView"; private static final SparseArray mTagToSvgView = new SparseArray<>(); + private static final SparseArray mTagToRunnable = new SparseArray<>(); static void setSvgView(int tag, SvgView svg) { mTagToSvgView.put(tag, svg); + Runnable task = mTagToRunnable.get(tag); + if (task != null) { + task.run(); + mTagToRunnable.delete(tag); + } + } + + static void runWhenViewIsAvailable(int tag, Runnable task) { + mTagToRunnable.put(tag, task); } static @Nullable SvgView getSvgViewByTag(int tag) { diff --git a/android/src/main/java/com/horcrux/svg/SvgViewModule.java b/android/src/main/java/com/horcrux/svg/SvgViewModule.java index b9ef4c03..42e749e7 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewModule.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewModule.java @@ -13,6 +13,8 @@ import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.UiThreadUtil; class SvgViewModule extends ReactContextBaseJavaModule { SvgViewModule(ReactApplicationContext reactContext) { @@ -24,13 +26,55 @@ class SvgViewModule extends ReactContextBaseJavaModule { return "RNSVGSvgViewManager"; } + static public void toDataURL(final int tag, final ReadableMap options, final Callback successCallback, final int attempt) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + SvgView svg = SvgViewManager.getSvgViewByTag(tag); + + if (svg == null) { + SvgViewManager.runWhenViewIsAvailable(tag, new Runnable() { + @Override + public void run() { + SvgView svg = SvgViewManager.getSvgViewByTag(tag); + if (svg == null) { // Should never happen + return; + } + svg.setToDataUrlTask(new Runnable() { + @Override + public void run() { + toDataURL(tag, options, successCallback, attempt + 1); + } + }); + } + }); + } else if (!svg.isRendered()) { + svg.setToDataUrlTask(new Runnable() { + @Override + public void run() { + toDataURL(tag, options, successCallback, attempt + 1); + } + }); + } else { + if (options != null) { + successCallback.invoke( + svg.toDataURL( + options.getInt("width"), + options.getInt("height") + ) + ); + } else { + successCallback.invoke(svg.toDataURL()); + } + } + } + } + ); + } @ReactMethod - public void toDataURL(int tag, Callback successCallback) { - SvgView svg = SvgViewManager.getSvgViewByTag(tag); - - if (svg != null) { - successCallback.invoke(svg.toDataURL()); - } + public void toDataURL(int tag, ReadableMap options, Callback successCallback) { + toDataURL(tag, options, successCallback, 0); } } diff --git a/android/src/main/java/com/horcrux/svg/TSpanView.java b/android/src/main/java/com/horcrux/svg/TSpanView.java index 32248517..6aff115f 100644 --- a/android/src/main/java/com/horcrux/svg/TSpanView.java +++ b/android/src/main/java/com/horcrux/svg/TSpanView.java @@ -21,6 +21,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.os.Build; +import android.view.View; import android.view.ViewParent; import com.facebook.react.bridge.ReactContext; @@ -47,6 +48,7 @@ class TSpanView extends TextView { private static final String OTF = ".otf"; private static final String TTF = ".ttf"; + private Path mCachedPath; @Nullable String mContent; private TextPathView textPath; ArrayList emoji = new ArrayList<>(); @@ -62,6 +64,17 @@ class TSpanView extends TextView { invalidate(); } + @Override + public void invalidate() { + mCachedPath = null; + super.invalidate(); + } + + void clearCache() { + mCachedPath = null; + super.clearCache(); + } + @Override void draw(Canvas canvas, Paint paint, float opacity) { if (mContent != null) { @@ -88,22 +101,73 @@ class TSpanView extends TextView { @Override Path getPath(Canvas canvas, Paint paint) { - if (mPath != null) { - return mPath; + if (mCachedPath != null) { + return mCachedPath; } if (mContent == null) { - mPath = getGroupPath(canvas, paint); - return mPath; + mCachedPath = getGroupPath(canvas, paint); + return mCachedPath; } setupTextPath(); pushGlyphContext(); - mPath = getLinePath(mContent, paint, canvas); + mCachedPath = getLinePath(mContent, paint, canvas); popGlyphContext(); - return mPath; + return mCachedPath; + } + + double getSubtreeTextChunksTotalAdvance(Paint paint) { + if (!Double.isNaN(cachedAdvance)) { + return cachedAdvance; + } + double advance = 0; + + if (mContent == null) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof TextView) { + TextView text = (TextView)child; + advance += text.getSubtreeTextChunksTotalAdvance(paint); + } + } + cachedAdvance = advance; + return advance; + } + + String line = mContent; + final int length = line.length(); + + if (length == 0) { + cachedAdvance = 0; + return advance; + } + + GlyphContext gc = getTextRootGlyphContext(); + FontData font = gc.getFont(); + applyTextPropertiesToPaint(paint, font); + + double letterSpacing = font.letterSpacing; + final boolean allowOptionalLigatures = letterSpacing == 0 && + font.fontVariantLigatures == FontVariantLigatures.normal; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String required = "'rlig', 'liga', 'clig', 'calt', 'locl', 'ccmp', 'mark', 'mkmk',"; + String defaultFeatures = required + "'kern', "; + if (allowOptionalLigatures) { + String additionalLigatures = "'hlig', 'cala', "; + paint.setFontFeatureSettings(defaultFeatures + additionalLigatures + font.fontFeatureSettings); + } else { + String disableDiscretionaryLigatures = "'liga' 0, 'clig' 0, 'dlig' 0, 'hlig' 0, 'cala' 0, "; + paint.setFontFeatureSettings(defaultFeatures + disableDiscretionaryLigatures + font.fontFeatureSettings); + } + paint.setLetterSpacing((float)(letterSpacing / (font.fontSize * mScale))); + } + + cachedAdvance = paint.measureText(line); + return cachedAdvance; } @SuppressWarnings("ConstantConditions") @@ -311,8 +375,10 @@ class TSpanView extends TextView { attributes, such as a ‘dx’ attribute value on a ‘tspan’ element. */ final TextAnchor textAnchor = font.textAnchor; - final double textMeasure = paint.measureText(line); + TextView anchorRoot = getTextAnchorRoot(); + final double textMeasure = anchorRoot.getSubtreeTextChunksTotalAdvance(paint); double offset = getTextAnchorOffset(textAnchor, textMeasure); + applyTextPropertiesToPaint(paint, font); int side = 1; double startOfRendering = 0; @@ -563,7 +629,7 @@ class TSpanView extends TextView { // this will just retrieve the bounding rect for 'x' paint.getTextBounds("x", 0, 1, bounds); int xHeight = bounds.height(); - baselineShift = xHeight / 2; + baselineShift = xHeight / 2.0; break; case central: @@ -691,7 +757,6 @@ class TSpanView extends TextView { final Matrix end = new Matrix(); final float[] startPointMatrixData = new float[9]; - final float[] midPointMatrixData = new float[9]; final float[] endPointMatrixData = new float[9]; emoji.clear(); @@ -961,6 +1026,9 @@ class TSpanView extends TextView { paint.setTypeface(typeface); paint.setTextSize((float) fontSize); paint.setTextAlign(Paint.Align.LEFT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + paint.setLetterSpacing(0); + } // Do these have any effect for anyone? Not for me (@msand) at least. // paint.setUnderlineText(underlineText); @@ -1000,6 +1068,9 @@ class TSpanView extends TextView { if (mRegion == null && mFillPath != null) { mRegion = getRegion(mFillPath); } + if (mRegion == null && mPath != null) { + mRegion = getRegion(mPath); + } if (mStrokeRegion == null && mStrokePath != null) { mStrokeRegion = getRegion(mStrokePath); } diff --git a/android/src/main/java/com/horcrux/svg/TextView.java b/android/src/main/java/com/horcrux/svg/TextView.java index 22be697d..0fa37376 100644 --- a/android/src/main/java/com/horcrux/svg/TextView.java +++ b/android/src/main/java/com/horcrux/svg/TextView.java @@ -14,6 +14,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Region; +import android.view.View; import android.view.ViewParent; import com.facebook.react.bridge.Dynamic; @@ -38,6 +39,7 @@ class TextView extends GroupView { @Nullable ArrayList mRotate; @Nullable ArrayList mDeltaX; @Nullable ArrayList mDeltaY; + double cachedAdvance = Double.NaN; public TextView(ReactContext reactContext) { super(reactContext); @@ -49,7 +51,12 @@ class TextView extends GroupView { return; } super.invalidate(); - clearChildCache(); + getTextContainer().clearChildCache(); + } + + void clearCache() { + cachedAdvance = Double.NaN; + super.clearCache(); } @ReactProp(name = "textLength") @@ -207,4 +214,45 @@ class TextView extends GroupView { boolean isTextNode = !(this instanceof TextPathView) && !(this instanceof TSpanView); getTextRootGlyphContext().pushContext(isTextNode, this, mFont, mPositionX, mPositionY, mDeltaX, mDeltaY, mRotate); } + + TextView getTextAnchorRoot() { + GlyphContext gc = getTextRootGlyphContext(); + ArrayList font = gc.mFontContext; + TextView node = this; + ViewParent parent = this.getParent(); + for (int i = font.size() - 1; i >= 0; i--) { + if (!(parent instanceof TextView) || font.get(i).textAnchor == TextProperties.TextAnchor.start || node.mPositionX != null) { + return node; + } + node = (TextView) parent; + parent = node.getParent(); + } + return node; + } + + double getSubtreeTextChunksTotalAdvance(Paint paint) { + if (!Double.isNaN(cachedAdvance)) { + return cachedAdvance; + } + double advance = 0; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof TextView) { + TextView text = (TextView) child; + advance += text.getSubtreeTextChunksTotalAdvance(paint); + } + } + cachedAdvance = advance; + return advance; + } + + TextView getTextContainer() { + TextView node = this; + ViewParent parent = this.getParent(); + while (parent instanceof TextView) { + node = (TextView) parent; + parent = node.getParent(); + } + return node; + } } diff --git a/android/src/main/java/com/horcrux/svg/UseView.java b/android/src/main/java/com/horcrux/svg/UseView.java index c5b14d5f..487de47f 100644 --- a/android/src/main/java/com/horcrux/svg/UseView.java +++ b/android/src/main/java/com/horcrux/svg/UseView.java @@ -11,6 +11,7 @@ package com.horcrux.svg; import android.annotation.SuppressLint; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; @@ -66,31 +67,33 @@ class UseView extends RenderableView { void draw(Canvas canvas, Paint paint, float opacity) { VirtualView template = getSvgView().getDefinedTemplate(mHref); - if (template != null) { - canvas.translate((float) relativeOnWidth(mX), (float) relativeOnHeight(mY)); - if (template instanceof RenderableView) { - ((RenderableView)template).mergeProperties(this); - } - - int count = template.saveAndSetupCanvas(canvas); - clip(canvas, paint); - - if (template instanceof SymbolView) { - SymbolView symbol = (SymbolView)template; - symbol.drawSymbol(canvas, paint, opacity, (float) relativeOnWidth(mW), (float) relativeOnHeight(mH)); - } else { - template.draw(canvas, paint, opacity * mOpacity); - } - - this.setClientRect(template.getClientRect()); - - template.restoreCanvas(canvas, count); - if (template instanceof RenderableView) { - ((RenderableView)template).resetProperties(); - } - } else { + if (template == null) { FLog.w(ReactConstants.TAG, "`Use` element expected a pre-defined svg template as `href` prop, " + - "template named: " + mHref + " is not defined."); + "template named: " + mHref + " is not defined."); + return; + } + + template.clearCache(); + canvas.translate((float) relativeOnWidth(mX), (float) relativeOnHeight(mY)); + if (template instanceof RenderableView) { + ((RenderableView)template).mergeProperties(this); + } + + int count = template.saveAndSetupCanvas(canvas); + clip(canvas, paint); + + if (template instanceof SymbolView) { + SymbolView symbol = (SymbolView)template; + symbol.drawSymbol(canvas, paint, opacity, (float) relativeOnWidth(mW), (float) relativeOnHeight(mH)); + } else { + template.draw(canvas, paint, opacity * mOpacity); + } + + this.setClientRect(template.getClientRect()); + + template.restoreCanvas(canvas, count); + if (template instanceof RenderableView) { + ((RenderableView)template).resetProperties(); } } @@ -105,6 +108,12 @@ class UseView extends RenderableView { mInvTransform.mapPoints(dst); VirtualView template = getSvgView().getDefinedTemplate(mHref); + if (template == null) { + FLog.w(ReactConstants.TAG, "`Use` element expected a pre-defined svg template as `href` prop, " + + "template named: " + mHref + " is not defined."); + return -1; + } + int hitChild = template.hitTest(dst); if (hitChild != -1) { return (template.isResponsible() || hitChild != template.getId()) ? hitChild : getId(); @@ -115,7 +124,17 @@ class UseView extends RenderableView { @Override Path getPath(Canvas canvas, Paint paint) { - // todo: - return new Path(); + VirtualView template = getSvgView().getDefinedTemplate(mHref); + if (template == null) { + FLog.w(ReactConstants.TAG, "`Use` element expected a pre-defined svg template as `href` prop, " + + "template named: " + mHref + " is not defined."); + return null; + } + Path path = template.getPath(canvas, paint); + Path use = new Path(); + Matrix m = new Matrix(); + m.setTranslate((float) relativeOnWidth(mX), (float) relativeOnHeight(mY)); + path.transform(m, use); + return use; } } diff --git a/android/src/main/java/com/horcrux/svg/VirtualView.java b/android/src/main/java/com/horcrux/svg/VirtualView.java index 07d28da0..933ab5ad 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualView.java +++ b/android/src/main/java/com/horcrux/svg/VirtualView.java @@ -101,7 +101,7 @@ abstract public class VirtualView extends ReactViewGroup { super.invalidate(); } - private void clearCache() { + void clearCache() { canvasDiagonal = -1; canvasHeight = -1; canvasWidth = -1; diff --git a/elements/ClipPath.js b/elements/ClipPath.js index 1c3c91b7..15a1807f 100644 --- a/elements/ClipPath.js +++ b/elements/ClipPath.js @@ -7,12 +7,13 @@ export default class ClipPath extends Shape { static displayName = 'ClipPath'; render() { - const { id, children } = this.props; + const { props } = this; + const { id, children } = props; return ( {children} diff --git a/elements/Image.js b/elements/Image.js index 7b6e13b5..f4c36b98 100644 --- a/elements/Image.js +++ b/elements/Image.js @@ -39,7 +39,9 @@ export default class SvgImage extends Shape { height={height} meetOrSlice={meetOrSliceTypes[modes[1]] || 0} align={alignEnum[modes[0]] || 'xMidYMid'} - src={Image.resolveAssetSource(href)} + src={Image.resolveAssetSource( + typeof href === 'string' ? { uri: href } : href, + )} /> ); } diff --git a/elements/Svg.js b/elements/Svg.js index 1bac2f4a..2ed783b8 100644 --- a/elements/Svg.js +++ b/elements/Svg.js @@ -49,9 +49,12 @@ export default class Svg extends Shape { this.root.setNativeProps(props); }; - toDataURL = callback => { - callback && - RNSVGSvgViewManager.toDataURL(findNodeHandle(this.root), callback); + toDataURL = (callback, options) => { + if (!callback) { + return; + } + const handle = findNodeHandle(this.root); + RNSVGSvgViewManager.toDataURL(handle, options, callback); }; render() { diff --git a/elements/TSpan.js b/elements/TSpan.js index 2362026d..94872861 100644 --- a/elements/TSpan.js +++ b/elements/TSpan.js @@ -14,30 +14,24 @@ export default class TSpan extends Shape { if (matrix) { props.matrix = matrix; } - const text = pickNotNil(extractText(props, false)); - this.root.setNativeProps({ - ...props, - ...text, - }); + const prop = propsAndStyles(props); + Object.assign(prop, pickNotNil(extractText(prop, false))); + this.root.setNativeProps(prop); }; render() { - const props = this.props; - const prop = propsAndStyles(props); - return ( - + const prop = propsAndStyles(this.props); + const props = extractProps( + { + ...prop, + x: null, + y: null, + }, + this, ); + Object.assign(props, extractText(prop, false)); + props.ref = this.refMethod; + return ; } } diff --git a/elements/Text.js b/elements/Text.js index 043472f7..24a586cd 100644 --- a/elements/Text.js +++ b/elements/Text.js @@ -15,30 +15,24 @@ export default class Text extends Shape { if (matrix) { props.matrix = matrix; } - const text = pickNotNil(extractText(props, true)); - this.root.setNativeProps({ - ...props, - ...text, - }); + const prop = propsAndStyles(props); + Object.assign(prop, pickNotNil(extractText(prop, true))); + this.root.setNativeProps(prop); }; render() { - const props = this.props; - const prop = propsAndStyles(props); - return ( - + const prop = propsAndStyles(this.props); + const props = extractProps( + { + ...prop, + x: null, + y: null, + }, + this, ); + Object.assign(props, extractText(prop, true)); + props.ref = this.refMethod; + return ; } } diff --git a/elements/TextPath.js b/elements/TextPath.js index 3ba7a754..7d2ddd2c 100644 --- a/elements/TextPath.js +++ b/elements/TextPath.js @@ -15,11 +15,8 @@ export default class TextPath extends Shape { if (matrix) { props.matrix = matrix; } - const text = pickNotNil(extractText(props, true)); - this.root.setNativeProps({ - ...props, - ...text, - }); + Object.assign(props, pickNotNil(extractText(props, true))); + this.root.setNativeProps(props); }; render() { @@ -27,45 +24,45 @@ export default class TextPath extends Shape { children, xlinkHref, href = xlinkHref, - startOffset, + startOffset = 0, method, spacing, side, alignmentBaseline, midLine, - ...props + ...prop } = this.props; const matched = href && href.match(idPattern); const match = matched && matched[1]; if (match) { - return ( - + const props = extractProps( + { + ...propsAndStyles(prop), + x: null, + y: null, + }, + this, ); + Object.assign( + props, + extractText( + { + children, + }, + true, + ), + { + href: match, + startOffset, + method, + spacing, + side, + alignmentBaseline, + midLine, + }, + ); + props.ref = this.refMethod; + return ; } console.warn( diff --git a/index.d.ts b/index.d.ts index ec02938e..5f1166e8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -80,7 +80,7 @@ export interface TouchableProps { } export interface ResponderProps extends ReactNative.GestureResponderHandlers { - pointerEvents?: (event: any) => any, + pointerEvents?: "box-none" | "none" | "box-only" | "auto", } // rgba values inside range 0 to 1 inclusive @@ -101,6 +101,10 @@ export interface ClipProps { clipRule?: FillRule, clipPath?: string } + +interface VectorEffectProps { + vectorEffect?: "none" | "non-scaling-stroke" | "nonScalingStroke" | "default" | "inherit" | "uri"; +} export interface DefinitionProps { id?: string, @@ -180,7 +184,7 @@ export interface CommonMaskProps { mask?: string; } -export interface CommonPathProps extends FillProps, StrokeProps, ClipProps, TransformProps, ResponderProps, TouchableProps, DefinitionProps, CommonMaskProps {} +export interface CommonPathProps extends FillProps, StrokeProps, ClipProps, TransformProps, VectorEffectProps, ResponderProps, TouchableProps, DefinitionProps, CommonMaskProps {} // Element props export interface CircleProps extends CommonPathProps { @@ -306,11 +310,13 @@ export interface StopProps { } export const Stop: React.ComponentClass; -export interface SvgProps extends ReactNative.ViewProperties { +export interface SvgProps extends GProps, ReactNative.ViewProperties { width?: NumberProp, height?: NumberProp, viewBox?: string, preserveAspectRatio?: string, + color?: int32ARGBColor | rgbaArray | string, + title?: string, } // Svg is both regular and default exported diff --git a/index.web.js b/index.web.js new file mode 100644 index 00000000..2d830be6 --- /dev/null +++ b/index.web.js @@ -0,0 +1,217 @@ +import { createElement } from 'react-native-web'; +import { resolve } from './lib/resolve'; +import { Component } from 'react'; + +/** + * `react-native-svg` supports additional props that aren't defined in the spec. + * This function replaces them in a spec conforming manner. + * + * @param {Object} props Properties given to us. + * @returns {Object} Cleaned object. + * @private + */ +function prepare(props) { + const { + translate, + scale, + rotation, + skewX, + skewY, + originX, + originY, + fontFamily, + fontSize, + fontWeight, + fontStyle, + style, + ...clean + } = props; + + const transform = []; + + if (originX != null || originY != null) { + transform.push(`translate(${originX || 0}, ${originY || 0})`); + } + if (translate != null) { + transform.push(`translate(${translate})`); + } + if (scale != null) { + transform.push(`scale(${scale})`); + } + // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block) + if (rotation != null) { + transform.push(`rotate(${rotation})`); + } + if (skewX != null) { + transform.push(`skewX(${skewX})`); + } + if (skewY != null) { + transform.push(`skewY(${skewY})`); + } + if (originX != null || originY != null) { + transform.push(`translate(${-originX || 0}, ${-originY || 0})`); + } + + if (transform.length) { + clean.transform = transform.join(' '); + } + + const styles = {}; + + if (fontFamily != null) { + styles.fontFamily = fontFamily; + } + if (fontSize != null) { + styles.fontSize = fontSize; + } + if (fontWeight != null) { + styles.fontWeight = fontWeight; + } + if (fontStyle != null) { + styles.fontStyle = fontStyle; + } + + clean.style = resolve(style, styles); + + return clean; +} + +export class Circle extends Component { + render() { + return createElement('circle', prepare(this.props)); + } +} + +export class ClipPath extends Component { + render() { + return createElement('clipPath', prepare(this.props)); + } +} + +export class Defs extends Component { + render() { + return createElement('defs', prepare(this.props)); + } +} + +export class Ellipse extends Component { + render() { + return createElement('ellipse', prepare(this.props)); + } +} + +export class G extends Component { + render() { + const { x, y, ...rest } = this.props; + + if ((x || y) && !rest.translate) { + rest.translate = `${x || 0}, ${y || 0}`; + } + + return createElement('g', prepare(rest)); + } +} + +export class Image extends Component { + render() { + return createElement('image', prepare(this.props)); + } +} + +export class Line extends Component { + render() { + return createElement('line', prepare(this.props)); + } +} + +export class LinearGradient extends Component { + render() { + return createElement('linearGradient', prepare(this.props)); + } +} + +export class Path extends Component { + render() { + return createElement('path', prepare(this.props)); + } +} + +export class Polygon extends Component { + render() { + return createElement('polygon', prepare(this.props)); + } +} + +export class Polyline extends Component { + render() { + return createElement('polyline', prepare(this.props)); + } +} + +export class RadialGradient extends Component { + render() { + return createElement('radialGradient', prepare(this.props)); + } +} + +export class Rect extends Component { + render() { + return createElement('rect', prepare(this.props)); + } +} + +export class Stop extends Component { + render() { + return createElement('stop', prepare(this.props)); + } +} + +export class Svg extends Component { + render() { + return createElement('svg', prepare(this.props)); + } +} + +export class Symbol extends Component { + render() { + return createElement('symbol', prepare(this.props)); + } +} + +export class Text extends Component { + render() { + return createElement('text', prepare(this.props)); + } +} + +export class TSpan extends Component { + render() { + return createElement('tspan', prepare(this.props)); + } +} + +export class TextPath extends Component { + render() { + return createElement('textPath', prepare(this.props)); + } +} + +export class Use extends Component { + render() { + return createElement('use', prepare(this.props)); + } +} + +export class Mask extends Component { + render() { + return createElement('mask', prepare(this.props)); + } +} + +export class Pattern extends Component { + render() { + return createElement('pattern', prepare(this.props)); + } +} + +export default Svg; diff --git a/ios/Elements/RNSVGGroup.m b/ios/Elements/RNSVGGroup.m index fc75689a..7a26cd67 100644 --- a/ios/Elements/RNSVGGroup.m +++ b/ios/Elements/RNSVGGroup.m @@ -174,7 +174,7 @@ } if (!event) { - NSPredicate *const anyActive = [NSPredicate predicateWithFormat:@"active == TRUE"]; + NSPredicate *const anyActive = [NSPredicate predicateWithFormat:@"self isKindOfClass: %@ AND active == TRUE", [RNSVGNode class]]; NSArray *const filtered = [self.subviews filteredArrayUsingPredicate:anyActive]; if ([filtered count] != 0) { return [filtered.lastObject hitTest:transformed withEvent:event]; diff --git a/ios/Elements/RNSVGImage.m b/ios/Elements/RNSVGImage.m index 3d8dc444..6d7bddf8 100644 --- a/ios/Elements/RNSVGImage.m +++ b/ios/Elements/RNSVGImage.m @@ -10,6 +10,7 @@ #import "RCTConvert+RNSVG.h" #import #import +#import #import #import "RNSVGViewBox.h" diff --git a/ios/Elements/RNSVGSvgView.h b/ios/Elements/RNSVGSvgView.h index 99112606..808c41ab 100644 --- a/ios/Elements/RNSVGSvgView.h +++ b/ios/Elements/RNSVGSvgView.h @@ -52,6 +52,8 @@ - (NSString *)getDataURL; +- (NSString *)getDataURLwithBounds:(CGRect)bounds; + - (CGRect)getContextBounds; - (void)drawRect:(CGRect)rect; diff --git a/ios/Elements/RNSVGSvgView.m b/ios/Elements/RNSVGSvgView.m index 8b71ba30..856a83d4 100644 --- a/ios/Elements/RNSVGSvgView.m +++ b/ios/Elements/RNSVGSvgView.m @@ -247,11 +247,26 @@ return nil; } - - (NSString *)getDataURL { UIGraphicsBeginImageContextWithOptions(_boundingBox.size, NO, 0); + [self clearChildCache]; [self drawRect:_boundingBox]; + [self clearChildCache]; + [self invalidate]; + NSData *imageData = UIImagePNGRepresentation(UIGraphicsGetImageFromCurrentImageContext()); + NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; + UIGraphicsEndImageContext(); + return base64; +} + +- (NSString *)getDataURLwithBounds:(CGRect)bounds +{ + UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0); + [self clearChildCache]; + [self drawRect:bounds]; + [self clearChildCache]; + [self invalidate]; NSData *imageData = UIImagePNGRepresentation(UIGraphicsGetImageFromCurrentImageContext()); NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; UIGraphicsEndImageContext(); diff --git a/ios/Elements/RNSVGUse.m b/ios/Elements/RNSVGUse.m index 5a42b568..14047b06 100644 --- a/ios/Elements/RNSVGUse.m +++ b/ios/Elements/RNSVGUse.m @@ -89,6 +89,9 @@ } else if (self.href) { // TODO: calling yellow box here RCTLogWarn(@"`Use` element expected a pre-defined svg template as `href` prop, template named: %@ is not defined.", self.href); + return; + } else { + return; } CGRect bounds = template.clientRect; self.clientRect = bounds; @@ -120,5 +123,16 @@ return nil; } +- (CGPathRef)getPath: (CGContextRef)context +{ + CGAffineTransform transform = CGAffineTransformMakeTranslation([self relativeOnWidth:self.x], [self relativeOnHeight:self.y]); + RNSVGNode const* template = [self.svgView getDefinedTemplate:self.href]; + if (!template) { + return nil; + } + CGPathRef path = [template getPath:context]; + return CGPathCreateCopyByTransformingPath(path, &transform); +} + @end diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index 8e861247..819a6631 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -251,6 +251,7 @@ 94241669213B0DB800088E93 /* RNSVGPattern.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSVGPattern.h; sourceTree = ""; }; 9424166A213B2FF100088E93 /* RNSVGPatternManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSVGPatternManager.h; sourceTree = ""; }; 9424166C213B302600088E93 /* RNSVGPatternManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSVGPatternManager.m; sourceTree = ""; }; + 94696EE92235A7F200C1D558 /* RNSVGVectorEffect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGVectorEffect.h; path = Utils/RNSVGVectorEffect.h; sourceTree = ""; }; 947F3809214810B800677F2A /* RNSVGMask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGMask.h; path = Elements/RNSVGMask.h; sourceTree = ""; }; 947F380A214810DC00677F2A /* RNSVGMask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGMask.m; path = Elements/RNSVGMask.m; sourceTree = ""; }; 947F380D2148118300677F2A /* RNSVGMaskManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGMaskManager.h; sourceTree = ""; }; @@ -463,6 +464,7 @@ 1039D29A1CE7212C001E90A8 /* Utils */ = { isa = PBXGroup; children = ( + 94696EE92235A7F200C1D558 /* RNSVGVectorEffect.h */, B56895A920352B36004DBF1E /* RNSVGBezierElement.h */, B56895A820352B35004DBF1E /* RNSVGBezierElement.m */, 7F69160D1E3703D800DA6EDC /* RNSVGUnits.h */, diff --git a/ios/RNSVGNode.h b/ios/RNSVGNode.h index bb94d891..83dde07e 100644 --- a/ios/RNSVGNode.h +++ b/ios/RNSVGNode.h @@ -118,4 +118,6 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; - (void)clearChildCache; +- (void)clearPath; + @end diff --git a/ios/RNSVGNode.m b/ios/RNSVGNode.m index 7d491e21..0c622d21 100644 --- a/ios/RNSVGNode.m +++ b/ios/RNSVGNode.m @@ -290,7 +290,7 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; if (_clipMask) { CGImageRelease(_clipMask); } - if ([_clipNode isSimpleClipPath]) { + if ([_clipNode isSimpleClipPath] || _clipNode.clipRule == kRNSVGCGFCRuleEvenodd) { _clipMask = nil; } else { CGRect bounds = CGContextGetClipBoundingBox(context); diff --git a/ios/RNSVGRenderable.h b/ios/RNSVGRenderable.h index d59d48ad..2973f25b 100644 --- a/ios/RNSVGRenderable.h +++ b/ios/RNSVGRenderable.h @@ -12,6 +12,7 @@ #import "RNSVGCGFCRule.h" #import "RNSVGNode.h" #import "RNSVGLength.h" +#import "RNSVGVectorEffect.h" #import "RNSVGPercentageConverter.h" @interface RNSVGRenderable : RNSVGNode @@ -27,6 +28,7 @@ @property (nonatomic, assign) CGFloat strokeMiterlimit; @property (nonatomic, strong) NSArray *strokeDasharray; @property (nonatomic, assign) CGFloat strokeDashoffset; +@property (nonatomic, assign) RNSVGVectorEffect vectorEffect; @property (nonatomic, copy) NSArray *propList; - (void)setHitArea:(CGPathRef)path; diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index d8067aae..e19db35a 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -10,6 +10,7 @@ #import "RNSVGClipPath.h" #import "RNSVGMask.h" #import "RNSVGViewBox.h" +#import "RNSVGVectorEffect.h" @implementation RNSVGRenderable { @@ -143,6 +144,15 @@ _strokeDashoffset = strokeDashoffset; } +- (void)setVectorEffect:(RNSVGVectorEffect)vectorEffect +{ + if (vectorEffect == _vectorEffect) { + return; + } + [self invalidate]; + _vectorEffect = vectorEffect; +} + - (void)setPropList:(NSArray *)propList { if (propList == _propList) { @@ -305,6 +315,11 @@ UInt32 saturate(CGFloat value) { self.clientRect = clientRect; + if (_vectorEffect == kRNSVGVectorEffectNonScalingStroke) { + path = CGPathCreateCopyByTransformingPath(path, &svgToClientTransform); + CGContextConcatCTM(context, CGAffineTransformInvert(svgToClientTransform)); + } + CGAffineTransform vbmatrix = self.svgView.getViewBoxTransform; CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms); CGAffineTransform matrix = CGAffineTransformConcat(transform, vbmatrix); @@ -493,12 +508,12 @@ UInt32 saturate(CGFloat value) { - (void)mergeProperties:(__kindof RNSVGRenderable *)target { - self.merging = true; NSArray *targetAttributeList = [target getAttributeList]; if (targetAttributeList.count == 0) { return; } + self.merging = true; NSMutableArray* attributeList = [self.propList mutableCopy]; _originProperties = [[NSMutableDictionary alloc] init]; diff --git a/ios/Text/RNSVGFontData.m b/ios/Text/RNSVGFontData.m index 0d902ac0..9b9d9805 100644 --- a/ios/Text/RNSVGFontData.m +++ b/ios/Text/RNSVGFontData.m @@ -84,25 +84,40 @@ RNSVGFontData *RNSVGFontData_Defaults; NSString* decoration = [font objectForKey:TEXT_DECORATION]; data->textDecoration = decoration ? RNSVGTextDecorationFromString(decoration) : parent->textDecoration; - NSString* kerning = [font objectForKey:KERNING]; - data->manualKerning = (kerning || parent->manualKerning ); CGFloat fontSize = data->fontSize; - data->kerning = kerning ? - [RNSVGFontData toAbsoluteWithNSString:kerning - fontSize:fontSize] - : parent->kerning; + id kerning = [font objectForKey:KERNING]; + data->manualKerning = (kerning || parent->manualKerning ); + if ([kerning isKindOfClass:NSNumber.class]) { + NSNumber* kern = kerning; + data->kerning = (CGFloat)[kern doubleValue]; + } else { + data->kerning = kerning ? + [RNSVGFontData toAbsoluteWithNSString:kerning + fontSize:fontSize] + : parent->kerning; + } - NSString* wordSpacing = [font objectForKey:WORD_SPACING]; - data->wordSpacing = wordSpacing ? - [RNSVGFontData toAbsoluteWithNSString:wordSpacing - fontSize:fontSize] - : parent->wordSpacing; + id wordSpacing = [font objectForKey:WORD_SPACING]; + if ([wordSpacing isKindOfClass:NSNumber.class]) { + NSNumber* ws = wordSpacing; + data->wordSpacing = (CGFloat)[ws doubleValue]; + } else { + data->wordSpacing = wordSpacing ? + [RNSVGFontData toAbsoluteWithNSString:wordSpacing + fontSize:fontSize] + : parent->wordSpacing; + } - NSString* letterSpacing = [font objectForKey:LETTER_SPACING]; - data->letterSpacing = letterSpacing ? - [RNSVGFontData toAbsoluteWithNSString:letterSpacing - fontSize:fontSize] - : parent->letterSpacing; + id letterSpacing = [font objectForKey:LETTER_SPACING]; + if ([letterSpacing isKindOfClass:NSNumber.class]) { + NSNumber* ls = letterSpacing; + data->wordSpacing = (CGFloat)[ls doubleValue]; + } else { + data->letterSpacing = letterSpacing ? + [RNSVGFontData toAbsoluteWithNSString:letterSpacing + fontSize:fontSize] + : parent->letterSpacing; + } return data; } diff --git a/ios/Text/RNSVGGlyphContext.h b/ios/Text/RNSVGGlyphContext.h index 723b12fc..de49110e 100644 --- a/ios/Text/RNSVGGlyphContext.h +++ b/ios/Text/RNSVGGlyphContext.h @@ -44,5 +44,6 @@ - (void)pushContext:(RNSVGGroup*)node font:(NSDictionary *)font; +- (NSArray*)getFontContext; @end diff --git a/ios/Text/RNSVGGlyphContext.m b/ios/Text/RNSVGGlyphContext.m index 93056dcd..43f30a12 100644 --- a/ios/Text/RNSVGGlyphContext.m +++ b/ios/Text/RNSVGGlyphContext.m @@ -107,6 +107,9 @@ @implementation RNSVGGlyphContext +- (NSArray*)getFontContext { + return mFontContext_; +} - (CTFontRef)getGlyphFont { diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index 70b11adc..11a0e98c 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -25,6 +25,7 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; BOOL isClosed; NSMutableArray *emoji; NSMutableArray *emojiTransform; + CGFloat cachedAdvance; } - (id)init @@ -41,6 +42,12 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; return self; } +- (void)clearPath +{ + [super clearPath]; + cachedAdvance = NAN; +} + - (void)setContent:(NSString *)content { if ([content isEqualToString:_content]) { @@ -100,6 +107,69 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; return path; } +- (CGFloat)getSubtreeTextChunksTotalAdvance +{ + if (!isnan(cachedAdvance)) { + return cachedAdvance; + } + CGFloat advance = 0; + + NSString *str = self.content; + if (!str) { + for (UIView *node in self.subviews) { + if ([node isKindOfClass:[RNSVGText class]]) { + RNSVGText *text = (RNSVGText*)node; + advance += [text getSubtreeTextChunksTotalAdvance]; + } + } + cachedAdvance = advance; + return advance; + } + + // Create a dictionary for this font + CTFontRef fontRef = [self getFontFromContext]; + RNSVGGlyphContext* gc = [self.textRoot getGlyphContext]; + RNSVGFontData* font = [gc getFont]; + + CGFloat letterSpacing = font->letterSpacing; + CGFloat kerning = font->kerning; + + bool allowOptionalLigatures = letterSpacing == 0 && font->fontVariantLigatures == RNSVGFontVariantLigaturesNormal; + + NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init]; + + NSNumber *lig = [NSNumber numberWithInt:allowOptionalLigatures ? 2 : 1]; + attrs[NSLigatureAttributeName] = lig; + CFDictionaryRef attributes; + if (fontRef != nil) { + attrs[NSFontAttributeName] = (__bridge id)fontRef; + } + float kern = (float)(letterSpacing + kerning); + NSNumber *kernAttr = [NSNumber numberWithFloat:kern]; + +#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES + if (___useiOS6Attributes) + { + [attrs setObject:kernAttr forKey:NSKernAttributeName]; + } + else +#endif + { + [attrs setObject:kernAttr forKey:(id)kCTKernAttributeName]; + } + + attributes = (__bridge CFDictionaryRef)attrs; + + CFStringRef string = (__bridge CFStringRef)str; + CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); + CTLineRef line = CTLineCreateWithAttributedString(attrString); + + CGRect textBounds = CTLineGetBoundsWithOptions(line, 0); + CGFloat textMeasure = CGRectGetWidth(textBounds); + cachedAdvance = textMeasure; + return textMeasure; +} + - (CGPathRef)getLinePath:(NSString *)str context:(CGContextRef)context { // Create a dictionary for this font @@ -292,8 +362,8 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI; attributes, such as a ‘dx’ attribute value on a ‘tspan’ element. */ enum RNSVGTextAnchor textAnchor = font->textAnchor; - CGRect textBounds = CTLineGetBoundsWithOptions(line, 0); - CGFloat textMeasure = CGRectGetWidth(textBounds); + RNSVGText *anchorRoot = [self getTextAnchorRoot]; + CGFloat textMeasure = [anchorRoot getSubtreeTextChunksTotalAdvance]; CGFloat offset = [RNSVGTSpan getTextAnchorOffset:textAnchor width:textMeasure]; bool hasTextPath = textPath != nil; diff --git a/ios/Text/RNSVGText.h b/ios/Text/RNSVGText.h index 138f18f9..913ce61d 100644 --- a/ios/Text/RNSVGText.h +++ b/ios/Text/RNSVGText.h @@ -23,5 +23,7 @@ - (CGPathRef)getGroupPath:(CGContextRef)context; - (CTFontRef)getFontFromContext; +- (CGFloat)getSubtreeTextChunksTotalAdvance; +- (RNSVGText*)getTextAnchorRoot; @end diff --git a/ios/Text/RNSVGText.m b/ios/Text/RNSVGText.m index 7efc8cf0..ee94c96a 100644 --- a/ios/Text/RNSVGText.m +++ b/ios/Text/RNSVGText.m @@ -18,6 +18,7 @@ RNSVGGlyphContext *_glyphContext; NSString *_alignmentBaseline; NSString *_baselineShift; + CGFloat cachedAdvance; } - (void)invalidate @@ -29,6 +30,12 @@ [self clearChildCache]; } +- (void)clearPath +{ + [super clearPath]; + cachedAdvance = NAN; +} + - (void)setTextLength:(RNSVGLength *)textLength { if ([textLength isEqualTo:_textLength]) { @@ -248,4 +255,39 @@ return [[self.textRoot getGlyphContext] getGlyphFont]; } +- (RNSVGText*)getTextAnchorRoot +{ + RNSVGGlyphContext* gc = [self.textRoot getGlyphContext]; + NSArray* font = [gc getFontContext]; + RNSVGText* node = self; + UIView* parent = [self superview]; + for (NSInteger i = [font count] - 1; i >= 0; i--) { + RNSVGFontData* fontData = [font objectAtIndex:i]; + if (![parent isKindOfClass:[RNSVGText class]] || + fontData->textAnchor == RNSVGTextAnchorStart || + node.positionX != nil) { + return node; + } + node = (RNSVGText*) parent; + parent = [node superview]; + } + return node; +} + +- (CGFloat)getSubtreeTextChunksTotalAdvance +{ + if (!isnan(cachedAdvance)) { + return cachedAdvance; + } + CGFloat advance = 0; + for (UIView *node in self.subviews) { + if ([node isKindOfClass:[RNSVGText class]]) { + RNSVGText *text = (RNSVGText*)node; + advance += [text getSubtreeTextChunksTotalAdvance]; + } + } + cachedAdvance = advance; + return advance; +} + @end diff --git a/ios/Utils/RNSVGVectorEffect.h b/ios/Utils/RNSVGVectorEffect.h new file mode 100644 index 00000000..0ab17036 --- /dev/null +++ b/ios/Utils/RNSVGVectorEffect.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, react-native-community. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +typedef CF_ENUM(int32_t, RNSVGVectorEffect) { + kRNSVGVectorEffectDefault, + kRNSVGVectorEffectNonScalingStroke, + kRNSVGVectorEffectInherit, + kRNSVGVectorEffectUri +}; diff --git a/ios/ViewManagers/RNSVGGroupManager.m b/ios/ViewManagers/RNSVGGroupManager.m index ffac110f..8daa2335 100644 --- a/ios/ViewManagers/RNSVGGroupManager.m +++ b/ios/ViewManagers/RNSVGGroupManager.m @@ -21,4 +21,16 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(font, NSDictionary) +RCT_CUSTOM_VIEW_PROPERTY(fontSize, id, RNSVGGroup) +{ + if ([json isKindOfClass:[NSString class]]) { + NSString *stringValue = (NSString *)json; + view.font = @{ @"fontSize": stringValue }; + } else { + NSNumber* number = (NSNumber*)json; + double num = [number doubleValue]; + view.font = @{@"fontSize": [NSNumber numberWithDouble:num] }; + } +} + @end diff --git a/ios/ViewManagers/RNSVGRenderableManager.m b/ios/ViewManagers/RNSVGRenderableManager.m index befdd937..fae93de7 100644 --- a/ios/ViewManagers/RNSVGRenderableManager.m +++ b/ios/ViewManagers/RNSVGRenderableManager.m @@ -31,6 +31,7 @@ RCT_EXPORT_VIEW_PROPERTY(strokeLinejoin, CGLineJoin) RCT_EXPORT_VIEW_PROPERTY(strokeDasharray, NSArray) RCT_EXPORT_VIEW_PROPERTY(strokeDashoffset, CGFloat) RCT_EXPORT_VIEW_PROPERTY(strokeMiterlimit, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(vectorEffect, int) RCT_EXPORT_VIEW_PROPERTY(propList, NSArray) @end diff --git a/ios/ViewManagers/RNSVGSvgViewManager.m b/ios/ViewManagers/RNSVGSvgViewManager.m index ae32ae60..c07a9f08 100644 --- a/ios/ViewManagers/RNSVGSvgViewManager.m +++ b/ios/ViewManagers/RNSVGSvgViewManager.m @@ -8,6 +8,7 @@ #import #import +#import #import "RNSVGSvgViewManager.h" #import "RNSVGSvgView.h" @@ -30,17 +31,52 @@ RCT_EXPORT_VIEW_PROPERTY(align, NSString) RCT_EXPORT_VIEW_PROPERTY(meetOrSlice, RNSVGVBMOS) RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) -RCT_EXPORT_METHOD(toDataURL:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) -{ + +- (void)toDataURL:(nonnull NSNumber *)reactTag options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback attempt:(int)attempt { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { __kindof UIView *view = viewRegistry[reactTag]; + NSString * b64; if ([view isKindOfClass:[RNSVGSvgView class]]) { RNSVGSvgView *svg = view; - callback(@[[svg getDataURL]]); + if (options == nil) { + b64 = [svg getDataURL]; + } else { + id width = [options objectForKey:@"width"]; + id height = [options objectForKey:@"height"]; + if (![width isKindOfClass:NSNumber.class] || + ![height isKindOfClass:NSNumber.class]) { + RCTLogError(@"Invalid width or height given to toDataURL"); + return; + } + NSNumber* w = width; + NSInteger wi = (NSInteger)[w intValue]; + NSNumber* h = height; + NSInteger hi = (NSInteger)[h intValue]; + + CGRect bounds = CGRectMake(0, 0, wi, hi); + b64 = [svg getDataURLwithBounds:bounds]; + } } else { RCTLogError(@"Invalid svg returned frin registry, expecting RNSVGSvgView, got: %@", view); + return; + } + if (b64) { + callback(@[b64]); + } else if (attempt < 1) { + void (^retryBlock)(void) = ^{ + [self toDataURL:reactTag options:options callback:callback attempt:(attempt + 1)]; + }; + + RCTExecuteOnUIManagerQueue(retryBlock); + } else { + callback(@[]); } }]; } +RCT_EXPORT_METHOD(toDataURL:(nonnull NSNumber *)reactTag options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback) +{ + [self toDataURL:reactTag options:options callback:callback attempt:0]; +} + @end diff --git a/ios/ViewManagers/RNSVGTextManager.m b/ios/ViewManagers/RNSVGTextManager.m index 93d8e536..a273ff08 100644 --- a/ios/ViewManagers/RNSVGTextManager.m +++ b/ios/ViewManagers/RNSVGTextManager.m @@ -68,4 +68,16 @@ RCT_CUSTOM_VIEW_PROPERTY(baselineShift, id, RNSVGText) RCT_EXPORT_VIEW_PROPERTY(lengthAdjust, NSString) RCT_EXPORT_VIEW_PROPERTY(alignmentBaseline, NSString) +RCT_CUSTOM_VIEW_PROPERTY(fontSize, id, RNSVGGroup) +{ + if ([json isKindOfClass:[NSString class]]) { + NSString *stringValue = (NSString *)json; + view.font = @{ @"fontSize": stringValue }; + } else { + NSNumber* number = (NSNumber*)json; + double num = [number doubleValue]; + view.font = @{@"fontSize": [NSNumber numberWithDouble:num] }; + } +} + @end diff --git a/lib/Matrix2D.js b/lib/Matrix2D.js index 763e607a..63434c70 100644 --- a/lib/Matrix2D.js +++ b/lib/Matrix2D.js @@ -4,6 +4,8 @@ */ const DEG_TO_RAD = Math.PI / 180; +export const identity = [1, 0, 0, 1, 0, 0]; + /** * Represents an affine transformation matrix, and provides tools for constructing and concatenating matrices. * @@ -85,6 +87,9 @@ export default class Matrix2D { * @return {Array} an array with current matrix values. **/ toArray = function() { + if (this.hasInitialState) { + return identity; + } return [this.a, this.b, this.c, this.d, this.tx, this.ty]; }; diff --git a/lib/extract/extractBrush.js b/lib/extract/extractBrush.js index 882fadfe..38e8f12b 100644 --- a/lib/extract/extractBrush.js +++ b/lib/extract/extractBrush.js @@ -1,5 +1,4 @@ -import extractColor from './extractColor'; -import { Platform } from 'react-native'; +import extractColor, { integerColor } from './extractColor'; const urlIdPattern = /^url\(#(.+)\)$/; @@ -8,7 +7,7 @@ const currentColorBrush = [2]; export default function extractBrush(color) { if (typeof color === 'number') { if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) { - return [0, Platform.OS === 'android' ? color | 0x0 : color]; + return [0, integerColor(color)]; } } diff --git a/lib/extract/extractColor.js b/lib/extract/extractColor.js index f2539279..9b1167b9 100644 --- a/lib/extract/extractColor.js +++ b/lib/extract/extractColor.js @@ -345,7 +345,7 @@ function rgbFromString(string) { return null; } - return Platform.OS === 'android' ? rgb | 0x0 : rgb; + return integerColor(rgb); } else { return null; } @@ -403,11 +403,22 @@ function colorFromString(string) { } } +const identity = x => x; + +const toSignedInt32 = x => x | 0x0; + +// 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 in that way. +export const integerColor = + Platform.OS === 'android' ? toSignedInt32 : identity; + // Returns 0xaarrggbb or null export default function extractColor(color) { if (typeof color === 'number') { if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) { - return Platform.OS === 'android' ? color | 0x0 : color; + return integerColor(color); } return null; } @@ -430,9 +441,5 @@ export default function extractColor(color) { Math.round(b * 255)) >>> 0; - // 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. - return Platform.OS === 'android' ? int32Color | 0x0 : int32Color; + return integerColor(int32Color); } diff --git a/lib/extract/extractFill.js b/lib/extract/extractFill.js index 0ab4b437..18ec9e40 100644 --- a/lib/extract/extractFill.js +++ b/lib/extract/extractFill.js @@ -1,28 +1,25 @@ import extractBrush from './extractBrush'; import extractOpacity from './extractOpacity'; -import { colorNames } from './extractColor'; -import { Platform } from 'react-native'; +import { colorNames, integerColor } from './extractColor'; const fillRules = { evenodd: 0, nonzero: 1, }; -const fillProps = ['fill', 'fillOpacity', 'fillRule']; -const numFillProps = fillProps.length; - // default fill is black -const defaultFill = [ - 0, - Platform.OS === 'android' ? colorNames.black | 0x0 : colorNames.black, -]; +const black = colorNames.black; +const defaultFill = [0, integerColor(black)]; export default function extractFill(props, styleProperties) { - for (let i = 0; i < numFillProps; i++) { - const name = fillProps[i]; - if (props.hasOwnProperty(name)) { - styleProperties.push(name); - } + if (props.fill != null) { + styleProperties.push('fill'); + } + if (props.fillOpacity != null) { + styleProperties.push('fillOpacity'); + } + if (props.fillRule != null) { + styleProperties.push('fillRule'); } const { fill, fillRule, fillOpacity } = props; diff --git a/lib/extract/extractResponder.js b/lib/extract/extractResponder.js index 810b9413..d8b697cf 100644 --- a/lib/extract/extractResponder.js +++ b/lib/extract/extractResponder.js @@ -3,29 +3,21 @@ import { PanResponder } from 'react-native'; const responderProps = Object.keys(PanResponder.create({}).panHandlers); const numResponderProps = responderProps.length; -const touchableProps = [ - 'disabled', - 'onPress', - 'onPressIn', - 'onPressOut', - 'onLongPress', - 'delayPressIn', - 'delayPressOut', - 'delayLongPress', -]; -const numTouchableProps = touchableProps.length; - function hasTouchableProperty(props) { - for (let i = 0; i < numTouchableProps; i++) { - if (props.hasOwnProperty(touchableProps[i])) { - return true; - } - } - return false; + return ( + props.disabled != null || + props.onPress || + props.onPressIn || + props.onPressOut || + props.onLongPress || + props.delayPressIn || + props.delayPressOut || + props.delayLongPress + ); } export default function extractResponder(props, ref) { - const extractedProps = {}; + const o = {}; let responsible = false; for (let i = 0; i < numResponderProps; i++) { @@ -33,31 +25,28 @@ export default function extractResponder(props, ref) { const value = props[key]; if (value) { responsible = true; - extractedProps[key] = value; + o[key] = value; } } const pointerEvents = props.pointerEvents; if (pointerEvents) { - extractedProps.pointerEvents = pointerEvents; + o.pointerEvents = pointerEvents; } if (hasTouchableProperty(props)) { responsible = true; - Object.assign(extractedProps, { - onStartShouldSetResponder: ref.touchableHandleStartShouldSetResponder, - onResponderTerminationRequest: - ref.touchableHandleResponderTerminationRequest, - onResponderGrant: ref.touchableHandleResponderGrant, - onResponderMove: ref.touchableHandleResponderMove, - onResponderRelease: ref.touchableHandleResponderRelease, - onResponderTerminate: ref.touchableHandleResponderTerminate, - }); + o.onResponderMove = ref.touchableHandleResponderMove; + o.onResponderGrant = ref.touchableHandleResponderGrant; + o.onResponderRelease = ref.touchableHandleResponderRelease; + o.onResponderTerminate = ref.touchableHandleResponderTerminate; + o.onStartShouldSetResponder = ref.touchableHandleStartShouldSetResponder; + o.onResponderTerminationRequest = ref.touchableHandleResponderTerminationRequest; } if (responsible) { - extractedProps.responsible = true; + o.responsible = true; } - return extractedProps; + return o; } diff --git a/lib/extract/extractStroke.js b/lib/extract/extractStroke.js index ec8fa516..27be678a 100644 --- a/lib/extract/extractStroke.js +++ b/lib/extract/extractStroke.js @@ -14,24 +14,39 @@ const joins = { round: 1, }; -const strokeProps = [ - 'stroke', - 'strokeWidth', - 'strokeOpacity', - 'strokeDasharray', - 'strokeDashoffset', - 'strokeLinecap', - 'strokeLinejoin', - 'strokeMiterlimit', -]; -const numStrokeProps = strokeProps.length; +const vectorEffects = { + none: 0, + default: 0, + nonScalingStroke: 1, + 'non-scaling-stroke': 1, + inherit: 2, + uri: 3 +}; export default function extractStroke(props, styleProperties) { - for (let i = 0; i < numStrokeProps; i++) { - const name = strokeProps[i]; - if (props.hasOwnProperty(name)) { - styleProperties.push(name); - } + if (props.stroke != null) { + styleProperties.push('stroke'); + } + if (props.strokeWidth != null) { + styleProperties.push('strokeWidth'); + } + if (props.strokeOpacity != null) { + styleProperties.push('strokeOpacity'); + } + if (props.strokeDasharray != null) { + styleProperties.push('strokeDasharray'); + } + if (props.strokeDashoffset != null) { + styleProperties.push('strokeDashoffset'); + } + if (props.strokeLinecap != null) { + styleProperties.push('strokeLinecap'); + } + if (props.strokeLinejoin != null) { + styleProperties.push('strokeLinejoin'); + } + if (props.strokeMiterlimit != null) { + styleProperties.push('strokeMiterlimit'); } const { stroke, strokeWidth = 1, strokeDasharray } = props; @@ -52,5 +67,6 @@ export default function extractStroke(props, styleProperties) { strokeWidth, strokeDashoffset: strokeDasharray ? +props.strokeDashoffset || 0 : null, strokeMiterlimit: parseFloat(props.strokeMiterlimit) || 4, + vectorEffect: vectorEffects[props.vectorEffect] || 0, }; } diff --git a/lib/extract/extractText.js b/lib/extract/extractText.js index d908079e..4b0f3d59 100644 --- a/lib/extract/extractText.js +++ b/lib/extract/extractText.js @@ -33,7 +33,7 @@ function parseFontString(font) { const isBold = /bold/.exec(match[1]); const isItalic = /italic/.exec(match[1]); cachedFontObjectsFromString[font] = { - fontSize: match[2] || '12', + fontSize: match[2] || 12, fontWeight: isBold ? 'bold' : 'normal', fontStyle: isItalic ? 'italic' : 'normal', fontFamily: extractSingleFontFamily(match[3]), diff --git a/lib/extract/extractTransform.js b/lib/extract/extractTransform.js index 94a76472..3bdbc5cb 100644 --- a/lib/extract/extractTransform.js +++ b/lib/extract/extractTransform.js @@ -1,4 +1,4 @@ -import Matrix2D from '../Matrix2D'; +import Matrix2D, { identity } from '../Matrix2D'; import transformParser from './transform'; const pooledMatrix = new Matrix2D(); @@ -30,6 +30,13 @@ function universal2axis(universal, axisX, axisY, defaultValue) { } else if (coords.length === 1) { x = y = +coords[0]; } + } else if (Array.isArray(universal)) { + if (universal.length === 2) { + x = +universal[0]; + y = +universal[1]; + } else if (universal.length === 1) { + x = y = +universal[0]; + } } axisX = +axisX; @@ -51,10 +58,8 @@ export function props2transform(props) { const skew = universal2axis(props.skew, props.skewX, props.skewY); const translate = universal2axis( props.translate, - // eslint-disable-next-line eqeqeq - props.translateX == null ? props.x || 0 : props.translateX, - // eslint-disable-next-line eqeqeq - props.translateY == null ? props.y || 0 : props.translateY, + props.translateX || props.x, + props.translateY || props.y, ); return { @@ -102,8 +107,6 @@ export function transformToMatrix(props, transform) { return pooledMatrix.toArray(); } -const identity = [1, 0, 0, 1, 0, 0]; - export default function extractTransform(props) { if (Array.isArray(props)) { return props; diff --git a/lib/resolve.js b/lib/resolve.js new file mode 100644 index 00000000..661fea27 --- /dev/null +++ b/lib/resolve.js @@ -0,0 +1,15 @@ +import { StyleSheet } from 'react-native-web'; + +// Kept in separate file, to avoid name collision with Symbol element +export function resolve(styleProp, cleanedProps) { + if (styleProp) { + return StyleSheet + ? [styleProp, cleanedProps] + : // Compatibility for arrays of styles in plain react web + styleProp[Symbol.iterator] + ? Object.assign({}, ...styleProp, cleanedProps) + : Object.assign({}, styleProp, cleanedProps); + } else { + return cleanedProps; + } +} diff --git a/package-lock.json b/package-lock.json index 5edf7825..ba3024c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-svg", - "version": "9.2.4", + "version": "9.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d0db0fd7..3503a11b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "9.2.4", + "version": "9.4.0", "name": "react-native-svg", "description": "SVG library for react-native", "repository": { @@ -7,7 +7,7 @@ "url": "https://github.com/react-native-community/react-native-svg" }, "license": "MIT", - "main": "./index.js", + "main": "./index", "keywords": [ "react-component", "react-native", @@ -20,7 +20,7 @@ ], "scripts": { "lint": "eslint ./", - "format": "prettier index.js './{elements,lib}/*.js' './lib/extract/e*.js' --write", + "format": "prettier index.js index.web.js './{elements,lib}/*.js' './lib/extract/e*.js' --write", "peg": "pegjs -o ./lib/extract/transform.js ./lib/extract/transform.peg" }, "peerDependencies": { diff --git a/screenShoots/pencil.png b/screenShoots/pencil.png new file mode 100644 index 00000000..7730a1b5 Binary files /dev/null and b/screenShoots/pencil.png differ