diff --git a/backend/src/main/java/com/printcalculator/service/admin/AdminMediaControllerService.java b/backend/src/main/java/com/printcalculator/service/admin/AdminMediaControllerService.java index 0e30a60..c0bf0ac 100644 --- a/backend/src/main/java/com/printcalculator/service/admin/AdminMediaControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/admin/AdminMediaControllerService.java @@ -350,7 +350,27 @@ public class AdminMediaControllerService { } String extension = GENERATED_FORMAT_EXTENSIONS.get(format); Path outputFile = generatedDirectory.resolve(preset.name() + "." + extension); - mediaFfmpegService.generateVariant(sourceFile, outputFile, dimensions.widthPx(), dimensions.heightPx(), format); + try { + mediaFfmpegService.generateVariant( + sourceFile, + outputFile, + dimensions.widthPx(), + dimensions.heightPx(), + format + ); + } catch (IOException e) { + if (FORMAT_AVIF.equals(format)) { + skippedFormats.add(format); + logger.warn( + "Skipping AVIF variant generation for asset {} preset '{}' because FFmpeg AVIF generation failed: {}", + asset.getId(), + preset.name(), + e.getMessage() + ); + continue; + } + throw e; + } MediaVariant variant = new MediaVariant(); variant.setMediaAsset(asset); diff --git a/backend/src/main/java/com/printcalculator/service/media/MediaFfmpegService.java b/backend/src/main/java/com/printcalculator/service/media/MediaFfmpegService.java index 4e50785..aac63e8 100644 --- a/backend/src/main/java/com/printcalculator/service/media/MediaFfmpegService.java +++ b/backend/src/main/java/com/printcalculator/service/media/MediaFfmpegService.java @@ -82,8 +82,6 @@ public class MediaFfmpegService { case "AVIF" -> { command.add("-c:v"); command.add(encoder); - command.add("-still-picture"); - command.add("1"); command.add("-crf"); command.add("30"); command.add("-b:v"); diff --git a/backend/src/test/java/com/printcalculator/service/admin/AdminMediaControllerServiceTest.java b/backend/src/test/java/com/printcalculator/service/admin/AdminMediaControllerServiceTest.java index 861996b..9f54e78 100644 --- a/backend/src/test/java/com/printcalculator/service/admin/AdminMediaControllerServiceTest.java +++ b/backend/src/test/java/com/printcalculator/service/admin/AdminMediaControllerServiceTest.java @@ -51,6 +51,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -275,6 +276,31 @@ class AdminMediaControllerServiceTest { .noneMatch(variant -> "WEBP".equals(variant.getFormat()) || "AVIF".equals(variant.getFormat()))); } + @Test + void uploadAsset_whenAvifGenerationFails_shouldKeepAssetReadyAndStoreOtherVariants() throws Exception { + when(mediaImageInspector.inspect(any(Path.class))).thenReturn( + new MediaImageInspector.ImageMetadata("image/png", "png", 1600, 900) + ); + doThrow(new java.io.IOException("FFmpeg failed to generate media variant. Unrecognized option 'still-picture'.")) + .when(mediaFfmpegService) + .generateVariant(any(Path.class), any(Path.class), anyInt(), anyInt(), org.mockito.ArgumentMatchers.eq("AVIF")); + + MockMultipartFile file = new MockMultipartFile( + "file", + "landing-hero.png", + "image/png", + "png-image-content".getBytes(StandardCharsets.UTF_8) + ); + + AdminMediaAssetDto dto = service.uploadAsset(file, " Landing hero ", " Main headline ", null); + + assertEquals("READY", dto.getStatus()); + assertEquals(7, dto.getVariants().size()); + assertTrue(dto.getVariants().stream().noneMatch(variant -> "AVIF".equals(variant.getFormat()))); + assertEquals(3, dto.getVariants().stream().filter(variant -> "JPEG".equals(variant.getFormat())).count()); + assertEquals(3, dto.getVariants().stream().filter(variant -> "WEBP".equals(variant.getFormat())).count()); + } + @Test void uploadAsset_withOversizedFile_shouldFailValidationBeforePersistence() { service = new AdminMediaControllerService( diff --git a/backend/src/test/java/com/printcalculator/service/media/MediaFfmpegServiceTest.java b/backend/src/test/java/com/printcalculator/service/media/MediaFfmpegServiceTest.java index d407a2e..386fa7a 100644 --- a/backend/src/test/java/com/printcalculator/service/media/MediaFfmpegServiceTest.java +++ b/backend/src/test/java/com/printcalculator/service/media/MediaFfmpegServiceTest.java @@ -6,8 +6,11 @@ import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; class MediaFfmpegServiceTest { @@ -61,4 +64,57 @@ class MediaFfmpegServiceTest { assertEquals("Media target file name must not start with '-'.", ex.getMessage()); } + + @Test + void generateVariant_avifShouldNotUseStillPictureFlag() throws Exception { + Path fakeFfmpeg = tempDir.resolve("fake-ffmpeg.sh"); + Files.writeString( + fakeFfmpeg, + """ + #!/bin/sh + if [ "$1" = "-hide_banner" ] && [ "$2" = "-encoders" ]; then + cat <<'EOF' + V..... mjpeg + V..... libwebp + V..... libaom-av1 + EOF + exit 0 + fi + + for arg in "$@"; do + if [ "$arg" = "-still-picture" ]; then + echo "Unrecognized option 'still-picture'. Error splitting the argument list: Option not found" + exit 1 + fi + done + + last_arg="" + for arg in "$@"; do + last_arg="$arg" + done + + mkdir -p "$(dirname "$last_arg")" + printf 'ok' > "$last_arg" + exit 0 + """ + ); + Files.setPosixFilePermissions( + fakeFfmpeg, + Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE + ) + ); + + MediaFfmpegService service = new MediaFfmpegService(fakeFfmpeg.toString()); + Path source = tempDir.resolve("input.png"); + Path target = tempDir.resolve("output.avif"); + Files.writeString(source, "image"); + + service.generateVariant(source, target, 120, 80, "AVIF"); + + assertTrue(Files.exists(target)); + assertEquals("ok", Files.readString(target)); + } }