Chienomi

【実例学習】RedNotebookに画像を入れるスクリプト

プログラミング::learnin

LearnInサブカテゴリ

プログラミングカテゴリのサブカテゴリとしてlearninを新設した。

なんか、前も私が日常的に書いているスクリプトをアップするというのをやったような気がするけど、見た感じちゃんとカテゴリとして独立していなかったので、今回改めてサブカテゴリとして作ることにした。

このサブカテゴリは短く、分かりやすいコードが解説される。 もしあなたがプログラミングを習得したいと思っているのであれば

  • シンプルでエッセンシャルなコードの書き方
  • 「日常的行為としてのプログラミング」を考え方

を学ぶ絶好の材料になるだろう。

私は日記にRedNotebookを使っている。 不満はあるが、自分で作り直すほどのものでもなく、なんとなく使い続けている。

日記には写真をつけたいことがとてもよくある。 RedNotebookでもそれは可能だが、結構手順が面倒だ。

ここではそれをツール化して、日記に写真を組み込みやすくする。

前提となるRedNotebookの仕様

  • 日記データは~/.rednotebook/data/${YYYY}-${mm}.txtである
  • 以前はYAMLだったのだが、今はまたちょっと違う形式になっているようだ
  • 日記データの形式は何度か変わっている
  • 日記本文のフォーマットはt2t(Text2Tag)である
  • 画像リンクの形式は[""${path}"".${ext}]である
  • 画像リンクを相対リンクにした場合、基準ディレクトリは~/.rednotebook/dataである
  • WebPやAVIFは画像リンクとして認識されない

目標

~/.rednotebook/dataが起点になる相対リンクなので、~/.rednotebook/data/photosとかを作っておけば""./photos/""で始められるので楽になる。 私は~/.rednotebook/data/photos/${YYYY}/${YYYY}${mm}${dd}-${n}.${ext}に置くようにしている。 スクリーンショット等はphotosの代わりにimagesを使う。

元となる画像ファイルをこのパスに変換することが必要だ。

絶対に指定する必要があるパラメータは元ファイルのパス。 指定できたほうが良いパラメータは日付。

高性能なスマートフォンの写真は巨大なファイルになってしまうので、変換もしたほうが良いだろう。 ファイルサイズが閾値を越える場合、画像サイズを制限することでファイルを圧縮する。WebPやAVIFは使えないので、本格的に圧縮するならjpegoptim等を使う必要があるだろう。

コード

Gist

#!/bin/zsh

typeset opthash
zparseopts -D -A opthash -- i

infile="$1"
date="$2"

typeset dirname=photos

usage() {
  print "rnbimg.zsh infile <yyyymmdd>" >&2
  exit 1
}

if [[ -n "${opthash[(i)-i]}" ]]
then
  dirname=images
fi

if [[ -z "$infile" || ! -e "$infile" ]]
then
  usage
fi

if [[ -z "$date" ]]
then
  date=$(date +%Y%m%d)
else
  if [[ $date != [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] ]]
  then
    usage
  fi
fi

typeset -i count=1
typeset ext=${infile:t:e}
typeset -i convert=0

typeset -i infile_size=$(stat -c %s "$infile")

if (( infile_size > 307200 )) # 300k
then
  convert=1
  ext=jpg
fi

typeset dest=$HOME/.rednotebook/data/$dirname/${date[1, 4]}/$date

if [[ ! -e ${dest:h} ]]
then
  mkdir -pv ${dest:h}
fi

while [[ -e $dest-$count.$ext ]]
do
  (( count++ ))
done

if (( convert == 1 ))
then
  magick "$infile" -resize 800x800 $dest-$count.$ext
else
  cp "$infile" $dest-$count.$ext
fi

dest_link=${dest/$HOME\/.rednotebook\/data/.}-$count

print "[\"\"$dest_link\"\".$ext]"

解説

Zsh

このコードはBashでは動かない、純粋なZshコードである。

typeset

Zshにおける型付き変数宣言。 型に加えてスコープの宣言を兼ねている。 宣言しない場合グローバルな文字列または配列変数になる。

-iは整数型の宣言。 整数型の変数として宣言することで、Arithmetic Evaluationのコンテキストで便利に使えるようになる。

if-infile

if [[ -z "$infile" || ! -e "$infile" ]]

[[ ... ]]がConditional Expression。

-zは文字列展開してから文字列である場合。 -eはファイルの存在。!が反転。

if-date

if [[ -z "$date" ]]
then
  date=$(date +%Y%m%d)
else
  if [[ $date != [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] ]]
  then
    usage
  fi
fi

これは第2引数である$dateを省略可能にするためのもの。 主旨が省略された場合の補完であるため、通常とは逆転した書き方になっている(「nullである場合が真」)が、機能的には2つのことをしている。

date=$(date +%Y%m%d)

これはdate(1)を使ってデフォルトの日付を指定している。

  if [[ $date != [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] ]]

こっちは型チェック。8桁の数字からなる値を与えられていることを確認し、そうでない場合はusage関数を呼び出して異常終了する。

Zshの場合もうひとつの書き方として

  if [[ $date != <10000000-99999999> ]]

と数値判定してもいい。 これはちょっと手抜きで

  if [[ $date != [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9] ]]

としたほうがより適切。

引数チェック

より適切な書き方があるという話をしたが、ここにおいては必要十分なので特により適切な書き方をする必要はない。

入力チェックにおいて重要なのは、プログラムを書いた人(または動作させている人)と入力データを用意した人が同一かどうかである。 今回の場合同一、つまりは「自分のためのコード」であり、コマンドの使い方を忘れたとか、あるいは手が滑って入力を間違えたといったケースをカバーできれば良い。 より厳密には

if [[ $date != 20[0-9][0-9](0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[01]) ]]

みたいに書くこともできるが、それは悪意を持った、または無邪気な入力を警戒しなければならない場合であって、自分用のリマインダーとしてはあまり役に立たない。 ワンタイムなコードは不要なものを切り捨てることも大事だ。

:t:e

typeset ext=${infile:t:e}

infileがソースファイルのパス。これを展開するにあたり、:tでパスエレメントの最後(要はファイル名)を返し、:eで拡張子を返している。

stat

typeset -i infile_size=$(stat -c %s "$infile")

ファイルのメタデータを使いたい場合はstat(1)を使おう。 -c %sでファイルサイズ(バイト)を出力している。

サイズ判定と圧縮工程のフラグ

if (( infile_size > 307200 )) # 300k
then
  convert=1
  ext=jpg
fi

$infile_sizeは入力ファイルのバイトサイズである。 (( ... ))は算術評価で、出力はなく終了ステータスで判定するためのもの。

(( infile_size > 307200 ))

でファイルサイズが300kiB超である場合となる。

convertは整数型で、実質的なbooleanとして扱っている。Zshにはbooleanがないので、Zsh内部で扱うのならC言語のように整数値をbooleanにするのが無難。-z/-nで判定するために文字列の有無にしても良いが。

$extは出力拡張子である。 もともとは

typeset ext=${infile:t:e}

だからソースファイルの拡張子なのだが、変換する場合はJPEGに変換したいので、jpgに固定している。

文字列スライス

typeset dest=$HOME/.rednotebook/data/$dirname/${date[1, 4]}/$date

${date[1, 4]}$dateの文字列の先頭4文字のスライスになる。 Zshは1 originであることをお忘れなく。

ディレクトリ作成

if [[ ! -e ${dest:h} ]]
then
  mkdir -pv ${dest:h}
fi

:hでパスエレメントの末尾を除いたもの(要はディレクトリ。dirname相当)が得られる。 これがなければmkdirというのは定番の流れ。

[[ -e ${dest:h} ]] || mkdir -p ${dest:h}

と短く書いても良い。

重複用インクリメント

while [[ -e $dest-$count.$ext ]]
do
  (( count++ ))
done

$countは整数型なので(( count++ ))でインクリメント可能。

whileでファイルの存在確認をし、存在する限りカウントをインクリメントするのも定番の処理。

変換の判定

if (( convert == 1 ))
then
  magick "$infile" -resize 800x800 $dest-$count.$ext
else
  cp "$infile" $dest-$count.$ext
fi

booleanとして使っている$convertで分岐。 真ならばImageMagickを使ってファイルを圧縮する。

convert(1)はmagick(1)を使えと怒られるのだが、引数のとり方に互換性がない(入力ファイルを先に書く必要がある)ので注意が必要。 -resizeで画像サイズの上限を指定。800x800なので長辺側が800pxになる。

圧縮の必要がないのならファイルをコピーするだけで良いのでcpを使う。

画像リンクの表示

目的を達成するためには

  • 画像を配置
  • テキストに画像リンクを配置

が必要。

画像リンクは入力支援もないし、ちょっとクセのあるフォーマットなので書きづらい。 だから、変換するだけでなく、画像リンクを出力したい。 出力すればコピペできるし、xsel -bにパイプしてもいい。