retry loading gifs as jpg/png on failure

This commit is contained in:
Dylan Knutson
2025-06-26 18:59:31 +00:00
parent 3a06181db8
commit 308232e01d
6 changed files with 140 additions and 27 deletions

View File

@@ -144,34 +144,50 @@ class BlobEntriesController < ApplicationController
).returns(T.nilable([String, String]))
end
def thumbnail_image_file(blob_file, width, height, file_ext)
blob_file_path = blob_file.absolute_file_path
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)
VipsUtil.try_load_gif(
blob_file_path,
load_gif: -> do
Rack::MiniProfiler.step("vips: load gif") do
# Use libvips' gifload with n=-1 to load all frames
image = Vips::Image.gifload(blob_file_path, n: -1)
num_frames = image.get("n-pages")
image_width, image_height = image.width, (image.height / num_frames)
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
if width >= image_width && height >= image_height
logger.info(
"gif is already smaller than requested thumbnail size",
)
return File.binread(blob_file_path), "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
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
end,
on_load_failed: ->(detected_content_type) do
case detected_content_type
when %r{image/png}
thumbnail_image_file(blob_file, width, height, "png")
when %r{image/jpeg}, %r{image/jpg}
thumbnail_image_file(blob_file, width, height, "jpeg")
else
raise
end
end,
)
else
# Original static image thumbnailing logic
image_buffer =
@@ -185,7 +201,10 @@ class BlobEntriesController < ApplicationController
Rack::MiniProfiler.step("vips: thumbnail image") do
logger.info("rendering thumbnail as jpeg")
[image_buffer.jpegsave_buffer(interlace: true, Q: 95), "image/jpeg"]
[
T.let(image_buffer.jpegsave_buffer(interlace: true, Q: 95), String),
"image/jpeg",
]
end
end
end

View File

@@ -14,7 +14,13 @@ class LoadedMedia
def self.from_file(content_type, media_path)
case content_type
when %r{image/gif}
LoadedMedia::Gif.new(media_path)
VipsUtil.try_load_gif(
media_path,
load_gif: -> { LoadedMedia::Gif.new(media_path) },
on_load_failed: ->(detected_content_type) do
return from_file(detected_content_type, media_path)
end,
)
when %r{image/jpeg}, %r{image/jpg}, %r{image/png}, %r{image/bmp}
LoadedMedia::StaticImage.new(media_path)
when %r{video/webm}

30
app/lib/vips_util.rb Normal file
View File

@@ -0,0 +1,30 @@
# typed: strict
module VipsUtil
extend T::Sig
sig do
type_parameters(:T)
.params(
media_path: String,
load_gif: T.proc.returns(T.nilable(T.type_parameter(:T))),
on_load_failed:
T
.proc
.params(detected_content_type: String)
.returns(T.nilable(T.type_parameter(:T))),
)
.returns(T.nilable(T.type_parameter(:T)))
end
def self.try_load_gif(media_path, load_gif:, on_load_failed:)
begin
load_gif.call
rescue Vips::Error
raise unless $!.message.include?("gifload: no frames in GIF")
content_type = T.let(`file -i #{media_path}`, T.nilable(String))
raise unless $?.success?
content_type = content_type&.split(":")&.last&.split(";")&.first&.strip
raise unless content_type
return on_load_failed.call(content_type)
end
end
end

View File

@@ -10,6 +10,8 @@ unless defined?(Rack::MiniProfiler)
end
RSpec.describe BlobEntriesController, type: :controller do
render_views
let(:blob_entry) do
create(
:blob_entry,
@@ -92,6 +94,32 @@ RSpec.describe BlobEntriesController, type: :controller do
end
end
context "png that has a gif extension and content type" do
let(:blob_entry) do
create(
:blob_entry,
content_type: "image/gif",
content:
File.read(
Rails.root.join("test/fixtures/files/images/fa.24326406.gif"),
),
)
end
it "renders the image as a gif" do
get :show,
params: {
sha256: sha256_hex,
thumb: "content-container",
format: "gif",
}
expect(response).to have_http_status(:success)
expect(response.content_type).to eq("image/jpeg")
expect(response.headers["Content-Disposition"]).to include("inline")
expect(response.body).to_not be_nil
end
end
it "sets cache headers" do
get :show, params: { sha256: sha256_hex }
expect(response.headers["Expires"]).to be_present

View File

@@ -91,5 +91,35 @@ RSpec.describe Domain::PostFile::Thumbnail, type: :model do
expect(thumbs_content_container.first.frame).to eq(0)
end
end
context "with a .gif file that is actually a .png" do
let(:post_file) do
create(
:domain_post_file,
:gif_file,
log_entry:
create(
:http_log_entry,
content_type: "image/gif",
response:
create(
:blob_file,
content_type: "image/gif",
contents:
File.binread(
Rails.root.join(
"test/fixtures/files/images/fa.24326406.gif",
),
),
),
),
)
end
it "successfully creates a thumbnail from a .gif file that is actually a .png" do
thumbnails = described_class.create_for_post_file!(post_file)
expect(thumbnails.size).to eq(2)
end
end
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB