factor out resizing logic
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -126,6 +126,7 @@ gem "ruby-prof"
|
||||
gem "ruby-prof-speedscope"
|
||||
gem "ruby-vips"
|
||||
gem "dhash-vips"
|
||||
gem "ffmpeg", git: "https://github.com/instructure/ruby-ffmpeg", tag: "v6.1.2"
|
||||
gem "table_print"
|
||||
gem "zstd-ruby"
|
||||
gem "rouge"
|
||||
|
||||
10
Gemfile.lock
10
Gemfile.lock
@@ -5,6 +5,14 @@ GIT
|
||||
specs:
|
||||
dtext_rb (1.11.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/instructure/ruby-ffmpeg
|
||||
revision: a3404b8fa275e2eb9549f074906461b0266a70ea
|
||||
tag: v6.1.2
|
||||
specs:
|
||||
ffmpeg (6.1.2)
|
||||
multi_json (~> 1.8)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/railsjazz/rails_live_reload
|
||||
revision: dcd3b73904594e2c5134c2f6e05954f3937a8d29
|
||||
@@ -269,6 +277,7 @@ GEM
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.4)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
neighbor (0.5.1)
|
||||
activerecord (>= 7)
|
||||
net-imap (0.5.4)
|
||||
@@ -582,6 +591,7 @@ DEPENDENCIES
|
||||
dtext_rb!
|
||||
factory_bot_rails
|
||||
faiss
|
||||
ffmpeg!
|
||||
good_job (~> 4.6)
|
||||
htmlbeautifier
|
||||
http (~> 5.2)
|
||||
|
||||
@@ -10,6 +10,8 @@ module Domain::StaticFileJobHelper
|
||||
|
||||
sig { params(post_file: Domain::PostFile).void }
|
||||
def download_post_file(post_file)
|
||||
should_enqueue_thumbnail_job = T.let(false, T::Boolean)
|
||||
|
||||
if post_file.state_terminal_error?
|
||||
logger.error(format_tags("terminal error state, skipping"))
|
||||
return
|
||||
@@ -53,6 +55,7 @@ module Domain::StaticFileJobHelper
|
||||
post_file.last_status_code = response.status_code
|
||||
logger.tagged(make_arg_tag(response.log_entry)) do
|
||||
if response.status_code == 200
|
||||
should_enqueue_thumbnail_job = true
|
||||
post_file.state_ok!
|
||||
post_file.retry_count = 0
|
||||
logger.info(format_tags("downloaded file"))
|
||||
@@ -82,5 +85,8 @@ module Domain::StaticFileJobHelper
|
||||
end
|
||||
ensure
|
||||
post_file.save! if post_file
|
||||
if should_enqueue_thumbnail_job
|
||||
defer_job(Domain::PostFileThumbnailJob, { post_file: })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
57
app/lib/loaded_media.rb
Normal file
57
app/lib/loaded_media.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LoadedMedia
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
abstract!
|
||||
|
||||
sig do
|
||||
params(content_type: String, media_path: String).returns(
|
||||
T.nilable(LoadedMedia),
|
||||
)
|
||||
end
|
||||
def self.from_file(content_type, media_path)
|
||||
case content_type
|
||||
when %r{image/gif}
|
||||
LoadedMedia::Gif.new(media_path)
|
||||
when %r{image/jpeg}, %r{image/jpg}, %r{image/png}, %r{image/bmp}
|
||||
LoadedMedia::StaticImage.new(media_path)
|
||||
when %r{video/webm}
|
||||
LoadedMedia::Webm.new(media_path)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
sig { abstract.returns(Integer) }
|
||||
def num_frames
|
||||
end
|
||||
|
||||
sig do
|
||||
abstract
|
||||
.params(frame: Integer, path: String, options: ThumbnailOptions)
|
||||
.void
|
||||
end
|
||||
def write_frame_thumbnail(frame, path, options)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
sig(:final) do
|
||||
params(image: Vips::Image, path: String, options: ThumbnailOptions).void
|
||||
end
|
||||
def write_image_thumbnail(image, path, options)
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
tmp_path =
|
||||
File.join(BlobFile::TMP_DIR, "thumbnail-#{SecureRandom.uuid}.jpg")
|
||||
image.thumbnail_image(
|
||||
options.width,
|
||||
height: options.height,
|
||||
size: options.size,
|
||||
).jpegsave(tmp_path, interlace: options.interlace, Q: options.quality)
|
||||
FileUtils.mv(tmp_path, path)
|
||||
ensure
|
||||
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
|
||||
end
|
||||
end
|
||||
29
app/lib/loaded_media/gif.rb
Normal file
29
app/lib/loaded_media/gif.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LoadedMedia::Gif < LoadedMedia
|
||||
sig { params(media_path: String).void }
|
||||
def initialize(media_path)
|
||||
@vips_image = T.let(Vips::Image.gifload(media_path, n: -1), Vips::Image)
|
||||
@num_frames = T.let(@vips_image.get("n-pages"), Integer)
|
||||
@width = T.let(@vips_image.width, Integer)
|
||||
@height = T.let(@vips_image.height / @num_frames, Integer)
|
||||
end
|
||||
|
||||
sig { override.returns(Integer) }
|
||||
def num_frames
|
||||
@num_frames
|
||||
end
|
||||
|
||||
sig do
|
||||
override
|
||||
.params(frame: Integer, path: String, options: ThumbnailOptions)
|
||||
.void
|
||||
end
|
||||
def write_frame_thumbnail(frame, path, options)
|
||||
# crop out the frame we're interested in
|
||||
# then save it to the path
|
||||
image = @vips_image.crop(0, frame * @height, @width, @height)
|
||||
write_image_thumbnail(image, path, options)
|
||||
end
|
||||
end
|
||||
23
app/lib/loaded_media/static_image.rb
Normal file
23
app/lib/loaded_media/static_image.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LoadedMedia::StaticImage < LoadedMedia
|
||||
sig { params(media_path: String).void }
|
||||
def initialize(media_path)
|
||||
@vips_image = T.let(Vips::Image.new_from_file(media_path), Vips::Image)
|
||||
end
|
||||
|
||||
sig { override.returns(Integer) }
|
||||
def num_frames
|
||||
1
|
||||
end
|
||||
|
||||
sig do
|
||||
override
|
||||
.params(frame: Integer, path: String, options: ThumbnailOptions)
|
||||
.void
|
||||
end
|
||||
def write_frame_thumbnail(frame, path, options)
|
||||
write_image_thumbnail(@vips_image, path, options)
|
||||
end
|
||||
end
|
||||
14
app/lib/loaded_media/thumbnail_options.rb
Normal file
14
app/lib/loaded_media/thumbnail_options.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LoadedMedia::ThumbnailOptions < T::ImmutableStruct
|
||||
extend T::Sig
|
||||
const :width, Integer
|
||||
const :height, T.nilable(Integer)
|
||||
# 0..100
|
||||
const :quality, T.nilable(Integer)
|
||||
# :both, :up, :down, :force
|
||||
const :size, Symbol
|
||||
const :interlace, T::Boolean
|
||||
const :for_frames, T::Array[Numeric]
|
||||
end
|
||||
47
app/lib/loaded_media/webm.rb
Normal file
47
app/lib/loaded_media/webm.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
FFMPEG.logger = Logger.new(nil)
|
||||
class LoadedMedia::Webm < LoadedMedia
|
||||
include HasColorLogger
|
||||
|
||||
sig { params(media_path: String).void }
|
||||
def initialize(media_path)
|
||||
@media = T.let(FFMPEG::Media.new(media_path), FFMPEG::Media)
|
||||
duration = @media.duration
|
||||
frame_rate = @media.frame_rate
|
||||
@num_frames = T.let((frame_rate * duration).to_i, Integer)
|
||||
@duration = T.let(duration, Float)
|
||||
|
||||
raise("no frames found in webm") if @num_frames.zero?
|
||||
end
|
||||
|
||||
sig { override.returns(Integer) }
|
||||
def num_frames
|
||||
@num_frames
|
||||
end
|
||||
|
||||
sig do
|
||||
override
|
||||
.params(frame: Integer, path: String, options: ThumbnailOptions)
|
||||
.void
|
||||
end
|
||||
def write_frame_thumbnail(frame, path, options)
|
||||
frame_time = (frame.to_f * @duration) / @num_frames
|
||||
frame_time = frame_time.clamp(0.0..(@duration * 0.99))
|
||||
|
||||
logger.info(
|
||||
format_tags(make_tag("frame", frame), make_tag("frame_time", frame_time)),
|
||||
)
|
||||
|
||||
tmp_path =
|
||||
File.join(BlobFile::TMP_DIR, "webm-#{frame}-#{SecureRandom.uuid}.png")
|
||||
FileUtils.mkdir_p(File.dirname(tmp_path))
|
||||
|
||||
@media.screenshot(tmp_path, { seek_time: frame_time })
|
||||
raise("screenshot @ #{frame} failed") unless File.exist?(tmp_path)
|
||||
image = Vips::Image.new_from_file(tmp_path)
|
||||
write_image_thumbnail(image, path, options)
|
||||
ensure
|
||||
FileUtils.rm_rf(tmp_path) if tmp_path && File.exist?(tmp_path)
|
||||
end
|
||||
end
|
||||
@@ -12,8 +12,7 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
validates :frame, presence: true
|
||||
validate :validate_content_type!
|
||||
|
||||
THUMB_ROOT_DIR =
|
||||
T.let(File.join(BlobFile::ROOT_DIR, "domain_post_file_thumbnails"), String)
|
||||
THUMB_ROOT_DIR = T.let(File.join(BlobFile::ROOT_DIR, "thumbnails"), String)
|
||||
|
||||
THUMB_FILE_PATH_PATTERN = T.let([2, 2, 1].freeze, T::Array[Integer])
|
||||
|
||||
@@ -41,23 +40,11 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
T::Array[Regexp],
|
||||
)
|
||||
|
||||
class ThumbnailOptions < T::ImmutableStruct
|
||||
extend T::Sig
|
||||
const :width, Integer
|
||||
const :height, T.nilable(Integer)
|
||||
# 0..100
|
||||
const :quality, T.nilable(Integer)
|
||||
# :both, :up, :down, :force
|
||||
const :size, Symbol
|
||||
const :interlace, T::Boolean
|
||||
const :for_frames, T::Array[Numeric]
|
||||
end
|
||||
|
||||
THUMB_TYPE_TO_OPTIONS =
|
||||
T.let(
|
||||
{
|
||||
content_container:
|
||||
ThumbnailOptions.new(
|
||||
LoadedMedia::ThumbnailOptions.new(
|
||||
width: 768,
|
||||
height: 2048,
|
||||
quality: 95,
|
||||
@@ -66,7 +53,7 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
for_frames: [0],
|
||||
),
|
||||
size_32_32:
|
||||
ThumbnailOptions.new(
|
||||
LoadedMedia::ThumbnailOptions.new(
|
||||
width: 32,
|
||||
height: 32,
|
||||
quality: 95,
|
||||
@@ -75,7 +62,7 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
for_frames: [0.0, 0.1, 0.5, 0.9, 1.0],
|
||||
),
|
||||
},
|
||||
T::Hash[Symbol, ThumbnailOptions],
|
||||
T::Hash[Symbol, LoadedMedia::ThumbnailOptions],
|
||||
)
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
@@ -86,8 +73,9 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
sha256_hex = HexUtil.bin2hex(sha256)
|
||||
[
|
||||
THUMB_ROOT_DIR,
|
||||
thumb_type.to_s,
|
||||
*BlobFile.path_segments(THUMB_FILE_PATH_PATTERN, sha256_hex),
|
||||
].join("/") + ".jpg"
|
||||
].join("/") + "-#{frame.to_i.to_s.rjust(2, "0")}.jpg"
|
||||
end
|
||||
|
||||
sig { params(post_file: Domain::PostFile).returns(T::Boolean) }
|
||||
@@ -113,8 +101,10 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
blob_file_path = post_file.blob&.absolute_file_path
|
||||
return [] unless blob_file_path
|
||||
|
||||
vips_image = Vips::Image.new_from_file(blob_file_path)
|
||||
num_frames = frames_for_image(content_type, vips_image)
|
||||
media = LoadedMedia.from_file(content_type, blob_file_path)
|
||||
return [] unless media
|
||||
|
||||
num_frames = media.num_frames
|
||||
logger.info(format_tags(make_tag("num_frames", num_frames)))
|
||||
return [] if num_frames.zero?
|
||||
|
||||
@@ -131,14 +121,14 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
logger.info(format_tags("creating thumbnail"))
|
||||
|
||||
# get the number of frames in the post file
|
||||
frame_numbers =
|
||||
frames_to_thumbnail =
|
||||
options
|
||||
.for_frames
|
||||
.map { |frame_fraction| (frame_fraction * (num_frames - 1)).to_i }
|
||||
.uniq
|
||||
.sort
|
||||
|
||||
frame_numbers.each do |frame|
|
||||
frames_to_thumbnail.each do |frame|
|
||||
logger.tagged(make_tag("frame", frame)) do
|
||||
thumbnail = post_file.thumbnails.build(thumb_type:, frame:)
|
||||
unless thumb_file_path = thumbnail.absolute_file_path
|
||||
@@ -149,24 +139,7 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
if File.exist?(thumb_file_path)
|
||||
logger.info(format_tags("thumbnail file already exists"))
|
||||
else
|
||||
FileUtils.mkdir_p(File.dirname(thumb_file_path))
|
||||
tmp_file_path =
|
||||
File.join(
|
||||
BlobFile::TMP_DIR,
|
||||
"thumb-#{thumb_type}-#{SecureRandom.uuid}",
|
||||
)
|
||||
logger.info(format_tags("writing thumbnail file"))
|
||||
# TODO - check that this works for gifs, mp4, etc
|
||||
vips_image.thumbnail_image(
|
||||
options.width,
|
||||
height: options.height,
|
||||
size: options.size,
|
||||
).jpegsave(
|
||||
tmp_file_path,
|
||||
interlace: options.interlace,
|
||||
Q: options.quality,
|
||||
)
|
||||
FileUtils.mv(tmp_file_path, thumb_file_path)
|
||||
media.write_frame_thumbnail(frame, thumb_file_path, options)
|
||||
end
|
||||
|
||||
thumbnail.save!
|
||||
@@ -179,18 +152,6 @@ class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
sig { params(content_type: String, vips_image: Vips::Image).returns(Integer) }
|
||||
def self.frames_for_image(content_type, vips_image)
|
||||
case content_type
|
||||
when %r{image/gif}
|
||||
vips_image.get("n-pages")
|
||||
when %r{video/webm}, %r{video/mp4}
|
||||
vips_image.get("n-frames")
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
|
||||
4
justfile
4
justfile
@@ -17,7 +17,9 @@ psql-dump-domain-fa-favs:
|
||||
@psql -P pager=off -c 'select user_id, post_id, 1 from domain_fa_favs limit 10000000;' -d redux_prod -h 10.166.33.171 -U scraper_redux -t -A -F ' '
|
||||
|
||||
test:
|
||||
bundle exec srb tc && RAILS_ENV=test bin/rake parallel:spec
|
||||
bundle exec srb tc
|
||||
rm -rf tmp/blob_files_test/thumbnails
|
||||
RAILS_ENV=test bin/rake parallel:spec
|
||||
|
||||
tc *args:
|
||||
bundle exec srb tc {{args}}
|
||||
|
||||
16
sorbet/rbi/dsl/loaded_media/webm.rbi
generated
Normal file
16
sorbet/rbi/dsl/loaded_media/webm.rbi
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
# typed: true
|
||||
|
||||
# DO NOT EDIT MANUALLY
|
||||
# This is an autogenerated file for dynamic methods in `LoadedMedia::Webm`.
|
||||
# Please instead update this file by running `bin/tapioca dsl LoadedMedia::Webm`.
|
||||
|
||||
|
||||
class LoadedMedia::Webm
|
||||
sig { returns(ColorLogger) }
|
||||
def logger; end
|
||||
|
||||
class << self
|
||||
sig { returns(ColorLogger) }
|
||||
def logger; end
|
||||
end
|
||||
end
|
||||
1037
sorbet/rbi/gems/ffmpeg@6.1.2-a3404b8fa275e2eb9549f074906461b0266a70ea.rbi
generated
Normal file
1037
sorbet/rbi/gems/ffmpeg@6.1.2-a3404b8fa275e2eb9549f074906461b0266a70ea.rbi
generated
Normal file
File diff suppressed because it is too large
Load Diff
268
sorbet/rbi/gems/multi_json@1.15.0.rbi
generated
Normal file
268
sorbet/rbi/gems/multi_json@1.15.0.rbi
generated
Normal file
@@ -0,0 +1,268 @@
|
||||
# typed: true
|
||||
|
||||
# DO NOT EDIT MANUALLY
|
||||
# This is an autogenerated file for types exported from the `multi_json` gem.
|
||||
# Please instead update this file by running `bin/tapioca gem multi_json`.
|
||||
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#1
|
||||
module MultiJson
|
||||
include ::MultiJson::Options
|
||||
extend ::MultiJson::Options
|
||||
extend ::MultiJson
|
||||
|
||||
# Get the current adapter class.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#70
|
||||
def adapter; end
|
||||
|
||||
# Set the JSON parser utilizing a symbol, string, or class.
|
||||
# Supported by default are:
|
||||
#
|
||||
# * <tt>:oj</tt>
|
||||
# * <tt>:json_gem</tt>
|
||||
# * <tt>:json_pure</tt>
|
||||
# * <tt>:ok_json</tt>
|
||||
# * <tt>:yajl</tt>
|
||||
# * <tt>:nsjsonserialization</tt> (MacRuby only)
|
||||
# * <tt>:gson</tt> (JRuby only)
|
||||
# * <tt>:jr_jackson</tt> (JRuby only)
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#90
|
||||
def adapter=(new_adapter); end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#26
|
||||
def cached_options(*_arg0); end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#129
|
||||
def current_adapter(options = T.unsafe(nil)); end
|
||||
|
||||
# Decode a JSON string into Ruby.
|
||||
#
|
||||
# <b>Options</b>
|
||||
#
|
||||
# <tt>:symbolize_keys</tt> :: If true, will use symbols instead of strings for the keys.
|
||||
# <tt>:adapter</tt> :: If set, the selected adapter will be used for this call.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#119
|
||||
def decode(string, options = T.unsafe(nil)); end
|
||||
|
||||
# The default adapter based on what you currently
|
||||
# have loaded and installed. First checks to see
|
||||
# if any adapters are already loaded, then checks
|
||||
# to see which are installed if none are loaded.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#46
|
||||
def default_adapter; end
|
||||
|
||||
# The default adapter based on what you currently
|
||||
# have loaded and installed. First checks to see
|
||||
# if any adapters are already loaded, then checks
|
||||
# to see which are installed if none are loaded.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#46
|
||||
def default_engine; end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#18
|
||||
def default_options; end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#11
|
||||
def default_options=(value); end
|
||||
|
||||
# Encodes a Ruby object as JSON.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#138
|
||||
def dump(object, options = T.unsafe(nil)); end
|
||||
|
||||
# Encodes a Ruby object as JSON.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#138
|
||||
def encode(object, options = T.unsafe(nil)); end
|
||||
|
||||
# Get the current adapter class.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#70
|
||||
def engine; end
|
||||
|
||||
# Set the JSON parser utilizing a symbol, string, or class.
|
||||
# Supported by default are:
|
||||
#
|
||||
# * <tt>:oj</tt>
|
||||
# * <tt>:json_gem</tt>
|
||||
# * <tt>:json_pure</tt>
|
||||
# * <tt>:ok_json</tt>
|
||||
# * <tt>:yajl</tt>
|
||||
# * <tt>:nsjsonserialization</tt> (MacRuby only)
|
||||
# * <tt>:gson</tt> (JRuby only)
|
||||
# * <tt>:jr_jackson</tt> (JRuby only)
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#90
|
||||
def engine=(new_adapter); end
|
||||
|
||||
# Decode a JSON string into Ruby.
|
||||
#
|
||||
# <b>Options</b>
|
||||
#
|
||||
# <tt>:symbolize_keys</tt> :: If true, will use symbols instead of strings for the keys.
|
||||
# <tt>:adapter</tt> :: If set, the selected adapter will be used for this call.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#119
|
||||
def load(string, options = T.unsafe(nil)); end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#98
|
||||
def load_adapter(new_adapter); end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#26
|
||||
def reset_cached_options!(*_arg0); end
|
||||
|
||||
# Set the JSON parser utilizing a symbol, string, or class.
|
||||
# Supported by default are:
|
||||
#
|
||||
# * <tt>:oj</tt>
|
||||
# * <tt>:json_gem</tt>
|
||||
# * <tt>:json_pure</tt>
|
||||
# * <tt>:ok_json</tt>
|
||||
# * <tt>:yajl</tt>
|
||||
# * <tt>:nsjsonserialization</tt> (MacRuby only)
|
||||
# * <tt>:gson</tt> (JRuby only)
|
||||
# * <tt>:jr_jackson</tt> (JRuby only)
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#90
|
||||
def use(new_adapter); end
|
||||
|
||||
# Executes passed block using specified adapter.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#144
|
||||
def with_adapter(new_adapter); end
|
||||
|
||||
# Executes passed block using specified adapter.
|
||||
#
|
||||
# source://multi_json//lib/multi_json.rb#144
|
||||
def with_engine(new_adapter); end
|
||||
|
||||
private
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#155
|
||||
def load_adapter_from_string_name(name); end
|
||||
end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#31
|
||||
MultiJson::ALIASES = T.let(T.unsafe(nil), Hash)
|
||||
|
||||
# source://multi_json//lib/multi_json/adapter_error.rb#2
|
||||
class MultiJson::AdapterError < ::ArgumentError
|
||||
# Returns the value of attribute cause.
|
||||
#
|
||||
# source://multi_json//lib/multi_json/adapter_error.rb#3
|
||||
def cause; end
|
||||
|
||||
class << self
|
||||
# source://multi_json//lib/multi_json/adapter_error.rb#5
|
||||
def build(original_exception); end
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy support
|
||||
#
|
||||
# source://multi_json//lib/multi_json/parse_error.rb#16
|
||||
MultiJson::DecodeError = MultiJson::ParseError
|
||||
|
||||
# source://multi_json//lib/multi_json/parse_error.rb#16
|
||||
MultiJson::LoadError = MultiJson::ParseError
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#2
|
||||
module MultiJson::Options
|
||||
# source://multi_json//lib/multi_json/options.rb#25
|
||||
def default_dump_options; end
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#21
|
||||
def default_load_options; end
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#17
|
||||
def dump_options(*args); end
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#8
|
||||
def dump_options=(options); end
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#13
|
||||
def load_options(*args); end
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#3
|
||||
def load_options=(options); end
|
||||
|
||||
private
|
||||
|
||||
# source://multi_json//lib/multi_json/options.rb#31
|
||||
def get_options(options, *args); end
|
||||
end
|
||||
|
||||
# source://multi_json//lib/multi_json/options_cache.rb#2
|
||||
module MultiJson::OptionsCache
|
||||
extend ::MultiJson::OptionsCache
|
||||
|
||||
# source://multi_json//lib/multi_json/options_cache.rb#10
|
||||
def fetch(type, key, &block); end
|
||||
|
||||
# source://multi_json//lib/multi_json/options_cache.rb#5
|
||||
def reset; end
|
||||
|
||||
private
|
||||
|
||||
# source://multi_json//lib/multi_json/options_cache.rb#24
|
||||
def write(cache, key); end
|
||||
end
|
||||
|
||||
# Normally MultiJson is used with a few option sets for both dump/load
|
||||
# methods. When options are generated dynamically though, every call would
|
||||
# cause a cache miss and the cache would grow indefinitely. To prevent
|
||||
# this, we just reset the cache every time the number of keys outgrows
|
||||
# 1000.
|
||||
#
|
||||
# source://multi_json//lib/multi_json/options_cache.rb#22
|
||||
MultiJson::OptionsCache::MAX_CACHE_SIZE = T.let(T.unsafe(nil), Integer)
|
||||
|
||||
# source://multi_json//lib/multi_json/parse_error.rb#2
|
||||
class MultiJson::ParseError < ::StandardError
|
||||
# Returns the value of attribute cause.
|
||||
#
|
||||
# source://multi_json//lib/multi_json/parse_error.rb#3
|
||||
def cause; end
|
||||
|
||||
# Returns the value of attribute data.
|
||||
#
|
||||
# source://multi_json//lib/multi_json/parse_error.rb#3
|
||||
def data; end
|
||||
|
||||
class << self
|
||||
# source://multi_json//lib/multi_json/parse_error.rb#5
|
||||
def build(original_exception, data); end
|
||||
end
|
||||
end
|
||||
|
||||
# source://multi_json//lib/multi_json.rb#33
|
||||
MultiJson::REQUIREMENT_MAP = T.let(T.unsafe(nil), Array)
|
||||
|
||||
# source://multi_json//lib/multi_json/version.rb#16
|
||||
MultiJson::VERSION = T.let(T.unsafe(nil), String)
|
||||
|
||||
# source://multi_json//lib/multi_json/version.rb#2
|
||||
class MultiJson::Version
|
||||
class << self
|
||||
# @return [String]
|
||||
#
|
||||
# source://multi_json//lib/multi_json/version.rb#10
|
||||
def to_s; end
|
||||
end
|
||||
end
|
||||
|
||||
# source://multi_json//lib/multi_json/version.rb#3
|
||||
MultiJson::Version::MAJOR = T.let(T.unsafe(nil), Integer)
|
||||
|
||||
# source://multi_json//lib/multi_json/version.rb#4
|
||||
MultiJson::Version::MINOR = T.let(T.unsafe(nil), Integer)
|
||||
|
||||
# source://multi_json//lib/multi_json/version.rb#5
|
||||
MultiJson::Version::PATCH = T.let(T.unsafe(nil), Integer)
|
||||
|
||||
# source://multi_json//lib/multi_json/version.rb#6
|
||||
MultiJson::Version::PRE = T.let(T.unsafe(nil), T.untyped)
|
||||
@@ -40,4 +40,15 @@ class Vips::Image
|
||||
sig { params(filename: String, opts: T.untyped).returns(String) }
|
||||
def jpegsave(filename, **opts)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
left: Integer,
|
||||
top: Integer,
|
||||
width: Integer,
|
||||
height: Integer,
|
||||
).returns(Vips::Image)
|
||||
end
|
||||
def crop(left, top, width, height)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,6 +21,32 @@ FactoryBot.define do
|
||||
end
|
||||
end
|
||||
|
||||
trait :gif_blob do
|
||||
content_type { "image/gif" }
|
||||
transient do
|
||||
contents do
|
||||
File.read(
|
||||
Rails.root.join(
|
||||
"test/fixtures/files/images/e621-5418755-animated.gif",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
trait :webm_blob do
|
||||
content_type { "video/webm" }
|
||||
transient do
|
||||
contents do
|
||||
File.read(
|
||||
Rails.root.join(
|
||||
"test/fixtures/files/images/e621-5421402-animated.webm",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
trait :text_blob do
|
||||
content_type { "text/plain" }
|
||||
transient { contents { "test content #{SecureRandom.alphanumeric(10)}" } }
|
||||
|
||||
@@ -60,6 +60,16 @@ FactoryBot.define do
|
||||
log_entry { create(:http_log_entry, :text_entry) }
|
||||
url_str { log_entry.uri_str }
|
||||
end
|
||||
|
||||
trait :gif_file do
|
||||
url_str { "https://example.com/image.gif" }
|
||||
log_entry { create(:http_log_entry, :gif_entry) }
|
||||
end
|
||||
|
||||
trait :webm_file do
|
||||
url_str { "https://example.com/image.webm" }
|
||||
log_entry { create(:http_log_entry, :webm_entry) }
|
||||
end
|
||||
end
|
||||
|
||||
FactoryBot.define do
|
||||
|
||||
@@ -61,5 +61,15 @@ FactoryBot.define do
|
||||
content_type { "text/plain" }
|
||||
response { create(:blob_file, :text_blob) }
|
||||
end
|
||||
|
||||
trait :gif_entry do
|
||||
content_type { "image/gif" }
|
||||
response { create(:blob_file, :gif_blob) }
|
||||
end
|
||||
|
||||
trait :webm_entry do
|
||||
content_type { "video/webm" }
|
||||
response { create(:blob_file, :webm_blob) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
132
spec/lib/loaded_media_spec.rb
Normal file
132
spec/lib/loaded_media_spec.rb
Normal file
@@ -0,0 +1,132 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
require "fileutils"
|
||||
require "securerandom"
|
||||
|
||||
RSpec.describe LoadedMedia do
|
||||
let(:gif_fixture_path) do
|
||||
Rails.root.join("test/fixtures/files/images/e621-5418755-animated.gif").to_s
|
||||
end
|
||||
let(:jpeg_fixture_path) do
|
||||
Rails
|
||||
.root
|
||||
.join("test/fixtures/files/images/thumb-ac63d9d7-low-quality.jpeg")
|
||||
.to_s
|
||||
end
|
||||
let(:webm_fixture_path) do
|
||||
Rails
|
||||
.root
|
||||
.join("test/fixtures/files/images/e621-5421402-animated.webm")
|
||||
.to_s
|
||||
end
|
||||
|
||||
let(:thumbnail_options) do
|
||||
LoadedMedia::ThumbnailOptions.new(
|
||||
width: 100,
|
||||
height: 100,
|
||||
quality: 95,
|
||||
size: :down,
|
||||
interlace: true,
|
||||
for_frames: [0],
|
||||
)
|
||||
end
|
||||
|
||||
describe ".from_file" do
|
||||
context "with a gif file" do
|
||||
it "creates a LoadedMedia::Gif instance" do
|
||||
media = LoadedMedia.from_file("image/gif", gif_fixture_path)
|
||||
|
||||
expect(media).to be_a(LoadedMedia::Gif)
|
||||
expect(media.num_frames).to eq(37)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a jpeg file" do
|
||||
it "creates a LoadedMedia::StaticImage instance" do
|
||||
media = LoadedMedia.from_file("image/jpeg", jpeg_fixture_path)
|
||||
|
||||
expect(media).to be_a(LoadedMedia::StaticImage)
|
||||
expect(media.num_frames).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a webm file" do
|
||||
it "creates a LoadedMedia::Webm instance" do
|
||||
media = LoadedMedia.from_file("video/webm", webm_fixture_path)
|
||||
|
||||
expect(media).to be_a(LoadedMedia::Webm)
|
||||
expect(media.num_frames).to eq(908)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an unsupported file type" do
|
||||
it "returns nil" do
|
||||
media = LoadedMedia.from_file("application/pdf", jpeg_fixture_path)
|
||||
|
||||
expect(media).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#write_frame_thumbnail" do
|
||||
let(:output_dir) do
|
||||
File.join(BlobFile::TMP_DIR, "loaded_media_spec-#{SecureRandom.uuid}")
|
||||
end
|
||||
before { FileUtils.mkdir_p(output_dir) }
|
||||
after { FileUtils.rm_rf(output_dir) }
|
||||
|
||||
def make_output_path(frame)
|
||||
File.join(output_dir, "thumbnail-#{frame}-#{SecureRandom.uuid}.jpg")
|
||||
end
|
||||
|
||||
context "with a gif file" do
|
||||
it "can extract and save a thumbnail" do
|
||||
media = LoadedMedia.from_file("image/gif", gif_fixture_path)
|
||||
|
||||
output_path = make_output_path(0)
|
||||
media.write_frame_thumbnail(0, output_path, thumbnail_options)
|
||||
|
||||
expect(File.exist?(output_path)).to be true
|
||||
expect(File.size(output_path)).to be > 0
|
||||
end
|
||||
end
|
||||
|
||||
context "with a jpeg file" do
|
||||
it "can create and save a thumbnail" do
|
||||
media = LoadedMedia.from_file("image/jpeg", jpeg_fixture_path)
|
||||
|
||||
output_path = make_output_path(0)
|
||||
media.write_frame_thumbnail(0, output_path, thumbnail_options)
|
||||
|
||||
expect(File.exist?(output_path)).to be true
|
||||
expect(File.size(output_path)).to be > 0
|
||||
end
|
||||
end
|
||||
|
||||
context "with a webm file" do
|
||||
it "can extract a frame and save a thumbnail" do
|
||||
media = LoadedMedia.from_file("video/webm", webm_fixture_path)
|
||||
|
||||
output_path = make_output_path(0)
|
||||
media.write_frame_thumbnail(0, output_path, thumbnail_options)
|
||||
|
||||
expect(File.exist?(output_path)).to be true
|
||||
expect(File.size(output_path)).to be > 0
|
||||
end
|
||||
|
||||
it "creates different frames", quiet: false do
|
||||
media = LoadedMedia.from_file("video/webm", webm_fixture_path)
|
||||
|
||||
output_path_1 = make_output_path(0)
|
||||
output_path_2 = make_output_path(453)
|
||||
media.write_frame_thumbnail(0, output_path_1, thumbnail_options)
|
||||
media.write_frame_thumbnail(453, output_path_2, thumbnail_options)
|
||||
|
||||
expect(output_path_1).not_to eq(output_path_2)
|
||||
expect(FileUtils.compare_file(output_path_1, output_path_2)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -33,20 +33,62 @@ RSpec.describe Domain::PostFile::Thumbnail, type: :model do
|
||||
it "successfully creates a thumbnail from a JPEG image" do
|
||||
post_file = create(:domain_post_file, :image_file)
|
||||
|
||||
# Create a PostFileThumbnail
|
||||
thumbnail =
|
||||
described_class.new(post_file:, thumb_type: :size_32_32, frame: 0)
|
||||
expect(thumbnail.save).to be true
|
||||
thumbnails = described_class.create_for_post_file!(post_file)
|
||||
# 1 for the 32x32
|
||||
# 1 for the content-container
|
||||
expect(thumbnails.size).to eq(2)
|
||||
|
||||
# Verify the thumbnail file was created
|
||||
expect(thumbnail.absolute_file_path).not_to be_nil
|
||||
expect(File.exist?(thumbnail.absolute_file_path)).to be true
|
||||
# expect the 32x32 thumbnail
|
||||
thumbs_32_32 = post_file.thumbnails.where(thumb_type: :size_32_32)
|
||||
expect(thumbs_32_32.size).to eq(1)
|
||||
expect(thumbs_32_32.first.frame).to eq(0)
|
||||
|
||||
# Verify the thumbnail has a reasonable size (should be smaller than original)
|
||||
original_size =
|
||||
File.size(post_file.log_entry.response.absolute_file_path)
|
||||
thumbnail_size = File.size(thumbnail.absolute_file_path)
|
||||
expect(thumbnail_size).to be < original_size
|
||||
# expect the content-container thumbnail
|
||||
thumbs_content_container =
|
||||
post_file.thumbnails.where(thumb_type: :content_container)
|
||||
expect(thumbs_content_container.size).to eq(1)
|
||||
expect(thumbs_content_container.first.frame).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "with GIF images" do
|
||||
it "successfully creates a thumbnails from a GIF image" do
|
||||
# 37 frames in the gif
|
||||
post_file = create(:domain_post_file, :gif_file)
|
||||
thumbnails = described_class.create_for_post_file!(post_file)
|
||||
# 5 for the 32x32
|
||||
# 1 for content-container
|
||||
expect(thumbnails.size).to eq(6)
|
||||
|
||||
# expect the frames at 0, 0.1, 0.5, 0.9 and 1.0 through the gif
|
||||
# so frame numbers 0, 3, 18, 32 and 36
|
||||
thumbs_32_32 = post_file.thumbnails.where(thumb_type: :size_32_32)
|
||||
expect(thumbs_32_32.map(&:frame)).to eq([0, 3, 18, 32, 36])
|
||||
|
||||
# expect the content-container thumbnail
|
||||
thumbs_content_container =
|
||||
post_file.thumbnails.where(thumb_type: :content_container)
|
||||
expect(thumbs_content_container.size).to eq(1)
|
||||
expect(thumbs_content_container.first.frame).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "with webm videos" do
|
||||
it "successfully creates a thumbnail from a webm video" do
|
||||
post_file = create(:domain_post_file, :webm_file)
|
||||
thumbnails = described_class.create_for_post_file!(post_file)
|
||||
expect(thumbnails.size).to eq(6)
|
||||
|
||||
# expect 5x 32x32 thumbnails, 1 for the content-container
|
||||
thumbs_32_32 = post_file.thumbnails.where(thumb_type: :size_32_32)
|
||||
expect(thumbs_32_32.size).to eq(5)
|
||||
expect(thumbs_32_32.map(&:frame)).to eq([0, 90, 453, 816, 907])
|
||||
|
||||
# expect the content-container thumbnail
|
||||
thumbs_content_container =
|
||||
post_file.thumbnails.where(thumb_type: :content_container)
|
||||
expect(thumbs_content_container.size).to eq(1)
|
||||
expect(thumbs_content_container.first.frame).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BIN
test/fixtures/files/images/e621-5418755-animated.gif
vendored
Normal file
BIN
test/fixtures/files/images/e621-5418755-animated.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 MiB |
BIN
test/fixtures/files/images/e621-5421402-animated.webm
vendored
Normal file
BIN
test/fixtures/files/images/e621-5421402-animated.webm
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user