BanditHijo.dev

Membuat Image Derivatives dengan Shrine pada Rails

Created at: 2020-02-19
Author by: BanditHijo

Prerequisite

ruby 2.6.3 rails 5.2.3 postgresql 11.5

Prakata

Pada catatan kali ini saya akan mendokumentasikan proses implementasi image processing dengan Shrine gem pada aplikasi Ruby on Rails.

Shrine itu apa yaa?

Definisi sederhananya, Shrine adalah file attachment toolkit untuk aplikasi Ruby.

Aplikasi Ruby artinya Shrine tidak hanya dapat digunakan oleh aplikasi yang dibuat dengan Ruby web framework seperti Ruby on Rails, melainkan dapat pula digunakan pada Ruby web framework yang lain seperti Sinatra, Hanami, Roda, Cuba, Grape.

Kenapa memilih menggunakan Shrine?

Shrine memiliki beberapa keuntungan diantaranya:

  1. Modular design, dimana kita dapat memilih fungsi apa yang akan kita pakai cukup dengan menggunakan plugin
  2. Memory friendly, streming upload dan download menjadi tidak masalah, meskipun dengan file berukuran besar
  3. Cloud storage, dukungan penyimpanan files yang berada di local disk, AWS S3, Google Cloud, Cloudinary dan lainnya
  4. Persistence integrations, bekerja dengan baik pada Sequel, ActiveRecord, ROM, Hanami dan Mongoid serta lainnya
  5. Flexible processing, mengenerate thumbnail eagerly atau onthe-fly dengan menggunakan imageMagick atau libvips
  6. dst

Nah, pada catatan kali ini, saya akan membahas spesifik mengenai poin ke 5, Flexible processing atau lebih khusus ke File processing, atau lebih khusus lagi adalah Image Processing yang akan digunakan untuk menggenerate set of thumbnails, atau dalam kata lain image derivatives (turunan gambar) dari gambar dengan ukuran asli yang diupload oleh user.

Sekenario

Misal, pada Web aplikasi yang saya buat, saya mengizinkan user untuk dapat mengupload gambar (image).

Namun, ternyata pengguna mengupload gambar dengan ukuran yang besar-besar, tentunya hal ini akan berpengaruh ke page load dari halaman web aplikasi kita.

Untuk mengakali ini, saya memanfaatkan fungsi image processing yang dapat dikonfigurasi di dalam Shrine untuk menggenerate set of thumbnails sesuai kebutuhan, misal dalam 3 ukuran (small, medium dan large).

Pemecahan Masalah

Pemasangan Shrine

Saya sudah memasang Shrine ke dalam Gemfile aplikasi saya.

Gemfile
1# ...
2# ...
3gem "shrine", "~> 3.0"

Pemasangan Image Processing

Selanjutnya, saya akan memasang dependensi untuk Shrine dapat melakukan image procesing, yaitu ImageMagick.

Pada catatan kali ini saya tidak menjelaskan mengenai penggunaan libvips.

Pada Arch Linux

$ sudo pacman -S imagemagick

Untuk distribusi yang lain, silahkan mencari paket ImageMagick yang tersedia pada repository distro masing-masing.

Selanjutnya tambahkan image_processing pada Gemfile.

Gemfile
1# ...
2# ...
3gem 'image_processing', '~> 1.8'

Install gem yang baru saja kita pasang.

$ bundle install

Konfigurasi Initializer Shrine

Tambahkan plugin derivatives pada Shrine initializer.

config/initializers/shrine.rb
1# ...
2# ...
3# ...
4
5Shrine.plugin ...
6Shrine.plugin ...
7Shrine.plugin ...
8Shrine.plugin :derivatives
9Shrine.plugin :default_url

Saya juga menambahkan plugin default_url agar image yang belum memiliki turunan, dapat menggunakan original image.

Models

Selanjutnya saya akan menambahkan fungsi untuk eager processing atau derivatives processing pada model image uploader.

app/models/image_uploader.rb
1require "image_processing/mini_magick"
2
3class ImageUploader < Shrine
4 # Image validation
5 Attacher.validate do
6 validate_max_size 2.megabyte, message: "is too large (max is 2 MB)"
7 validate_mime_type_inclusion ['image/jpg', 'image/jpeg', 'image/png']
8 end
9
10 # Eager processing / derivatives processing
11 Attacher.derivatives do |original|
12 magick = ImageProcessing::MiniMagick.source(original).saver(quality: 90)
13 {
14 large: magick.resize_to_limit!(800, 800),
15 medium: magick.resize_to_limit!(500, 500),
16 small: magick.resize_to_limit!(300, 300),
17 }
18 end
19
20 # Fallback to original
21 Attacher.default_url do |derivative: nil, **|
22 file&.url if derivative
23 end
24end

Saya rasa, pada blok eager processing / derivatives processing sudah dapat dipahami yaa.

Kalau ingin memodifikasi ukurannya, dapat dilakukan pada blok tersebut.

Saye memilih menggunakan ImageMagick sebagai backend processing karena lebih familiar.

Namun, saya sempat membaca kalau libvips dapat memproses lebih ringan dan lebih cepat. Saya akan coba pada kesempatan yang lain.

Model image_uploader.rb ini tentunya akan dipanggil pada model-model yang memiliki field image_data:text atau dengan nama yang lain namun berakhiran _data:text, caranya dengan menambahkan include seperti di bawah ini.

app/models/nama_model.rb
1class NamaModel < ApplicationRecord
2 # ...
3 # ...
4
5 # Contoh dengan field image_data
6 include ImageUploader::Attachment(:image)
7 # Atau, dengan nama field yang berbeda
8 include ImageUploader::Attachment(:header_image)
9
10 # ...
11 # ...
12end

Controller

Selanjutnya, pada controller yang menggunakan image uploader, saya akan menambahkan method .image_derivatives!.

Misal, saya memiliki posts controller.

app/controllers/admin/posts_controller.rb
1class Admin::PostsController < AdminsController
2 def index
3 # ...
4 end
5
6 def show
7 # ...
8 end
9
10 def new
11 @post = Post.new
12 end
13
14 def create
15 @post = Post.new(post_params)
16 # Calls derivatives processor
17 @post.image_derivatives! if @post.image.present?
18 if @post.save
19 redirect_to admin_post_path(@post)
20 else
21 render :new
22 end
23 end
24
25 def edit
26 @post = Post.find(params[:id])
27 end
28
29 def update
30 @post = Post.find(params[:id])
31 if @post.update(post_params)
32 # Calls derivatives processor
33 @post.image_derivatives! if @post.image.present?
34 @post.save
35 redirect_to admin_post_path(@post)
36 else
37 render :edit
38 end
39 end
40
41 def destroy
42 # ...
43 end
44
45 def delete_image_attachment
46 # ...
47 end
48
49 private
50
51 def post_params
52 params.require(:post).permit(:title, :content, :image)
53 end
54end

Gimana kalau field image_data berada pada tabel yang berelasi?

app/controllers/admin/experiences_controller.rb
1class Admin::ExperiencesController < AdminsController
2
3 def index
4 # ...
5 end
6
7 def show
8 # ...
9 end
10
11 def new
12 @experience = Experience.new
13 @experience.build_photo
14 end
15
16 def create
17 @experience = Experience.new(experience_params)
18 # memanggil attribute image_data yang ada pada tabel photos
19 @experience.photo.image_experience_derivatives! if @experience.photo.image_experience.present?
20
21 if @experience.save
22 redirect_to admin_experience_path(@experience)
23 else
24 @experience.build_photo
25 render :new
26 end
27 end
28
29 def edit
30 @experience = Experience.find(params[:id])
31 end
32
33 def update
34 @experience = Experience.find(params[:id])
35
36 if @experience.update(experience_params)
37 # memanggil attribute image_data yang ada pada tabel photos
38 @experience.photo.image_experience_derivatives! if @experience.photo.image_experience.present?
39 @experience.save
40 redirect_to admin_experience_path(@experience)
41 else
42 render :edit
43 end
44 end
45
46 def destroy
47 # ...
48 end
49
50 private
51
52 def experience_params
53 params.require(:experience).permit(..., ..., ...)
54 end
55end

Views

Setelah gambar selesai diupload, gambar akan disimpan pada image_data atau nama apa saja <attachment>_data dalam catatan saya ini, berupa gambar.

Sekarang cara memanggilnya pada view template.

Perhatikan bagian image_tag.

app/views/public/posts/index.html.erb
1<!-- Post list -->
2<% @posts.each do |post| %>
3 <div class="row">
4 <div class="col-md-3">
5 <%= link_to public_post_path(post) do %>
6 <%= image_tag (post.image.present? ? post.image_url(:small) : "img-default.jpg"), class: "w-100 h-100" %>
7 <% end %>
8 </div>
9 <div class="col-md-9">
10 <span class="text-normal"><%= post.created_at.strftime("%B %e, %Y") %></span>
11 <%= link_to public_post_path(post) do %>
12 <h6><%= post.title.titleize %></h6>
13 <% end %>
14 <p class="text-normal">
15 <% html = post.content %>
16 <%= strip_tags(html).truncate(150) %>
17 <%= link_to "more..", public_post_path(post), class: "text-primary" %>
18 </p>
19 </div>
20 </div>
21<% end %>
22<!-- END Post list -->

Saya menggunakan bentuk image_tag seperti ini,

1<%= image_tag (post.image.present? ? post.image_url(:small) : "img-default.jpg") %>

Terlihat bahwa saya menggunakan pengkondisian satu baris.

Apabila image.present?
Bernilai Benar, maka tampilkan image_url(:small)
Bernilai Salah, maka tampilkan img-default.jpg

Saya sudah menambahkan plugin default_url pada Shrine initializer, yang berguna untuk, memberikan fallback image ke original image, apabila post tersebut, belum memiliki gambar yang sudah di derivatives (diturunkan).

Bisa saja, tidak perlu menambahkan plugin default_url, namun pada image_tag akan menjadi 3 kondisi seperti ini,

1<%= image_tag (post.image_url(:small) || post.image_url || "img-default.jpg") %>

Atau teman-teman dapat pula membuat view helper sendiri untuk menghandlenya.

Nah, contoh blok kode di atas adalah untuk membuat tampilan thumbnail dari artikel list yang ad di posts#index. Karena itu, saya menggunakan ukuran :small.

Berdasarkan image turunan yang saya definikan di dalam model image_uploader, terdapat 3 ukuran, :small, :medium, dan :large.

Tinggal dikondisikan saja, ukuran-ukuran mana saja yang akan digunakan pada template.

Misal pada bagian detail artikel, saya menggunakan ukuran :medium. Lalu apabila gambar diklik, gambar akan menampilkan ukuran :large.

Tambahan

Misalkan, aplikasi yang kita buat sudah terdapat gambar-gambar yang sudah diupload.

Bagaimanakan kita dapat membuat gambar turunan dari semua gambar yang sudah terlanjur berada di dalam server?

Nah, tinggal dibuatkan saja script yang akan menjalankan proses tersebut.

Saya akan buat direktori baru bernama script dan membuat file baru bernama derivatives_bomb.rb.

Namanya sengaja saya buat keren. Biar sedikit memprovokasi.

script/derivatives_bomb.rb
1# WHAT IS THIS?
2# This script is for adding derivatives in existing photos purposes
3# Existing photos means, photos that had been on server before image
4# derivatives had implemented
5# If there are any questions about this script, Please take a look on
6# https://shrinerb.com/docs/changing-derivatives
7#
8# HOW TO USE?
9# Run this script on Terminal.
10# $ rails runner scripts/derivatives_bomb.rb
11
12# Model-model yang memiliki image_data di dalam fieldnya
13normal_targets = ['User', 'Post']
14normal_targets.each do |model|
15 puts "\n#{model}s image derivating process starting..."
16 progress = 'Progress ['
17 i = 0
18 (model.constantize).find_each do |photo|
19 i = i + 1
20 if i % 1 == 0
21 progress << "|"
22 print "\r"
23 print progress + " #{i} / #{(model.constantize).all.size} %]"
24 $stdout.flush
25 sleep 0.05
26 end
27
28 attacher = photo.image_attacher
29 next unless attacher.stored?
30 attacher.create_derivatives
31 begin
32 attacher.atomic_persist
33 rescue Shrine::AttachmentChanged,
34 ActiveRecord::RecordNotFound
35 attacher.delete_derivatives
36 end
37 end
38 puts " DONE!"
39end
40
41# Model yang field image_data nya berada pada tabel yang berelasi
42# Misal, tabel experiences yang menyimpan data foto pada tabel photos
43puts "\nExperiences image derivating process starting..."
44progress = 'Progress ['
45i = 0
46Experience.find_each do |exp|
47 i = i + 1
48 if i % 1 == 0
49 progress << "|"
50 print "\r"
51 print progress + " #{i} / #{Experience.all.size} %]"
52 $stdout.flush
53 sleep 0.05
54 end
55
56 # photo_image field
57 attacher = exp.photo.photo_experience_attacher
58 next unless attacher.stored?
59 attacher.create_derivatives
60 begin
61 attacher.atomic_persist
62 rescue Shrine::AttachmentChanged,
63 ActiveRecord::RecordNotFound
64 attacher.delete_derivatives
65 end
66end
67puts " DONE!"
68
69puts """
70d8888b. .d88b. d8b db d88888b db
7188 `8D .8P Y8. 888o 88 88' 88
7288 88 88 88 88V8o 88 88ooooo YP
7388 88 88 88 88 V8o88 88~~~~~
7488 .8D `8b d8' 88 V888 88. db
75Y8888D' `Y88P' VP V8P Y88888P YP
76"""

Cara menjalankannya sangat mudah,

$ rails runner script/derivatives_bomb.rb

Tunggu prosesnya sampai selesai.

Running via Spring preloader in process 135133

Users image derivating process starting...
Progress [|||||||||| 10 / 10 %] DONE!

Posts image derivating process starting...
Progress [|||||||||||||||||||| 20/20 %] DONE!

Experiences image derivating process starting...
Progress [|||||||||||||||||||||||||||||| 30 / 30 %] DONE!

d8888b.  .d88b.  d8b   db d88888b db
88  `8D .8P  Y8. 888o  88 88'     88
88   88 88    88 88V8o 88 88ooooo YP
88   88 88    88 88 V8o88 88~~~~~
88  .8D `8b  d8' 88  V888 88.     db
Y8888D'  `Y88P'  VP   V8P Y88888P YP

Ahahaha.

Yah, bagaimanapun script ini masih jauh dari sempurna. Masih remah-remah biskuit selamet.

Namun, cukup berguna bagi saya.

Pesan Penulis

Catatan ini masih jauh dari kata sempurna.

Karena keterbatasan waktu, ilmu dan pemahaman yang saya miliki.

Apabila terdapat kendala dalam mengaplikasikan Shrine image processing ini, teman-teman dapat merujuk pada sumber-sumber dokumentasi resmi yang saya sertakan di bawah.

Sepertinya cukup segini saja.

Mudah-mudahan dapat bermanfaat bagi teman-teman yang memerlukan.

Terima kasih.

(^_^)

Referensi

  1. github.com/shrinerb/shrine
    Diakses tanggal: 2020/02/19

  2. github.com/janko/image_processing
    Diakses tanggal: 2020/02/19

  3. shrinerb.com/docs/processing
    Diakses tanggal: 2020/02/19

  4. shrinerb.com/docs/changing-derivatives
    Diakses tanggal: 2020/02/19