diff --git a/app/build.gradle b/app/build.gradle index b8d2357d6..ee2661e75 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,3 +161,10 @@ def orgJavaLocation() { return "com.orgzly:org-java:$org_java_version" } } + +apply plugin: 'kotlin-kapt' + +dependencies { + implementation 'com.github.bumptech.glide:glide:4.8.0' + kapt 'com.github.bumptech.glide:compiler:4.8.0' +} diff --git a/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java b/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java index c1408c6d1..4768cd3ca 100644 --- a/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java +++ b/app/src/main/java/com/orgzly/android/prefs/AppPreferences.java @@ -541,6 +541,37 @@ public static void setLastRepeatOnTimeShift(Context context, boolean value) { getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply(); } + /* + * Allow inlining images + */ + public static boolean displayInlineImages(Context context) { + return getDefaultSharedPreferences(context).getBoolean( + context.getResources().getString(R.string.pref_key_display_inline_images), + context.getResources().getBoolean(R.bool.pref_default_display_inline_images)); + } + + public static void displayInlineImages(Context context, boolean value) { + String key = context.getResources().getString(R.string.pref_key_display_inline_images); + getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply(); + } + + public static boolean enableImageScaling(Context context) { + return getDefaultSharedPreferences(context).getBoolean( + context.getResources().getString(R.string.pref_key_enable_image_scaling), + context.getResources().getBoolean(R.bool.pref_default_enable_image_scaling)); + } + + public static void enableImageScaling(Context context, boolean value) { + String key = context.getResources().getString(R.string.pref_key_enable_image_scaling); + getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply(); + } + + public static int setImageFixedWidth(Context context) { + return Integer.valueOf(getDefaultSharedPreferences(context).getString( + context.getResources().getString(R.string.pref_key_set_image_fixed_width), + context.getResources().getString(R.string.pref_default_set_image_fixed_width))); + } + /* * Note's metadata visibility */ diff --git a/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt b/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt index df6b4b935..542993fc4 100644 --- a/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt +++ b/app/src/main/java/com/orgzly/android/ui/ImageLoader.kt @@ -1,8 +1,26 @@ package com.orgzly.android.ui +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.os.Environment +import android.support.v4.content.FileProvider import android.text.Spannable +import android.text.style.ImageSpan +import android.view.View +import com.orgzly.BuildConfig +import com.orgzly.android.App +import com.orgzly.android.prefs.AppPreferences import com.orgzly.android.ui.views.TextViewWithMarkup import com.orgzly.android.ui.views.style.FileLinkSpan +import com.orgzly.android.util.AppPermissions +import java.io.File +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.SimpleTarget +import com.bumptech.glide.request.transition.Transition +import com.bumptech.glide.request.RequestOptions object ImageLoader { @@ -11,17 +29,137 @@ object ImageLoader { val context = textWithMarkup.context // Only if AppPreferences.displayImages(context) is true - // loadImages(textWithMarkup.text as Spannable) + // Setup image visualization inside the note + if ( AppPreferences.displayInlineImages(context) + // Storage permission has been granted + && AppPermissions.isGranted(context, AppPermissions.Usage.EXTERNAL_FILES_ACCESS)) { + // Load the associated image for each FileLinkSpan + SpanUtils.forEachSpan(textWithMarkup.text as Spannable, FileLinkSpan::class.java) { span -> + loadImage(textWithMarkup, span) + } + } } - private fun loadImages(text: Spannable) { - SpanUtils.forEachSpan(text, FileLinkSpan::class.java) { span -> - loadImage(span) + private fun loadImage(textWithMarkup: TextViewWithMarkup, span: FileLinkSpan) { + val path = span.path + + if (hasSupportedExtension(path)) { + val text = textWithMarkup.text as Spannable + // Get the current context + val context = App.getAppContext() + + // Get the file + val file = File(Environment.getExternalStorageDirectory(), path) + + if(file.exists()) { + // Get the Uri + val contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) + + // Get image sizes to reduce their memory footprint by rescaling + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().toString() + "/" + path, options) + + val size = fitDrawable(textWithMarkup, options.outWidth, options.outHeight) + + // Setup a placeholder + val drawable = ColorDrawable(Color.TRANSPARENT) + drawable.setBounds(0, 0, size.first, size.second) + + Glide.with(context) + .asBitmap() + // Use a placeholder + .apply(RequestOptions().placeholder(drawable)) + // Override the bitmap size, mainly used for big images + // as it's useless to display more pixel that the pixel density allows + .apply(RequestOptions().override(size.first, size.second)) + .load(contentUri) + .into(object : SimpleTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + val bd = BitmapDrawable(App.getAppContext().resources, resource) + fitDrawable(textWithMarkup, bd) + text.setSpan(ImageSpan(bd), text.getSpanStart(span), text.getSpanEnd(span), text.getSpanFlags(span)) + } + }) + } } } - private fun loadImage(span: FileLinkSpan) { - val path = span.path + fun hasSupportedExtension(path: String): Boolean { + var ret = false + var index: Int + var s = "" + + // Find the last slash in the path + // to avoid case of a point in the path and a file without extension + index = path.lastIndexOf("/") + + // If we found the last slash, extract the file name + if (index != -1) { + s = path.substring(index + 1) + } + + // Extract the extension + index = s.lastIndexOf(".") + + // If we found an extension, extract it and test it + if (index != -1) { + s = s.substring(index + 1).toLowerCase() + + if (s == "jpg" || s == "jpeg" || s == "gif" + || s == "png" || s == "bmp" || s == "webp") { + ret = true + } + } + + return ret + } + + fun fitDrawable(view: View, width: Int, height: Int): Pair { + var newWidth = width + var newHeight = height + + // Get the display metrics to be able to rescale the image if needed + val metrics = view.context.resources.displayMetrics + + // Use either a fixed size or a scaled size according to user preferences + var fixedSize = -1 + if (!AppPreferences.enableImageScaling(view.context)) { + fixedSize = AppPreferences.setImageFixedWidth(view.context) + } + + // Before image loading view.width might not be initialized + // So we take a default maximum value that will be reduced + var maxWidth = view.width + if(maxWidth == 0) + { + maxWidth = metrics.widthPixels + } + + // If we are using a fixedSize + if (fixedSize > 0) { + // Keep aspect ratio when using fixed size + val ratio = height.toFloat() / width.toFloat() + newWidth = fixedSize + newHeight = (fixedSize * ratio).toInt() + // Otherwise if we are using rescaling and the image is wider that the max width + } else if (width > maxWidth) { + //Compute image ratio + val ratio = height.toFloat() / width.toFloat() + // Ensure that the images have a minimum size + val width = Math.max(maxWidth, 256).toFloat() + + newWidth = width.toInt() + newHeight = (width * ratio).toInt() + } + + return Pair(newWidth, newHeight) + } + fun fitDrawable(view: View, drawable: BitmapDrawable) { + // Compute the new size of the drawable + val newSize = fitDrawable(view, drawable.bitmap.width, drawable.bitmap.height) + // Set the bounds to match the new size + drawable.setBounds(0, 0, newSize.first, newSize.second) } } \ No newline at end of file diff --git a/app/src/main/res/values/prefs_keys.xml b/app/src/main/res/values/prefs_keys.xml index 9ab38ff07..41a4986ac 100644 --- a/app/src/main/res/values/prefs_keys.xml +++ b/app/src/main/res/values/prefs_keys.xml @@ -354,6 +354,15 @@ pref_key_set_last_repeat_on_time_shift true + pref_key_display_inline_images + true + + pref_key_enable_image_scaling + true + + pref_key_set_image_fixed_width + 128 + pref_key_separate_notes_with_new_line @string/pref_value_separate_notes_with_new_line_multi_line_notes_only diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 53df06648..dd1b077b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -538,4 +538,11 @@ No application found to open this file + Display inline images + Display inline images in notes + + Enable image scaling + Use predefined sizes or enable image scaling + + Set inlined image fixed width (in pixels) diff --git a/app/src/main/res/xml/prefs_screen_notebooks.xml b/app/src/main/res/xml/prefs_screen_notebooks.xml index 22dfebc78..798918e8d 100644 --- a/app/src/main/res/xml/prefs_screen_notebooks.xml +++ b/app/src/main/res/xml/prefs_screen_notebooks.xml @@ -2,6 +2,7 @@ @@ -148,6 +149,27 @@ android:summary="@string/set_last_repeat_on_time_shift_summary" android:defaultValue="@bool/pref_default_set_last_repeat_on_time_shift"/> + + + + +