Skip to content

Commit

Permalink
Merge pull request #72 from psuzn/sp/image-handler
Browse files Browse the repository at this point in the history
Image handling improvements
  • Loading branch information
mikepenz authored Sep 1, 2023
2 parents 870825a + 008d272 commit 2c4e8fc
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 23 deletions.
8 changes: 7 additions & 1 deletion app/src/main/java/com/mikepenz/markdown/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ fun MainLayout() {
Sometimes it will even contain images within the text
![Image](https://avatars.githubusercontent.com/u/1476232?v=4)
Images can also be of different sizes
![Image](https://placehold.co/1000x200/png)
![Image](https://placehold.co/200x1000/png)
After installing GPG Suite (or your preferred solution) first create a new key.
Supports reference links:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.mikepenz.markdown.utils

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.size.Size
Expand All @@ -16,3 +22,19 @@ internal actual fun imagePainter(url: String): Painter? {
.build()
)
}

@Composable
internal actual fun painterIntrinsicSize(painter: Painter): androidx.compose.ui.geometry.Size {

var size by remember(painter) { mutableStateOf(painter.intrinsicSize) }

if (painter is AsyncImagePainter) {
LaunchedEffect(painter.state) {
painter.state.painter?.let {
size = it.intrinsicSize
}
}
}

return size
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,11 @@ val LocalMarkdownTypography = compositionLocalOf<MarkdownTypography> {
*/
val LocalMarkdownPadding = staticCompositionLocalOf<MarkdownPadding> {
error("No local Padding")
}

/**
* Local [ImageTransformer] provider
*/
val LocalImageTransformer = staticCompositionLocalOf<ImageTransformer> {
error("No local ImageTransformer")
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ fun Markdown(
typography: MarkdownTypography = markdownTypography(),
padding: MarkdownPadding = markdownPadding(),
modifier: Modifier = Modifier.fillMaxSize(),
flavour: MarkdownFlavourDescriptor = GFMFlavourDescriptor()
flavour: MarkdownFlavourDescriptor = GFMFlavourDescriptor(),
imageTransformer: ImageTransformer = ImageTransformerImpl()
) {
CompositionLocalProvider(
LocalReferenceLinkHandler provides ReferenceLinkHandlerImpl(),
LocalMarkdownPadding provides padding,
LocalMarkdownColors provides colors,
LocalMarkdownTypography provides typography,
LocalImageTransformer provides imageTransformer
) {
Column(modifier) {
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ package com.mikepenz.markdown.compose.elements
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import com.mikepenz.markdown.compose.LocalImageTransformer
import com.mikepenz.markdown.utils.findChildOfTypeRecursive
import com.mikepenz.markdown.utils.imagePainter
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.getTextInNode

@Composable
internal fun MarkdownImage(content: String, node: ASTNode) {

val link = node.findChildOfTypeRecursive(MarkdownElementTypes.LINK_DESTINATION)?.getTextInNode(content)?.toString() ?: return
imagePainter(link)?.let {

LocalImageTransformer.current.transform(link)?.let { painter ->
Image(
painter = it,
painter = painter,
contentDescription = "Markdown Image", // TODO
contentScale = ContentScale.FillWidth,
alignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth()
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
package com.mikepenz.markdown.compose.elements

import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.sp
import com.mikepenz.markdown.compose.LocalImageTransformer
import com.mikepenz.markdown.compose.LocalMarkdownColors
import com.mikepenz.markdown.compose.LocalMarkdownTypography
import com.mikepenz.markdown.compose.LocalReferenceLinkHandler
import com.mikepenz.markdown.model.rememberMarkdownImageState
import com.mikepenz.markdown.utils.TAG_IMAGE_URL
import com.mikepenz.markdown.utils.TAG_URL
import com.mikepenz.markdown.utils.imagePainter


@Composable
internal fun MarkdownText(
Expand All @@ -39,6 +44,7 @@ internal fun MarkdownText(
val uriHandler = LocalUriHandler.current
val referenceLinkHandler = LocalReferenceLinkHandler.current
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
val imageState = rememberMarkdownImageState()

val hasUrl = content.getStringAnnotations(TAG_URL, 0, content.length).any()
val textModifier = if (hasUrl) modifier.pointerInput(Unit) {
Expand All @@ -52,25 +58,42 @@ internal fun MarkdownText(
}
} else modifier


Text(
text = content,
modifier = textModifier,
modifier = textModifier
.onPlaced {
imageState.setContainerSize(it.parentLayoutCoordinates!!.size)
}
.animateContentSize(),
style = style,
color = LocalMarkdownColors.current.text,
inlineContent = mapOf(
TAG_IMAGE_URL to InlineTextContent(
Placeholder(180.sp, 180.sp, PlaceholderVerticalAlign.Bottom) // TODO, identify flexible scaling!
) { link ->
imagePainter(link)?.let { painter ->
Image(
painter = painter,
contentDescription = "Image", // TODO
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
inlineContent = mapOf(TAG_IMAGE_URL to InlineTextContent(
Placeholder(
width = imageState.imageSize.width.sp,
height = imageState.imageSize.height.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.Bottom
)
) { link ->

val transformer = LocalImageTransformer.current

transformer.transform(link)?.let {

val intrinsicSize = transformer.intrinsicSize(it)

LaunchedEffect(intrinsicSize) {
imageState.setImageSize(intrinsicSize)
}

Image(
painter = it,
contentDescription = "Image",
alignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth()
)
}
),
}),
onTextLayout = { layoutResult.value = it }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mikepenz.markdown.model

import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.painter.Painter

interface ImageTransformer {

@Composable
fun transform(link: String): Painter?

@Composable
fun intrinsicSize(painter: Painter): Size {
return painter.intrinsicSize
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mikepenz.markdown.model

import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.painter.Painter
import com.mikepenz.markdown.utils.imagePainter
import com.mikepenz.markdown.utils.painterIntrinsicSize

internal class ImageTransformerImpl : ImageTransformer {

@Composable
override fun transform(link: String): Painter? {
return imagePainter(link)
}

@Composable
override fun intrinsicSize(painter: Painter): Size {
return painterIntrinsicSize(painter)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.mikepenz.markdown.model

import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.toSize

internal interface MarkdownImageState {
val imageSize: Size
fun setContainerSize(intSize: IntSize)
fun setImageSize(size: Size)
}

internal class MarkdownImageStateImpl(private val density: Density) : MarkdownImageState {

private var parentSize by mutableStateOf(Size.Unspecified)

private var intrinsicImageSize by mutableStateOf(Size.Unspecified)

override val imageSize by derivedStateOf {
with(density) {
if (parentSize.isUnspecified) {
Size(180f,180f)
} else if (intrinsicImageSize.isUnspecified) {
Size(parentSize.width.toSp().value,180f)
} else {
val width = minOf(intrinsicImageSize.width, parentSize.width)

val height = if (intrinsicImageSize.width < parentSize.width) {
intrinsicImageSize.height
} else {
(intrinsicImageSize.height * parentSize.width) / intrinsicImageSize.width
}
Size(width.toSp().value,height.toSp().value)
}
}
}

override fun setContainerSize(intSize: IntSize) {
parentSize = intSize.toSize()
}

override fun setImageSize(size: Size) {
intrinsicImageSize = size
}
}

@Composable
internal fun rememberMarkdownImageState(): MarkdownImageState {
val density = LocalDensity.current
return remember(density) {
MarkdownImageStateImpl(density)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.mikepenz.markdown.utils

import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.painter.Painter

@Composable
internal expect fun imagePainter(url: String): Painter?

@Composable
internal expect fun painterIntrinsicSize(painter: Painter): Size
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.mikepenz.markdown.utils

import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.painter.Painter

@Composable
internal actual fun imagePainter(url: String): Painter? {
return null
}

@Composable
internal actual fun painterIntrinsicSize(painter: Painter): Size {
return painter.intrinsicSize
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mikepenz.markdown.utils

import androidx.compose.runtime.*
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
Expand All @@ -17,6 +18,12 @@ internal actual fun imagePainter(url: String): Painter? {
return fetchImage(url)?.let { BitmapPainter(it) }
}


@Composable
internal actual fun painterIntrinsicSize(painter: Painter): Size{
return painter.intrinsicSize
}

@Composable
fun fetchImage(url: String): ImageBitmap? {
var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.mikepenz.markdown.utils

import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.painter.Painter

@Composable
internal actual fun imagePainter(url: String): Painter? {
return null
return null as Painter? // just `return null` crashes on native
}

@Composable
internal actual fun painterIntrinsicSize(painter: Painter): Size{
return painter.intrinsicSize
}

0 comments on commit 2c4e8fc

Please sign in to comment.