Ok, I've struggled my way though this for almost 2 days now, and finally have something I'm sort-of happy with. There's still room for improvement. Eventually I got it sorted using Vips with some tip offs from this github conversation and this GoRails thread on saving variants in a job.
Model:
class Company < ApplicationRecord # :nodoc:
has_one_attached :logo do |attachable|
attachable.variant :medium, resize_to_fit: [300, nil]
attachable.variant :large, resize_to_fit: [700, nil]
end
after_save :process_logo_if_changed
private
def process_logo_if_changed
ImagePreprocessJob.perform_later(logo.id) if logo.blob&.saved_changes?
end
end
Job:
class ImagePreprocessJob < ApplicationJob
queue_as :latency_5m
def perform(attachment_id)
attachment = ActiveStorage::Attachment.find(attachment_id)
raise "Attachment is not an image" unless attachment&.image?
# svg and jfif will break Vips variants, convert to png
if attachment.content_type == "image/svg+xml" || jfif?(attachment.blob)
convert_to_png(attachment)
attachment = attachment.record.send(attachment.name) # switch to new png attachment
end
raise "Attachment ID: #{attachment.id} is not representable" unless attachment.representable?
# save variants
attachment.representation(resize_to_fit: [300, nil]).processed # medium size
attachment.representation(resize_to_fit: [700, nil]).processed # large size
end
def convert_to_png(attachment)
filename = attachment.filename.to_s.rpartition(".").first # remove existing extension
png = Vips::Image.new_from_buffer(attachment.blob.download, "")
attachment.purge
attachment.record.send(attachment.name).attach(
io: StringIO.new(png.write_to_buffer(".png")),
filename: "#{filename}.png",
content_type: "image/png"
)
end
def jfif?(blob)
file_content = blob.download
return (file_content[6..9] == "JFIF")
end
end
I played with preprocessed: true
in the model as described in the Active Storage Guide, but it would fill the log up with errors as it tries to create variants on invariable svg files before the job runs. So I just moved the processing/saving of variants into the job.
I was not able to solve this using the image_processing gem despite trying several ways. On the whole it was still far more difficult and a more convoluted solution than I expected - I won't mark this as the answer for quite a while as I'd love to see a more elegant and streamlined implementation, and I'm open to suggestions on how this could be improved.