Imagemagickを使った減色処理

こんにちわ。今週のテックブログ、担当のURAです。
最近めっきりと寒くなりましたね。私は毎朝子供を保育園に連れて行くのですが、なぜか保育園は暑いのです。外は肌寒いのに、保育園の中で子供を教室に連れて行ったり荷物をセットしているだけで、ちょっと汗をかくくらい暑いのです。これ保育園あるあるだと思うのですが、世のお父さん方どうでしょうか?

ですよね?保育園、暑いですよね?
はい、全力で共感してもらえて満足したので、今回のテーマ「減色」の話に移ります。

Why 減色?

最近、とあるプロジェクトで「減色」の実装をしていました。
弊社は様々なアイテムにオンデマンドでプリントする会社です。
プリント方法にも色々あり、インクジェットプリントであれば写真のようなフルカラーの画像をプリントできますが、プリントする色数が1色のみであったり、2色のみであったりと限られているプリント方法もあります。
そして、注文時に指定された加工方法の色数と、お客様がアップロードしたデザインデータの色数は必ずしも一致しません。
そこで、デザインデータを決まった色数に減らす処理、つまり「減色」が自動的にできると、デザインデータを補正できるというわけです。

Imagemagickで減色してみる

減色処理にはいくつかアルゴリズムがあります。
今回は、Imagemagickを使って減少する方法をご紹介します。
説明のため、とある画像を8色に減色することを考えていきます。

色のビット深度を指定して減色する

「ビット深度」とは、画像の各チャネルを何ビットで表現しているかという数値です。
例えば、ビット深度が8ビットのRGB画像とは、Red / Green / Blueチャネルのそれぞれが8ビット(256色)で表現されている画像であり、1ピクセルはRed / Green / Blueを組み合わせた情報ですので、
256 * 256 * 256 ≒ 1678万
約1678万パターンの色表現が可能な画像という意味です。

Imagemagickの「depth」コマンドを使うと、ビット深度を指定して減らすことができます。
お題は8色に減色することですので、ビット深度を1ビット(Red:2 * Green:2 * Blue:2 = 8色)に減色します。
convert dance.png -depth 1 dance-depth1.png
単純に画像のビット数を減らすと、肌色が消えてしまいました。

パレットを指定して減色する

Imagemagickの「remap」コマンドを使うと、指定したパレットに使われている色のみになるよう、画像を再構成することができます。
今回は、パレットの色は元画像を見ながら大まかに決定し、以下のようなコマンドでパレットを作成しました。
convert -size 60x60 xc:"rgb(0,0,0)" xc:"rgb(255,255,255)" xc:"rgb(255,0,0)" xc:"rgb(0,255,0)" xc:"rgb(0,255,255)" xc:"rgb(0,0,255)" xc:"rgb(255,0,255)" xc:"rgb(254,220,189)" +append palette8.png
remapコマンドは以下です。
convert dance.png +dither -remap palette8.png dance-remap.png
上記のコマンドに「+dither」とありますが、これは色を組み合わせて中間色を表現する「ディザ」処理を入れないという意味です。
減色する8色以外の中間色が表現されてしまうと見づらいので、今回はディザ処理を省いています。

パレットを自身で作成したため肌色は出ていますが、細かくパレットを調整したわけではないので、例えば洋服の水色は色に差が出ています。
また、パレットの8色にしか減色できないため、様々な画像を自動的に減色する用途には向かなそうです。

画像の色分布から似た色を減色する

Imagemagickの「colors」コマンドを使うと、画像の色分布を元に似た色を減らすことができます。
アルゴリズムについて知りたい方は以下をご覧ください。
https://imagemagick.org/script/quantize.php
convert dance.png +dither -colors 8 dance-colors8.png
元画像の主要な色が再現できていそうです。

k-means法を使って減色する

k-means法とは、k個のクラスタに平均を使ってクラスタリングするアルゴリズムです。
機械学習界隈でも登場するアルゴリズムの一つですので、興味のある方は知らべてみてください。
https://ja.wikipedia.org/wiki/K%E5%B9%B3%E5%9D%87%E6%B3%95

Imagemagickでのコマンドは「kmeans」を使います。
magick dance.png -kmeans 8 dance-kmeans8.png
これも元画像の主要な色が再現できていそうです。
「colors」コマンドと比較すると、顔色が良くなった(頬の色がより再現できている)ような…?

まとめ

Imagemagickを使った減色処理について紹介しました。
紹介しておいてなんなのですが、とあるプロジェクトでは減色処理にImagemagickは採用せず、独自実装する道を選びました。
Imagemagickは誰でも使えますし、独自実装することでより直感的な減色の結果が得られるなら、ユーザビリティーの面で差別化できますしね。

横がダメなら縦でやってみる

こんにちは。イメージマジック三浦です。うだるような暑さの毎日から一変し、過ごしやすい気候に変わってきました。寒暖の差が大きくなってきましたので、体調を崩さないように気を付けていきたいところです。 今回はSQLの集計クエリを書いていた時の話です。

やりたいこと

・同じ期間内でテーブルA,Bをそれぞれ日付と区分(区分はAとBの両方にある)で集計し、その結果セットを以下イメージの形式で取りたい。
 yyyy-mm-dd | 区分1の集計値 | 区分2の集計値 | 区分3の集計値 | …
ただし、テーブルAとBには以下の状態が想定されます。
  1. AとBの両方に含まれる日付がある
  2. Aだけに含まれる日付がある
  3. Bだけに含まれる日付がある

横ではできなかった理由

2の条件と3の条件が両立する可能性があったため、以下のように横方向にデータを結合していく方式では、どうしても集計漏れが出ます。
SELECT * FROM A INNER JOIN B ON A.日付=B.日付 WHERE …
SELECT * FROM A LEFT JOIN B ON A.日付=B.日付 WHERE …
SELECT * FROM A RIGHT JOIN B ON A.日付=B.日付 WHERE …
完全外部結合で対応しようとおもいきや、MySQLやmariaDBは完全外部結合が使えませんので、横方向へのデータ結合では要件を満たすことができません。
※これは使えない
SELECT * FROM A FULL OUTER JOIN B ON A.日付=B.日付 WHERE …

完全外部結合ができないわけではない

このようなクエリにより、完全外部結合を再現することはできます。
SELECT * FROM A LEFT  JOIN B ON A.日付=B.日付 WHERE …
UNION
SELECT * FROM A RIGHT JOIN B ON A.日付=B.日付 WHERE …
または
SELECT * FROM A LEFT JOIN B ON A.日付=B.日付 WHERE …
UNION
SELECT * FROM B LEFT JOIN A ON B.日付=A.日付 WHERE …
しかし、今回はAとBそれぞれに集計対象の条件が異なり、同じ集計条件を1クエリ内で2度書く必要がある分、クエリが複雑になります。スポット集計用ならまだしも、これからもずっと運用していく予定のクエリだったので、複雑になることを避けるために導入を見送りました。 しかし、UNIONを使う方針は有力でした。

UNIONを使って事前集計を行う

UNIONを使って縦方向の結合により集計値を取得します。 UNIONを使うためには、SELECT文のカラム数を同じにする必要があるので、カラム数合わせの0をセットします。後の集計のため、エイリアスもつけています。UNIONにより重複が排除される効果もあります。
SELECT 日付, 区分
, Aの集計値1, Aの集計値2, …
, 0 AS Bの集計値1, 0 AS Bの集計値2, …
FROM A
WHERE …
GROUP BY 日付, 区分
UNION
SELECT 日付, 区分
, 0 AS Aの集計値1, 0 AS Aの集計値2, …
, Bの集計値1, Bの集計値2, …
FROM B
WHERE …
GROUP BY 日付, 区分

本集計

予備集計のクエリをインラインビューとし、インラインビュー内の集計値をさらに集計します。ダミーのカラム値を0としたことで、SUMを実行した時に影響しないようになっています。
SELECT
日付
, SUM(CASE WHEN X.区分 = 1 THEN Xの集計値 ELSE 0 END) AS summary1
, SUM(CASE WHEN X.区分 = 2 THEN Xの集計値 ELSE 0 END) AS summary2
, SUM(CASE WHEN X.区分 = 3 THEN Xの集計値 ELSE 0 END) AS summary3
, SUM(CASE WHEN X.区分 = 4 THEN Xの集計値 ELSE 0 END) AS summary4
, …
FROM (
SELECT 日付, 区分
, Aの集計値1, Aの集計値2, …
, 0 AS Bの集計値1, 0 AS Bの集計値2, …
FROM A
WHERE …
GROUP BY 日付, 区分
UNION
SELECT 日付, 区分
, 0 AS Aの集計値1, 0 AS Aの集計値2, …
, Bの集計値1, Bの集計値2, …
FROM B
WHERE …
GROUP BY 日付, 区分
) X
GROUP BY X.日付
SUMの結果について、もれなくエイリアスを定義しておくことが最後のポイントです。これにより、プログラムロジック内で集計値を扱いやすくなります。

小話

「インラインビュー」という言葉を初めて聞いたのは、新卒2年目の時に入った案件で見た設計ドキュメントでした。当時は何も分からないながら、何とかしてクエリを書きましたが、そのクエリは廃棄されてしまったそうです。 おまけに、そのことを聞いたのは案件を外れて半年後でした。 派遣エンジニアだった頃の、1つの思い出です。

LAN内のPCにはIPではなくホスト名でアクセスしようと思った話

イメージ・マジックの安藤です。
最近誕生日を迎えました。20代でいられるのもあとわずかです。

今回は内製のデスクトップアプリケーション(以下アプリ)の運用にあたってネットワーク周りでの気づきの話です。

前置き

弊社では、生産性の向上や属人性の排除といった観点で生産工程の一部を自動化しています。
諸々の言及は避けますが、そのシステムの一環として加工を早く済ませるためにアプリを介してPC間とPC加工機間で通信を行う仕組みがあります。 イメージ 画像は何かを表しているようでほとんど何も表していないイメージ図です。

アプリの目的は加工機にデータを送ることです。アプリは内部で簡易的なサーバをたてているため、接続先を指定すればアプリ間でエンドポイントを通じて通信を行うことができます。PC間で何やかんやをして素早くデータを加工機に送るという仕組みがアプリにあります。

何が起こったか

前述の前置きを踏まえて本題なのですが、ある日現場の方からデータの送信が遅いという話があり実際に現場に赴き調査することとしました。

データの送信が遅くなっている原因はアプリ間の通信ができていないことにありました。そこで、通信ができない理由を探ったところ以下のことが分かりました。
  • 各PCのプライベートIPの割り当てがDHCPで管理されている
  • アプリに設定されている接続先がIPで指定されている
  • アプリに設定されているIPが実際に接続したいIPと異なっていた
以上のことから、これは接続先を正しく指定すれば一応の解決ができるわけですが、PCのIPが変わるたびにこのようなことが起こってはたまらないため対策をすることにしました。

Windowsにホスト名解決をさせる

現場で働いている方々は、PCには詳しくないことが多いのでトラブルの際に解決しやすい方向に倒すほうがいいです。
今回はアプリに設定する接続先をホスト名に変えることで対処することにしました。WindowsではPC名がホスト名にあたります。コマンドプロンプトかPowerShellでhostnameコマンドでも確認できます。
名前解決でもIPが変わった際にキャッシュによって正しい解決ができない可能性が残りますが、そこはDNSキャッシュクリアのコマンドを直接指示するかそういった機能をアプリに組み込むことで今後対処できるかなと思います。

おまけ:PC間のpingがWindowsのFWで防がれている件

Windows10のプライベートネットワークはデフォルトでICMPがブロックされるようになっているため、ファイアウォールでこれを許可する必要がありました。「ファイルとプリンターの共有 (エコー要求 – ICMPv4 受信)」「ファイルとプリンターの共有 (エコー要求 – ICMPv6 受信)」が該当する項目でした。
原因の調査中に引っかかりファイアウォールを無効化すると通ったため気づきました。

おわりに

Webアプリでは問題の再現と解決はしやすいことが多いですが、デスクトップのアプリだとその要因が個々のPC環境に依存して解決しにくいことが多いのでこれを解決する仕組みを更に考えていきたいと思いました。

Illustratorスクリプトあれこれ その2

はじめに

こんにちは、イメージ・マジックのもあいです。

今回は直近で作業をしていたIllustratorスクリプトの備忘録になります。

アートボードサイズと画像のサイズを一致させたかったが・・・ずれた

Illustratorのアートボードとそこに配置する画像のサイズを一致させる方法を、最初はサーバ側でImageMagickのIdentifyコマンドでピクセル数を求めてから加工に必要なDPIを使ってmm単位で算出していました。1259pxで200DPIですと159.893mmとなるので、切り上げて160mmとなります。この値をIllustratorでdocumtns.addの引数として渡していたのですが、アートボードのサイズと画像サイズが微妙にずれることが発覚しました。

原因としては、小数点を切り上げてしまったことが原因でした。

そもそも

Illustratorスクリプトでアートボードのサイズと画像のサイズを一致させるための操作方法は、「オブジェクト」→「アートボード」→「オブジェクト全体に会わせる」を選択する必要があるのですが、この操作をアクションで記録できなかったので、アートボードのサイズを指定するときに画像の大きさ(mm)を指定していました。
ですが、それがうまくいかない場合があるのが今回のパターンでした。

executeCommandMenu

Illustratorスクリプトにはドキュメントに記載が無い(少なくとも見つけられませんでした)executeCommandMenuというものがあり、この機能を使うことによりアクションで記録できない機能も実行することが可能になります。
今回の場合の具体的なコードですが

app.executeMenuCommand("Fit Artboard to artwork bounds");

となりました。この呼び出しだけでサーバ側の算出処理が不要になりました。

最後に

小数点以下の値を無視すると痛い目に遭いました。

Vue.jsでそれっぽい何かを作る

株式会社イメージ・マジックの技術ブログ、先週の担当のsoenoです。 何か面白いものを…と考えること数日。いつのまにか期限は過ぎていました。 そこで今回は終わらせることを大切に美しいコード的なものからは目をそらしつつvue.jsを触ろうと思います。


作るもの

左のマス目を見ながら右に同じ図形を写し取るというゲーム(の手前)のようなものを作成しました。 落下やパーツの消去がないので、テトリスや、インベーダゲームよりとりかかりやすいかと思います。 ここから発展させて爆弾を埋め込んで危険度を表示させるとマインスイーパー、数字を隠せば数独、文字を入れてもらうとクロスワードなりになるのではないでしょうか。

使うもの

  • CDN: Vue(ダウンロードすらしたくないとき)
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  • HTML
  • css

進めかた

見本の絵を表示させる。

  1. htmlでvueのelに割り当てる要素を作成します。
  2. スクリプト内に多次元配列を作成。trueを選択状態として実際お値で埋める。
  3. html内でv-forを使用して配列分の要素を作成。
  4. cssで1枡分のスタイルを設定。(まず幅と高さ。)
  5. 多次元配列から1枡分の選択状態を表示させる。

操作側を作る

  1. 見本同様に多次元配列を作成。(お好みでスクリプトから生成)
  2. 見本同様にv-forを使用して配列分の要素を作成。
  3. 見本用のcssをこちらにも充てる。
  4. 見本同様に多次元配列から1枡分の選択状態を表示させる。
  5. クリックできるようにする。

イメージの確認

  1. マス目の選択状態が変わったら見本と比較して一致しているかを確認する。

結果

以下のコードに~.html的な名前を付けてデスクトップに置くと動くはずです。


よろしければ落として遊んでみてください。
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>vue</title>
    <style type="text/css">
      .wrapStyle{
          margin: 0 50px;
          display: inline-block;
      }
      .celWrap{
          display: inline-block;
          box-sizing: content-box;
          margin:0;
          display: inline-block;
      }
      .cel{
          border:solid thin #eeeeee; 
          box-sizing: border-box;
          display: inline-block;
          margin:0;
          padding:0;
      }
      .cel.is-active{
          background-color:#111111;
      }
    </style>
</head>
<body>
<div id="app">
  <div class="wrapStyle" v-bind:style="boxStyle">
    <p>見本</p>
    <div v-for="(items, yoko) in resultList" v-bind:style="celWrap">
      <div v-for="(item, tate) in items" 
            v-bind:style="celStyle" class="cel" 
            v-bind:class="{'is-active': resultList[yoko][tate]}">
      </div> 
    </div>
  </div>
  <div class="wrapStyle" v-bind:style="boxStyle">
    <p>操作エリア</p>
    <div v-for="(items, yoko) in userList" v-bind:style="celWrap">
      <div v-for="(item, tate) in items"
           v-on:click="changeCel(userList[yoko][tate])" :data-yoko="yoko" :data-tate="tate"
           v-bind:style="celStyle" class="cel"
           v-bind:class="{'is-active': userList[yoko][tate]}">
      </div>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script language="javascript">
new Vue({
  el: '#app',
  data: {
    cellSize:30,
    resultList: [
      [false,false,true,false,false],
      [false,false,true,false,false],
      [false,false,true,false,false],
      [false,false,true,false,false],
      [false,false,true,false,false],
    ],
    userList: [
      [false,false,false,false,false],
      [false,false,false,false,false],
      [false,false,false,false,false],
      [false,false,false,false,false],
      [false,false,false,false,false],
    ],
    
    boxStyle:{
      width:0,
    },
    celWrap:{
      height:0,
    },
    celStyle:{
      width:0,
      height:0,
    },
  },
  created() {
    this.celStyle.width = this.cellSize+ 'px';
    this.celStyle.height = this.cellSize+ 'px';
    this.celWrap.height = this.cellSize+ 'px';
    this.boxStyle.width = this.cellSize * this.resultList[0].length+ 'px';
  },
  
  methods: {
    changeCel() {
      const yoko = parseInt(event.currentTarget.dataset.yoko);
      const tate = parseInt(event.currentTarget.dataset.tate);
      Vue.set(this.userList[yoko], tate, !this.userList[yoko][tate]);

      //表示が変更されたら揃ったか確認する。
      this.check();
    },
    check() {
      for(let i=0,len = this.resultList.length; i<len; i++){
        for(let ii=0,len2 = this.resultList[i].length; ii<len2; ii++){
          if(this.userList[i][ii] !== this.resultList[i][ii]){
            //不一致があったら処理を抜ける
            return;
          }
        }
      }
      setTimeout(function () {
        alert("揃いました。");
      }, 100);
    },
  },
})
</script>
</body>
</html>
 

改善、変更候補

  1. cellSizeの値を変更すると画面上の1枡分のサイズが変わります。簡素な処理がいい場合はここを確定させcssに持たせると処理は短くなります。
  2. userListにユーザーの選択を持たせていますが、ここは結果用の配列から生成したほうがミスによる数の不一致などがなくなりいいのではないかと思います。

LinuxサーバをVPN(L2TP)クライアントにする

こんにちは、岡野です。

先日、さくらインターネットのVPSと社内ネットワーク(VPN構築済み)をL2TPで接続しました。 特に問題なく接続できたのですが手順を共有します。

前提条件

VPN: L2TP

VPS: Ubuntu 20.04、ufwで通信制限中(incoming & outgoing)

接続手順

  1. VPNサーバの対応アルゴリズムを調べる

NetworkManager-l2tp Wikiにあるシェルスクリプトを保存後、以下を実行。VPNサーバの対応アルゴリズムが表示される。

$ chmod +x ike-scan.sh
$ sudo ./ike-scan.sh <VPNサーバのIP> | grep SA=
抜粋
SA=(Enc=AES Hash=SHA1 Auth=PSK Group=2:modp1024 KeyLength=256 LifeType=Seconds LifeDuration(4)=0x00007080)
  1. VPSのネットワーク設定を確認する

さくらインターネットで設定済みのネットワーク設定を確認。あとで使用する。

$ cat /etc/netplan/01-netcfg.yaml
network:
version: 2
renderer: networkd
ethernets:
  ens3:
    addresses: [***.***.***.***/23]
    gateway4: ***.***.***.***
    nameservers:
      addresses: [***.***.***.***, ***.***.***.***]
  1. L2TPソフトウェアをインストールする

$ sudo apt install network-manager-l2tp
  1. NetworkManager管理に切り替える

$ sudo vi /etc/netplan/01-netcfg.yaml
network:
version: 2
renderer: NetworkManager
# 残りは削除
$ sudo reboot
  1. NetworkManagerの接続情報を設定する

以下、VPSコンソールから実施。

$ sudo nmtui
# 「2. VPSのネットワーク設定を確認する」の内容を設定する
$ sudo reboot
  1. VPNの接続情報を設定する

VPN接続情報を以下の通り設定する。IKE対応アルゴリズムには「1. VPNの対応アルゴリズムを調べる」で調べた中で、より強力なものを使用する(例:aes256-sha1-modp1024)。

$ sudo nmcli connection add \
type vpn \
con-name <接続名、適当に。例:vpn> \
autoconnect no \
ifname -- \
ipv4.method auto \
vpn-type l2tp \
vpn.secrets password=<パスワード> \
vpn.data \
"password-flags = 0, require-mppe = yes, user = <ユーザ名>, refuse-chap = yes, refuse-mschap = yes, gateway = <VPNサーバのIP>, refuse-pap = yes, ipsec-enabled = yes, ipsec-psk = <共有鍵>, ipsec-ike=<IKE対応アルゴリズム>, ipsec-esp=aes128-sha1"
  1. VPN接続する

ログを表示しながらVPN接続を試みる。

$ journalctl -f &
$ sudo nmcli conn up vpn

以下の様なエラーが出力されるため

Jul 19 09:04:16 *** kernel: [148247.538797] [UFW BLOCK] IN= OUT=ens3 SRC=***.***.***.*** DST=***.***.***.*** LEN=268 TOS=0x00 PREC=0x00 TTL=64 ID=13477 DF PROTO=UDP SPT=500 DPT=500 LEN=248

ufwで解除設定しながらsudo nmcli conn up vpnを繰り返す。具体的には以下の様なufw設定になる。

$ sudo ufw status
***.***.***.*** 500/udp     ALLOW OUT   Anywhere                   # vpn (ipsec)
***.***.***.*** 1701/udp   ALLOW OUT   Anywhere                   # vpn (l2tp)
***.***.***.***/esp         ALLOW OUT   Anywhere                   # vpn (esp)

必要に応じてルーティング設定も行う。

  1. VPN切断する

VPN切断は以下。

$ sudo nmcli conn down vpn

[仮想環境] VirtualBox, Vagrantの関係について

こんにちは。初投稿のふくまです。
直近の投稿者のお2人と同様、入社して3か月目です。
私は後楽園駅に通勤しているのですが、春日駅もすぐ近くにあるため乗り換え可能な路線が多く、便利な場所だなーと日々感じております。

VirtualBox, Vagrantについてまとめようと思ったきっかけ

弊社の開発本部では、開発のためにPC上に仮想環境を構築します。その際に、VirtualBoxとVagrantを使用します。 powershell / コマンドプロンプトでvagrantコマンドを叩いて仮想マシンを作成するのですが、その際に以下のような疑問を持ちました。
  • VirtualBoxってソフトをインストールはしたけど、環境構築の過程で一度も使ってないのでは?
  • VirtualBox, Vagrantの関係って何なの?
  • 仮想マシンっていうけど、どこにあるの?
環境構築中にこのような疑問を持っていましたが、きちんと内容を理解しながら開発をすることが出来ていませんでした。 このような疑問に答えられるような形で、まとめてみます。

[疑問1] VirtualBoxは環境構築で一度も使わない?

A. 使用しています。ただし、Vagrantを通して使用しています。 PC上にゲストOSを立ち上げることの出来る仮想化ですが、仮想化を行う機能自体はVirtualBoxのものです。

[疑問2] VirtualBox, Vagrantの関係って何?

A. Vagrantには設定ファイルとvagrantコマンドを使用して、VirtualBoxを通じ仮想環境を構築してくれる役割があります。 言い換えると、VagrantはVirtualBoxを利用できるツールということです。

[疑問3] 仮想マシンってどこにあるの?

A. PC内部にあります。ホストOSの仮想化ソフト(VirtualBox等)の上で、ゲストOS(Linux等)として動作することになります。(下図イメージ) 弊社の場合、WindowsOS上で仮想化ソフトVirtualBoxとそれを操作するVagrantをインストールします。そして、Vagrantの設定ファイルに仮想マシンの設定やゲストOSに何を指定するかなどを記載します。あとはvagrantコマンドを打てば環境を作ってくれるので、作成後はsshで仮想マシンにログインして開発に関連する作業を実施します。

まとめ

  • VirtualBoxは仮想マシンを作ってくれる。
  • VagrantはVirtualBoxを便利に利用できる。
  • VirtualBoxとVagrantを合わせて使用することで、仮想環境の構築がより簡単になる。

WSLについて

こんにちは、イメージマジックのSuzukiです。 入社して早3カ月が過ぎました。
コロナの影響で運動不足になりがちな日々を過ごしております。
業務ではWSLでも検証等を行っています。
その為、今回はWSLについて書いていこうと思います。
WSLとは
WSLはWindowsOS上でLinuxの実行環境を実現するサブシステムです。
最近、WSL2というものがリリースされたようです。
以下の記事を参考にしてWSLをインストールしました。
参考:https://qiita.com/Brutus/items/f26af71d3cc6f50d1640
  • WSLはwindowsと親和性が高い
    起動が早い、Windowsとのファイルアクセスがしやすい、などが挙げられます。
  • WSL2はWSL1と共存ができる
    WSL2はWSL1を置き換えるものではありません。
    ディストリビューションごとにWSL1またはWSL 2のどちらで動作させるかを変更できます。任意のタイミングでWSL1とWSL2を切り替えることも可能です。
    具体的には、PowerShellを管理者権限で起動し、以下コマンドを実行して任意のディストリビューションをWSL2へ変換します。
wsl --set-version ディストリビューション名 2
 
  • 専用の仮想マシン内で起動
    WSL1は仮想マシンではなく、LinuxのシステムコールをWindowsのAPIに変換し、バイナリを実行する形式でした。
    しかしWSL2は仮想マシンでバイナリを実行するものに改められています。
    またWSL2ではDockerなどカーネル依存するソフトの動作が、最大のメリットです。
 
最後に
入社以来変わったことですが
以前よりコマンドラインツールに対して耐性ができたように思います。
しかし英語はまだまだ苦手意識があるため、修練の必要性を非常に感じました。
特にIntelliJは英語メインで辞書を片側に訳しながら作業を進めています。
やるべき事は多々ありますが、これからも頑張っていきます!
参考:https://qiita.com/poramal/items/3562472d52fe60f61c56

MySQLで文字列結合しようとしたら何も表示されなかった話

こんにちは~。たにすぎです。
入社してそろそろ3か月経ちますが、入ったときには既にみんなマスク着用だったので、何かの拍子に外した姿を見ると「誰だ……?」と2秒くらい固まります。
いやあ、早く素顔で笑える日が来てほしいですね。

さて、タイトルで既に原因お分かりの方もいるかと思いますが、
NULLな文字列をCONCATでつなげたら悲しい思いをしたのでその話をします。

今回の状況

「苗字(半角スペース)名前」の形で氏名を表示する時に何も考えずにCONCATでつなげた結果を表示しようとしたら痛い目にあいました。
こういうデータがあって、 サンプルのユーザーテーブル 以下のようなクエリを書きました。
SELECT CONCAT(name_last, ' ', name_first) AS name FROM scores s
 

結果

素直にconcatだけ使った結果
あれ……? 1番下が何も表示されないな。

なんで消えちゃうの

思ってたのと違う結果になったのでドキュメント読みます。 https://dev.mysql.com/doc/refman/5.6/ja/string-functions.html#function_concat
引数のいずれかかが NULL である場合、CONCAT() は NULL を返します。
はい。
つなげる文字列の中で1つでもNULLがあると、結果がNULLになるんですね。知らなかった……

書き換え案

氏名で片方がない場合はあまりなさそうですが、住所で建物名がないとかは割とありそうなのでなんとかしたいです。 

IFNULL

IFNULLをつかってNULLの場合は空に置き換えてから繋ぐように書き換えてみます。
SELECT
CONCAT(name_last, ' ', name_first) AS 'IFNULL使ってないname',
CONCAT( IFNULL(name_last,''), ' ', IFNULL(name_first, '') ) AS 'IFNULL使ったname'
FROM users u
できたできた。

COALESCE

COALESCEでも結果は同じだけど、なんだかもったいないような……
SELECT
CONCAT(name_last, ' ', name_first) AS 'そのままCONCAT',
CONCAT( COALESCE(name_last,''), ' ', COALESCE(name_first, '') ) AS 'COALESCE使ったname'
FROM users u
coalesceで繋いだ例

GROUP_CONCAT

GROUP_CONCATは複数行の結果をカンマ区切りで返してくれる関数です。
こちらはどうなるんでしょう?
SELECT GROUP_CONCAT(name_last), GROUP_CONCAT(name_first)
FROM users
group_concatにnull
あ、GROUP_CONCATの方はNULLは気にしないでくれるみたいですね。
https://dev.mysql.com/doc/refman/5.6/ja/group-by-functions.html#function_group-concat

おわりに

めんどくさがるのよくないですね!
何らかの理由でIF_NULLが使えない場合はCASEとIS_NULLで対応したらいいのかな……

おまけ: PostgreSQLの場合は?

これは完全に言い訳なんですが、ここ数年はPostgreSQLをメインに使っていて、そっちはNULL含む項目をCONCATで繋げても無視した結果を返してくれていたんです……
でもそういえばCONCAT使わずに「||」(パイプ)で繋げた場合はNULLになってましたね……

SELECT CONCAT(name_last, ' ', name_first) AS "CONCATの結果", name_last || ' ' || name_first AS "パイプの結果" FROM users

vim+uniteからneovim+deniteへの移行

社内の傾向としてはエディタはIntelliJやvscodeが主流で、私は元々vi系エディタのユーザなので最近はvscodeをvimのキーバインドで使ってます。

IDEは多機能なのはいいんですが、正直使いこなせてない感がすごいし、ぶっちゃけほぼeclipseしか使ったことない。
vscodeはvimキーバインドでもかなり使えるんですが、ちょいちょい気になる瞬間があり、なによりエディタは慣れたやつのほうがいい。
というか正直vimも使い切れてない。なので、まあそれならと比較的指が慣れてるvimの実装の一つでありずっと気になっていたneovimをこのブログ書く機会に一念発起してつかってみることにしました。

ubuntu18.04でapt使って入れようとすると、何もしないとneovimのバージョンが0.2系なのですが、リポジトリを追加することでneovim0.3系がinstallできます。その前にpython関係のやつもわすれずにいれときます。
sudo apt-get install python-dev python-pip python4-dev python3-pip 
sudo apt-get install software-properties-common 
sudo add-apt-repository ppa:neovim-ppa/stable 
sudo apt-get update 
sudo apt-get install neovim
今まで利用していた.vimrcをそのまま.config/nvim/init.vimにコピーしてまずは起動してみると、起動自体はできたのですが、プラグインを動かそうとするとTERMの設定がおかしというエラーが。
:checkhealthしてみろ、と親切なメッセージがでていたのでやってみると、TERMの設定がtmuxとzshの間で設定が異なっているのが問題だったらしく以下の設定を追加。

.tmux.conf
set-option -g default-terminal "screen-256color"
 
vimのpluginとしておなじみ便利なuniteをつかってたんですが、この機会に後継?っぽいdeniteを使ってみることにします。(これがneovim0.2系だと動かない
 
.vimrc
"unite.vim関連


" バッファ一覧
nnoremap <silent> ,ub :<C-u>Unite buffer<CR>
" ファイル一覧
nnoremap <silent> ,uf :<C-u>UniteWithBufferDir -buffer-name=files file<CR>
" レジスタ一覧
nnoremap <silent> ,ur :<C-u>Unite -buffer-name=register register<CR>
" 最近使用したファイル一覧
nnoremap <silent> ,um :<C-u>Unite file_mru<CR>
" 常用セット
nnoremap <silent> ,uu :<C-u>Unite buffer file_mru<CR>
" 全部乗せ
nnoremap <silent> ,ua :<C-u>UniteWithBufferDir -buffer-name=files buffer file_mru bookmark file<CR>


" ウィンドウを分割して開く
au FileType unite nnoremap <silent> <buffer> <expr> <C-j> unite#do_action('split')
au FileType unite inoremap <silent> <buffer> <expr> <C-j> unite#do_action('split')
" ウィンドウを縦に分割して開く
au FileType unite nnoremap <silent> <buffer> <expr> <C-l> unite#do_action('vsplit')
au FileType unite inoremap <silent> <buffer> <expr> <C-l> unite#do_action('vsplit')
" ESCキーを2回押すと終了する
au FileType unite nnoremap <silent> <buffer> <ESC><ESC> :q<CR>
au FileType unite inoremap <silent> <buffer> <ESC><ESC> <ESC>:q<CR>
uniteの時につかってた設定。これを。。。
 
init.vim
" denite.vim関連


" バッファ一覧
nnoremap <silent> ,ub :<C-u>Denite buffer<CR>
" ファイル一覧
nnoremap <silent> ,uf :<C-u>DeniteWithBufferDir -buffer-name=files file<CR>
" レジスタ一覧
nnoremap <silent> ,ur :<C-u>Denite -buffer-name=register register<CR>
" 最近使用したファイル一覧
nnoremap <silent> ,um :<C-u>Denite file_mru<CR>
" 常用セット
nnoremap <silent> ,uu :<C-u>Denite buffer file_mru<CR>
" 全部乗せ
nnoremap <silent> ,ua :<C-u>DeniteBufferDir -buffer-name=files buffer file_mru bookmark file<CR>


" ウィンドウを分割して開く
au FileType denite nnoremap <silent> <buffer> <expr> <C-j> denite#do_action('split')
au FileType denite inoremap <silent> <buffer> <expr> <C-j> denite#do_action('split')
" ウィンドウを縦に分割して開く
au FileType denite nnoremap <silent> <buffer> <expr> <C-l> denite#do_action('vsplit')
au FileType denite inoremap <silent> <buffer> <expr> <C-l> denite#do_action('vsplit')
" ESCキーを2回押すと終了する
au FileType denite nnoremap <silent> <buffer> <ESC><ESC> :q<CR>
au FileType denite inoremap <silent> <buffer> <ESC><ESC> <ESC>:q<CR>
 
こうだなきっと(uniteをdeniteに変えただけ

なんとこんな雑な変換のやり方でも一覧系はだいたい全部うごいたんですが、ファイル一覧だけ動かない。DeniteWithBufferDirがないっぽいので、:help DeniteしてみてみるとDeniteBufferDirという名前でほしいのが取れそう。
 
なので該当箇所をこれに変更します。あとなぜかbookmarkがなかったので全部乗せからbookmarkも外しときます(uniteだと定義がなくてもエラーになってなかった
 
init.vim
" ファイル一覧
nnoremap <silent> ,uf :<C-u>DeniteBufferDir -buffer-name=files file<CR>
 :
" 全部乗せ
nnoremap <silent> ,ua :<C-u>DeniteBufferDir -buffer-name=files buffer file_mru file<CR>
 
これだけだと動かなくて、checkhealthした際に出ていたアドバイスに従って、:UpdateRemotePluginを実行。これでDenite自体は起動できました。

ただ、なんかinsertモードから戻るときのescapeの反応が異様に遅い。。
しらべてるとtmuxのescape-timeのせいだという記事をみかけたのでとりあえずそれのせいにしとこう。

.tmux.conf
set -s escape-time 10
 
よしこれで終わりかと思ったら、deniteのbufferから該当箇所にとべない?
denite.txtにあるexampleからまるっと以下の設定をコピー

init.vim
  autocmd FileType denite call s:denite_my_settings()
  function! s:denite_my_settings() abort
    nnoremap <silent><buffer><expr> <CR>
    \ denite#do_map('do_action')
    nnoremap <silent><buffer><expr> d
    \ denite#do_map('do_action', 'delete')
    nnoremap <silent><buffer><expr> p
    \ denite#do_map('do_action', 'preview')
    nnoremap <silent><buffer><expr> q
    \ denite#do_map('quit')
    nnoremap <silent><buffer><expr> i
    \ denite#do_map('open_filter_buffer')
    nnoremap <silent><buffer><expr> <Space>
    \ denite#do_map('toggle_select').'j'
  endfunction
 
。。。ってここで気づいたんですが分割して開くやつも動いてなかった。
 
 
au FileType denite nnoremap <silent> <buffer> <expr> <C-j> denite#do_action('split')
これをこんなかんじでdenite_my_settingsの中に張り付けなおしていきます。
 
init.vim
autocmd FileType denite call s:denite_my_settings()
function! s:denite_my_settings() abort
             :
  nnoremap <silent><buffer><expr><C-j> denite#do_map('do_action','split')
             :
endfunction
これで検索結果のバッファからEnterキーでとんだりsplitして画面を開いたりできるようになりました。


ついでに最近でも開発が続いてるripgrepをdeniteから使うようにしてみたいと思います。
つかってるubuntuのバージョンだとaptにはなかったし、snapは利用できない環境だし、しょうがないのでdebian向けのパッケージでいれます。
curl -LO https://github.com/BurntSushi/ripgrep/releases/download/11.0.2/ripgrep_11.0.2_amd64.deb
sudo dpkg -i ripgrep_11.0.2_amd64.deb

以下の設定をinit.vimに追加。
 
init.vim
nnoremap <silent> ,ug :<C-u>Denite grep -buffer-name=grep-buffer-denite<CR>
nnoremap <silent> ,uw :<C-u>DeniteCursorWord grep -buffer-name=grep-buffer-denite<CR>
nnoremap <silent> ,ue :<C-u>Denite -resume -buffer-name=grep-buffer-denite<CR>
nnoremap <silent> ,un :<C-u>Denite -resume -buffer-name=grep-buffer-denite -select=+1 -immediately<CR>
nnoremap <silent> ,up :<C-u>Denite -resume -buffer-name=grep-buffer-denite -select=-1 -immediately<CR>


call denite#custom#var('grep', 'command', ['rg'])
call denite#custom#var('grep', 'recursive_opts', [])
call denite#custom#var('grep', 'pattern_opt', [])
call denite#custom#var('grep', 'default_opts', ['-S', '--vimgrep','--no-heading'])
 

これでripgrep結果のdenite bufferからファイルにとんだり、キャッシュした検索結果からfilterして探したりとできるようになりました。

これだけだとなんなので、次に補完機能などを強力にしてくれるvim-lspを導入します。
vim-lsp-settingsで簡略化できるようですが、まずはそのありがたみと、素の設定のめんどくささを知るためにそのまま設定してみます。とりあえずtypescriptとphpでチャレンジ。
vundleユーザなので以下をpluginとして追加。
asynccompleteでtypescriptを補完するためにtscompletejob、asynccomplete-tscompletejob(長い)を入れます
 
init.vim
Plugin 'prabirshrestha/async.vim'
Plugin 'prabirshrestha/vim-lsp'
Plugin 'prabirshrestha/asynccomplete.vim'
Plugin 'prabirshrestha/asynccomplete-lsp.vim'
Plugin 'runoshun/tscompletejob'
Plugin 'prabirshrestha/asynccomplete-tscompletejob.vim'
このあたり(https://github.com/prabirshrestha/vim-lsp/wiki/Servers-TypeScript)を参考に入れていきます。まずはlanguageサーバをインストール。*1
npm install -g typescript-language-server intelephense
plugin設定に追加。intelephenseはプラグイン設定いらない模様。
 
init.vim
Plugin 'ryanolsonx/vim-lsp-typescript'
各language server設定。このあたりは記載されてるまま。*2
init.vim
" php 
if executable('intelephense')
  augroup LspPHPIntelephense
    au!
    au User lsp_setup call lsp#register_server({
        \ 'name': 'intelephense',
        \ 'cmd': {server_info->[&shell, &shellcmdflag, 'intelephense --stdio']},
        \ 'whitelist': ['php'],
        \ 'initialization_options': {'storagePath': '/tmp/intelephense'},
        \ 'workspace_config': {
        \   'intelephense': {
        \     'files': {
        \       'maxSize': 1000000,
        \       'associations': ['*.php', '*.phtml'],
        \       'exclude': [],
        \     },
        \     'completion': {
        \       'insertUseDeclaration': v:true,
        \       'fullyQualifyGlobalConstantsAndFunctions': v:false,
        \       'triggerParameterHints': v:true,
        \       'maxItems': 100,
        \     },
        \     'format': {
        \       'enable': v:true
        \     },
        \   },
        \ }
        \})
  augroup END
endif



" typescript
if executable('typescript-language-server')
    au User lsp_setup call lsp#register_server({
        \ 'name': 'typescript-language-server',
        \ 'cmd': {server_info->[&shell, &shellcmdflag, 'typescript-language-server --stdio']},
        \ 'root_uri':{server_info->lsp#utils#path_to_uri(lsp#utils#find_nearest_parent_file_directory(lsp#utils#get_buffer_path(), 'tsconfig.json'))},
        \ 'whitelist': ['typescript', 'typescript.tsx'],
        \ })
endif
 
vim-lspの設定。これもとりあえずはそのまま。g dでカーソル箇所のワードの定義箇所を探してくれます便利。F2でシンボル一括リネーム。
 
init.vim
function! s:on_lsp_buffer_enabled() abort
    setlocal omnifunc=lsp#complete
    setlocal signcolumn=yes
    nmap <buffer> gd <plug>(lsp-definition)
    nmap <buffer> <f2> <plug>(lsp-rename)
    " refer to doc to add more commands
endfunction


augroup lsp_install
    au!
    " call s:on_lsp_buffer_enabled only for languages that has the server registered.
    autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
 
エラー表示と補完候補のリフレッシュタイミングの設定はいろいろためしてみましたが、結局デフォルトの挙動が好みだったのと、popup更新もう少しゆっくりでもいいかなってことでg:asyncomplete_popup_delayの値を少し増やすだけにしました。
 
で、結局lsp-serverと補完の設定とか、言語を追加するたびにペタペタやらんといかんわけですね。手順は決まってるといえ、確かに長いし、なにより面倒くさい。そこでvim-lsp-settingsの出番。
 
init.vim
Plugin 'mattn/vim-lsp-settings'
を:VundleInstallして、
 
init.vim
if empty(globpath(&rtp, 'autoload/lsp.vim'))
  finish
endif
 
でおわり。*1,*2の設定はいらなくなります。 あとは使いたい言語編集時に:LspInstallServerするようにすれば裏で設定してくれます。ありがたや。
 
 
思ったより長くなってしまったので、ここまでにしたいと思います。最後までご覧いただきありがとうございました。