From d16d6d6cc0ab07389d805d27f9f23d2c1ccbc187 Mon Sep 17 00:00:00 2001 From: Veena Date: Tue, 12 May 2020 11:15:37 +0530 Subject: [PATCH] Fix #1020: Rendering of Svg images from server. (#1019) * Added Svgencoder * Update UrlImageParser.kt * updated kdoc * Update SvgSoftwareLayerSetter.kt * Update SvgEncoder.kt * Update HtmlParserTest.kt * Update RepositoryGlideModule.kt * Update RepositoryGlideModule.kt * Update RepositoryGlideModule.kt * Update SvgSoftwareLayerSetter.kt * Update RepositoryGlideModule.kt * Update UrlImageParser.kt * update changes. * updated changes. * updated nit changes * Fixed lint errors * updated new changes * removed encoder file. * nit changes * Update GlideImageLoader.kt * updated from PictureDrawable to Picture. * Update UrlImageParser.kt * Update GlideImageLoader.kt --- .../org/oppia/app/parser/HtmlParserTest.kt | 10 ----- utility/build.gradle | 1 + .../org/oppia/util/parser/GlideImageLoader.kt | 22 ++++++++++ .../java/org/oppia/util/parser/ImageLoader.kt | 3 ++ .../util/parser/RepositoryGlideModule.kt | 7 +++- .../java/org/oppia/util/parser/SvgDecoder.kt | 31 ++++++++++++++ .../util/parser/SvgDrawableTranscoder.kt | 20 +++++++++ .../org/oppia/util/parser/UrlImageParser.kt | 41 +++++++++++++++---- 8 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 utility/src/main/java/org/oppia/util/parser/SvgDecoder.kt create mode 100644 utility/src/main/java/org/oppia/util/parser/SvgDrawableTranscoder.kt diff --git a/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt index 7326f30ec2a..9e9e627a2df 100644 --- a/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt @@ -151,16 +151,6 @@ class HtmlParserTest { assertThat(bulletSpan1).isNotNull() } - class FakeImageLoader : ImageLoader { - override fun load(imageUrl: String, target: CustomTarget) { - - } - } - - private fun getResources(): Resources { - return ApplicationProvider.getApplicationContext().resources - } - @Qualifier annotation class TestDispatcher // TODO(#89): Move this to a common test application component. diff --git a/utility/build.gradle b/utility/build.gradle index a12c2dc30cd..9b5cc74b857 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -47,6 +47,7 @@ dependencies { implementation( 'androidx.appcompat:appcompat:1.0.2', 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', + 'com.caverock:androidsvg-aar:1.4', 'com.github.bumptech.glide:glide:4.9.0', 'com.google.dagger:dagger:2.24', "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version", diff --git a/utility/src/main/java/org/oppia/util/parser/GlideImageLoader.kt b/utility/src/main/java/org/oppia/util/parser/GlideImageLoader.kt index a9ec6ea3640..75f46dd101f 100644 --- a/utility/src/main/java/org/oppia/util/parser/GlideImageLoader.kt +++ b/utility/src/main/java/org/oppia/util/parser/GlideImageLoader.kt @@ -2,7 +2,10 @@ package org.oppia.util.parser import android.content.Context import android.graphics.Bitmap +import android.graphics.Picture import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.CustomTarget import javax.inject.Inject import org.oppia.util.caching.AssetRepository @@ -14,6 +17,7 @@ class GlideImageLoader @Inject constructor( @CacheAssetsLocally private val cacheAssetsLocally: Boolean, private val assetRepository: AssetRepository ) : ImageLoader { + override fun load(imageUrl: String, target: CustomTarget) { val model: Any = if (cacheAssetsLocally) { object : ImageAssetFetcher { @@ -27,4 +31,22 @@ class GlideImageLoader @Inject constructor( .load(model) .into(target) } + + override fun loadSvg(imageUrl: String, target: CustomTarget) { + val model: Any = if (cacheAssetsLocally) { + object : ImageAssetFetcher { + override fun fetchImage(): ByteArray = assetRepository.loadRemoteBinaryAsset(imageUrl)() + + override fun getImageIdentifier(): String = imageUrl + } + } else imageUrl + + // TODO(#45): Ensure the image caching flow is properly hooked up. + Glide.with(context) + .`as`(Picture::class.java) + .fitCenter() + .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.NONE)) + .load(model) + .into(target) + } } diff --git a/utility/src/main/java/org/oppia/util/parser/ImageLoader.kt b/utility/src/main/java/org/oppia/util/parser/ImageLoader.kt index 1035893ba9e..b272f166d58 100644 --- a/utility/src/main/java/org/oppia/util/parser/ImageLoader.kt +++ b/utility/src/main/java/org/oppia/util/parser/ImageLoader.kt @@ -1,10 +1,13 @@ package org.oppia.util.parser import android.graphics.Bitmap +import android.graphics.Picture import com.bumptech.glide.request.target.CustomTarget /** Loads an image from the provided URL into the specified target, optionally caching it. */ interface ImageLoader { fun load(imageUrl: String, target: CustomTarget) + + fun loadSvg(imageUrl: String, target: CustomTarget) } diff --git a/utility/src/main/java/org/oppia/util/parser/RepositoryGlideModule.kt b/utility/src/main/java/org/oppia/util/parser/RepositoryGlideModule.kt index 4fa1ec02928..711c1bfe164 100644 --- a/utility/src/main/java/org/oppia/util/parser/RepositoryGlideModule.kt +++ b/utility/src/main/java/org/oppia/util/parser/RepositoryGlideModule.kt @@ -1,10 +1,12 @@ package org.oppia.util.parser import android.content.Context +import android.graphics.Picture import com.bumptech.glide.Glide import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule +import com.caverock.androidsvg.SVG import java.io.InputStream import org.oppia.util.caching.AssetRepository @@ -12,6 +14,9 @@ import org.oppia.util.caching.AssetRepository @GlideModule class RepositoryGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - registry.prepend(ImageAssetFetcher::class.java, InputStream::class.java, RepositoryModelLoader.Factory()) + // TODO(#1039): Introduce custom type OppiaImage for rendering Bitmap and Svg. + registry.register(SVG::class.java, Picture::class.java, SvgDrawableTranscoder()) + .append(InputStream::class.java, SVG::class.java, SvgDecoder()) + .append(ImageAssetFetcher::class.java, InputStream::class.java, RepositoryModelLoader.Factory()) } } diff --git a/utility/src/main/java/org/oppia/util/parser/SvgDecoder.kt b/utility/src/main/java/org/oppia/util/parser/SvgDecoder.kt new file mode 100644 index 00000000000..352d7a373fd --- /dev/null +++ b/utility/src/main/java/org/oppia/util/parser/SvgDecoder.kt @@ -0,0 +1,31 @@ +package org.oppia.util.parser + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.caverock.androidsvg.SVG +import com.caverock.androidsvg.SVGParseException +import java.io.IOException +import java.io.InputStream + +/** Decodes an SVG internal representation from an {@link InputStream}. */ +class SvgDecoder : ResourceDecoder { + + override fun handles(source: InputStream, options: Options): Boolean { + return true + } + + override fun decode( + source: InputStream, + width: Int, + height: Int, + options: Options + ): Resource? { + return try { + SimpleResource(source.use { SVG.getFromInputStream(it) }) + } catch (ex: SVGParseException) { + throw IOException("Cannot load SVG from stream", ex) + } + } +} diff --git a/utility/src/main/java/org/oppia/util/parser/SvgDrawableTranscoder.kt b/utility/src/main/java/org/oppia/util/parser/SvgDrawableTranscoder.kt new file mode 100644 index 00000000000..b9dfa26e6fd --- /dev/null +++ b/utility/src/main/java/org/oppia/util/parser/SvgDrawableTranscoder.kt @@ -0,0 +1,20 @@ +package org.oppia.util.parser + +import android.graphics.Picture +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder +import com.caverock.androidsvg.SVG + +/** SvgDrawableTranscoder converts SVG to PictureDrawable. */ +class SvgDrawableTranscoder : ResourceTranscoder { + override fun transcode( + toTranscode: Resource, + options: Options + ): Resource? { + val svg: SVG = toTranscode.get() + val picture = svg.renderToPicture() + return SimpleResource(picture) + } +} diff --git a/utility/src/main/java/org/oppia/util/parser/UrlImageParser.kt b/utility/src/main/java/org/oppia/util/parser/UrlImageParser.kt index f7c2ce18516..feacd38fa7c 100644 --- a/utility/src/main/java/org/oppia/util/parser/UrlImageParser.kt +++ b/utility/src/main/java/org/oppia/util/parser/UrlImageParser.kt @@ -3,9 +3,11 @@ package org.oppia.util.parser import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas +import android.graphics.Picture import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.graphics.drawable.PictureDrawable import android.text.Html import android.view.ViewTreeObserver import android.widget.TextView @@ -36,21 +38,41 @@ class UrlImageParser private constructor( override fun getDrawable(urlString: String): Drawable { val imageUrl = String.format(imageDownloadUrlTemplate, entityType, entityId, urlString) val urlDrawable = UrlDrawable() - val target = BitmapTarget(urlDrawable) - imageLoader.load( - gcsPrefix + gcsResource + imageUrl, - target - ) + // TODO(#1039): Introduce custom type OppiaImage for rendering Bitmap and Svg. + if (imageUrl.endsWith("svg", ignoreCase = true)) { + val target = SvgTarget(urlDrawable) + imageLoader.loadSvg( + gcsPrefix + gcsResource + imageUrl, + target + ) + } else { + val target = BitmapTarget(urlDrawable) + imageLoader.load( + gcsPrefix + gcsResource + imageUrl, + target + ) + } return urlDrawable } - private inner class BitmapTarget(private val urlDrawable: UrlDrawable) : CustomTarget() { + private inner class BitmapTarget(urlDrawable: UrlDrawable) : CustomImageTarget( + urlDrawable, { resource -> BitmapDrawable(context.resources, resource) } + ) + + private inner class SvgTarget(urlDrawable: UrlDrawable) : CustomImageTarget( + urlDrawable, { resource -> PictureDrawable(resource) } + ) + + private open inner class CustomImageTarget( + private val urlDrawable: UrlDrawable, + private val drawableFactory: (T) -> Drawable + ) : CustomTarget() { override fun onLoadCleared(placeholder: Drawable?) { // No resources to clear. } - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - val drawable = BitmapDrawable(context.resources, resource) + override fun onResourceReady(resource: T, transition: Transition?) { + val drawable = drawableFactory(resource) htmlContentTextView.post { htmlContentTextView.width { val drawableHeight = drawable.intrinsicHeight @@ -60,7 +82,8 @@ class UrlImageParser private constructor( } else { 0 } - val rect = Rect(initialDrawableMargin, 0, drawableWidth + initialDrawableMargin, drawableHeight) + val rect = + Rect(initialDrawableMargin, 0, drawableWidth + initialDrawableMargin, drawableHeight) drawable.bounds = rect urlDrawable.bounds = rect urlDrawable.drawable = drawable