Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Produce lossless WebP images #265

Merged
merged 14 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ therefore counting towards the 4 contributions goal.
* The official documentation of the Telegram Bot API can be found [here](https://core.telegram.org/bots)
* The library used by the bot to work with Telegram is [Java Telegram Bot API](https://github.com/pengrad/java-telegram-bot-api)
* Video conversion uses [FFmpeg](https://ffmpeg.org/) and [JAVE2](https://github.com/a-schild/jave2)
* Image conversion uses [ImageIO](https://docs.oracle.com/en/java/javase/20/docs/api/java.desktop/javax/imageio/ImageIO.html), [TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys), [imgscalr](https://github.com/rkalla/imgscalr), and [Pngtastic](https://github.com/depsypher/pngtastic)
* Image conversion uses [Scrimage](https://github.com/sksamuel/scrimage), [ImageIO](https://docs.oracle.com/en/java/javase/20/docs/api/java.desktop/javax/imageio/ImageIO.html), and [TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys)
* Animated sticker validation uses [Gson](https://github.com/google/gson)
* MIME type analysis is performed using [Apache Tika](https://tika.apache.org/)

Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ dependencies {
implementation libs.imageio.jpeg
implementation libs.imageio.psd
implementation libs.imageio.webp
implementation libs.imgscalr
implementation libs.jave
implementation libs.logback.classic
implementation libs.logback.core
implementation libs.pngtastic
implementation libs.scrimage.core
implementation libs.scrimage.webp
implementation libs.slf4j.api
implementation libs.telegram.bot.api
implementation libs.tika
Expand Down
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
logback = "1.5.6"
scrimage = "4.1.3"
twelvemonkeys = "3.11.0"

[libraries]
Expand All @@ -11,15 +12,15 @@ imageio-bmp = { module = "com.twelvemonkeys.imageio:imageio-bmp", version.ref =
imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg", version.ref = "twelvemonkeys" }
imageio-psd = { module = "com.twelvemonkeys.imageio:imageio-psd", version.ref = "twelvemonkeys" }
imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
imgscalr = "org.imgscalr:imgscalr-lib:4.2"
jave = "ws.schild:jave-core:3.5.0"
junit = "org.junit.jupiter:junit-jupiter:5.10.3"
junit-platform = "org.junit.platform:junit-platform-launcher:1.10.3"
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" }
mockwebserver = "com.squareup.okhttp3:mockwebserver3-junit5:5.0.0-alpha.14"
okio = "com.squareup.okio:okio:3.9.0"
pngtastic = "com.github.depsypher:pngtastic:1.8"
scrimage-core = { module = "com.sksamuel.scrimage:scrimage-core", version.ref = "scrimage" }
scrimage-webp = { module = "com.sksamuel.scrimage:scrimage-webp", version.ref = "scrimage" }
slf4j-api = "org.slf4j:slf4j-api:2.0.16"
telegram-bot-api = "com.github.pengrad:java-telegram-bot-api:7.8.0"
tika = "org.apache.tika:tika-core:2.9.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import com.googlecode.pngtastic.core.PngImage;
import com.googlecode.pngtastic.core.PngOptimizer;
import com.sksamuel.scrimage.ImmutableImage;
import com.sksamuel.scrimage.webp.WebpWriter;
import org.apache.tika.Tika;
import org.imgscalr.Scalr;
import org.imgscalr.Scalr.Mode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ws.schild.jave.EncoderException;
Expand All @@ -32,7 +30,6 @@
import ws.schild.jave.process.ProcessLocator;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -81,7 +78,7 @@ public static File convert(File inputFile) throws TelegramApiException {
if (image != null) {
boolean isFileSizeCompliant = isFileSizeLowerThan(inputFile, MAX_IMAGE_FILE_SIZE);

return convertToPng(image, mimeType, isFileSizeCompliant);
return convertToWebp(image, mimeType, isFileSizeCompliant);
}
} catch (TelegramApiException e) {
LOGGER.atWarn().setCause(e).log("The file with {} MIME type could not be converted", mimeType);
Expand Down Expand Up @@ -182,16 +179,16 @@ private static boolean isFileSizeLowerThan(File file, long threshold) throws Tel
* If the file isn't a supported image, {@code null} is returned.
*
* @param file the file to read
* @return the image, if supported by {@link ImageIO}
* @return the image, if supported by Scrimage
* @throws TelegramApiException if an error occurred processing passed-in file
*/
private static BufferedImage toImage(File file) throws TelegramApiException {
private static ImmutableImage toImage(File file) throws TelegramApiException {
LOGGER.atTrace().log("Loading image information");

try {
return ImageIO.read(file);
return ImmutableImage.loader().fromFile(file);
} catch (IOException e) {
throw new TelegramApiException("Unable to retrieve the image from passed-in file", e);
return null;
}
}

Expand All @@ -206,26 +203,22 @@ private static boolean isSupportedVideo(String mimeType) {
}

/**
* Given an image file, it converts it to a png file of the proper dimension (max 512 x 512).
* Given an image file, it converts it to a webp file of the proper dimension (max 512 x 512).
*
* @param image the image to convert to png
* @param image the image to convert to webp
* @param mimeType the MIME type of the file
* @param isFileSizeCompliant {@code true} if the file does not exceed Telegram's limit
* @return converted image, {@code null} if no conversion was required
* @throws TelegramApiException if an error occurred processing passed-in image
*/
private static File convertToPng(BufferedImage image, String mimeType, boolean isFileSizeCompliant) throws TelegramApiException {
try {
if (isImageCompliant(image, mimeType) && isFileSizeCompliant) {
LOGGER.atInfo().log("The image doesn't need conversion");

return null;
}
private static File convertToWebp(ImmutableImage image, String mimeType, boolean isFileSizeCompliant) throws TelegramApiException {
if (isImageCompliant(image, mimeType) && isFileSizeCompliant) {
LOGGER.atInfo().log("The image doesn't need conversion");

return createPngFile(resizeImage(image));
} finally {
image.flush();
return null;
}

return createWebpFile(resizeImage(image));
}

/**
Expand All @@ -236,8 +229,8 @@ private static File convertToPng(BufferedImage image, String mimeType, boolean i
* @param mimeType the MIME type of the file
* @return {@code true} if the file is compliant
*/
private static boolean isImageCompliant(BufferedImage image, String mimeType) {
return ("image/png".equals(mimeType) || "image/webp".equals(mimeType)) && isSizeCompliant(image.getWidth(), image.getHeight());
private static boolean isImageCompliant(ImmutableImage image, String mimeType) {
return ("image/png".equals(mimeType) || "image/webp".equals(mimeType)) && isSizeCompliant(image.width, image.height);
}

/**
Expand All @@ -258,59 +251,38 @@ private static boolean isSizeCompliant(int width, int height) {
* @param image the image to be resized
* @return resized image
*/
private static BufferedImage resizeImage(BufferedImage image) {
private static ImmutableImage resizeImage(ImmutableImage image) {
LOGGER.atTrace().log("Resizing image");

return Scalr.resize(image, Mode.AUTOMATIC, MAX_SIZE);
return image.max(MAX_SIZE, MAX_SIZE);
}

/**
* Creates a new <i>.png</i> file from passed-in {@code image}.
* If the resulting image exceeds Telegram's threshold, it will be optimized using {@link PngOptimizer}.
* Creates a new <i>.webp</i> file from passed-in {@code image}.
*
* @param image the image to convert to png
* @return png image
* @param image the image to convert to webp
* @return webp image
* @throws TelegramApiException if an error occurs creating the temp file
* @throws MediaOptimizationException if the image size could not be reduced enough to meet Telegram's requirements
*/
private static File createPngFile(BufferedImage image) throws TelegramApiException {
var pngImage = createTempFile("png");
private static File createWebpFile(ImmutableImage image) throws TelegramApiException {
var webpImage = createTempFile("webp");

LOGGER.atTrace().log("Writing output image file");

try {
ImageIO.write(image, "png", pngImage);
image.output(WebpWriter.MAX_LOSSLESS_COMPRESSION, webpImage);

if (!isFileSizeLowerThan(pngImage, MAX_IMAGE_FILE_SIZE)) {
optimizeImage(pngImage);
if (!isFileSizeLowerThan(webpImage, MAX_IMAGE_FILE_SIZE)) {
throw new MediaOptimizationException("The image size could not be reduced enough to meet Telegram's requirements");
}
} catch (IOException e) {
throw new TelegramApiException("An unexpected error occurred trying to create resulting image", e);
} finally {
image.flush();
}

LOGGER.atTrace().log("Image conversion completed successfully");

return pngImage;
}

/**
* Performs an optimization aimed to reduce the image's size using {@link PngOptimizer}.
*
* @param pngImage the file to optimize
* @throws IOException if the optimization process fails
* @throws TelegramApiException if the image size could not be reduced enough to meet Telegram's requirements
*/
private static void optimizeImage(File pngImage) throws IOException, TelegramApiException {
LOGGER.atTrace().log("Optimizing image size");

var imagePath = pngImage.getPath();
new PngOptimizer().optimize(new PngImage(imagePath, "INFO"), imagePath, false, null);

if (!isFileSizeLowerThan(pngImage, MAX_IMAGE_FILE_SIZE)) {
throw new MediaOptimizationException("The image size could not be reduced enough to meet Telegram's requirements");
}
return webpImage;
}

/**
Expand Down Expand Up @@ -402,7 +374,7 @@ private static File convertWithFfmpeg(File file, MultimediaInfo mediaInfo) throw
"ffmpeg",
"-v", "error",
"-i", file.getAbsolutePath(),
"-vf", "scale = " + videoDetails.width() + ":" + videoDetails.height() + ", fps = " + videoDetails.frameRate(),
"-vf", "scale=" + videoDetails.width() + ":" + videoDetails.height() + ",fps=" + videoDetails.frameRate(),
"-c:v", "libvpx-" + VP9_CODEC,
"-b:v", "256k",
"-crf", "32",
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<appender-ref ref="CONSOLE"/>
</root>

<logger name="com.sksamuel.scrimage" level="warn" />
<logger name="org.apache.tika" level="info" />
<logger name="ws.schild.jave" level="info" />
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private static void assertImageConsistency(File result, int expectedWidth, int e
var actualExtension = getExtension(result);

assertAll("Image validation failed",
() -> assertThat("image's extension must be png", actualExtension, is(equalTo(".png"))),
() -> assertThat("image's extension must be png", actualExtension, is(equalTo(".webp"))),
() -> assertThat("image's width is not correct", image.getWidth(), is(equalTo(expectedWidth))),
() -> assertThat("image's height is not correct", image.getHeight(), is(equalTo(expectedHeight))),
() -> assertThat("image size should not exceed 512 KB", Files.size(result.toPath()), is(lessThanOrEqualTo(MAX_IMAGE_FILE_SIZE)))
Expand Down Expand Up @@ -105,15 +105,15 @@ void resizeTiffImage() throws Exception {
var tiffImage = loadResource("valid.tiff");
var result = MediaHelper.convert(tiffImage);

assertImageConsistency(result, 512, 342);
assertImageConsistency(result, 512, 341);
}

@Test
void resizePsdImage() throws Exception {
var psdImage = loadResource("valid.psd");
var result = MediaHelper.convert(psdImage);

assertImageConsistency(result, 512, 384);
assertImageConsistency(result, 512, 383);
}

@Test
Expand Down
Binary file modified src/test/resources/detailed.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading