[PART 1 of 2]
Working with HEIC files is a challenge. As of today, there is not a single browser that renders HEIC/HEIF files. I had the challenge at work of being able to handle these type of files. I'm working with a Rails 6 app and here's what it took for being able to handle these files.
First of all, I followed this blog post. As is explained in the post, a custom Rails previewer is needed. After setting up the HEICPreviewer, and using Minimagick for doing the conversion to a png file, everything seemed to be working by running the test implemented. However, when running the app and uploading a HEIC picture, I was running into a ActiveStorage::UnpreviewableError. This took me into a deep dive in Rails Previewers. Here's what I learned.
After some time debugging, I understood the ActiveStorage::UnpreviewableError error. This error raises when preview is called on a blob that is not previewable. You may be wondering, how does Rails know when a blob is previeweable or not? You can find out yourself by running
Rails.configuration.active_storage.previewers
in a Rails console. By default, you get these previewers.
[ActiveStorage::Previewer::PopplerPDFPreviewer,ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer]
Previewers
Let's take one step back though. What are Rails Previewers anyway? By definition, a previewer extracts a preview image from a blob. As stated in the docs, ActiveRecord provides previewers for videos and PDF, as seen in the default previewers above. In the case of the PDF previewers, it extracts the first page. For video previewers, it extracts the first frame. This is what Previewers can do. It lets you handle distinct type files and do something with them.
With Mario's approach, I created a subclass of ActiveStorage::Previewer and created a HeicPreviewer. There are two methods that have to be implemented in any ActiveStorage::Previewer subclass. These methods are:
def accept?(blob) end def preview(**options) end
Here's an interesting bit on the accept? method:
This method checks if the blob passed to the method, is accepted in the Previewer. For instance, in the Previewer::PopplerPDFPreviewer the accept? method checks if the blob is of content_type 'pdf' and if the @pdftoppm_exists
In our example with the HEIC file, Rails will go through each of the previewers available (listed above as out-of-the-box) and will run accept? on each previewer to know which previewer can handle it. It will take the first one that returns true to accept? as seen in this bit of code:
# Implemented in ActiveStorage::Preview model #https://github.com/rails/rails/blob/83217025a171593547d1268651b446d3533e2019/activestorage/app/models/active_storage/preview.rb#L111 def previewer_class ActiveStorage.previewers.detect { |klass| klass.accept?(blob) } end
Following our example, when passing a HEIC file, our declared HeicPreviewer will handle it since it will go through every Previewer defined and expect a true value returned from the accept? method. This is how our accept?method looks like in our HEIC previewer:
# As shown here https://mariochavez.io/desarrollo/2021/06/22/heic-support-active-storage def accept?(blob) blob.content_type == "image/heic" && minimagick_exists? end
Now, upon calling preview on our attachable, the magic of MiniMagick kicks in and does the conversion to a png file, as shown in the following bit of code
# As shown here https://mariochavez.io/desarrollo/2021/06/22/heic-support-active-storage/ def preview(transformations = {}) download_blob_to_tempfile do |input| io = ImageProcessing::MiniMagick.source(input).convert("png").call yield io: io, filename: "#{blob.filename.base}.png", content_type: "image/png" end end
How to use our HeicPreviewer
Now, for using the our previewer, we need to call preview on the image and pass a transformation to it. Let's look at an example
# In the view where you want to accept HEIC images <%= image_tag image_preview(@user.picture) %> # In our users_helper.rb def image_preview(image) if image.attached? && image.content_type == "image/heic" return image.preview(resize_to_limit: [300, 300]).processed.service_url end end
By doing the call to preview, our HeicPreviewer will handle it and do the magic! 🎉
That's it for the part of it when it comes to creating a Previewer in Rails. In part 2 of this article, we'll look at a different use case in the frontend.
To be continued in part 2...