Fix a bug where generating thumbnails failed for certain images when
using libvips 8.10. Specifically, it failed for single-channel greyscale
images and four-channel CMYK images without an embedded color profile.
In these cases we specified an sRGB fallback profile, but under libvips
8.10 this failed because the sRGB profile was incompatible with
single-channel and four-channel images. Before libvips 8.10 this worked,
but as of 8.10 it's a hard error.
The way libvips handles fallback color profiles differs across versions,
so we have to use different arguments for different versions. In 8.7,
vips doesn't have builtin color profiles, so we have to specify our own
manually. In 8.9, it has builtin profiles, so we can omit the import
profile, but we're still required to set the export profile to sRGB,
otherwise it will leave CMYK images as CMYK when generating thumbnails.
In 8.10, we have to _not_ to set the import or export profile to sRGB,
otherwise it will fail with an incompatible profile error when it tries
to convert CMYK images to RGB.
The builtin sRGB profile used by libvips[1] is different than the one we
used previously[2]. The builtin one comes from LCMS[3], whereas ours
came from ArgyllCMS.[4] Not all sRGB profiles are created the same[5],
so this may result in some imperceptible differences in thumbnail
output. The ArgyllCMS profile was used before because it seemed to be
the best one[6], but realistically it probably doesn't matter.
1: https://github.com/libvips/libvips/blob/v8.10.6/libvips/colour/profiles/sRGB.icm
2: 906eec190d/config/sRGB.icm
3: https://www.littlecms.com/
4: https://www.argyllcms.com/
5: https://ninedegreesbelow.com/photography/srgb-profile-comparison.html
6: https://ninedegreesbelow.com/photography/srgb-profile-comparison.html#addendum
93 lines
2.9 KiB
Ruby
93 lines
2.9 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
|
|
# 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, 10)
|
|
THUMBNAIL_OPTIONS = { size: :down, linear: false, no_rotate: true }
|
|
CROP_OPTIONS = { crop: :attention, linear: false, no_rotate: true }
|
|
elsif Vips.at_least_libvips?(8, 8)
|
|
THUMBNAIL_OPTIONS = { size: :down, linear: false, no_rotate: true, export_profile: "srgb" }
|
|
CROP_OPTIONS = { crop: :attention, linear: false, no_rotate: true, export_profile: "srgb" }
|
|
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
|
|
|
|
def is_corrupt?
|
|
image.stats
|
|
false
|
|
rescue Vips::Error
|
|
true
|
|
end
|
|
|
|
def is_animated?
|
|
is_animated_gif? || is_animated_png?
|
|
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 && image.get("n-pages") > 1
|
|
# older versions of libvips that don't support n-pages will raise an error
|
|
rescue Vips::Error
|
|
false
|
|
end
|
|
|
|
def is_animated_png?
|
|
file_ext == :png && APNGInspector.new(file.path).inspect!.animated?
|
|
end
|
|
|
|
# @return [Vips::Image] the Vips image object for the file
|
|
def image
|
|
Vips::Image.new_from_file(file.path, fail: true)
|
|
end
|
|
|
|
memoize :image, :dimensions, :is_corrupt?, :is_animated_gif?, :is_animated_png?
|
|
end
|