chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:31 +08:00
commit f06de36198
4585 changed files with 588418 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+28
View File
@@ -0,0 +1,28 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
plugins {
alias(libs.plugins.image.toolbox.library)
alias(libs.plugins.image.toolbox.compose)
}
android.namespace = "com.t8rin.image"
dependencies {
implementation(projects.lib.gesture)
implementation(libs.androidx.palette.ktx)
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,110 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.image
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntRect
/**
* Receiver scope being used by the children parameter of [ImageWithConstraints]
*/
@Stable
interface ImageScope {
/**
* The constraints given by the parent layout in pixels.
*
* Use [minWidth], [maxWidth], [minHeight] or [maxHeight] if you need value in [Dp].
*/
val constraints: Constraints
/**
* The minimum width in [Dp].
*
* @see constraints for the values in pixels.
*/
val minWidth: Dp
/**
* The maximum width in [Dp].
*
* @see constraints for the values in pixels.
*/
val maxWidth: Dp
/**
* The minimum height in [Dp].
*
* @see constraints for the values in pixels.
*/
val minHeight: Dp
/**
* The maximum height in [Dp].
*
* @see constraints for the values in pixels.
*/
val maxHeight: Dp
/**
* Width of area inside BoxWithConstraints that is scaled based on [ContentScale]
* This is width of the [Canvas] draw [ImageBitmap]
*/
val imageWidth: Dp
/**
* Height of area inside BoxWithConstraints that is scaled based on [ContentScale]
* This is height of the [Canvas] draw [ImageBitmap]
*/
val imageHeight: Dp
/**
* [IntRect] that covers boundaries of [ImageBitmap]
*/
val rect: IntRect
}
internal data class ImageScopeImpl(
private val density: Density,
override val constraints: Constraints,
override val imageWidth: Dp,
override val imageHeight: Dp,
override val rect: IntRect,
) : ImageScope {
override val minWidth: Dp get() = with(density) { constraints.minWidth.toDp() }
override val maxWidth: Dp
get() = with(density) {
if (constraints.hasBoundedWidth) constraints.maxWidth.toDp() else Dp.Infinity
}
override val minHeight: Dp get() = with(density) { constraints.minHeight.toDp() }
override val maxHeight: Dp
get() = with(density) {
if (constraints.hasBoundedHeight) constraints.maxHeight.toDp() else Dp.Infinity
}
}
@@ -0,0 +1,250 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.image
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import com.t8rin.image.util.getParentSize
import com.t8rin.image.util.getScaledBitmapRect
/**
* A composable that lays out and draws a given [ImageBitmap]. This will attempt to
* size the composable according to the [ImageBitmap]'s given width and height. However, an
* optional [Modifier] parameter can be provided to adjust sizing or draw additional content (ex.
* background). Any unspecified dimension will leverage the [ImageBitmap]'s size as a minimum
* constraint.
*
* [ImageScope] returns constraints, width and height of the drawing area based on [contentScale]
* and rectangle of [imageBitmap] drawn. When a bitmap is displayed scaled to fit area of Composable
* space used for drawing image is represented with [ImageScope.imageWidth] and
* [ImageScope.imageHeight].
*
* When we display a bitmap 1000x1000px with [ContentScale.Crop] if it's cropped to 500x500px
* [ImageScope.rect] returns `IntRect(250,250,750,750)`.
*
* @param alignment determines where image will be aligned inside [BoxWithConstraints]
* This is observable when bitmap image/width ratio differs from [Canvas] that draws [ImageBitmap]
* @param contentDescription text used by accessibility services to describe what this image
* represents. This should always be provided unless this image is used for decorative purposes,
* and does not represent a meaningful action that a user can take. This text should be
* localized, such as by using [androidx.compose.ui.res.stringResource] or similar
* @param contentScale how image should be scaled inside Canvas to match parent dimensions.
* [ContentScale.Fit] for instance maintains src ratio and scales image to fit inside the parent.
* @param alpha Opacity to be applied to [imageBitmap] from 0.0f to 1.0f representing
* fully transparent to fully opaque respectively
* @param colorFilter ColorFilter to apply to the [imageBitmap] when drawn into the destination
* @param filterQuality Sampling algorithm applied to the [imageBitmap] when it is scaled and drawn
* into the destination. The default is [FilterQuality.Low] which scales using a bilinear
* sampling algorithm
* @param content is a Composable that can be matched at exact position where [imageBitmap] is drawn.
* This is useful for drawing thumbs, cropping or another layout that should match position
* with the image that is scaled is drawn
* @param drawImage flag to draw image on canvas. Some Composables might only require
* the calculation and rectangle bounds of image after scaling but not drawing.
* Composables like image cropper that scales or
* rotates image. Drawing here again have 2 drawings overlap each other.
*/
@Composable
fun ImageWithConstraints(
modifier: Modifier = Modifier,
imageBitmap: ImageBitmap,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
contentDescription: String? = null,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
drawImage: Boolean = true,
content: @Composable ImageScope.() -> Unit = {}
) {
val semantics = if (contentDescription != null) {
Modifier.semantics {
this.contentDescription = contentDescription
this.role = Role.Image
}
} else {
Modifier
}
BoxWithConstraints(
modifier = modifier
.then(semantics),
contentAlignment = alignment,
) {
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
val (boxWidth: Int, boxHeight: Int) = getParentSize(bitmapWidth, bitmapHeight)
// Src is Bitmap, Dst is the container(Image) that Bitmap will be displayed
val srcSize = Size(bitmapWidth.toFloat(), bitmapHeight.toFloat())
val dstSize = Size(boxWidth.toFloat(), boxHeight.toFloat())
val scaleFactor = contentScale.computeScaleFactor(srcSize, dstSize)
// Image is the container for bitmap that is located inside Box
// image bounds can be smaller or bigger than its parent based on how it's scaled
val imageWidth = bitmapWidth * scaleFactor.scaleX
val imageHeight = bitmapHeight * scaleFactor.scaleY
val bitmapRect = getScaledBitmapRect(
boxWidth = boxWidth,
boxHeight = boxHeight,
imageWidth = imageWidth,
imageHeight = imageHeight,
bitmapWidth = bitmapWidth,
bitmapHeight = bitmapHeight
)
ImageLayout(
constraints = constraints,
imageBitmap = imageBitmap,
bitmapRect = bitmapRect,
imageWidth = imageWidth,
imageHeight = imageHeight,
boxWidth = boxWidth,
boxHeight = boxHeight,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality,
drawImage = drawImage,
content = content
)
}
}
@Composable
private fun ImageLayout(
constraints: Constraints,
imageBitmap: ImageBitmap,
bitmapRect: IntRect,
imageWidth: Float,
imageHeight: Float,
boxWidth: Int,
boxHeight: Int,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
drawImage: Boolean = true,
content: @Composable ImageScope.() -> Unit
) {
val density = LocalDensity.current
// Dimensions of canvas that will draw this Bitmap
val canvasWidthInDp: Dp
val canvasHeightInDp: Dp
with(density) {
canvasWidthInDp = imageWidth.coerceAtMost(boxWidth.toFloat()).toDp()
canvasHeightInDp = imageHeight.coerceAtMost(boxHeight.toFloat()).toDp()
}
// Send rectangle of Bitmap drawn to Canvas as bitmapRect, content scale modes like
// crop might crop image from center so Rect can be such as IntRect(250,250,500,500)
// canvasWidthInDp, and canvasHeightInDp are Canvas dimensions coerced to Box size
// that covers Canvas
val imageScopeImpl = ImageScopeImpl(
density = density,
constraints = constraints,
imageWidth = canvasWidthInDp,
imageHeight = canvasHeightInDp,
rect = bitmapRect
)
// width and height params for translating draw position if scaled Image dimensions are
// bigger than Canvas dimensions
if (drawImage) {
ImageImpl(
modifier = Modifier.size(canvasWidthInDp, canvasHeightInDp),
imageBitmap = imageBitmap,
alpha = alpha,
width = imageWidth.toInt(),
height = imageHeight.toInt(),
colorFilter = colorFilter,
filterQuality = filterQuality
)
}
imageScopeImpl.content()
}
@Composable
private fun ImageImpl(
modifier: Modifier,
imageBitmap: ImageBitmap,
width: Int,
height: Int,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
) {
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
Canvas(modifier = modifier.clipToBounds()) {
val canvasWidth = size.width.toInt()
val canvasHeight = size.height.toInt()
// Translate to left or down when Image size is bigger than this canvas.
// ImageSize is bigger when scale modes like Crop is used which enlarges image
// For instance 1000x1000 image can be 1000x2000 for a Canvas with 1000x1000
// so top is translated -500 to draw center of ImageBitmap
translate(
top = (-height + canvasHeight) / 2f,
left = (-width + canvasWidth) / 2f,
) {
drawImage(
imageBitmap,
srcSize = IntSize(bitmapWidth, bitmapHeight),
dstSize = IntSize(width, height),
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
}
}
}
@@ -0,0 +1,105 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.image.util
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
/**
* Get Rectangle of [ImageBitmap] with [bitmapWidth] and [bitmapHeight] that is drawn inside
* Canvas with [imageWidth] and [imageHeight]. [boxWidth] and [boxHeight] belong
* to [BoxWithConstraints] that contains Canvas.
* @param boxWidth width of the parent container
* @param boxHeight height of the parent container
* @param imageWidth width of the [Canvas] that draws [ImageBitmap]
* @param imageHeight height of the [Canvas] that draws [ImageBitmap]
* @param bitmapWidth intrinsic width of the [ImageBitmap]
* @param bitmapHeight intrinsic height of the [ImageBitmap]
* @return [IntRect] that covers [ImageBitmap] bounds. When image [ContentScale] is crop
* this rectangle might return smaller rectangle than actual [ImageBitmap] and left or top
* of the rectangle might be bigger than zero.
*/
internal fun getScaledBitmapRect(
boxWidth: Int,
boxHeight: Int,
imageWidth: Float,
imageHeight: Float,
bitmapWidth: Int,
bitmapHeight: Int
): IntRect {
// Get scale of box to width of the image
// We need a rect that contains Bitmap bounds to pass if any child requires it
// For a image with 100x100 px with 300x400 px container and image with crop 400x400px
// So we need to pass top left as 0,50 and size
val scaledBitmapX = boxWidth / imageWidth
val scaledBitmapY = boxHeight / imageHeight
val topLeft = IntOffset(
x = (bitmapWidth * (imageWidth - boxWidth) / imageWidth / 2)
.coerceAtLeast(0f).toInt(),
y = (bitmapHeight * (imageHeight - boxHeight) / imageHeight / 2)
.coerceAtLeast(0f).toInt()
)
val size = IntSize(
width = (bitmapWidth * scaledBitmapX).toInt().coerceAtMost(bitmapWidth),
height = (bitmapHeight * scaledBitmapY).toInt().coerceAtMost(bitmapHeight)
)
return IntRect(offset = topLeft, size = size)
}
/**
* Get [IntSize] of the parent or container that contains [Canvas] that draws [ImageBitmap]
* @param bitmapWidth intrinsic width of the [ImageBitmap]
* @param bitmapHeight intrinsic height of the [ImageBitmap]
* @return size of parent Composable. When Modifier is assigned with fixed or finite size
* they are used, but when any dimension is set to infinity intrinsic dimensions of
* [ImageBitmap] are returned
*/
internal fun BoxWithConstraintsScope.getParentSize(
bitmapWidth: Int,
bitmapHeight: Int
): IntSize {
// Check if Composable has fixed size dimensions
val hasBoundedDimens = constraints.hasBoundedWidth && constraints.hasBoundedHeight
// Check if Composable has infinite dimensions
val hasFixedDimens = constraints.hasFixedWidth && constraints.hasFixedHeight
// Box is the parent(BoxWithConstraints) that contains Canvas under the hood
// Canvas aspect ratio or size might not match parent but it's upper bounds are
// what are passed from parent. Canvas cannot be bigger or taller than BoxWithConstraints
val boxWidth: Int = if (hasBoundedDimens || hasFixedDimens) {
constraints.maxWidth
} else {
constraints.minWidth.coerceAtLeast(bitmapWidth)
}
val boxHeight: Int = if (hasBoundedDimens || hasFixedDimens) {
constraints.maxHeight
} else {
constraints.minHeight.coerceAtLeast(bitmapHeight)
}
return IntSize(boxWidth, boxHeight)
}