From 66d9f632ea464b6b26120311929c1fd3d4a90557 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sat, 4 Apr 2026 16:10:50 +0200 Subject: [PATCH 1/4] Add check to determine whether an image is an animated WebP file to avoid unexpected FFmpeg failures --- .../stickerify/media/MediaHelper.java | 41 ++++++++++++++++++- .../stickerify/media/MediaHelperTest.java | 2 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java index 1f1d518a..063dbce9 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -73,7 +73,7 @@ public final class MediaHelper { return null; } - if (mimeType.startsWith("image/")) { + if (isSupportedImage(inputFile, mimeType)) { if (isImageCompliant(inputFile, mimeType)) { LOGGER.atInfo().log("The image doesn't need conversion"); return null; @@ -288,6 +288,45 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation && animation.height() == MAX_SIDE_LENGTH; } + /** + * Checks if the MIME type corresponds to one of the supported image formats. + * + * @param image the image file to check + * @param mimeType the MIME type to check + * @return {@code true} if the MIME type is supported + */ + private static boolean isSupportedImage(File image, String mimeType) { + if ("image/webp".equals(mimeType) && isAnimatedWebp(image)) { + LOGGER.atInfo().log("The image is an animated WebP"); + return false; + } + + return mimeType.startsWith("image/"); + } + + /** + * Detects if a WebP file is animated by checking its file header. + * + * @param file the WebP file to check + * @return {@code true} if the file is an animated WebP + */ + private static boolean isAnimatedWebp(File file) { + try (var fileInputStream = new FileInputStream(file)) { + var header = new byte[256]; + int bytesRead = fileInputStream.read(header); + + if (bytesRead < 32) { + return false; + } + + var headerContent = new String(header, UTF_8); + return headerContent.contains("ANIM"); + } catch (IOException e) { + LOGGER.atWarn().setCause(e).log("An error occurred checking if the file is an animated WebP"); + return false; + } + } + /** * Checks if passed-in image is already compliant with Telegram's requisites. * diff --git a/src/test/java/com/github/stickerifier/stickerify/media/MediaHelperTest.java b/src/test/java/com/github/stickerifier/stickerify/media/MediaHelperTest.java index 5aa342aa..174ad277 100644 --- a/src/test/java/com/github/stickerifier/stickerify/media/MediaHelperTest.java +++ b/src/test/java/com/github/stickerifier/stickerify/media/MediaHelperTest.java @@ -260,7 +260,7 @@ void resizeAnimatedWebpVideo() { var webpVideo = loadResource("animated.webp"); var ex = assertThrows(MediaException.class, () -> MediaHelper.convert(webpVideo)); - assertThat(ex.getMessage(), equalTo("FFmpeg image conversion failed")); + assertThat(ex.getMessage(), equalTo("The file with image/webp MIME type is not supported")); } @Test From 88272f7d997c8b77fa4427892dce76c2b61ac911 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sat, 4 Apr 2026 16:47:45 +0200 Subject: [PATCH 2/4] Add useful details to Javadoc --- .../com/github/stickerifier/stickerify/media/MediaHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java index 063dbce9..a085d09d 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -290,6 +290,7 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation /** * Checks if the MIME type corresponds to one of the supported image formats. + * If the image file is an animated WebP, {@code false} is returned as they are not currently supported. * * @param image the image file to check * @param mimeType the MIME type to check From 63c85c14eaf21c282bec19ea69bad51c864e0261 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sat, 4 Apr 2026 17:35:12 +0200 Subject: [PATCH 3/4] Rework webp animation detection to check on extended details --- .../stickerify/media/MediaHelper.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java index a085d09d..d18c1204 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -10,6 +10,7 @@ import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FILE_SIZE; import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FRAMES; import static com.github.stickerifier.stickerify.media.MediaConstraints.VP9_CODEC; +import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; import com.github.stickerifier.stickerify.exception.CorruptedFileException; @@ -46,6 +47,12 @@ public final class MediaHelper { private static final int IMAGE_KEEP_ASPECT_RATIO = -1; private static final int VIDEO_KEEP_ASPECT_RATIO = -2; + private static final int WEBP_CHUNK_TYPE_OFFSET = 12; + private static final int WEBP_CHUNK_TYPE_LENGTH = 4; + private static final int WEBP_FLAGS_BYTE_OFFSET = 20; + private static final int WEBP_ANIMATION_BIT_MASK = 0x02; + private static final String WEBP_EXTENDED_FILE_FORMAT = "VP8X"; + /** * Based on the type of passed-in file, it converts it into the proper media. * If no conversion was needed, {@code null} is returned. @@ -313,15 +320,16 @@ private static boolean isSupportedImage(File image, String mimeType) { */ private static boolean isAnimatedWebp(File file) { try (var fileInputStream = new FileInputStream(file)) { - var header = new byte[256]; - int bytesRead = fileInputStream.read(header); - - if (bytesRead < 32) { + var header = new byte[WEBP_FLAGS_BYTE_OFFSET + 1]; + if (fileInputStream.read(header) < 21) { return false; } - var headerContent = new String(header, UTF_8); - return headerContent.contains("ANIM"); + var chunkHeader = new String(header, WEBP_CHUNK_TYPE_OFFSET, WEBP_CHUNK_TYPE_LENGTH, ISO_8859_1); + boolean isExtendedFormat = WEBP_EXTENDED_FILE_FORMAT.equals(chunkHeader); + boolean hasAnimationFlag = (header[WEBP_FLAGS_BYTE_OFFSET] & WEBP_ANIMATION_BIT_MASK) != 0; + + return isExtendedFormat && hasAnimationFlag; } catch (IOException e) { LOGGER.atWarn().setCause(e).log("An error occurred checking if the file is an animated WebP"); return false; From f7b5930168578cf0c30ee56421f41bc9af6034f2 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sat, 4 Apr 2026 18:03:33 +0200 Subject: [PATCH 4/4] Switch to readNBytes to retrieve header content consistently --- .../github/stickerifier/stickerify/media/MediaHelper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java index d18c1204..af26601f 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -50,6 +50,7 @@ public final class MediaHelper { private static final int WEBP_CHUNK_TYPE_OFFSET = 12; private static final int WEBP_CHUNK_TYPE_LENGTH = 4; private static final int WEBP_FLAGS_BYTE_OFFSET = 20; + private static final int WEBP_HEADER_SIZE = 21; private static final int WEBP_ANIMATION_BIT_MASK = 0x02; private static final String WEBP_EXTENDED_FILE_FORMAT = "VP8X"; @@ -320,8 +321,8 @@ private static boolean isSupportedImage(File image, String mimeType) { */ private static boolean isAnimatedWebp(File file) { try (var fileInputStream = new FileInputStream(file)) { - var header = new byte[WEBP_FLAGS_BYTE_OFFSET + 1]; - if (fileInputStream.read(header) < 21) { + var header = fileInputStream.readNBytes(WEBP_HEADER_SIZE); + if (header.length < WEBP_HEADER_SIZE) { return false; }