Map avatar crop frames using full original dimensions on both clients

This commit is contained in:
evgeny committed 2026-10-06 21:10:24 +02:00
1 parent a0fbf51be0
commit 3e1cc8af85
3 files changed
+36 -23

No files matched your search

@@ -36,6 +36,7 @@ fun AvatarCropDialog(uri: Uri, onCancel: () -> Unit, onConfirm: (File, AvatarCro
val context=LocalContext.current
var source by remember(uri) { mutableStateOf<File?>(null) }
var bitmap by remember(uri) { mutableStateOf<Bitmap?>(null) }
var dimensions by remember(uri) { mutableStateOf(IntSize.Zero) }
var failed by remember(uri) { mutableStateOf(false) }
var handedOver by remember(uri) { mutableStateOf(false) }
var zoom by rememberSaveable(uri.toString()) { mutableStateOf(1f) }
@@ -53,9 +54,9 @@ fun AvatarCropDialog(uri: Uri, onCancel: () -> Unit, onConfirm: (File, AvatarCro
val (w,h)=PhotoUtil.orientedDimensions(file.path)
require(w>0 && h>0 && w.toLong()*h<=16L*1024*1024) { "original exceeds 16 megapixels" }
var sample=1; while (maxOf(w,h)/sample>1600) sample*=2
requireNotNull(PhotoUtil.decodeOriented(file.path,sample)) { "cannot decode editor image" }
requireNotNull(PhotoUtil.decodeOriented(file.path,sample)) { "cannot decode editor image" } to IntSize(w,h)
}
source=copied; bitmap=image
source=copied; bitmap=image.first; dimensions=image.second
} catch (e: Exception) {
withContext(NonCancellable+Dispatchers.IO) {
copied?.let { if (!it.delete()) LogManager.addLog("WARN","Avatar","Cannot remove editor source=$it") }
@@ -77,25 +78,26 @@ fun AvatarCropDialog(uri: Uri, onCancel: () -> Unit, onConfirm: (File, AvatarCro
if (bitmap==null && !failed) LinearProgressIndicator(Modifier.fillMaxWidth())
if (failed) Text(stringResource(R.string.avatar_load_failed),color=MaterialTheme.colorScheme.error)
val image=bitmap
val photoW=dimensions.width; val photoH=dimensions.height
val side=minOf(bounds.width,bounds.height)*0.8f
val scale=if (image==null) 1f else maxOf(side/image.width,side/image.height)*zoom
val mx=if (image==null) 0f else ((image.width*scale-side)/2).coerceAtLeast(0f)
val my=if (image==null) 0f else ((image.height*scale-side)/2).coerceAtLeast(0f)
val scale=if (image==null) 1f else maxOf(side/photoW,side/photoH)*zoom
val mx=if (image==null) 0f else ((photoW*scale-side)/2).coerceAtLeast(0f)
val my=if (image==null) 0f else ((photoH*scale-side)/2).coerceAtLeast(0f)
val dx=ox.coerceIn(-mx,mx); val dy=oy.coerceIn(-my,my)
Canvas(Modifier.weight(1f).fillMaxWidth().onSizeChanged { bounds=it }
.pointerInput(image,bounds) {
if (image!=null) detectTransformGestures { _,pan,factor,_ ->
val old=zoom; zoom=(zoom*factor).coerceIn(1f,16f)
val s=maxOf(side/image.width,side/image.height)*zoom
ox=(ox*zoom/old+pan.x).coerceIn(-(image.width*s-side)/2,(image.width*s-side)/2)
oy=(oy*zoom/old+pan.y).coerceIn(-(image.height*s-side)/2,(image.height*s-side)/2)
val s=maxOf(side/photoW,side/photoH)*zoom
ox=(ox*zoom/old+pan.x).coerceIn(-(photoW*s-side)/2,(photoW*s-side)/2)
oy=(oy*zoom/old+pan.y).coerceIn(-(photoH*s-side)/2,(photoH*s-side)/2)
}
}) {
drawRect(Color.Black)
if (image!=null && side>0) {
drawImage(image.asImageBitmap(),dstOffset=IntOffset(((size.width-image.width*scale)/2+dx).roundToInt(),
((size.height-image.height*scale)/2+dy).roundToInt()),
dstSize=IntSize((image.width*scale).roundToInt(),(image.height*scale).roundToInt()))
drawImage(image.asImageBitmap(),dstOffset=IntOffset(((size.width-photoW*scale)/2+dx).roundToInt(),
((size.height-photoH*scale)/2+dy).roundToInt()),
dstSize=IntSize((photoW*scale).roundToInt(),(photoH*scale).roundToInt()))
val x=(size.width-side)/2; val y=(size.height-side)/2; val shade=Color.Black.copy(alpha=0.6f)
drawRect(shade,size=Size(size.width,y)); drawRect(shade,Offset(0f,y+side),Size(size.width,y))
drawRect(shade,Offset(0f,y),Size(x,side)); drawRect(shade,Offset(x+side,y),Size(x,side))
@@ -105,10 +107,9 @@ fun AvatarCropDialog(uri: Uri, onCancel: () -> Unit, onConfirm: (File, AvatarCro
Row(Modifier.fillMaxWidth(),horizontalArrangement=Arrangement.End) {
TextButton(onClick=onCancel) { Text(stringResource(R.string.cancel)) }
TextButton(enabled=image!=null && source!=null && side>0,onClick={
val selected=requireNotNull(image)
val crop=AvatarCrop(((selected.width*scale-side-2*dx)/(2*scale*selected.width)).toDouble(),
((selected.height*scale-side-2*dy)/(2*scale*selected.height)).toDouble(),
side.toDouble()/scale/selected.width,side.toDouble()/scale/selected.height)
val crop=AvatarCrop(((photoW*scale-side-2*dx)/(2*scale*photoW)).toDouble(),
((photoH*scale-side-2*dy)/(2*scale*photoH)).toDouble(),
side.toDouble()/scale/photoW,side.toDouble()/scale/photoH)
handedOver=true; onConfirm(requireNotNull(source),crop)
}) { Text(stringResource(R.string.avatar_upload)) }
}