Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas: trying to draw too large(143769912bytes) #329

Closed
stefan-niedermann opened this issue Jan 14, 2021 · 19 comments
Closed

Canvas: trying to draw too large(143769912bytes) #329

stefan-niedermann opened this issue Jan 14, 2021 · 19 comments

Comments

@stefan-niedermann
Copy link

stefan-niedermann commented Jan 14, 2021

  • Markwon version: 4.6.1

A user reported in my downstream app Nextcloud Notes an issue when rendering an image.

The following markdown leads reproducible to the below stacktrace:

![](http://otakurevolution.com/storyimgs/falldog/GundamTimeline/Falldogs_GundamTimeline_v13_April2020.png)
java.lang.RuntimeException: Canvas: trying to draw too large(143769912bytes) bitmap.
	at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:280)
	at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
	at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:548)
	at io.noties.markwon.image.AsyncDrawable.draw(AsyncDrawable.java:326)
	at io.noties.markwon.image.AsyncDrawableSpan.draw(AsyncDrawableSpan.java:126)
	at android.text.TextLine.handleReplacement(TextLine.java:1011)
	at android.text.TextLine.handleRun(TextLine.java:1158)
	at android.text.TextLine.drawRun(TextLine.java:491)
	at android.text.TextLine.draw(TextLine.java:286)
	at android.text.Layout.drawText(Layout.java:576)
	at android.widget.Editor.drawHardwareAcceleratedInner(Editor.java:1957)
	at android.widget.Editor.drawHardwareAccelerated(Editor.java:1876)
	at android.widget.Editor.onDraw(Editor.java:1816)
	at android.widget.TextView.onDraw(TextView.java:7989)
	at android.view.View.draw(View.java:21975)
	at android.view.View.updateDisplayListIfDirty(View.java:20852)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.draw(View.java:21978)
	at android.widget.ScrollView.draw(ScrollView.java:1835)
	at android.view.View.updateDisplayListIfDirty(View.java:20852)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.draw(View.java:21978)
	at android.view.View.updateDisplayListIfDirty(View.java:20852)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at androidx.coordinatorlayout.widget.CoordinatorLayout.drawChild(CoordinatorLayout.java:1277)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.updateDisplayListIfDirty(View.java:20843)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at androidx.fragment.app.FragmentContainerView.drawChild(FragmentContainerView.java:235)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at androidx.fragment.app.FragmentContainerView.dispatchDraw(FragmentContainerView.java:223)
	at android.view.View.draw(View.java:21978)
	at android.view.View.updateDisplayListIfDirty(View.java:20852)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.updateDisplayListIfDirty(View.java:20843)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.updateDisplayListIfDirty(View.java:20843)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.updateDisplayListIfDirty(View.java:20843)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.updateDisplayListIfDirty(View.java:20843)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.updateDisplayListIfDirty(View.java:20843)
	at android.view.View.draw(View.java:21707)
	at android.view.ViewGroup.drawChild(ViewGroup.java:4432)
	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4193)
	at android.view.View.draw(View.java:21978)
	at com.android.internal.policy.DecorView.draw(DecorView.java:808)
	at android.view.View.updateDisplayListIfDirty(View.java:20852)
	at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:581)
	at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:587)
	at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:664)
	at android.view.ViewRootImpl.draw(ViewRootImpl.java:3767)
	at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3495)
	at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2779)
	at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1745)
	at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7768)
	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967)
	at android.view.Choreographer.doCallbacks(Choreographer.java:791)
	at android.view.Choreographer.doFrame(Choreographer.java:726)
	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
	at android.os.Handler.handleCallback(Handler.java:883)
	at android.os.Handler.dispatchMessage(Handler.java:100)
	at android.os.Looper.loop(Looper.java:214)
	at android.app.ActivityThread.main(ActivityThread.java:7356)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)

I have the following information additionally from the users device:

OS Version: 3.18.91-g8bbffc1d427(eng.root.20210107.120301)
OS API Level: 29
Device: heroltexx
Manufacturer: samsung
Model (and Product): SM-G930F (heroltexx)
  1. Can we avoid the crash at all?
  2. Is it thrown by intention?
  3. How am i supposed to catch this exception? It currently crashes the whole application i don't know how i could catch it...

Expected behavior is that the app does not crash but render the alt text instead if it ain't possible to render an image.

@noties
Copy link
Owner

noties commented Jan 18, 2021

Hello @stefan-niedermann ,

that is a pretty big image and markwon-image won't be able to display it because it handles relatively simple cases and do not manipulate displayed image in any way. I see that your project also has image-glide module added, can it display this big image normally?

@stefan-niedermann
Copy link
Author

We don't use the Glide integration yet (plan to do it in the future, have some trouble with our custom glide modules).

Nevertheless: is it possible to catch the error and just display the alt text in this case?

@noties
Copy link
Owner

noties commented Jan 18, 2021

I think you should be able to use own instance of AsyncDrawableSpan that wraps super.draw(canvas) call as a band-aid solution. But the problem here is that image module stores the whole image in memory, so it is also possible to encounter the OutOfMemoryException, which you won't be able to catch. Let me think what can be done

@noties
Copy link
Owner

noties commented Jan 19, 2021

I've added in the latest snapshot version the DefaultDownScalingMediaDecoder class. You can see it in action in the HugeImageSample sample

@stefan-niedermann
Copy link
Author

The source code looks good 👍 🙂 Looking forward to test it with the next version.

@noties
Copy link
Owner

noties commented Jan 24, 2021

Why wait 😄 ? You can already test the snapshot and it is what snapshots really are - to gather data before releasing

@stefan-niedermann
Copy link
Author

Well, you're right 😆 I will test if and report here as soon as i have time 🙂

@stefan-niedermann
Copy link
Author

Confirmed to work :)

grafik

@stefan-niedermann
Copy link
Author

@noties sorry for the question (i know it well and i hate it too 😆 ) but do you have any estimated date when 4.6.2 will be released?

@noties
Copy link
Owner

noties commented Feb 8, 2021

Hello @stefan-niedermann ,

my estimate would be right about now 😄 I've released the 4.6.2 version

@noties noties closed this as completed Feb 8, 2021
@stefan-niedermann
Copy link
Author

@noties Sorry for reanimating this solved issue - but i tried to switch to Glide recently and i am wondering how the DefaultDownScalingMediaDecoder can be used in combination with the GlideImages plugin? (It fails for the same reason)

@noties
Copy link
Owner

noties commented Jun 2, 2021

Hello @stefan-niedermann ,

I think Glide should have a similar functionality. And DownsampleStrategy seems to be suitable for this kind of a feature

@stefan-niedermann
Copy link
Author

True, though it seems not to be possible to do it with the GlideImagesPlugin provided by the markwon lib. I can reimplement the plugin myself, but i expected the glide plugin to cover the same features (or more) as the images plugin 😉

@noties
Copy link
Owner

noties commented Jun 2, 2021

GlideImagePlugin represents just a facade to Glide, so everything that is possible to do with Glide is possible to do with GlideImagesPlugin. And Glide is really much more feature rich image loader than Markwon provides. You can take a look at the GlideGifImageSample sample to see how it creates own GlideStore and overrides RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) method:

@NonNull
@Override
public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) {
  final String destination = drawable.getDestination();
  return requestManager
    .load(destination)
     // the downsampling
    .downsample(/**/)
     // transform can also be used (according to the docs)
    .transform(/**/);
}

@stefan-niedermann
Copy link
Author

stefan-niedermann commented Jun 3, 2021

Ah, i see. I didn't know that one can pass an own GlideStore implementation into GlideImagesPlugin.create(). The GlideGifImageSample indeed explains this pretty well.

However i am still facing the same exception when trying to render a huge image. I have tried each of the commented lines to apply the downsampling, all of them with the very same result.
To ensure that my code works at all, i also added a .placeholder which works fine until the problematic image gets loaded and the app crashes.

    @NonNull
    @Override
    public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) {
        return requestManager
                .load(drawable.getDestination())
                .downsample(DownsampleStrategy.AT_LEAST)
//                .downsample(DownsampleStrategy.AT_MOST)
//                .downsample(DownsampleStrategy.CENTER_OUTSIDE)
//                .downsample(DownsampleStrategy.CENTER_INSIDE)
//                .downsample(DownsampleStrategy.FIT_CENTER)
//                .centerInside()
//                .fitCenter()
//                .transform(new CenterCrop())
//                .apply(RequestOptions.downsampleOf(DownsampleStrategy.AT_LEAST))
                .placeholder(R.drawable.ic_baseline_image_24)
                .error(R.drawable.ic_baseline_broken_image_24);
    }

I also added an .error fallback image as well (which does not show up before the app crashes).

@noties
Copy link
Owner

noties commented Jun 5, 2021

Hello @stefan-niedermann ,

this is one of the reasons I do no use Glide, it is very confusing and has a lot of moving parts.

In your code snippet you do not limit the maximum size of loaded image. All the Downsample.* options would be functioning only if you manually set the size of loaded resource (for example, via .override(width, height)). There is no option to process a resource if it exceeds certain dimension out-of-box.

But you can create a custom DownsampleStrategy that does that. For example:

class DownsampleWithMaxWidth extends DownsampleStrategy {

  private final int maxWidth;

  DownsampleWithMaxWidth(int maxWidth) {
    this.maxWidth = maxWidth;
  }

  @Override
  public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
    // do not scale down if fits requested dimension
    if (sourceWidth < maxWidth) {
      return 1F;
    }
    return (float) maxWidth / sourceWidth;
  }

  @Override
  public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
    // go figure
    return SampleSizeRounding.MEMORY;
  }
}

Naturally, it is advisable to take into account maximum height also. And then use it in your GlideStore:

return requestManager
  .load(destination)
  // value is for demonstration only, use a proper one
  .downsample(new DownsampleWithMaxWidth(1024));

@stefan-niedermann
Copy link
Author

Awesome, i will try that! Thank you very much for your efforts. ❤️

While Glide has some drawbacks, it is still a good choice for my use case (for example we have to authenticate some requests with the Nextcloud Single-Sign-On API which work out of the box).

@stefan-niedermann
Copy link
Author

Confirmed to work - thanks again!

@noties
Copy link
Owner

noties commented Jun 5, 2021

You are welcome 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants