Go 言語で QR コードを生成するコマンドを作ってみた
はじめに
- Go 言語で QR コードを生成するコマンドを作ってみました
- 作業メモ的に残しておきます
- 無駄に長いです
リポジトリの作成
何はともあれ、Github でリポジトリを作成します。とりあえず go-mkqrcode
という名前で作ってみました。
git clone git@github.com:yoshi389111/go-mkqrcode.git
cd go-mkqrcode
デフォルトの Initial commit に絵文字 をつけたいので
rebase -i
します。
git rebase -i --root
# `Inital commit` -> `:tada: Inital commit`
git push -f
QR コードを作るライブラリ
Go 言語で QR コードを作るライブラリとして有名な github.com/boombuler/barcode
を使用することにしました。
- package barcode - MIT ライセンス
go get github.com/boombuler/barcode
サンプルコードをもとに、とりあえず QR コードを出力してみます。
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
というもののようです。
これを使って、QR コードを作るデータや、出力先ファイル名を指定可能にしてみます。
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 コードの場合、ターミナルに直接表示できたら便利かもしれません(あんまり長いデータだとターミナルからはみ出ちゃいそうですが)。
そこで、テキストとしても出力できるようにしてみます。
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 セル分の余白が必要になります。
もちろん、出力した画像やテキストに 4 セル分の余白がなくても、最終的に QR コードを掲示する際に余白があれば良いわけですが、出力時にマージンが指定できるとよいのではないかと思います。
オプションでマージンを指定できるようにしてみました。
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
の機能としてマージンを指定することができなそうだったので、自力で画像を作ってみました。
いろいろオプションをつけてみる
ほかにもいろいろオプションをつけてみます。
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 のコマンドラインの文法 - ショートオプション
- GNU のオプションのスタイル - POSIX を拡張してロングオプションを定義
- 【参考】 コマンドラインプログラムにおける引数、オプションなどの標準仕様
特にヘルプ( -h
と --help
)とバージョン情報( -v
と --version
)の表示については、一般的に使われやすいものなので、両対応をしておきたいことが多いと思います。
しかし標準の flag
では、ロングオプションとショートオプションを同時に定義ができないようです。
少し調べてみると flag
以外にも様々なコマンドライン処理用のライブラリがあるようです。
そのなかで flag
によく似た pborman/getopt
を使ってみようと思います。
- package getopt - BSD-3-Clause
go get github.com/pborman/getopt
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"