6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-17

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

はじめに

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

hello world

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

リポジトリの作成

何はともあれ、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 を使用することにしました。

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 系のコマンドは、最近はショートオプションとロングオプションの両方が使えるのが一般的になっている気がします。

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

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

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

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"

hello world

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?