uploads: add support for uploading .avif files.
Features of AVIF include: * Lossless and lossy compression. * High dynamic range (HDR) images * Wide color gamut images (i.e. 10- and 12-bit color depths) * Transparency (through alpha planes). * Animations (with an optional cover image). * Auxiliary image sequences, where the file contains a single primary image and a short secondary video, like Apple's Live Photos. * Metadata rotation, mirroring, and cropping. The AVIF format is still relatively new and some of these features aren't well supported by browsers or other software: * Animated AVIFs aren't supported by Firefox or by libvips. * HDR images aren't supported by Firefox. * Rotated, mirrored, and cropped AVIFs aren't supported by Firefox or Chrome. * Image grids, where the file contains multiple images that are tiled together into one big image, aren't supported by Firefox. * AVIF as a whole has only been supported for a year or two by Chrome and Firefox, and less than a year by Safari. For these reasons, only basic AVIFs that don't use animation, rotation, cropping, or image grids can be uploaded.
BIN
test/files/avif/Image grid example.avif
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
7
test/files/avif/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Test file sources:
|
||||
|
||||
* https://github.com/link-u/avif-sample-images
|
||||
* https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles
|
||||
* https://github.com/AOMediaCodec/libavif/tree/main/tests/data
|
||||
* https://colinbendell.github.io/webperf/animated-gif-decode/avif.html
|
||||
* https://0xc0000054.github.io/pdn-avif/using-image-grids.html
|
||||
BIN
test/files/avif/alpha_video.avif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv444_full_qp40.avif
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
test/files/avif/kimono.crop.avif
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
test/files/avif/kimono.mirror-horizontal.avif
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
test/files/avif/kimono.rotate90.avif
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
test/files/avif/paris_icc_exif_xmp.avif
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
BIN
test/files/avif/sequence-with-pitm-avif-major.avif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
test/files/avif/sequence-with-pitm.avif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
test/files/avif/sequence-without-pitm.avif
Normal file
BIN
test/files/avif/star-8bpc.avif
Normal file
BIN
test/files/avif/tiger_3layer_1res.avif
Normal file
|
After Width: | Height: | Size: 69 KiB |
@@ -195,6 +195,33 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
context "for an unsupported AVIF file" do
|
||||
should "fail for a grid image" do
|
||||
create_upload!("test/files/avif/Image grid example.avif", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
|
||||
should "fail for a cropped image" do
|
||||
create_upload!("test/files/avif/kimono.crop.avif", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
|
||||
should "fail for a rotated image" do
|
||||
create_upload!("test/files/avif/kimono.rotate90.avif", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
|
||||
should "fail for an image sequence" do
|
||||
create_upload!("test/files/avif/sequence-with-pitm.avif", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
|
||||
should "fail for a still image with an auxiliary image sequence" do
|
||||
create_upload!("test/files/avif/sequence-with-pitm-avif-major.avif", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
end
|
||||
end
|
||||
|
||||
context "for a video longer than the video length limit" do
|
||||
should "fail for a regular user" do
|
||||
create_upload!("https://cdn.donmai.us/original/63/cb/63cb09f2526ef3ac14f11c011516ad9b.webm", user: @user)
|
||||
@@ -267,6 +294,12 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
|
||||
should_upload_successfully("test/files/test-512x512.webm")
|
||||
should_upload_successfully("test/files/test-audio.m4v")
|
||||
# should_upload_successfully("test/files/compressed.swf")
|
||||
|
||||
should_upload_successfully("test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif")
|
||||
should_upload_successfully("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv420_limited_qp40.avif")
|
||||
should_upload_successfully("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv444_full_qp40.avif")
|
||||
should_upload_successfully("test/files/avif/paris_icc_exif_xmp.avif")
|
||||
should_upload_successfully("test/files/avif/tiger_3layer_1res.avif")
|
||||
end
|
||||
|
||||
context "uploading multiple files from your computer" do
|
||||
|
||||
@@ -21,6 +21,10 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
assert_equal([32, 32], MediaFile.open("test/files/test-static-32x32.gif").dimensions)
|
||||
end
|
||||
|
||||
should "determine the correct dimensions for an AVIF file" do
|
||||
assert_equal([2048, 858], MediaFile.open("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv420_limited_qp40.avif").dimensions)
|
||||
end
|
||||
|
||||
should "determine the correct dimensions for a webm file" do
|
||||
skip unless MediaFile.videos_enabled?
|
||||
assert_equal([512, 512], MediaFile.open("test/files/test-512x512.webm").dimensions)
|
||||
@@ -85,6 +89,12 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
assert_equal(:gif, MediaFile.open("test/files/test-static-32x32.gif").file_ext)
|
||||
end
|
||||
|
||||
should "determine the correct extension for an AVIF file" do
|
||||
Dir["test/files/avif/*.avif"].each do |file|
|
||||
assert_equal(:avif, MediaFile.open(file).file_ext)
|
||||
end
|
||||
end
|
||||
|
||||
should "determine the correct extension for a webm file" do
|
||||
assert_equal(:webm, MediaFile.open("test/files/test-512x512.webm").file_ext)
|
||||
end
|
||||
@@ -127,6 +137,7 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
assert_equal([150, 101], MediaFile.open("test/files/test.jpg").preview(150, 150).dimensions)
|
||||
assert_equal([113, 150], MediaFile.open("test/files/test.png").preview(150, 150).dimensions)
|
||||
assert_equal([150, 150], MediaFile.open("test/files/test.gif").preview(150, 150).dimensions)
|
||||
assert_equal([150, 63], MediaFile.open("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv420_limited_qp40.avif").preview(150, 150).dimensions)
|
||||
end
|
||||
|
||||
should "generate a preview image for an animated image" do
|
||||
@@ -294,6 +305,72 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
context "an AVIF file" do
|
||||
should "be able to read AVIF files" do
|
||||
Dir["test/files/avif/*.avif"].each do |file|
|
||||
assert_nothing_raised { MediaFile.open(file).attributes }
|
||||
end
|
||||
end
|
||||
|
||||
should "detect supported files" do
|
||||
assert_equal(true, MediaFile.open("test/files/avif/paris_icc_exif_xmp.avif").is_supported?)
|
||||
assert_equal(true, MediaFile.open("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv420_limited_qp40.avif").is_supported?)
|
||||
assert_equal(true, MediaFile.open("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv444_full_qp40.avif").is_supported?)
|
||||
assert_equal(true, MediaFile.open("test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif").is_supported?)
|
||||
assert_equal(true, MediaFile.open("test/files/avif/tiger_3layer_1res.avif").is_supported?)
|
||||
end
|
||||
|
||||
should "detect unsupported files" do
|
||||
assert_equal(false, MediaFile.open("test/files/avif/Image grid example.avif").is_supported?)
|
||||
assert_equal(false, MediaFile.open("test/files/avif/kimono.crop.avif").is_supported?)
|
||||
assert_equal(false, MediaFile.open("test/files/avif/kimono.rotate90.avif").is_supported?)
|
||||
assert_equal(false, MediaFile.open("test/files/avif/sequence-with-pitm-avif-major.avif").is_supported?)
|
||||
assert_equal(false, MediaFile.open("test/files/avif/sequence-with-pitm.avif").is_supported?)
|
||||
assert_equal(false, MediaFile.open("test/files/avif/sequence-without-pitm.avif").is_supported?)
|
||||
assert_equal(false, MediaFile.open("test/files/avif/star-8bpc.avif").is_supported?)
|
||||
|
||||
# XXX These should be unsupported, but aren't.
|
||||
# assert_equal(false, MediaFile.open("test/files/avif/alpha_video.avif").is_supported?)
|
||||
# assert_equal(false, MediaFile.open("test/files/avif/plum-blossom-small-profile0.8bpc.yuv420.alpha-full.avif").is_supported?)
|
||||
# assert_equal(false, MediaFile.open("test/files/avif/kimono.mirror-horizontal.avif").is_supported?)
|
||||
end
|
||||
|
||||
should "detect animated files" do
|
||||
assert_equal(true, MediaFile.open("test/files/avif/sequence-with-pitm.avif").is_animated?)
|
||||
assert_equal(true, MediaFile.open("test/files/avif/sequence-without-pitm.avif").is_animated?)
|
||||
assert_equal(true, MediaFile.open("test/files/avif/alpha_video.avif").is_animated?)
|
||||
assert_equal(true, MediaFile.open("test/files/avif/star-8bpc.avif").is_animated?)
|
||||
|
||||
assert_equal(48, MediaFile.open("test/files/avif/sequence-with-pitm.avif").frame_count)
|
||||
assert_equal(95, MediaFile.open("test/files/avif/sequence-without-pitm.avif").frame_count)
|
||||
assert_equal(48, MediaFile.open("test/files/avif/alpha_video.avif").frame_count)
|
||||
assert_equal(5, MediaFile.open("test/files/avif/star-8bpc.avif").frame_count)
|
||||
end
|
||||
|
||||
should "detect static images with an auxiliary image sequence" do
|
||||
assert_equal(true, MediaFile.open("test/files/avif/sequence-with-pitm-avif-major.avif").metadata.is_animated_avif?)
|
||||
assert_equal(false, MediaFile.open("test/files/avif/sequence-with-pitm-avif-major.avif").is_animated?)
|
||||
assert_equal(1, MediaFile.open("test/files/avif/sequence-with-pitm-avif-major.avif").frame_count)
|
||||
end
|
||||
|
||||
should "detect rotated images" do
|
||||
assert_equal(true, MediaFile.open("test/files/avif/kimono.rotate90.avif").metadata.is_rotated?)
|
||||
end
|
||||
|
||||
should "detect monochrome images" do
|
||||
assert_equal(true, MediaFile.open("test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif").metadata.is_greyscale?)
|
||||
end
|
||||
|
||||
should "be able to generate a preview" do
|
||||
assert_equal([180, 75], MediaFile.open("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv420_limited_qp40.avif").preview(180, 180).dimensions)
|
||||
assert_equal([180, 75], MediaFile.open("test/files/avif/hdr_cosmos01000_cicp9-16-9_yuv444_full_qp40.avif").preview(180, 180).dimensions)
|
||||
assert_equal([180, 135], MediaFile.open("test/files/avif/paris_icc_exif_xmp.avif").preview(180, 180).dimensions)
|
||||
assert_equal([180, 180], MediaFile.open("test/files/avif/Image grid example.avif").preview(180, 180).dimensions)
|
||||
assert_equal([180, 120], MediaFile.open("test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif").preview(180, 180).dimensions)
|
||||
assert_equal([180, 123], MediaFile.open("test/files/avif/tiger_3layer_1res.avif").preview(180, 180).dimensions)
|
||||
end
|
||||
end
|
||||
|
||||
context "a corrupt GIF" do
|
||||
should "still read the metadata" do
|
||||
@file = MediaFile.open("test/files/test-corrupt.gif")
|
||||
|
||||
@@ -1217,12 +1217,19 @@ class PostTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
context "a greyscale image missing the greyscale tag" do
|
||||
should "automatically add the greyscale tag" do
|
||||
should "automatically add the greyscale tag for a monochrome JPEG file" do
|
||||
@media_asset = MediaAsset.upload!("test/files/test-grey-no-profile.jpg")
|
||||
@post.update!(md5: @media_asset.md5)
|
||||
@post.reload.update!(tag_string: "tagme")
|
||||
assert_equal("greyscale tagme", @post.tag_string)
|
||||
end
|
||||
|
||||
should "automatically add the greyscale tag for a monochrome AVIF file" do
|
||||
@media_asset = MediaAsset.upload!("test/files/avif/fox.profile0.8bpc.yuv420.monochrome.avif")
|
||||
@post.update!(md5: @media_asset.md5)
|
||||
@post.reload.update!(tag_string: "tagme")
|
||||
assert_equal("greyscale tagme", @post.tag_string)
|
||||
end
|
||||
end
|
||||
|
||||
context "an exif-rotated image missing the exif_rotation tag" do
|
||||
|
||||