Skip to content

Commit

Permalink
feat: use webview to render images
Browse files Browse the repository at this point in the history
  • Loading branch information
criticalAY authored and mikehardy committed Nov 20, 2024
1 parent b44e59b commit 2109254
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,25 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Base64
import android.view.View
import android.widget.ImageView
import android.webkit.WebView
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.IntentCompat
import androidx.core.os.BundleCompat
import androidx.lifecycle.lifecycleScope
import com.canhub.cropper.CropException
import com.google.android.material.button.MaterialButton
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.CrashReportService
import com.ichi2.anki.DrawingActivity
import com.ichi2.anki.R
Expand All @@ -63,18 +67,20 @@ import com.ichi2.utils.show
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.text.DecimalFormat

private const val SVG_IMAGE = "image/svg+xml"

@NeedsTest("Ensure correct option is executed i.e. gallery or camera")
class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_image) {
override val title: String
get() = resources.getString(R.string.multimedia_editor_popup_image)

private lateinit var imagePreview: ImageView
private lateinit var imageFileSize: TextView

private lateinit var selectedImageOptions: ImageOptions
Expand All @@ -97,7 +103,6 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
}

Activity.RESULT_OK -> {
view?.findViewById<TextView>(R.id.no_image_textview)?.visibility = View.GONE
val data = result.data
if (data == null) {
Timber.w("handleSelectImageIntent() no intent provided")
Expand Down Expand Up @@ -129,7 +134,6 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
}

Activity.RESULT_OK -> {
view?.findViewById<TextView>(R.id.no_image_textview)?.visibility = View.GONE
val intent = result.data ?: return@registerForActivityResult
Timber.d("Intent not null, handling the result")
handleDrawingResult(intent)
Expand All @@ -155,7 +159,6 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_

isPictureTaken -> {
Timber.d("Image successfully captured")
view?.findViewById<TextView>(R.id.no_image_textview)?.visibility = View.GONE
handleTakePictureResult(viewModel.currentMultimediaPath.value)
}

Expand Down Expand Up @@ -187,7 +190,7 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
updateAndDisplayImageSize(cropResultData.uriPath)
viewModel.updateCurrentMultimediaPath(cropResultData.uriPath)
viewModel.updateCurrentMultimediaUri(cropResultData.uriContent)
imagePreview.setImageURI(cropResultData.uriContent)
previewImage(cropResultData.uriContent)
}
}
else -> {
Expand Down Expand Up @@ -266,7 +269,6 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupMenu(multimediaMenu)
imagePreview = view.findViewById(R.id.image_preview)
imageFileSize = view.findViewById(R.id.image_size_textview)

handleImageUri()
Expand All @@ -275,10 +277,7 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_

private fun handleImageUri() {
fun processExternalImage(uri: Uri): Uri? = internalizeUri(uri)?.let { Uri.fromFile(it) }

if (imageUri != null) {
view?.findViewById<TextView>(R.id.no_image_textview)?.visibility = View.GONE

val internalUri = imageUri?.let { processExternalImage(it) }
handleSelectImageIntent(internalUri)
} else {
Expand Down Expand Up @@ -394,7 +393,7 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
val drawImagePath = internalizedPick.absolutePath
Timber.i("handleDrawingResult() Decoded image: '%s'", drawImagePath)

imagePreview.setImageURI(imageUri)
previewImage(imageUri)
viewModel.updateCurrentMultimediaPath(drawImagePath)
viewModel.updateCurrentMultimediaUri(imageUri)
updateAndDisplayImageSize(drawImagePath)
Expand All @@ -410,7 +409,7 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
val imageFile = File(imagePath)
viewModel.updateCurrentMultimediaPath(imagePath)
viewModel.updateCurrentMultimediaUri(getUriForFile(imageFile))
imagePreview.setImageURI(getUriForFile(imageFile))
viewModel.currentMultimediaUri.value?.let { previewImage(it) }
updateAndDisplayImageSize(imagePath)

showCropDialog(getString(R.string.crop_image))
Expand Down Expand Up @@ -461,53 +460,146 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
}
}

/**
* Handles the selected image from the intent by previewing it in a WebView and internalizing the URI.
* Updates the ViewModel with the selected image's path and size.
*
* @param imageUri The URI of the selected image.
*/
private fun handleSelectImageIntent(imageUri: Uri?) {
val mimeType = imageUri?.let { context?.contentResolver?.getType(it) }
if (mimeType == "image/svg+xml") {
Timber.i("Selected image is an SVG.")
view?.findViewById<TextView>(R.id.no_image_textview)?.apply {
text = resources.getString(R.string.multimedia_editor_svg_preview)
visibility = View.VISIBLE
}
} else {
// reset the no preview text
view?.findViewById<TextView>(R.id.no_image_textview)?.apply {
text = null
visibility = View.GONE
}
}

if (imageUri == null) {
Timber.w("handleSelectImageIntent() selectedImage was null")
showSomethingWentWrong()
return
}

previewImage(imageUri)

// Handle internalizing the URI (optional)
val internalizedPick = internalizeUri(imageUri)
if (internalizedPick == null) {
Timber.w(
"handleSelectImageIntent() unable to internalize image from Uri %s",
imageUri
)
Timber.w("handleSelectImageIntent() unable to internalize image from Uri %s", imageUri)
showSomethingWentWrong()
return
}

// Update ViewModel with image data
val imagePath = internalizedPick.absolutePath

viewModel.updateCurrentMultimediaUri(imageUri)
viewModel.updateCurrentMultimediaPath(imagePath)
imagePreview.setImageURI(imageUri)
viewModel.selectedMediaFileSize = internalizedPick.length()

// Optionally update and display the image size
updateAndDisplayImageSize(imagePath)
}

/**
* Previews the selected image in a WebView.
* Handles both SVG and non-SVG images (e.g., JPG, PNG) and displays the image based on its MIME type.
*
* @param imageUri The URI of the selected image.
*/
private fun previewImage(imageUri: Uri) {
val mimeType = context?.contentResolver?.getType(imageUri)

// Get the WebView and set it visible
view?.findViewById<WebView>(R.id.multimedia_webview)?.apply {
visibility = View.VISIBLE

// Load image based on its MIME type
// SVGs require special handling due to their XML-based format and rendering complexities.
// Raster images (e.g., JPG, PNG) can be rendered directly using an <img> tag in HTML.
when (mimeType) {
SVG_IMAGE -> loadSvgImage(imageUri)
else -> loadImage(imageUri)
}
}
}

/**
* Loads and previews an SVG image in the WebView.
*
* @param imageUri The URI of the SVG image.
*/
private fun WebView.loadSvgImage(imageUri: Uri) {
val svgData = loadSvgFromUri(imageUri)
if (svgData != null) {
Timber.i("Selected image is an SVG.")

loadDataWithBaseURL(null, svgData, SVG_IMAGE, "UTF-8", null)
} else {
Timber.w("Failed to load SVG from URI")
showErrorInWebView()
}
}

/**
* Loads and previews a non-SVG image (e.g., JPG, PNG) in the WebView.
*
* @param imageUri The URI of the non-SVG image.
*/
private fun WebView.loadImage(imageUri: Uri) {
val imagePath = imageUri.toString()
val htmlData = """
<html>
<body style="margin:0;padding:0;">
<img src="$imagePath" style="width:100%;height:auto;" />
</body>
</html>
""".trimIndent()

loadDataWithBaseURL(null, htmlData, "text/html", "UTF-8", null)
}

private fun drawableToBase64png(context: Context, drawableResId: Int): String {
val drawable = ContextCompat.getDrawable(context, drawableResId)
val bitmap = (drawable as BitmapDrawable).bitmap

return ByteArrayOutputStream().use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
val byteArray = outputStream.toByteArray()
Base64.encodeToString(byteArray, Base64.DEFAULT)
}
}

/** Shows an error image along with an error text **/
private fun showErrorInWebView() {
val base64Image = drawableToBase64png(requireContext(), R.drawable.ic_image_not_supported)

val errorHtml = """
<html>
<body style="text-align:center;">
<img src="data:image/png;base64,$base64Image" alt="${TR.notetypeErrorNoImageToShow()}"/>
</body>
</html>
""".trimIndent()

view?.findViewById<WebView>(R.id.multimedia_webview)?.loadDataWithBaseURL(null, errorHtml, "text/html", "UTF-8", null)
}

private fun requestCrop() {
val imageUri = viewModel.currentMultimediaUri.value ?: return
val intent = com.ichi2.imagecropper.ImageCropperLauncher.ImageUri(imageUri).getIntent(requireContext())
imageCropperLauncher.launch(intent)
}

/**
* Loads an SVG file from the given URI and returns its content as a string.
*
* @param uri The URI of the SVG file to be loaded.
* @return The content of the SVG file as a string, or null if an error occurs.
*/
private fun loadSvgFromUri(uri: Uri): String? {
return try {
context?.contentResolver?.openInputStream(uri)?.use { inputStream ->
inputStream.bufferedReader().readText()
}
} catch (e: Exception) {
Timber.w(e, "Error reading SVG from URI")
null
}
}

private fun handleCropResultError(error: Exception) {
// cropImage can give us more information. Not sure it is actionable so for now just log it.
Timber.w(error, "cropImage threw an error")
Expand Down Expand Up @@ -563,7 +655,7 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
// TODO: see if we can use one value to the viewModel
viewModel.updateCurrentMultimediaUri(imageUri)
viewModel.updateCurrentMultimediaPath(outFile.path)
imagePreview.setImageURI(imageUri)
previewImage(imageUri)
viewModel.selectedMediaFileSize = outFile.length()
updateAndDisplayImageSize(outFile.path)

Expand Down
14 changes: 2 additions & 12 deletions AnkiDroid/src/main/res/layout/fragment_multimedia_image.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,8 @@
app:layout_constraintBottom_toTopOf="@id/image_size_textview"
style="@style/CardView.PreviewerStyle" >

<TextView
android:id="@+id/no_image_textview"
android:paddingBottom="30dp"
android:gravity="center"
android:layout_gravity="bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<ImageView
android:src="@drawable/ic_image_not_supported"
android:id="@+id/image_preview"
android:scaleType="fitCenter"
<WebView
android:id="@+id/multimedia_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

Expand Down
1 change: 0 additions & 1 deletion AnkiDroid/src/main/res/values/16-multimedia-editor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@

<!-- not a good message, when an unexpected error happens -->
<string name="multimedia_editor_something_wrong">Something went wrong</string>
<string name="multimedia_editor_svg_preview">SVGs are not available for preview</string>
<string name="multimedia_editor_png_paste_error">Error converting clipboard image to png: %s</string>
<string name="multimedia_editor_attach_mm_content" comment="used for screen readers. Not visible to the user">Attach multimedia content to the %1$s field</string>

Expand Down

0 comments on commit 2109254

Please sign in to comment.