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.
125 lines
3.4 KiB
Ruby
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
|