weixin_39908588
weixin_39908588
2021-01-12 09:19

Android SVG.toDataURL Rendering results in indexOutOfBoundsException

Greetings,

When I attempt to render a hidden SVG using toDataURL I fairly consistently end up with an java.lang.IndexOutOfBoundsException. However, I do not encounter it at the same index consistently.

I have narrowed the exception down to being involved in GlyphContext.popContext every time. However, I'm uncertain how the ArrayLists that are involved are getting out of sync with the mTop variable.

I have collected a few stack traces for when it has occurred:

Here are my relevant versions:


react-native-cli: 2.0.1
react-native: 0.58.4
react-native-svg: 9.2.4

The Device I am running this on is a Pixel 3 XL Running Android Pie (Android 9) Build Number PQ1A.190105.004

The SVG Element I am rendering has the following structure after I perform an auto-text sizing pass on it. I am attempting to invoke the toDataURL method after a callback from onLayout being passed to the Svg element (not shown).

Here is the following structure.

react
  <svg width="{1000}" height="{720}" fill="#ffff" style="{{" width: height:>
    <group width="{1000}" height="{720}" scale="{scale}">
      <group x="{46}" y="{46}" width="{908}" height="{628}">
        <group x="{0}" y="{0}" width="{908}" height="{380}">
          <g x="{0}" y="{0}" width="{908}" height="{380}">
            <rect width="{908}" height="{380}" fill="#fff0"></rect>
            <text lengthadjust="spacingAndGlyphs" fill="#000" fontfamily="sans-serif" textanchor="middle" alignmentbaseline="central" fontweight="800">
              {[{
                ax: 908 * 0.5,
                ay: 180 * 0.5
                text: "given",
                fontSize: 140
              }].map((line, index) => {
                return (
                  <tspan key="{index}" x="{line.ax}" y="{line.ay}" fontsize="{line.fontSize}">
                    {line.text}
                  </tspan>
                );
              })}
            </text>
          </g>
          <g x="{0}" y="{180}" width="{908}" height="{140}">
            <rect width="{908}" height="{140}" fill="#fff0"></rect>
            <text lengthadjust="spacingAndGlyphs" fill="#000" fontfamily="sans-serif" textanchor="middle" alignmentbaseline="central" fontweight="600">
              {[{
                ax: 908 * 0.5,
                ay: 140 * 0.5
                text: "family",
                fontSize: 90
              }].map((line, index) => {
                return (
                  <tspan key="{index}" x="{line.ax}" y="{line.ay}" fontsize="{line.fontSize}">
                    {line.text}
                  </tspan>
                );
              })}
            </text>
          </g>
          <g x="{0}" y="{320}" width="{908}" height="{60}">
            <rect width="{908}" height="{60}" fill="#fff0"></rect>
            <text lengthadjust="spacingAndGlyphs" fill="#000" fontfamily="sans-serif" textanchor="middle" alignmentbaseline="central" fontweight="300">
              {[{
                ax: 908 * 0.5,
                ay: 60 * 0.5
                text: "company",
                fontSize: 60
              }].map((line, index) => {
                return (
                  <tspan key="{index}" x="{line.ax}" y="{line.ay}" fontsize="{line.fontSize}">
                    {line.text}
                  </tspan>
                );
              })}
            </text>
          </g>
        </group>
        <group x="{0}" y="{380}" width="{908}" height="{248}">
          <group y="{0}" width="{908}" height="{208}">
            {badgeImageData && <image width="{908}" height="{188}" href="%7B%7B" uri: badgeimagedata></image>}
          </group>
          <g x="{0}" y="{320}" width="{908}" height="{40}">
            <rect width="{908}" height="{40}" fill="#fff0"></rect>
            <text lengthadjust="spacingAndGlyphs" fill="#000" fontfamily="sans-serif" textanchor="middle" alignmentbaseline="central" fontweight="300">
              {[{
                ax: 908 * 0.5,
                ay: 40 * 0.5
                text: "tag",
                fontSize: 40
              }].map((line, index) => {
                return (
                  <tspan key="{index}" x="{line.ax}" y="{line.ay}" fontsize="{line.fontSize}">
                    {line.text}
                  </tspan>
                );
              })}
            </text>
          </g>
        </group>
      </group>
    </group>
  </svg>

Here are some of the stack traces I have collected:


D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.app.my, PID: 20441
    java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
        at java.util.ArrayList.remove(ArrayList.java:503)
        at com.horcrux.svg.GlyphContext.popContext(GlyphContext.java:264)
        at com.horcrux.svg.GroupView.popGlyphContext(GroupView.java:74)
        at com.horcrux.svg.TSpanView.getPath(TSpanView.java:104)
        at com.horcrux.svg.GroupView.getPath(GroupView.java:148)
        at com.horcrux.svg.TextView.getGroupPath(TextView.java:199)
        at com.horcrux.svg.TextView.draw(TextView.java:136)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.SvgView.drawChildren(SvgView.java:281)
        at com.horcrux.svg.SvgView.drawOutput(SvgView.java:233)
        at com.horcrux.svg.SvgView.onDraw(SvgView.java:102)
        at android.view.View.draw(View.java:20207)
        at android.view.View.updateDisplayListIfDirty(View.java:19082)
        at android.view.View.draw(View.java:19935)
        at android.view.ViewGroup.drawChild(ViewGroup.java:4333)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4112)
        at com.facebook.react.views.view.ReactViewGroup.dispatchDraw(ReactViewGroup.java:672)
        at android.view.View.updateDisplayListIfDirty(View.java:19073)
        at android.view.View.draw(View.java:19935)
        at android.view.ViewGroup.drawChild(ViewGroup.java:4333)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4112)
        at com.facebook.react.ReactRootView.dispatchDraw(ReactRootView.java:225)
        at android.view.View.updateDisplayListIfDirty(View.java:19073)
        at android.view.View.draw(View.java:19935)
        at android.view.ViewGroup.drawChild(ViewGroup.java:4333)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4112)
        at android.view.View.updateDisplayListIfDirty(View.java:19073)
        at android.view.View.draw(View.java:19935)
        at android.view.ViewGroup.drawChild(ViewGroup.java:4333)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4112)
        at android.view.View.updateDisplayListIfDirty(View.java:19073)
        at android.view.View.draw(View.java:19935)
        at android.view.ViewGroup.drawChild(ViewGroup.java:4333)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4112)
        at android.view.View.draw(View.java:20210)
        at com.android.internal.policy.DecorView.draw(DecorView.java:780)
        at android.view.View.updateDisplayListIfDirty(View.java:19082)
        at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:686)
        at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:692)
        at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:801)
        at android.view.ViewRootImpl.draw(ViewRootImpl.java:3311)
        at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3115)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2484)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1460)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7183)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)
        at android.view.Choreographer.doCallbacks(Choreographer.java:761)
        at android.view.Choreographer.doFrame(Choreographer.java:696)
E/AndroidRuntime:     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6718)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

E/unknown:ReactNative: Exception in native call
    java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
        at java.util.ArrayList.get(ArrayList.java:439)
        at com.horcrux.svg.GlyphContext.popContext(GlyphContext.java:279)
        at com.horcrux.svg.GroupView.popGlyphContext(GroupView.java:74)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:126)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.SvgView.drawChildren(SvgView.java:281)
        at com.horcrux.svg.SvgView.toDataURL(SvgView.java:315)
        at com.horcrux.svg.SvgViewModule.toDataURL(SvgViewModule.java:44)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:158)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:29)
        at android.os.Looper.loop(Looper.java:193)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$3.run(MessageQueueThreadImpl.java:192)
        at java.lang.Thread.run(Thread.java:764)

E/unknown:ReactNative: Exception in native call
    java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
        at java.util.ArrayList.get(ArrayList.java:437)
        at com.horcrux.svg.GlyphContext.popContext(GlyphContext.java:282)
        at com.horcrux.svg.GroupView.popGlyphContext(GroupView.java:74)
        at com.horcrux.svg.TextView.getGroupPath(TextView.java:200)
        at com.horcrux.svg.TextView.draw(TextView.java:136)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.GroupView.drawGroup(GroupView.java:102)
        at com.horcrux.svg.GroupView.draw(GroupView.java:81)
        at com.horcrux.svg.RenderableView.render(RenderableView.java:311)
        at com.horcrux.svg.SvgView.drawChildren(SvgView.java:281)
        at com.horcrux.svg.SvgView.toDataURL(SvgView.java:315)
        at com.horcrux.svg.SvgViewModule.toDataURL(SvgViewModule.java:44)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:158)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:29)
        at android.os.Looper.loop(Looper.java:193)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$3.run(MessageQueueThreadImpl.java:192)
        at java.lang.Thread.run(Thread.java:764)

I apologize for the long post, but I wanted to provide enough information to potentially identify where the issue may be. As I stated before, it seems odd that the structures would become out-of-sync with the mTop variable, but that's what it seems like is occurring to me.

As a last note, this is the correct version of the plugin, but I did merge the fork to allow for the toDataURL calls to allow for a specified height and width, in order to rasterize a smaller image size. However, it doesn't seem like it should affect anything.

Please let me know if you have any questions,

Thanks,

~ Ayiga

该提问来源于开源项目:react-native-svg/react-native-svg

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

13条回答

  • weixin_39908588 weixin_39908588 4月前

    there may be concurrency issues. To be honest I wrote the patch just to get things working and I committed it after the fact, so it doesn't have the changes from the patches you were working on.

    Also, to be fair my patch still gets the width and height issue occasionally as well, so in an attempt to counteract that issue, I added a small delay before retrieving the data URL. However, since I'm waiting a static amount of time, there's still no real guarantee that it will resolve the width and `height issues.

    the GlyphContext.push and GlyphContext.pop appear to be balanced in all places that call them, which is why the issue was confusing to me in the first place. The ultimate cause of the issue comes down to two different rendering processes being called at once, and the GlyphContext being replaced in the Group element itself. The patch I wrote avoids this by not storing the GlyphContext on the Group, so it must be passed almost everywhere. In doing this, there's no way for the GlyphContext on a Group to be replaced, so the push and pop calls seem to remain balanced. But I do agree, that poping more times than pushing is an error, and perhaps that should be handled appropriately.

    I would imagine that perhaps your original path with the synchronized keyword addition may also solve the problem, but I haven't tested it thoroughly to be absolutely certain. Since I was already moving forward with the patch I had written and I had a deadline, I didn't have the time to give it it's proper due-diligence yet. Although it still needs to be modified to fix the method Overloading issue. Based on your patch, while renaming the overloaded methods to avoid the error, it does appear that the crash due to a different number of push and pops being called is no longer occurring, so that is definitely one way to solve that problem.

    When it comes to width and height of the rendered output and the devicePixelRatio of the device, I find this somewhat problematic. The toDataURL call does. As an example, consider the SVG output from my example. (In my debug versions I add a dashed line around certain elements to help me with layout).

    When submitting through the non-height and width overrides, I get the following results: screen shot 2019-02-25 at 6 44 49 pm screen shot 2019-02-25 at 6 42 29 pm

    And then, when using the width and height overrides, I see the following: screen shot 2019-02-25 at 6 45 29 pm screen shot 2019-02-25 at 6 41 49 pm

    Now this is due in large part, I suspect, to using the default density supplied by Virtual View and having no way to override it. It seems, to me, that the width and height should always be multiplied by this density scale, and in order to make the width and height aspects work like this, they would need to be multiplied by the desired density. Ultimately it means that the target density may need to be another field that is passed in, and it would default to the device's. On iOS this is much easier as the ImageContext that is created can automatically be given a scale. Using this information, it may be better, or even necessary, to have the width, height, and density live on the GlyphContext itself (It already stores the width and height).

    Please let me know if you have any questions,

    Thanks,

    ~ Ayiga

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    Can you try with the develop branch, I've made some fixes to toDataUrl and am hoping it would resolve your issues as well.

    点赞 评论 复制链接分享
  • weixin_39904116 weixin_39904116 4月前

    I got the same error, which stable version i can use ... toDataURL() was working perfectly, and now through this error:

    RNSVGSvgViewManager.toDataURL got 2 arguments, expected 3 .

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    Can you try with v9.2.5 ? This seems to work fine at least:

    jsx
    import React, { Component } from 'react';
    import { View, Image, StyleSheet } from 'react-native';
    import Svg, { Path } from 'react-native-svg';
    
    class SvgComponent extends Component {
      state = {};
      onRef = ref => {
        if (ref) {
          this.ref = ref;
        }
      };
      onLayout = () => {
        this.ref.toDataURL(base64 => {
          this.setState({ base64 });
        });
      };
      render() {
        return (
          <view style="{{" flex:>
            <svg ref="{this.onRef}" onlayout="{this.onLayout}" viewbox="0 0 400 400" width="100%" height="250">
              <path fill="none" stroke="#00f" d="M1 1h398v398H1z"></path>
              <path d="M100 100h200L200 300z" fill="red" stroke="#00f" strokewidth="{3}"></path>
            </svg>
            <view style="{{" width: flex: margintop: borderwidth: alignitems: justifycontent:>
              {this.state.base64 && (
                <image source="{{" uri: style="{{" width: height:></image>
              )}
            </view>
          </view>
        );
      }
    }
    export default class App extends React.Component {
      render() {
        return (
          <view style="{styles.container}">
            <svgcomponent></svgcomponent>
          </view>
        );
      }
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        paddingTop: 50,
        backgroundColor: '#ecf0f1',
        padding: 8,
      },
    });
    
    
    点赞 评论 复制链接分享
  • weixin_39904116 weixin_39904116 4月前

    Thank you :) ... I tried with v 9.2.3 as well, and it works perfectly like before

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    Very interesting. I wonder if it's being render twice at the same time, once by the normal view hierarchy, and once by the toDataUrl call. Could you try doing the call in the ref callback, like this:

    jsx
            <svg ref="{ele"> {
                ele.toDataURL(
                  base64Image => {
                    console.log(base64Image);
                  }
                );
              }}
    </svg>

    Alternatively, try doing the rendering a bit later using a setTimeout. I'll try running the code later. If you find any way to trigger it reliably, please let me know.

    点赞 评论 复制链接分享
  • weixin_39908588 weixin_39908588 4月前

    I just woke up this morning, and I came to the conclusion that it must be executing the same code at the same time, as you suggested. Once to draw the element in the hidden view, and another time to try and render the element as a string.

    It would also explain the other errors that some people are getting on Android, as I have also been encountering them.

    https://github.com/react-native-community/react-native-svg/issues/489 is caused by the resetProperties call in VirtualView. But that's just when the issue becomes apparent. The real reason is due to the iteration in VirtualView.mergeProperties. Because two things could be running the mergeProperties code at roughly the same time, it causes the mOriginProperties to be populated by more things than are expected by mLastMergedList. This can be solved by writing to a temporary ArrayList inside the for loop and assigning to the private member mOriginProperties afterwards.

    Ultimately, however, the crux of the issue is storing parts of the render state within the Objects that are representing the Elements themselves. If the parts of the code that hold the render state could be moved out of those objects, the issue would go away entirely. However, that would require re-writing much of the render / draw code to accept a passed GlyphContext around, and may be a bit of work.

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    I guess making a Lock on the root svg element and acquiring it in the root render and the toDataUrl method should ensure that only one render pass is active at any one time. https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    Or, just making SvgView.drawChildren synchronized

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    can you try with https://github.com/react-native-community/react-native-svg/commit/fdd8f93dac675263c1aed741f0d137345cdc1498

    点赞 评论 复制链接分享
  • weixin_39908588 weixin_39908588 4月前

    So with fdd8f93 I end up getting an error from react-native upon invoking toDataURL. It looks like react-native doesn't much appreciate method overloading as the Error states: RNSVGSvgViewManager.toDataURL got 2 arguments, expected 3.

    If I rename the toDataURL method that allows for the options to something like, toDataURLWithOptions, and then modify the Svg.js file accordingly, I no longer receive that crash.

    It looks like I can get a single SVG to compute, but if I queue them up back-to-back I end up with a width and height must be > 0 error.

    I should also note, that I did attempt the toDataURL call with the specified width and height, and the resulting image is the correct size, but the contents are still rendered at the scaled resolution, meaning the contents gets cut off on my Pixel 3 XL. (I know this is referencing another issue, but I figured I'd let you know, either way).

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    I suspect there will still be concurrency issues in your patch, the drawChildren method will need to be synchronized at the very least, but the toDataUrl method which has a custom size on the canvas will also need to be synchronized, and, it will need to call

    java
            clearChildCache();
    

    As the first and the last thing it does. Otherwise, any paths which have % units dependant on the width, height or diagonal, will render incorrectly.

    点赞 评论 复制链接分享
  • weixin_39562606 weixin_39562606 4月前

    Also, if the GlyphContext pop is called a different number of times than push, then it's a bug and it should certainly throw an exception.

    点赞 评论 复制链接分享

相关推荐