Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package com.crosspaste.image
import androidx.compose.ui.graphics.Color
import com.crosspaste.app.AppFileType
import com.crosspaste.path.UserDataPathProvider
import com.crosspaste.utils.Box
import com.crosspaste.utils.ColorUtils.hsvToRgb
import com.crosspaste.utils.ColorUtils.isNearWhiteOrBlack
import com.crosspaste.utils.ColorUtils.rgbToHsv
import com.crosspaste.utils.ColorUtils.toHexString
import com.crosspaste.utils.StripedMutex
import com.crosspaste.utils.ioDispatcher
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.util.collections.*
import kotlinx.coroutines.withContext
import org.jetbrains.skia.Bitmap
import org.jetbrains.skia.Image
Expand All @@ -23,8 +25,7 @@ class DesktopIconColorExtractor(

private val logger = KotlinLogging.logger {}

// Color cache
private val colorCache = mutableMapOf<String, Color?>()
private val colorCache: MutableMap<String, Box<Color>> = ConcurrentMap()

private val mutex = StripedMutex()

Expand All @@ -33,22 +34,22 @@ class DesktopIconColorExtractor(
*/
suspend fun getBackgroundColor(source: String): Color? =
mutex.withLock(source) {
if (colorCache.containsKey(source)) {
colorCache[source]
} else {
withContext(ioDispatcher) {
val color =
runCatching {
val path = userDataPathProvider.resolve("$source.png", AppFileType.ICON)
val bytes = path.toNioPath().readBytes()
extractColorFromBytes(bytes)
}.getOrNull()

logger.debug { "$source - ${color?.toHexString()}" }

colorCache[source] = color
color
}
colorCache[source]?.let { cached ->
return@withLock cached.getOrNull()
}

withContext(ioDispatcher) {
val color =
runCatching {
val path = userDataPathProvider.resolve("$source.png", AppFileType.ICON)
val bytes = path.toNioPath().readBytes()
extractColorFromBytes(bytes)
}.getOrNull()

logger.debug { "$source - ${color?.toHexString()}" }

colorCache[source] = Box.of(color)
color
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ class DesktopThumbnailLoader(
) {
runCatching {
val properties = Properties()
properties.load(getOriginMetaPath(pasteFileCoordinate).toNioPath().inputStream().buffered())
getOriginMetaPath(pasteFileCoordinate).toNioPath().inputStream().buffered().use { input ->
properties.load(input)
}
properties.getProperty(DIMENSIONS)?.let {
imageInfoBuilder.add(createPasteInfoWithoutConverter(DIMENSIONS, it))
}
Expand Down Expand Up @@ -125,7 +127,9 @@ class DesktopThumbnailLoader(
val properties = Properties()
properties.setProperty(SIZE, "$fileSize")
properties.setProperty(DIMENSIONS, "$originalWidth x $originalHeight")
properties.store(getOriginMetaPath(value).toNioPath().outputStream().buffered(), null)
getOriginMetaPath(value).toNioPath().outputStream().buffered().use { output ->
properties.store(output, null)
}
}
}
}
36 changes: 20 additions & 16 deletions app/src/desktopMain/kotlin/com/crosspaste/image/WebpImageWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,27 @@ class WebpImageWriter : ImageWriter<BufferedImage> {
): Boolean =
runCatching {
val writer = ImageIO.getImageWritersByMIMEType("image/webp").next()

// Configure encoding parameters
val writeParam =
(writer.defaultWriteParam as WebPWriteParam).apply {
compressionType = CompressionType.Lossy
alphaCompressionAlgorithm = 1
useSharpYUV = true
try {
// Configure encoding parameters
val writeParam =
(writer.defaultWriteParam as WebPWriteParam).apply {
compressionType = CompressionType.Lossy
alphaCompressionAlgorithm = 1
useSharpYUV = true
}

fileUtils.createDir(imagePath.noOptionParent)

// Configure the output on the ImageWriter and encode
val output = FileImageOutputStream(imagePath.toFile())
output.use { output ->
writer.output = output
writer.write(null, IIOImage(image, null, null), writeParam)
true
}

fileUtils.createDir(imagePath.noOptionParent)

// Configure the output on the ImageWriter
writer.output = FileImageOutputStream(imagePath.toFile())

// Encode
writer.write(null, IIOImage(image, null, null), writeParam)
true
} finally {
writer.dispose()
}
}.getOrElse {
logger.warn(it) { "Failed to write image to webp" }
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ object MacCropTransformation : Transformation() {
// 4) Fallback: if extractSubset fails (rare), copy the pixels manually
val copy = coil3.Bitmap()
copy.allocN32Pixels(width, height)
input.readPixels(srcX = left, srcY = top)
val pixels = input.readPixels(copy.imageInfo, copy.rowBytes, srcX = left, srcY = top)
if (pixels != null) {
copy.installPixels(pixels)
}
return copy
}
}
25 changes: 25 additions & 0 deletions shared/src/commonMain/kotlin/com/crosspaste/utils/Box.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.crosspaste.utils

sealed class Box<out T> {

companion object {
fun <T> of(value: T?): Box<T> =
if (value != null) {
Present(value)
} else {
Empty
}
}

data class Present<T>(
val value: T,
) : Box<T>()

object Empty : Box<Nothing>()

fun getOrNull(): T? =
when (this) {
is Present -> value
Empty -> null
}
}