diff --git a/app/models/media_asset.rb b/app/models/media_asset.rb index 4f0b65f24..b9e9c7ca2 100644 --- a/app/models/media_asset.rb +++ b/app/models/media_asset.rb @@ -30,4 +30,48 @@ class MediaAsset < ApplicationRecord def is_animated_png? file_ext == "png" && metadata.fetch("PNG:AnimationFrames", 1) > 1 end -end \ No newline at end of file + + # @see https://exiftool.org/TagNames/JPEG.html + # @see https://exiftool.org/TagNames/PNG.html + # @see https://danbooru.donmai.us/posts?tags=exif:File:ColorComponents=1 + # @see https://danbooru.donmai.us/posts?tags=exif:PNG:ColorType=Grayscale + def is_greyscale? + metadata["File:ColorComponents"] == 1 || + metadata["PNG:ColorType"] == "Grayscale" || + metadata["PNG:ColorType"] == "Grayscale with Alpha" + + # Not always accurate: + # metadata["ICC-header:ColorSpaceData"] == "GRAY" || + # metadata["XMP-photoshop:ColorMode"] == "Grayscale" || + # metadata["XMP-photoshop:ICCProfileName"] == "EPSON Gray - Gamma 2.2" || + # metadata["XMP-photoshop:ICCProfileName"] == "Gray Gamma 2.2" + end + + # https://exiftool.org/TagNames/EXIF.html + def is_rotated? + metadata["IFD0:Orientation"].in?(["Rotate 90 CW", "Rotate 270 CW", "Rotate 180"]) || + metadata["IFD1:Orientation"].in?(["Rotate 90 CW", "Rotate 270 CW", "Rotate 180"]) + end + + # Some animations technically have a finite loop count, but loop for hundreds + # or thousands of times. Only count animations with a low loop count as non-repeating. + def is_non_repeating_animation? + loop_count.in?(0..10) + end + + # @see https://exiftool.org/TagNames/GIF.html + # @see https://exiftool.org/TagNames/PNG.html + # @see https://danbooru.donmai.us/posts?tags=-exif:GIF:AnimationIterations=Infinite+animated_gif + # @see https://danbooru.donmai.us/posts?tags=-exif:PNG:AnimationPlays=inf+animated_png + def loop_count + return Float::INFINITY if metadata["GIF:AnimationIterations"] == "Infinite" + return Float::INFINITY if metadata["PNG:AnimationPlays"] == "inf" + return metadata["GIF:AnimationIterations"] if metadata["GIF:AnimationIterations"].present? + return metadata["PNG:AnimationPlays"] if metadata["PNG:AnimationPlays"].present? + + # If the AnimationIterations tag isn't present, then it's counted as a loop count of 0. + return 0 if is_animated_gif? && metadata["GIF:AnimationIterations"].nil? + + nil + end +end diff --git a/app/models/post.rb b/app/models/post.rb index ff124de70..4b70f75fb 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -498,7 +498,7 @@ class Post < ApplicationRecord end def add_automatic_tags(tags) - tags -= %w[incredibly_absurdres absurdres highres lowres huge_filesize flash video ugoira animated_gif animated_png] + tags -= %w[incredibly_absurdres absurdres highres lowres huge_filesize flash video ugoira animated_gif animated_png exif_rotation non-repeating_animation] if image_width >= 10_000 || image_height >= 10_000 tags << "incredibly_absurdres" @@ -543,6 +543,10 @@ class Post < ApplicationRecord tags << "animated_gif" if media_asset.is_animated_gif? tags << "animated_png" if media_asset.is_animated_png? + tags << "greyscale" if media_asset.is_greyscale? + tags << "exif_rotation" if media_asset.is_rotated? + tags << "non-repeating_animation" if media_asset.is_non_repeating_animation? + tags end diff --git a/test/files/test-animated-86x52-loop-1.gif b/test/files/test-animated-86x52-loop-1.gif new file mode 100644 index 000000000..966d3a6af Binary files /dev/null and b/test/files/test-animated-86x52-loop-1.gif differ diff --git a/test/files/test-animated-86x52-loop-2.gif b/test/files/test-animated-86x52-loop-2.gif new file mode 100644 index 000000000..6a79df302 Binary files /dev/null and b/test/files/test-animated-86x52-loop-2.gif differ diff --git a/test/files/test-rotation-90cw.jpg b/test/files/test-rotation-90cw.jpg new file mode 100644 index 000000000..fc2b837c4 Binary files /dev/null and b/test/files/test-rotation-90cw.jpg differ diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index b9548cf3c..44502e4f2 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -1318,6 +1318,38 @@ class PostTest < ActiveSupport::TestCase end end + context "a greyscale image missing the greyscale tag" do + should "automatically add the greyscale tag" do + @media_asset = MediaAsset.create!(file: "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 + end + + context "an exif-rotated image missing the exif_rotation tag" do + should "automatically add the exif_rotation tag" do + @media_asset = MediaAsset.create!(file: "test/files/test-rotation-90cw.jpg") + @post.update!(md5: @media_asset.md5) + @post.reload.update!(tag_string: "tagme") + assert_equal("exif_rotation tagme", @post.tag_string) + end + end + + context "a non-repeating GIF missing the non-repeating_animation tag" do + should "automatically add the non-repeating_animation tag" do + @media_asset = MediaAsset.create!(file: "test/files/test-animated-86x52-loop-1.gif") + @post.update!(md5: @media_asset.md5) + @post.reload.update!(tag_string: "tagme") + assert_equal("animated animated_gif non-repeating_animation tagme", @post.tag_string) + + @media_asset = MediaAsset.create!(file: "test/files/test-animated-86x52-loop-2.gif") + @post.update!(md5: @media_asset.md5) + @post.reload.update!(tag_string: "tagme") + assert_equal("animated animated_gif non-repeating_animation tagme", @post.tag_string) + end + end + should "have an array representation of its tags" do post = FactoryBot.create(:post) post.reload