diff --git a/husky/app/build.gradle.kts b/husky/app/build.gradle.kts index 6ebee0a..82268d9 100644 --- a/husky/app/build.gradle.kts +++ b/husky/app/build.gradle.kts @@ -217,8 +217,10 @@ dependencies { implementation(ApplicationLibs.Dagger.daggerSupport) implementation(ApplicationLibs.Glide.glide) - implementation(ApplicationLibs.Glide.glideOkhttp) kapt(ApplicationLibs.Glide.glideCompiler) + implementation(ApplicationLibs.Glide.glideImage) + implementation(ApplicationLibs.Glide.glideImageViewFactory) + implementation(ApplicationLibs.Glide.glideOkhttp) implementation(ApplicationLibs.Google.flexbox) implementation(ApplicationLibs.Google.exoplayer) @@ -244,14 +246,13 @@ dependencies { implementation(ApplicationLibs.acraMail) implementation(ApplicationLibs.acraNotification) implementation(ApplicationLibs.androidImageCropper) + implementation(ApplicationLibs.androidSvg) implementation(ApplicationLibs.autodispose) implementation(ApplicationLibs.autodisposeAndroidArchComp) implementation(ApplicationLibs.bigImageViewer) implementation(ApplicationLibs.conscryptAndroid) implementation(ApplicationLibs.filemojiCompat) implementation(ApplicationLibs.fragmentviewbindingdelegateKt) - implementation(ApplicationLibs.glideImage) - implementation(ApplicationLibs.glideImageViewFactory) implementation(ApplicationLibs.markdownEdit) implementation(ApplicationLibs.materialDrawer) implementation(ApplicationLibs.materialDrawerIconics) diff --git a/husky/app/proguard-rules.pro b/husky/app/proguard-rules.pro index e5721d6..712fb0f 100644 --- a/husky/app/proguard-rules.pro +++ b/husky/app/proguard-rules.pro @@ -24,6 +24,7 @@ -keepclassmembers class * extends android.app.Activity { public void *(android.view.View); } + # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations -keepclassmembers enum * { public static **[] values(); diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgBitmapTranscoder.kt b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgBitmapTranscoder.kt new file mode 100644 index 0000000..dc0a0c8 --- /dev/null +++ b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgBitmapTranscoder.kt @@ -0,0 +1,29 @@ +package com.keylesspalace.tusky.core.utils.image + +import android.graphics.Bitmap +import android.graphics.Canvas +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 + +class SvgBitmapTranscoder : ResourceTranscoder { + override fun transcode(toTranscode: Resource, options: Options): Resource { + val svg = toTranscode.get() + val width = svg.documentWidth.toInt().takeIf { it > 0 } + ?: (svg.documentViewBox.right - svg.documentViewBox.left).toInt() + val height = svg.documentHeight.toInt().takeIf { it > 0 } + ?: (svg.documentViewBox.bottom - svg.documentViewBox.top).toInt() + + val picture = svg.renderToPicture(width, height) + val drawable = PictureDrawable(picture) + + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawPicture(drawable.picture) + + return SimpleResource(bitmap) + } +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgDecoder.kt b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgDecoder.kt new file mode 100644 index 0000000..27ad18c --- /dev/null +++ b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgDecoder.kt @@ -0,0 +1,64 @@ +/* + * Husky -- A Pleroma client for Android + * + * Copyright (C) 2021 The Husky Developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Based on Glide's SVG decoder. Rewriten to Kotlin. + */ + +package com.keylesspalace.tusky.core.utils.image + +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.bumptech.glide.request.target.Target.SIZE_ORIGINAL +import com.caverock.androidsvg.SVG +import com.caverock.androidsvg.SVGParseException +import java.io.IOException +import java.io.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 { + try { + val svg = SVG.getFromInputStream(source) + + if(width != SIZE_ORIGINAL) { + svg.documentWidth = width.toFloat() + } + + if(height != SIZE_ORIGINAL) { + svg.documentHeight = height.toFloat() + } + + return SimpleResource(svg) + } catch(ex: SVGParseException) { + throw IOException("Cannot load SVG from stream", ex); + } + } +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgDrawableTranscoder.kt b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgDrawableTranscoder.kt new file mode 100644 index 0000000..64f7b66 --- /dev/null +++ b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgDrawableTranscoder.kt @@ -0,0 +1,62 @@ +/* + * Husky -- A Pleroma client for Android + * + * Copyright (C) 2021 The Husky Developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Some code is based on https://github.com/qoqa/glide-svg + * + * No copyright asserted on the source code of this repository. + */ + +package com.keylesspalace.tusky.core.utils.image + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +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 + +class SvgDrawableTranscoder(private val context: Context) : ResourceTranscoder { + + override fun transcode(toTranscode: Resource, options: Options): Resource { + return SimpleResource( + BitmapDrawable( + context.resources, + getBitmap(toTranscode.get()).get() + ) + ) + } + + private fun getBitmap(svg: SVG): SimpleResource { + val width = svg.documentWidth.toInt() + val height = svg.documentHeight.toInt() + + val drawable = PictureDrawable(svg.renderToPicture(width, height)) + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawPicture(drawable.picture) + + return SimpleResource(bitmap) + } +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgModule.java b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgModule.java new file mode 100644 index 0000000..08fc1b9 --- /dev/null +++ b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgModule.java @@ -0,0 +1,46 @@ +/* + * Husky -- A Pleroma client for Android + * + * Copyright (C) 2021 The Husky Developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.keylesspalace.tusky.core.utils.image; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.LibraryGlideModule; +import com.caverock.androidsvg.SVG; +import java.io.InputStream; + +/** + * Module for adding SVG decoding support to Glide. + */ +@GlideModule +public class SvgModule extends LibraryGlideModule { + + @Override + public void registerComponents( + @NonNull Context context, + @NonNull Glide glide, + @NonNull Registry registry) { + registry.register(SVG.class, Drawable.class, new SvgDrawableTranscoder(context)) + .append(InputStream.class, SVG.class, new SvgDecoder()); + } +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt index fc84d6f..45e26ad 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -1,19 +1,25 @@ -/* Copyright 2020 Tusky Contributors +/* + * Husky -- A Pleroma client for Android * - * This file is a part of Tusky. + * Copyright (C) 2021 The Husky Developers + * Copyright (C) 2021 Tusky Contributors * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ @file:JvmName("CustomEmojiHelper") + package com.keylesspalace.tusky.util import android.graphics.Canvas @@ -23,28 +29,35 @@ import android.text.SpannableString import android.text.Spanned import android.text.style.ReplacementSpan import android.view.View - +import android.webkit.MimeTypeMap +import androidx.preference.PreferenceManager import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition import com.keylesspalace.tusky.entity.Emoji - +import com.keylesspalace.tusky.settings.PrefKeys import java.lang.ref.WeakReference import java.util.regex.Pattern -import androidx.preference.PreferenceManager -import com.keylesspalace.tusky.settings.PrefKeys /** - * replaces emoji shortcodes in a text with EmojiSpans - * @param text the text containing custom emojis - * @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances) - * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) - * @return the text with the shortcodes replaced by EmojiSpans -*/ -fun CharSequence.emojify(emojis: List?, view: View, forceSmallEmoji: Boolean) : CharSequence { - if(emojis.isNullOrEmpty()) + * Replaces emoji shortcodes in a text with EmojiSpans. + * + * @param text the text containing custom emojis. + * @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances). + * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable). + * + * @return The text with the shortcodes replaced by EmojiSpans + */ +fun CharSequence.emojify( + emojis: List?, + view: View, + forceSmallEmoji: Boolean = false +): CharSequence { + if(emojis.isNullOrEmpty()) { return this + } val builder = SpannableString.valueOf(this) val pm = PreferenceManager.getDefaultSharedPreferences(view.context) @@ -63,24 +76,36 @@ fun CharSequence.emojify(emojis: List?, view: View, forceSmallEmoji: Bool } builder.setSpan(span, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - Glide.with(view) - .asDrawable() - .load(url) - .into(span.getTarget(animate)) + + var glideRequest = Glide.with(view).load(url) + val mimetype = getMimeType(url) + if(mimetype == MIME.SVG) { + glideRequest = glideRequest + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .override(512, 512) + } + glideRequest.into(span.getTarget(animate)) } } + return builder } -fun CharSequence.emojify(emojis: List?, view: View) : CharSequence { +fun CharSequence.emojify(emojis: List?, view: View): CharSequence { return this.emojify(emojis, view, false) } open class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() { var imageDrawable: Drawable? = null - - override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?) : Int { - if (fm != null) { + + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { + if(fm != null) { /* update FontMetricsInt or otherwise span does not get drawn when * it covers the whole text */ val metrics = paint.fontMetricsInt @@ -89,11 +114,21 @@ open class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSp fm.descent = (metrics.descent * 2.0f).toInt() fm.bottom = (metrics.bottom * 3.5f).toInt() } - + return (paint.textSize * 2.0).toInt() } - - override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { imageDrawable?.let { drawable -> canvas.save() @@ -108,21 +143,24 @@ open class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSp canvas.restore() } } - - fun getTarget(animate : Boolean): Target { + + fun getTarget(animate: Boolean): Target { return object : CustomTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { viewWeakReference.get()?.let { view -> if(animate && resource is Animatable) { val callback = resource.callback - resource.callback = object: Drawable.Callback { + resource.callback = object : Drawable.Callback { override fun unscheduleDrawable(p0: Drawable, p1: Runnable) { callback?.unscheduleDrawable(p0, p1) } + override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) { callback?.scheduleDrawable(p0, p1, p2) } + override fun invalidateDrawable(p0: Drawable) { callback?.invalidateDrawable(p0) view.invalidate() @@ -135,16 +173,21 @@ open class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSp view.invalidate() } } - + override fun onLoadCleared(placeholder: Drawable?) {} } } } -class SmallEmojiSpan(viewWeakReference: WeakReference) - : EmojiSpan(viewWeakReference) { - override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { - if (fm != null) { +class SmallEmojiSpan(viewWeakReference: WeakReference) : EmojiSpan(viewWeakReference) { + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { + if(fm != null) { /* update FontMetricsInt or otherwise span does not get drawn when * it covers the whole text */ val metrics = paint.fontMetricsInt @@ -157,3 +200,32 @@ class SmallEmojiSpan(viewWeakReference: WeakReference) return paint.textSize.toInt() } } + +/** + * Get the Mimetype fron the URL. + * + * @return MIME - The Mimetype. + */ +private fun getMimeType(url: String?): MIME { + var type: String? = null + val extension = MimeTypeMap.getFileExtensionFromUrl(url) + if(extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } + return MIME.getMime(type) +} + +private enum class MIME(private val mimetype: String) { + + NONE(""), + SVG("image/svg+xml"); + + companion object { + fun getMime(mime: String?): MIME { + return when(mime) { + SVG.mimetype -> SVG + else -> NONE + } + } + } +} diff --git a/husky/buildSrc/src/main/kotlin/AppLibs.kt b/husky/buildSrc/src/main/kotlin/AppLibs.kt index f64751a..4a8d723 100644 --- a/husky/buildSrc/src/main/kotlin/AppLibs.kt +++ b/husky/buildSrc/src/main/kotlin/AppLibs.kt @@ -16,6 +16,7 @@ object ApplicationLibs { private object Versions { const val acra = "5.8.4" const val androidImageCropper = "2.8.0" + const val androidSvg = "1.4" const val appcompat = "1.3.1" const val autodispose = "1.4.0" const val bigImageViewer = "1.7.0" @@ -105,6 +106,9 @@ object ApplicationLibs { object Glide { const val glide = "com.github.bumptech.glide:glide:${Versions.glide}" const val glideCompiler = "com.github.bumptech.glide:compiler:${Versions.glide}" + const val glideImage = "com.github.piasy:GlideImageLoader:${Versions.glideImage}" + const val glideImageViewFactory = + "com.github.piasy:GlideImageViewFactory:${Versions.glideImage}" const val glideOkhttp = "com.github.bumptech.glide:okhttp3-integration:${Versions.glide}" } @@ -145,6 +149,7 @@ object ApplicationLibs { const val acraMail = "ch.acra:acra-mail:${Versions.acra}" const val acraNotification = "ch.acra:acra-notification:${Versions.acra}" + const val androidSvg = "com.caverock:androidsvg-aar:${Versions.androidSvg}" const val androidImageCropper = "com.theartofdev.edmodo:android-image-cropper:${Versions.androidImageCropper}" const val autodispose = "com.uber.autodispose:autodispose:${Versions.autodispose}" @@ -155,9 +160,6 @@ object ApplicationLibs { const val filemojiCompat = "de.c1710:filemojicompat:${Versions.filemojiCompat}" const val fragmentviewbindingdelegateKt = "com.github.Zhuinden:fragmentviewbindingdelegate-kt:${Versions.fragmentviewbindingdelegateKt}" - const val glideImage = "com.github.piasy:GlideImageLoader:${Versions.glideImage}" - const val glideImageViewFactory = - "com.github.piasy:GlideImageViewFactory:${Versions.glideImage}" const val markdownEdit = "com.github.Tunous:MarkdownEdit:${Versions.markdownEdit}" const val materialDrawer = "com.mikepenz:materialdrawer:${Versions.materialDrawer}" const val materialDrawerIconics =