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 12 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.drawable.PictureDrawable
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<PictureDrawable>) {
val model: Any = if (cacheAssetsLocally) {
object : ImageAssetFetcher {
override fun fetchImage(): ByteArray = assetRepository.loadRemoteBinaryAsset(imageUrl)()

override fun getImageIdentifier(): String = imageUrl
}
} else imageUrl

Glide.with(context)
.`as`(PictureDrawable::class.java)
.fitCenter()
.listener(SvgSoftwareLayerSetter())
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.RESOURCE))
.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.drawable.PictureDrawable
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<PictureDrawable>)
}
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.drawable.PictureDrawable
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())
registry.register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder())
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
.prepend(SVG::class.java, SvgEncoder())
BenHenning marked this conversation as resolved.
Show resolved Hide resolved
.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
}
}
32 changes: 32 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,32 @@
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 {
val svg = SVG.getFromInputStream(source)
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
SimpleResource(svg)
} catch (ex: SVGParseException) {
throw IOException("Cannot load SVG from stream", ex)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.oppia.util.parser

import android.graphics.drawable.PictureDrawable
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?, PictureDrawable?> {
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
override fun transcode(
toTranscode: Resource<SVG?>,
options: Options
): Resource<PictureDrawable?>? {
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
val svg: SVG = toTranscode.get()
val picture = svg.renderToPicture()
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
val drawable = PictureDrawable(picture)
return SimpleResource(drawable)
}
}
29 changes: 29 additions & 0 deletions utility/src/main/java/org/oppia/util/parser/SvgEncoder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.oppia.util.parser

import com.bumptech.glide.load.EncodeStrategy
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceEncoder
import com.bumptech.glide.load.engine.Resource
import com.caverock.androidsvg.SVG
import java.io.File
import java.io.FileOutputStream

/** Encodes an SVG internal representation to {@link FileOutputStream}. */
class SvgEncoder : ResourceEncoder<SVG?> {
veena14cs marked this conversation as resolved.
Show resolved Hide resolved

override fun getEncodeStrategy(options: Options): EncodeStrategy {
return EncodeStrategy.SOURCE
}

override fun encode(data: Resource<SVG?>, file: File, options: Options): Boolean {
return try {
val svg: SVG = data.get()
val picture = svg.renderToPicture()
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
picture.writeToStream(FileOutputStream(file))
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
true
} catch (e: Exception) {
e.printStackTrace()
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.oppia.util.parser

import android.graphics.drawable.PictureDrawable
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target

/**
* Listener which updates the {@link ImageView} to be software rendered, because {@link
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
* com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on a
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
* hardware backed {@link android.graphics.Canvas Canvas}.
*/
class SvgSoftwareLayerSetter : RequestListener<PictureDrawable> {
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<PictureDrawable?>?,
isFirstResource: Boolean
): Boolean {
return false
}

override fun onResourceReady(
resource: PictureDrawable?,
model: Any?,
target: Target<PictureDrawable?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
return false
}
}
54 changes: 48 additions & 6 deletions utility/src/main/java/org/oppia/util/parser/UrlImageParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.graphics.Canvas
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,11 +37,19 @@ 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
)
if (imageUrl.endsWith("svg")) {
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
val target = SvgBitmapTarget(urlDrawable)
imageLoader.loadSvg(
gcsPrefix + gcsResource + imageUrl,
target
)
} else {
val target = BitmapTarget(urlDrawable)
imageLoader.load(
gcsPrefix + gcsResource + imageUrl,
target
)
}
return urlDrawable
}

Expand All @@ -60,7 +69,40 @@ 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
htmlContentTextView.text = htmlContentTextView.text
htmlContentTextView.invalidate()
}
}
}
}

private inner class SvgBitmapTarget(private val urlDrawable: UrlDrawable) :
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
CustomTarget<PictureDrawable>() {
override fun onLoadCleared(placeholder: Drawable?) {
// No resources to clear.
}

override fun onResourceReady(
resource: PictureDrawable,
transition: Transition<in PictureDrawable>?
) {
val drawable = PictureDrawable(resource.picture)
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
htmlContentTextView.post {
htmlContentTextView.width {
val drawableHeight = drawable.intrinsicHeight
val drawableWidth = drawable.intrinsicWidth
val initialDrawableMargin = if (imageCenterAlign) {
calculateInitialMargin(it, drawableWidth)
} else {
0
}
val rect =
Rect(initialDrawableMargin, 0, drawableWidth + initialDrawableMargin, drawableHeight)
drawable.bounds = rect
urlDrawable.bounds = rect
urlDrawable.drawable = drawable
Expand Down