|
10 | 10 | import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FILE_SIZE; |
11 | 11 | import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_VIDEO_FRAMES; |
12 | 12 | import static com.github.stickerifier.stickerify.media.MediaConstraints.VP9_CODEC; |
| 13 | +import static java.nio.charset.StandardCharsets.ISO_8859_1; |
13 | 14 | import static java.nio.charset.StandardCharsets.UTF_8; |
14 | 15 |
|
15 | 16 | import com.github.stickerifier.stickerify.exception.CorruptedFileException; |
@@ -46,6 +47,13 @@ public final class MediaHelper { |
46 | 47 | private static final int IMAGE_KEEP_ASPECT_RATIO = -1; |
47 | 48 | private static final int VIDEO_KEEP_ASPECT_RATIO = -2; |
48 | 49 |
|
| 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 | + |
49 | 57 | /** |
50 | 58 | * Based on the type of passed-in file, it converts it into the proper media. |
51 | 59 | * If no conversion was needed, {@code null} is returned. |
@@ -73,7 +81,7 @@ public final class MediaHelper { |
73 | 81 | return null; |
74 | 82 | } |
75 | 83 |
|
76 | | - if (mimeType.startsWith("image/")) { |
| 84 | + if (isSupportedImage(inputFile, mimeType)) { |
77 | 85 | if (isImageCompliant(inputFile, mimeType)) { |
78 | 86 | LOGGER.atInfo().log("The image doesn't need conversion"); |
79 | 87 | return null; |
@@ -288,6 +296,47 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation |
288 | 296 | && animation.height() == MAX_SIDE_LENGTH; |
289 | 297 | } |
290 | 298 |
|
| 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 | + |
291 | 340 | /** |
292 | 341 | * Checks if passed-in image is already compliant with Telegram's requisites. |
293 | 342 | * |
|
0 commit comments