animated gif resizing
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -36,3 +36,4 @@
|
||||
- [ ] Do PCA on user factors table to display a 2D plot of users
|
||||
- [ ] Use links found in descriptions to indicate re-scanning a post? (e.g. for comic next/prev links)
|
||||
- [ ] fix for IDs that have a dot in them - e.g. https://refurrer.com/users/fa@jakke.
|
||||
- [ ] Rich inline links to e621 e.g. https://refurrer.com/posts/fa@60070060
|
||||
|
||||
@@ -5,16 +5,27 @@ class BlobEntriesController < ApplicationController
|
||||
sig { void }
|
||||
def show
|
||||
thumb = params[:thumb]
|
||||
raise("invalid thumb #{thumb}") if thumb.present? && !thumb_params(thumb)
|
||||
if thumb.present? && !thumb_params(thumb)
|
||||
raise ActionController::BadRequest.new("invalid thumbnail #{thumb}")
|
||||
end
|
||||
|
||||
expires_dur = 1.year
|
||||
if thumb.present?
|
||||
expires_dur = 1.week
|
||||
else
|
||||
expires_dur = 1.year
|
||||
end
|
||||
response.headers["Expires"] = expires_dur.from_now.httpdate
|
||||
expires_in expires_dur, public: true
|
||||
|
||||
sha256 = params[:sha256]
|
||||
etag = sha256
|
||||
etag += "-#{thumb}" if thumb
|
||||
return unless stale?(last_modified: Time.at(0), strong_etag: etag)
|
||||
unless stale?(
|
||||
last_modified: Time.at(0),
|
||||
strong_etag: strong_etag_for_request,
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
sha256 = T.let(params[:sha256], String)
|
||||
raise ActionController::BadRequest.new("no file specified") if sha256.blank?
|
||||
|
||||
if show_blob_file(sha256, thumb)
|
||||
return
|
||||
@@ -30,15 +41,21 @@ class BlobEntriesController < ApplicationController
|
||||
sig { params(sha256: String, thumb: T.nilable(String)).returns(T::Boolean) }
|
||||
def show_blob_file(sha256, thumb)
|
||||
if thumb
|
||||
filename = "thumb-#{thumb}-#{sha256}"
|
||||
filename = T.must(filename[..File.extname(filename).length])
|
||||
filename += ".jpeg"
|
||||
cache_key = "vips:thumbnail:#{sha256}:#{thumb}"
|
||||
thumb_params = thumb_params(thumb)
|
||||
if thumb_params.nil?
|
||||
raise ActionController::BadRequest.new("invalid thumbnail: #{thumb}")
|
||||
end
|
||||
|
||||
# if the requested format is gif, and the thumbnail type is content-container, we want to
|
||||
# thumbnail the gif into another gif. Else, always thumbnail into a jpeg.
|
||||
file_ext = "jpeg"
|
||||
if params[:format] == "gif" && thumb == "content-container"
|
||||
file_ext = "gif"
|
||||
end
|
||||
|
||||
width, height = thumb_params
|
||||
filename = "thumb-#{sha256}-#{thumb}.#{file_ext}"
|
||||
cache_key = "vips:#{filename}"
|
||||
thumb_data =
|
||||
Rack::MiniProfiler.step("vips: load from cache") do
|
||||
Rails
|
||||
@@ -49,9 +66,9 @@ class BlobEntriesController < ApplicationController
|
||||
content_type =
|
||||
blob_file.content_type || "application/octet-stream"
|
||||
if helpers.is_renderable_video_type?(content_type)
|
||||
thumbnail_video_file(blob_file, width, height)
|
||||
thumbnail_video_file(blob_file, width, height, file_ext)
|
||||
elsif helpers.is_renderable_image_type?(content_type)
|
||||
thumbnail_image_file(blob_file, width, height)
|
||||
thumbnail_image_file(blob_file, width, height, file_ext)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -63,8 +80,8 @@ class BlobEntriesController < ApplicationController
|
||||
end
|
||||
|
||||
send_data(
|
||||
thumb_data,
|
||||
type: "image/jpg",
|
||||
thumb_data[0],
|
||||
type: thumb_data[1],
|
||||
disposition: "inline",
|
||||
filename: filename,
|
||||
)
|
||||
@@ -84,11 +101,14 @@ class BlobEntriesController < ApplicationController
|
||||
end
|
||||
|
||||
sig do
|
||||
params(blob_file: BlobFile, width: Integer, height: Integer).returns(
|
||||
T.nilable(String),
|
||||
)
|
||||
params(
|
||||
blob_file: BlobFile,
|
||||
width: Integer,
|
||||
height: Integer,
|
||||
thumb: String,
|
||||
).returns(T.nilable([String, String]))
|
||||
end
|
||||
def thumbnail_video_file(blob_file, width, height)
|
||||
def thumbnail_video_file(blob_file, width, height, thumb)
|
||||
video_file = blob_file.absolute_file_path
|
||||
temp_thumb_file = Tempfile.new(%w[video-thumb .png])
|
||||
process_result =
|
||||
@@ -111,26 +131,62 @@ class BlobEntriesController < ApplicationController
|
||||
|
||||
thumb_data_tmp = File.read(T.must(temp_thumb_file.path), mode: "rb")
|
||||
temp_thumb_file.unlink
|
||||
thumb_data_tmp
|
||||
[thumb_data_tmp, "image/jpeg"]
|
||||
end
|
||||
|
||||
# Returns a tuple of the thumbnail data and the content type
|
||||
sig do
|
||||
params(blob_file: BlobFile, width: Integer, height: Integer).returns(
|
||||
T.nilable(String),
|
||||
)
|
||||
params(
|
||||
blob_file: BlobFile,
|
||||
width: Integer,
|
||||
height: Integer,
|
||||
file_ext: String,
|
||||
).returns(T.nilable([String, String]))
|
||||
end
|
||||
def thumbnail_image_file(blob_file, width, height)
|
||||
image_buffer =
|
||||
Rack::MiniProfiler.step("vips: load image") do
|
||||
T.unsafe(Vips::Image).thumbnail(
|
||||
blob_file.absolute_file_path,
|
||||
width,
|
||||
height: height,
|
||||
)
|
||||
end
|
||||
def thumbnail_image_file(blob_file, width, height, file_ext)
|
||||
if file_ext == "gif"
|
||||
Rack::MiniProfiler.step("vips: load gif") do
|
||||
# Use libvips' gifload with n=-1 to load all frames
|
||||
image = Vips::Image.gifload(blob_file.absolute_file_path, n: -1)
|
||||
num_frames = image.get("n-pages")
|
||||
image_width, image_height = image.width, (image.height / num_frames)
|
||||
|
||||
Rack::MiniProfiler.step("vips: thumbnail image") do
|
||||
image_buffer.jpegsave_buffer
|
||||
if width >= image_width && height >= image_height
|
||||
logger.info("gif is already smaller than requested thumbnail size")
|
||||
return [
|
||||
File.read(blob_file.absolute_file_path, mode: "rb"),
|
||||
"image/gif"
|
||||
]
|
||||
end
|
||||
|
||||
Rack::MiniProfiler.step("vips: thumbnail gif") do
|
||||
image = image.thumbnail_image(width, height: height)
|
||||
image_buffer =
|
||||
image.gifsave_buffer(
|
||||
dither: 1,
|
||||
effort: 1,
|
||||
interframe_maxerror: 16,
|
||||
interpalette_maxerror: 10,
|
||||
interlace: true,
|
||||
)
|
||||
[image_buffer, "image/gif"]
|
||||
end
|
||||
end
|
||||
else
|
||||
# Original static image thumbnailing logic
|
||||
image_buffer =
|
||||
Rack::MiniProfiler.step("vips: load image") do
|
||||
T.unsafe(Vips::Image).thumbnail(
|
||||
blob_file.absolute_file_path,
|
||||
width,
|
||||
height: height,
|
||||
)
|
||||
end
|
||||
|
||||
Rack::MiniProfiler.step("vips: thumbnail image") do
|
||||
logger.info("rendering thumbnail as jpeg")
|
||||
[image_buffer.jpegsave_buffer(interlace: true, Q: 95), "image/jpeg"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -148,7 +204,14 @@ class BlobEntriesController < ApplicationController
|
||||
when "medium"
|
||||
[800, 600]
|
||||
when "content-container"
|
||||
[2048, 2048]
|
||||
[768, 2048]
|
||||
# [2048, 2048]
|
||||
# [128, 128]
|
||||
end
|
||||
end
|
||||
|
||||
sig { returns(String) }
|
||||
def strong_etag_for_request
|
||||
[params[:sha256], params[:thumb], params[:format]].compact.join("-")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,24 +27,49 @@ module LogEntriesHelper
|
||||
end
|
||||
|
||||
sig { params(content_type: String).returns(T.nilable(String)) }
|
||||
def ext_for_content_type(content_type)
|
||||
case content_type
|
||||
when "image/jpeg"
|
||||
"jpeg"
|
||||
when "image/jpg"
|
||||
"jpg"
|
||||
when "image/png"
|
||||
"png"
|
||||
when "image/gif"
|
||||
def thumbnail_extension_for_content_type(content_type)
|
||||
return nil unless is_thumbable_content_type?(content_type)
|
||||
extension = extension_for_content_type(content_type)
|
||||
if extension == "gif"
|
||||
"gif"
|
||||
when "video/webm"
|
||||
else
|
||||
"jpeg"
|
||||
end
|
||||
end
|
||||
|
||||
sig { params(content_type: String).returns(T.nilable(String)) }
|
||||
def extension_for_content_type(content_type)
|
||||
content_type = content_type.split(";")[0]
|
||||
return nil unless content_type
|
||||
|
||||
extension = Rack::Mime::MIME_TYPES.invert[content_type]
|
||||
return extension[1..] if extension
|
||||
|
||||
case content_type
|
||||
when %r{image/jpeg}
|
||||
"jpeg"
|
||||
when %r{image/jpg}
|
||||
"jpg"
|
||||
when %r{image/png}
|
||||
"png"
|
||||
when %r{image/gif}
|
||||
"gif"
|
||||
when %r{video/webm}
|
||||
"webm"
|
||||
when "audio/mpeg"
|
||||
when %r{audio/mpeg}
|
||||
"mp3"
|
||||
when "audio/mp3"
|
||||
when %r{audio/mp3}
|
||||
"mp3"
|
||||
when "audio/wav"
|
||||
when %r{audio/wav}
|
||||
"wav"
|
||||
when %r{application/pdf}
|
||||
"pdf"
|
||||
when %r{application/rtf}
|
||||
"rtf"
|
||||
when %r{application/msword}
|
||||
"doc"
|
||||
when %r{application/vnd\.openxmlformats-officedocument\.wordprocessingml\.document}
|
||||
"docx"
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<% path = blob_path(HexUtil.bin2hex(log_entry.response_sha256)) %>
|
||||
<% path = blob_path(
|
||||
HexUtil.bin2hex(log_entry.response_sha256),
|
||||
format: extension_for_content_type(log_entry.content_type),
|
||||
) %>
|
||||
<section class="flex grow justify-center overflow-clip">
|
||||
<% if is_renderable_image_type?(log_entry.content_type) %>
|
||||
<%= render partial: "log_entries/renderers/image", locals: { log_entry:, path: } %>
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
<% path = blob_path(HexUtil.bin2hex(log_entry.response_sha256), format: "jpg", thumb: "content-container") %>
|
||||
<img alt="image" src="<%= path %>" class="md:rounded-md" />
|
||||
<% content_container_thumb_path = blob_path(
|
||||
HexUtil.bin2hex(log_entry.response_sha256),
|
||||
format: thumbnail_extension_for_content_type(log_entry.content_type),
|
||||
thumb: "content-container",
|
||||
) %>
|
||||
<%= link_to path do %>
|
||||
<img alt="image" src="<%= content_container_thumb_path %>" class="md:rounded-md" />
|
||||
<% end %>
|
||||
|
||||
18
sorbet/rbi/shims/activesupport_cache_store.rbi
Normal file
18
sorbet/rbi/shims/activesupport_cache_store.rbi
Normal file
@@ -0,0 +1,18 @@
|
||||
# typed: strict
|
||||
|
||||
class ActiveSupport::Cache::Store
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
|
||||
sig do
|
||||
type_parameters(:T)
|
||||
.params(
|
||||
key: T.untyped,
|
||||
options: T.untyped,
|
||||
block: T.proc.returns(T.type_parameter(:T)),
|
||||
)
|
||||
.returns(T.type_parameter(:T))
|
||||
end
|
||||
def fetch(key, options = nil, &block)
|
||||
end
|
||||
end
|
||||
14
sorbet/rbi/shims/rack_miniprofiler.rbi
Normal file
14
sorbet/rbi/shims/rack_miniprofiler.rbi
Normal file
@@ -0,0 +1,14 @@
|
||||
# typed: strict
|
||||
|
||||
class Rack::MiniProfiler
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
|
||||
sig do
|
||||
type_parameters(:T)
|
||||
.params(name: String, block: T.proc.returns(T.type_parameter(:T)))
|
||||
.returns(T.type_parameter(:T))
|
||||
end
|
||||
def self.step(name, &block)
|
||||
end
|
||||
end
|
||||
24
sorbet/rbi/shims/vips.rbi
Normal file
24
sorbet/rbi/shims/vips.rbi
Normal file
@@ -0,0 +1,24 @@
|
||||
# typed: strict
|
||||
class Vips::Image
|
||||
sig { params(name: String, opts: T.untyped).returns(Vips::Image) }
|
||||
def self.new_from_file(name, **opts)
|
||||
end
|
||||
|
||||
sig { params(name: String, n: T.nilable(Integer)).returns(Vips::Image) }
|
||||
def self.gifload(name, n: nil)
|
||||
end
|
||||
|
||||
sig { params(scale: T.untyped).returns(Vips::Image) }
|
||||
def resize(scale)
|
||||
end
|
||||
|
||||
sig { params(opts: T.untyped).returns(String) }
|
||||
def gifsave_buffer(**opts)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(width: Integer, height: T.nilable(Integer)).returns(Vips::Image)
|
||||
end
|
||||
def thumbnail_image(width, height: nil)
|
||||
end
|
||||
end
|
||||
@@ -79,7 +79,7 @@ RSpec.describe BlobEntriesController, type: :controller do
|
||||
it "generates thumbnail for valid size" do
|
||||
get :show, params: { sha256: sha256_hex, thumb: "tiny" }
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.content_type).to eq("image/jpg")
|
||||
expect(response.content_type).to eq("image/jpeg")
|
||||
expect(response.headers["Content-Disposition"]).to include("inline")
|
||||
expect(response.body).to_not be_nil
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user