a7171af0a3
* Fix avatar and header issues by using custom geometry detector Revert a part of #6508. The file passed to dynamic styles method was not actually a file, but an instance of Paperclip::Attachment, which broke all styles by always returning {} from the method. One problem with GIF avatars was that Paperclip::GeometryDetector reported wrong dimensions for them, e.g. 120x120 GIF avatar would for some reason be detected as 120x53. By writing our own geometry parser, we can use FastImage, which also happens to be faster than ImageMagick, to detect image dimensions, which are also correct. Unfortunately, this PR does not implement skipping a `convert` entirely if the dimensions are already correct, as I found no easy way to write that behaviour into Paperclip without rewriting the Paperclip::Thumbnail class. * Only invoke convert if dimension or format needs to be changed
216 lines
5.5 KiB
Ruby
216 lines
5.5 KiB
Ruby
# frozen_string_literal: true
|
|
# == Schema Information
|
|
#
|
|
# Table name: media_attachments
|
|
#
|
|
# id :integer not null, primary key
|
|
# status_id :integer
|
|
# file_file_name :string
|
|
# file_content_type :string
|
|
# file_file_size :integer
|
|
# file_updated_at :datetime
|
|
# remote_url :string default(""), not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# shortcode :string
|
|
# type :integer default("image"), not null
|
|
# file_meta :json
|
|
# account_id :integer
|
|
# description :text
|
|
#
|
|
|
|
require 'mime/types'
|
|
|
|
class MediaAttachment < ApplicationRecord
|
|
self.inheritance_column = nil
|
|
|
|
enum type: [:image, :gifv, :video, :unknown]
|
|
|
|
IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze
|
|
VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v'].freeze
|
|
|
|
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
|
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
|
|
|
|
IMAGE_STYLES = {
|
|
original: {
|
|
geometry: '1280x1280>',
|
|
file_geometry_parser: FastGeometryParser,
|
|
},
|
|
|
|
small: {
|
|
geometry: '400x400>',
|
|
file_geometry_parser: FastGeometryParser,
|
|
},
|
|
}.freeze
|
|
|
|
VIDEO_STYLES = {
|
|
small: {
|
|
convert_options: {
|
|
output: {
|
|
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
|
|
},
|
|
},
|
|
format: 'png',
|
|
time: 0,
|
|
},
|
|
}.freeze
|
|
|
|
belongs_to :account, inverse_of: :media_attachments, optional: true
|
|
belongs_to :status, inverse_of: :media_attachments, optional: true
|
|
|
|
has_attached_file :file,
|
|
styles: ->(f) { file_styles f },
|
|
processors: ->(f) { file_processors f },
|
|
convert_options: { all: '-quality 90 -strip' }
|
|
|
|
include Remotable
|
|
|
|
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
|
|
validates_attachment_size :file, less_than: 8.megabytes
|
|
|
|
validates :account, presence: true
|
|
validates :description, length: { maximum: 420 }, if: :local?
|
|
|
|
scope :attached, -> { where.not(status_id: nil) }
|
|
scope :unattached, -> { where(status_id: nil) }
|
|
scope :local, -> { where(remote_url: '') }
|
|
scope :remote, -> { where.not(remote_url: '') }
|
|
|
|
default_scope { order(id: :asc) }
|
|
|
|
def local?
|
|
remote_url.blank?
|
|
end
|
|
|
|
def needs_redownload?
|
|
file.blank? && remote_url.present?
|
|
end
|
|
|
|
def to_param
|
|
shortcode
|
|
end
|
|
|
|
before_create :prepare_description, unless: :local?
|
|
before_create :set_shortcode
|
|
before_post_process :set_type_and_extension
|
|
before_save :set_meta
|
|
|
|
class << self
|
|
private
|
|
|
|
def file_styles(f)
|
|
if f.instance.file_content_type == 'image/gif'
|
|
{
|
|
small: IMAGE_STYLES[:small],
|
|
original: {
|
|
format: 'mp4',
|
|
convert_options: {
|
|
output: {
|
|
'movflags' => 'faststart',
|
|
'pix_fmt' => 'yuv420p',
|
|
'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
|
|
'vsync' => 'cfr',
|
|
'b:v' => '1300K',
|
|
'maxrate' => '500K',
|
|
'bufsize' => '1300K',
|
|
'crf' => 18,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
|
|
IMAGE_STYLES
|
|
else
|
|
VIDEO_STYLES
|
|
end
|
|
end
|
|
|
|
def file_processors(f)
|
|
if f.file_content_type == 'image/gif'
|
|
[:gif_transcoder]
|
|
elsif VIDEO_MIME_TYPES.include? f.file_content_type
|
|
[:video_transcoder]
|
|
else
|
|
[:thumbnail]
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def set_shortcode
|
|
self.type = :unknown if file.blank? && !type_changed?
|
|
|
|
return unless local?
|
|
|
|
loop do
|
|
self.shortcode = SecureRandom.urlsafe_base64(14)
|
|
break if MediaAttachment.find_by(shortcode: shortcode).nil?
|
|
end
|
|
end
|
|
|
|
def prepare_description
|
|
self.description = description.strip[0...420] unless description.nil?
|
|
end
|
|
|
|
def set_type_and_extension
|
|
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
|
|
extension = appropriate_extension
|
|
basename = Paperclip::Interpolations.basename(file, :original)
|
|
file.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
|
|
end
|
|
|
|
def set_meta
|
|
meta = populate_meta
|
|
return if meta == {}
|
|
file.instance_write :meta, meta
|
|
end
|
|
|
|
def populate_meta
|
|
meta = {}
|
|
|
|
file.queued_for_write.each do |style, file|
|
|
meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file)
|
|
end
|
|
|
|
meta
|
|
end
|
|
|
|
def image_geometry(file)
|
|
width, height = FastImage.size(file.path)
|
|
|
|
return {} if width.nil?
|
|
|
|
{
|
|
width: width,
|
|
height: height,
|
|
size: "#{width}x#{height}",
|
|
aspect: width.to_f / height.to_f,
|
|
}
|
|
end
|
|
|
|
def video_metadata(file)
|
|
movie = FFMPEG::Movie.new(file.path)
|
|
|
|
return {} unless movie.valid?
|
|
|
|
{
|
|
width: movie.width,
|
|
height: movie.height,
|
|
frame_rate: movie.frame_rate,
|
|
duration: movie.duration,
|
|
bitrate: movie.bitrate,
|
|
}
|
|
end
|
|
|
|
def appropriate_extension
|
|
mime_type = MIME::Types[file.content_type]
|
|
|
|
extensions_for_mime_type = mime_type.empty? ? [] : mime_type.first.extensions
|
|
original_extension = Paperclip::Interpolations.extension(file, :original)
|
|
|
|
extensions_for_mime_type.include?(original_extension) ? original_extension : extensions_for_mime_type.first
|
|
end
|
|
end
|