Files
danbooru/app/logical/media_file/image.rb
evazion 2595f18b2f posts: fix calculation of animated PNG duration.
Fix certain animated PNGs returning NaN as the duration because the
frame rate was being reported as "0/0" by FFMpeg. This happens when the
animation has zero delay between frames. This is supposed to mean a PNG
with an infinitely fast frame rate, but in practice browsers limit it to
around 10FPS. The exact frame rate browsers will use is unknown and
implementation defined.
2021-10-06 21:04:36 -05:00

125 lines
3.4 KiB
Ruby

# 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
# http://jcupitt.github.io/libvips/API/current/VipsForeignSave.html#vips-jpegsave
JPEG_OPTIONS = { Q: 90, background: 255, strip: true, interlace: true, optimize_coding: true }
# http://jcupitt.github.io/libvips/API/current/libvips-resample.html#vips-thumbnail
THUMBNAIL_OPTIONS = { size: :down, linear: false }
CROP_OPTIONS = { crop: :attention, linear: false }
def dimensions
image.size
rescue Vips::Error
[0, 0]
end
def is_corrupt?
image.stats
false
rescue Vips::Error
true
end
def duration
if is_animated_gif?
if metadata.has_key?("GIF:Duration")
# GIF:Duration => "9.03 s"
metadata["GIF:Duration"].to_f
else
# If GIF:Duration is absent then it means the GIF has an unlimited
# framerate. In this situation we assume the browser will play the GIF
# at 10 FPS; this is browser dependent.
#
# A GIF consists of a series of frames, each frame having a separate frame
# delay. The duration of the GIF is the sum of each frame delay. If the frame
# delay of each frame is zero, then it means the GIF has an unlimited framerate
# and should be played as fast as possible. In reality, browsers cap the
# framerate to around 10FPS.
(frame_count * (1.0/10.0)).round(2)
end
elsif is_animated_png?
frame_count.to_f / frame_rate
else
nil
end
end
def frame_count
if file_ext == :gif
image.get("n-pages")
elsif file_ext == :png
metadata.fetch("PNG:AnimationFrames", 1)
else
nil
end
end
def frame_rate
if is_animated_gif?
frame_count / duration
elsif is_animated_png?
# XXX As with GIFs, animated PNGs can have an unspecified frame rate.
# Assume 10FPS if the frame rate is unspecified.
video.frame_rate.presence || 10.0
else
nil
end
end
def channels
image.bands
end
def colorspace
image.interpretation
end
# @see https://github.com/jcupitt/libvips/wiki/HOWTO----Image-shrinking
# @see http://jcupitt.github.io/libvips/API/current/Using-vipsthumbnail.md.html
def preview(width, height)
output_file = Tempfile.new(["image-preview", ".jpg"])
resized_image = preview_frame.image.thumbnail_image(width, height: height, **THUMBNAIL_OPTIONS)
resized_image.jpegsave(output_file.path, **JPEG_OPTIONS)
MediaFile::Image.new(output_file)
end
def crop(width, height)
output_file = Tempfile.new(["image-crop", ".jpg"])
resized_image = preview_frame.image.thumbnail_image(width, height: height, **CROP_OPTIONS)
resized_image.jpegsave(output_file.path, **JPEG_OPTIONS)
MediaFile::Image.new(output_file)
end
def preview_frame
if is_animated?
FFmpeg.new(file).smart_video_preview
else
self
end
end
def is_animated_gif?
file_ext == :gif && is_animated?
end
def is_animated_png?
file_ext == :png && is_animated?
end
# @return [Vips::Image] the Vips image object for the file
def image
Vips::Image.new_from_file(file.path, fail: true).autorot
end
def video
FFmpeg.new(file)
end
memoize :image, :video, :dimensions, :is_corrupt?, :is_animated_gif?, :is_animated_png?
end