Moving image loading functionality to standalone module
This commit is contained in:
parent
453880bd62
commit
661f72da0f
@ -22,6 +22,11 @@ import ru.noties.markwon.priority.Priority;
|
||||
*/
|
||||
public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
|
||||
@ -32,11 +37,6 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
|
||||
@ -57,13 +57,6 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Priority priority() {
|
||||
// by default all come after CorePlugin
|
||||
return Priority.after(CorePlugin.class);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String processMarkdown(@NonNull String markdown) {
|
||||
|
@ -37,13 +37,26 @@ public abstract class Markwon {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to obtain an instance of {@link Builder}.
|
||||
* Factory method to obtain an instance of {@link Builder} with {@link CorePlugin} added.
|
||||
*
|
||||
* @see Builder
|
||||
* @see #builderNoCore(Context)
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull Context context) {
|
||||
return new MarkwonBuilderImpl(context)
|
||||
// @since 4.0.0-SNAPSHOT add CorePlugin
|
||||
.usePlugin(CorePlugin.create());
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to obtain an instance of {@link Builder} without {@link CorePlugin}.
|
||||
*
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builderNoCore(@NonNull Context context) {
|
||||
return new MarkwonBuilderImpl(context);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package ru.noties.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -9,19 +10,17 @@ import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.priority.PriorityProcessor;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
|
||||
private final Context context;
|
||||
@ -30,8 +29,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
|
||||
private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;
|
||||
|
||||
private PriorityProcessor priorityProcessor;
|
||||
|
||||
MarkwonBuilderImpl(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
@ -69,13 +66,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
@NonNull
|
||||
public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) {
|
||||
this.priorityProcessor = priorityProcessor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon build() {
|
||||
@ -85,21 +75,12 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
"method to add them");
|
||||
}
|
||||
|
||||
// this class will sort plugins to match a priority/dependency graph that we have
|
||||
PriorityProcessor priorityProcessor = this.priorityProcessor;
|
||||
if (priorityProcessor == null) {
|
||||
// strictly speaking we do not need updating this field
|
||||
// as we are not building this class to be reused between multiple `build` calls
|
||||
priorityProcessor = this.priorityProcessor = PriorityProcessor.create();
|
||||
}
|
||||
|
||||
// please note that this method must not modify supplied collection
|
||||
// if nothing should be done -> the same collection can be returned
|
||||
final List<MarkwonPlugin> plugins = preparePlugins(priorityProcessor, this.plugins);
|
||||
final List<MarkwonPlugin> plugins = preparePlugins(this.plugins);
|
||||
|
||||
final Parser.Builder parserBuilder = new Parser.Builder();
|
||||
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
|
||||
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
|
||||
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
|
||||
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
|
||||
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
|
||||
@ -108,7 +89,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.configureParser(parserBuilder);
|
||||
plugin.configureTheme(themeBuilder);
|
||||
plugin.configureImages(asyncDrawableLoaderBuilder);
|
||||
plugin.configureConfiguration(configurationBuilder);
|
||||
plugin.configureVisitor(visitorBuilder);
|
||||
plugin.configureSpansFactory(spanFactoryBuilder);
|
||||
@ -117,7 +97,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
|
||||
final MarkwonConfiguration configuration = configurationBuilder.build(
|
||||
themeBuilder.build(),
|
||||
asyncDrawableLoaderBuilder.build(),
|
||||
htmlRendererBuilder.build(),
|
||||
spanFactoryBuilder.build());
|
||||
|
||||
@ -133,62 +112,107 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
static List<MarkwonPlugin> preparePlugins(
|
||||
@NonNull PriorityProcessor priorityProcessor,
|
||||
@NonNull List<MarkwonPlugin> plugins) {
|
||||
|
||||
// with this method we will ensure that CorePlugin is added IF and ONLY IF
|
||||
// there are plugins that depend on it. If CorePlugin is added, or there are
|
||||
// no plugins that require it, CorePlugin won't be added
|
||||
final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins);
|
||||
|
||||
return priorityProcessor.process(out);
|
||||
static List<MarkwonPlugin> preparePlugins(@NonNull List<MarkwonPlugin> plugins) {
|
||||
return new RegistryImpl(plugins).process();
|
||||
}
|
||||
|
||||
// @since 4.0.0-SNAPSHOT
|
||||
private static class RegistryImpl implements MarkwonPlugin.Registry {
|
||||
|
||||
private final List<MarkwonPlugin> origin;
|
||||
private final List<MarkwonPlugin> plugins;
|
||||
private final Set<MarkwonPlugin> pending;
|
||||
|
||||
RegistryImpl(@NonNull List<MarkwonPlugin> origin) {
|
||||
this.origin = origin;
|
||||
this.plugins = new ArrayList<>(origin.size());
|
||||
this.pending = new HashSet<>(3);
|
||||
}
|
||||
|
||||
// this method will _implicitly_ add CorePlugin if there is at least one plugin
|
||||
// that depends on CorePlugin
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) {
|
||||
// loop over plugins -> if CorePlugin is found -> break;
|
||||
// iterate over all plugins and check if CorePlugin is requested
|
||||
|
||||
boolean hasCore = false;
|
||||
boolean hasCoreDependents = false;
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
|
||||
// here we do not check for exact match (a user could've subclasses CorePlugin
|
||||
// and supplied it. In this case we DO NOT implicitly add CorePlugin
|
||||
//
|
||||
// if core is present already we do not need to iterate anymore -> as nothing
|
||||
// will be changed (and we actually do not care if there are any dependents of Core
|
||||
// as it's present anyway)
|
||||
if (CorePlugin.class.isAssignableFrom(plugin.getClass())) {
|
||||
hasCore = true;
|
||||
break;
|
||||
@Override
|
||||
public <P extends MarkwonPlugin> P require(@NonNull Class<P> plugin) {
|
||||
return get(plugin);
|
||||
}
|
||||
|
||||
// if plugin has CorePlugin in dependencies -> mark for addition
|
||||
if (!hasCoreDependents) {
|
||||
// here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass
|
||||
// or something -> ignore)
|
||||
if (plugin.priority().after().contains(CorePlugin.class)) {
|
||||
hasCoreDependents = true;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public <P extends MarkwonPlugin> void require(
|
||||
@NonNull Class<P> plugin,
|
||||
@NonNull MarkwonPlugin.Action<? super P> action) {
|
||||
action.apply(get(plugin));
|
||||
}
|
||||
|
||||
// important thing here is to check if corePlugin is added
|
||||
// add it _only_ if it's not present
|
||||
if (hasCoreDependents && !hasCore) {
|
||||
final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1);
|
||||
// add default instance of CorePlugin
|
||||
out.add(CorePlugin.create());
|
||||
out.addAll(plugins);
|
||||
return out;
|
||||
@NonNull
|
||||
List<MarkwonPlugin> process() {
|
||||
for (MarkwonPlugin plugin : origin) {
|
||||
configure(plugin);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
private void configure(@NonNull MarkwonPlugin plugin) {
|
||||
|
||||
// important -> check if it's in plugins
|
||||
// if it is -> no need to configure (already configured)
|
||||
|
||||
if (!plugins.contains(plugin)) {
|
||||
|
||||
if (pending.contains(plugin)) {
|
||||
throw new IllegalStateException("Cyclic dependency chain found: " + pending);
|
||||
}
|
||||
|
||||
// start tracking plugins that are pending for configuration
|
||||
pending.add(plugin);
|
||||
|
||||
plugin.configure(this);
|
||||
|
||||
// stop pending tracking
|
||||
pending.remove(plugin);
|
||||
|
||||
// check again if it's included (a child might've configured it already)
|
||||
// add to out-collection if not already present
|
||||
// this is a bit different from `find` method as it does check for exact instance
|
||||
// and not a sub-type
|
||||
if (!plugins.contains(plugin)) {
|
||||
plugins.add(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private <P extends MarkwonPlugin> P get(@NonNull Class<P> type) {
|
||||
|
||||
// check if present already in plugins
|
||||
// find in origin, if not found -> throw, else add to out-plugins
|
||||
|
||||
P plugin = find(plugins, type);
|
||||
|
||||
if (plugin == null) {
|
||||
|
||||
plugin = find(origin, type);
|
||||
|
||||
if (plugin == null) {
|
||||
throw new IllegalStateException("Requested plugin is not added: " +
|
||||
"" + type.getName() + ", plugins: " + origin);
|
||||
}
|
||||
|
||||
configure(plugin);
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <P extends MarkwonPlugin> P find(
|
||||
@NonNull List<MarkwonPlugin> plugins,
|
||||
@NonNull Class<P> type) {
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
if (type.isAssignableFrom(plugin.getClass())) {
|
||||
//noinspection unchecked
|
||||
return (P) plugin;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,15 @@ public class MarkwonConfiguration {
|
||||
Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public Builder asyncDrawableLoader(@NonNull AsyncDrawableLoader asyncDrawableLoader) {
|
||||
this.asyncDrawableLoader = asyncDrawableLoader;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
|
||||
this.syntaxHighlight = syntaxHighlight;
|
||||
@ -149,15 +158,18 @@ public class MarkwonConfiguration {
|
||||
@NonNull
|
||||
public MarkwonConfiguration build(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull AsyncDrawableLoader asyncDrawableLoader,
|
||||
@NonNull MarkwonHtmlRenderer htmlRenderer,
|
||||
@NonNull MarkwonSpansFactory spansFactory) {
|
||||
|
||||
this.theme = theme;
|
||||
this.asyncDrawableLoader = asyncDrawableLoader;
|
||||
this.htmlRenderer = htmlRenderer;
|
||||
this.spansFactory = spansFactory;
|
||||
|
||||
// @since 4.0.0-SNAPSHOT
|
||||
if (asyncDrawableLoader == null) {
|
||||
asyncDrawableLoader = AsyncDrawableLoader.noOp();
|
||||
}
|
||||
|
||||
if (syntaxHighlight == null) {
|
||||
syntaxHighlight = new SyntaxHighlightNoOp();
|
||||
}
|
||||
|
@ -9,10 +9,6 @@ import org.commonmark.parser.Parser;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.MediaDecoder;
|
||||
import ru.noties.markwon.image.SchemeHandler;
|
||||
import ru.noties.markwon.priority.Priority;
|
||||
|
||||
/**
|
||||
* Class represents a plugin (extension) to Markwon to configure how parsing and rendering
|
||||
@ -25,6 +21,35 @@ import ru.noties.markwon.priority.Priority;
|
||||
*/
|
||||
public interface MarkwonPlugin {
|
||||
|
||||
/**
|
||||
* @see Registry#require(Class, Action)
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
interface Action<P extends MarkwonPlugin> {
|
||||
void apply(@NonNull P p);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #configure(Registry)
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
interface Registry {
|
||||
|
||||
@NonNull
|
||||
<P extends MarkwonPlugin> P require(@NonNull Class<P> plugin);
|
||||
|
||||
<P extends MarkwonPlugin> void require(
|
||||
@NonNull Class<P> plugin,
|
||||
@NonNull Action<? super P> action);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will be called before any other during {@link Markwon} instance construction.
|
||||
*
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
void configure(@NonNull Registry registry);
|
||||
|
||||
/**
|
||||
* Method to configure <code>org.commonmark.parser.Parser</code> (for example register custom
|
||||
* extension, etc).
|
||||
@ -39,17 +64,6 @@ public interface MarkwonPlugin {
|
||||
*/
|
||||
void configureTheme(@NonNull MarkwonTheme.Builder builder);
|
||||
|
||||
/**
|
||||
* Configure image loading functionality. For example add new content-types
|
||||
* {@link AsyncDrawableLoader.Builder#addMediaDecoder(String, MediaDecoder)}, a transport
|
||||
* layer (network, file, etc) {@link AsyncDrawableLoader.Builder#addSchemeHandler(String, SchemeHandler)}
|
||||
* or modify existing properties.
|
||||
*
|
||||
* @see AsyncDrawableLoader
|
||||
* @see AsyncDrawableLoader.Builder
|
||||
*/
|
||||
void configureImages(@NonNull AsyncDrawableLoader.Builder builder);
|
||||
|
||||
/**
|
||||
* Configure {@link MarkwonConfiguration}
|
||||
*
|
||||
@ -82,9 +96,6 @@ public interface MarkwonPlugin {
|
||||
*/
|
||||
void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder);
|
||||
|
||||
@NonNull
|
||||
Priority priority();
|
||||
|
||||
/**
|
||||
* Process input markdown and return new string to be used in parsing stage further.
|
||||
* Can be described as <code>pre-processing</code> of markdown String.
|
||||
|
@ -95,12 +95,6 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
||||
.setFactory(ThematicBreak.class, new ThematicBreakSpanFactory());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Priority priority() {
|
||||
return Priority.none();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
OrderedListItemSpan.measure(textView, markdown);
|
||||
|
@ -13,22 +13,6 @@ import java.util.concurrent.Executors;
|
||||
|
||||
public abstract class AsyncDrawableLoader {
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface DrawableProvider {
|
||||
@Nullable
|
||||
Drawable provide();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@ -71,123 +55,4 @@ public abstract class AsyncDrawableLoader {
|
||||
|
||||
@Nullable
|
||||
public abstract Drawable placeholder();
|
||||
|
||||
public static class Builder {
|
||||
|
||||
ExecutorService executorService;
|
||||
final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
|
||||
final Map<String, MediaDecoder> mediaDecoders = new HashMap<>(3);
|
||||
MediaDecoder defaultMediaDecoder;
|
||||
DrawableProvider placeholderDrawableProvider;
|
||||
DrawableProvider errorDrawableProvider;
|
||||
|
||||
AsyncDrawableLoader implementation;
|
||||
|
||||
@NonNull
|
||||
public Builder executorService(@NonNull ExecutorService executorService) {
|
||||
this.executorService = executorService;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder addSchemeHandler(@NonNull String scheme, @NonNull SchemeHandler schemeHandler) {
|
||||
schemeHandlers.put(scheme, schemeHandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder addSchemeHandler(@NonNull Collection<String> schemes, @NonNull SchemeHandler schemeHandler) {
|
||||
for (String scheme : schemes) {
|
||||
schemeHandlers.put(scheme, schemeHandler);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder addMediaDecoder(@NonNull String contentType, @NonNull MediaDecoder mediaDecoder) {
|
||||
mediaDecoders.put(contentType, mediaDecoder);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder addMediaDecoder(@NonNull Collection<String> contentTypes, @NonNull MediaDecoder mediaDecoder) {
|
||||
for (String contentType : contentTypes) {
|
||||
mediaDecoders.put(contentType, mediaDecoder);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder removeSchemeHandler(@NonNull String scheme) {
|
||||
schemeHandlers.remove(scheme);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder removeMediaDecoder(@NonNull String contentType) {
|
||||
mediaDecoders.remove(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) {
|
||||
this.defaultMediaDecoder = mediaDecoder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public Builder placeholderDrawableProvider(@NonNull DrawableProvider placeholderDrawableProvider) {
|
||||
this.placeholderDrawableProvider = placeholderDrawableProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public Builder errorDrawableProvider(@NonNull DrawableProvider errorDrawableProvider) {
|
||||
this.errorDrawableProvider = errorDrawableProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Please note that if implementation is supplied, all configuration properties
|
||||
* (scheme-handlers, media-decoders, placeholder, etc) of this builder instance
|
||||
* will be ignored.
|
||||
*
|
||||
* @param implementation {@link AsyncDrawableLoader} implementation to be used.
|
||||
* @since 3.0.1
|
||||
*/
|
||||
@NonNull
|
||||
public Builder implementation(@NonNull AsyncDrawableLoader implementation) {
|
||||
this.implementation = implementation;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoader build() {
|
||||
|
||||
// NB, all other configuration properties will be ignored if
|
||||
// implementation is specified
|
||||
if (implementation != null) {
|
||||
return implementation;
|
||||
}
|
||||
|
||||
// if we have no schemeHandlers -> we cannot show anything
|
||||
// OR if we have no media decoders
|
||||
if (schemeHandlers.size() == 0
|
||||
|| (mediaDecoders.size() == 0 && defaultMediaDecoder == null)) {
|
||||
return new AsyncDrawableLoaderNoOp();
|
||||
}
|
||||
|
||||
if (executorService == null) {
|
||||
executorService = Executors.newCachedThreadPool();
|
||||
}
|
||||
|
||||
return new AsyncDrawableLoaderImpl(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.InputStream;
|
||||
@ -7,12 +9,48 @@ import java.io.InputStream;
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ImageItem {
|
||||
public abstract class ImageItem {
|
||||
|
||||
/**
|
||||
* Create an {@link ImageItem} with result, so no further decoding is required.
|
||||
*
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static ImageItem withResult(@Nullable Drawable drawable) {
|
||||
return new WithResult(drawable);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ImageItem withDecodingNeeded(
|
||||
@Nullable String contentType,
|
||||
@Nullable InputStream inputStream) {
|
||||
return new WithDecodingNeeded(contentType, inputStream);
|
||||
}
|
||||
|
||||
private ImageItem() {
|
||||
}
|
||||
|
||||
public static class WithResult extends ImageItem {
|
||||
|
||||
private final Drawable result;
|
||||
|
||||
WithResult(@Nullable Drawable drawable) {
|
||||
result = drawable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Drawable result() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WithDecodingNeeded extends ImageItem {
|
||||
|
||||
private final String contentType;
|
||||
private final InputStream inputStream;
|
||||
|
||||
public ImageItem(
|
||||
WithDecodingNeeded(
|
||||
@Nullable String contentType,
|
||||
@Nullable InputStream inputStream) {
|
||||
this.contentType = contentType;
|
||||
@ -28,4 +66,5 @@ public class ImageItem {
|
||||
public InputStream inputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import ru.noties.markwon.MarkwonPlugin;
|
||||
* @see MarkwonPlugin#priority()
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class Priority {
|
||||
|
||||
@NonNull
|
||||
|
27
markwon-image/build.gradle
Normal file
27
markwon-image/build.gradle
Normal file
@ -0,0 +1,27 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion config['compile-sdk']
|
||||
buildToolsVersion config['build-tools']
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion config['min-sdk']
|
||||
targetSdkVersion config['target-sdk']
|
||||
versionCode 1
|
||||
versionName version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
api project(':markwon-core')
|
||||
|
||||
deps.with {
|
||||
compileOnly it['android-gif']
|
||||
compileOnly it['android-svg']
|
||||
compileOnly it['okhttp']
|
||||
}
|
||||
}
|
||||
|
||||
registerArtifact(this)
|
4
markwon-image/gradle.properties
Normal file
4
markwon-image/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
||||
POM_NAME=Image
|
||||
POM_ARTIFACT_ID=image
|
||||
POM_DESCRIPTION=Markwon image loading module (with GIF and SVG support)
|
||||
POM_PACKAGING=aar
|
1
markwon-image/src/main/AndroidManifest.xml
Normal file
1
markwon-image/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="ru.noties.markwon.image" />
|
@ -0,0 +1,110 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class AsyncDrawableLoaderBuilder {
|
||||
|
||||
ExecutorService executorService;
|
||||
final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
|
||||
final Map<String, MediaDecoder> mediaDecoders = new HashMap<>(3);
|
||||
MediaDecoder defaultMediaDecoder;
|
||||
DrawableProvider placeholderDrawableProvider;
|
||||
DrawableProvider errorDrawableProvider;
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder executorService(@NonNull ExecutorService executorService) {
|
||||
this.executorService = executorService;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder addSchemeHandler(@NonNull String scheme, @NonNull SchemeHandler schemeHandler) {
|
||||
schemeHandlers.put(scheme, schemeHandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder addSchemeHandler(@NonNull Collection<String> schemes, @NonNull SchemeHandler schemeHandler) {
|
||||
for (String scheme : schemes) {
|
||||
schemeHandlers.put(scheme, schemeHandler);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder addMediaDecoder(@NonNull String contentType, @NonNull MediaDecoder mediaDecoder) {
|
||||
mediaDecoders.put(contentType, mediaDecoder);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder addMediaDecoder(@NonNull Collection<String> contentTypes, @NonNull MediaDecoder mediaDecoder) {
|
||||
for (String contentType : contentTypes) {
|
||||
mediaDecoders.put(contentType, mediaDecoder);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder removeSchemeHandler(@NonNull String scheme) {
|
||||
schemeHandlers.remove(scheme);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder removeMediaDecoder(@NonNull String contentType) {
|
||||
mediaDecoders.remove(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) {
|
||||
this.defaultMediaDecoder = mediaDecoder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder placeholderDrawableProvider(@NonNull DrawableProvider placeholderDrawableProvider) {
|
||||
this.placeholderDrawableProvider = placeholderDrawableProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public AsyncDrawableLoaderBuilder errorDrawableProvider(@NonNull DrawableProvider errorDrawableProvider) {
|
||||
this.errorDrawableProvider = errorDrawableProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawableLoader build() {
|
||||
|
||||
// if we have no schemeHandlers -> we cannot show anything
|
||||
// OR if we have no media decoders
|
||||
if (schemeHandlers.size() == 0
|
||||
|| (mediaDecoders.size() == 0 && defaultMediaDecoder == null)) {
|
||||
return new AsyncDrawableLoaderNoOp();
|
||||
}
|
||||
|
||||
if (executorService == null) {
|
||||
executorService = Executors.newCachedThreadPool();
|
||||
}
|
||||
|
||||
return new AsyncDrawableLoaderImpl(this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,290 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
|
||||
|
||||
private final ExecutorService executorService;
|
||||
private final Map<String, SchemeHandler> schemeHandlers;
|
||||
private final Map<String, MediaDecoder> mediaDecoders;
|
||||
private final MediaDecoder defaultMediaDecoder;
|
||||
private final DrawableProvider placeholderDrawableProvider;
|
||||
private final DrawableProvider errorDrawableProvider;
|
||||
|
||||
private final Handler mainThread;
|
||||
|
||||
// @since 3.1.0-SNAPSHOT use a hash-map with a weak AsyncDrawable as key for multiple requests
|
||||
// for the same destination
|
||||
private final Map<WeakReference<AsyncDrawable>, Future<?>> requests = new HashMap<>(2);
|
||||
|
||||
AsyncDrawableLoaderImpl(@NonNull Builder builder) {
|
||||
this.executorService = builder.executorService;
|
||||
this.schemeHandlers = builder.schemeHandlers;
|
||||
this.mediaDecoders = builder.mediaDecoders;
|
||||
this.defaultMediaDecoder = builder.defaultMediaDecoder;
|
||||
this.placeholderDrawableProvider = builder.placeholderDrawableProvider;
|
||||
this.errorDrawableProvider = builder.errorDrawableProvider;
|
||||
this.mainThread = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NonNull final AsyncDrawable drawable) {
|
||||
|
||||
// primitive synchronization via main-thread
|
||||
if (!isMainThread()) {
|
||||
mainThread.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
load(drawable);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// okay, if by some chance requested drawable already has a future associated -> no-op
|
||||
// as AsyncDrawable cannot change `destination` (immutable field)
|
||||
// @since 3.1.0-SNAPSHOT
|
||||
if (hasTaskAssociated(drawable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final WeakReference<AsyncDrawable> reference = new WeakReference<>(drawable);
|
||||
requests.put(reference, execute(drawable.getDestination(), reference));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel(@NonNull final AsyncDrawable drawable) {
|
||||
|
||||
if (!isMainThread()) {
|
||||
mainThread.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cancel(drawable);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator =
|
||||
requests.entrySet().iterator();
|
||||
|
||||
AsyncDrawable key;
|
||||
Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
|
||||
entry = iterator.next();
|
||||
key = entry.getKey().get();
|
||||
|
||||
// if key is null or it contains requested AsyncDrawable -> cancel
|
||||
if (shouldCleanUp(key) || key == drawable) {
|
||||
entry.getValue().cancel(true);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasTaskAssociated(@NonNull AsyncDrawable drawable) {
|
||||
|
||||
final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator =
|
||||
requests.entrySet().iterator();
|
||||
|
||||
boolean result = false;
|
||||
|
||||
AsyncDrawable key;
|
||||
Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
|
||||
entry = iterator.next();
|
||||
key = entry.getKey().get();
|
||||
|
||||
// clean-up
|
||||
if (shouldCleanUp(key)) {
|
||||
entry.getValue().cancel(true);
|
||||
iterator.remove();
|
||||
} else if (key == drawable) {
|
||||
result = true;
|
||||
// do not break, let iteration continue to possibly clean-up the rest references
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void cleanUp() {
|
||||
|
||||
final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator =
|
||||
requests.entrySet().iterator();
|
||||
|
||||
AsyncDrawable key;
|
||||
Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
|
||||
entry = iterator.next();
|
||||
key = entry.getKey().get();
|
||||
|
||||
// clean-up of already referenced or detached drawables
|
||||
if (shouldCleanUp(key)) {
|
||||
entry.getValue().cancel(true);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
|
||||
//
|
||||
// // todo: we cannot reliably identify request by the destination, as if
|
||||
// // markdown input has multiple images with the same destination as source
|
||||
// // we will be tracking only one of them (the one appears the last). We should
|
||||
// // move to AsyncDrawable based identification. This method also _maybe_
|
||||
// // should include the ImageSize (comment @since 3.1.0-SNAPSHOT)
|
||||
//
|
||||
// requests.put(destination, execute(destination, drawable));
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void cancel(@NonNull String destination) {
|
||||
//
|
||||
// // todo: as we are moving away from a single request for a destination,
|
||||
// // we should re-evaluate this cancellation logic, as if there are multiple images
|
||||
// // in markdown input all of them will be cancelled (won't delivered), even if
|
||||
// // only a single drawable is detached. Cancellation must also take
|
||||
// // the AsyncDrawable argument (comment @since 3.1.0-SNAPSHOT)
|
||||
//
|
||||
// //
|
||||
// final Future<?> request = requests.remove(destination);
|
||||
// if (request != null) {
|
||||
// request.cancel(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Drawable placeholder() {
|
||||
return placeholderDrawableProvider != null
|
||||
? placeholderDrawableProvider.provide()
|
||||
: null;
|
||||
}
|
||||
|
||||
private Future<?> execute(@NonNull final String destination, @NonNull final WeakReference<AsyncDrawable> reference) {
|
||||
|
||||
// todo: error handing (simply applying errorDrawable is not a good solution
|
||||
// as reason for an error is unclear (no scheme handler, no input data, error decoding, etc)
|
||||
|
||||
// todo: more efficient ImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal
|
||||
// for big images for sure. We _could_ introduce internal Drawable that will check for
|
||||
// image bounds (but we will need to cache inputStream in order to inspect and optimize
|
||||
// input image...)
|
||||
|
||||
return executorService.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
final ImageItem item;
|
||||
|
||||
final Uri uri = Uri.parse(destination);
|
||||
|
||||
final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme());
|
||||
|
||||
if (schemeHandler != null) {
|
||||
item = schemeHandler.handle(destination, uri);
|
||||
} else {
|
||||
item = null;
|
||||
}
|
||||
|
||||
final InputStream inputStream = item != null
|
||||
? item.inputStream()
|
||||
: null;
|
||||
|
||||
Drawable result = null;
|
||||
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
|
||||
MediaDecoder mediaDecoder = mediaDecoders.get(item.contentType());
|
||||
if (mediaDecoder == null) {
|
||||
mediaDecoder = defaultMediaDecoder;
|
||||
}
|
||||
|
||||
if (mediaDecoder != null) {
|
||||
result = mediaDecoder.decode(inputStream);
|
||||
}
|
||||
|
||||
} finally {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if result is null, we assume it's an error
|
||||
if (result == null) {
|
||||
result = errorDrawableProvider != null
|
||||
? errorDrawableProvider.provide()
|
||||
: null;
|
||||
}
|
||||
|
||||
final Drawable out = result;
|
||||
|
||||
mainThread.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
if (out != null) {
|
||||
|
||||
// this doesn't work with markdown input with multiple images with the
|
||||
// same source (comment @since 3.1.0-SNAPSHOT)
|
||||
// final boolean canDeliver = requests.remove(destination) != null;
|
||||
// if (canDeliver) {
|
||||
// final AsyncDrawable asyncDrawable = reference.get();
|
||||
// if (asyncDrawable != null && asyncDrawable.isAttached()) {
|
||||
// asyncDrawable.setResult(out);
|
||||
// }
|
||||
// }
|
||||
|
||||
// todo: AsyncDrawable cannot change destination, so if it's
|
||||
// attached and not garbage-collected, we can deliver the result.
|
||||
// Note that there is no cache, so attach/detach of drawables
|
||||
// will always request a new entry.. (comment @since 3.1.0-SNAPSHOT)
|
||||
final AsyncDrawable asyncDrawable = reference.get();
|
||||
if (asyncDrawable != null && asyncDrawable.isAttached()) {
|
||||
asyncDrawable.setResult(out);
|
||||
}
|
||||
}
|
||||
|
||||
requests.remove(reference);
|
||||
cleanUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean shouldCleanUp(@Nullable AsyncDrawable drawable) {
|
||||
return drawable == null || !drawable.isAttached();
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private static boolean isMainThread() {
|
||||
return Looper.myLooper() == Looper.getMainLooper();
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public abstract class ImageItem {
|
||||
|
||||
/**
|
||||
* Create an {@link ImageItem} with result, so no further decoding is required.
|
||||
*
|
||||
* @see #withDecodingNeeded(String, InputStream)
|
||||
* @see WithResult
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static ImageItem withResult(@NonNull Drawable drawable) {
|
||||
return new WithResult(drawable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link ImageItem} that requires further decoding of InputStream.
|
||||
*
|
||||
* @see #withResult(Drawable)
|
||||
* @see WithDecodingNeeded
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static ImageItem withDecodingNeeded(
|
||||
@Nullable String contentType,
|
||||
@NonNull InputStream inputStream) {
|
||||
return new WithDecodingNeeded(contentType, inputStream);
|
||||
}
|
||||
|
||||
|
||||
private ImageItem() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
public abstract boolean hasResult();
|
||||
|
||||
/**
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
public abstract boolean hasDecodingNeeded();
|
||||
|
||||
/**
|
||||
* @see #hasResult()
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public abstract WithResult getAsWithResult();
|
||||
|
||||
/**
|
||||
* @see #hasDecodingNeeded()
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public abstract WithDecodingNeeded getAsWithDecodingNeeded();
|
||||
|
||||
/**
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
public static class WithResult extends ImageItem {
|
||||
|
||||
private final Drawable result;
|
||||
|
||||
private WithResult(@NonNull Drawable drawable) {
|
||||
result = drawable;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Drawable result() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDecodingNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public WithResult getAsWithResult() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public WithDecodingNeeded getAsWithDecodingNeeded() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
public static class WithDecodingNeeded extends ImageItem {
|
||||
|
||||
private final String contentType;
|
||||
private final InputStream inputStream;
|
||||
|
||||
private WithDecodingNeeded(
|
||||
@Nullable String contentType,
|
||||
@NonNull InputStream inputStream) {
|
||||
this.contentType = contentType;
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String contentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public InputStream inputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDecodingNeeded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public WithResult getAsWithResult() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public WithDecodingNeeded getAsWithDecodingNeeded() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Image;
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.MarkwonSpansFactory;
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
|
||||
public class ImagesPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
/**
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
public interface DrawableProvider {
|
||||
@Nullable
|
||||
Drawable provide();
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
public static ImagesPlugin create(@NonNull Context context) {
|
||||
return new ImagesPlugin(context, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Special scheme that is used {@code file:///android_asset/}
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
@NonNull
|
||||
public static ImagesPlugin createWithAssets(@NonNull Context context) {
|
||||
return new ImagesPlugin(context, true);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final boolean useAssets;
|
||||
|
||||
protected ImagesPlugin(Context context, boolean useAssets) {
|
||||
this.context = context;
|
||||
this.useAssets = useAssets;
|
||||
}
|
||||
|
||||
// we must expose scheme handling... so it's available during construction and via `require`
|
||||
|
||||
// @Override
|
||||
// public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
|
||||
//
|
||||
// final FileSchemeHandler fileSchemeHandler = useAssets
|
||||
// ? FileSchemeHandler.createWithAssets(context.getAssets())
|
||||
// : FileSchemeHandler.create();
|
||||
//
|
||||
// builder
|
||||
// .addSchemeHandler(DataUriSchemeHandler.SCHEME, DataUriSchemeHandler.create())
|
||||
// .addSchemeHandler(FileSchemeHandler.SCHEME, fileSchemeHandler)
|
||||
// .addSchemeHandler(
|
||||
// Arrays.asList(
|
||||
// NetworkSchemeHandler.SCHEME_HTTP,
|
||||
// NetworkSchemeHandler.SCHEME_HTTPS),
|
||||
// NetworkSchemeHandler.create())
|
||||
// .defaultMediaDecoder(ImageMediaDecoder.create(context.getResources()));
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.setFactory(Image.class, new ImageSpanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Image.class, new MarkwonVisitor.NodeVisitor<Image>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) {
|
||||
|
||||
// if there is no image spanFactory, ignore
|
||||
final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Image.class);
|
||||
if (spanFactory == null) {
|
||||
visitor.visitChildren(image);
|
||||
return;
|
||||
}
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
visitor.visitChildren(image);
|
||||
|
||||
// we must check if anything _was_ added, as we need at least one char to render
|
||||
if (length == visitor.length()) {
|
||||
visitor.builder().append('\uFFFC');
|
||||
}
|
||||
|
||||
final MarkwonConfiguration configuration = visitor.configuration();
|
||||
|
||||
final Node parent = image.getParent();
|
||||
final boolean link = parent instanceof Link;
|
||||
|
||||
final String destination = configuration
|
||||
.urlProcessor()
|
||||
.process(image.getDestination());
|
||||
|
||||
final RenderProps props = visitor.renderProps();
|
||||
|
||||
// apply image properties
|
||||
// Please note that we explicitly set IMAGE_SIZE to null as we do not clear
|
||||
// properties after we applied span (we could though)
|
||||
ImageProps.DESTINATION.set(props, destination);
|
||||
ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link);
|
||||
ImageProps.IMAGE_SIZE.set(props, null);
|
||||
|
||||
visitor.setSpans(length, spanFactory.getSpans(configuration, props));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
AsyncDrawableScheduler.unschedule(textView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
AsyncDrawableScheduler.schedule(textView);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public abstract class MediaDecoder {
|
||||
|
||||
/**
|
||||
* Changes since 4.0.0-SNAPSHOT:
|
||||
* <ul>
|
||||
* <li>Returns `non-null` drawable</li>
|
||||
* <li>Added `contentType` method parameter</li>
|
||||
* <li>Added `throws Exception` to method signature</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws Exception since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Drawable decode(
|
||||
@Nullable String contentType,
|
||||
@NonNull InputStream inputStream
|
||||
) throws Exception;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public abstract class SchemeHandler {
|
||||
|
||||
/**
|
||||
* Changes since 4.0.0-SNAPSHOT:
|
||||
* <ul>
|
||||
* <li>Returns `non-null` image-item</li>
|
||||
* <li>added `throws Exception` to method signature</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws Exception since 4.0.0-SNAPSHOT
|
||||
* @see ImageItem#withResult(android.graphics.drawable.Drawable)
|
||||
* @see ImageItem#withDecodingNeeded(String, java.io.InputStream)
|
||||
*/
|
||||
@NonNull
|
||||
public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri) throws Exception;
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package ru.noties.markwon.image.data;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public class DataUri {
|
||||
|
||||
private final String contentType;
|
||||
private final boolean base64;
|
||||
private final String data;
|
||||
|
||||
public DataUri(@Nullable String contentType, boolean base64, @Nullable String data) {
|
||||
this.contentType = contentType;
|
||||
this.base64 = base64;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String contentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public boolean base64() {
|
||||
return base64;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String data() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DataUri{" +
|
||||
"contentType='" + contentType + '\'' +
|
||||
", base64=" + base64 +
|
||||
", data='" + data + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
DataUri dataUri = (DataUri) o;
|
||||
|
||||
if (base64 != dataUri.base64) return false;
|
||||
if (contentType != null ? !contentType.equals(dataUri.contentType) : dataUri.contentType != null)
|
||||
return false;
|
||||
return data != null ? data.equals(dataUri.data) : dataUri.data == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = contentType != null ? contentType.hashCode() : 0;
|
||||
result = 31 * result + (base64 ? 1 : 0);
|
||||
result = 31 * result + (data != null ? data.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package ru.noties.markwon.image.data;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
public abstract class DataUriDecoder {
|
||||
|
||||
@Nullable
|
||||
public abstract byte[] decode(@NonNull DataUri dataUri) throws Throwable;
|
||||
|
||||
@NonNull
|
||||
public static DataUriDecoder create() {
|
||||
return new Impl();
|
||||
}
|
||||
|
||||
static class Impl extends DataUriDecoder {
|
||||
|
||||
private static final String CHARSET = "UTF-8";
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public byte[] decode(@NonNull DataUri dataUri) throws Throwable {
|
||||
|
||||
final String data = dataUri.data();
|
||||
|
||||
if (!TextUtils.isEmpty(data)) {
|
||||
if (dataUri.base64()) {
|
||||
return Base64.decode(data.getBytes(CHARSET), Base64.DEFAULT);
|
||||
} else {
|
||||
return data.getBytes(CHARSET);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package ru.noties.markwon.image.data;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public abstract class DataUriParser {
|
||||
|
||||
@Nullable
|
||||
public abstract DataUri parse(@NonNull String input);
|
||||
|
||||
|
||||
@NonNull
|
||||
public static DataUriParser create() {
|
||||
return new Impl();
|
||||
}
|
||||
|
||||
static class Impl extends DataUriParser {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DataUri parse(@NonNull String input) {
|
||||
|
||||
final int index = input.indexOf(',');
|
||||
// we expect exactly one comma
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String contentType;
|
||||
final boolean base64;
|
||||
|
||||
if (index > 0) {
|
||||
final String part = input.substring(0, index);
|
||||
final String[] parts = part.split(";");
|
||||
final int length = parts.length;
|
||||
if (length > 0) {
|
||||
// if one: either content-type or base64
|
||||
if (length == 1) {
|
||||
final String value = parts[0];
|
||||
if ("base64".equals(value)) {
|
||||
contentType = null;
|
||||
base64 = true;
|
||||
} else {
|
||||
contentType = value.indexOf('/') > -1
|
||||
? value
|
||||
: null;
|
||||
base64 = false;
|
||||
}
|
||||
} else {
|
||||
contentType = parts[0].indexOf('/') > -1
|
||||
? parts[0]
|
||||
: null;
|
||||
base64 = "base64".equals(parts[length - 1]);
|
||||
}
|
||||
} else {
|
||||
contentType = null;
|
||||
base64 = false;
|
||||
}
|
||||
} else {
|
||||
contentType = null;
|
||||
base64 = false;
|
||||
}
|
||||
|
||||
final String data;
|
||||
if (index < input.length()) {
|
||||
final String value = input.substring(index + 1, input.length()).replaceAll("\n", "");
|
||||
if (value.length() == 0) {
|
||||
data = null;
|
||||
} else {
|
||||
data = value;
|
||||
}
|
||||
} else {
|
||||
data = null;
|
||||
}
|
||||
|
||||
return new DataUri(contentType, base64, data);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package ru.noties.markwon.image.data;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
import ru.noties.markwon.image.ImageItem;
|
||||
import ru.noties.markwon.image.SchemeHandler;
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class DataUriSchemeHandler extends SchemeHandler {
|
||||
|
||||
public static final String SCHEME = "data";
|
||||
|
||||
@NonNull
|
||||
public static DataUriSchemeHandler create() {
|
||||
return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create());
|
||||
}
|
||||
|
||||
private static final String START = "data:";
|
||||
|
||||
private final DataUriParser uriParser;
|
||||
private final DataUriDecoder uriDecoder;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
DataUriSchemeHandler(@NonNull DataUriParser uriParser, @NonNull DataUriDecoder uriDecoder) {
|
||||
this.uriParser = uriParser;
|
||||
this.uriDecoder = uriDecoder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
|
||||
|
||||
if (!raw.startsWith(START)) {
|
||||
throw new IllegalStateException("Invalid data-uri: " + raw);
|
||||
}
|
||||
|
||||
final String part = raw.substring(START.length());
|
||||
|
||||
final DataUri dataUri = uriParser.parse(part);
|
||||
if (dataUri == null) {
|
||||
throw new IllegalStateException("Invalid data-uri: " + raw);
|
||||
}
|
||||
|
||||
final byte[] bytes;
|
||||
try {
|
||||
bytes = uriDecoder.decode(dataUri);
|
||||
} catch (Throwable t) {
|
||||
throw new IllegalStateException("Cannot decode data-uri: " + raw, t);
|
||||
}
|
||||
|
||||
if (bytes == null) {
|
||||
throw new IllegalStateException("Decoding data-uri failed: " + raw);
|
||||
}
|
||||
|
||||
return ImageItem.withDecodingNeeded(
|
||||
dataUri.contentType(),
|
||||
new ByteArrayInputStream(bytes));
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package ru.noties.markwon.image.file;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.image.ImageItem;
|
||||
import ru.noties.markwon.image.SchemeHandler;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class FileSchemeHandler extends SchemeHandler {
|
||||
|
||||
public static final String SCHEME = "file";
|
||||
|
||||
/**
|
||||
* @see ru.noties.markwon.urlprocessor.UrlProcessorAndroidAssets
|
||||
*/
|
||||
@NonNull
|
||||
public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) {
|
||||
return new FileSchemeHandler(assetManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static FileSchemeHandler create() {
|
||||
return new FileSchemeHandler(null);
|
||||
}
|
||||
|
||||
private static final String FILE_ANDROID_ASSETS = "android_asset";
|
||||
|
||||
@Nullable
|
||||
private final AssetManager assetManager;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
FileSchemeHandler(@Nullable AssetManager assetManager) {
|
||||
this.assetManager = assetManager;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
|
||||
|
||||
final List<String> segments = uri.getPathSegments();
|
||||
if (segments == null
|
||||
|| segments.size() == 0) {
|
||||
// pointing to file & having no path segments is no use
|
||||
throw new IllegalStateException("Invalid file path: " + raw);
|
||||
}
|
||||
|
||||
final InputStream inputStream;
|
||||
|
||||
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
|
||||
final String fileName = uri.getLastPathSegment();
|
||||
|
||||
if (assets) {
|
||||
|
||||
// no handling of assets here if we have no assetsManager
|
||||
if (assetManager != null) {
|
||||
|
||||
final StringBuilder path = new StringBuilder();
|
||||
for (int i = 1, size = segments.size(); i < size; i++) {
|
||||
if (i != 1) {
|
||||
path.append('/');
|
||||
}
|
||||
path.append(segments.get(i));
|
||||
}
|
||||
// load assets
|
||||
|
||||
try {
|
||||
inputStream = assetManager.open(path.toString());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Exception obtaining asset file: " +
|
||||
"" + raw + ", path: " + path.toString(), e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Supplied file path points to assets, " +
|
||||
"but FileSchemeHandler was not supplied with AssetsManager. " +
|
||||
"Use `#createWithAssets` factory method to create FileSchemeHandler " +
|
||||
"that can handle android assets");
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
final String path = uri.getPath();
|
||||
if (TextUtils.isEmpty(path)) {
|
||||
throw new IllegalStateException("Invalid file path: " + raw + ", " + path);
|
||||
}
|
||||
|
||||
try {
|
||||
inputStream = new BufferedInputStream(new FileInputStream(new File(path)));
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new IllegalStateException("Exception reading file: " + raw, e);
|
||||
}
|
||||
}
|
||||
|
||||
final String contentType = MimeTypeMap
|
||||
.getSingleton()
|
||||
.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName));
|
||||
|
||||
return ImageItem.withDecodingNeeded(contentType, inputStream);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package ru.noties.markwon.image.gif;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import pl.droidsonroids.gif.GifDrawable;
|
||||
import ru.noties.markwon.image.DrawableUtils;
|
||||
import ru.noties.markwon.image.MediaDecoder;
|
||||
|
||||
/**
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class GifMediaDecoder extends MediaDecoder {
|
||||
|
||||
public static final String CONTENT_TYPE = "image/gif";
|
||||
|
||||
@NonNull
|
||||
public static GifMediaDecoder create(boolean autoPlayGif) {
|
||||
return new GifMediaDecoder(autoPlayGif);
|
||||
}
|
||||
|
||||
private final boolean autoPlayGif;
|
||||
|
||||
protected GifMediaDecoder(boolean autoPlayGif) {
|
||||
this.autoPlayGif = autoPlayGif;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
|
||||
|
||||
final byte[] bytes;
|
||||
try {
|
||||
bytes = readBytes(inputStream);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot read GIF input-stream", e);
|
||||
}
|
||||
|
||||
final GifDrawable drawable;
|
||||
try {
|
||||
drawable = newGifDrawable(bytes);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Exception creating GifDrawable", e);
|
||||
}
|
||||
|
||||
DrawableUtils.applyIntrinsicBounds(drawable);
|
||||
|
||||
if (!autoPlayGif) {
|
||||
drawable.pause();
|
||||
}
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected GifDrawable newGifDrawable(@NonNull byte[] bytes) throws IOException {
|
||||
return new GifDrawable(bytes);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static byte[] readBytes(@NonNull InputStream stream) throws IOException {
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
final int length = 1024 * 8;
|
||||
final byte[] buffer = new byte[length];
|
||||
int read;
|
||||
while ((read = stream.read(buffer, 0, length)) != -1) {
|
||||
outputStream.write(buffer, 0, read);
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package ru.noties.markwon.image.network;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import ru.noties.markwon.image.ImageItem;
|
||||
import ru.noties.markwon.image.SchemeHandler;
|
||||
|
||||
/**
|
||||
* A simple network scheme handler that is not dependent on any external libraries.
|
||||
*
|
||||
* @see #create()
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class NetworkSchemeHandler extends SchemeHandler {
|
||||
|
||||
public static final String SCHEME_HTTP = "http";
|
||||
public static final String SCHEME_HTTPS = "https";
|
||||
|
||||
@NonNull
|
||||
public static NetworkSchemeHandler create() {
|
||||
return new NetworkSchemeHandler();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
|
||||
|
||||
final ImageItem imageItem;
|
||||
try {
|
||||
|
||||
final URL url = new URL(raw);
|
||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.connect();
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode >= 200 && responseCode < 300) {
|
||||
final String contentType = contentType(connection.getHeaderField("Content-Type"));
|
||||
final InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
||||
imageItem = ImageItem.withDecodingNeeded(contentType, inputStream);
|
||||
} else {
|
||||
throw new IOException("Bad response code: " + responseCode + ", url: " + raw);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Exception obtaining network resource: " + raw, e);
|
||||
}
|
||||
|
||||
return imageItem;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static String contentType(@Nullable String contentType) {
|
||||
|
||||
if (contentType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int index = contentType.indexOf(';');
|
||||
if (index > -1) {
|
||||
return contentType.substring(0, index);
|
||||
}
|
||||
|
||||
return contentType;
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package ru.noties.markwon.image.network;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import ru.noties.markwon.image.ImageItem;
|
||||
import ru.noties.markwon.image.SchemeHandler;
|
||||
|
||||
/**
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
class OkHttpNetworkSchemeHandler extends SchemeHandler {
|
||||
|
||||
/**
|
||||
* @see #create(OkHttpClient)
|
||||
*/
|
||||
@NonNull
|
||||
public static OkHttpNetworkSchemeHandler create() {
|
||||
return new OkHttpNetworkSchemeHandler(new OkHttpClient());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static OkHttpNetworkSchemeHandler create(@NonNull OkHttpClient client) {
|
||||
return new OkHttpNetworkSchemeHandler(client);
|
||||
}
|
||||
|
||||
private static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
OkHttpNetworkSchemeHandler(@NonNull OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
|
||||
|
||||
final Request request = new Request.Builder()
|
||||
.url(raw)
|
||||
.tag(raw)
|
||||
.build();
|
||||
|
||||
final Response response;
|
||||
try {
|
||||
response = client.newCall(request).execute();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Exception obtaining network resource: " + raw, e);
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
throw new IllegalStateException("Could not obtain network response: " + raw);
|
||||
}
|
||||
|
||||
final ResponseBody body = response.body();
|
||||
final InputStream inputStream = body != null
|
||||
? body.byteStream()
|
||||
: null;
|
||||
|
||||
if (inputStream == null) {
|
||||
throw new IllegalStateException("Response does not contain body: " + raw);
|
||||
}
|
||||
|
||||
final String contentType = response.header(HEADER_CONTENT_TYPE);
|
||||
|
||||
return ImageItem.withDecodingNeeded(contentType, inputStream);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package ru.noties.markwon.image.svg;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import ru.noties.markwon.image.DrawableUtils;
|
||||
import ru.noties.markwon.image.MediaDecoder;
|
||||
|
||||
/**
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public class SvgMediaDecoder extends MediaDecoder {
|
||||
|
||||
public static final String CONTENT_TYPE = "image/svg+xml";
|
||||
|
||||
/**
|
||||
* @see #create(Resources)
|
||||
* @since 4.0.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static SvgMediaDecoder create() {
|
||||
return new SvgMediaDecoder(Resources.getSystem());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SvgMediaDecoder create(@NonNull Resources resources) {
|
||||
return new SvgMediaDecoder(resources);
|
||||
}
|
||||
|
||||
private final Resources resources;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
SvgMediaDecoder(Resources resources) {
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
|
||||
|
||||
final SVG svg;
|
||||
try {
|
||||
svg = SVG.getFromInputStream(inputStream);
|
||||
} catch (SVGParseException e) {
|
||||
throw new IllegalStateException("Exception decoding SVG", e);
|
||||
}
|
||||
|
||||
final float w = svg.getDocumentWidth();
|
||||
final float h = svg.getDocumentHeight();
|
||||
final float density = resources.getDisplayMetrics().density;
|
||||
|
||||
final int width = (int) (w * density + .5F);
|
||||
final int height = (int) (h * density + .5F);
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
canvas.scale(density, density);
|
||||
svg.renderToCanvas(canvas);
|
||||
|
||||
final Drawable drawable = new BitmapDrawable(resources, bitmap);
|
||||
DrawableUtils.applyIntrinsicBounds(drawable);
|
||||
return drawable;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ include ':app', ':sample',
|
||||
':markwon-ext-tables',
|
||||
':markwon-ext-tasklist',
|
||||
':markwon-html',
|
||||
':markwon-image',
|
||||
':markwon-image-gif',
|
||||
':markwon-image-okhttp',
|
||||
':markwon-image-svg',
|
||||
|
Loading…
x
Reference in New Issue
Block a user