mirror of https://github.com/tootsuite/mastodon
Change image processing from ImageMagick to libvips
parent
4ef0b48b95
commit
b2407c3450
1
Gemfile
1
Gemfile
|
@ -21,6 +21,7 @@ gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||||
gem 'blurhash', '~> 0.1'
|
gem 'blurhash', '~> 0.1'
|
||||||
gem 'fog-core', '<= 2.4.0'
|
gem 'fog-core', '<= 2.4.0'
|
||||||
gem 'fog-openstack', '~> 1.0', require: false
|
gem 'fog-openstack', '~> 1.0', require: false
|
||||||
|
gem 'image_processing', '~> 1.0'
|
||||||
gem 'kt-paperclip', '~> 7.2'
|
gem 'kt-paperclip', '~> 7.2'
|
||||||
gem 'md-paperclip-azure', '~> 2.2', require: false
|
gem 'md-paperclip-azure', '~> 2.2', require: false
|
||||||
|
|
||||||
|
|
|
@ -346,6 +346,9 @@ GEM
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
idn-ruby (0.1.5)
|
idn-ruby (0.1.5)
|
||||||
|
image_processing (1.12.2)
|
||||||
|
mini_magick (>= 4.9.5, < 5)
|
||||||
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
inline_svg (1.9.0)
|
inline_svg (1.9.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
|
@ -432,6 +435,7 @@ GEM
|
||||||
mime-types (3.5.2)
|
mime-types (3.5.2)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2024.0305)
|
mime-types-data (3.2024.0305)
|
||||||
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.6)
|
mini_portile2 (2.8.6)
|
||||||
minitest (5.22.3)
|
minitest (5.22.3)
|
||||||
|
@ -681,6 +685,8 @@ GEM
|
||||||
ruby-saml (1.16.0)
|
ruby-saml (1.16.0)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
rexml
|
rexml
|
||||||
|
ruby-vips (2.2.1)
|
||||||
|
ffi (~> 1.12)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
rufus-scheduler (3.9.1)
|
rufus-scheduler (3.9.1)
|
||||||
|
@ -867,6 +873,7 @@ DEPENDENCIES
|
||||||
i18n (= 1.14.1)
|
i18n (= 1.14.1)
|
||||||
i18n-tasks (~> 1.0)
|
i18n-tasks (~> 1.0)
|
||||||
idn-ruby
|
idn-ruby
|
||||||
|
image_processing (~> 1.0)
|
||||||
inline_svg
|
inline_svg
|
||||||
irb (~> 1.8)
|
irb (~> 1.8)
|
||||||
json-ld
|
json-ld
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Account::Avatar
|
||||||
class_methods do
|
class_methods do
|
||||||
def avatar_styles(file)
|
def avatar_styles(file)
|
||||||
styles = { original: { geometry: '400x400#', file_geometry_parser: FastGeometryParser } }
|
styles = { original: { geometry: '400x400#', file_geometry_parser: FastGeometryParser } }
|
||||||
styles[:static] = { geometry: '400x400#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
|
styles[:static] = { geometry: '400x400#', format: 'png', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
|
||||||
styles
|
styles
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ module Account::Avatar
|
||||||
|
|
||||||
included do
|
included do
|
||||||
# Avatar upload
|
# Avatar upload
|
||||||
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '+profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, processors: [:lazy_thumbnail]
|
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, processors: [:lazy_thumbnail]
|
||||||
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
|
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
|
||||||
validates_attachment_size :avatar, less_than: LIMIT
|
validates_attachment_size :avatar, less_than: LIMIT
|
||||||
remotable_attachment :avatar, LIMIT, suppress_errors: false
|
remotable_attachment :avatar, LIMIT, suppress_errors: false
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Account::Header
|
||||||
class_methods do
|
class_methods do
|
||||||
def header_styles(file)
|
def header_styles(file)
|
||||||
styles = { original: { pixels: MAX_PIXELS, file_geometry_parser: FastGeometryParser } }
|
styles = { original: { pixels: MAX_PIXELS, file_geometry_parser: FastGeometryParser } }
|
||||||
styles[:static] = { format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
|
styles[:static] = { format: 'png', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
|
||||||
styles
|
styles
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ module Account::Header
|
||||||
|
|
||||||
included do
|
included do
|
||||||
# Header upload
|
# Header upload
|
||||||
has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '+profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, processors: [:lazy_thumbnail]
|
has_attached_file :header, styles: ->(f) { header_styles(f) }, processors: [:lazy_thumbnail]
|
||||||
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
|
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
|
||||||
validates_attachment_size :header, less_than: LIMIT
|
validates_attachment_size :header, less_than: LIMIT
|
||||||
remotable_attachment :header, LIMIT, suppress_errors: false
|
remotable_attachment :header, LIMIT, suppress_errors: false
|
||||||
|
|
|
@ -170,18 +170,13 @@ class MediaAttachment < ApplicationRecord
|
||||||
|
|
||||||
DEFAULT_STYLES = [:original].freeze
|
DEFAULT_STYLES = [:original].freeze
|
||||||
|
|
||||||
GLOBAL_CONVERT_OPTIONS = {
|
|
||||||
all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp -define jpeg:dct-method=float',
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
belongs_to :account, inverse_of: :media_attachments, optional: true
|
belongs_to :account, inverse_of: :media_attachments, optional: true
|
||||||
belongs_to :status, inverse_of: :media_attachments, optional: true
|
belongs_to :status, inverse_of: :media_attachments, optional: true
|
||||||
belongs_to :scheduled_status, inverse_of: :media_attachments, optional: true
|
belongs_to :scheduled_status, inverse_of: :media_attachments, optional: true
|
||||||
|
|
||||||
has_attached_file :file,
|
has_attached_file :file,
|
||||||
styles: ->(f) { file_styles f },
|
styles: ->(f) { file_styles f },
|
||||||
processors: ->(f) { file_processors f },
|
processors: ->(f) { file_processors f }
|
||||||
convert_options: GLOBAL_CONVERT_OPTIONS
|
|
||||||
|
|
||||||
before_file_validate :set_type_and_extension
|
before_file_validate :set_type_and_extension
|
||||||
before_file_validate :check_video_dimensions
|
before_file_validate :check_video_dimensions
|
||||||
|
@ -192,8 +187,7 @@ class MediaAttachment < ApplicationRecord
|
||||||
|
|
||||||
has_attached_file :thumbnail,
|
has_attached_file :thumbnail,
|
||||||
styles: THUMBNAIL_STYLES,
|
styles: THUMBNAIL_STYLES,
|
||||||
processors: [:lazy_thumbnail, :blurhash_transcoder, :color_extractor],
|
processors: [:lazy_thumbnail, :blurhash_transcoder, :color_extractor]
|
||||||
convert_options: GLOBAL_CONVERT_OPTIONS
|
|
||||||
|
|
||||||
validates_attachment_content_type :thumbnail, content_type: IMAGE_MIME_TYPES
|
validates_attachment_content_type :thumbnail, content_type: IMAGE_MIME_TYPES
|
||||||
validates_attachment_size :thumbnail, less_than: IMAGE_LIMIT
|
validates_attachment_size :thumbnail, less_than: IMAGE_LIMIT
|
||||||
|
|
|
@ -55,7 +55,7 @@ class PreviewCard < ApplicationRecord
|
||||||
|
|
||||||
has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy
|
has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy
|
||||||
|
|
||||||
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: [:lazy_thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, validate_media_type: false
|
||||||
|
|
||||||
validates :url, presence: true, uniqueness: true, url: true
|
validates :url, presence: true, uniqueness: true, url: true
|
||||||
validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES
|
validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES
|
||||||
|
|
|
@ -41,7 +41,7 @@ class SiteUpload < ApplicationRecord
|
||||||
mascot: {}.freeze,
|
mascot: {}.freeze,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
has_attached_file :file, styles: ->(file) { STYLES[file.instance.var.to_sym] }, convert_options: { all: '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, processors: [:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
|
has_attached_file :file, styles: ->(file) { STYLES[file.instance.var.to_sym] }, processors: [:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
|
||||||
|
|
||||||
validates_attachment_content_type :file, content_type: %r{\Aimage/.*\z}
|
validates_attachment_content_type :file, content_type: %r{\Aimage/.*\z}
|
||||||
validates :file, presence: true
|
validates :file, presence: true
|
||||||
|
|
|
@ -5,10 +5,9 @@ module Paperclip
|
||||||
def make
|
def make
|
||||||
return @file unless options[:style] == :small || options[:blurhash]
|
return @file unless options[:style] == :small || options[:blurhash]
|
||||||
|
|
||||||
pixels = convert(':source -depth 8 RGB:-', source: File.expand_path(@file.path)).unpack('C*')
|
image = Vips::Image.new_from_file(@file.path, access: :sequential)
|
||||||
geometry = options.fetch(:file_geometry_parser).from_file(@file)
|
|
||||||
|
|
||||||
attachment.instance.blurhash = Blurhash.encode(geometry.width, geometry.height, pixels, **(options[:blurhash] || {}))
|
attachment.instance.blurhash = Blurhash.encode(image.width, image.height, image.to_a.flatten, **(options[:blurhash] || {}))
|
||||||
|
|
||||||
@file
|
@file
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,20 +1,50 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Paperclip
|
module Paperclip
|
||||||
class LazyThumbnail < Paperclip::Thumbnail
|
class LazyThumbnail < Paperclip::Processor
|
||||||
def make
|
class PixelGeometryParser
|
||||||
return File.open(@file.path) unless needs_convert?
|
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
|
||||||
|
|
||||||
if options[:geometry]
|
Paperclip::Geometry.new(width, height)
|
||||||
min_side = [@current_geometry.width, @current_geometry.height].min.to_i
|
end
|
||||||
options[:geometry] = "#{min_side}x#{min_side}#" if @target_geometry.square? && min_side < @target_geometry.width
|
end
|
||||||
elsif options[:pixels]
|
|
||||||
width = Math.sqrt(options[:pixels] * (@current_geometry.width.to_f / @current_geometry.height)).round.to_i
|
def initialize(file, options = {}, attachment = nil)
|
||||||
height = Math.sqrt(options[:pixels] * (@current_geometry.height.to_f / @current_geometry.width)).round.to_i
|
super
|
||||||
options[:geometry] = "#{width}x#{height}>"
|
|
||||||
|
@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)
|
||||||
|
end
|
||||||
|
|
||||||
|
def make
|
||||||
|
source = File.open(@file.path)
|
||||||
|
|
||||||
|
return source unless needs_convert?
|
||||||
|
|
||||||
|
vips = ImageProcessing::Vips.source(source)
|
||||||
|
|
||||||
|
vips = if @crop
|
||||||
|
vips.resize_to_fill(@target_geometry.width, @target_geometry.height, sharpen: false, crop: :attention)
|
||||||
|
else
|
||||||
|
vips.resize_to_limit(@target_geometry.width, @target_geometry.height, sharpen: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
vips = vips.custom do |image|
|
||||||
|
image.mutate do |mutable|
|
||||||
|
image.get_fields.each do |field|
|
||||||
|
mutable.remove!(field) unless field == 'icc-profile-data'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Paperclip::Thumbnail.make(file, options, attachment)
|
vips.convert(@format)
|
||||||
|
.saver(interlace: true, quality: 90)
|
||||||
|
.call
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
Loading…
Reference in New Issue