Skip to content

Commit 6d0599d

Browse files
authored
Prevent animated WebP conversion failures (#487)
1 parent e8e4d08 commit 6d0599d

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FILE_SIZE;
1111
import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FRAMES;
1212
import static com.github.stickerifier.stickerify.media.MediaConstraints.VP9_CODEC;
13+
import static java.nio.charset.StandardCharsets.ISO_8859_1;
1314
import static java.nio.charset.StandardCharsets.UTF_8;
1415

1516
import com.github.stickerifier.stickerify.exception.CorruptedFileException;
@@ -46,6 +47,13 @@ public final class MediaHelper {
4647
private static final int IMAGE_KEEP_ASPECT_RATIO = -1;
4748
private static final int VIDEO_KEEP_ASPECT_RATIO = -2;
4849

50+
private static final int WEBP_CHUNK_TYPE_OFFSET = 12;
51+
private static final int WEBP_CHUNK_TYPE_LENGTH = 4;
52+
private static final int WEBP_FLAGS_BYTE_OFFSET = 20;
53+
private static final int WEBP_HEADER_SIZE = 21;
54+
private static final int WEBP_ANIMATION_BIT_MASK = 0x02;
55+
private static final String WEBP_EXTENDED_FILE_FORMAT = "VP8X";
56+
4957
/**
5058
* Based on the type of passed-in file, it converts it into the proper media.
5159
* If no conversion was needed, {@code null} is returned.
@@ -73,7 +81,7 @@ public final class MediaHelper {
7381
return null;
7482
}
7583

76-
if (mimeType.startsWith("image/")) {
84+
if (isSupportedImage(inputFile, mimeType)) {
7785
if (isImageCompliant(inputFile, mimeType)) {
7886
LOGGER.atInfo().log("The image doesn't need conversion");
7987
return null;
@@ -288,6 +296,47 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation
288296
&& animation.height() == MAX_SIDE_LENGTH;
289297
}
290298

299+
/**
300+
* Checks if the MIME type corresponds to one of the supported image formats.
301+
* If the image file is an animated WebP, {@code false} is returned as they are not currently supported.
302+
*
303+
* @param image the image file to check
304+
* @param mimeType the MIME type to check
305+
* @return {@code true} if the MIME type is supported
306+
*/
307+
private static boolean isSupportedImage(File image, String mimeType) {
308+
if ("image/webp".equals(mimeType) && isAnimatedWebp(image)) {
309+
LOGGER.atInfo().log("The image is an animated WebP");
310+
return false;
311+
}
312+
313+
return mimeType.startsWith("image/");
314+
}
315+
316+
/**
317+
* Detects if a WebP file is animated by checking its file header.
318+
*
319+
* @param file the WebP file to check
320+
* @return {@code true} if the file is an animated WebP
321+
*/
322+
private static boolean isAnimatedWebp(File file) {
323+
try (var fileInputStream = new FileInputStream(file)) {
324+
var header = fileInputStream.readNBytes(WEBP_HEADER_SIZE);
325+
if (header.length < WEBP_HEADER_SIZE) {
326+
return false;
327+
}
328+
329+
var chunkHeader = new String(header, WEBP_CHUNK_TYPE_OFFSET, WEBP_CHUNK_TYPE_LENGTH, ISO_8859_1);
330+
boolean isExtendedFormat = WEBP_EXTENDED_FILE_FORMAT.equals(chunkHeader);
331+
boolean hasAnimationFlag = (header[WEBP_FLAGS_BYTE_OFFSET] & WEBP_ANIMATION_BIT_MASK) != 0;
332+
333+
return isExtendedFormat && hasAnimationFlag;
334+
} catch (IOException e) {
335+
LOGGER.atWarn().setCause(e).log("An error occurred checking if the file is an animated WebP");
336+
return false;
337+
}
338+
}
339+
291340
/**
292341
* Checks if passed-in image is already compliant with Telegram's requisites.
293342
*

src/test/java/com/github/stickerifier/stickerify/media/MediaHelperTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ void resizeAnimatedWebpVideo() {
260260
var webpVideo = loadResource("animated.webp");
261261

262262
var ex = assertThrows(MediaException.class, () -> MediaHelper.convert(webpVideo));
263-
assertThat(ex.getMessage(), equalTo("FFmpeg image conversion failed"));
263+
assertThat(ex.getMessage(), equalTo("The file with image/webp MIME type is not supported"));
264264
}
265265

266266
@Test

0 commit comments

Comments
 (0)