Middlemanでサムネイル画像を作る方法

middleman

Middlemanでこのようなサムネイル画像を作る方法をご紹介します。このようなサムネイル画像をMiddlemanでも作ることができます。

Middlemanで作ったサムネイル画像(メイン)

サイドバーによくある小さなサムネイルももちろん可能です。

Middlemanで作ったサムネイル画像(サイドバー)

  1. サムネイルを生成するためのgem
  2. middleman-simple-thumbnailerの使い方
    1. 導入方法
    2. 使い方
    3. 記事の最初にでてくる画像をサムネイルにする方法
    4. 記事内の画像が表示されなくなってしまう場合
    5. 100px以下のサイズを指定するとエラーが起きる
    6. build時に元ファイルに更新があったものだけ作り直すようにする
    7. middleman-s3_syncでサムネイル画像を同期してくれない
  3. さいごに

サムネイルを生成するためのgem

Middlemanのサムネイル生成のgemで有名なのが次の2つです。

両方試したのですが、私は前者を採用しました。というのは、後者はサムネイルを生成する画像があるディレクトリを指定する必要があったためです。画像を特定のディレクトリにまとめている人はよいのですが、自分の場合は下のような記事ごとのディレクトリに画像を保存してありました。

source/articles/2016-11/02-tom-yum-goong-noodle/images/

なので、保存ディレクトリを気にしなくてよい、前者を採用しました。

middleman-simple-thumbnailerの使い方

導入方法

Gemfileに以下を追記しbundle installを実行します。

1
gem 'middleman-simple-thumbnailer'

config.rbに以下を追記すれば使えるようになります。

1
activate :middleman_simple_thumbnailer

うまくbundle installできなかったり、正常に動かない場合

お使いの端末にGraphicsMagickというモジュールをインストールする必要がある場合があります。

Macでhomebrewをお使いのかたは下のようにインストールできます。

1
brew install graphicsmagick

使い方

View側でimage_tagというヘルパーメソッドを次のように使うことができます。

1
= image_tag image, resize_to: '50x50', class: 'thumbnail'

image_tagはもともとMiddlemanにあるヘルパーメソッドですが、middleman-simple-thumbnailerをインストールすると、resize_toというオプションが追加されます。これに生成したいサムネイルのサイズを指定してあげるだけです。

image_tagがやってくれることはざっくり次の2点です。

  • imageで指定した画像のresize_toで指定したサイズのバージョンを作る
  • src属性に生成したサムネイルのパスを指定したimgタグを出力する

縦と横の両方それぞれ指定できるわけではない

上の例では '50x50'と指定していますが、縦と横のサイズが違う場合、どちらも50pxにしてくれるというわけではないようです。長い方を50にし、縦横の比率を変えずに短いほうの長さが決まるようです。

なので、縦横比を変えたい場合は、CSSのheigtやwidth属性で制御するのがよいでしょう。

記事の最初にでてくる画像をサムネイルにする方法

先ほどの例では、imageのところに、いちいちサムネイル化したい画像のパスを指定しなければなりません。通常のサイトやブログ運営では、記事の一番最初に出てくる画像を勝手にサムネイルにしてくれたら楽だと思います。その方法をご説明します。

まず、記事のオブジェクトを引数にとると、メインの画像のパスを返してくれるヘルパーメソッドを作ります。ヘルパーメソッドはconfig.rb内で、helpers doの中で定義することができます。

1
2
3
4
5
6
7
8
9
helpers do
  def main_img_path(article)
    imgs = Nokogiri::HTML.parse(article.body).xpath('//img')
    blank_thumbnail = "/images/blank_thumbnail.png"
    return blank_thumbnail if imgs.blank?
    img_src = imgs.first.attr('src')
    img_src =~ /^http.*$/ ?  blank_thumbnail : img_src
  end
end

Nokogiriというライブラリでhtmlをパースし、最初に出てくるimgタグのsrc属性を取得しています。また画像を使っていない場合や、外部サイトの画像を使っている場合には、空白用の画像のパスを返すようにしています。

View側では下のように、記事のオブジェクトを引数にして使います。

1
  = image_tag main_img_path(article), resize_to: '300'

記事内の画像が表示されなくなってしまう場合

記事をKramdownというMarkdownパーサーを使っている場合などに、上の手順を行うと記事内の画像が表示されなくなる場合があります。

Kramdownだと下のように相対パスを指定した場合、current_articleからの相対パスと解釈してくれて適切なパスに変換してくれます。

1
![サンプル画像](sample.jpg)

下のように適切なパスを出力してくれる。

1
<img alt="サンプル画像" src="/articles/2016-11/09-middleman-thumbnail/images/sample.jpg">

ですが、上の手順で、記事の一番上の画像のパスを取り出す処理にて、対象の記事がcurrent_articleでない時に、記事を生成することになります。生成される画像のパスは下のようにサイト全体のimagesディレクトリからの相対パスにしてしまいます。

1
<img alt="サンプル画像2" src="/images/images/sample.jpg">

その記事のURLにアクセスした時に、current_articleがその記事になるので適切に生成し直してくれるのでは?と思うかもしれませんが、一度生成された記事はキャッシュの中に残りそれが出力されてしまいます。Middlemanではキャッシュのクリアを任意にすることができないようです。

なので、記事を表示するテンプレートにて、適切なパスに置換する処理を加えます。まずヘルパーメソッドに下のように追記します。

1
2
3
4
5
6
7
def render_with_correct_img(article)
  doc = Nokogiri::HTML(article.body)
  doc.xpath("//img[substring(@src, 1, 15) ='/images/images/']").each do |elm|
    elm["src"] = "/" + article.path.gsub(/index\.html/,'') + 'images/' + elm.attr("src").split("/").last
  end
  doc.css('body')[0].inner_html
end

記事を出力するView側にて下のようにします。

1
= render_with_correct_img article

また、この問題が発生している人は、先ほど作ったmain_img_path内で取得するパスもおかしい可能性が高いです。main_img_path内でもrender_with_correct_imgを使って下のように修正します。2行目が修正されています。

1
2
3
4
5
6
7
def main_img_path(article)
  imgs = Nokogiri::HTML(render_with_correct_img(article)).xpath('//img')
  blank_thumbnail = "/images/blank_thumbnail.png"
  return blank_thumbnail if imgs.blank?
  img_src = imgs.first.attr('src')
  img_src =~ /^http.*$/ ?  blank_thumbnail : img_src
end

100px以下のサイズを指定するとエラーが起きる

これは自分のパソコンだけかもしれませんが、resize_toで100以下を指定すると、次のようなエラーが発生しました。

1
MiniMagick::Error at /bootcamp-vs-parallels-11-vs-fusion-8/`gm mogrify -resize 100 /var/folders/vc/3xzdspjn5_j53wlgmf23prsh0000gn/T/mini_magick20161108-56972-1ucpfe.gif` failed with error:Ruby/Users/taktak/blog/webfood/vendor/bundle/ruby/2.1.0/gems/mini_magick-4.5.1/lib/mini_magick/shell.rb: in run, line 18 WebGET localhost/bootcamp-vs-parallels-11-vs-fusion-8/

なので、小さくても150程度にし、さらに小さくしたい場合はcss側で調整するようにしたほうがいいです。

build時に元ファイルに更新があったものだけ作り直すようにする

build時に全サムネイルを作り直します。しかも、同じサムネイルファイルでも複数箇所から呼び出している場合、その回数分生成し直すという原始的な仕様なので非常に時間がかかります。

なので、毎回全サムネイルを作る直すのをやめ、サムネイルと元ファイルのタイムスタンプを保存する管理ファイルを作り、元ファイルのタイムスタンプが変更された場合にのみ、一度だけ生成するように細工をしました。

まず、config.rbに次のコードを入れ、middleman build --cleanとオプションをつけないとサムネイルを削除しないようにします。デフォルトだと逆で、--no-cleanとつけて削除しないようになっています。

1
2
3
4
5
6
Middleman::Cli::BuildAction.class_eval do
  protected
  def should_clean?
    ARGV.include?("--clean")
  end
end

そして、config.rbからclass_evalを使うことで、gemを直接修正しなくても挙動を変えることが可能です。元ファイルから問題のinitializeメソッドをまるまるコピーし、コードを追加しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
MiddlemanSimpleThumbnailer::Extension.class_eval do
  def initialize(app, options_hash={}, &block)
    super
    app.after_build do |builder|
      conf_path      = "#{build_dir}/thumbnails.yml"
      conf_update    = false
      thumbnails     = {}
      conf_exist     = File.exist?(conf_path)
      thumbnails     = YAML.load_file(conf_path) if conf_exist
      target_imgs    = MiddlemanSimpleThumbnailer::Image.all_objects
      target_imgs.each do |img|
        org_timestamp    = File.mtime( source + img.img_path)
        file_exist       = File.exist?( build_dir + img.resized_img_path)
        if file_exist && thumbnails[img.resized_img_path] == org_timestamp
          next
        else
          img.save!
          if file_exist
            builder.say_status :update, "#{build_dir + img.resized_img_path}"
          else
            builder.say_status :create, "#{build_dir + img.resized_img_path}"
          end
          thumbnails[img.resized_img_path] = org_timestamp
          conf_update = true
        end
      end
      (thumbnails.keys - target_imgs.map(&:resized_img_path).uniq).each do |img|
        File.delete        build_dir + img
        builder.say_status :delete, "#{build_dir + img}", :red
        thumbnails.delete  img
        conf_update        = true
      end
      if conf_update
        File.open(conf_path,'w') do |h|
           h.write thumbnails.to_yaml
        end
        if conf_exist
          builder.say_status :update, "#{conf_path}"
        else
          builder.say_status :create, "#{conf_path}"
        end
      else
        builder.say_status :identical, "#{conf_path}", :blue if conf_exist
      end
    end
  end
end

※ middleman-simple-thumbnailer (1.0.2)の場合

ちなみに、config.rbの追記する場所は、acitivateする次の部分より上に書かないと反映されません。

1
activate :middleman_simple_thumbnailer

これで大幅にbuild時間が短くなりました。

middleman-s3_syncでサムネイル画像を同期してくれない

Amazon S3を利用していて、ファイルの同期にmiddleman-s3_syncというgemを使っている場合は、デフォルトだとサムネイル画像は同期してくれません。

こちらもconfig.rbに細工することで同期することが可能です。

1
2
3
4
5
6
7
8
9
10
if ARGV[0] == "s3_sync"
  Middleman::S3SyncExtension.class_eval do
    def manipulate_resource_list(mm_resources)
      thumbnails = Dir.glob(["**/*.300.jpg", "**/*.150.jpg"])
      ::Middleman::S3Sync.mm_resources = mm_resources + thumbnails.map do |t|
        Sitemap::Resource.new(app.sitemap, t.gsub(/^build\//,""), File.join(app.root, t))
      end
    end
  end
end

こちらは、acitvate :s3_syncよりも上に追記してください。

"**/*.300.jpg", "**/*.150.jpg"という部分はサムネイルのファイル名をワイルドカードで指定しています。ファイル名の数字の部分はご自身の使い方で変わるので適宜修正してください。

さいごに

いかかでしたでしょうか?きれいにサムネイルを表示できましたでしょうか?

ちなみに、この作業で使ったそれぞれのライブラリのバージョンは以下です。

ライブラリ名 バージョン
middleman-core 3.4.0
middleman-blog 3.6.0.beta.2
middleman-simple-thumbnailer 1.0.2
middleman-s3_sync 3.3.4