Fix #5065: .webp images upload support
Add ability to upload .webp images. Animated WebP images aren't supported. This is because they aren't supported by FFmpeg yet[1], so generating thumbnails and samples for them would be more complicated than for other formats. [1]: https://trac.ffmpeg.org/ticket/4907
@@ -48,7 +48,7 @@ class ExifTool
|
||||
end
|
||||
|
||||
def is_animated?
|
||||
frame_count.to_i > 1 || is_animated_avif?
|
||||
frame_count.to_i > 1 || is_animated_webp? || is_animated_avif?
|
||||
end
|
||||
|
||||
def is_animated_gif?
|
||||
@@ -59,6 +59,10 @@ class ExifTool
|
||||
file_ext == :png && is_animated?
|
||||
end
|
||||
|
||||
def is_animated_webp?
|
||||
file_ext == :webp && metadata["RIFF:Duration"].present?
|
||||
end
|
||||
|
||||
def is_animated_avif?
|
||||
file_ext == :avif && metadata["QuickTime:CompatibleBrands"].to_a.include?("avis")
|
||||
end
|
||||
@@ -123,8 +127,10 @@ class ExifTool
|
||||
def loop_count
|
||||
return Float::INFINITY if metadata["GIF:AnimationIterations"] == "Infinite"
|
||||
return Float::INFINITY if metadata["PNG:AnimationPlays"] == "inf"
|
||||
return Float::INFINITY if metadata["RIFF:AnimationLoopCount"] == "inf"
|
||||
return metadata["GIF:AnimationIterations"] if has_key?("GIF:AnimationIterations")
|
||||
return metadata["PNG:AnimationPlays"] if has_key?("PNG:AnimationPlays")
|
||||
return metadata["RIFF:AnimationLoopCount"] if has_key?("RIFF:AnimationLoopCount")
|
||||
|
||||
# If the AnimationIterations tag isn't present, then it's counted as a loop count of 0.
|
||||
return 0 if is_animated_gif? && !has_key?("GIF:AnimationIterations")
|
||||
@@ -153,6 +159,8 @@ class ExifTool
|
||||
:avif
|
||||
elsif has_key?("QuickTime:MovieHeaderVersion")
|
||||
:mp4
|
||||
elsif keys.grep(/\ARIFF:/).any?
|
||||
:webp
|
||||
elsif has_key?("Matroska:DocType")
|
||||
:webm
|
||||
elsif has_key?("Flash:FlashVersion")
|
||||
|
||||
@@ -26,7 +26,7 @@ class MediaFile
|
||||
file = Kernel.open(file, "r", binmode: true) unless file.respond_to?(:read)
|
||||
|
||||
case file_ext(file)
|
||||
when :jpg, :gif, :png, :avif
|
||||
when :jpg, :gif, :png, :webp, :avif
|
||||
MediaFile::Image.new(file, **options)
|
||||
when :swf
|
||||
MediaFile::Flash.new(file, **options)
|
||||
@@ -62,6 +62,10 @@ class MediaFile
|
||||
when /\A\x1a\x45\xdf\xa3/n
|
||||
:webm
|
||||
|
||||
# https://developers.google.com/speed/webp/docs/riff_container
|
||||
when /\ARIFF....WEBP/
|
||||
:webp
|
||||
|
||||
# https://www.ftyps.com
|
||||
# isom (common) - MP4 Base Media v1 [IS0 14496-12:2003]
|
||||
# mp42 (common) - MP4 v2 [ISO 14496-14]
|
||||
@@ -144,7 +148,7 @@ class MediaFile
|
||||
|
||||
# @return [Boolean] true if the file is an image
|
||||
def is_image?
|
||||
file_ext.in?([:jpg, :png, :gif, :avif])
|
||||
file_ext.in?(%i[jpg png gif webp avif])
|
||||
end
|
||||
|
||||
# @return [Boolean] true if the file is a video
|
||||
|
||||
@@ -16,6 +16,8 @@ class MediaFile::Image < MediaFile
|
||||
when :avif
|
||||
# XXX Mirrored AVIFs should be unsupported too, but we currently can't detect the mirrored flag using exiftool or ffprobe.
|
||||
!metadata.is_rotated? && !metadata.is_cropped? && !metadata.is_grid_image? && !metadata.is_animated_avif?
|
||||
when :webp
|
||||
!is_animated?
|
||||
else
|
||||
true
|
||||
end
|
||||
@@ -34,11 +36,12 @@ class MediaFile::Image < MediaFile
|
||||
end
|
||||
|
||||
def frame_count
|
||||
if file_ext == :gif
|
||||
image.get("n-pages")
|
||||
elsif file_ext == :png
|
||||
case file_ext
|
||||
when :gif, :webp
|
||||
image.get("n-pages") if image.get_fields.include?("n-pages")
|
||||
when :png
|
||||
metadata.fetch("PNG:AnimationFrames", 1)
|
||||
elsif file_ext == :avif
|
||||
when :avif
|
||||
video.frame_count
|
||||
else
|
||||
nil
|
||||
@@ -124,6 +127,10 @@ class MediaFile::Image < MediaFile
|
||||
file_ext == :png && is_animated?
|
||||
end
|
||||
|
||||
def is_animated_webp?
|
||||
file_ext == :webp && is_animated?
|
||||
end
|
||||
|
||||
def is_animated_avif?
|
||||
file_ext == :avif && is_animated?
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class MediaAsset < ApplicationRecord
|
||||
class Error < StandardError; end
|
||||
|
||||
FILE_TYPES = %w[jpg png gif avif mp4 webm swf zip]
|
||||
FILE_TYPES = %w[jpg png gif webp avif mp4 webm swf zip]
|
||||
FILE_KEY_LENGTH = 9
|
||||
VARIANTS = %i[preview 180x180 360x360 720x720 sample original]
|
||||
MAX_FILE_SIZE = Danbooru.config.max_file_size.to_i
|
||||
@@ -372,7 +372,7 @@ class MediaAsset < ApplicationRecord
|
||||
|
||||
concerning :FileTypeMethods do
|
||||
def is_image?
|
||||
file_ext.in?(%w[jpg png gif avif])
|
||||
file_ext.in?(%w[jpg png gif webp avif])
|
||||
end
|
||||
|
||||
def is_static_image?
|
||||
|
||||
@@ -188,7 +188,7 @@ class Post < ApplicationRecord
|
||||
end
|
||||
|
||||
def is_image?
|
||||
file_ext =~ /jpg|gif|png|avif/i
|
||||
file_ext =~ /jpg|gif|png|webp|avif/i
|
||||
end
|
||||
|
||||
def is_flash?
|
||||
|
||||
BIN
test/files/webp/2_webp_a.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
test/files/webp/2_webp_ll.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
test/files/webp/Exif2.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
6
test/files/webp/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Test file sources:
|
||||
|
||||
* https://github.com/webmproject/libwebp-test-data
|
||||
* https://developers.google.com/speed/webp/gallery1
|
||||
* https://zpl.fi/exif-orientation-in-different-formats/
|
||||
* https://git.zpl.fi/exif-orientation/about/
|
||||
BIN
test/files/webp/fjord.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
test/files/webp/lossless1.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
test/files/webp/lossy_alpha1.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
test/files/webp/nyancat.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
test/files/webp/test.webp
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
@@ -197,6 +197,13 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
context "for an unsupported WebP file" do
|
||||
should "fail for an animated WebP" do
|
||||
create_upload!("test/files/webp/nyancat.webp", user: @user)
|
||||
assert_match("File type is not supported", Upload.last.error)
|
||||
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)
|
||||
@@ -309,6 +316,9 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
|
||||
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")
|
||||
|
||||
should_upload_successfully("test/files/webp/test.webp")
|
||||
should_upload_successfully("test/files/webp/fjord.webp")
|
||||
end
|
||||
|
||||
context "uploading multiple files from your computer" do
|
||||
|
||||
@@ -170,7 +170,7 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
|
||||
assert_autocomplete_equals(["filetype:gif"], "filetype:g", :tag_query)
|
||||
assert_autocomplete_equals(["filetype:swf"], "filetype:s", :tag_query)
|
||||
assert_autocomplete_equals(["filetype:zip"], "filetype:z", :tag_query)
|
||||
assert_autocomplete_equals(["filetype:webm"], "filetype:w", :tag_query)
|
||||
assert_autocomplete_equals(["filetype:webm", "filetype:webp"], "filetype:w", :tag_query)
|
||||
assert_autocomplete_equals(["filetype:mp4"], "filetype:m", :tag_query)
|
||||
|
||||
assert_autocomplete_equals(["commentary:true"], "commentary:tru", :tag_query)
|
||||
|
||||
@@ -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 a WebP file" do
|
||||
assert_equal([550, 368], MediaFile.open("test/files/webp/fjord.webp").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
|
||||
@@ -89,6 +93,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 a WebP file" do
|
||||
Dir["test/files/webp/*.webp"].each do |file|
|
||||
assert_equal(:webp, MediaFile.open(file).file_ext)
|
||||
end
|
||||
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)
|
||||
@@ -137,6 +147,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, 100], MediaFile.open("test/files/webp/fjord.webp").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
|
||||
|
||||
@@ -311,6 +322,40 @@ class MediaFileTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
context "a WebP file" do
|
||||
should "be able to read WebP files" do
|
||||
Dir["test/files/webp/*.webp"].each do |file|
|
||||
assert_nothing_raised { MediaFile.open(file).attributes }
|
||||
end
|
||||
end
|
||||
|
||||
should "detect animated files" do
|
||||
assert_equal(true, MediaFile.open("test/files/webp/nyancat.webp").is_animated?)
|
||||
assert_equal(true, MediaFile.open("test/files/webp/nyancat.webp").is_animated_webp?)
|
||||
assert_equal(true, MediaFile.open("test/files/webp/nyancat.webp").metadata.is_animated?)
|
||||
assert_equal(false, MediaFile.open("test/files/webp/nyancat.webp").is_supported?)
|
||||
assert_equal(12, MediaFile.open("test/files/webp/nyancat.webp").frame_count)
|
||||
assert_equal(Float::INFINITY, MediaFile.open("test/files/webp/nyancat.webp").metadata.loop_count)
|
||||
|
||||
# assert_equal(0.84, MediaFile.open("test/files/webp/nyancat.webp").duration)
|
||||
end
|
||||
|
||||
should "be able to generate a preview" do
|
||||
assert_equal([128, 128], MediaFile.open("test/files/webp/test.webp").preview(180, 180).dimensions)
|
||||
assert_equal([176, 180], MediaFile.open("test/files/webp/2_webp_a.webp").preview(180, 180).dimensions)
|
||||
assert_equal([176, 180], MediaFile.open("test/files/webp/2_webp_ll.webp").preview(180, 180).dimensions)
|
||||
assert_equal([180, 120], MediaFile.open("test/files/webp/Exif2.webp").preview(180, 180).dimensions)
|
||||
assert_equal([180, 120], MediaFile.open("test/files/webp/fjord.webp").preview(180, 180).dimensions)
|
||||
assert_equal([180, 55], MediaFile.open("test/files/webp/lossless1.webp").preview(180, 180).dimensions)
|
||||
assert_equal([180, 55], MediaFile.open("test/files/webp/lossy_alpha1.webp").preview(180, 180).dimensions)
|
||||
end
|
||||
|
||||
should "ignore EXIF orientation tags" do
|
||||
# XXX It's possible for .webp files to contain the IFD0:Orientation tag, but browsers currently ignore it, so we do too.
|
||||
assert_equal(false, MediaFile.open("test/files/webp/Exif2.webp").metadata.is_rotated?)
|
||||
end
|
||||
end
|
||||
|
||||
context "an AVIF file" do
|
||||
should "be able to read AVIF files" do
|
||||
Dir["test/files/avif/*.avif"].each do |file|
|
||||
|
||||