image, add DefaultDownScalingMediaDecoder
This commit is contained in:
parent
1bc45e0195
commit
3069432bc2
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# Snapshot
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
* `image` - `DefaultDownScalingMediaDecoder` which scales displayed images down ([#329])
|
||||||
|
|
||||||
|
[#329]: https://github.com/noties/Markwon/issues/329
|
||||||
|
|
||||||
|
|
||||||
# 4.6.1
|
# 4.6.1
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
|
@ -1,4 +1,16 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"javaClassName": "io.noties.markwon.app.samples.image.HugeImageSample",
|
||||||
|
"id": "20210118165230",
|
||||||
|
"title": "Huge image downscaling",
|
||||||
|
"description": "Downscale displayed images with `BitmapOptions` 2 step rendering (measure, downscale), use `DefaultDownScalingMediaDecoder`",
|
||||||
|
"artifacts": [
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"javaClassName": "io.noties.markwon.app.samples.html.HtmlCssStyleParserSample",
|
"javaClassName": "io.noties.markwon.app.samples.html.HtmlCssStyleParserSample",
|
||||||
"id": "20210118155530",
|
"id": "20210118155530",
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
package io.noties.markwon.app.samples.image;
|
||||||
|
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.app.sample.Tags;
|
||||||
|
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||||
|
import io.noties.markwon.image.DefaultDownScalingMediaDecoder;
|
||||||
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||||
|
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||||
|
|
||||||
|
@MarkwonSampleInfo(
|
||||||
|
id = "20210118165230",
|
||||||
|
title = "Huge image downscaling",
|
||||||
|
description = "Downscale displayed images with `BitmapOptions` 2 step rendering " +
|
||||||
|
"(measure, downscale), use `DefaultDownScalingMediaDecoder`",
|
||||||
|
artifacts = MarkwonArtifact.IMAGE,
|
||||||
|
tags = Tags.image
|
||||||
|
)
|
||||||
|
public class HugeImageSample extends MarkwonTextViewSample {
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
|
||||||
|
// NB! this is based on the width of the widget. In case you have big vertical
|
||||||
|
// images (with big vertical dimension, use some reasonable value or fallback to real OpenGL
|
||||||
|
// maximum, see: https://stackoverflow.com/questions/15313807/android-maximum-allowed-width-height-of-bitmap
|
||||||
|
|
||||||
|
final int width = textView.getWidth();
|
||||||
|
if (width > 0) {
|
||||||
|
renderWithMaxWidth(width);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreDraw() {
|
||||||
|
final int w = textView.getWidth();
|
||||||
|
if (w > 0) {
|
||||||
|
renderWithMaxWidth(w);
|
||||||
|
|
||||||
|
final ViewTreeObserver observer = textView.getViewTreeObserver();
|
||||||
|
if (observer.isAlive()) {
|
||||||
|
observer.removeOnPreDrawListener(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderWithMaxWidth(int maxWidth) {
|
||||||
|
|
||||||
|
final String md = "" +
|
||||||
|
"# Huge image\n\n" +
|
||||||
|
"\n\n" +
|
||||||
|
"hey!";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(context)
|
||||||
|
.usePlugin(ImagesPlugin.create(plugin -> {
|
||||||
|
plugin
|
||||||
|
.defaultMediaDecoder(DefaultDownScalingMediaDecoder.create(maxWidth, 0));
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,7 +8,7 @@ android.enableJetifier=true
|
|||||||
android.enableBuildCache=true
|
android.enableBuildCache=true
|
||||||
android.buildCacheDir=build/pre-dex-cache
|
android.buildCacheDir=build/pre-dex-cache
|
||||||
|
|
||||||
VERSION_NAME=4.6.1
|
VERSION_NAME=4.6.2-SNAPSHOT
|
||||||
|
|
||||||
GROUP=io.noties.markwon
|
GROUP=io.noties.markwon
|
||||||
POM_DESCRIPTION=Markwon markdown for Android
|
POM_DESCRIPTION=Markwon markdown for Android
|
||||||
|
@ -0,0 +1,173 @@
|
|||||||
|
package io.noties.markwon.image;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link MediaDecoder} that additionally process media resource to optionally
|
||||||
|
* scale it down to fit specified maximum values. Should be used to ensure that no exception is raised
|
||||||
|
* whilst rendering ({@code Canvas: trying to draw too large(Xbytes) bitmap}) or {@code OutOfMemoryException} is thrown.
|
||||||
|
*
|
||||||
|
* <strong>NB</strong> this media decoder will create a temporary file for each incoming media resource,
|
||||||
|
* which can have a performance penalty (IO)
|
||||||
|
*
|
||||||
|
* @since $SNAPSHOT;
|
||||||
|
*/
|
||||||
|
public class DefaultDownScalingMediaDecoder extends MediaDecoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Values {@code <= 0} are ignored, a dimension is considered to be not restrained any limit in such case
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static DefaultDownScalingMediaDecoder create(int maxWidth, int maxHeight) {
|
||||||
|
return create(Resources.getSystem(), maxWidth, maxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static DefaultDownScalingMediaDecoder create(
|
||||||
|
@NonNull Resources resources,
|
||||||
|
int maxWidth,
|
||||||
|
int maxHeight
|
||||||
|
) {
|
||||||
|
return new DefaultDownScalingMediaDecoder(resources, maxWidth, maxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Resources resources;
|
||||||
|
private final int maxWidth;
|
||||||
|
private final int maxHeight;
|
||||||
|
|
||||||
|
private DefaultDownScalingMediaDecoder(@NonNull Resources resources, int maxWidth, int maxHeight) {
|
||||||
|
this.resources = resources;
|
||||||
|
this.maxWidth = maxWidth;
|
||||||
|
this.maxHeight = maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://android.jlelse.eu/loading-large-bitmaps-efficiently-in-android-66826cd4ad53
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
|
||||||
|
|
||||||
|
final File file = writeToTempFile(inputStream);
|
||||||
|
try {
|
||||||
|
|
||||||
|
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inJustDecodeBounds = true;
|
||||||
|
|
||||||
|
BitmapFactory
|
||||||
|
.decodeStream(readFile(file), null, options);
|
||||||
|
|
||||||
|
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
|
||||||
|
options.inJustDecodeBounds = false;
|
||||||
|
|
||||||
|
final Bitmap bitmap = BitmapFactory
|
||||||
|
.decodeStream(readFile(file), null, options);
|
||||||
|
|
||||||
|
return new BitmapDrawable(resources, bitmap);
|
||||||
|
} finally {
|
||||||
|
// we no longer need the temporary file
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static File writeToTempFile(@NonNull InputStream inputStream) {
|
||||||
|
final File file;
|
||||||
|
try {
|
||||||
|
file = File.createTempFile("markwon", null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final OutputStream outputStream;
|
||||||
|
try {
|
||||||
|
outputStream = new BufferedOutputStream(new FileOutputStream(file, false));
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] buffer = new byte[1024 * 8];
|
||||||
|
int length;
|
||||||
|
try {
|
||||||
|
while ((length = inputStream.read(buffer)) > 0) {
|
||||||
|
outputStream.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
outputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static InputStream readFile(@NonNull File file) {
|
||||||
|
try {
|
||||||
|
return new BufferedInputStream(new FileInputStream(file));
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// see: https://developer.android.com/topic/performance/graphics/load-bitmap.html#load-bitmap
|
||||||
|
private static int calculateInSampleSize(@NonNull BitmapFactory.Options options, int maxWidth, int maxHeight) {
|
||||||
|
final int w = options.outWidth;
|
||||||
|
final int h = options.outHeight;
|
||||||
|
|
||||||
|
final boolean hasMaxWidth = maxWidth > 0;
|
||||||
|
final boolean hasMaxHeight = maxHeight > 0;
|
||||||
|
|
||||||
|
final int inSampleSize;
|
||||||
|
if (hasMaxWidth && hasMaxHeight) {
|
||||||
|
// minimum of both
|
||||||
|
inSampleSize = Math.min(calculateInSampleSize(w, maxWidth), calculateInSampleSize(h, maxHeight));
|
||||||
|
} else if (hasMaxWidth) {
|
||||||
|
inSampleSize = calculateInSampleSize(w, maxWidth);
|
||||||
|
} else if (hasMaxHeight) {
|
||||||
|
inSampleSize = calculateInSampleSize(h, maxHeight);
|
||||||
|
} else {
|
||||||
|
// else no sampling, as we have no dimensions to base our calculations on
|
||||||
|
inSampleSize = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inSampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int calculateInSampleSize(int actual, int max) {
|
||||||
|
int inSampleSize = 1;
|
||||||
|
final int half = actual / 2;
|
||||||
|
while ((half / inSampleSize) > max) {
|
||||||
|
inSampleSize *= 2;
|
||||||
|
}
|
||||||
|
return inSampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<String> supportedTypes() {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,10 @@ import java.util.Collections;
|
|||||||
* This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases.
|
* This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases.
|
||||||
* Here we just assume that supplied InputStream is of image type and try to decode it.
|
* Here we just assume that supplied InputStream is of image type and try to decode it.
|
||||||
*
|
*
|
||||||
|
* <strong>NB</strong> if you are dealing with big images that require down scaling see {@link DefaultDownScalingMediaDecoder}
|
||||||
|
* which additionally down scales displayed images.
|
||||||
|
*
|
||||||
|
* @see DefaultDownScalingMediaDecoder
|
||||||
* @since 1.1.0
|
* @since 1.1.0
|
||||||
*/
|
*/
|
||||||
public class DefaultMediaDecoder extends MediaDecoder {
|
public class DefaultMediaDecoder extends MediaDecoder {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user