-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathQRCodeBuilder.kt
297 lines (268 loc) · 10.4 KB
/
QRCodeBuilder.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package qrcode
import qrcode.QRCode.Companion.EMPTY_FN
import qrcode.QRCodeBuilder.QRCodeShapesEnum.CIRCLE
import qrcode.QRCodeBuilder.QRCodeShapesEnum.CUSTOM
import qrcode.QRCodeBuilder.QRCodeShapesEnum.ROUNDED_SQUARE
import qrcode.QRCodeBuilder.QRCodeShapesEnum.SQUARE
import qrcode.color.Colors
import qrcode.color.DefaultColorFunction
import qrcode.color.LinearGradientColorFunction
import qrcode.color.QRCodeColorFunction
import qrcode.internals.QRMath
import qrcode.raw.ErrorCorrectionLevel
import qrcode.raw.QRCodeProcessor
import qrcode.render.QRCodeGraphics
import qrcode.render.QRCodeGraphicsFactory
import qrcode.shape.CircleShapeFunction
import qrcode.shape.DefaultShapeFunction
import qrcode.shape.QRCodeShapeFunction
import qrcode.shape.RoundSquaresShapeFunction
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
import kotlin.jvm.JvmOverloads
@JsExport
@OptIn(ExperimentalJsExport::class)
class QRCodeBuilder @JvmOverloads constructor(
private val shape: QRCodeShapesEnum,
private var customShapeFunction: QRCodeShapeFunction? = null,
) {
private var customColorFunction: QRCodeColorFunction? = null
private var squareSize: Int = QRCodeProcessor.DEFAULT_CELL_SIZE
private var color: Int = Colors.BLACK
private var endColor: Int? = null
private var vertical: Boolean = true
private var background: Int = Colors.WHITE
private var innerSpace: Int = innerSpace()
private var radiusInPixels: Int = RoundSquaresShapeFunction.defaultRadius(squareSize)
private var drawLogoAction: QRCode.(QRCodeGraphics, Int, Int) -> Unit = EMPTY_FN
private var drawLogoBeforeAction: QRCode.(QRCodeGraphics, Int, Int) -> Unit = EMPTY_FN
private var userDoAfter: QRCode.(QRCodeGraphics, Int, Int) -> Unit = EMPTY_FN
private var userDoBefore: QRCode.(QRCodeGraphics, Int, Int) -> Unit = EMPTY_FN
private var graphicsFactory: QRCodeGraphicsFactory = QRCodeGraphicsFactory()
private var errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.VERY_HIGH
private var minTypeNum: Int = 6
enum class QRCodeShapesEnum {
SQUARE,
CIRCLE,
ROUNDED_SQUARE,
CUSTOM
}
private fun innerSpace() =
when (shape) {
SQUARE -> 1
CIRCLE -> CircleShapeFunction.defaultInnerSpace(squareSize)
ROUNDED_SQUARE -> RoundSquaresShapeFunction.defaultInnerSpace(squareSize)
CUSTOM -> 0
}.takeIf { it < squareSize } ?: 0
/** Size of each individual space in the QRCode (each cell). */
fun withSize(size: Int): QRCodeBuilder {
squareSize = size.coerceAtLeast(1)
return withInnerSpacing(innerSpace())
}
/**
* Color of the cells of the QRCode.
*
* Expected to be the Integer that represents an RGBA color. In short, use the [Colors] helpers ;)
*
* @see Colors
* @see Colors.css
* @see Colors.rgba
* @see Colors.withAlpha
*/
fun withColor(color: Int): QRCodeBuilder {
this.color = color
return this
}
/**
* Background color of the QRCode.
*
* Expected to be the Integer that represents an RGBA color. In short, use the [Colors] helpers ;)
*
* @see Colors
*/
fun withBackgroundColor(bgColor: Int): QRCodeBuilder {
background = bgColor
return this
}
/**
* Uses a [LinearGradientColorFunction] to choose colors for the QRCode.
*
* By default, the gradient will be a vertical one (top-to-bottom)
*
* If [endColor] is `null`, a [DefaultColorFunction] will be used instead.
*
* @see Colors
*/
@JvmOverloads
fun withGradientColor(startColor: Int, endColor: Int?, vertical: Boolean = true): QRCodeBuilder {
color = startColor
this.endColor = endColor
this.vertical = vertical
return this
}
/**
* Radius of the edges of the Rounded Squares. Only applies for Rounded Squares. If set to a negative number,
* the default radius will be used.
*/
fun withRadius(radius: Int): QRCodeBuilder {
radiusInPixels = radius.takeIf { it >= 0 } ?: RoundSquaresShapeFunction.defaultRadius(squareSize)
return this
}
/** How much space there should be around each QRCode Cell. Defaults to 1 pixel, or 0 if a custom shape function is being used. */
@JvmOverloads
fun withInnerSpacing(innerSpacing: Int? = null): QRCodeBuilder {
innerSpace = innerSpacing?.takeIf { it >= 0 } ?: innerSpace()
return this
}
/**
* Adds an image on top of the QRCode, at the center of it.
*
* If [clearLogoArea] is `false` the cells behind the logo will be drawn as normal.
*
*/
@JvmOverloads
fun withLogo(logo: ByteArray?, width: Int, height: Int, clearLogoArea: Boolean = true): QRCodeBuilder {
if (logo != null) {
if (clearLogoArea) {
drawLogoBeforeAction = { _, _, _ ->
val logoX = (computedSize - width) / 2
val logoY = (computedSize - height) / 2
rawData.forEach { row ->
row.forEach { cell ->
val cellX = cell.absoluteX(squareSize) + squareSize
val cellY = cell.absoluteY(squareSize) + squareSize
cell.rendered = !QRMath.rectsIntersect(
logoX,
logoY,
width,
height,
cellX,
cellY,
squareSize,
squareSize,
)
}
}
}
} else {
drawLogoBeforeAction = EMPTY_FN
}
drawLogoAction = { canvas, xOffset, yOffset ->
val logoX = xOffset + (computedSize - width) / 2
val logoY = yOffset + (computedSize - height) / 2
canvas.drawImage(logo, logoX, logoY)
}
}
return this
}
/** Run a piece of code after the rendering is done. */
fun withAfterRenderAction(action: QRCode.(QRCodeGraphics) -> Unit): QRCodeBuilder {
userDoAfter = { it, _, _ -> action(it) }
return this
}
/** Run a piece of code before the rendering is done. */
fun withBeforeRenderAction(action: QRCode.(QRCodeGraphics) -> Unit): QRCodeBuilder {
userDoBefore = { it, _, _ -> action(it) }
return this
}
/** Use a custom [QRCodeGraphicsFactory] instead of the default. */
fun withGraphicsFactory(factory: QRCodeGraphicsFactory): QRCodeBuilder {
graphicsFactory = factory
return this
}
/**
* Sets the [QRCode.colorFn] value to a custom one. If set, the builder will ignore [color] and [background].
*
* Default is `null`, meaning a [DefaultColorFunction] will be created from the [color] and [background] values.
*
* @see QRCodeColorFunction
* @see DefaultColorFunction
*/
fun withCustomColorFunction(colorFn: QRCodeColorFunction?): QRCodeBuilder {
this.customColorFunction = colorFn
return this
}
/**
* Sets the [QRCode.shapeFn] value to a custom one. If set, the builder will ignore the [shape] parameter.
*
* Default is `null`, meaning a [QRCodeShapeFunction] will be created for the selected [shape].
*
* If [shape] is [CUSTOM] but [customShapeFunction] is not set, a [DefaultShapeFunction] will be used.
*
* @see QRCodeShapeFunction
* @see DefaultShapeFunction
* @see RoundSquaresShapeFunction
* @see CircleShapeFunction
*/
fun withCustomShapeFunction(shapeFn: QRCodeShapeFunction?): QRCodeBuilder {
this.customShapeFunction = shapeFn
return this
}
/**
* The level of error correction to apply to the QR Code. Defaults to [ErrorCorrectionLevel.VERY_HIGH].
*
* In short, this configures how much data loss we can tolerate. Higher error correction = Readable QR Codes even
* with large parts hidden/crumpled/deformed.
*
* @see ErrorCorrectionLevel
*/
fun withErrorCorrectionLevel(ecl: ErrorCorrectionLevel): QRCodeBuilder {
this.errorCorrectionLevel = ecl
return this
}
/**
* The minimum level of "information density" this QRCode will maintain. Defaults to 6.
*
* This is complex to explain, but basically the lower this value the fewer squares the QR Code _**might**_ have.
*
* This is simply a way to make sure QR Codes for very few characters are readable :)
*
*/
fun withMinimumInformationDensity(minTypeNum: Int): QRCodeBuilder {
this.minTypeNum = minTypeNum
return this
}
private val beforeFn: QRCode.(QRCodeGraphics, Int, Int) -> Unit
get() = { canvas, xOffset, yOffset ->
drawLogoBeforeAction(canvas, xOffset, yOffset)
userDoBefore(canvas, xOffset, yOffset)
}
private val afterFn: QRCode.(QRCodeGraphics, Int, Int) -> Unit
get() = { canvas, xOffset, yOffset ->
drawLogoAction(canvas, xOffset, yOffset)
userDoAfter(canvas, xOffset, yOffset)
}
private val colorFunction: QRCodeColorFunction
get() = when (endColor) {
null -> customColorFunction ?: DefaultColorFunction(foreground = color, background)
else -> customColorFunction ?: LinearGradientColorFunction(
startForegroundColor = color,
endForegroundColor = endColor!!,
background,
)
}
private val shapeFunction: QRCodeShapeFunction
get() = customShapeFunction ?: when (shape) {
SQUARE, CUSTOM -> DefaultShapeFunction(squareSize, innerSpace = innerSpace)
CIRCLE -> CircleShapeFunction(squareSize, innerSpace = innerSpace)
ROUNDED_SQUARE -> RoundSquaresShapeFunction(squareSize, radiusInPixels, innerSpace = innerSpace)
}
/**
* Builds a [QRCode] instance ready to use.
*
* @see QRCode.renderToBytes
* @see QRCode.render
*/
fun build(data: String) =
QRCode(
data,
squareSize,
colorFunction,
shapeFunction,
graphicsFactory,
errorCorrectionLevel,
minTypeNum,
beforeFn,
afterFn,
)
}