diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 994a41d050..9d8fa2702d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,7 +9,7 @@ RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSI # [Optional] Uncomment this section to install additional OS packages. RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libpam-dev + && apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev # [Optional] Uncomment this line to install additional gems. RUN gem install foreman diff --git a/.github/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml index 3a6fba9402..3e232f134c 100644 --- a/.github/actions/setup-ruby/action.yml +++ b/.github/actions/setup-ruby/action.yml @@ -14,7 +14,7 @@ runs: shell: bash run: | sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev ${{ inputs.additional-system-dependencies }} + sudo apt-get install -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }} - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 2bfa59e6b1..5f2297381a 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -133,7 +133,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev + additional-system-dependencies: ffmpeg libpam-dev - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' @@ -148,6 +148,93 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + test-libvips: + name: Libvips tests + runs-on: ubuntu-24.04 + + needs: + - build + + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} + RAILS_ENV: test + ALLOW_NOPAM: true + PAM_ENABLED: true + PAM_DEFAULT_SERVICE: pam_test + PAM_CONTROLLED_SERVICE: pam_test_controlled + OIDC_ENABLED: true + OIDC_SCOPE: read + SAML_ENABLED: true + CAS_ENABLED: true + BUNDLE_WITH: 'pam_authentication test' + GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} + MASTODON_USE_LIBVIPS: true + + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.1' + - '3.2' + - '.ruby-version' + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: './' + name: ${{ github.sha }} + + - name: Expand archived asset artifacts + run: | + tar xvzf artifacts.tar.gz + + - name: Set up Ruby environment + uses: ./.github/actions/setup-ruby + with: + ruby-version: ${{ matrix.ruby-version}} + additional-system-dependencies: ffmpeg libpam-dev libyaml-dev + + - name: Load database schema + run: './bin/rails db:create db:schema:load db:seed' + + - run: bin/rspec --tag paperclip_processing + + - name: Upload coverage reports to Codecov + if: matrix.ruby-version == '.ruby-version' + uses: codecov/codecov-action@v4 + with: + files: coverage/lcov/mastodon.lcov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + test-e2e: name: End to End testing runs-on: ubuntu-latest @@ -209,7 +296,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + additional-system-dependencies: ffmpeg - name: Set up Javascript environment uses: ./.github/actions/setup-javascript @@ -329,7 +416,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + additional-system-dependencies: ffmpeg - name: Set up Javascript environment uses: ./.github/actions/setup-javascript diff --git a/Dockerfile b/Dockerfile index c90d5dc980..6d342db437 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,6 +43,8 @@ ENV \ # Apply Mastodon version information MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \ +# Enable libvips + MASTODON_USE_LIBVIPS=true \ # Apply Mastodon static files and YJIT options RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \ RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \ @@ -97,7 +99,7 @@ RUN \ curl \ ffmpeg \ file \ - imagemagick \ + libvips42 \ libjemalloc2 \ patchelf \ procps \ diff --git a/Gemfile b/Gemfile index d9de331827..ca32d0cca1 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 1.0', require: false gem 'kt-paperclip', '~> 7.2' gem 'md-paperclip-azure', '~> 2.2', require: false +gem 'ruby-vips', '~> 2.2', require: false gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' diff --git a/Gemfile.lock b/Gemfile.lock index b5192c925a..bf5340a5b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -763,6 +763,8 @@ GEM ruby-saml (1.16.0) nokogiri (>= 1.13.10) rexml + ruby-vips (2.2.1) + ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) rufus-scheduler (3.9.1) @@ -1023,6 +1025,7 @@ DEPENDENCIES rubocop-rspec ruby-prof ruby-progressbar (~> 1.13) + ruby-vips (~> 2.2) rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb index 97cdaf589e..9dd0d393f9 100644 --- a/app/lib/admin/metrics/dimension/software_versions_dimension.rb +++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb @@ -10,7 +10,7 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim protected def perform_query - [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version].compact + [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version].compact end def mastodon_version @@ -71,6 +71,17 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim nil end + def libvips_version + return unless Rails.configuration.x.use_vips + + { + key: 'libvips', + human_key: 'libvips', + value: Vips.version_string, + human_value: Vips.version_string, + } + end + def redis_info @redis_info ||= if redis.is_a?(Redis::Namespace) redis.redis.info diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb index f457f5822b..a83e178fc4 100644 --- a/app/models/concerns/attachmentable.rb +++ b/app/models/concerns/attachmentable.rb @@ -69,7 +69,7 @@ module Attachmentable original_extension = Paperclip::Interpolations.extension(attachment, :original) proper_extension = extensions_for_mime_type.first.to_s extension = extensions_for_mime_type.include?(original_extension) ? original_extension : proper_extension - extension = 'jpeg' if extension == 'jpe' + extension = 'jpeg' if ['jpe', 'jfif'].include?(extension) extension end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 11fdd9d88a..cbfc393786 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -57,7 +57,11 @@ class PreviewCard < ApplicationRecord has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy belongs_to :author_account, class_name: 'Account', optional: true - has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false + has_attached_file :image, + processors: [Rails.configuration.x.use_vips ? :lazy_thumbnail : :thumbnail, :blurhash_transcoder], + styles: ->(f) { image_styles(f) }, + convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, + validate_media_type: false validates :url, presence: true, uniqueness: true, url: true validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES diff --git a/config/application.rb b/config/application.rb index a8e313069d..069eb37740 100644 --- a/config/application.rb +++ b/config/application.rb @@ -27,7 +27,7 @@ require_relative '../lib/sanitize_ext/sanitize_config' require_relative '../lib/redis/namespace_extensions' require_relative '../lib/paperclip/url_generator_extensions' require_relative '../lib/paperclip/attachment_extensions' -require_relative '../lib/paperclip/lazy_thumbnail' + require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/media_type_spoof_detector_extensions' require_relative '../lib/paperclip/transcoder' @@ -100,6 +100,14 @@ module Mastodon config.before_configuration do require 'mastodon/redis_config' + + config.x.use_vips = ENV['MASTODON_USE_LIBVIPS'] == 'true' + + if config.x.use_vips + require_relative '../lib/paperclip/vips_lazy_thumbnail' + else + require_relative '../lib/paperclip/lazy_thumbnail' + end end config.to_prepare do diff --git a/config/initializers/vips.rb b/config/initializers/vips.rb new file mode 100644 index 0000000000..25a17b2a17 --- /dev/null +++ b/config/initializers/vips.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +if Rails.configuration.x.use_vips + ENV['VIPS_BLOCK_UNTRUSTED'] = 'true' + + require 'vips' + + abort('Incompatible libvips version, please install libvips >= 8.13') unless Vips.at_least_libvips?(8, 13) + + Vips.block('VipsForeign', true) + + %w( + VipsForeignLoadNsgif + VipsForeignLoadJpeg + VipsForeignLoadPng + VipsForeignLoadWebp + VipsForeignLoadHeif + VipsForeignSavePng + VipsForeignSaveSpng + VipsForeignSaveJpeg + VipsForeignSaveWebp + ).each do |operation| + Vips.block(operation, false) + end + + Vips.block_untrusted(true) +end diff --git a/lib/paperclip/blurhash_transcoder.rb b/lib/paperclip/blurhash_transcoder.rb index c22c20c57a..e9cecef50c 100644 --- a/lib/paperclip/blurhash_transcoder.rb +++ b/lib/paperclip/blurhash_transcoder.rb @@ -5,12 +5,26 @@ module Paperclip def make return @file unless options[:style] == :small || options[:blurhash] - pixels = convert(':source -depth 8 RGB:-', source: File.expand_path(@file.path)).unpack('C*') - geometry = options.fetch(:file_geometry_parser).from_file(@file) + width, height, data = blurhash_params + # Guard against segfaults if data has unexpected size + raise RangeError("Invalid image data size (expected #{width * height * 3}, got #{data.size})") if data.size != width * height * 3 # TODO: should probably be another exception type - attachment.instance.blurhash = Blurhash.encode(geometry.width, geometry.height, pixels, **(options[:blurhash] || {})) + attachment.instance.blurhash = Blurhash.encode(width, height, data, **(options[:blurhash] || {})) @file end + + private + + def blurhash_params + if Rails.configuration.x.use_vips + image = Vips::Image.thumbnail(@file.path, 100) + [image.width, image.height, image.colourspace(:srgb).extract_band(0, n: 3).to_a.flatten] + else + pixels = convert(':source -depth 8 RGB:-', source: File.expand_path(@file.path)).unpack('C*') + geometry = options.fetch(:file_geometry_parser).from_file(@file) + [geometry.width, geometry.height, pixels] + end + end end end diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb index d2f7e7c602..b5992f90bc 100644 --- a/lib/paperclip/color_extractor.rb +++ b/lib/paperclip/color_extractor.rb @@ -7,15 +7,10 @@ module Paperclip MIN_CONTRAST = 3.0 ACCENT_MIN_CONTRAST = 2.0 FREQUENCY_THRESHOLD = 0.01 + BINS = 10 def make - depth = 8 - - # Determine background palette by getting colors close to the image's edge only - background_palette = palette_from_histogram(convert(':source -alpha set -gravity Center -region 75%x75% -fill None -colorize 100% -alpha transparent +region -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) - - # Determine foreground palette from the whole image - foreground_palette = palette_from_histogram(convert(':source -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) + background_palette, foreground_palette = Rails.configuration.x.use_vips ? palettes_from_libvips : palettes_from_imagemagick background_color = background_palette.first || foreground_palette.first foreground_colors = [] @@ -78,6 +73,75 @@ module Paperclip private + def palettes_from_libvips + image = downscaled_image + block_edge_dim = (image.height * 0.25).floor + line_edge_dim = (image.width * 0.25).floor + + edge_image = begin + top = image.crop(0, 0, image.width, block_edge_dim) + bottom = image.crop(0, image.height - block_edge_dim, image.width, block_edge_dim) + left = image.crop(0, block_edge_dim, line_edge_dim, image.height - (block_edge_dim * 2)) + right = image.crop(image.width - line_edge_dim, block_edge_dim, line_edge_dim, image.height - (block_edge_dim * 2)) + top.join(bottom, :vertical).join(left, :horizontal).join(right, :horizontal) + end + + background_palette = palette_from_image(edge_image) + foreground_palette = palette_from_image(image) + [background_palette, foreground_palette] + end + + def palettes_from_imagemagick + depth = 8 + + # Determine background palette by getting colors close to the image's edge only + background_palette = palette_from_im_histogram(convert(':source -alpha set -gravity Center -region 75%x75% -fill None -colorize 100% -alpha transparent +region -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) + + # Determine foreground palette from the whole image + foreground_palette = palette_from_im_histogram(convert(':source -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) + [background_palette, foreground_palette] + end + + def downscaled_image + image = Vips::Image.new_from_file(@file.path, access: :random).thumbnail_image(100) + + image.colourspace(:srgb).extract_band(0, n: 3) + end + + def palette_from_image(image) + # `hist_find_ndim` will create a BINS×BINS×BINS 3D histogram of the image + # represented as an image of size BINS×BINS with `BINS` bands. + # The number of occurrences of a color (r, g, b) is thus encoded in band `b` at pixel position `(r, g)` + histogram = image.hist_find_ndim(bins: BINS) + + # `histogram.max` returns an array of maxima with their pixel positions, but we don't know in which + # band they are + _, colors = histogram.max(size: 10, out_array: true, x_array: true, y_array: true) + + colors['out_array'].zip(colors['x_array'], colors['y_array']).map do |v, x, y| + rgb_from_xyv(histogram, x, y, v) + end.reverse + end + + # rubocop:disable Naming/MethodParameterName + def rgb_from_xyv(image, x, y, v) + pixel = image.getpoint(x, y) + + # Unfortunately, we only have the first 2 dimensions, so try to + # guess the third one by looking up the value + + # NOTE: this means that if multiple bins with the same `r` and `g` + # components have the same number of occurrences, we will always return + # the one with the lowest `b` value. This means that in case of a tie, + # we will return the same color twice and skip the ones it tied with. + z = pixel.find_index(v) + + r = (x + 0.5) * 256 / BINS + g = (y + 0.5) * 256 / BINS + b = (z + 0.5) * 256 / BINS + ColorDiff::Color::RGB.new(r, g, b) + end + def w3c_contrast(color1, color2) luminance1 = (color1.to_xyz.y * 0.01) + 0.05 luminance2 = (color2.to_xyz.y * 0.01) + 0.05 @@ -89,7 +153,6 @@ module Paperclip end end - # rubocop:disable Naming/MethodParameterName def rgb_to_hsl(r, g, b) r /= 255.0 g /= 255.0 @@ -170,7 +233,7 @@ module Paperclip ColorDiff::Color::RGB.new(*hsl_to_rgb(hue, saturation, light)) end - def palette_from_histogram(result, quantity) + def palette_from_im_histogram(result, quantity) frequencies = result.scan(/([0-9]+):/).flatten.map(&:to_f) hex_values = result.scan(/\#([0-9A-Fa-f]{6,8})/).flatten total_frequencies = frequencies.sum.to_f diff --git a/lib/paperclip/vips_lazy_thumbnail.rb b/lib/paperclip/vips_lazy_thumbnail.rb new file mode 100644 index 0000000000..06d99bf79d --- /dev/null +++ b/lib/paperclip/vips_lazy_thumbnail.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +module Paperclip + class LazyThumbnail < Paperclip::Processor + GIF_MAX_FPS = 60 + GIF_MAX_FRAMES = 3000 + GIF_PALETTE_COLORS = 32 + + ALLOWED_FIELDS = %w( + icc-profile-data + ).freeze + + class PixelGeometryParser + def self.parse(current_geometry, pixels) + width = Math.sqrt(pixels * (current_geometry.width.to_f / current_geometry.height)).round.to_i + height = Math.sqrt(pixels * (current_geometry.height.to_f / current_geometry.width)).round.to_i + + Paperclip::Geometry.new(width, height) + end + end + + def initialize(file, options = {}, attachment = nil) + super + + @crop = options[:geometry].to_s[-1, 1] == '#' + @current_geometry = options.fetch(:file_geometry_parser, Geometry).from_file(@file) + @target_geometry = options[:pixels] ? PixelGeometryParser.parse(@current_geometry, options[:pixels]) : options.fetch(:string_geometry_parser, Geometry).parse(options[:geometry].to_s) + @format = options[:format] + @current_format = File.extname(@file.path) + @basename = File.basename(@file.path, @current_format) + + correct_current_format! + end + + def make + return File.open(@file.path) unless needs_convert? + + dst = TempfileFactory.new.generate([@basename, @format ? ".#{@format}" : @current_format].join) + + if preserve_animation? + if @target_geometry.nil? || (@current_geometry.width <= @target_geometry.width && @current_geometry.height <= @target_geometry.height) + target_width = 'iw' + target_height = 'ih' + else + scale = [@target_geometry.width.to_f / @current_geometry.width, @target_geometry.height.to_f / @current_geometry.height].min + target_width = (@current_geometry.width * scale).round + target_height = (@current_geometry.height * scale).round + end + + # The only situation where we use crop on GIFs is cropping them to a square + # aspect ratio, such as for avatars, so this is the only special case we + # implement. If cropping ever becomes necessary for other situations, this will + # need to be expanded. + crop_width = crop_height = [target_width, target_height].min if @target_geometry&.square? + + filter = begin + if @crop + "scale=#{target_width}:#{target_height}:force_original_aspect_ratio=increase,crop=#{crop_width}:#{crop_height}" + else + "scale=#{target_width}:#{target_height}:force_original_aspect_ratio=decrease" + end + end + + command = Terrapin::CommandLine.new(Rails.configuration.x.ffmpeg_binary, '-nostdin -i :source -map_metadata -1 -fpsmax :max_fps -frames:v :max_frames -filter_complex :filter -y :destination', logger: Paperclip.logger) + command.run({ source: @file.path, filter: "#{filter},split[a][b];[a]palettegen=max_colors=#{GIF_PALETTE_COLORS}[p];[b][p]paletteuse=dither=bayer", max_fps: GIF_MAX_FPS, max_frames: GIF_MAX_FRAMES, destination: dst.path }) + else + transformed_image.write_to_file(dst.path, **save_options) + end + + dst + rescue Terrapin::ExitStatusError => e + raise Paperclip::Error, "Error while optimizing #{@basename}: #{e}" + rescue Terrapin::CommandNotFoundError + raise Paperclip::Errors::CommandNotFoundError, 'Could not run the `ffmpeg` command. Please install ffmpeg.' + end + + private + + def correct_current_format! + # If the attachment was uploaded through a base64 payload, the tempfile + # will not have a file extension. It could also have the wrong file extension, + # depending on what the uploaded file was named. We correct for this in the final + # file name, which is however not yet physically in place on the temp file, so we + # need to use it here. Mind that this only reliably works if this processor is + # the first in line and we're working with the original, unmodified file. + @current_format = File.extname(attachment.instance_read(:file_name)) + end + + def transformed_image + # libvips has some optimizations for resizing an image on load. If we don't need to + # resize the image, we have to load it a different way. + if @target_geometry.nil? + Vips::Image.new_from_file(preserve_animation? ? "#{@file.path}[n=-1]" : @file.path, access: :sequential).copy.mutate do |mutable| + (mutable.get_fields - ALLOWED_FIELDS).each do |field| + mutable.remove!(field) + end + end + else + Vips::Image.thumbnail(@file.path, @target_geometry.width, height: @target_geometry.height, **thumbnail_options).mutate do |mutable| + (mutable.get_fields - ALLOWED_FIELDS).each do |field| + mutable.remove!(field) + end + end + end + end + + def thumbnail_options + @crop ? { crop: :centre } : { size: :down } + end + + def save_options + case @format + when 'jpg' + { Q: 90, interlace: true } + else + {} + end + end + + def preserve_animation? + @format == 'gif' || (@format.blank? && @current_format == '.gif') + end + + def needs_convert? + needs_different_geometry? || needs_different_format? || needs_metadata_stripping? + end + + def needs_different_geometry? + (options[:geometry] && @current_geometry.width != @target_geometry.width && @current_geometry.height != @target_geometry.height) || + (options[:pixels] && @current_geometry.width * @current_geometry.height > options[:pixels]) + end + + def needs_different_format? + @format.present? && @current_format != ".#{@format}" + end + + def needs_metadata_stripping? + @attachment.instance.respond_to?(:local?) && @attachment.instance.local? + end + end +end diff --git a/spec/fixtures/files/monochrome.png b/spec/fixtures/files/monochrome.png new file mode 100644 index 0000000000..fa36101cad Binary files /dev/null and b/spec/fixtures/files/monochrome.png differ diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 1b9a13c38c..221645ac5a 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -139,6 +139,12 @@ RSpec.describe MediaAttachment, :paperclip_processing do it_behaves_like 'static 600x400 image', 'image/png', '.png' end + describe 'monochrome jpg' do + let(:media) { Fabricate(:media_attachment, file: attachment_fixture('monochrome.png')) } + + it_behaves_like 'static 600x400 image', 'image/png', '.png' + end + describe 'webp' do let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.webp')) } @@ -203,7 +209,9 @@ RSpec.describe MediaAttachment, :paperclip_processing do expect(media.type).to eq 'audio' expect(media.file.meta['original']['duration']).to be_within(0.05).of(0.235102) expect(media.thumbnail.present?).to be true - expect(media.file.meta['colors']['background']).to eq '#3088d4' + + # NOTE: Our libvips and ImageMagick implementations currently have different results + expect(media.file.meta['colors']['background']).to eq(ENV['MASTODON_USE_LIBVIPS'] ? '#268cd9' : '#3088d4') expect(media.file_file_name).to_not eq 'boop.ogg' end end