uploads: move thumbnail generation code to MediaFile.
* Move image thumbnail generation code to MediaFile::Image. * Move video thumbnail generation code to MediaFile::Video. * Move ugoira->webm conversion code to MediaFile::Ugoira. This separates thumbnail generation from the upload process so that it's possible to generate thumbnails outside of uploads.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Adapted from https://github.com/dim/ruby-imagespec/blob/f2f3ce8bb5b1b411f8658e66a891a095261d94c0/lib/image_spec/parser/swf.rb
|
||||
# License: https://github.com/dim/ruby-imagespec/blob/master/LICENSE
|
||||
|
||||
class MediaFile::Flash < MediaFile::Image
|
||||
class MediaFile::Flash < MediaFile
|
||||
def dimensions
|
||||
# Read the entire stream into memory because the
|
||||
# dimensions aren't stored in a standard location
|
||||
|
||||
@@ -1,9 +1,46 @@
|
||||
class MediaFile::Image < MediaFile
|
||||
def dimensions
|
||||
image.size
|
||||
# Taken from ArgyllCMS 2.0.0 (see also: https://ninedegreesbelow.com/photography/srgb-profile-comparison.html)
|
||||
SRGB_PROFILE = "#{Rails.root}/config/sRGB.icm"
|
||||
|
||||
# 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
|
||||
if Vips.at_least_libvips?(8, 8)
|
||||
THUMBNAIL_OPTIONS = { size: :down, linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE }
|
||||
CROP_OPTIONS = { crop: :attention, linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE }
|
||||
else
|
||||
THUMBNAIL_OPTIONS = { size: :down, linear: false, auto_rotate: false, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE }
|
||||
CROP_OPTIONS = { crop: :attention, linear: false, auto_rotate: false, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE }
|
||||
end
|
||||
|
||||
def dimensions
|
||||
image.size
|
||||
rescue Vips::Error
|
||||
[0, 0]
|
||||
end
|
||||
|
||||
# https://github.com/jcupitt/libvips/wiki/HOWTO----Image-shrinking
|
||||
# http://jcupitt.github.io/libvips/API/current/Using-vipsthumbnail.md.html
|
||||
def preview(width, height)
|
||||
output_file = Tempfile.new(["image-preview", ".jpg"])
|
||||
resized_image = 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 = image.thumbnail_image(width, height: height, **CROP_OPTIONS)
|
||||
resized_image.jpegsave(output_file.path, **JPEG_OPTIONS)
|
||||
|
||||
MediaFile::Image.new(output_file)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def image
|
||||
@image ||= Vips::Image.new_from_file(file.path)
|
||||
@image ||= Vips::Image.new_from_file(file.path, fail: true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,93 @@
|
||||
class MediaFile::Ugoira < MediaFile
|
||||
def dimensions
|
||||
tempfile = Tempfile.new
|
||||
folder = Zip::File.new(file.path)
|
||||
folder.first.extract(tempfile.path) { true }
|
||||
extend Memoist
|
||||
class Error < StandardError; end
|
||||
attr_reader :frame_data
|
||||
|
||||
image_file = MediaFile.open(tempfile)
|
||||
image_file.dimensions
|
||||
ensure
|
||||
image_file.close
|
||||
tempfile.close!
|
||||
def self.conversion_enabled?
|
||||
system("ffmpeg -version > /dev/null") && system("mkvmerge --version > /dev/null")
|
||||
end
|
||||
|
||||
def initialize(file, frame_data: {}, **options)
|
||||
super(file, **options)
|
||||
@frame_data = frame_data
|
||||
end
|
||||
|
||||
def close
|
||||
file.close
|
||||
zipfile.close
|
||||
preview_frame.close
|
||||
end
|
||||
|
||||
def dimensions
|
||||
preview_frame.dimensions
|
||||
end
|
||||
|
||||
def preview(width, height)
|
||||
preview_frame.preview(width, height)
|
||||
end
|
||||
|
||||
def crop(width, height)
|
||||
preview_frame.crop(width, height)
|
||||
end
|
||||
|
||||
# XXX should take width and height and resize image
|
||||
def convert
|
||||
raise NotImplementedError, "can't convert ugoira to webm: ffmpeg or mkvmerge not installed" unless self.class.conversion_enabled?
|
||||
|
||||
Dir.mktmpdir("ugoira-#{md5}") do |tmpdir|
|
||||
output_file = Tempfile.new(["ugoira-conversion", ".webm"], binmode: true)
|
||||
|
||||
FileUtils.mkdir_p("#{tmpdir}/images")
|
||||
|
||||
zipfile.each do |entry|
|
||||
path = File.join(tmpdir, "images", entry.name)
|
||||
entry.extract(path)
|
||||
end
|
||||
|
||||
# Duplicate last frame to avoid it being displayed only for a very short amount of time.
|
||||
last_file_name = zipfile.entries.last.name
|
||||
last_file_name =~ /\A(\d{6})(\.\w{,4})\Z/
|
||||
new_last_index = $1.to_i + 1
|
||||
file_ext = $2
|
||||
new_last_filename = ("%06d" % new_last_index) + file_ext
|
||||
path_from = File.join(tmpdir, "images", last_file_name)
|
||||
path_to = File.join(tmpdir, "images", new_last_filename)
|
||||
FileUtils.cp(path_from, path_to)
|
||||
|
||||
delay_sum = 0
|
||||
timecodes_path = File.join(tmpdir, "timecodes.tc")
|
||||
File.open(timecodes_path, "w+") do |f|
|
||||
f.write("# timecode format v2\n")
|
||||
frame_data.each do |img|
|
||||
f.write("#{delay_sum}\n")
|
||||
delay_sum += (img["delay"] || img["delay_msec"])
|
||||
end
|
||||
f.write("#{delay_sum}\n")
|
||||
f.write("#{delay_sum}\n")
|
||||
end
|
||||
|
||||
ext = zipfile.first.name.match(/\.(\w{,4})$/)[1]
|
||||
ffmpeg_out, status = Open3.capture2e("ffmpeg -i #{tmpdir}/images/%06d.#{ext} -codec:v libvpx -crf 4 -b:v 5000k -an #{tmpdir}/tmp.webm")
|
||||
raise Error, "ffmpeg failed: #{ffmpeg_out}" unless status.success?
|
||||
|
||||
mkvmerge_out, status = Open3.capture2e("mkvmerge -o #{output_file.path} --webm --timecodes 0:#{tmpdir}/timecodes.tc #{tmpdir}/tmp.webm")
|
||||
raise Error, "mkvmerge failed: #{mkvmerge_out}" unless status.success?
|
||||
|
||||
MediaFile.open(output_file)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def zipfile
|
||||
Zip::File.new(file.path)
|
||||
end
|
||||
|
||||
def preview_frame
|
||||
tempfile = Tempfile.new("ugoira-preview", binmode: true)
|
||||
zipfile.entries.first.extract(tempfile.path) { true } # 'true' means overwrite the existing tempfile.
|
||||
MediaFile.open(tempfile)
|
||||
end
|
||||
|
||||
memoize :zipfile, :preview_frame
|
||||
end
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
class MediaFile::Video < MediaFile
|
||||
extend Memoist
|
||||
|
||||
def dimensions
|
||||
[video.width, video.height]
|
||||
end
|
||||
|
||||
def video
|
||||
@video ||= FFMPEG::Movie.new(file.path)
|
||||
def preview(max_width, max_height)
|
||||
preview_frame.preview(max_width, max_height)
|
||||
end
|
||||
|
||||
def crop(max_width, max_height)
|
||||
preview_frame.crop(max_width, max_height)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def video
|
||||
FFMPEG::Movie.new(file.path)
|
||||
end
|
||||
|
||||
def preview_frame
|
||||
vp = Tempfile.new(["video-preview", ".jpg"], binmode: true)
|
||||
video.screenshot(vp.path, seek_time: 0)
|
||||
MediaFile.open(vp.path)
|
||||
end
|
||||
|
||||
memoize :video, :preview_frame
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user