Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby:mini_magickを使って画像に文字(テキスト)を合成する

みなさん、こんにちは。まどぎわです(・∀・)
今日は、mini_magicと使って画像にテキストを合成する方法をメモしておきますφ(..)

mini_magicを使うと某匿名質問サービスみたいに画像に文字を合成する機能を結構簡単に作ることが出来ます🙌

↓公式のリポジトリはこちら↓

github.com

mini_magicの使い方

準備

mini_magicを使うには、ImageMagickというアプリがインストールされている必要があります。 まずはインストールされているかどうか確認してください。

$ convert -version
Version: ImageMagick 6.9.9-40 Q16 x86_64 2018-07-14 http://www.imagemagick.org
Copyright: © 1999-2018 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Features: Cipher DPC Modules
Delegates (built-in): bzlib freetype jng jpeg ltdl lzma png tiff xml zlib

インストールされていなかったかたは、「ImageMagick インストール」等でググッってインストールしてください(..)

mini_magicのインストール

mini_magicも通常のGemと同じようにGemfileに追記するか、下記コマンドを実行することでインストールすることが出来ます。

gem 'mini_magick'

文字列を受け取って画像を生成してみる

背景画像を準備

まずは文字列を埋め込む背景画像を用意します。今回は、こんな感じのを用意しました。 f:id:madogiwa0124:20180721163042p:plain

文字列を受け取って画像に合成する処理を作ってみる

実際の実装を下記に記載しておきますφ(..)

class ImageHelper
  require 'mini_magick'
  require 'securerandom'

  BASE_IMAGE_PATH = './app/assets/images/bg_image.png'.freeze
  GRAVITY = 'center'.freeze
  TEXT_POSITION = '0,0'.freeze
  FONT = './app/assets/fonts/komorebi-gothic.ttf'.freeze
  FONT_SIZE = 65
  INDENTION_COUNT = 11
  ROW_LIMIT = 8

  class << self
    # 合成後のFileClassを生成
    def build(text)
      text = prepare_text(text)
      @image = MiniMagick::Image.open(BASE_IMAGE_PATH)
      configuration(text)
    end

    # 合成後のFileの書き出し
    def write(text)
      build(text)
      @image.write uniq_file_name
    end

    private

    # uniqなファイル名を返却
    def uniq_file_name
      "#{SecureRandom.hex}.png"
    end

    # 設定関連の値を代入
    def configuration(text)
      @image.combine_options do |config|
        config.font FONT
        config.gravity GRAVITY
        config.pointsize FONT_SIZE
        config.draw "text #{TEXT_POSITION} '#{text}'"
      end
    end

    # 背景にいい感じに収まるように文字を調整して返却
    def prepare_text(text)
      text.scan(/.{1,#{INDENTION_COUNT}}/)[0...ROW_LIMIT].join("\n")
    end
  end
end

こんな感じで、使うことが出来ます(・∀・)

# 生成した画像のFileClassを取得
ImageHelper.build('何かしらの文字列を合成してみる').tempfile.open.read
# 生成した画像の書き出し
ImageHelper.write('何かしらの文字列を合成してみる')

↓出来た画像がこちら↓ f:id:madogiwa0124:20180721164012p:plain

おわりに

設定周りのやり方を調べるのに結構苦労しましたが、実装は意外とシンプルですね(・∀・)
ちなみに私も最近作ったサービスでも使ってみました🙌
↓みたいなQuizを投稿して画像でシェア出来るサービスです(・∀・) quiz-quiq.herokuapp.com

参考資料

keruuweb.com

github.com

RubyonRails:current_userへのメソッド呼び出しで発生したN+1に対応する

まどぎわです(・∀・)今日は、current_userへのN+1問題の対応についてです!
railsで開発していると、N+1問題にぶつかって対応することが多いかと思います。 普通であれば、DBアクセス時にincludes等を使って関連モデルをキャッシュしておくことで対応することが多いかと思います。

しかし、current_userのメソッド呼び出しでN+1問題が発生してしまった場合、だいたいDBアクセス処理はライブラリで行われているため、DBアクセス時に関連モデルをキャッシュしようとしても、「どうすれば、いいんや・・・」という気持ちになり、対応方法を調べたのでメモしておきますφ(..)

前提

今回は、下記のようなUserモデルとQuestionモデルとQuestionAnswerモデルがあり、Userモデルに#answerd?というUserがその質問に回答済みかどうかを判定するメソッドを定義しています。
何も対応しないとQuiz一覧画面等で、このメソッドを呼び出すとN+1問題が発生してしまいます。。。

class User < ApplicationRecord
  has_many :question_answers

  def answerd?(question)
    question.question_answers.to_a.select{ |answer| answer.user_id == id }.present?
  end
end

class QuestionAnswer < ApplicationRecord
  belongs_to :user
  belongs_to :question
end

class Question < ApplicationRecord
  has_many :question_answers
end

対応方法

current_userをもとにUserインスタンスを再生成する

controller内でcurrent_userをもとにUserインスタンスを再生成し、その際にincludesで関連モデルをキャッシュするようにします!これにより、#answerd?を呼び出してもキャッシュされた値から処理が行われるので、N+1問題を防ぐことが出来ます🙌

class QuizzesController < ApplicationController
  def index
    @user = User.includes(:question_answers).find(current_user.id)
    @questions = Quiz.includes(:choices)
  end
end

参考

stackoverflow.com

RubyonRails:rails5.2の新機能「ActiveStrage」で画像アップロード処理をお手軽に実装してみる

Rails5.2でActiveStrageというファイルアップロード系の機能が新しく追加されましたね(・∀・)
ちょっと自分が作っているアプリで使ってみたので使い方をφ(..)メモメモ

ActiveStrageとは?

ActiveStrageとは、Rails5.2から実装されたファイルアップロード機能です。
今までは画像等をアップロードするときにはCarrierWaveShrineといったGemを使うことが多かったと思いますが、それがRailsだけで簡単に出来るようになりました🙌

railsguides.jp

ActiveStrageを使ってみた

ソースだけ見たい方はこちら👀

github.com

準備

まずはActiveStrageを使うために下記コマンドを実行します。

$ rails active_storage:install
$ rails db:migrate

そして、ファイルのアップロード先を設定します。アップロード先はconfig.active_storage.serviceに設定されたkeyでstarage.yamlを参照します。

strage.yml

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

development.rb

  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :local

ファイルをアップロードしてみる

まずはModelにファイルのプロパティを定義します。1つのファイルをアップロードする場合はhas_one_attached、複数のファイルをアップロードする場合はhas_many_attachedを使用します。

class User
  has_one_attached :avatar
end

次にcontrollerとviewを次のように実装します。
新規登録の場合は、パラメーターにavatorを追加してnewの引数に渡してあげるだけでいいのですが、既存レコードにファイルを設定する場合は.avatar.attachを使用する必要があります。

controller

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: message('user', 'create')
    else
      render :new
    end
  end

  def update
    @user = User.find(current_user.id)
    @user.avatar.attach(params[:user][:avatar])
    if @user.update(user_params)
      redirect_to @user, notice: message('user', 'update')
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :name, :password, :password_confirmation, :avatar)
  end

view

= form_for @user do |f|
  %div.form-group
    = f.label :avatar
    = f.file_field :avatar, class: 'file-inputs'
  .actions
    = f.submit t('common.save'), class: 'btn btn-primary'

これで、画面からアップロードが出来るようになりました!!(・∀・)

バリデーションを実装してみる

しかし、このままでは画像以外がアップロードされてしまったり、大きなファイルがアップロードされてしまうといったことが防げません。。。
ActiveStrageは、まだvalidationの機能が無いので、自分で実装する必要があります😥

今回は、サイズのチェックと画像のみをアップロード可能とするようなバリデーションをサンプルとして載せましたので、参考までに。

class User < ApplicationRecord
  validate  :file_validation, if: -> { avatar.attached? }

  private

  def file_validation
    if avatar.blob.byte_size > 1_000_000
      file_raise_error('のファイル容量が大きすぎます')
    elsif !avatar.blob.content_type.starts_with?('image/')
      file_raise_error('は、画像以外はアップロード出来ません')
    end
  end

  def file_raise_error(message)
    avatar.purge
    errors.add(:avatar, message)
  end
end

おわりに

ActiveStrageはシンプルなファイルアップロード機能を実装するのであれば、お手軽に実装出来てよさそうに感じました(・∀・)
しかし、バリデーションを独自で実装する必要があるため、あまり複雑なアップロード処理にはまだ向かなそうですね😥

参考

tech.unifa-e.com

railsguides.jp

stackoverflow.com

関連モデルの関連モデルの読み込み処理を行うN+1問題を解決する。

関連モデルの関連モデルを参照する場合のN+1問題の解消方法で少しハマったのでφ(..)メモメモ

問題

例えば、下記のようなClassがあった場合にtask.user.groupを取得しようとするとGroupの読み込み処理によりN+1問題が発生する。
そのためTaskを取得する際にはTask.all.includes(:user, :group)といった対策が考えられるが、groupはタスクにassociationとして定義されていないので、指定出来ない。。。

class Task < ApplicationRecord
  belongs_to :user, required: false
  belongs_to :owner, class_name: 'User'
end

class User < ApplicationRecord
  has_many :tasks
  has_one :user_group
  has_one :group, through: :user_group
end

class UserGroup < ApplicationRecord
  belongs_to :user
  belongs_to :group
end

class Group < ApplicationRecord
  has_many :user_groups
  has_many :users, through: :user_groups
end

解決策

下記のようにincludes(user: :group)とすることで、関連モデルのassociationを使ってincludesを行うことが出来る🙌

Task.all.includes(user: :group)

参考

qiita.com

RubyonRails:accepts_nested_attributes_forとfields_forを使って紐づくモデルのフォームを作成する

railsの機能のaccepts_nested_attributes_forfields_forを使うと紐づくモデルも合わせて簡単に作成できたのでメモφ(..)メモメモ

今回の想定ケース

今回はクイズのように質問があって、それに紐づく選択肢があるようなケースを想定してます。
実装イメージはこんな感じです🙌 f:id:madogiwa0124:20180603212606p:plain

手順

手順はこんな感じです。

  • Modelに合わせて作成するモデルをaccepts_nested_attributes_forで設定
  • Controllerで合わせて作成するモデルのインスタンスを生成
  • Viewでfields_forを使って合わせて作成するモデルのフォームを作成

Model

class Quiz < ApplicationRecord
  has_many :choices, dependent: :destroy
  # 合わせて作成するモデルを`accepts_nested_attributes_for`で設定
  accepts_nested_attributes_for :choices
end

class Choice < ApplicationRecord
  belongs_to :quiz
end

Controller

class QuizzesController < ApplicationController
  def new
    @quiz = Quiz.new
    # 合わせて作成するモデルのインスタンスを生成
    @quiz.choices.new
    # 複数のフォームを作る場合は、複数回newする
    # n.times { @quiz.choices.new }
  end

  def create
    @quiz = Quiz.new(quiz_params)
    if @quiz.save
      redirect_to quizzes_path
    else
      render :new
    end
  end

  private

  def quiz_params
    params.require(:quiz).permit(:title, :body, :explanation, choices_attributes: %i[id sentence correct])
  end
end

View

= form_for @quiz do |f|
  - if @quiz.errors.any?
    = render 'layouts/error_form', resource: @quiz
  .form-group
    = f.label :title, 'タイトル'
    %br
    = f.text_field :title
  .form-group  
    = f.label :body, '本文'
    %br
    = f.text_area :body  
  .form-group
    = f.label :explanation, '解説'
    %br
    = f.text_area :explanation
  .form-group.choices_area
    %h3 選択肢
    %ul
      - # `fields_for`を使って合わせて作成するモデルのフォームを作成
      = f.fields_for :choices do |choice|
        %li
          = choice.text_field :sentence
          = choice.check_box :correct
  = f.submit '保存'

参考資料

fields_for - リファレンス - - Railsドキュメント

api.rubyonrails.org

ruby-rails.hatenadiary.com

Ruby:ShrineでUploader個別にアップロード先を設定するメモ

最近、ファイルアップロード用のgemであるShrineを使用する機会があったんですが、Uploader別にアップロード先を設定する方法が全然見つからず、だいぶハマってしまったので、やり方をφ(..)メモメモ

github.com

とりあえず公式のQuick startを見てみる

Shrine.storageに各Uploaderが共通で使用するストレージを情報を設定できる。

config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"), # permanent
}

Shrine.plugin :sequel # or :activerecord
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
Shrine.plugin :rack_file # for non-Rails apps

共通のアップロード先にアップロードするだけだったら、簡単で使いやすい😃

アップロード先を個別に設定する

Quick startを見てみるとShrine.stragesにアップロード先の情報を設定されているため、この値を変更する必要があるっぽい。 でも単純にShrine.strageを変更すると、画像アップロード処理全体に影響してしまう...😭

各クラスにShrine.stragesがなぜ反映されているか?

Shrine.staragesグローバル変数みたいな感じで定義されていて、それを参照しているのかと思ったけど、 どうやらサブクラスのクラスインスタンス変数としてdupでコピーされた値が反映されているっぽい😳

https://github.com/shrinerb/shrine/blob/master/lib/shrine.rb#L101

class Shrine
  @storages = {}
  module Base
    # 省略
    subclass.instance_variable_set(:@storages, storages.dup)

各Uploaderクラスに個別でアップロードパスを設定するには?

サブクラスのクラスインスタンス変数として設定されているということは、こんな感じでUploader別に設定出来るのでは?? 👀

class HogeImageUploader < Shrine
  @storages = {
    cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/hoge/cache"), # temporary
    store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/hoge/store"), # permanent
  }

image_urlも変わってる、できたっぽい🙌

# 個別の設定が使用されている
[1] pry(main)> HogeImage.first.image_url
=> "/uploads/store/hoge/hogehoge.png"
# 共通設定が使用されている
[2] pry(main)> FugaImage.first.image_url
=> "/uploads/store/fugafuga.png"

S3のバケットを使う場合でも同じように個別に設定してあげればOKみたいです😃

class HogeImageUploader < Shrine
  s3_options = {
    bucket:            "my-bucket", # required
    access_key_id:     "abc",
    secret_access_key: "xyz",
    region:            "my-region",
  }

  @storages = {
    cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
    store: Shrine::Storage::S3.new(**s3_options),
  }

2018/06/03追記

default_stragepluginをつかっても下記のような感じで出来るみたいですφ(..)

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"), # permanent
  special_cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
  special_store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"), # permanent
}

Shrine.plugin :sequel # or :activerecord
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
Shrine.plugin :rack_file # for non-Rails apps
class HogeImageUploader < Shrine
  plugin :default_storage, cache: :special_cache, store: :special_store
end

Shrine::Plugins::DefaultStorage

GithubMarketplaceで自動テスト、コード解析環境が簡単に出来て最高かよってなった話

GithubMarketPlaceからCircleciを導入してみたら、初めてだったたけど意外と簡単に、しかも無料で自動テスト・コード解析環境が作れて「最高かよ」という気持ちになった💪
ちなみに構築した環境は下記のような感じで、普通のrailsアプリケーションです🚋

key val
Langage ruby 2.5
Framework rails 5.2
Database posgresql
Testing Framework rspec
Code analyze rubocop

やり方メモ📝

GithubMarketPlaceでCircleciを導入する

まずはここから、Circleciを導入しましょう! github.com

こんな感じでcircleciを検索して、、、 f:id:madogiwa0124:20180524231227p:plain

私はもう導入済なので、「Edit your plan」となってしまってますが、「Setup a plan」になっていると思います🙌 f:id:madogiwa0124:20180524231558p:plain

クリックしたらcircleciの導入は完了です!

circleciにプロジェクトを追加する

circleciの導入が完了したら、次はリポジトリを紐付けます! Add Projectから自分のリポジトリを追加しましょう! f:id:madogiwa0124:20180524231739p:plain

circleci/config.ymlを作成する

リポジトリを追加したら、下記コマンドを実行してcircleciの設定ファイルを作成します。

$ mkdir circleci
$ touch circleci/config.yml

config.ymlの中身は私は下記のように設定しました。

version: 2
jobs:
  build:
    docker:
       - image: circleci/ruby:2.5.1-node-browsers
         environment:
           RAILS_ENV: test
           PGHOST: 127.0.0.1
       - image: circleci/postgres
         environment:
           POSTGRES_USER: circleci

    working_directory: ~/repo

    steps:
      - checkout

      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "Gemfile.lock" }}
          - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle

      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
        
      # Database setup
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

      # run tests!
      - run:
          name: run tests
          command: |
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"            
            bundle exec rspec $TEST_FILES
      - run:
          name: run code analyze
          command: bundle exec rubocop

結果を確認する

circleci/config.ymlを作成したら、いよいよリポジトリにpushします(・∀・) そうすると、、、 f:id:madogiwa0124:20180524232316p:plain

おおー、無事にコミット時にrspecとrubocopが実行されてますね🌟 f:id:madogiwa0124:20180524232443p:plain

おわりに

どうですかね?私はあまりこういう環境とか作ったことなかったんですが、結構簡単に作れてびっくりしました👀
circleciで自動テスト+コード解析して、本番環境にHerokuを使えばGithubのブランチと同期させて自動デプロイとかいう環境が無料で作れるとか、個人開発がはかどりますね😇!