Go 言語で QR コードを生成するコマンドを作ってみた

# Go 言語で QR コードを生成するコマンドを作ってみた

## はじめに

* Go 言語で QR コードを生成するコマンドを作ってみました

![hello world](https://raw.githubusercontent.com/wiki/yoshi389111/go-mkqrcode/images/hello_world.gif)

* 作業メモ的に残しておきます
* 無駄に長いです

https://github.com/yoshi389111/go-mkqrcode

## リポジトリの作成

何はともあれ、Github でリポジトリを作成します。とりあえず `go-mkqrcode` という名前で作ってみました。

```
git clone git@github.com:yoshi389111/go-mkqrcode.git
cd go-mkqrcode
```

デフォルトの Initial commit に絵文字 :tada: をつけたいので `rebase -i` します。

```
git rebase -i --root
# `Inital commit` -> `:tada: Inital commit`
git push -f
```

## QR コードを作るライブラリ

Go 言語で QR コードを作るライブラリとして有名な `github.com/boombuler/barcode` を使用することにしました。


* [package barcode](https://pkg.go.dev/github.com/boombuler/barcode) - MIT ライセンス

```
go get github.com/boombuler/barcode
```

サンプルコードをもとに、とりあえず QR コードを出力してみます。

```go
package main

import (
	"image/png"
	"log"
	"os"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

func main() {
	qrCode, err := qr.Encode("Hello World", qr.M, qr.Auto)
	if err != nil {
		log.Fatal(err)
	}

	qrCode, err = barcode.Scale(qrCode, 200, 200)
	if err != nil {
		log.Fatal(err)
	}

	file, err := os.Create("qrcode.png")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	err = png.Encode(file, qrCode)
	if err != nil {
		log.Fatal(err)
	}
}
```

ほとんどサンプルのままで、エラー処理だけ追加しました。

```
go run mkqrcode.go
```

とりあえず動かしてみて、QR コード(pngファイル)が正しく生成されていることを確認しました。

## オプション引数を処理する flag

Go 言語の標準ライブラリで、コマンドライン（オプション）を処理するものは `flag` というもののようです。

* [Package flag](https://golang.org/pkg/flag/)

これを使って、QR コードを作るデータや、出力先ファイル名を指定可能にしてみます。

```go
package main

import (
	"flag"
	"fmt"
	"image/png"
	"log"
	"os"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

func main() {
	outFile := flag.String("o", "", "output file name")
	flag.Parse()
	args := flag.Args()
	if len(args) != 1 {
		flag.Usage()
		os.Exit(1)
	}
	if *outFile == "" {
		fmt.Fprintln(os.Stderr, "output file is required.")
		os.Exit(1)
	}

	message := args[0]

	qrCode, err := qr.Encode(message, qr.M, qr.Auto)
	if err != nil {
		log.Fatal(err)
	}

	qrCode, err = barcode.Scale(qrCode, 200, 200)
	if err != nil {
		log.Fatal(err)
	}

	file, err := os.Create(*outFile)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	err = png.Encode(file, qrCode)
	if err != nil {
		log.Fatal(err)
	}
}
```

出力ファイル名は `-o` オプションで指定して、コマンドライン引数を QR コードを作るメッセージとしました。
後で `-o` を省略可にしようと思いますが、現時点では必須のオプションという意味不明な状態です。

```
go run mkqrcode.go -o hello.png hello
```

## QR コードをテキストとしても出力してみる

QR コードを png ファイルとしては出力できるようになりましたが、小さい QR コードの場合、ターミナルに直接表示できたら便利かもしれません（あんまり長いデータだとターミナルからはみ出ちゃいそうですが）。

そこで、テキストとしても出力できるようにしてみます。

```go
package main

import (
	"flag"
	"fmt"
	"image/color"
	"image/png"
	"log"
	"os"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

func main() {
	outFile := flag.String("o", "", "output file name")
	flag.Parse()
	args := flag.Args()
	if len(args) != 1 {
		flag.Usage()
		os.Exit(1)
	}

	message := args[0]

	qrCode, err := qr.Encode(message, qr.M, qr.Auto)
	if err != nil {
		log.Fatal(err)
	}

	if *outFile != "" {
		err = outputQrCode(qrCode, 200, *outFile)
		if err != nil {
			log.Fatal(err)
		}

	} else {
		black := "\033[40m  \033[0m"
		white := "\033[47m  \033[0m"
		printQrCode(qrCode, black, white)
	}
}

func outputQrCode(qrCode barcode.Barcode, size int, fileName string) error {
	qrCode, err := barcode.Scale(qrCode, size, size)
	if err != nil {
		return err
	}

	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()

	err = png.Encode(file, qrCode)
	return err
}

func printQrCode(qrCode barcode.Barcode, black, white string) {
	rect := qrCode.Bounds()
	for y := rect.Min.Y; y < rect.Max.Y; y++ {
		for x := rect.Min.X; x < rect.Max.X; x++ {
			if qrCode.At(x, y) == color.Black {
				fmt.Print(black)
			} else {
				fmt.Print(white)
			}
		}
		fmt.Println()
	}
}
```

これで `-o` オプションは省略可能になりました。

テキストで出力する場合、白と黒はそれぞれ ANSI エスケープシーケンスで背景色を白/黒に設定して空白 2 文字を出力するようにしています。

```
go run mkqrcode.go hello
```

## マージンを設定する

ターミナルに表示してみると、QR コードの周りの余白が足りない気がしてきます。

一般的に、QR コードの周りにはマージンとして 4 セル分の余白が必要になります。

* [QR コードドットコム - コード領域を確定するためのポイント](https://www.qrcode.com/howto/code.html)

もちろん、出力した画像やテキストに 4 セル分の余白がなくても、最終的に QR コードを掲示する際に余白があれば良いわけですが、出力時にマージンが指定できるとよいのではないかと思います。

オプションでマージンを指定できるようにしてみました。

```go
package main

import (
	"flag"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"log"
	"os"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

func main() {
	outFile := flag.String("o", "", "output file name")
	margin := flag.Int("m", 4, "margin of QR-code")
	flag.Parse()
	args := flag.Args()
	if len(args) != 1 {
		flag.Usage()
		os.Exit(1)
	}
	if *margin < 0 {
		fmt.Fprintln(os.Stderr, "Margin is negative")
		os.Exit(1)
	}

	message := args[0]

	qrCode, err := qr.Encode(message, qr.M, qr.Auto)
	if err != nil {
		log.Fatal(err)
	}

	if *outFile != "" {
		err = outputQrCode(qrCode, *margin, 200, *outFile)
		if err != nil {
			log.Fatal(err)
		}

	} else {
		black := "\033[40m  \033[0m"
		white := "\033[47m  \033[0m"
		printQrCode(qrCode, *margin, black, white)
	}
}

func outputQrCode(qrCode barcode.Barcode, margin, size int, fileName string) error {
	rect := qrCode.Bounds()
	cells := rect.Max.X + margin*2
	if size < cells {
		return fmt.Errorf("size is too small. required %d <= size", cells)
	}

	img := image.NewRGBA(image.Rect(0, 0, size, size))
	for y := 0; y < size; y++ {
		for x := 0; x < size; x++ {
			cx := x*cells/size - margin
			cy := y*cells/size - margin
			if rect.Min.X <= cx && cx < rect.Max.X &&
				rect.Min.Y <= cy && cy < rect.Max.Y &&
				qrCode.At(cx, cy) == color.Black {

				img.Set(x, y, color.Black)
			} else {
				img.Set(x, y, color.White)
			}
		}
	}

	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()

	err = png.Encode(file, img)
	return err
}

func printQrCode(qrCode barcode.Barcode, margin int, black, white string) {
	rect := qrCode.Bounds()
	for y := rect.Min.Y - margin; y < rect.Max.Y+margin; y++ {
		for x := rect.Min.X - margin; x < rect.Max.X+margin; x++ {
			if rect.Min.X <= x && x < rect.Max.X &&
				rect.Min.Y <= y && y < rect.Max.Y &&
				qrCode.At(x, y) == color.Black {

				fmt.Print(black)
			} else {
				fmt.Print(white)
			}
		}
		fmt.Println()
	}
}
```

マージンは `-m` オプションで指定します。

`boombuler/barcode` の機能としてマージンを指定することができなそうだったので、自力で画像を作ってみました。

## いろいろオプションをつけてみる

ほかにもいろいろオプションをつけてみます。

```go
package main

import (
	"flag"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"log"
	"os"
	"strings"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

const versionInfo = "@(#) $Id: mkqrcode.go 0.5.0 2020-05-17 14:59 yoshi389111 Exp $"

func usage() {
	o := flag.CommandLine.Output()
	fmt.Fprintf(o, "Usage: %s [options] MESSAGE", flag.CommandLine.Name())
	flag.PrintDefaults()
}

func main() {
	outFile := flag.String("o", "", "output file name")
	margin := flag.Int("m", 4, "margin of QR-code")
	black := flag.String("b", "", "pattern of black")
	white := flag.String("w", "", "pattern of white")
	size := flag.Int("s", 200, "size of QR-code(Image)")
	optLevel := flag.String("l", "M", "error correction level [L|M|Q|H]")
	optEncoding := flag.String("e", "auto", "encoding of QR-code [auto|numeric|alphanumeric|unicode]")
	version := flag.Bool("v", false, "show version info")
	flag.Usage = usage
	flag.Parse()
	args := flag.Args()
	if *version {
		fmt.Println(versionInfo)
		os.Exit(0)
	}
	if len(args) != 1 {
		flag.Usage()
		os.Exit(1)
	}
	if *margin < 0 {
		fmt.Fprintln(os.Stderr, "margin is negative")
		os.Exit(1)
	}
	if *black == "" && *white == "" {
		*black = "\033[40m  \033[0m"
		*white = "\033[47m  \033[0m"
	} else if *black == "" || *white == "" {
		fmt.Fprintln(os.Stderr, "specify both black and white")
		os.Exit(1)
	}
	var level qr.ErrorCorrectionLevel
	var encoding qr.Encoding

	switch strings.ToUpper(*optLevel) {
	case "L":
		level = qr.L
	case "M":
		level = qr.M
	case "Q":
		level = qr.Q
	case "H":
		level = qr.H
	default:
		fmt.Fprintf(os.Stderr, "invalid error correction level. (%s)\n", *optLevel)
		os.Exit(1)
	}
	switch strings.ToLower(*optEncoding) {
	case "auto":
		encoding = qr.Auto
	case "numeric":
		encoding = qr.Numeric
	case "alphanumeric":
		encoding = qr.AlphaNumeric
	case "unicode":
		encoding = qr.Unicode
	default:
		fmt.Fprintf(os.Stderr, "invalid encoding. (%s)\n", *optEncoding)
		os.Exit(1)
	}

	message := args[0]

	qrCode, err := qr.Encode(message, level, encoding)
	if err != nil {
		log.Fatal(err)
	}

	if *outFile != "" {
		err = outputQrCode(qrCode, *margin, *size, *outFile)
		if err != nil {
			log.Fatal(err)
		}

	} else {
		printQrCode(qrCode, *margin, *black, *white)
	}
}
// 以下略
```

## flag をやめて getopt にしてみる

Unix/Linux 系のコマンドは、最近はショートオプションとロングオプションの両方が使えるのが一般的になっている気がします。

* POSIX のコマンドラインの文法 - ショートオプション
  * [12. Utility Conventions](https://pubs.opengroup.org/onlinepubs/009696899/basedefs/xbd_chap12.html)
  * [A.12 Utility Conventions](https://pubs.opengroup.org/onlinepubs/009696899/xrat/xbd_chap12.html)
* GNU のオプションのスタイル - POSIX を拡張してロングオプションを定義
  * [4.8 Standards for Command Line Interfaces](https://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html)
  * [25.1.1 Program Argument Syntax Conventions](https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html)
* 【参考】 [コマンドラインプログラムにおける引数、オプションなどの標準仕様](http://yohshiy.blog.fc2.com/blog-entry-260.html)

特にヘルプ( `-h` と `--help` )とバージョン情報( `-v` と `--version` )の表示については、一般的に使われやすいものなので、両対応をしておきたいことが多いと思います。

しかし標準の `flag` では、ロングオプションとショートオプションを同時に定義ができないようです。

少し調べてみると `flag` 以外にも様々なコマンドライン処理用のライブラリがあるようです。
そのなかで `flag` によく似た `pborman/getopt` を使ってみようと思います。

* [package getopt](https://pkg.go.dev/github.com/pborman/getopt) - BSD-3-Clause

```
go get github.com/pborman/getopt
```

```go
package main

import (
	"fmt"
	"image"
	"image/color"
	"image/png"
	"log"
	"os"
	"strings"

	"github.com/pborman/getopt/v2"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/qr"
)

const versionInfo = "@(#) $Id: mkqrcode.go 0.6.0 2020-05-17 15:34 yoshi389111 Exp $"

func main() {
	getopt.SetParameters("MESSAGE")
	outFile := getopt.StringLong("output", 'o', "", "output file name", "FILE")
	margin := getopt.IntLong("margin", 'm', 4, "margin of QR-code", "NUMBER")
	black := getopt.StringLong("black", 'b', "", "pattern of black", "STRING")
	white := getopt.StringLong("white", 'w', "", "pattern of white", "STRING")
	size := getopt.IntLong("size", 's', 200, "size of QR-code(Image)", "NUMBER")
	optLevel := getopt.EnumLong("level", 'l',
		[]string{"L", "M", "Q", "H"},
		"M", "error correction level", "{L|M|Q|H}")
	optEncoding := getopt.EnumLong("encoding", 'e',
		[]string{"auto", "numeric", "alphanumeric", "unicode"},
		"auto", "encoding of QR-code", "{auto|numeric|alphanumeric|unicode}")
	version := getopt.BoolLong("version", 'v', "show version info")
	help := getopt.BoolLong("help", 'h', "show help message")
	getopt.Parse()
	args := getopt.Args()
	if *help {
		getopt.PrintUsage(os.Stdout)
		os.Exit(0)
	}
	if *version {
		fmt.Println(versionInfo)
		os.Exit(0)
	}
	if len(args) != 1 {
		getopt.Usage()
		os.Exit(1)
    }
// 以下略
```

## Installation

```
go get github.com/yoshi389111/go-mkqrcode
```

## demo

```
go-mkqrcode "hello world"
```

![hello world](https://raw.githubusercontent.com/wiki/yoshi389111/go-mkqrcode/images/hello_world.gif)