2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
require "fastimage"
|
|
|
|
|
|
|
|
class UploadCreator
|
|
|
|
|
2017-05-18 06:13:13 -04:00
|
|
|
TYPES_TO_CROP ||= %w{avatar card_background custom_emoji profile_background}.each(&:freeze)
|
|
|
|
|
2020-09-27 23:52:05 -04:00
|
|
|
ALLOWED_SVG_ELEMENTS ||= %w{
|
2020-12-07 19:16:41 -05:00
|
|
|
circle clipPath defs ellipse feGaussianBlur filter g line linearGradient
|
2020-09-18 14:13:07 -04:00
|
|
|
marker path polygon polyline radialGradient rect stop style svg text
|
2020-12-07 19:16:41 -05:00
|
|
|
textPath tref tspan use
|
2017-05-18 06:13:13 -04:00
|
|
|
}.each(&:freeze)
|
2017-05-10 18:16:57 -04:00
|
|
|
|
2020-09-27 23:52:05 -04:00
|
|
|
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
|
|
|
|
deprecate_constant 'WHITELISTED_SVG_ELEMENTS', 'UploadCreator::ALLOWED_SVG_ELEMENTS'
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
# Available options
|
|
|
|
# - type (string)
|
|
|
|
# - origin (string)
|
2017-06-12 16:41:29 -04:00
|
|
|
# - for_group_message (boolean)
|
2017-05-10 18:16:57 -04:00
|
|
|
# - for_theme (boolean)
|
2017-06-12 16:41:29 -04:00
|
|
|
# - for_private_message (boolean)
|
2017-06-23 06:13:48 -04:00
|
|
|
# - pasted (boolean)
|
2018-04-19 07:30:31 -04:00
|
|
|
# - for_export (boolean)
|
2019-07-30 23:16:03 -04:00
|
|
|
# - for_gravatar (boolean)
|
2021-05-19 11:24:52 -04:00
|
|
|
# - skip_validations (boolean)
|
2017-05-10 18:16:57 -04:00
|
|
|
def initialize(file, filename, opts = {})
|
|
|
|
@file = file
|
2018-08-21 12:11:01 -04:00
|
|
|
@filename = (filename || "").gsub(/[^[:print:]]/, "")
|
|
|
|
@upload = Upload.new(original_filename: @filename, filesize: 0)
|
2017-05-10 18:16:57 -04:00
|
|
|
@opts = opts
|
2021-05-19 11:24:52 -04:00
|
|
|
@opts[:validate] = opts[:skip_validations].present? ? !ActiveRecord::Type::Boolean.new.cast(opts[:skip_validations]) : true
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def create_for(user_id)
|
|
|
|
if filesize <= 0
|
|
|
|
@upload.errors.add(:base, I18n.t("upload.empty"))
|
|
|
|
return @upload
|
|
|
|
end
|
|
|
|
|
2020-06-11 10:06:48 -04:00
|
|
|
@image_info = FastImage.new(@file) rescue nil
|
|
|
|
is_image = FileHelper.is_supported_image?(@filename)
|
|
|
|
is_image ||= @image_info && FileHelper.is_supported_image?("test.#{@image_info.type}")
|
|
|
|
is_image = false if @opts[:for_theme]
|
2020-06-10 15:30:53 -04:00
|
|
|
|
2020-06-11 10:06:48 -04:00
|
|
|
DistributedMutex.synchronize("upload_#{user_id}_#{@filename}") do
|
2020-07-22 21:40:09 -04:00
|
|
|
# We need to convert HEIFs early because FastImage does not consider them as images
|
|
|
|
if convert_heif_to_jpeg?
|
|
|
|
convert_heif!
|
|
|
|
is_image = FileHelper.is_supported_image?("test.#{@image_info.type}")
|
|
|
|
end
|
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
if is_image
|
2017-05-10 18:16:57 -04:00
|
|
|
extract_image_info!
|
|
|
|
return @upload if @upload.errors.present?
|
|
|
|
|
2018-11-06 23:29:14 -05:00
|
|
|
if @image_info.type.to_s == "svg"
|
2020-07-26 20:23:54 -04:00
|
|
|
clean_svg!
|
2018-08-16 23:41:30 -04:00
|
|
|
elsif !Rails.env.test? || @opts[:force_optimize]
|
2020-10-23 12:38:28 -04:00
|
|
|
convert_to_jpeg! if convert_png_to_jpeg? || should_alter_quality?
|
2017-05-10 18:16:57 -04:00
|
|
|
fix_orientation! if should_fix_orientation?
|
|
|
|
crop! if should_crop?
|
|
|
|
optimize! if should_optimize?
|
2021-06-21 07:13:24 -04:00
|
|
|
downsize! if should_downsize?
|
|
|
|
return @upload if is_still_too_big?
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
2018-08-16 23:41:30 -04:00
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
# conversion may have switched the type
|
2018-08-16 23:41:30 -04:00
|
|
|
image_type = @image_info.type.to_s
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
2020-01-15 22:50:27 -05:00
|
|
|
# compute the sha of the file and generate a unique hash
|
|
|
|
# which is only used for secure uploads
|
2017-05-10 18:16:57 -04:00
|
|
|
sha1 = Upload.generate_digest(@file)
|
2020-01-15 22:50:27 -05:00
|
|
|
unique_hash = SecureRandom.hex(20) if SiteSetting.secure_media
|
2017-05-10 18:16:57 -04:00
|
|
|
|
2020-01-15 22:50:27 -05:00
|
|
|
# we do not check for duplicate uploads if secure media is
|
|
|
|
# enabled because we use a unique access hash to differentiate
|
|
|
|
# between uploads instead of the sha1, and to get around various
|
|
|
|
# access/permission issues for uploads
|
|
|
|
if !SiteSetting.secure_media
|
|
|
|
# do we already have that upload?
|
|
|
|
@upload = Upload.find_by(sha1: sha1)
|
|
|
|
|
|
|
|
# make sure the previous upload has not failed
|
|
|
|
if @upload && @upload.url.blank?
|
|
|
|
@upload.destroy
|
|
|
|
@upload = nil
|
|
|
|
end
|
2017-05-10 18:16:57 -04:00
|
|
|
|
2020-01-15 22:50:27 -05:00
|
|
|
# return the previous upload if any
|
|
|
|
if @upload
|
2020-07-03 13:16:54 -04:00
|
|
|
add_metadata!
|
2020-01-15 22:50:27 -05:00
|
|
|
UserUpload.find_or_create_by!(user_id: user_id, upload_id: @upload.id) if user_id
|
|
|
|
return @upload
|
|
|
|
end
|
2018-09-20 01:33:10 -04:00
|
|
|
end
|
2017-05-10 18:16:57 -04:00
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
fixed_original_filename = nil
|
2018-08-20 02:08:05 -04:00
|
|
|
|
2019-01-04 09:30:17 -05:00
|
|
|
if is_image
|
2018-08-20 02:08:05 -04:00
|
|
|
current_extension = File.extname(@filename).downcase.sub("jpeg", "jpg")
|
|
|
|
expected_extension = ".#{image_type}".downcase.sub("jpeg", "jpg")
|
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
# we have to correct original filename here, no choice
|
|
|
|
# otherwise validation will fail and we can not save
|
|
|
|
# TODO decide if we only run the validation on the extension
|
2018-08-20 02:08:05 -04:00
|
|
|
if current_extension != expected_extension
|
2019-01-04 09:30:17 -05:00
|
|
|
basename = File.basename(@filename, current_extension).presence || "image"
|
2018-08-20 02:08:05 -04:00
|
|
|
fixed_original_filename = "#{basename}#{expected_extension}"
|
2018-08-19 22:18:49 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
# create the upload otherwise
|
|
|
|
@upload = Upload.new
|
|
|
|
@upload.user_id = user_id
|
2018-08-19 22:18:49 -04:00
|
|
|
@upload.original_filename = fixed_original_filename || @filename
|
2017-05-10 18:16:57 -04:00
|
|
|
@upload.filesize = filesize
|
2020-01-15 22:50:27 -05:00
|
|
|
@upload.sha1 = SiteSetting.secure_media? ? unique_hash : sha1
|
|
|
|
@upload.original_sha1 = SiteSetting.secure_media? ? sha1 : nil
|
2017-05-10 18:16:57 -04:00
|
|
|
@upload.url = ""
|
|
|
|
@upload.origin = @opts[:origin][0...1000] if @opts[:origin]
|
2018-08-12 22:55:06 -04:00
|
|
|
@upload.extension = image_type || File.extname(@filename)[1..10]
|
2017-05-10 18:16:57 -04:00
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
if is_image
|
2021-03-01 11:44:00 -05:00
|
|
|
if @image_info.type.to_s == 'svg'
|
2021-04-11 23:55:54 -04:00
|
|
|
w, h = [0, 0]
|
|
|
|
|
2021-07-19 15:10:17 -04:00
|
|
|
# identify can behave differently depending on how it's compiled and
|
|
|
|
# what programs (e.g. inkscape) are installed on your system.
|
|
|
|
# 'MSVG:' forces ImageMagick to use internal routines and behave
|
|
|
|
# consistently whether it's running from our docker container or not
|
2021-04-11 23:55:54 -04:00
|
|
|
begin
|
|
|
|
w, h = Discourse::Utils
|
2021-07-19 15:10:17 -04:00
|
|
|
.execute_command("identify", "-ping", "-format", "%w %h", "MSVG:#{@file.path}", timeout: Upload::MAX_IDENTIFY_SECONDS)
|
|
|
|
.split(' ').map(&:to_i)
|
2021-04-11 23:55:54 -04:00
|
|
|
rescue
|
|
|
|
# use default 0, 0
|
|
|
|
end
|
2021-03-01 11:44:00 -05:00
|
|
|
else
|
|
|
|
w, h = @image_info.size
|
|
|
|
end
|
|
|
|
|
|
|
|
@upload.thumbnail_width, @upload.thumbnail_height = ImageSizer.resize(w, h)
|
|
|
|
@upload.width, @upload.height = w, h
|
2021-01-13 12:01:30 -05:00
|
|
|
@upload.animated = animated?
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
2020-07-03 13:16:54 -04:00
|
|
|
add_metadata!
|
2021-01-28 18:03:44 -05:00
|
|
|
|
|
|
|
if SiteSetting.secure_media
|
|
|
|
secure, reason = UploadSecurity.new(@upload, @opts.merge(creating: true)).should_be_secure_with_reason
|
|
|
|
attrs = @upload.secure_params(secure, reason, "upload creator")
|
|
|
|
@upload.assign_attributes(attrs)
|
|
|
|
end
|
|
|
|
|
2021-06-15 09:10:03 -04:00
|
|
|
# Callbacks using this event to validate the upload or the file must add errors to the
|
|
|
|
# upload errors object.
|
|
|
|
DiscourseEvent.trigger(:before_upload_creation, @file, is_image, @upload, @opts[:validate])
|
|
|
|
return @upload unless @upload.errors.empty? && @upload.save(validate: @opts[:validate])
|
2020-07-20 16:59:37 -04:00
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
# store the file and update its url
|
|
|
|
File.open(@file.path) do |f|
|
2018-08-07 01:15:00 -04:00
|
|
|
url = Discourse.store.store_upload(f, @upload)
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
if url.present?
|
2019-01-04 01:16:22 -05:00
|
|
|
@upload.url = url
|
2021-05-19 11:24:52 -04:00
|
|
|
@upload.save!(validate: @opts[:validate])
|
2017-05-10 18:16:57 -04:00
|
|
|
else
|
|
|
|
@upload.errors.add(:url, I18n.t("upload.store_failure", upload_id: @upload.id, user_id: user_id))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-06 23:29:14 -05:00
|
|
|
if @upload.errors.empty? && is_image && @opts[:type] == "avatar" && @upload.extension != "svg"
|
2019-05-02 04:08:12 -04:00
|
|
|
Jobs.enqueue(:create_avatar_thumbnails, upload_id: @upload.id)
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
2018-09-20 01:33:10 -04:00
|
|
|
if @upload.errors.empty?
|
|
|
|
UserUpload.find_or_create_by!(user_id: user_id, upload_id: @upload.id) if user_id
|
|
|
|
end
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
@upload
|
|
|
|
end
|
|
|
|
ensure
|
2019-10-11 05:13:10 -04:00
|
|
|
if @file
|
|
|
|
@file.respond_to?(:close!) ? @file.close! : @file.close
|
|
|
|
end
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def extract_image_info!
|
|
|
|
@image_info = FastImage.new(@file) rescue nil
|
|
|
|
@file.rewind
|
|
|
|
|
|
|
|
if @image_info.nil?
|
|
|
|
@upload.errors.add(:base, I18n.t("upload.images.not_supported_or_corrupted"))
|
|
|
|
elsif filesize <= 0
|
|
|
|
@upload.errors.add(:base, I18n.t("upload.empty"))
|
FIX: Allow SVG uploads if dimensions are a fraction of a unit (#13409)
* FIX: Allow SVG uploads if dimensions are a fraction of a unit
`UploadCreator` counts the number of pixels in an file to determine if it is valid. `pixels` is calculated by multiplying the width and height of the image, as determined by FastImage.
SVG files can have their width/height expressed in a variety of different units of measurement. For example, ‘px’, ‘in’, ‘cm’, ‘mm’, ‘pt’, ‘pc’, etc are all valid within SVG files. If an image has a width of `0.5in`, FastImage may interpret this as being a width of `0`, meaning it will report the `size` as being `0`.
However, we don’t need to concern ourselves with the number of ‘pixels’ in a SVG files, as that is irrelevant for this file format, so we can skip over the check for `pixels == 0` when processing this file type.
* DEV: Speed up getting SVG dimensions
The `-ping` flag prevents the entire image from being rasterized before a result is returned. See:
https://imagemagick.org/script/command-line-options.php#ping
2021-06-17 15:56:11 -04:00
|
|
|
elsif pixels == 0 && @image_info.type.to_s != 'svg'
|
2017-05-10 18:16:57 -04:00
|
|
|
@upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
|
2020-09-13 20:10:55 -04:00
|
|
|
elsif max_image_pixels > 0 && pixels >= max_image_pixels * 2
|
|
|
|
@upload.errors.add(:base, I18n.t("upload.images.larger_than_x_megapixels", max_image_megapixels: SiteSetting.max_image_megapixels * 2))
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-06-01 17:12:37 -04:00
|
|
|
MIN_PIXELS_TO_CONVERT_TO_JPEG ||= 1280 * 720
|
|
|
|
|
2019-02-11 00:28:43 -05:00
|
|
|
def convert_png_to_jpeg?
|
|
|
|
return false unless @image_info.type == :png
|
2017-06-23 06:13:48 -04:00
|
|
|
return true if @opts[:pasted]
|
2017-06-26 08:21:47 -04:00
|
|
|
return false if SiteSetting.png_to_jpg_quality == 100
|
2017-06-23 06:13:48 -04:00
|
|
|
pixels > MIN_PIXELS_TO_CONVERT_TO_JPEG
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
2018-11-20 19:00:52 -05:00
|
|
|
MIN_CONVERT_TO_JPEG_BYTES_SAVED = 75_000
|
|
|
|
MIN_CONVERT_TO_JPEG_SAVING_RATIO = 0.70
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
def convert_to_jpeg!
|
2021-03-16 10:44:41 -04:00
|
|
|
return if @opts[:for_site_setting]
|
2019-01-04 09:30:17 -05:00
|
|
|
return if filesize < MIN_CONVERT_TO_JPEG_BYTES_SAVED
|
2018-11-20 19:00:52 -05:00
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
jpeg_tempfile = Tempfile.new(["image", ".jpg"])
|
|
|
|
|
2018-07-25 16:00:04 -04:00
|
|
|
from = @file.path
|
|
|
|
to = jpeg_tempfile.path
|
|
|
|
|
|
|
|
OptimizedImage.ensure_safe_paths!(from, to)
|
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
from = OptimizedImage.prepend_decoder!(from, nil, filename: "image.#{@image_info.type}")
|
2018-07-25 17:55:06 -04:00
|
|
|
to = OptimizedImage.prepend_decoder!(to)
|
2017-12-19 05:31:18 -05:00
|
|
|
|
2020-10-23 12:38:28 -04:00
|
|
|
opts = {}
|
|
|
|
desired_quality = [SiteSetting.png_to_jpg_quality, SiteSetting.recompress_original_jpg_quality].compact.min
|
|
|
|
target_quality = @upload.target_image_quality(from, desired_quality)
|
|
|
|
opts = { quality: target_quality } if target_quality
|
|
|
|
|
2017-12-19 05:31:18 -05:00
|
|
|
begin
|
2020-10-23 12:38:28 -04:00
|
|
|
execute_convert(from, to, opts)
|
2017-12-19 05:31:18 -05:00
|
|
|
rescue
|
|
|
|
# retry with debugging enabled
|
2020-10-23 12:38:28 -04:00
|
|
|
execute_convert(from, to, opts.merge(debug: true))
|
2017-12-19 05:31:18 -05:00
|
|
|
end
|
2017-05-10 18:16:57 -04:00
|
|
|
|
2018-11-20 19:00:52 -05:00
|
|
|
new_size = File.size(jpeg_tempfile.path)
|
|
|
|
|
|
|
|
keep_jpeg = new_size < filesize * MIN_CONVERT_TO_JPEG_SAVING_RATIO
|
|
|
|
keep_jpeg &&= (filesize - new_size) > MIN_CONVERT_TO_JPEG_BYTES_SAVED
|
|
|
|
|
|
|
|
if keep_jpeg
|
2019-10-11 05:13:10 -04:00
|
|
|
@file.respond_to?(:close!) ? @file.close! : @file.close
|
2017-05-10 18:16:57 -04:00
|
|
|
@file = jpeg_tempfile
|
2017-06-22 10:53:49 -04:00
|
|
|
extract_image_info!
|
2017-05-10 18:16:57 -04:00
|
|
|
else
|
2019-10-11 05:13:10 -04:00
|
|
|
jpeg_tempfile.close!
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-07-22 21:40:09 -04:00
|
|
|
def convert_heif_to_jpeg?
|
2020-08-07 11:17:50 -04:00
|
|
|
File.extname(@filename).downcase.match?(/\.hei(f|c)$/)
|
2020-07-22 21:40:09 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def convert_heif!
|
|
|
|
jpeg_tempfile = Tempfile.new(["image", ".jpg"])
|
|
|
|
from = @file.path
|
|
|
|
to = jpeg_tempfile.path
|
|
|
|
OptimizedImage.ensure_safe_paths!(from, to)
|
|
|
|
|
|
|
|
begin
|
|
|
|
execute_convert(from, to)
|
|
|
|
rescue
|
|
|
|
# retry with debugging enabled
|
2020-10-23 12:38:28 -04:00
|
|
|
execute_convert(from, to, { debug: true })
|
2020-07-22 21:40:09 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
@file.respond_to?(:close!) ? @file.close! : @file.close
|
|
|
|
@file = jpeg_tempfile
|
|
|
|
extract_image_info!
|
|
|
|
end
|
|
|
|
|
2021-04-11 23:55:54 -04:00
|
|
|
MAX_CONVERT_FORMAT_SECONDS = 20
|
2020-10-23 12:38:28 -04:00
|
|
|
def execute_convert(from, to, opts = {})
|
2018-07-25 16:00:04 -04:00
|
|
|
command = [
|
|
|
|
"convert",
|
|
|
|
from,
|
|
|
|
"-auto-orient",
|
2018-07-25 17:08:02 -04:00
|
|
|
"-background", "white",
|
|
|
|
"-interlace", "none",
|
2020-10-23 12:38:28 -04:00
|
|
|
"-flatten"
|
2018-07-25 16:00:04 -04:00
|
|
|
]
|
2020-10-23 12:38:28 -04:00
|
|
|
command << "-debug" << "all" if opts[:debug]
|
|
|
|
command << "-quality" << opts[:quality].to_s if opts[:quality]
|
2018-07-25 16:00:04 -04:00
|
|
|
command << to
|
2017-12-19 05:31:18 -05:00
|
|
|
|
2021-04-11 23:55:54 -04:00
|
|
|
Discourse::Utils.execute_command(
|
|
|
|
*command,
|
|
|
|
failure_message: I18n.t("upload.png_to_jpg_conversion_failure_message"),
|
|
|
|
timeout: MAX_CONVERT_FORMAT_SECONDS
|
|
|
|
)
|
2017-12-19 05:31:18 -05:00
|
|
|
end
|
|
|
|
|
2020-10-23 12:38:28 -04:00
|
|
|
def should_alter_quality?
|
2021-01-13 12:01:30 -05:00
|
|
|
return false if animated?
|
2020-10-26 15:10:19 -04:00
|
|
|
|
2021-02-12 13:37:35 -05:00
|
|
|
desired_quality = @image_info.type == :png ? SiteSetting.png_to_jpg_quality : SiteSetting.recompress_original_jpg_quality
|
|
|
|
@upload.target_image_quality(@file.path, desired_quality).present?
|
2020-10-23 12:38:28 -04:00
|
|
|
end
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
def should_downsize?
|
2021-01-13 12:01:30 -05:00
|
|
|
max_image_size > 0 && filesize >= max_image_size && !animated?
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def downsize!
|
|
|
|
3.times do
|
|
|
|
original_size = filesize
|
2019-10-04 10:22:57 -04:00
|
|
|
down_tempfile = Tempfile.new(["down", ".#{@image_info.type}"])
|
2018-08-16 13:20:07 -04:00
|
|
|
|
2019-10-11 05:13:10 -04:00
|
|
|
from = @file.path
|
|
|
|
to = down_tempfile.path
|
|
|
|
|
|
|
|
OptimizedImage.ensure_safe_paths!(from, to)
|
|
|
|
|
2018-08-16 13:20:07 -04:00
|
|
|
OptimizedImage.downsize(
|
2019-10-11 05:13:10 -04:00
|
|
|
from,
|
|
|
|
to,
|
2021-01-12 10:07:07 -05:00
|
|
|
"50%",
|
2020-04-10 08:46:47 -04:00
|
|
|
scale_image: true,
|
2018-08-16 13:20:07 -04:00
|
|
|
raise_on_error: true
|
|
|
|
)
|
|
|
|
|
2019-10-11 05:13:10 -04:00
|
|
|
@file.respond_to?(:close!) ? @file.close! : @file.close
|
2019-10-04 10:22:57 -04:00
|
|
|
@file = down_tempfile
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
extract_image_info!
|
2018-08-16 13:20:07 -04:00
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
return if filesize >= original_size || pixels == 0 || !should_downsize?
|
|
|
|
end
|
2020-03-25 06:59:16 -04:00
|
|
|
rescue
|
|
|
|
@upload.errors.add(:base, I18n.t("upload.optimize_failure_message"))
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def is_still_too_big?
|
|
|
|
if max_image_pixels > 0 && pixels >= max_image_pixels
|
|
|
|
@upload.errors.add(:base, I18n.t("upload.images.larger_than_x_megapixels", max_image_megapixels: SiteSetting.max_image_megapixels))
|
|
|
|
true
|
|
|
|
elsif max_image_size > 0 && filesize >= max_image_size
|
|
|
|
@upload.errors.add(:base, I18n.t("upload.images.too_large", max_size_kb: SiteSetting.max_image_size_kb))
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-07-26 20:23:54 -04:00
|
|
|
def clean_svg!
|
2017-05-10 18:16:57 -04:00
|
|
|
doc = Nokogiri::XML(@file)
|
2020-07-26 20:23:54 -04:00
|
|
|
doc.xpath(svg_allowlist_xpath).remove
|
2019-12-11 09:28:35 -05:00
|
|
|
doc.xpath("//@*[starts-with(name(), 'on')]").remove
|
2020-07-08 23:31:48 -04:00
|
|
|
doc.css('use').each do |use_el|
|
|
|
|
if use_el.attr('href')
|
|
|
|
use_el.remove_attribute('href') unless use_el.attr('href').starts_with?('#')
|
|
|
|
end
|
|
|
|
if use_el.attr('xlink:href')
|
|
|
|
use_el.remove_attribute('xlink:href') unless use_el.attr('xlink:href').starts_with?('#')
|
|
|
|
end
|
|
|
|
end
|
2017-05-10 18:16:57 -04:00
|
|
|
File.write(@file.path, doc.to_s)
|
|
|
|
@file.rewind
|
|
|
|
end
|
|
|
|
|
2017-07-10 10:35:23 -04:00
|
|
|
def should_fix_orientation?
|
|
|
|
# orientation is between 1 and 8, 1 being the default
|
|
|
|
# cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
|
|
|
|
@image_info.orientation.to_i > 1
|
|
|
|
end
|
|
|
|
|
2021-04-11 23:55:54 -04:00
|
|
|
MAX_FIX_ORIENTATION_TIME = 5
|
2017-07-10 10:35:23 -04:00
|
|
|
def fix_orientation!
|
2018-07-25 16:00:04 -04:00
|
|
|
path = @file.path
|
|
|
|
|
|
|
|
OptimizedImage.ensure_safe_paths!(path)
|
2018-08-19 22:18:49 -04:00
|
|
|
path = OptimizedImage.prepend_decoder!(path, nil, filename: "image.#{@image_info.type}")
|
2018-07-25 16:00:04 -04:00
|
|
|
|
2021-04-11 23:55:54 -04:00
|
|
|
Discourse::Utils.execute_command('convert', path, '-auto-orient', path, timeout: MAX_FIX_ORIENTATION_TIME)
|
2018-07-25 16:00:04 -04:00
|
|
|
|
2017-07-10 10:35:23 -04:00
|
|
|
extract_image_info!
|
|
|
|
end
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
def should_crop?
|
2021-01-13 12:01:30 -05:00
|
|
|
return false if ['profile_background', 'card_background', 'custom_emoji'].include?(@opts[:type]) && animated?
|
2020-11-11 19:22:38 -05:00
|
|
|
|
2017-05-18 06:13:13 -04:00
|
|
|
TYPES_TO_CROP.include?(@opts[:type])
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def crop!
|
|
|
|
max_pixel_ratio = Discourse::PIXEL_RATIOS.max
|
2018-08-19 22:18:49 -04:00
|
|
|
filename_with_correct_ext = "image.#{@image_info.type}"
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
case @opts[:type]
|
|
|
|
when "avatar"
|
|
|
|
width = height = Discourse.avatar_sizes.max
|
2020-10-16 06:41:27 -04:00
|
|
|
OptimizedImage.resize(@file.path, @file.path, width, height, filename: filename_with_correct_ext)
|
2017-05-10 18:16:57 -04:00
|
|
|
when "profile_background"
|
|
|
|
max_width = 850 * max_pixel_ratio
|
|
|
|
width, height = ImageSizer.resize(@image_info.size[0], @image_info.size[1], max_width: max_width, max_height: max_width)
|
2020-10-16 06:41:27 -04:00
|
|
|
OptimizedImage.downsize(@file.path, @file.path, "#{width}x#{height}\>", filename: filename_with_correct_ext)
|
2017-05-10 18:16:57 -04:00
|
|
|
when "card_background"
|
|
|
|
max_width = 590 * max_pixel_ratio
|
|
|
|
width, height = ImageSizer.resize(@image_info.size[0], @image_info.size[1], max_width: max_width, max_height: max_width)
|
2020-10-16 06:41:27 -04:00
|
|
|
OptimizedImage.downsize(@file.path, @file.path, "#{width}x#{height}\>", filename: filename_with_correct_ext)
|
2017-05-10 18:16:57 -04:00
|
|
|
when "custom_emoji"
|
2020-10-16 06:41:27 -04:00
|
|
|
OptimizedImage.downsize(@file.path, @file.path, "100x100\>", filename: filename_with_correct_ext)
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
2017-06-22 10:53:49 -04:00
|
|
|
extract_image_info!
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def should_optimize?
|
|
|
|
# GIF is too slow (plus, we'll soon be converting them to MP4)
|
|
|
|
# Optimizing SVG is useless
|
|
|
|
return false if @file.path =~ /\.(gif|svg)$/i
|
|
|
|
# Safeguard for large PNGs
|
|
|
|
return pixels < 2_000_000 if @file.path =~ /\.png/i
|
|
|
|
# Everything else is fine!
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
def optimize!
|
|
|
|
OptimizedImage.ensure_safe_paths!(@file.path)
|
2017-07-25 05:48:39 -04:00
|
|
|
FileHelper.optimize_image!(@file.path)
|
2017-06-22 10:53:49 -04:00
|
|
|
extract_image_info!
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def filesize
|
|
|
|
File.size?(@file.path).to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
def max_image_size
|
2017-05-11 04:03:28 -04:00
|
|
|
@max_image_size ||= SiteSetting.max_image_size_kb.kilobytes
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def max_image_pixels
|
2017-05-11 04:03:28 -04:00
|
|
|
@max_image_pixels ||= SiteSetting.max_image_megapixels * 1_000_000
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def pixels
|
|
|
|
@image_info.size&.reduce(:*).to_i
|
|
|
|
end
|
|
|
|
|
2020-07-26 20:23:54 -04:00
|
|
|
def svg_allowlist_xpath
|
2020-09-27 23:52:05 -04:00
|
|
|
@@svg_allowlist_xpath ||= "//*[#{ALLOWED_SVG_ELEMENTS.map { |e| "name()!='#{e}'" }.join(" and ") }]"
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|
|
|
|
|
2020-07-03 13:16:54 -04:00
|
|
|
def add_metadata!
|
|
|
|
@upload.for_private_message = true if @opts[:for_private_message]
|
|
|
|
@upload.for_group_message = true if @opts[:for_group_message]
|
|
|
|
@upload.for_theme = true if @opts[:for_theme]
|
|
|
|
@upload.for_export = true if @opts[:for_export]
|
|
|
|
@upload.for_site_setting = true if @opts[:for_site_setting]
|
|
|
|
@upload.for_gravatar = true if @opts[:for_gravatar]
|
|
|
|
end
|
|
|
|
|
2021-01-13 12:01:30 -05:00
|
|
|
private
|
|
|
|
|
|
|
|
def animated?
|
|
|
|
return @animated if @animated != nil
|
|
|
|
|
|
|
|
@animated ||= begin
|
|
|
|
is_animated = FastImage.animated?(@file)
|
|
|
|
type = @image_info.type.to_s
|
|
|
|
|
|
|
|
if is_animated != nil
|
|
|
|
# FastImage will return nil if it cannot determine if animated
|
|
|
|
is_animated
|
|
|
|
elsif type == "gif" || type == "webp"
|
|
|
|
# Only GIFs, WEBPs and a few other unsupported image types can be animated
|
|
|
|
OptimizedImage.ensure_safe_paths!(@file.path)
|
|
|
|
|
|
|
|
command = ["identify", "-format", "%n\\n", @file.path]
|
2021-04-11 23:55:54 -04:00
|
|
|
frames = Discourse::Utils.execute_command(*command, timeout: Upload::MAX_IDENTIFY_SECONDS).to_i rescue 1
|
2021-01-13 12:01:30 -05:00
|
|
|
|
|
|
|
frames > 1
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-05-10 18:16:57 -04:00
|
|
|
end
|