Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix #1020: Rendering of Svg images from server. #1019

Merged
merged 25 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 0 additions & 10 deletions app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,6 @@ class HtmlParserTest {
assertThat(bulletSpan1).isNotNull()
}

class FakeImageLoader : ImageLoader {
override fun load(imageUrl: String, target: CustomTarget<Bitmap>) {

}
}

private fun getResources(): Resources {
return ApplicationProvider.getApplicationContext<Context>().resources
}

@Qualifier annotation class TestDispatcher

// TODO(#89): Move this to a common test application component.
Expand Down
1 change: 1 addition & 0 deletions utility/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions utility/src/main/java/org/oppia/util/parser/GlideImageLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Bitmap>) {
val model: Any = if (cacheAssetsLocally) {
object : ImageAssetFetcher {
Expand All @@ -27,4 +31,22 @@ class GlideImageLoader @Inject constructor(
.load(model)
.into(target)
}

override fun loadSvg(imageUrl: String, target: CustomTarget<Picture>) {
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))
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
.load(model)
.into(target)
}
}
3 changes: 3 additions & 0 deletions utility/src/main/java/org/oppia/util/parser/ImageLoader.kt
Original file line number Diff line number Diff line change
@@ -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<Bitmap>)

fun loadSvg(imageUrl: String, target: CustomTarget<Picture>)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
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
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
import org.oppia.util.caching.AssetRepository

/** Custom [AppGlideModule] to enable loading images from [AssetRepository] via Glide. */
@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())
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
}
}
31 changes: 31 additions & 0 deletions utility/src/main/java/org/oppia/util/parser/SvgDecoder.kt
Original file line number Diff line number Diff line change
@@ -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<InputStream?, SVG?> {
veena14cs marked this conversation as resolved.
Show resolved Hide resolved

override fun handles(source: InputStream, options: Options): Boolean {
return true
}

override fun decode(
source: InputStream,
width: Int,
height: Int,
options: Options
): Resource<SVG?>? {
return try {
SimpleResource(source.use { SVG.getFromInputStream(it) })
} catch (ex: SVGParseException) {
throw IOException("Cannot load SVG from stream", ex)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<SVG?, Picture?> {
override fun transcode(
toTranscode: Resource<SVG?>,
options: Options
): Resource<Picture?>? {
val svg: SVG = toTranscode.get()
val picture = svg.renderToPicture()
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
return SimpleResource(picture)
}
}
41 changes: 32 additions & 9 deletions utility/src/main/java/org/oppia/util/parser/UrlImageParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Bitmap>() {
private inner class BitmapTarget(urlDrawable: UrlDrawable) : CustomImageTarget<Bitmap>(
urlDrawable, { resource -> BitmapDrawable(context.resources, resource) }
)

private inner class SvgTarget(urlDrawable: UrlDrawable) : CustomImageTarget<Picture>(
urlDrawable, { resource -> PictureDrawable(resource) }
)

private open inner class CustomImageTarget<T>(
private val urlDrawable: UrlDrawable,
private val drawableFactory: (T) -> Drawable
) : CustomTarget<T>() {
override fun onLoadCleared(placeholder: Drawable?) {
// No resources to clear.
}

override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val drawable = BitmapDrawable(context.resources, resource)
override fun onResourceReady(resource: T, transition: Transition<in T>?) {
val drawable = drawableFactory(resource)
htmlContentTextView.post {
htmlContentTextView.width {
val drawableHeight = drawable.intrinsicHeight
Expand All @@ -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
Expand Down