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 {
|
public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureParser(@NonNull Parser.Builder builder) {
|
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
|
@Override
|
||||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
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
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String processMarkdown(@NonNull String markdown) {
|
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 Builder
|
||||||
|
* @see #builderNoCore(Context)
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Builder builder(@NonNull Context context) {
|
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);
|
return new MarkwonBuilderImpl(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package ru.noties.markwon;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@ -9,19 +10,17 @@ import org.commonmark.parser.Parser;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import ru.noties.markwon.core.CorePlugin;
|
|
||||||
import ru.noties.markwon.core.MarkwonTheme;
|
import ru.noties.markwon.core.MarkwonTheme;
|
||||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
||||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
|
||||||
import ru.noties.markwon.priority.PriorityProcessor;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
class MarkwonBuilderImpl implements Markwon.Builder {
|
class MarkwonBuilderImpl implements Markwon.Builder {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -30,8 +29,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
|
|
||||||
private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;
|
private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;
|
||||||
|
|
||||||
private PriorityProcessor priorityProcessor;
|
|
||||||
|
|
||||||
MarkwonBuilderImpl(@NonNull Context context) {
|
MarkwonBuilderImpl(@NonNull Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
@ -69,13 +66,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("UnusedReturnValue")
|
|
||||||
@NonNull
|
|
||||||
public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) {
|
|
||||||
this.priorityProcessor = priorityProcessor;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Markwon build() {
|
public Markwon build() {
|
||||||
@ -85,21 +75,12 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
"method to add them");
|
"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
|
// please note that this method must not modify supplied collection
|
||||||
// if nothing should be done -> the same collection can be returned
|
// 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 Parser.Builder parserBuilder = new Parser.Builder();
|
||||||
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
|
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
|
||||||
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
|
|
||||||
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
|
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
|
||||||
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
|
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
|
||||||
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
|
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
|
||||||
@ -108,7 +89,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
for (MarkwonPlugin plugin : plugins) {
|
for (MarkwonPlugin plugin : plugins) {
|
||||||
plugin.configureParser(parserBuilder);
|
plugin.configureParser(parserBuilder);
|
||||||
plugin.configureTheme(themeBuilder);
|
plugin.configureTheme(themeBuilder);
|
||||||
plugin.configureImages(asyncDrawableLoaderBuilder);
|
|
||||||
plugin.configureConfiguration(configurationBuilder);
|
plugin.configureConfiguration(configurationBuilder);
|
||||||
plugin.configureVisitor(visitorBuilder);
|
plugin.configureVisitor(visitorBuilder);
|
||||||
plugin.configureSpansFactory(spanFactoryBuilder);
|
plugin.configureSpansFactory(spanFactoryBuilder);
|
||||||
@ -117,7 +97,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
|
|
||||||
final MarkwonConfiguration configuration = configurationBuilder.build(
|
final MarkwonConfiguration configuration = configurationBuilder.build(
|
||||||
themeBuilder.build(),
|
themeBuilder.build(),
|
||||||
asyncDrawableLoaderBuilder.build(),
|
|
||||||
htmlRendererBuilder.build(),
|
htmlRendererBuilder.build(),
|
||||||
spanFactoryBuilder.build());
|
spanFactoryBuilder.build());
|
||||||
|
|
||||||
@ -133,62 +112,107 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@NonNull
|
@NonNull
|
||||||
static List<MarkwonPlugin> preparePlugins(
|
static List<MarkwonPlugin> preparePlugins(@NonNull List<MarkwonPlugin> plugins) {
|
||||||
@NonNull PriorityProcessor priorityProcessor,
|
return new RegistryImpl(plugins).process();
|
||||||
@NonNull List<MarkwonPlugin> plugins) {
|
}
|
||||||
|
|
||||||
// with this method we will ensure that CorePlugin is added IF and ONLY IF
|
// @since 4.0.0-SNAPSHOT
|
||||||
// there are plugins that depend on it. If CorePlugin is added, or there are
|
private static class RegistryImpl implements MarkwonPlugin.Registry {
|
||||||
// no plugins that require it, CorePlugin won't be added
|
|
||||||
final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins);
|
private final List<MarkwonPlugin> origin;
|
||||||
|
private final List<MarkwonPlugin> plugins;
|
||||||
return priorityProcessor.process(out);
|
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
|
@NonNull
|
||||||
static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) {
|
@Override
|
||||||
// loop over plugins -> if CorePlugin is found -> break;
|
public <P extends MarkwonPlugin> P require(@NonNull Class<P> plugin) {
|
||||||
// iterate over all plugins and check if CorePlugin is requested
|
return get(plugin);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if plugin has CorePlugin in dependencies -> mark for addition
|
@Override
|
||||||
if (!hasCoreDependents) {
|
public <P extends MarkwonPlugin> void require(
|
||||||
// here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass
|
@NonNull Class<P> plugin,
|
||||||
// or something -> ignore)
|
@NonNull MarkwonPlugin.Action<? super P> action) {
|
||||||
if (plugin.priority().after().contains(CorePlugin.class)) {
|
action.apply(get(plugin));
|
||||||
hasCoreDependents = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// important thing here is to check if corePlugin is added
|
@NonNull
|
||||||
// add it _only_ if it's not present
|
List<MarkwonPlugin> process() {
|
||||||
if (hasCoreDependents && !hasCore) {
|
for (MarkwonPlugin plugin : origin) {
|
||||||
final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1);
|
configure(plugin);
|
||||||
// add default instance of CorePlugin
|
|
||||||
out.add(CorePlugin.create());
|
|
||||||
out.addAll(plugins);
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugins;
|
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() {
|
Builder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.0.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Builder asyncDrawableLoader(@NonNull AsyncDrawableLoader asyncDrawableLoader) {
|
||||||
|
this.asyncDrawableLoader = asyncDrawableLoader;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
|
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
|
||||||
this.syntaxHighlight = syntaxHighlight;
|
this.syntaxHighlight = syntaxHighlight;
|
||||||
@ -149,15 +158,18 @@ public class MarkwonConfiguration {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public MarkwonConfiguration build(
|
public MarkwonConfiguration build(
|
||||||
@NonNull MarkwonTheme theme,
|
@NonNull MarkwonTheme theme,
|
||||||
@NonNull AsyncDrawableLoader asyncDrawableLoader,
|
|
||||||
@NonNull MarkwonHtmlRenderer htmlRenderer,
|
@NonNull MarkwonHtmlRenderer htmlRenderer,
|
||||||
@NonNull MarkwonSpansFactory spansFactory) {
|
@NonNull MarkwonSpansFactory spansFactory) {
|
||||||
|
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
this.asyncDrawableLoader = asyncDrawableLoader;
|
|
||||||
this.htmlRenderer = htmlRenderer;
|
this.htmlRenderer = htmlRenderer;
|
||||||
this.spansFactory = spansFactory;
|
this.spansFactory = spansFactory;
|
||||||
|
|
||||||
|
// @since 4.0.0-SNAPSHOT
|
||||||
|
if (asyncDrawableLoader == null) {
|
||||||
|
asyncDrawableLoader = AsyncDrawableLoader.noOp();
|
||||||
|
}
|
||||||
|
|
||||||
if (syntaxHighlight == null) {
|
if (syntaxHighlight == null) {
|
||||||
syntaxHighlight = new SyntaxHighlightNoOp();
|
syntaxHighlight = new SyntaxHighlightNoOp();
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,6 @@ import org.commonmark.parser.Parser;
|
|||||||
|
|
||||||
import ru.noties.markwon.core.MarkwonTheme;
|
import ru.noties.markwon.core.MarkwonTheme;
|
||||||
import ru.noties.markwon.html.MarkwonHtmlRenderer;
|
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
|
* 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 {
|
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
|
* Method to configure <code>org.commonmark.parser.Parser</code> (for example register custom
|
||||||
* extension, etc).
|
* extension, etc).
|
||||||
@ -39,17 +64,6 @@ public interface MarkwonPlugin {
|
|||||||
*/
|
*/
|
||||||
void configureTheme(@NonNull MarkwonTheme.Builder builder);
|
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}
|
* Configure {@link MarkwonConfiguration}
|
||||||
*
|
*
|
||||||
@ -82,9 +96,6 @@ public interface MarkwonPlugin {
|
|||||||
*/
|
*/
|
||||||
void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder);
|
void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder);
|
||||||
|
|
||||||
@NonNull
|
|
||||||
Priority priority();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process input markdown and return new string to be used in parsing stage further.
|
* 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.
|
* 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());
|
.setFactory(ThematicBreak.class, new ThematicBreakSpanFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Priority priority() {
|
|
||||||
return Priority.none();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||||
OrderedListItemSpan.measure(textView, markdown);
|
OrderedListItemSpan.measure(textView, markdown);
|
||||||
|
@ -13,22 +13,6 @@ import java.util.concurrent.Executors;
|
|||||||
|
|
||||||
public abstract class AsyncDrawableLoader {
|
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
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
@ -71,123 +55,4 @@ public abstract class AsyncDrawableLoader {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public abstract Drawable placeholder();
|
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;
|
package ru.noties.markwon.image;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -7,12 +9,48 @@ import java.io.InputStream;
|
|||||||
/**
|
/**
|
||||||
* @since 2.0.0
|
* @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 String contentType;
|
||||||
private final InputStream inputStream;
|
private final InputStream inputStream;
|
||||||
|
|
||||||
public ImageItem(
|
WithDecodingNeeded(
|
||||||
@Nullable String contentType,
|
@Nullable String contentType,
|
||||||
@Nullable InputStream inputStream) {
|
@Nullable InputStream inputStream) {
|
||||||
this.contentType = contentType;
|
this.contentType = contentType;
|
||||||
@ -28,4 +66,5 @@ public class ImageItem {
|
|||||||
public InputStream inputStream() {
|
public InputStream inputStream() {
|
||||||
return inputStream;
|
return inputStream;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import ru.noties.markwon.MarkwonPlugin;
|
|||||||
* @see MarkwonPlugin#priority()
|
* @see MarkwonPlugin#priority()
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public abstract class Priority {
|
public abstract class Priority {
|
||||||
|
|
||||||
@NonNull
|
@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-tables',
|
||||||
':markwon-ext-tasklist',
|
':markwon-ext-tasklist',
|
||||||
':markwon-html',
|
':markwon-html',
|
||||||
|
':markwon-image',
|
||||||
':markwon-image-gif',
|
':markwon-image-gif',
|
||||||
':markwon-image-okhttp',
|
':markwon-image-okhttp',
|
||||||
':markwon-image-svg',
|
':markwon-image-svg',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user