diff --git a/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorImpl.kt b/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorImpl.kt index 0e40fee4f171f3abfb4ba3ca72ff025e0d848d05..fe811dc42209e872644c838cbcca238d79b56684 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorImpl.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorImpl.kt @@ -20,6 +20,8 @@ package org.briarproject.briar.desktop.attachment.media import mu.KotlinLogging import org.briarproject.briar.api.attachment.MediaConstants.MAX_IMAGE_SIZE +import java.awt.geom.AffineTransform +import java.awt.image.AffineTransformOp import java.awt.image.BufferedImage import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -29,19 +31,39 @@ import javax.imageio.IIOImage import javax.imageio.ImageIO import javax.imageio.ImageWriteParam import javax.inject.Inject +import kotlin.math.max class ImageCompressorImpl @Inject internal constructor() : ImageCompressor { companion object { val LOG = KotlinLogging.logger {} + + const val MAX_ATTACHMENT_DIMENSION = 1000 } override fun compressImage(image: BufferedImage): InputStream { val out = ByteArrayOutputStream() + + // First make sure we're dealing with an image without alpha channel. Alpha channels are not supported by JPEG + // compression later on. If the image contains an alpha channel, we draw it onto an image without alpha channel. val withoutAlpha = if (!image.colorModel.hasAlpha()) image else { val replacement = BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB) replacement.apply { createGraphics().drawImage(image, 0, 0, null) } } + + // Now determine the factor by which we scale down the image in order to end up with an image + // with both sides smaller than [MAX_ATTACHMENT_DIMENSION]. + val maxSize = max(withoutAlpha.width, withoutAlpha.height) + var factor = 1 + while (maxSize / factor > MAX_ATTACHMENT_DIMENSION) { + factor *= 2 + } + + // Now, if we determined a factor greater 1 reduce image dimensions + val scaled = if (factor != 1) scaleDown(withoutAlpha, factor) else withoutAlpha + + // After that, compress image. Try with maximum quality and reduce until we can compress below + // a size of [MAX_IMAGE_SIZE]. We try quality levels 100, 90, ..., 20, 10. for (quality in 100 downTo 1 step 10) { val jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next() jpgWriter.output = ImageIO.createImageOutputStream(out) @@ -50,7 +72,7 @@ class ImageCompressorImpl @Inject internal constructor() : ImageCompressor { jpgWriteParam.compressionMode = ImageWriteParam.MODE_EXPLICIT jpgWriteParam.compressionQuality = quality / 100f - val outputImage = IIOImage(withoutAlpha, null, null) + val outputImage = IIOImage(scaled, null, null) jpgWriter.write(null, outputImage, jpgWriteParam) jpgWriter.dispose() @@ -62,4 +84,22 @@ class ImageCompressorImpl @Inject internal constructor() : ImageCompressor { } throw IOException() } + + private fun scaleDown(image: BufferedImage, factor: Int): BufferedImage { + // Calculate new images dimensions + val w = image.width / factor + val h = image.height / factor + // Determine vertical and horizontal scale factors. We accept some minimal distortion here as it is quite + // possible that the integer division above led to different effective scale factors. Since we need integral + // dimensions there's not much we could do about that. + val sx = w / image.width.toDouble() + val sy = h / image.height.toDouble() + // Create new image of same type and scale down + var resized = BufferedImage(w, h, image.type) + val at = AffineTransform().also { + it.scale(sx, sy) + } + val scale = AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR) + return scale.filter(image, resized) + } } diff --git a/src/test/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorTest.kt b/src/test/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorTest.kt index 62647bc9c278313364a8135741bf5110c4d69306..f88fee4fa4394a775be042c8f2470a5211e7c29b 100644 --- a/src/test/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorTest.kt +++ b/src/test/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorTest.kt @@ -27,7 +27,7 @@ class ImageCompressorTest { private val compressor = ImageCompressorImpl() @Test - fun `can compress voronoi image`() { + fun `can compress voronoi diagram`() { // load image val input = Thread.currentThread().contextClassLoader.getResourceAsStream("images/voronoi1.png") val image = input.use { @@ -64,5 +64,4 @@ class ImageCompressorTest { } println("image size: ${reloaded.width}x${reloaded.height}") } - }