Skip to content

Commit

Permalink
Auto-crop images when importing
Browse files Browse the repository at this point in the history
  • Loading branch information
fibelatti committed Jan 13, 2024
1 parent 93cc80e commit 724ae0c
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -215,28 +215,15 @@ private fun PhotoWidgetConfigureContent(
)
}

val allPhotosCropped = remember(photos) { photos.all { it.isCropped } }

FilledTonalButton(
onClick = onAddToHomeClick,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, top = 32.dp, end = 16.dp),
enabled = allPhotosCropped,
) {
Text(text = stringResource(id = R.string.photo_widget_configure_add_to_home))
}

AnimatedVisibility(visible = !allPhotosCropped) {
Text(
text = stringResource(id = R.string.photo_widget_configure_cropping_required),
modifier = Modifier.padding(start = 72.dp, top = 8.dp, end = 72.dp),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall,
)
}

Spacer(modifier = Modifier.size(32.dp))
}
}
Expand Down Expand Up @@ -358,17 +345,13 @@ private fun PhotoPicker(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
val croppedPhotos = photos.count { it.isCropped }

Text(
text = stringResource(
id = if (photos.isEmpty()) {
R.string.photo_widget_configure_select_photo
} else {
R.string.photo_widget_configure_selected_photos
},
croppedPhotos,
photos.size,
),
modifier = Modifier.padding(horizontal = 16.dp),
style = MaterialTheme.typography.titleMedium,
Expand Down Expand Up @@ -426,24 +409,6 @@ private fun PhotoPicker(
role = Role.Image,
onClick = { onPhotoClick(photo) },
),
badge = {
if (!photo.isCropped) {
Image(
painter = painterResource(id = R.drawable.ic_crop),
contentDescription = "",
modifier = Modifier
.padding(all = 8.dp)
.background(
color = MaterialTheme.colorScheme.errorContainer,
shape = CircleShape,
)
.padding(all = 4.dp)
.size(size = 12.dp)
.align(Alignment.BottomEnd),
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onErrorContainer),
)
}
},
)
}
}
Expand Down Expand Up @@ -566,11 +531,11 @@ fun ShapedPhoto(
height = photoBitmap.height,
)

photoBitmap.withPolygonalShape(shape).asImageBitmap()
photoBitmap.withPolygonalShape(roundedPolygon = shape).asImageBitmap()
}
} else {
remember(photo, aspectRatio) {
photoBitmap.withRoundedCorners().asImageBitmap()
photoBitmap.withRoundedCorners(desiredAspectRatio = aspectRatio).asImageBitmap()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,13 @@ class PhotoWidgetConfigureViewModel @Inject constructor(
current.copy(
photos = current.photos.map { photo ->
if (photo.path == path) {
photo.copy(
isCropped = true,
timestamp = System.currentTimeMillis(),
)
photo.copy(timestamp = System.currentTimeMillis())
} else {
photo
}
},
selectedPhoto = if (current.selectedPhoto?.path == path) {
current.selectedPhoto.copy(
isCropped = true,
timestamp = System.currentTimeMillis(),
)
current.selectedPhoto.copy(timestamp = System.currentTimeMillis())
} else {
current.selectedPhoto
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ package com.fibelatti.photowidget.model
data class LocalPhoto(
val name: String,
val path: String,
val isCropped: Boolean,
val timestamp: Long = System.currentTimeMillis(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.fibelatti.photowidget.model

import androidx.annotation.StringRes
import com.fibelatti.photowidget.R
import kotlin.math.max
import kotlin.math.min

enum class PhotoWidgetAspectRatio(
val x: Float,
Expand All @@ -26,5 +28,9 @@ enum class PhotoWidgetAspectRatio(
),
;

val aspectRatio: Float get(): Float = x / y
val aspectRatio: Float
get() = x / y

val scale: Float
get() = min(x, y) / max(x, y)
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ object PhotoWidgetShapeBuilder {
}
}

private fun calculateMatrix(bounds: RectF, width: Int, height: Int): Matrix {
fun calculateMatrix(bounds: RectF, width: Int, height: Int): Matrix {
val scale = calculateScale(bounds = bounds, width = width, height = height)
val scaledLeft = scale * bounds.left
val scaledTop = scale * bounds.top
Expand Down
79 changes: 71 additions & 8 deletions app/src/main/java/com/fibelatti/photowidget/platform/BitmapKtx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,103 @@ package com.fibelatti.photowidget.platform

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import androidx.core.graphics.toRectF
import androidx.graphics.shapes.RoundedPolygon
import androidx.graphics.shapes.drawPolygon
import com.fibelatti.photowidget.model.PhotoWidgetAspectRatio
import com.fibelatti.photowidget.model.PhotoWidgetShapeBuilder
import kotlin.math.min
import kotlin.math.roundToInt

fun Bitmap.withRoundedCorners(
desiredAspectRatio: PhotoWidgetAspectRatio,
radius: Float = 64f,
): Bitmap = withTransformation { canvas, rect, paint ->
): Bitmap = withTransformation(desiredAspectRatio = desiredAspectRatio) { canvas, rect, paint ->
canvas.drawRoundRect(rect.toRectF(), radius, radius, paint)
}

fun Bitmap.withPolygonalShape(
roundedPolygon: RoundedPolygon,
): Bitmap = withTransformation { canvas, _, paint ->
canvas.drawPolygon(polygon = roundedPolygon, paint = paint)
): Bitmap = withTransformation(desiredAspectRatio = PhotoWidgetAspectRatio.SQUARE) { canvas, rect, paint ->
canvas.drawPolygon(
polygon = roundedPolygon.also {
it.transform(
matrix = PhotoWidgetShapeBuilder.calculateMatrix(
bounds = rect.toRectF(),
width = rect.width(),
height = rect.height(),
),
)
},
paint = paint,
)
}

private inline fun Bitmap.withTransformation(body: (Canvas, Rect, Paint) -> Unit): Bitmap {
val output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
private inline fun Bitmap.withTransformation(
desiredAspectRatio: PhotoWidgetAspectRatio,
body: (Canvas, Rect, Paint) -> Unit,
): Bitmap {
val source = when (desiredAspectRatio) {
PhotoWidgetAspectRatio.SQUARE -> {
val size = min(height, width)

val top = (height - size) / 2
val left = (width - size) / 2

Rect(left, top, left + size, top + size)
}

PhotoWidgetAspectRatio.TALL -> {
val baseWidth = (height * PhotoWidgetAspectRatio.TALL.scale)

val scaledWidth = baseWidth.roundToInt().coerceAtMost(width)
val scaledHeight = if (baseWidth > width) {
(width / baseWidth.roundToInt()) * height
} else {
height
}

val top = (height - scaledHeight) / 2
val left = (width - scaledWidth) / 2

Rect(left, top, left + scaledWidth, top + scaledHeight)
}

PhotoWidgetAspectRatio.WIDE -> {
val baseHeight = (width * PhotoWidgetAspectRatio.WIDE.scale)

val scaledHeight = baseHeight.roundToInt().coerceAtMost(height)
val scaledWidth = if (baseHeight > height) {
(height / baseHeight.roundToInt()) * width
} else {
width
}

val top = (height - scaledHeight) / 2
val left = (width - scaledWidth) / 2

Rect(left, top, left + scaledWidth, top + scaledHeight)
}
}
val destination = Rect(0, 0, source.width(), source.height())

val output = Bitmap.createBitmap(source.width(), source.height(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val paint = Paint().apply {
isAntiAlias = true
}
val rect = Rect(0, 0, width, height)

canvas.drawARGB(0, 0, 0, 0)

body(canvas, rect, paint)
body(canvas, if (PhotoWidgetAspectRatio.SQUARE == desiredAspectRatio) source else destination, paint)

paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
canvas.drawBitmap(this, rect, rect, paint)
canvas.drawBitmap(this, source, destination, paint)

return output
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ class PhotoWidgetProvider : AppWidgetProvider() {
height = bitmap.height,
)

bitmap.withPolygonalShape(shape)
bitmap.withPolygonalShape(roundedPolygon = shape)
} else {
bitmap.withRoundedCorners()
bitmap.withRoundedCorners(desiredAspectRatio = aspectRatio)
}

return RemoteViews(context.packageName, R.layout.photo_widget).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ class PhotoWidgetStorage @Inject constructor(@ApplicationContext context: Contex
return@withContext LocalPhoto(
name = newPhotoName,
path = croppedPhoto.path,
isCropped = false,
)
}

Expand All @@ -100,7 +99,6 @@ class PhotoWidgetStorage @Inject constructor(@ApplicationContext context: Contex
LocalPhoto(
name = file,
path = "$dir/$file",
isCropped = true,
)
}
}
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/res/values-pt/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<string name="photo_widget_configure_navigate_back_warning">As alterações serão perdidas, voltar mesmo assim?</string>

<string name="photo_widget_configure_select_photo">Escolha uma foto</string>
<string name="photo_widget_configure_selected_photos">Fotos escolhidas (%1$d/%2$d recortadas)</string>
<string name="photo_widget_configure_selected_photos">Fotos escolhidas</string>

<string name="photo_widget_configure_menu_crop">Recortar</string>
<string name="photo_widget_configure_menu_remove">Remover</string>
Expand All @@ -55,7 +55,6 @@

<string name="photo_widget_configure_applied_shape">Forma</string>
<string name="photo_widget_configure_add_to_home">Adicionar à tela inicial</string>
<string name="photo_widget_configure_cropping_required">Você deve recortar todas as fotos antes de adicionar este widget à sua tela inicial</string>

<!-- Common Actions -->
<string name="photo_widget_action_yes">Sim</string>
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<string name="photo_widget_configure_navigate_back_warning">Any changes will be lost, go back anyway?</string>

<string name="photo_widget_configure_select_photo">Select a photo</string>
<string name="photo_widget_configure_selected_photos">Selected photos (%1$d/%2$d cropped)</string>
<string name="photo_widget_configure_selected_photos">Selected photos</string>

<string name="photo_widget_configure_menu_crop">Crop</string>
<string name="photo_widget_configure_menu_remove">Remove</string>
Expand All @@ -55,7 +55,6 @@

<string name="photo_widget_configure_applied_shape">Applied shape</string>
<string name="photo_widget_configure_add_to_home">Add to home screen</string>
<string name="photo_widget_configure_cropping_required">You must crop all photos before adding this widget to your home screen</string>

<!-- Common Actions -->
<string name="photo_widget_action_yes">Yes</string>
Expand Down

0 comments on commit 724ae0c

Please sign in to comment.