Skip to content

Commit

Permalink
Add capability to have different shapes of the dots
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Scholz committed Nov 27, 2023
1 parent 999da75 commit cdc53c7
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 1 deletion.
110 changes: 110 additions & 0 deletions kotlin-sample/src/main/kotlin/io/github/simonscholz/CustomDotsMain.kt
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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(
Expand All @@ -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 {
Expand All @@ -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 }

Expand All @@ -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<Int, Int, Int, Graphics2D, Unit>) = 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,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
},
;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit cdc53c7

Please sign in to comment.