Files
danbooru/app/logical/media_file/image.rb
evazion 2f2c73eebb media assets: fix dimensions of corrupt GIFs.
Fix certain corrupt GIFs returning dimensions of 0x0. This happened
when the GIF was too corrupt for libvips to read. Fixed by using
ExifTool to read the dimensions instead.

Also add validations to ensure that it's not possible to have media
assets with a width or height of 0.
2022-10-31 15:18:02 -05:00

168 lines
4.8 KiB
Ruby

# frozen_string_literal: true
# A MediaFile for a JPEG, PNG, or GIF file. Uses libvips for resizing images.
#
# @see https://github.com/libvips/ruby-vips
# @see https://libvips.github.io/libvips/API/current
class MediaFile::Image < MediaFile
delegate :thumbnail_image, to: :image
def dimensions
image.size
rescue Vips::Error
[metadata.width, metadata.height]
rescue
[0, 0]
end
def is_supported?
case file_ext
when :avif
# XXX Mirrored AVIFs should be unsupported too, but we currently can't detect the mirrored flag using exiftool or ffprobe.
!metadata.is_rotated? && !metadata.is_cropped? && !metadata.is_grid_image? && !metadata.is_animated_avif?
when :webp
!is_animated?
else
true
end
end
def is_corrupt?
image.stats
false
rescue Vips::Error
true
end
def duration
return nil if !is_animated?
video.duration
end
def frame_count
case file_ext
when :gif, :webp
n_pages
when :png
metadata.fetch("PNG:AnimationFrames", 1)
when :avif
video.frame_count
else
nil
end
end
# @return [Integer, nil] The frame count for gif and webp images, or possibly nil if the file doesn't have a frame count or is corrupt.
def n_pages
image.get("n-pages")
rescue Vips::Error
nil
end
def frame_rate
return nil if !is_animated? || frame_count.nil? || duration.nil? || duration == 0
frame_count / duration
end
def channels
image.bands
end
def colorspace
image.interpretation
end
def resize!(max_width, max_height, format: :jpeg, quality: 85, **options)
# @see https://www.libvips.org/API/current/Using-vipsthumbnail.md.html
# @see https://www.libvips.org/API/current/libvips-resample.html#vips-thumbnail
if colorspace.in?(%i[srgb rgb16])
resized_image = thumbnail_image(max_width, height: max_height, import_profile: "srgb", export_profile: "srgb", **options)
elsif colorspace == :cmyk
# Leave CMYK as CMYK for better color accuracy than sRGB.
resized_image = thumbnail_image(max_width, height: max_height, import_profile: "cmyk", export_profile: "cmyk", intent: :relative, **options)
elsif colorspace.in?(%i[b-w grey16]) && has_embedded_profile?
# Convert greyscale to sRGB so that the color profile is properly applied before we strip it.
resized_image = thumbnail_image(max_width, height: max_height, export_profile: "srgb", **options)
elsif colorspace.in?(%i[b-w grey16])
# Otherwise, leave greyscale without a profile as greyscale because
# converting it to sRGB would change it from 1 channel to 3 channels.
resized_image = thumbnail_image(max_width, height: max_height, **options)
else
raise NotImplementedError
end
if resized_image.has_alpha?
resized_image = resized_image.flatten(background: 255)
end
output_file = Tempfile.new(["image-preview-#{md5}", ".#{format.to_s}"])
case format.to_sym
when :jpeg
# https://www.libvips.org/API/current/VipsForeignSave.html#vips-jpegsave
resized_image.jpegsave(output_file.path, Q: quality, strip: true, interlace: true, optimize_coding: true, optimize_scans: true, trellis_quant: true, overshoot_deringing: true, quant_table: 3)
when :webp
# https://www.libvips.org/API/current/VipsForeignSave.html#vips-webpsave
resized_image.webpsave(output_file.path, Q: quality, preset: :drawing, smart_subsample: false, effort: 4, strip: true)
when :avif
# https://www.libvips.org/API/current/VipsForeignSave.html#vips-heifsave
resized_image.heifsave(output_file.path, Q: quality, compression: :av1, effort: 4, strip: true)
else
raise NotImplementedError
end
MediaFile::Image.new(output_file)
end
def preview!(max_width, max_height, **options)
w, h = MediaFile.scale_dimensions(width, height, max_width, max_height)
preview_frame.resize!(w, h, size: :force, **options)
end
def preview_frame
if is_animated?
FFmpeg.new(file).smart_video_preview
else
self
end
end
def is_animated?
frame_count.to_i > 1
end
def is_animated_gif?
file_ext == :gif && is_animated?
end
def is_animated_png?
file_ext == :png && is_animated?
end
def is_animated_webp?
file_ext == :webp && is_animated?
end
def is_animated_avif?
file_ext == :avif && is_animated?
end
# Return true if the image has an embedded ICC color profile.
def has_embedded_profile?
image.icc_import(embedded: true)
true
rescue Vips::Error
false
end
# @return [Vips::Image] the Vips image object for the file
def image
Vips::Image.new_from_file(file.path, fail: strict).autorot
end
def video
FFmpeg.new(file)
end
memoize :image, :video, :dimensions, :is_corrupt?, :is_animated_gif?, :is_animated_png?
end