アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

QRコードを文字列で構成する

目標

実装

言語はKotlinで実装しましたが、生成QRコードの2値情報が作れれば何でも実装できます。

ライブラリは zxing だけ使います。
build.gradle

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.zxing:core:3.5.0")
}

sizeが0で最小のQRコードができます。
なるべく余白がない文字がいいので、探した所、「█」と「▀」と「▄」の記号を見つけました。縦長なので、縦2列で1つの文字を使います。

実装1

import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel


class CharQRCodeGenerator {
    fun generate(text: String) {
        val hints: MutableMap<EncodeHintType, Any> = mutableMapOf()
        hints[EncodeHintType.MARGIN] = 0
        hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.L

        val size = 0
        val matrix = MultiFormatWriter().encode(
            text,
            BarcodeFormat.QR_CODE, size, size, hints
        )

        println(
            generateTwoYQRCode(matrix.toList())
        )
    }

    private fun BitMatrix.toList(): List<List<Boolean>> {
        val matrix = this
        return (0 until matrix.height).map { y ->
            (0 until matrix.width).map { x ->
                matrix.get(x, y)
            }
        }
    }

    private fun generateTwoYQRCode(matrix: List<List<Boolean>>): String {
        val result = matrix.windowed(size = 2, step = 2, partialWindows = true).map { yList ->
            (yList[0].indices).map { xIndex ->
                val yTop = yList[0][xIndex]
                val yBottom = yList.getOrNull(1)?.get(xIndex) ?: false

                when {
                    yTop && yBottom -> {
                        "█"
                    }
                    yTop && !yBottom -> {
                        "▀"
                    }
                    !yTop && yBottom -> {
                        "▄"
                    }
                    else -> {
                        " "
                    }
                }
            }
        }

        return result.joinToString("\n") { row ->
            row.joinToString("")
        }
    }
}

出力結果

こちらで https://matsudamper.hatenablog.com/entry/2022/07/24/063640?hoge=hoge&hoge=hoge&hoge=hoge&hoge=hogeQRを生成した結果。こちらになりました。

█▀▀▀▀▀█ ▀▄▄▀▀▀▀ ▄█▀▄▀▀▄ █ ▀ ▄ █▀▀▀▀▀█
█ ███ █   ▀ ██▀▄▄▄▀▀███▀▀█▀ █ █ ███ █
█ ▀▀▀ █  ▀▄▀▄ █ █▄█▀▄▄ ▄▀▀█ █ █ ▀▀▀ █
▀▀▀▀▀▀▀ █ █▄▀▄▀ ▀ █ ▀ █▄▀▄▀▄█ ▀▀▀▀▀▀▀
▀█ ██▄▀  ▀▀▀▄ ▄▄▀█▄█  ▀██▄▄▄█▄▀▄▄  ▄▀
▀█▄▄ █▀ ██ █▄▀ ▀▀▄ ▄▄▀▄▀ ▀▄█▀█▀█▀▀█▀▀
██▄█▄ ▀ ▄▄█▀▀ ▄▄  ▄ ▀ ██▄▄▄██▀▀▀▄█▄█▀
▄▀   █▀▄▄ ▀█▀▀▄▀ ▀ ▀▄ ▄ █▀▀█ ▀ ▀▄ ▄▀█
▀▄ ▀▀ ▀▄▄▄ ▄▄█ █ ▄▄██  ▀▄█ ▄ ▄▀█ █▀▀▀
█  ▀█ ▀▀ ▄ ▀▄▄ █▄▀ █  ██ ▄▄▀█▀▄ ▀▄  █
 ▀██▀▀▀█▄▄▄ ▀ ██▀█▀▄█ ▀██ ▀▄▀▀▀███ █▀
▄█ ▀██▀ ▀▀▄ ▀▀▄█▀█  ▄ █▀█  █▄▀█▀▀▄▄▄█
▄ ▄█▀ ▀██▀▀ █▄▄▄█▄▄▀▀ ██▄▄ ██▄▀▀▄▀ ▄▀
█▀▀▄▀█▀█▄██  ▀▀▀█ ██▀▀▄▄█ ▄▀▄▀██▄▄█▀█
▀ ▀▀▀ ▀▀█▀█▀▀██▄  ▄▄█  ▀▄█▄▀█▀▀▀█▄██ 
█▀▀▀▀▀█  ▀██ █ ██  ▀ ▀▀▀██▀▄█ ▀ ██▀ ▀
█ ███ █ █▄█▄▀▄███▀▄▄ ▄▀▄▄▄▄▀▀██▀▀ ▄ ▀
█ ▀▀▀ █ ▄▄█▀  ▀▀▀▄  ▀ ▀▀█▀▄ ▀▀▄█▀▄█▄█
▀▀▀▀▀▀▀ ▀▀  ▀  ▀▀   ▀  ▀  ▀ ▀ ▀ ▀   ▀

GitHubに貼り付けた様子

こちらでは読み込めませんでした。

しかし、検出パターンを黒く塗りつぶせば読み込めることがわかりました。

実装2

しっかり検出パターンを作らなければいけませんが、文字列の上下にスペースがあるので厳しいです。なので、白黒のパターンを反転させてみることにしました。

import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel


class CharQRCodeGenerator {
    fun generate(text: String) {
        val hints: MutableMap<EncodeHintType, Any> = mutableMapOf()
        hints[EncodeHintType.MARGIN] = 1 // 周りの縁用にマージンを設定
        hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.L

        val size = 0
        val matrix = MultiFormatWriter().encode(
            text,
            BarcodeFormat.QR_CODE, size, size, hints
        )

        println(
            generateTwoYQRCode(matrix.toList())
        )
    }

    private fun BitMatrix.toList(): List<List<Boolean>> {
        val matrix = this
        return (0 until matrix.height).map { y ->
            (0 until matrix.width).map { x ->
                matrix.get(x, y).not() // 反転させる
            }
        }
    }

    private fun generateTwoYQRCode(matrix: List<List<Boolean>>): String {
        val result = matrix.windowed(size = 2, step = 2, partialWindows = true).map { yList ->
            (yList[0].indices).map { xIndex ->
                val yTop = yList[0][xIndex]
                val yBottom = yList.getOrNull(1)?.get(xIndex) ?: false

                when {
                    yTop && yBottom -> {
                        "█"
                    }
                    yTop && !yBottom -> {
                        "▀"
                    }
                    !yTop && yBottom -> {
                        "▄"
                    }
                    else -> {
                        " "
                    }
                }
            }
        }

        return result.joinToString("\n") { row ->
            row.joinToString("")
        }
    }
}

出力結果

█▀▀▀▀▀▀▀█▀██▀▀▀▀██▀▀█▀▀██▀█▀███▀▀▀▀▀▀▀█
█ █▀▀▀█ ██▄ █▀▀▀█▄▄▀ ▀▀ ▀ ▀▀█ █ █▀▀▀█ █
█ █   █ ██▀█▀▄▄▀▄ ▄▀▀▄▄▄█▀ ▀█ █ █   █ █
█ ▀▀▀▀▀ █▀█ █ █ █ ▄ █ ▄▀▄▀█ █ █ ▀▀▀▀▀ █
█▀▀█▀▀█▀█▄▀  █▄██▀▀▄▀██  ▀▄█▄ █▀█████▀█
█▀ █▄▄ ▀█▀▀█▀▄▀▄ ▀▄▄▄█▀█ ▄ ▄   ▀  ▀▀ ▀█
█▀ ▄ █▄▀█▄▄▀  ████▄█▄ █ ▀██▄ ▀ ▀ █▀▄▀▀█
█▄ ▄▄▄▀▀█▄▄ ▀▀▀▄ █▀▄▀██▄▄    ▄▀█▀▄▄▄ ▀█
█ ██▀▀▄▀▄▄██▄█▀▄▀███▀ █▄▀▄▀█▄██▀▀▄▀ ▀ █
█▀▄█▀▀█▀ ▄▄█ ▄▄█ █ ▄ ▄█▀▀▄▄█ ▀ █▄▀▄██▀█
█▄▀▀▀ ▀▀▀█▄██ ▄▀  ▀▀▄▀█  ▀▄ █ ▀ ▀▀ █▀ █
██▀▄ ▀▀▀▄  ▄█▀▀▄ ▀ █▄▄█▀  ██ █▀▀  ▄█▄▀█
█▄▄█▀ ▄▀▀▀▀ █▀█▄▄▀▄█▀ █ ▀▄██  █ ▀█ ▄▄ █
█ ▀ ▄▀▀▀ ▄▀▀█▄    ▄ ▀▀▀▄▄ ▄█ ▄ ▀▀▄█▀ ▀█
█ █▀ ▀▄▀    ▀▀▀▀█▄█▄▄▀█▄ ▄▀▄▀ ▀   ▄ ▀▄█
█▀▀▀▀▀▀▀█▄▀ ▀█ ▄ ▀█▄ ▄▀▀▀   █ █▀█   ▄▀█
█ █▀▀▀█ █▀█ ▄▀▄▀  ▀████▀█▄▄█  ▀▀▀ ▄██▀█
█ █   █ █▄▄  █▄   █▄▄▀▄▀   ▄█▀ ▄▀▀█ █▀█
█ ▀▀▀▀▀ █  ▄█▀██▀▀▄██▀██▀▄█ █▀█ ▄▀▄▄▄ █
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

GitHubに貼り付けた様子

こちらは読み込めました。

おわりに

GitHubが画像アップロードAPIを提供していなくて、IssueにQRコードを貼り付けるのが困難なので、これで解決しました。