diff --git a/kotlin-sample/src/main/kotlin/io/github/simonscholz/CustomDotsMain.kt b/kotlin-sample/src/main/kotlin/io/github/simonscholz/CustomDotsMain.kt new file mode 100644 index 0000000..11b9b56 --- /dev/null +++ b/kotlin-sample/src/main/kotlin/io/github/simonscholz/CustomDotsMain.kt @@ -0,0 +1,110 @@ +package io.github.simonscholz + +import io.github.simonscholz.qrcode.QrCodeConfig +import io.github.simonscholz.qrcode.QrCodeDotShape +import io.github.simonscholz.qrcode.QrCodeFactory +import java.awt.Color +import java.awt.Graphics2D +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths + +fun main() { + val path = Paths.get(System.getProperty("user.home"), "qr-code-samples") + Files.createDirectories(path) + val qrCodeDir = path.toAbsolutePath().toString() + val qrCodeApi = QrCodeFactory.createQrCodeApi() + QrCodeConfig.Builder("https://simonscholz.github.io/") + .qrCodeDotStyler(QrCodeDotShape.CIRCLE) + .qrCodeSize(800) + .build() + .run { + qrCodeApi.createQrCodeImage(this) + .toFile(File(qrCodeDir, "/qr-with-CIRCLE-dots-kotlin.png")) + } + + QrCodeConfig.Builder("https://simonscholz.github.io/") + .qrCodeDotStyler(QrCodeDotShape.ROUNDED_SQUARE) + .qrCodeSize(800) + .build() + .run { + qrCodeApi.createQrCodeImage(this) + .toFile(File(qrCodeDir, "/qr-with-ROUND-SQUARE-dots-kotlin.png")) + } + + QrCodeConfig.Builder("https://simonscholz.github.io/") + .qrCodeDotStyler(QrCodeDotShape.HEXAGON) + .qrCodeSize(800) + .build() + .run { + qrCodeApi.createQrCodeImage(this) + .toFile(File(qrCodeDir, "/qr-with-HEXAGON-dots-kotlin.png")) + } + + QrCodeConfig.Builder("https://simonscholz.github.io/") + .qrCodeDotStyler(QrCodeDotShape.TRIANGLE) + .qrCodeSize(800) + .build() + .run { + qrCodeApi.createQrCodeImage(this) + .toFile(File(qrCodeDir, "/qr-with-TRIANGLE-dots-kotlin.png")) + } + + QrCodeConfig.Builder("https://simonscholz.github.io/") + .qrCodeDotStyler(QrCodeDotShape.HEART) + .qrCodeSize(800) + .build() + .run { + qrCodeApi.createQrCodeImage(this) + .toFile(File(qrCodeDir, "/qr-with-HEART-dots-kotlin.png")) + } + + QrCodeConfig.Builder("https://simonscholz.github.io/") + .qrCodeDotStyler(QrCodeDotShape.HOUSE) + .qrCodeSize(800) + .build() + .run { + qrCodeApi.createQrCodeImage(this) + .toFile(File(qrCodeDir, "/qr-with-HOUSE-dots-kotlin.png")) + } + + QrCodeConfig.Builder("https://simonscholz.github.io/") + .qrCodeDotStyler(::drawColorfulHouseWithDoorAndWindow) + .qrCodeSize(800) + .build() + .run { + qrCodeApi.createQrCodeImage(this) + .toFile(File(qrCodeDir, "/qr-with-COLORFUL-HOUSE-dots-kotlin.png")) + } +} + +private fun drawColorfulHouseWithDoorAndWindow(x: Int, y: Int, size: Int, graphic: Graphics2D) { + val roofHeight = size / 2 + val houseWidth = size + val houseHeight = size - roofHeight + + // Draw the base of the house + graphic.color = Color.RED + graphic.fillRect(x, y + roofHeight, houseWidth, houseHeight) + + // Draw the roof + graphic.color = Color.BLUE + val roofXPoints = intArrayOf(x, x + houseWidth / 2, x + houseWidth) + val roofYPoints = intArrayOf(y + roofHeight, y, y + roofHeight) + graphic.fillPolygon(roofXPoints, roofYPoints, 3) + + // Draw the door + val doorWidth = size / 5 + val doorHeight = size / 2 - 1 + val doorX = x + (houseWidth - size / 5) / 2 + size / 10 + val doorY = y + roofHeight + houseHeight - doorHeight + 1 + graphic.color = Color.GREEN + graphic.fillRect(doorX, doorY, doorWidth, doorHeight) + + // Draw the window + val windowSize = size / 5 + val windowX = x + size / 5 + val windowY = y + roofHeight + size / 5 + graphic.color = Color.YELLOW + graphic.fillRect(windowX, windowY, windowSize, windowSize) +} diff --git a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/QrCodeConfig.kt b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/QrCodeConfig.kt index c7c46aa..0ed2890 100644 --- a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/QrCodeConfig.kt +++ b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/QrCodeConfig.kt @@ -1,7 +1,9 @@ package io.github.simonscholz.qrcode import java.awt.Color +import java.awt.Graphics2D import java.awt.Image +import kotlin.reflect.KFunction4 const val DEFAULT_IMG_SIZE = 300 @@ -13,6 +15,7 @@ const val DEFAULT_IMG_SIZE = 300 * @param qrLogoConfig - configuration of the logo to be rendered in the middle of the qr code, may be null * @param qrCodeColorConfig - configuration of the colors of the qr code * @param qrPositionalSquaresConfig - configure the positional squares on the qr code + * @param qrCodeDotStyler - configure the shape of the dots in the qr code, also see [QrCodeDotShape] * @param qrBorderConfig - configure the border of the qr code */ class QrCodeConfig @JvmOverloads constructor( @@ -21,6 +24,7 @@ class QrCodeConfig @JvmOverloads constructor( val qrLogoConfig: QrLogoConfig? = null, val qrCodeColorConfig: QrCodeColorConfig = QrCodeColorConfig(), val qrPositionalSquaresConfig: QrPositionalSquaresConfig = QrPositionalSquaresConfig(), + val qrCodeDotStyler: QrCodeDotStyler = QrCodeDotShape.SQUARE, val qrBorderConfig: QrBorderConfig? = null, ) { init { @@ -34,6 +38,7 @@ class QrCodeConfig @JvmOverloads constructor( private var qrCodeColorConfig: QrCodeColorConfig = QrCodeColorConfig() private var qrPositionalSquaresConfig: QrPositionalSquaresConfig = QrPositionalSquaresConfig() private var qrBorderConfig: QrBorderConfig? = null + private var qrCodeDotStyler: QrCodeDotStyler = QrCodeDotShape.SQUARE fun qrCodeSize(qrCodeSize: Int) = apply { this.qrCodeSize = qrCodeSize } @@ -57,12 +62,25 @@ class QrCodeConfig @JvmOverloads constructor( this.qrBorderConfig = QrBorderConfig(color, relativeSize, relativeBorderRound) } + fun qrCodeDotStyler(qrCodeDotStyler: QrCodeDotStyler) = apply { + this.qrCodeDotStyler = qrCodeDotStyler + } + + fun qrCodeDotStyler(qrCodeDotStyler: KFunction4) = apply { + this.qrCodeDotStyler = object : QrCodeDotStyler { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + qrCodeDotStyler.invoke(x, y, dotSize, graphics) + } + } + } + fun build() = QrCodeConfig( qrCodeText = qrCodeText, qrCodeSize = qrCodeSize, qrLogoConfig = qrLogoConfig, qrCodeColorConfig = qrCodeColorConfig, qrPositionalSquaresConfig = qrPositionalSquaresConfig, + qrCodeDotStyler = qrCodeDotStyler, qrBorderConfig = qrBorderConfig, ) } diff --git a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/QrCodeDotStyler.kt b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/QrCodeDotStyler.kt new file mode 100644 index 0000000..81a28c1 --- /dev/null +++ b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/QrCodeDotStyler.kt @@ -0,0 +1,47 @@ +package io.github.simonscholz.qrcode + +import io.github.simonscholz.qrcode.internal.graphics.CustomQrCodeDotStyler +import java.awt.Graphics2D + +interface QrCodeDotStyler { + fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) +} + +enum class QrCodeDotShape : QrCodeDotStyler { + SQUARE { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + graphics.fillRect(x, y, dotSize, dotSize) + } + }, + ROUNDED_SQUARE { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + graphics.fillRoundRect(x, y, dotSize, dotSize, 10, 10) + } + }, + CIRCLE { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + graphics.fillArc(x, y, dotSize, dotSize, 0, 360) + } + }, + HEXAGON { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + CustomQrCodeDotStyler.drawHexagon(x, y, dotSize, graphics) + } + }, + TRIANGLE { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + CustomQrCodeDotStyler.drawEquilateralTriangle(x, y, dotSize, graphics) + } + }, + HEART { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + CustomQrCodeDotStyler.drawHeart(x, y, dotSize, graphics) + } + }, + HOUSE { + override fun createDot(x: Int, y: Int, dotSize: Int, graphics: Graphics2D) { + CustomQrCodeDotStyler.drawHouse(x, y, dotSize, graphics) + } + }, + ; +} diff --git a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/api/QrCodeApiImpl.kt b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/api/QrCodeApiImpl.kt index f19f907..eb32fa9 100644 --- a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/api/QrCodeApiImpl.kt +++ b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/api/QrCodeApiImpl.kt @@ -45,6 +45,7 @@ internal class QrCodeApiImpl : QrCodeApi { quietZone = qrCodeConfig.qrBorderConfig?.let { 1 } ?: 0, // have a quietZone if we have a border borderWidth = qrCodeConfig.qrBorderConfig?.let { relativeSize(qrCodeConfig.qrCodeSize, it.relativeSize) } ?: 0, relativeBorderRound = qrCodeConfig.qrBorderConfig?.relativeBorderRound ?: .0, + customDotStyler = qrCodeConfig.qrCodeDotStyler::createDot, ) graphics.drawImage(qrCode, 0, 0, null) diff --git a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/graphics/CustomQrCodeDotStyler.kt b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/graphics/CustomQrCodeDotStyler.kt new file mode 100644 index 0000000..c0ea877 --- /dev/null +++ b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/graphics/CustomQrCodeDotStyler.kt @@ -0,0 +1,74 @@ +package io.github.simonscholz.qrcode.internal.graphics + +import java.awt.Graphics2D +import java.awt.Point +import java.awt.Polygon +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +internal object CustomQrCodeDotStyler { + + fun drawHouse(x: Int, y: Int, size: Int, graphic: Graphics2D) { + val roofHeight = size / 2 + val houseWidth = size - 1 // -1 to have a gap between the houses + val houseHeight = size - roofHeight + + // Draw the base of the house + graphic.fillRect(x, y + roofHeight, houseWidth, houseHeight) + + // Draw the roof + val roofXPoints = intArrayOf(x, x + houseWidth / 2, x + houseWidth) + val roofYPoints = intArrayOf(y + roofHeight, y, y + roofHeight) + graphic.fillPolygon(roofXPoints, roofYPoints, 3) + } + + fun drawHeart(x: Int, y: Int, size: Int, graphic: Graphics2D) { + val heartWidth = handleRoundingIssues(size) + val heartHeight = handleRoundingIssues(size) + val gap = heartWidth / 4 + + // Draw the left arc of the heart + graphic.fillArc(x, y, heartWidth / 2, heartHeight / 2, 0, 180) + + // Draw the right arc of the heart + graphic.fillArc(x + heartWidth / 2, y, heartWidth / 2, heartHeight / 2, 0, 180) + + // Draw the bottom triangle of the heart + val triangleXPoints = intArrayOf(x, x + heartWidth / 2, x + heartWidth, x) + val triangleYPoints = intArrayOf(y + heartHeight / 2 - gap, y + heartHeight - gap, y + heartHeight / 2 - gap, y + heartHeight / 2 - gap) + graphic.fillPolygon(triangleXPoints, triangleYPoints, 4) + } + + private fun handleRoundingIssues(size: Int): Int = + if (size % 2 == 0) { + size + } else { + size - 1 + } + + fun drawHexagon(x: Int, y: Int, size: Int, graphic: Graphics2D) { + val hexRadius = size / 2 + graphic.fillPolygon(createHexagon(Point(x + hexRadius, y + hexRadius), hexRadius)) + } + + private fun createHexagon(center: Point, radius: Int): Polygon { + val polygon = Polygon() + for (i in 0..5) { + polygon.addPoint( + (center.x + radius * cos(i * 2 * Math.PI / 6.0)).toInt(), + (center.y + radius * sin(i * 2 * Math.PI / 6.0)).toInt(), + ) + } + return polygon + } + + fun drawEquilateralTriangle(x: Int, y: Int, size: Int, graphic: Graphics2D) { + val triangleHeight = (sqrt(3.0) / 2 * size).toInt() + + val triangleXPoints = intArrayOf(x, x + size / 2, x + size, x) + val triangleYPoints = intArrayOf(y + triangleHeight, y, y + triangleHeight, y + triangleHeight) + + graphic.fillPolygon(triangleXPoints, triangleYPoints, 3) + } +} diff --git a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/qr/QrCodeCreator.kt b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/qr/QrCodeCreator.kt index 40b37a6..7add550 100644 --- a/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/qr/QrCodeCreator.kt +++ b/qr-code/src/main/kotlin/io/github/simonscholz/qrcode/internal/qr/QrCodeCreator.kt @@ -48,6 +48,7 @@ internal class QrCodeCreator { quietZone: Int, borderWidth: Int, relativeBorderRound: Double, + customDotStyler: ((x: Int, y: Int, size: Int, graphics: Graphics2D) -> Unit)? = null, ): BufferedImage { val qrCode: QRCode = Encoder.encode(qrCodeText, ErrorCorrectionLevel.H, encodeHintTypes()) val (positionalSquares, dataSquares) = PositionalsUtil.renderResult(qrCode, size, quietZone) @@ -74,7 +75,8 @@ internal class QrCodeCreator { dataSquares.forEach { s -> if (s.isFilled) { graphics.color = fillColor - graphics.fillRect(s.x, s.y, s.size, s.size) + customDotStyler?.invoke(s.x, s.y, s.size, graphics) + ?: graphics.fillRect(s.x, s.y, s.size, s.size) } else { graphics.color = bgColor graphics.fillRect(s.x, s.y, s.size, s.size)