BLOG

Hugo で画像をリサイズしたりトリミングしたりするテンプレートを書いてみる

Created at:
Category: Hugo

Drupal をはじめとする一般的な CMS であれば、アップロードした画像ファイルのサイズ調整などを自動で処理する機能が準備されています。

Hugo にも同様の機能 がありましたので、試してみました。

やりたいことは、ブログ一覧に表示されるタイトル画像を縮小すること。そして、ブログの各投稿のページは上下を切り取った画像を表示すること。この 2 点です。

しかし、思った通りに動作しません。調べながら画像ファイルのサイズ調整の仕組みを作っていたのですが、結構はまってしまったのでまとめておこうと思います。

画像ファイルは記事毎に置く

先に結論を書きます。これが出来ていないから動作しませんでした。

Hugo のサイト にも次のように書かれています :

The image is a Page Resource, and the processing methods listed below does not work on images inside your /static folder.

/static フォルダー内の画像では機能しません ということみたいです。ちゃんと読めていませんでした。

さて、今回どのようなディレクトリ構造に変更したのかを以下にまとめます。

今までは、以下のように content/blog/ に Markdown で書いた記事を置き、 content/blog/images/ に画像をまとめて置いていました。つまり以下のような状態です。

content
└── blog/
    ├── _index.md
    ├── images/
    │   ├── fugafuga.jpg
    │   ├── hogehoge.jpg
    │   ├── post01-head.jpg
    │   └── post02-head.jpg
    ├── post01.md
    └── post02.md

2つの投稿を post01post02 というディレクトリに変更し、記事と画像を置きました。 画像は、各ディレクトリの下に images というディレクトリを作成して置いています。 ただし、 images というディレクトリは必須ではありません。

以下のような状態になります。

content
└── blog/
    ├── _index.md
    ├── images/
    │   └── hogehoge.jpg
    ├── post01/
    │   ├── images/
    │   │   └── post01-head.jpg
    │   └── index.md
    └── post02/
        ├── images/
        │   ├── fugafuga.jpg
        │   └── post02-head.jpg
        └── index.md

こうすることで記事と画像が関連付けられ、記事の内容と画像とを一緒に表示するテンプレートの記述が可能になります。

どういったテンプレートを記述すればよいのか、次章以降で紹介します。

画像処理のテンプレート

この記事ではテンプレートの書き方までは詳しく説明しません。

partial テンプレート と shortcode テンプレートのどちらを利用するかが最初はわかりにくいと思いますが、 テンプレートの中で利用するテンプレートか?それとも記事の中で利用するテンプレートか?のルールで使い分けています。

  • テンプレートの中から呼び出す partial テンプレート
    • ブログの記事一覧にリサイズした画像を表示するプログラム
    • ブログの記事ページのヘッダ部分に上下を切り落とした画像を表示するプログラム
  • 記事の中から呼び出す shortcode テンプレート
    • 記事の中で画像を表示する為のプログラム

ブログ記事一覧用 partial テンプレート

テーマや root ディレクトリの layouts/partials/ ディレクトリに以下のファイルを blog_summary.html という名前で作成します。

<section class="card">
  {{ $head_img := .Resources.GetMatch "images/*head.jpg" }}
  {{ if $head_img }}
    {{/* `*head.jpg` というファイル名の画像が記事に関連付けられている場合 */}}
    {{ $small := $head_img.Fit "360x270 q80" }}
    <img src="{{ $small.RelPermalink }}" class="card-img-top">
  {{ else }}
    <img src="/images/default_thumbnail.jpg" class="card-img-top">
  {{ end }}
  <div class="card-body">
    <h1 class="card-title">{{ .Title }}</h1>
    <div>{{ .Summary  }}</div>
  </div>
</section>

処理の内容は、以下のようになっています。

  • .Resources.GetMatch で、各記事に関連付けられたリソースから images/*head.jpg にマッチするファイルを探し、そのなかからひとつ目を取り出します(2行目)
  • マッチするファイルが存在した場合、 $head_img.Fit "360x270 q80" で幅 360px 高さ 270px におさまるように画像を縮小します。JPEG クオリティは 80 になります(5行目)
  • マッチするファイルが存在しない場合、 /images/default_thumbnail.jpg を表示させる img タグを生成します(8行目)

このテンプレートを実際に利用するには、 layouts/blog/list.html の内部で、上記 blog_summary.html テンプレートを呼び出すようにします。

例えば、以下のような感じに記述します。

{{ define "main" }}
<div class="container">
  <div class="row">
    {{ range .Paginator.Pages }}
    <div class="col-4">
      {{ partial "blog_summary.html" . }}
    </div>
    {{ end }}
  </div>
</div>
{{ end }}

各記事を順に取得( 4 行目)して blog_summary.html テンプレートに処理させます( 6 行目)。

なお、blog 記事のヘッダ部分に関連付けられた画像の上下をトリミングして表示する処理も作りましたが、 blog-summary.html を参考にすれば簡単に書けるはずなので、省略します。

記事の中で画像を表示する shortcode テンプレート

記事の中で画像とキャプションを指定すると figure タグを生成する shortcode テンプレートを作成します。 画像を丁度良いサイズに縮小するのはプログラムにやらせたいので、 600x480 のサイズに縮小した画像を生成するようにしました。縮小画像をクリックするとオリジナルサイズの画像が表示されます(スマホだと間違ってタップしそうなのが嫌かもw)。

<figure class="figure">
  <a href="{{ .Get "src" }}" target="_blank" title="original size">
    {{ $original_image := .Page.Resources.GetMatch (.Get "src") }}
    {{ if $original_image }}
      {{ $resized := $original_image.Fit "600x480 q80" }}
      <img src="{{ $resized.RelPermalink }}"
           class="figure-img img-fluid" alt="{{ .Get "caption" }}">
    {{ else }}
      <img src="{{ .Get "src" }}"
           class="figure-img img-fluid" alt="{{ .Get "caption" }}">
    {{ end }}
  </a>
  <figcaption class="figure-caption text-center" >
    {{ .Get "caption" }}
  </figcaption>
</figure>

処理内容は簡単なものだけなので省略します。

プログラム中の .Get "src".Get "caption" は呼び出し元の名前付きパラメータの値を取得しています。 ブログの記事( Markdown ファイル)の中では、以下のように名前付きパラメータで記述して下さい。

{{< figure src="images/redmine-theme-bbsite.png" caption="Redmine テーマ「bbsite」" >}}

各関数の使い方とか

せっかく色々と調べたので、メモしておきます。

画像の編集関数

画像の編集のなかから今回調べた Resize, Fit, Fill の 3つをまとめます。

Resize 指定サイズに画像を変更

幅が 1200px になるように縮小または拡大します。アスペクト比をそのままに高さを計算して調整します。

{{ $image := $resource.Resize "1200x" }} 

高さが 480px になるように縮小または拡大します。アスペクト比をそのままに幅を計算して調整します。

{{ $image := $resource.Resize "x480" }} 

幅を 200px に、高さを 480px になるように縮小または拡大します。アスペクト比が変わってしまいます。 例えば、元画像が 1200x900 サイズであれば、幅が極端に縮んだ画像になります。

{{ $image := $resource.Resize "200x480" }} 

Fit 指定した幅と高さにおさまるように画像を縮小

アスペクト比をそのままに、指定幅と高さの枠におさまるように画像を縮小または拡大します。

指定した枠のサイズが元画像のアスペクト比と異なる場合であっても、元画像のアスペクト比のまま拡大・縮小します。

{{ $image := $resource.Fit "200x480" }} 

例えば、幅 1200px 高さ 900px の画像で Fit 関数を呼び出すと、幅 200px 高さ 150px に変更します。高さのほうが計算された値になっています。

Resize 関数と違い、幅と高さの両方を指定しないとエラーになります。

Fill リサイズしてトリミングします

アスペクト比をそのままに、指定幅か、指定高さ 480px で拡大・縮小した後に、もう一辺の指定サイズにトリミングします。

{{ $image := $resource.Fill "200x480" }} 

例えば、幅 1200px 高さ 900px の画像で Fill 関数を呼び出すと、幅 640px 高さ 480px に縮小した後、幅 200px になるように両端を切り落とします。

画像の固定位置を指定することで、どの部分を切り落とすか指定できます。 設定できるのは、 Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight となります。

幅 1200px 高さ 900px の画像を下記のように右下を固定して処理すると、左端のみが切り落とされます。

{{ $image := $resource.Fill "200x480 BottomRight" }}

Fit 関数と同じように、幅と高さの両方を指定しないとエラーになります。

オプション

上記編集関数は、オプションとして JPEG クオリティ、回転、基準位置(Anchor 。Fill で利用可能)、サイズ変更時のフィルタ( Resample Filter ) を利用指定できます。

その他

上記関数の他には、フィルターや Exif 属性の取り出しが出来る関数もあります。

これらの関数は組み合わせても使えます。例えば、幅 400px 高さ 300px に縮小してからセピア色のフィルターを掛ける場合は以下のようになります。

{{ $image := ($resource.Fit "400x300").Filter (images.Sepia 40) }} 

リソースの取得方法

リソースの取得方法を以下に記載します。

ByType リソースの種類で指定

関連付けられた画像を全て取得します。

{{ .Resources.ByType "image" }}

Match リソースの名前

関連付けられたリソースから “abc*” にマッチするものを全て取得します。 メタデータが設定 されていれば name 属性でマッチします。 設定されていなければ、ファイル名でマッチします。

{{ .Resources.Match "abc*" }}

マッチするリソースのなかから一つだけ取り出すには、 GetMatch を用います。

{{ .Resources.GetMatch "abc*" }}

参考文献

この記事は以下のサイトを参考にしています。

この記事にも一部掲載していますが、画像処理の方法や、ページリソースの取得方法と属性を調べるには Hugo のリファレンスマニュアルが参考になります。