From d2a6002c6a8412c818752736d232233232e57a5c Mon Sep 17 00:00:00 2001 From: Maciej Procyk Date: Thu, 8 Feb 2024 12:25:04 +0100 Subject: [PATCH] add qr-code based on qrose --- qr-code/build.gradle.kts | 69 ++ .../compose/qrcode/PlatformByteArray.kt | 16 + .../in/procyk/compose/qrcode/Converters.kt | 66 ++ .../in/procyk/compose/qrcode/DrawCache.kt | 111 +++ .../compose/qrcode/PlatformByteArray.kt | 34 + .../kotlin/in/procyk/compose/qrcode/QRCode.kt | 223 ++++++ .../in/procyk/compose/qrcode/QRCodeEnums.kt | 95 +++ .../in/procyk/compose/qrcode/QrCodePainter.kt | 716 ++++++++++++++++++ .../kotlin/in/procyk/compose/qrcode/QrData.kt | 246 ++++++ .../compose/qrcode/internal/BitBuffer.kt | 72 ++ .../compose/qrcode/internal/ErrorMessage.kt | 40 + .../compose/qrcode/internal/Polynomial.kt | 81 ++ .../compose/qrcode/internal/QRCodeSetup.kt | 304 ++++++++ .../compose/qrcode/internal/QRCodeSquare.kt | 121 +++ .../procyk/compose/qrcode/internal/QRData.kt | 161 ++++ .../procyk/compose/qrcode/internal/QRMath.kt | 69 ++ .../procyk/compose/qrcode/internal/QRUtil.kt | 375 +++++++++ .../procyk/compose/qrcode/internal/RSBlock.kt | 222 ++++++ .../qrcode/options/DelicateQRCodeApi.kt | 32 + .../compose/qrcode/options/Neighbors.kt | 91 +++ .../compose/qrcode/options/QrBallShape.kt | 89 +++ .../procyk/compose/qrcode/options/QrBrush.kt | 229 ++++++ .../compose/qrcode/options/QrCodeMatrix.kt | 94 +++ .../compose/qrcode/options/QrCodeShape.kt | 223 ++++++ .../procyk/compose/qrcode/options/QrColors.kt | 52 ++ .../qrcode/options/QrErrorCorrectionLevel.kt | 68 ++ .../compose/qrcode/options/QrFrameShape.kt | 174 +++++ .../procyk/compose/qrcode/options/QrLogo.kt | 47 ++ .../compose/qrcode/options/QrLogoPadding.kt | 71 ++ .../compose/qrcode/options/QrLogoShape.kt | 40 + .../compose/qrcode/options/QrOptions.kt | 107 +++ .../compose/qrcode/options/QrPixelShape.kt | 51 ++ .../compose/qrcode/options/QrShapeModifier.kt | 46 ++ .../qrcode/options/QrShapeModifiers.kt | 146 ++++ .../procyk/compose/qrcode/options/QrShapes.kt | 65 ++ .../in/procyk/compose/qrcode/options/Util.kt | 39 + .../dsl/InternalQrColorsBuilderScope.kt | 67 ++ .../options/dsl/InternalQrLogoBuilderScope.kt | 58 ++ .../dsl/InternalQrOptionsBuilderScope.kt | 67 ++ .../dsl/InternalQrShapesBuilderScope.kt | 82 ++ .../options/dsl/QrColorsBuilderScope.kt | 43 ++ .../qrcode/options/dsl/QrLogoBuilderScope.kt | 47 ++ .../options/dsl/QrOptionsBuilderScope.kt | 64 ++ .../options/dsl/QrShapesBuilderScope.kt | 49 ++ .../compose/qrcode/PlatformByteArray.kt | 21 + settings.gradle.kts | 1 + 46 files changed, 5184 insertions(+) create mode 100644 qr-code/build.gradle.kts create mode 100644 qr-code/src/androidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/Converters.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/DrawCache.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCode.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCodeEnums.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrCodePainter.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrData.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/BitBuffer.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/ErrorMessage.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/Polynomial.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSetup.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSquare.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRData.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRMath.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRUtil.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/RSBlock.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/DelicateQRCodeApi.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Neighbors.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBallShape.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBrush.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeMatrix.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeShape.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrColors.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrErrorCorrectionLevel.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrFrameShape.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogo.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoPadding.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoShape.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrOptions.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrPixelShape.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifier.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifiers.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapes.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Util.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrColorsBuilderScope.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrLogoBuilderScope.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrOptionsBuilderScope.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrShapesBuilderScope.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrColorsBuilderScope.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrLogoBuilderScope.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrOptionsBuilderScope.kt create mode 100644 qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrShapesBuilderScope.kt create mode 100644 qr-code/src/nonAndroidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt diff --git a/qr-code/build.gradle.kts b/qr-code/build.gradle.kts new file mode 100644 index 0000000..26cb724 --- /dev/null +++ b/qr-code/build.gradle.kts @@ -0,0 +1,69 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlin.multiplatform) apply true + alias(libs.plugins.android.library) apply true + alias(libs.plugins.compose.multiplatform) apply true + + id("convention.publication") apply true +} + +repositories { + mavenCentral() + google() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +} + +group = "in.procyk.compose" +version = libs.versions.compose.extensions.get() + +kotlin { + jvm("desktop") + + androidTarget { + publishLibraryVariants("release") + } + iosX64() + iosArm64() + iosSimulatorArm64() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + } + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain.dependencies { + implementation(compose.ui) + implementation(compose.runtime) + implementation(compose.foundation) + } + val wasmJsMain by getting + val desktopMain by getting + create("nonAndroidMain") { + dependsOn(commonMain.get()) + + wasmJsMain.dependsOn(this) + desktopMain.dependsOn(this) + iosMain.get().dependsOn(this) + } + } +} + +android { + compileSdk = libs.versions.android.compileSdk.get().toInt() + namespace = "in.procyk.compose.qrcode" + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} diff --git a/qr-code/src/androidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt b/qr-code/src/androidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt new file mode 100644 index 0000000..941707f --- /dev/null +++ b/qr-code/src/androidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt @@ -0,0 +1,16 @@ +package `in`.procyk.compose.qrcode + +import android.graphics.Bitmap +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import java.io.ByteArrayOutputStream + +actual fun ImageBitmap.toByteArray(format: ImageFormat): ByteArray = ByteArrayOutputStream().use { + asAndroidBitmap().compress(format.toCompressFormat(), 100, it) + it.toByteArray() +} + +private fun ImageFormat.toCompressFormat(): Bitmap.CompressFormat = when (this) { + ImageFormat.PNG -> Bitmap.CompressFormat.PNG + ImageFormat.JPEG -> Bitmap.CompressFormat.JPEG +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/Converters.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/Converters.kt new file mode 100644 index 0000000..432c830 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/Converters.kt @@ -0,0 +1,66 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +/** + * Converts [Painter] to image with desired [width], [height] and [format] and returns its bytes. + * */ +fun Painter.toByteArray(width : Int, height: Int, format : ImageFormat = ImageFormat.PNG) : ByteArray = + toImageBitmap(width, height).toByteArray(format) + +/** + * Converts [Painter] to [ImageBitmap] with desired [width], [height], [alpha] and [colorFilter] + * */ +fun Painter.toImageBitmap( + width : Int, + height : Int, + alpha : Float = 1f, + colorFilter: ColorFilter? = null +) : ImageBitmap { + + val bmp = ImageBitmap(width, height) + val canvas = Canvas(bmp) + + CanvasDrawScope().draw( + density = Density(1f, 1f), + layoutDirection = LayoutDirection.Ltr, + canvas = canvas, + size = Size(width.toFloat(), height.toFloat()) + ) { + draw(this@draw.size, alpha, colorFilter) + } + + return bmp +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/DrawCache.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/DrawCache.kt new file mode 100644 index 0000000..45a1b29 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/DrawCache.kt @@ -0,0 +1,111 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode + +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.toSize + +/** + * Creates a drawing environment that directs its drawing commands to an [ImageBitmap] + * which can be drawn directly in another [DrawScope] instance. This is useful to cache + * complicated drawing commands across frames especially if the content has not changed. + * Additionally some drawing operations such as rendering paths are done purely in + * software so it is beneficial to cache the result and render the contents + * directly through a texture as done by [DrawScope.drawImage] + */ +internal class DrawCache { + + @PublishedApi internal var mCachedImage: ImageBitmap? = null + private var cachedCanvas: Canvas? = null + private var scopeDensity: Density? = null + private var layoutDirection: LayoutDirection = LayoutDirection.Ltr + private var size: IntSize = IntSize.Zero + + private val cacheScope = CanvasDrawScope() + + /** + * Draw the contents of the lambda with receiver scope into an [ImageBitmap] with the provided + * size. If the same size is provided across calls, the same [ImageBitmap] instance is + * re-used and the contents are cleared out before drawing content in it again + */ + fun drawCachedImage( + size: IntSize, + density: Density, + layoutDirection: LayoutDirection, + block: DrawScope.() -> Unit + ) { + this.scopeDensity = density + this.layoutDirection = layoutDirection + var targetImage = mCachedImage + var targetCanvas = cachedCanvas + if (targetImage == null || + targetCanvas == null || + size.width > targetImage.width || + size.height > targetImage.height + ) { + targetImage = ImageBitmap(size.width, size.height) + targetCanvas = Canvas(targetImage) + + mCachedImage = targetImage + cachedCanvas = targetCanvas + } + this.size = size + cacheScope.draw(density, layoutDirection, targetCanvas, size.toSize()) { + clear() + block() + } + targetImage.prepareToDraw() + } + + /** + * Draw the cached content into the provided [DrawScope] instance + */ + fun drawInto( + target: DrawScope, + alpha: Float = 1.0f, + colorFilter: ColorFilter? = null + ) { + val targetImage = mCachedImage + check(targetImage != null) { + "drawCachedImage must be invoked first before attempting to draw the result " + + "into another destination" + } + target.drawImage(targetImage, srcSize = size, alpha = alpha, colorFilter = colorFilter) + } + + /** + * Helper method to clear contents of the draw environment from the given bounds of the + * DrawScope + */ + private fun DrawScope.clear() { + drawRect(color = Color.Black, blendMode = BlendMode.Clear) + } +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt new file mode 100644 index 0000000..45176db --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt @@ -0,0 +1,34 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode + +import androidx.compose.ui.graphics.ImageBitmap + +enum class ImageFormat { + PNG, JPEG +} + +expect fun ImageBitmap.toByteArray(format: ImageFormat = ImageFormat.PNG) : ByteArray diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCode.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCode.kt new file mode 100644 index 0000000..85fd41e --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCode.kt @@ -0,0 +1,223 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode + +import `in`.procyk.compose.qrcode.QRCodeDataType.* +import `in`.procyk.compose.qrcode.internal.* +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.applyMaskPattern +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.setupBottomLeftPositionProbePattern +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.setupPositionAdjustPattern +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.setupTimingPattern +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.setupTopLeftPositionProbePattern +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.setupTopRightPositionProbePattern +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.setupTypeInfo +import `in`.procyk.compose.qrcode.internal.QRCodeSetup.setupTypeNumber +import `in`.procyk.compose.qrcode.options.QrCodeMatrix +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +internal class QRCode @JvmOverloads constructor( + private val data: String, + private val errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.M, + private val dataType: QRCodeDataType = QRUtil.getDataType(data) +) { + private val qrCodeData: QRData = when (dataType) { + NUMBERS -> QRNumber(data) + UPPER_ALPHA_NUM -> QRAlphaNum(data) + DEFAULT -> QR8BitByte(data) + } + + companion object { + const val DEFAULT_CELL_SIZE = 1 + private const val PAD0 = 0xEC + private const val PAD1 = 0x11 + + /** + * Calculates a suitable value for the [dataType] field for you. + */ + @JvmStatic + @JvmOverloads + fun typeForDataAndECL( + data: String, + errorCorrectionLevel: ErrorCorrectionLevel, + dataType: QRCodeDataType = QRUtil.getDataType(data) + ): Int { + val qrCodeData = when (dataType) { + NUMBERS -> QRNumber(data) + UPPER_ALPHA_NUM -> QRAlphaNum(data) + DEFAULT -> QR8BitByte(data) + } + val dataLength = qrCodeData.length() + + for (typeNum in 1 until errorCorrectionLevel.maxTypeNum) { + if (dataLength <= QRUtil.getMaxLength(typeNum, dataType, errorCorrectionLevel)) { + return typeNum + } + } + + return 40 + } + } + + + + @JvmOverloads + fun encode( + type: Int = typeForDataAndECL(data, errorCorrectionLevel), + maskPattern: MaskPattern = MaskPattern.PATTERN000 + ): QrCodeMatrix { + val moduleCount = type * 4 + 17 + val modules: Array> = + Array(moduleCount) { Array(moduleCount) { null } } + + setupTopLeftPositionProbePattern(modules) + setupTopRightPositionProbePattern(modules) + setupBottomLeftPositionProbePattern(modules) + + setupPositionAdjustPattern(type, modules) + setupTimingPattern(moduleCount, modules) + setupTypeInfo(errorCorrectionLevel, maskPattern, moduleCount, modules) + + if (type >= 7) { + setupTypeNumber(type, moduleCount, modules) + } + + val data = createData(type) + + applyMaskPattern(data, maskPattern, moduleCount, modules) + + return QrCodeMatrix( + modules.map { + it.map { pixel -> + if (pixel?.dark == true) + QrCodeMatrix.PixelType.DarkPixel + else QrCodeMatrix.PixelType.LightPixel + } + } + ) + } + + private fun createData(type: Int): IntArray { + val rsBlocks = RSBlock.getRSBlocks(type, errorCorrectionLevel) + val buffer = BitBuffer() + + buffer.put(qrCodeData.dataType.value, 4) + buffer.put(qrCodeData.length(), qrCodeData.getLengthInBits(type)) + qrCodeData.write(buffer) + + val totalDataCount = rsBlocks.sumOf { it.dataCount } * 8 + + if (buffer.lengthInBits > totalDataCount) { + throw IllegalArgumentException("Code length overflow (${buffer.lengthInBits} > $totalDataCount)") + } + + if (buffer.lengthInBits + 4 <= totalDataCount) { + buffer.put(0, 4) + } + + while (buffer.lengthInBits % 8 != 0) { + buffer.put(false) + } + + while (true) { + if (buffer.lengthInBits >= totalDataCount) { + break + } + + buffer.put(PAD0, 8) + + if (buffer.lengthInBits >= totalDataCount) { + break + } + + buffer.put(PAD1, 8) + } + + return createBytes(buffer, rsBlocks) + } + + private fun createBytes(buffer: BitBuffer, rsBlocks: Array): IntArray { + var offset = 0 + var maxDcCount = 0 + var maxEcCount = 0 + var totalCodeCount = 0 + val dcData = Array(rsBlocks.size) { IntArray(0) } + val ecData = Array(rsBlocks.size) { IntArray(0) } + + rsBlocks.forEachIndexed { i, it -> + val dcCount = it.dataCount + val ecCount = it.totalCount - dcCount + + totalCodeCount += it.totalCount + maxDcCount = maxDcCount.coerceAtLeast(dcCount) + maxEcCount = maxEcCount.coerceAtLeast(ecCount) + + // Init dcData[i] + dcData[i] = IntArray(dcCount) { idx -> 0xff and buffer.buffer[idx + offset] } + offset += dcCount + + // Init ecData[i] + val rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount) + val rawPoly = Polynomial(dcData[i], rsPoly.len() - 1) + val modPoly = rawPoly.mod(rsPoly) + val ecDataSize = rsPoly.len() - 1 + + ecData[i] = IntArray(ecDataSize) { idx -> + val modIndex = idx + modPoly.len() - ecDataSize + if ((modIndex >= 0)) modPoly[modIndex] else 0 + } + } + + var index = 0 + val data = IntArray(totalCodeCount) + + for (i in 0 until maxDcCount) { + for (r in rsBlocks.indices) { + if (i < dcData[r].size) { + data[index++] = dcData[r][i] + } + } + } + + for (i in 0 until maxEcCount) { + for (r in rsBlocks.indices) { + if (i < ecData[r].size) { + data[index++] = ecData[r][i] + } + } + } + + return data + } + + override fun toString(): String = + "QRCode(data=$data" + + ", errorCorrectionLevel=$errorCorrectionLevel" + + ", dataType=$dataType" + + ", qrCodeData=${qrCodeData::class.simpleName}" + + ")" +} + diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCodeEnums.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCodeEnums.kt new file mode 100644 index 0000000..34e1c1e --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QRCodeEnums.kt @@ -0,0 +1,95 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode + + +/** + * The level of Error Correction to apply to the QR Code image. The Higher the Error Correction, the lower quality + * **print** the QRCode can be (think of "wow, even with the paper a bit crumpled, it still read the QR Code!" - that + * is likely a [Q] or [H] error correction). + * + * The trade-off is the amount of data you can encode. The higher the error correction level, the less amount of data + * you'll be able to encode. + * + * Please consult [Kazuhiko's Online Demo](https://kazuhikoarase.github.io/qrcode-generator/js/demo/) where at the time + * of writing a handy table showed how many bytes can be encoded given a data type ([QRCodeDataType]) and Error Correction Level. + * + * This library automatically tries to fit ~2048 bytes into the QR Code regardless of error correction level. That is + * the reason and meaning of [maxTypeNum]. + * + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/ErrorCorrectionLevel.java) + * + * @param value Value associated with this error correction level + * @param maxTypeNum Maximum `type` value which can fit 2048 bytes. Used to automatically calculate the `type` value. + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal enum class ErrorCorrectionLevel(val value: Int, val maxTypeNum: Int) { + L(1, 21), + M(0, 25), + Q(3, 30), + H(2, 34) +} + +/** + * Patterns to apply to the QRCode. They change how the QRCode looks in the end. + * + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/MaskPattern.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal enum class MaskPattern { + /** This is the default pattern (no pattern is applied) */ + PATTERN000, + PATTERN001, + PATTERN010, + PATTERN011, + PATTERN100, + PATTERN101, + PATTERN110, + PATTERN111 +} + +/** + * QRCode Modes. Basically represents which kind of data is being encoded. + * + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/Mode.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal enum class QRCodeDataType(val value: Int) { + /** Strictly numerical data. Like huge integers. These can be way bigger than [Long.MAX_VALUE]. */ + NUMBERS(1 shl 0), + + /** Represents Uppercase Alphanumerical data. Allowed characters: `[0-9A-Z $%*+\-./:]`. */ + UPPER_ALPHA_NUM(1 shl 1), + + /** This can be any kind of data. With this you can encode Strings, URLs, images, files, anything. */ + DEFAULT(1 shl 2) +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrCodePainter.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrCodePainter.kt new file mode 100644 index 0000000..6879e88 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrCodePainter.kt @@ -0,0 +1,716 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.IntSize +import `in`.procyk.compose.qrcode.options.* +import `in`.procyk.compose.qrcode.options.dsl.QrOptionsBuilderScope +import kotlin.math.ceil +import kotlin.math.roundToInt + +/** + * Create and remember QR code painter + * + * @param data QR code payload + * @param keys keys of the [options] block. QR code will be re-generated when any key is changed. + * @param options [QrOptions] builder block + * */ +@Composable +fun rememberQrCodePainter( + data : String, + vararg keys : Any?, + options : QrOptionsBuilderScope.() -> Unit +) : QrCodePainter = rememberQrCodePainter( + data = data, + options = remember(keys) { QrOptions(options) } +) + +/** + * Create and remember QR code painter + * + * @param data QR code payload + * @param options QR code styling options + * */ +@Composable +fun rememberQrCodePainter( + data : String, + options : QrOptions +) : QrCodePainter = remember(data, options) { + QrCodePainter(data, options) +} + +@Composable +fun rememberQrCodePainter( + data : String, + shapes: QrShapes = QrShapes(), + colors : QrColors = QrColors(), + logo : QrLogo = QrLogo(), + errorCorrectionLevel: QrErrorCorrectionLevel = QrErrorCorrectionLevel.Auto, + fourEyed : Boolean = false, +) : QrCodePainter = rememberQrCodePainter( + data = data, + options = remember(shapes, colors, logo, errorCorrectionLevel, fourEyed) { + QrOptions( + shapes = shapes, + colors = colors, + logo = logo, + errorCorrectionLevel = errorCorrectionLevel, + fourEyed = fourEyed + ) + } +) + +/** + * Encodes [data] payload and renders it into the compose [Painter] using styling [options] + * */ +@Immutable +class QrCodePainter( + val data : String, + val options: QrOptions = QrOptions(), +) : Painter() { + + private val initialMatrixSize : Int + + private val actualCodeMatrix = options.shapes.code.run { + + val initialMatrix = QRCode( + data = data, + errorCorrectionLevel = + if (options.errorCorrectionLevel == QrErrorCorrectionLevel.Auto) + options.errorCorrectionLevel.fit(options).lvl + else options.errorCorrectionLevel.lvl + ).encode() + + initialMatrixSize = initialMatrix.size + + initialMatrix.transform() + } + + private var codeMatrix = actualCodeMatrix + + override val intrinsicSize: Size = Size( + codeMatrix.size.toFloat() * 10f, + codeMatrix.size.toFloat() * 10f + ) + + private val shapeIncrease = (codeMatrix.size - initialMatrixSize)/2 + + private val balls = mutableListOf( + 2 + shapeIncrease to 2 + shapeIncrease, + 2 + shapeIncrease to initialMatrixSize - 5 + shapeIncrease, + initialMatrixSize - 5 + shapeIncrease to 2 + shapeIncrease + ).apply { + if (options.fourEyed) + this += initialMatrixSize - 5 + shapeIncrease to initialMatrixSize - 5 + shapeIncrease + }.toList() + + private val frames = mutableListOf( + shapeIncrease to shapeIncrease, + shapeIncrease to initialMatrixSize - 7 + shapeIncrease, + initialMatrixSize - 7 + shapeIncrease to shapeIncrease + ).apply { + if (options.fourEyed) { + this += initialMatrixSize - 7 + shapeIncrease to initialMatrixSize - 7 + shapeIncrease + } + }.toList() + + private val shouldSeparateDarkPixels + get() = options.colors.dark.mode == QrBrushMode.Separate + + private val shouldSeparateLightPixels + get() = options.colors.light.mode == QrBrushMode.Separate + + private val shouldSeparateFrames + get() = options.colors.frame.isSpecified || shouldSeparateDarkPixels + + private val shouldSeparateBalls + get() = options.colors.ball.isSpecified || shouldSeparateDarkPixels + + private var colorFilter: ColorFilter? = null + private var alpha: Float = 1f + + private val cacheDrawScope = DrawCache() + private var cachedSize: Size? = null + + override fun toString(): String { + return "QrCodePainter(data = $data)" + } + + override fun hashCode(): Int { + return data.hashCode() * 31 + options.hashCode() + } + + override fun applyAlpha(alpha: Float): Boolean { + this.alpha = alpha + return true + } + + override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { + this.colorFilter = colorFilter + return true + } + + private val DrawScope.logoSize + get() = size * options.logo.size + + private val DrawScope.logoPaddingSize + get() = logoSize.width * (1 + options.logo.padding.size) + + private val DrawScope.pixelSize : Float + get() = minOf(size.width, size.height) / codeMatrix.size + + private val drawBlock: DrawScope.() -> Unit = { draw() } + + private fun DrawScope.draw() { + + val pixelSize = pixelSize + + prepareLogo(pixelSize) + + val (dark, light) = createMainElements(pixelSize) + + if (shouldSeparateDarkPixels || shouldSeparateLightPixels) { + drawSeparatePixels(pixelSize) + } + + if (!shouldSeparateLightPixels) { + drawPath( + path = light, + brush = options.colors.light + .brush(pixelSize * codeMatrix.size, Neighbors.Empty), + ) + } + + if (!shouldSeparateDarkPixels) { + drawPath( + path = dark, + brush = options.colors.dark + .brush(pixelSize * codeMatrix.size, Neighbors.Empty), + ) + } + + if (shouldSeparateFrames) { + drawFrames(pixelSize) + } + + if (shouldSeparateBalls) { + drawBalls(pixelSize) + } + + drawLogo() + } + + + + override fun DrawScope.onDraw() { + + if (cachedSize != size) { + + codeMatrix = actualCodeMatrix.copy() + + cacheDrawScope.drawCachedImage( + size = IntSize(ceil(size.width).toInt(), ceil(size.height).toInt()), + density = this, + layoutDirection = layoutDirection, + block = drawBlock + ) + } + cacheDrawScope.drawInto( + target = this, + alpha = alpha, + colorFilter = colorFilter + ) + } + + + private fun DrawScope.drawSeparatePixels( + pixelSize: Float, + ){ + val darkPaint = darkPaintFactory(pixelSize) + val lightPaint = lightPaintFactory(pixelSize) + + val darkPixelPath = darkPixelPathFactory(pixelSize) + val lightPixelPath = lightPixelPathFactory(pixelSize) + + repeat(codeMatrix.size) { i -> + repeat(codeMatrix.size) inner@{ j -> + if (isInsideFrameOrBall(i, j)) + return@inner + + translate( + left = i * pixelSize, + top = j * pixelSize + ) { + if (shouldSeparateDarkPixels && codeMatrix[i, j] == QrCodeMatrix.PixelType.DarkPixel) { + val n = codeMatrix.neighbors(i, j) + drawPath( + path = darkPixelPath.next(n), + brush = darkPaint.next(n), + ) + } + if (shouldSeparateLightPixels && codeMatrix[i, j] == QrCodeMatrix.PixelType.LightPixel) { + val n = codeMatrix.neighbors(i, j) + + drawPath( + path = lightPixelPath.next(n), + brush = lightPaint.next(n), + ) + } + } + } + } + } + + + private fun DrawScope.prepareLogo(pixelSize: Float) { + + val ps = logoPaddingSize + + if (options.logo.padding is QrLogoPadding.Natural) { + val logoPath = options.logo.shape.newPath( + size = ps, + neighbors = Neighbors.Empty + ).apply { + translate( + Offset( + (size.width - ps) / 2f, + (size.height - ps) / 2f, + ) + ) + } + + val darkPathF = darkPixelPathFactory(pixelSize) + val lightPathF = lightPixelPathFactory(pixelSize) + + val logoPixels = (codeMatrix.size * + options.logo.size.coerceIn(0f, 1f) * + (1 + options.logo.padding.size.coerceIn(0f, 1f))).roundToInt() + 1 + + val xRange = + (codeMatrix.size - logoPixels) / 2 until (codeMatrix.size + logoPixels) / 2 + val yRange = + (codeMatrix.size - logoPixels) / 2 until (codeMatrix.size + logoPixels) / 2 + + for (x in xRange) { + for (y in yRange) { + val neighbors = codeMatrix.neighbors(x, y) + val offset = Offset(x * pixelSize, y * pixelSize) + + val darkPath = darkPathF.next(neighbors).apply { + translate(offset) + } + val lightPath = lightPathF.next(neighbors).apply { + translate(offset) + } + + if ( + codeMatrix[x, y] == QrCodeMatrix.PixelType.DarkPixel && + logoPath.intersects(darkPath) || + codeMatrix[x, y] == QrCodeMatrix.PixelType.LightPixel && + logoPath.intersects(lightPath) + ) { + codeMatrix[x, y] = QrCodeMatrix.PixelType.Logo + } + } + } + } + } + + private fun DrawScope.drawLogo() { + + val ps = logoPaddingSize + + if (options.logo.padding is QrLogoPadding.Accurate){ + val path = options.logo.shape.newPath( + size = ps, + neighbors = Neighbors.Empty + ) + + translate( + left = center.x - ps / 2, + top = center.y - ps / 2 + ) { + drawPath(path, Color.Black, blendMode = BlendMode.Clear) + } + } + + options.logo.painter?.let { + it.run { + translate( + left = center.x - logoSize.width / 2, + top = center.y - logoSize.height / 2 + ) { + draw(logoSize, alpha, colorFilter) + } + } + } + } + + + private fun DrawScope.drawBalls( + pixelSize: Float + ) { + val brush by ballBrushFactory(pixelSize) + val path by ballShapeFactory(pixelSize) + + balls.forEach { + + translate( + it.first * pixelSize, + it.second * pixelSize + ) { + drawPath( + path = path, + brush = brush, + ) + } + } + } + + + private fun DrawScope.drawFrames( + pixelSize: Float + ) { + val ballBrush by frameBrushFactory(pixelSize) + val ballPath by frameShapeFactory(pixelSize) + + frames.forEach { + + translate( + it.first * pixelSize, + it.second * pixelSize + ) { + drawPath( + path = ballPath, + brush = ballBrush, + ) + } + } + } + + private fun createMainElements( + pixelSize: Float + ): Pair { + + val darkPath = Path().apply { + fillType = PathFillType.EvenOdd + } + val lightPath = Path().apply { + fillType = PathFillType.EvenOdd + } + + val rotatedFramePath by frameShapeFactory(pixelSize) + val rotatedBallPath by ballShapeFactory(pixelSize) + + val darkPixelPathFactory = darkPixelPathFactory(pixelSize) + val lightPixelPathFactory = lightPixelPathFactory(pixelSize) + + for (x in 0 until codeMatrix.size) { + for (y in 0 until codeMatrix.size) { + + val neighbors = codeMatrix.neighbors(x, y) + + when { + !shouldSeparateFrames && isFrameStart(x, y) -> + darkPath + .addPath( + path = rotatedFramePath, + offset = Offset(x * pixelSize, y * pixelSize) + ) + + + !shouldSeparateBalls && isBallStart(x, y) -> + darkPath + .addPath( + path = rotatedBallPath, + offset = Offset(x * pixelSize, y * pixelSize) + ) + + isInsideFrameOrBall(x, y) -> Unit + + !shouldSeparateDarkPixels && codeMatrix[x, y] == QrCodeMatrix.PixelType.DarkPixel -> + darkPath + .addPath( + path = darkPixelPathFactory.next(neighbors), + offset = Offset(x * pixelSize, y * pixelSize) + ) + + !shouldSeparateLightPixels && codeMatrix[x, y] == QrCodeMatrix.PixelType.LightPixel -> + lightPath + .addPath( + path = lightPixelPathFactory.next(neighbors), + offset = Offset(x * pixelSize, y * pixelSize) + ) + + } + } + } + return darkPath to lightPath + } + + + private fun isFrameStart(x: Int, y: Int) = + x - shapeIncrease == 0 && y - shapeIncrease == 0 || + x - shapeIncrease == 0 && y - shapeIncrease == initialMatrixSize - 7 || + x - shapeIncrease == initialMatrixSize - 7 && y - shapeIncrease == 0 || + options.fourEyed && x - shapeIncrease == initialMatrixSize - 7 && y - shapeIncrease == initialMatrixSize - 7 + + private fun isBallStart(x: Int, y: Int) = + x - shapeIncrease == 2 && y - shapeIncrease ==initialMatrixSize - 5 || + x - shapeIncrease == initialMatrixSize - 5 && y - shapeIncrease == 2 || + x - shapeIncrease == 2 && y - shapeIncrease == 2 || + options.fourEyed && x - shapeIncrease == initialMatrixSize - 5 && y - shapeIncrease == initialMatrixSize - 5 + + private fun isInsideFrameOrBall(x: Int, y: Int): Boolean { + return x - shapeIncrease in -1..7 && y - shapeIncrease in -1..7 || + x - shapeIncrease in -1..7 && y - shapeIncrease in initialMatrixSize - 8 until initialMatrixSize + 1 || + x - shapeIncrease in initialMatrixSize - 8 until initialMatrixSize + 1 && y - shapeIncrease in -1..7 || + options.fourEyed && x - shapeIncrease in initialMatrixSize - 8 until initialMatrixSize + 1 && y - shapeIncrease in initialMatrixSize - 8 until initialMatrixSize + 1 + } + + private fun darkPaintFactory(pixelSize: Float) = + pixelBrushFactory( + brush = options.colors.dark, + separate = shouldSeparateDarkPixels, + pixelSize = pixelSize + ) + + private fun lightPaintFactory(pixelSize: Float) = + pixelBrushFactory( + brush = options.colors.light, + separate = shouldSeparateLightPixels, + pixelSize = pixelSize + ) + + private fun ballBrushFactory(pixelSize: Float) = + eyeBrushFactory(brush = options.colors.ball, pixelSize = pixelSize) + + private fun frameBrushFactory(pixelSize: Float) = + eyeBrushFactory(brush = options.colors.frame, pixelSize = pixelSize) + + + private fun ballShapeFactory(pixelSize: Float): Lazy = + rotatedPathFactory( + shape = options.shapes.ball, + shapeSize = pixelSize * BALL_SIZE + ) + + private fun frameShapeFactory(pixelSize: Float): Lazy = + rotatedPathFactory( + shape = options.shapes.frame, + shapeSize = pixelSize * FRAME_SIZE + ) + + private fun darkPixelPathFactory(pixelSize: Float) = + pixelPathFactory( + shape = options.shapes.darkPixel, + pixelSize = pixelSize + ) + + private fun lightPixelPathFactory(pixelSize: Float) = + pixelPathFactory( + shape = options.shapes.lightPixel, + pixelSize = pixelSize + ) + + private fun pixelPathFactory( + shape : QrShapeModifier, + pixelSize: Float + ) : NeighborsBasedFactory { + val path = Path() + + return NeighborsBasedFactory { + path.rewind() + path.apply { + shape.run { + path(pixelSize, it) + } + } + path + } + } + + private fun rotatedPathFactory( + shape: QrShapeModifier, + shapeSize: Float, + ): Lazy { + + var number = 0 + + val path = Path() + + val factory = NeighborsBasedFactory { + path.apply { + rewind() + fillType = PathFillType.EvenOdd + shape.run { path(shapeSize, it) } + } + } + + return Recreating { + factory.next(Neighbors.forEyeWithNumber(number, options.fourEyed)).apply { + if (options.shapes.centralSymmetry) { + val angle = when (number) { + 0 -> 0f + 1 -> -90f + 2 -> 90f + else -> 180f + } + rotate(angle, Offset(shapeSize/2, shapeSize/2)) + } + }.also { + number = (number + 1) % if (options.fourEyed) 4 else 3 + } + } + } + + + private fun eyeBrushFactory( + brush: QrBrush, + pixelSize: Float + ): Lazy { + val b = brush + .takeIf { it.isSpecified } + ?: QrBrush.Default + + var number = 0 + + val factory = { + b.brush( + size = pixelSize, + neighbors = Neighbors.forEyeWithNumber(number, options.fourEyed) + ).also { + number = (number + 1) % if (options.fourEyed) 4 else 3 + } + } + + return Recreating(factory) + } + + private fun pixelBrushFactory( + brush: QrBrush, + separate: Boolean, + pixelSize: Float, + ): NeighborsBasedFactory { + + val size = if (separate) + pixelSize + else codeMatrix.size * pixelSize + + val joinBrush by lazy { brush.brush(size, Neighbors.Empty) } + + return NeighborsBasedFactory { + if (separate) + brush.brush(size, it) + else joinBrush + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as QrCodePainter + + if (data != other.data) return false + if (options != other.options) return false + + return true + } +} + +private const val BALL_SIZE = 3 +private const val FRAME_SIZE = 7 + + +private fun Path.rotate(degree: Float, pivot : Offset) { + translate(-pivot) + transform(Matrix().apply { rotateZ(degree) }) + translate(pivot) +} + +private fun Path.intersects(other : Path) = + Path.combine( + operation = PathOperation.Intersect, + path1 = this, + path2 = other + ).isEmpty.not() + +private class Recreating( + private val factory : () -> T +) : Lazy { + override val value: T + get() = factory() + + override fun isInitialized(): Boolean = true +} + +private fun Neighbors.Companion.forEyeWithNumber(number : Int, fourthEyeEnabled : Boolean) : Neighbors { + return when (number) { + 0 -> Neighbors(bottom = true, right = true, bottomRight = fourthEyeEnabled) + 1 -> Neighbors(bottom = fourthEyeEnabled, left = true, bottomLeft = true) + 2 -> Neighbors(top = true, topRight = true, right = fourthEyeEnabled) + 3 -> Neighbors(top = true, left = true, topLeft = true) + + else -> throw IllegalStateException("Incorrect eye number: $number") + } +} + +private fun interface NeighborsBasedFactory { + fun next(neighbors: Neighbors) : T +} + + +private fun QrErrorCorrectionLevel.fit( + options: QrOptions +) : QrErrorCorrectionLevel { + + val logoSize = options.logo.size* + (1 + options.logo.padding.size) //* +// options.shapes.code.shapeSizeIncrease + + val hasLogo = options.logo.padding != QrLogoPadding.Empty + + return if (this == QrErrorCorrectionLevel.Auto) + when { + !hasLogo -> QrErrorCorrectionLevel.Low + logoSize > .3 -> QrErrorCorrectionLevel.High + logoSize in .2 .. .3 && lvl < ErrorCorrectionLevel.Q -> + QrErrorCorrectionLevel.MediumHigh + logoSize > .05f && lvl < ErrorCorrectionLevel.M -> + QrErrorCorrectionLevel.Medium + else -> this + } else this +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrData.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrData.kt new file mode 100644 index 0000000..3cf1014 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/QrData.kt @@ -0,0 +1,246 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode + +object QrData { + fun text(text: String): String = text + + fun phone(phoneNumber: String): String = "TEL:$phoneNumber" + + fun email( + email: String, + copyTo: String? = null, + subject: String? = null, + body: String? = null, + ): String = buildString { + append("mailto:$email") + + if (listOf(copyTo, subject, body).any { it.isNullOrEmpty().not() }) { + append("?") + } + val querries = buildList { + if (copyTo.isNullOrEmpty().not()) { + add("cc=$copyTo") + } + if (subject.isNullOrEmpty().not()) { + add("subject=${escape(subject!!)}") + } + if (body.isNullOrEmpty().not()) { + add("body=${escape(body!!)}") + } + } + append(querries.joinToString(separator = "&")) + } + + fun sms( + phoneNumber: String, + subject: String, + isMMS: Boolean, + ): String = "${if (isMMS) "MMS" else "SMS"}:" + + "$phoneNumber${if (subject.isNotEmpty()) ":$subject" else ""}" + + + fun meCard( + name: String? = null, + address: String? = null, + phoneNumber: String? = null, + email: String? = null, + ): String = buildString { + append("MECARD:") + if (name != null) + append("N:$name;") + + if (address != null) + append("ADR:$address;") + + if (phoneNumber != null) + append("TEL:$phoneNumber;") + + if (email != null) + append("EMAIL:$email;") + + append(";") + } + + fun vCard( + name: String? = null, + company: String? = null, + title: String? = null, + phoneNumber: String? = null, + email: String? = null, + address: String? = null, + website: String? = null, + note: String? = null, + ): String = buildString { + append("BEGIN:VCARD\n") + append("VERSION:3.0\n") + if (name != null) + append("N:$name\n") + + if (company != null) + append("ORG:$company\n") + + if (title != null) + append("TITLE$title\n") + + if (phoneNumber != null) + append("TEL:$phoneNumber\n") + + if (website != null) + append("URL:$website\n") + + if (email != null) + append("EMAIL:$email\n") + + if (address != null) + append("ADR:$address\n") + + if (note != null) { + append("NOTE:$note\n") + } + append("END:VCARD") + } + + fun bizCard( + firstName: String? = null, + secondName: String? = null, + job: String? = null, + company: String? = null, + address: String? = null, + phone: String? = null, + email: String? = null, + ): String = buildString { + append("BIZCARD:") + if (firstName != null) + append("N:$firstName;") + + if (secondName != null) + append("X:$secondName;") + + if (job != null) + append("T:$job;") + + if (company != null) + append("C:$company;") + + if (address != null) + append("A:$address;") + + if (phone != null) + append("B:$phone;") + + if (email != null) + append("E:$email;") + + append(";") + } + + fun wifi( + authentication: String? = null, + ssid: String? = null, + psk: String? = null, + hidden: Boolean = false, + ): String = buildString { + append("WIFI:") + if (ssid != null) + append("S:${escape(ssid)};") + + if (authentication != null) + append("T:${authentication};") + + if (psk != null) + append("P:${escape(psk)};") + + append("H:$hidden;") + } + + fun enterpriseWifi( + ssid: String? = null, + psk: String? = null, + hidden: Boolean = false, + user: String? = null, + eap: String? = null, + phase: String? = null, + ): String = buildString { + append("WIFI:") + if (ssid != null) + append("S:${escape(ssid)};") + + if (user != null) + append("U:${escape(user)};") + + if (psk != null) + append("P:${escape(psk)};") + + if (eap != null) + append("E:${escape(eap)};") + + if (phase != null) + append("PH:${escape(phase)};") + + append("H:$hidden;") + } + + + fun event( + uid: String? = null, + stamp: String? = null, + organizer: String? = null, + start: String? = null, + end: String? = null, + summary: String? = null, + ): String = buildString { + append("BEGIN:VEVENT\n") + if (uid != null) + append("UID:$uid\n") + if (stamp != null) + append("DTSTAMP:$stamp\n") + if (organizer != null) + append("ORGANIZER:$organizer\n") + + if (start != null) + append("DTSTART:$start\n") + + if (end != null) + append("DTEND:$end\n") + if (summary != null) + append("SUMMARY:$summary\n") + + append("END:VEVENT") + } + + fun location( + lat: Float, + lon: Float, + ): String = "GEO:$lat,$lon" +} + +private fun escape(text: String): String = text.replace("\\", "\\\\") + .replace(",", "\\,") + .replace(";", "\\;") + .replace(".", "\\.") + .replace("\"", "\\\"") + .replace("'", "\\'") \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/BitBuffer.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/BitBuffer.kt new file mode 100644 index 0000000..909b666 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/BitBuffer.kt @@ -0,0 +1,72 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.internal + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/BitBuffer.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal class BitBuffer { + var buffer: IntArray + private set + var lengthInBits: Int + private set + private val increments = 32 + + private operator fun get(index: Int): Boolean = + buffer[index / 8] ushr 7 - index % 8 and 1 == 1 + + fun put(num: Int, length: Int) { + for (i in 0.. + result[i] = result[i] xor gexp(glog(it) + ratio) + } + + Polynomial(result).mod(other) + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSetup.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSetup.kt new file mode 100644 index 0000000..a206878 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSetup.kt @@ -0,0 +1,304 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.internal + +import `in`.procyk.compose.qrcode.ErrorCorrectionLevel +import `in`.procyk.compose.qrcode.MaskPattern +import `in`.procyk.compose.qrcode.internal.QRCodeRegion.* +import `in`.procyk.compose.qrcode.internal.QRCodeSquareType.POSITION_PROBE + +/** + * Object with helper methods and constants to setup stuff into the QRCode such as Position Probes and Timing Probes. + * + * @author Rafael Lins - g0dkar + */ +internal object QRCodeSetup { + private const val DEFAULT_PROBE_SIZE = 7 + + fun setupTopLeftPositionProbePattern( + modules: Array>, + probeSize: Int = DEFAULT_PROBE_SIZE + ) { + setupPositionProbePattern(0, 0, modules, probeSize) + } + + fun setupTopRightPositionProbePattern( + modules: Array>, + probeSize: Int = DEFAULT_PROBE_SIZE + ) { + setupPositionProbePattern(modules.size - probeSize, 0, modules, probeSize) + } + + fun setupBottomLeftPositionProbePattern( + modules: Array>, + probeSize: Int = DEFAULT_PROBE_SIZE + ) { + setupPositionProbePattern(0, modules.size - probeSize, modules, probeSize) + } + + fun setupPositionProbePattern( + rowOffset: Int, + colOffset: Int, + modules: Array>, + probeSize: Int = DEFAULT_PROBE_SIZE + ) { + val modulesSize = modules.size + + for (row in -1..probeSize) { + for (col in -1..probeSize) { + if (!isInsideModules(row, rowOffset, col, colOffset, modulesSize)) { + continue + } + + val isDark = isTopBottomRowSquare(row, col, probeSize) || + isLeftRightColSquare(row, col, probeSize) || + isMidSquare(row, col, probeSize) + + val region = findSquareRegion(row, col, probeSize) + + modules[row + rowOffset][col + colOffset] = QRCodeSquare( + dark = isDark, + row = row + rowOffset, + col = col + colOffset, + squareInfo = QRCodeSquareInfo(POSITION_PROBE, region), + moduleSize = modulesSize + ) + } + } + } + + private fun isInsideModules(row: Int, rowOffset: Int, col: Int, colOffset: Int, modulesSize: Int) = + row + rowOffset in 0.. when (col) { // 0 x ?: ┌───┐ + 0 -> TOP_LEFT_CORNER // 0 x 0: ┌ + probeSize - 1 -> TOP_RIGHT_CORNER // 0 x MAX: ┐ + probeSize -> MARGIN // Outside boundaries + else -> TOP_MID // between: ─ + } + + probeSize - 1 -> when (col) { // MAX x ?: └───┘ + 0 -> BOTTOM_LEFT_CORNER // MAX x 0: └ + probeSize - 1 -> BOTTOM_RIGHT_CORNER // MAX x MAX: ┘ + probeSize -> MARGIN // Outside boundaries + else -> BOTTOM_MID // between: ─ + } + + probeSize -> MARGIN // Outside boundaries + else -> when (col) { // Inside boundaries but not in any edge + 0 -> LEFT_MID + probeSize - 1 -> RIGHT_MID + probeSize -> MARGIN // Outside boundaries + else -> CENTER // Middle/Center square + } + } + + fun setupPositionAdjustPattern(type: Int, modules: Array>) { + val pos = QRUtil.getPatternPosition(type) + + for (i in pos.indices) { + for (j in pos.indices) { + val row = pos[i] + val col = pos[j] + + if (modules[row][col] != null) { + continue + } + + for (r in -2..2) { + for (c in -2..2) { + modules[row + r][col + c] = QRCodeSquare( + dark = r == -2 || r == 2 || c == -2 || c == 2 || r == 0 && c == 0, + row = row + r, + col = col + c, + squareInfo = QRCodeSquareInfo(QRCodeSquareType.POSITION_ADJUST, QRCodeRegion.UNKNOWN), + moduleSize = modules.size + ) + } + } + } + } + } + + fun setupTimingPattern(moduleCount: Int, modules: Array>) { + for (r in 8..> + ) { + val data = errorCorrectionLevel.value shl 3 or maskPattern.ordinal + val bits = QRUtil.getBCHTypeInfo(data) + + for (i in 0..14) { + val mod = bits shr i and 1 == 1 + + if (i < 6) { + set(i, 8, mod, modules) + } else if (i < 8) { + set(i + 1, 8, mod, modules) + } else { + set(moduleCount - 15 + i, 8, mod, modules) + } + } + + for (i in 0..14) { + val mod = bits shr i and 1 == 1 + + if (i < 8) { + set(8, moduleCount - i - 1, mod, modules) + } else if (i < 9) { + set(8, 15 - i, mod, modules) + } else { + set(8, 15 - i - 1, mod, modules) + } + } + + set(moduleCount - 8, 8, true, modules) + } + + fun setupTypeNumber(type: Int, moduleCount: Int, modules: Array>) { + val bits = QRUtil.getBCHTypeNumber(type) + + for (i in 0..17) { + val mod = bits shr i and 1 == 1 + set(i / 3, i % 3 + moduleCount - 8 - 3, mod, modules) + } + + for (i in 0..17) { + val mod = bits shr i and 1 == 1 + set(i % 3 + moduleCount - 8 - 3, i / 3, mod, modules) + } + } + + fun applyMaskPattern( + data: IntArray, + maskPattern: MaskPattern, + moduleCount: Int, + modules: Array> + ) { + var inc = -1 + var bitIndex = 7 + var byteIndex = 0 + var row = moduleCount - 1 + var col = moduleCount - 1 + + while (col > 0) { + if (col == 6) { + col-- + } + + while (true) { + for (c in 0..1) { + if (modules[row][col - c] == null) { + var dark = false + + if (byteIndex < data.size) { + dark = (data[byteIndex] ushr bitIndex) and 1 == 1 + } + + val mask = QRUtil.getMask(maskPattern, row, col - c) + if (mask) { + dark = !dark + } + + set(row, col - c, dark, modules) + + bitIndex-- + if (bitIndex == -1) { + byteIndex++ + bitIndex = 7 + } + } + } + + row += inc + if (row < 0 || moduleCount <= row) { + row -= inc + inc = -inc + break + } + } + + col -= 2 + } + } + + private fun set(row: Int, col: Int, value: Boolean, modules: Array>) { + val qrCodeSquare = modules[row][col] + + if (qrCodeSquare != null) { + qrCodeSquare.dark = value + } else { + modules[row][col] = QRCodeSquare( + dark = value, + row = row, + col = col, + moduleSize = modules.size + ) + } + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSquare.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSquare.kt new file mode 100644 index 0000000..67da9ca --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRCodeSquare.kt @@ -0,0 +1,121 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.internal + +import `in`.procyk.compose.qrcode.internal.QRCodeRegion.* +import `in`.procyk.compose.qrcode.internal.QRCodeSquareType.DEFAULT +import `in`.procyk.compose.qrcode.internal.QRCodeSquareType.MARGIN + +/** + * Represents a single QRCode square unit. It has information about its "color" (either dark or bright), + * its position (row and column) and what it represents. + * + * It can be part of a position probe (aka those big squares at the extremities), part of a position + * adjustment square, part of a timing pattern or just another square as any other :) + * + * @author Rafael Lins - g0dkar + */ +internal class QRCodeSquare( + /** Is this a painted square? */ + var dark: Boolean, + /** The row (top-to-bottom) that this square represents. */ + val row: Int, + /** The column (left-to-right) that this square represents. */ + val col: Int, + /** How big is the whole QRCode matrix? (e.g. if this is "16" then this is part of a 16x16 matrix) */ + val moduleSize: Int, + /** What does this square represent within the QRCode? */ + val squareInfo: QRCodeSquareInfo = QRCodeSquareInfo(DEFAULT, UNKNOWN) +) + +/** + * Returns information on the square itself. It has the [type] of square and its [region] within its relative type. + * + * For example, if `type = POSITION_PROBE` then [region] will represent where within the Position Probe this square + * is positioned. A [region] of [QRCodeRegion.TOP_LEFT_CORNER] for example represents the top left corner of the + * position probe this particular square is part of (a QRCode have 3 position probes). + */ +internal class QRCodeSquareInfo( + private val type: QRCodeSquareType, + private val region: QRCodeRegion +) { + companion object { + internal fun margin() = QRCodeSquareInfo(MARGIN, QRCodeRegion.MARGIN) + } +} + +/** + * The types available for squares in a QRCode. + * + * @author Rafael Lins - g0dkar + */ +internal enum class QRCodeSquareType { + /** Part of a position probe: one of those big squares at the extremities of the QRCode. */ + POSITION_PROBE, + + /** Part of a position adjustment pattern: just like a position probe, but much smaller. */ + POSITION_ADJUST, + + /** Part of the timing pattern. Make it a square like any other :) */ + TIMING_PATTERN, + + /** Anything special. Just a square. */ + DEFAULT, + + /** Used to point out that this is part of the margin. */ + MARGIN +} + +/** + * Represents which part/region of a given square type a particular, single square is. + * + * For example, a position probe is visually composed of multiple squares that form a bigger one. + * + * For example, this is what a position probe normally looks like (squares spaced for ease of understanding): + * + * ``` + * A■■■■B + * ■ ■■ ■ + * ■ ■■ ■ + * C■■■■D + * ``` + * + * The positions marked with `A`, `B`, `C` and `D` would be regions [TOP_LEFT_CORNER], [TOP_RIGHT_CORNER], + * [BOTTOM_LEFT_CORNER] and [BOTTOM_RIGHT_CORNER] respectively. + */ +internal enum class QRCodeRegion { + TOP_LEFT_CORNER, + TOP_RIGHT_CORNER, + TOP_MID, + LEFT_MID, + RIGHT_MID, + CENTER, + BOTTOM_LEFT_CORNER, + BOTTOM_RIGHT_CORNER, + BOTTOM_MID, + MARGIN, + UNKNOWN +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRData.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRData.kt new file mode 100644 index 0000000..e01bfb7 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRData.kt @@ -0,0 +1,161 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.internal + +import `in`.procyk.compose.qrcode.QRCodeDataType +import `in`.procyk.compose.qrcode.QRCodeDataType.* + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/QRData.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal abstract class QRData(val dataType: QRCodeDataType, val data: String) { + abstract fun length(): Int + + abstract fun write(buffer: BitBuffer) + + fun getLengthInBits(type: Int): Int = + when (type) { + in 1..9 -> { + when (dataType) { + NUMBERS -> 10 + UPPER_ALPHA_NUM -> 9 + DEFAULT -> 8 + } + } + in 1..26 -> { + when (dataType) { + NUMBERS -> 12 + UPPER_ALPHA_NUM -> 11 + DEFAULT -> 16 + } + } + in 1..40 -> { + when (dataType) { + NUMBERS -> 14 + UPPER_ALPHA_NUM -> 13 + DEFAULT -> 16 + } + } + else -> { + throw IllegalArgumentException("'type' must be greater than 0 and cannot be greater than 40: $type") + } + } +} + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/QR8BitByte.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal class QR8BitByte(data: String) : QRData(DEFAULT, data) { + private val dataBytes = data.encodeToByteArray() + + override fun write(buffer: BitBuffer) { + for (i in dataBytes.indices) { + buffer.put(dataBytes[i].toInt(), 8) + } + } + + override fun length(): Int = + dataBytes.size +} + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/QRAlphaNum.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal class QRAlphaNum(data: String) : QRData(UPPER_ALPHA_NUM, data) { + override fun write(buffer: BitBuffer) { + var i = 0 + val dataLength = data.length + while (i + 1 < dataLength) { + buffer.put(charCode(data[i]) * 45 + charCode(data[i + 1]), 11) + i += 2 + } + if (i < dataLength) { + buffer.put(charCode(data[i]), 6) + } + } + + override fun length(): Int = data.length + + private fun charCode(c: Char): Int = + when (c) { + in '0'..'9' -> c - '0' + in 'A'..'Z' -> c - 'A' + 10 + else -> { + when (c) { + ' ' -> 36 + '$' -> 37 + '%' -> 38 + '*' -> 39 + '+' -> 40 + '-' -> 41 + '.' -> 42 + '/' -> 43 + ':' -> 44 + else -> throw IllegalArgumentException("Illegal character: $c") + } + } + } +} + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/QRNumber.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal class QRNumber(data: String) : QRData(NUMBERS, data) { + override fun write(buffer: BitBuffer) { + var i = 0 + val len = length() + + while (i + 2 < len) { + val num = data.substring(i, i + 3).toInt() + buffer.put(num, 10) + i += 3 + } + + if (i < len) { + if (len - i == 1) { + val num = data.substring(i, i + 1).toInt() + buffer.put(num, 4) + } else if (len - i == 2) { + val num = data.substring(i, i + 2).toInt() + buffer.put(num, 7) + } + } + } + + override fun length(): Int = data.length +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRMath.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRMath.kt new file mode 100644 index 0000000..8c68f3c --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRMath.kt @@ -0,0 +1,69 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.internal + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/QRMath.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal object QRMath { + private val EXP_TABLE = IntArray(256) + private val LOG_TABLE = IntArray(256) + + fun glog(n: Int): Int = LOG_TABLE[n] + + fun gexp(n: Int): Int { + var i = n + while (i < 0) { + i += 255 + } + while (i >= 256) { + i -= 255 + } + return EXP_TABLE[i] + } + + init { + for (i in 0..7) { + EXP_TABLE[i] = 1 shl i + } + + for (i in 8..255) { + EXP_TABLE[i] = ( + EXP_TABLE[i - 4] + xor EXP_TABLE[i - 5] + xor EXP_TABLE[i - 6] + xor EXP_TABLE[i - 8] + ) + } + + for (i in 0..254) { + LOG_TABLE[EXP_TABLE[i]] = i + } + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRUtil.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRUtil.kt new file mode 100644 index 0000000..b87f1b6 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/QRUtil.kt @@ -0,0 +1,375 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.internal + +import `in`.procyk.compose.qrcode.ErrorCorrectionLevel +import `in`.procyk.compose.qrcode.MaskPattern +import `in`.procyk.compose.qrcode.MaskPattern.* +import `in`.procyk.compose.qrcode.QRCodeDataType + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/QRUtil.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal object QRUtil { + fun getPatternPosition(typeNumber: Int): IntArray = PATTERN_POSITION_TABLE[typeNumber - 1] + + private val PATTERN_POSITION_TABLE = arrayOf( + intArrayOf(), + intArrayOf(6, 18), + intArrayOf(6, 22), + intArrayOf(6, 26), + intArrayOf(6, 30), + intArrayOf(6, 34), + intArrayOf(6, 22, 38), + intArrayOf(6, 24, 42), + intArrayOf(6, 26, 46), + intArrayOf(6, 28, 50), + intArrayOf(6, 30, 54), + intArrayOf(6, 32, 58), + intArrayOf(6, 34, 62), + intArrayOf(6, 26, 46, 66), + intArrayOf(6, 26, 48, 70), + intArrayOf(6, 26, 50, 74), + intArrayOf(6, 30, 54, 78), + intArrayOf(6, 30, 56, 82), + intArrayOf(6, 30, 58, 86), + intArrayOf(6, 34, 62, 90), + intArrayOf(6, 28, 50, 72, 94), + intArrayOf(6, 26, 50, 74, 98), + intArrayOf(6, 30, 54, 78, 102), + intArrayOf(6, 28, 54, 80, 106), + intArrayOf(6, 32, 58, 84, 110), + intArrayOf(6, 30, 58, 86, 114), + intArrayOf(6, 34, 62, 90, 118), + intArrayOf(6, 26, 50, 74, 98, 122), + intArrayOf(6, 30, 54, 78, 102, 126), + intArrayOf(6, 26, 52, 78, 104, 130), + intArrayOf(6, 30, 56, 82, 108, 134), + intArrayOf(6, 34, 60, 86, 112, 138), + intArrayOf(6, 30, 58, 86, 114, 142), + intArrayOf(6, 34, 62, 90, 118, 146), + intArrayOf(6, 30, 54, 78, 102, 126, 150), + intArrayOf(6, 24, 50, 76, 102, 128, 154), + intArrayOf(6, 28, 54, 80, 106, 132, 158), + intArrayOf(6, 32, 58, 84, 110, 136, 162), + intArrayOf(6, 26, 54, 82, 110, 138, 166), + intArrayOf(6, 30, 58, 86, 114, 142, 170) + ) + + private val MAX_LENGTH = arrayOf( + arrayOf( + intArrayOf(41, 25, 17, 10), + intArrayOf(34, 20, 14, 8), + intArrayOf(27, 16, 11, 7), + intArrayOf(17, 10, 7, 4) + ), + arrayOf( + intArrayOf(77, 47, 32, 20), + intArrayOf(63, 38, 26, 16), + intArrayOf(48, 29, 20, 12), + intArrayOf(34, 20, 14, 8) + ), + arrayOf( + intArrayOf(127, 77, 53, 32), + intArrayOf(101, 61, 42, 26), + intArrayOf(77, 47, 32, 20), + intArrayOf(58, 35, 24, 15) + ), + arrayOf( + intArrayOf(187, 114, 78, 48), + intArrayOf(149, 90, 62, 38), + intArrayOf(111, 67, 46, 28), + intArrayOf(82, 50, 34, 21) + ), + arrayOf( + intArrayOf(255, 154, 106, 65), + intArrayOf(202, 122, 84, 52), + intArrayOf(144, 87, 60, 37), + intArrayOf(106, 64, 44, 27) + ), + arrayOf( + intArrayOf(322, 195, 134, 82), + intArrayOf(255, 154, 106, 65), + intArrayOf(178, 108, 74, 45), + intArrayOf(139, 84, 58, 36) + ), + arrayOf( + intArrayOf(370, 224, 154, 95), + intArrayOf(293, 178, 122, 75), + intArrayOf(207, 125, 86, 53), + intArrayOf(154, 93, 64, 39) + ), + arrayOf( + intArrayOf(461, 279, 192, 118), + intArrayOf(365, 221, 152, 93), + intArrayOf(259, 157, 108, 66), + intArrayOf(202, 122, 84, 52) + ), + arrayOf( + intArrayOf(552, 335, 230, 141), + intArrayOf(432, 262, 180, 111), + intArrayOf(312, 189, 130, 80), + intArrayOf(235, 143, 98, 60) + ), + arrayOf( + intArrayOf(652, 395, 271, 167), + intArrayOf(513, 311, 213, 131), + intArrayOf(364, 221, 151, 93), + intArrayOf(288, 174, 119, 74) + ), + arrayOf( + intArrayOf(772, 468, 321, 198), + intArrayOf(604, 366, 251, 155), + intArrayOf(427, 259, 177, 109), + intArrayOf(331, 200, 137, 85) + ), + arrayOf( + intArrayOf(883, 535, 367, 226), + intArrayOf(691, 419, 287, 177), + intArrayOf(489, 296, 203, 125), + intArrayOf(374, 227, 155, 96) + ), + arrayOf( + intArrayOf(1022, 619, 425, 262), + intArrayOf(796, 483, 331, 204), + intArrayOf(580, 352, 241, 149), + intArrayOf(427, 259, 177, 109) + ), + arrayOf( + intArrayOf(1101, 667, 458, 282), + intArrayOf(871, 528, 362, 223), + intArrayOf(621, 376, 258, 159), + intArrayOf(468, 283, 194, 120) + ), + arrayOf( + intArrayOf(1250, 758, 520, 320), + intArrayOf(991, 600, 412, 254), + intArrayOf(703, 426, 292, 180), + intArrayOf(530, 321, 220, 136) + ), + arrayOf( + intArrayOf(1408, 854, 586, 361), + intArrayOf(1082, 656, 450, 277), + intArrayOf(775, 470, 322, 198), + intArrayOf(602, 365, 250, 154) + ), + arrayOf( + intArrayOf(1548, 938, 644, 397), + intArrayOf(1212, 734, 504, 310), + intArrayOf(876, 531, 364, 224), + intArrayOf(674, 408, 280, 173) + ), + arrayOf( + intArrayOf(1725, 1046, 718, 442), + intArrayOf(1346, 816, 560, 345), + intArrayOf(948, 574, 394, 243), + intArrayOf(746, 452, 310, 191) + ), + arrayOf( + intArrayOf(1903, 1153, 792, 488), + intArrayOf(1500, 909, 624, 384), + intArrayOf(1063, 644, 442, 272), + intArrayOf(813, 493, 338, 208) + ), + arrayOf( + intArrayOf(2061, 1249, 858, 528), + intArrayOf(1600, 970, 666, 410), + intArrayOf(1159, 702, 482, 297), + intArrayOf(919, 557, 382, 235) + ), + arrayOf( + intArrayOf(2232, 1352, 929, 572), + intArrayOf(1708, 1035, 711, 438), + intArrayOf(1224, 742, 509, 314), + intArrayOf(969, 587, 403, 248) + ), + arrayOf( + intArrayOf(2409, 1460, 1003, 618), + intArrayOf(1872, 1134, 779, 480), + intArrayOf(1358, 823, 565, 348), + intArrayOf(1056, 640, 439, 270) + ), + arrayOf( + intArrayOf(2620, 1588, 1091, 672), + intArrayOf(2059, 1248, 857, 528), + intArrayOf(1468, 890, 611, 376), + intArrayOf(1108, 672, 461, 284) + ), + arrayOf( + intArrayOf(2812, 1704, 1171, 721), + intArrayOf(2188, 1326, 911, 561), + intArrayOf(1588, 963, 661, 407), + intArrayOf(1228, 744, 511, 315) + ), + arrayOf( + intArrayOf(3057, 1853, 1273, 784), + intArrayOf(2395, 1451, 997, 614), + intArrayOf(1718, 1041, 715, 440), + intArrayOf(1286, 779, 535, 330) + ), + arrayOf( + intArrayOf(3283, 1990, 1367, 842), + intArrayOf(2544, 1542, 1059, 652), + intArrayOf(1804, 1094, 751, 462), + intArrayOf(1425, 864, 593, 365) + ), + arrayOf( + intArrayOf(3517, 2132, 1465, 902), + intArrayOf(2701, 1637, 1125, 692), + intArrayOf(1933, 1172, 805, 496), + intArrayOf(1501, 910, 625, 385) + ), + arrayOf( + intArrayOf(3669, 2223, 1528, 940), + intArrayOf(2857, 1732, 1190, 732), + intArrayOf(2085, 1263, 868, 534), + intArrayOf(1581, 958, 658, 405) + ), + arrayOf( + intArrayOf(3909, 2369, 1628, 1002), + intArrayOf(3035, 1839, 1264, 778), + intArrayOf(2181, 1322, 908, 559), + intArrayOf(1677, 1016, 698, 430) + ), + arrayOf( + intArrayOf(4158, 2520, 1732, 1066), + intArrayOf(3289, 1994, 1370, 843), + intArrayOf(2358, 1429, 982, 604), + intArrayOf(1782, 1080, 742, 457) + ), + arrayOf( + intArrayOf(4417, 2677, 1840, 1132), + intArrayOf(3486, 2113, 1452, 894), + intArrayOf(2473, 1499, 1030, 634), + intArrayOf(1897, 1150, 790, 486) + ), + arrayOf( + intArrayOf(4686, 2840, 1952, 1201), + intArrayOf(3693, 2238, 1538, 947), + intArrayOf(2670, 1618, 1112, 684), + intArrayOf(2022, 1226, 842, 518) + ), + arrayOf( + intArrayOf(4965, 3009, 2068, 1273), + intArrayOf(3909, 2369, 1628, 1002), + intArrayOf(2805, 1700, 1168, 719), + intArrayOf(2157, 1307, 898, 553) + ), + arrayOf( + intArrayOf(5253, 3183, 2188, 1347), + intArrayOf(4134, 2506, 1722, 1060), + intArrayOf(2949, 1787, 1228, 756), + intArrayOf(2301, 1394, 958, 590) + ) + ) + + fun getMaxLength(typeNumber: Int, dataType: QRCodeDataType, errorCorrectionLevel: ErrorCorrectionLevel): Int = + MAX_LENGTH[typeNumber - 1][errorCorrectionLevel.ordinal][dataType.ordinal] + + fun getErrorCorrectPolynomial(errorCorrectLength: Int): Polynomial { + var a = Polynomial(intArrayOf(1)) + for (i in 0.. (i + j) % 2 == 0 + PATTERN001 -> i % 2 == 0 + PATTERN010 -> j % 3 == 0 + PATTERN011 -> (i + j) % 3 == 0 + PATTERN100 -> (i / 2 + j / 3) % 2 == 0 + PATTERN101 -> (i * j) % 2 + (i * j) % 3 == 0 + PATTERN110 -> ((i * j) % 2 + (i * j) % 3) % 2 == 0 + PATTERN111 -> ((i * j) % 3 + (i + j) % 2) % 2 == 0 + } + + /** + * Returns a suitable [QRCodeDataType] to the given input String based on a simple matching. + * + * @see QRCodeDataType + */ + fun getDataType(s: String): QRCodeDataType = + if (isAlphaNum(s)) { + if (isNumber(s)) { + QRCodeDataType.NUMBERS + } else { + QRCodeDataType.UPPER_ALPHA_NUM + } + } else { + QRCodeDataType.DEFAULT + } + + private fun isNumber(s: String) = s.matches(Regex("^\\d+$")) + private fun isAlphaNum(s: String) = s.matches(Regex("^[0-9A-Z $%*+\\-./:]+$")) + + private const val G15 = ( + 1 shl 10 or (1 shl 8) or (1 shl 5) + or (1 shl 4) or (1 shl 2) or (1 shl 1) or (1 shl 0) + ) + private const val G18 = ( + 1 shl 12 or (1 shl 11) or (1 shl 10) + or (1 shl 9) or (1 shl 8) or (1 shl 5) or (1 shl 2) or (1 shl 0) + ) + private const val G15_MASK = ( + 1 shl 14 or (1 shl 12) or (1 shl 10) + or (1 shl 4) or (1 shl 1) + ) + + fun getBCHTypeInfo(data: Int): Int { + var d = data shl 10 + while (getBCHDigit(d) - getBCHDigit(G15) >= 0) { + d = d xor (G15 shl getBCHDigit(d) - getBCHDigit(G15)) + } + return data shl 10 or d xor G15_MASK + } + + fun getBCHTypeNumber(data: Int): Int { + var d = data shl 12 + while (getBCHDigit(d) - getBCHDigit(G18) >= 0) { + d = d xor (G18 shl getBCHDigit(d) - getBCHDigit(G18)) + } + return data shl 12 or d + } + + private fun getBCHDigit(data: Int): Int { + var i = data + var digit = 0 + while (i != 0) { + digit++ + i = i ushr 1 + } + return digit + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/RSBlock.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/RSBlock.kt new file mode 100644 index 0000000..8b8701a --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/internal/RSBlock.kt @@ -0,0 +1,222 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.internal + +import `in`.procyk.compose.qrcode.ErrorCorrectionLevel + +/** + * Rewritten in Kotlin from the [original (GitHub)](https://github.com/kazuhikoarase/qrcode-generator/blob/master/java/src/main/java/com/d_project/qrcode/RSBlock.java) + * + * @author Rafael Lins - g0dkar + * @author Kazuhiko Arase - kazuhikoarase + */ +internal class RSBlock(val totalCount: Int, val dataCount: Int) { + companion object { + private val RS_BLOCK_TABLE = arrayOf( + intArrayOf(1, 26, 19), + intArrayOf(1, 26, 16), + intArrayOf(1, 26, 13), + intArrayOf(1, 26, 9), + intArrayOf(1, 44, 34), + intArrayOf(1, 44, 28), + intArrayOf(1, 44, 22), + intArrayOf(1, 44, 16), + intArrayOf(1, 70, 55), + intArrayOf(1, 70, 44), + intArrayOf(2, 35, 17), + intArrayOf(2, 35, 13), + intArrayOf(1, 100, 80), + intArrayOf(2, 50, 32), + intArrayOf(2, 50, 24), + intArrayOf(4, 25, 9), + intArrayOf(1, 134, 108), + intArrayOf(2, 67, 43), + intArrayOf(2, 33, 15, 2, 34, 16), + intArrayOf(2, 33, 11, 2, 34, 12), + intArrayOf(2, 86, 68), + intArrayOf(4, 43, 27), + intArrayOf(4, 43, 19), + intArrayOf(4, 43, 15), + intArrayOf(2, 98, 78), + intArrayOf(4, 49, 31), + intArrayOf(2, 32, 14, 4, 33, 15), + intArrayOf(4, 39, 13, 1, 40, 14), + intArrayOf(2, 121, 97), + intArrayOf(2, 60, 38, 2, 61, 39), + intArrayOf(4, 40, 18, 2, 41, 19), + intArrayOf(4, 40, 14, 2, 41, 15), + intArrayOf(2, 146, 116), + intArrayOf(3, 58, 36, 2, 59, 37), + intArrayOf(4, 36, 16, 4, 37, 17), + intArrayOf(4, 36, 12, 4, 37, 13), + intArrayOf(2, 86, 68, 2, 87, 69), + intArrayOf(4, 69, 43, 1, 70, 44), + intArrayOf(6, 43, 19, 2, 44, 20), + intArrayOf(6, 43, 15, 2, 44, 16), + intArrayOf(4, 101, 81), + intArrayOf(1, 80, 50, 4, 81, 51), + intArrayOf(4, 50, 22, 4, 51, 23), + intArrayOf(3, 36, 12, 8, 37, 13), + intArrayOf(2, 116, 92, 2, 117, 93), + intArrayOf(6, 58, 36, 2, 59, 37), + intArrayOf(4, 46, 20, 6, 47, 21), + intArrayOf(7, 42, 14, 4, 43, 15), + intArrayOf(4, 133, 107), + intArrayOf(8, 59, 37, 1, 60, 38), + intArrayOf(8, 44, 20, 4, 45, 21), + intArrayOf(12, 33, 11, 4, 34, 12), + intArrayOf(3, 145, 115, 1, 146, 116), + intArrayOf(4, 64, 40, 5, 65, 41), + intArrayOf(11, 36, 16, 5, 37, 17), + intArrayOf(11, 36, 12, 5, 37, 13), + intArrayOf(5, 109, 87, 1, 110, 88), + intArrayOf(5, 65, 41, 5, 66, 42), + intArrayOf(5, 54, 24, 7, 55, 25), + intArrayOf(11, 36, 12, 7, 37, 13), + intArrayOf(5, 122, 98, 1, 123, 99), + intArrayOf(7, 73, 45, 3, 74, 46), + intArrayOf(15, 43, 19, 2, 44, 20), + intArrayOf(3, 45, 15, 13, 46, 16), + intArrayOf(1, 135, 107, 5, 136, 108), + intArrayOf(10, 74, 46, 1, 75, 47), + intArrayOf(1, 50, 22, 15, 51, 23), + intArrayOf(2, 42, 14, 17, 43, 15), + intArrayOf(5, 150, 120, 1, 151, 121), + intArrayOf(9, 69, 43, 4, 70, 44), + intArrayOf(17, 50, 22, 1, 51, 23), + intArrayOf(2, 42, 14, 19, 43, 15), + intArrayOf(3, 141, 113, 4, 142, 114), + intArrayOf(3, 70, 44, 11, 71, 45), + intArrayOf(17, 47, 21, 4, 48, 22), + intArrayOf(9, 39, 13, 16, 40, 14), + intArrayOf(3, 135, 107, 5, 136, 108), + intArrayOf(3, 67, 41, 13, 68, 42), + intArrayOf(15, 54, 24, 5, 55, 25), + intArrayOf(15, 43, 15, 10, 44, 16), + intArrayOf(4, 144, 116, 4, 145, 117), + intArrayOf(17, 68, 42), + intArrayOf(17, 50, 22, 6, 51, 23), + intArrayOf(19, 46, 16, 6, 47, 17), + intArrayOf(2, 139, 111, 7, 140, 112), + intArrayOf(17, 74, 46), + intArrayOf(7, 54, 24, 16, 55, 25), + intArrayOf(34, 37, 13), + intArrayOf(4, 151, 121, 5, 152, 122), + intArrayOf(4, 75, 47, 14, 76, 48), + intArrayOf(11, 54, 24, 14, 55, 25), + intArrayOf(16, 45, 15, 14, 46, 16), + intArrayOf(6, 147, 117, 4, 148, 118), + intArrayOf(6, 73, 45, 14, 74, 46), + intArrayOf(11, 54, 24, 16, 55, 25), + intArrayOf(30, 46, 16, 2, 47, 17), + intArrayOf(8, 132, 106, 4, 133, 107), + intArrayOf(8, 75, 47, 13, 76, 48), + intArrayOf(7, 54, 24, 22, 55, 25), + intArrayOf(22, 45, 15, 13, 46, 16), + intArrayOf(10, 142, 114, 2, 143, 115), + intArrayOf(19, 74, 46, 4, 75, 47), + intArrayOf(28, 50, 22, 6, 51, 23), + intArrayOf(33, 46, 16, 4, 47, 17), + intArrayOf(8, 152, 122, 4, 153, 123), + intArrayOf(22, 73, 45, 3, 74, 46), + intArrayOf(8, 53, 23, 26, 54, 24), + intArrayOf(12, 45, 15, 28, 46, 16), + intArrayOf(3, 147, 117, 10, 148, 118), + intArrayOf(3, 73, 45, 23, 74, 46), + intArrayOf(4, 54, 24, 31, 55, 25), + intArrayOf(11, 45, 15, 31, 46, 16), + intArrayOf(7, 146, 116, 7, 147, 117), + intArrayOf(21, 73, 45, 7, 74, 46), + intArrayOf(1, 53, 23, 37, 54, 24), + intArrayOf(19, 45, 15, 26, 46, 16), + intArrayOf(5, 145, 115, 10, 146, 116), + intArrayOf(19, 75, 47, 10, 76, 48), + intArrayOf(15, 54, 24, 25, 55, 25), + intArrayOf(23, 45, 15, 25, 46, 16), + intArrayOf(13, 145, 115, 3, 146, 116), + intArrayOf(2, 74, 46, 29, 75, 47), + intArrayOf(42, 54, 24, 1, 55, 25), + intArrayOf(23, 45, 15, 28, 46, 16), + intArrayOf(17, 145, 115), + intArrayOf(10, 74, 46, 23, 75, 47), + intArrayOf(10, 54, 24, 35, 55, 25), + intArrayOf(19, 45, 15, 35, 46, 16), + intArrayOf(17, 145, 115, 1, 146, 116), + intArrayOf(14, 74, 46, 21, 75, 47), + intArrayOf(29, 54, 24, 19, 55, 25), + intArrayOf(11, 45, 15, 46, 46, 16), + intArrayOf(13, 145, 115, 6, 146, 116), + intArrayOf(14, 74, 46, 23, 75, 47), + intArrayOf(44, 54, 24, 7, 55, 25), + intArrayOf(59, 46, 16, 1, 47, 17), + intArrayOf(12, 151, 121, 7, 152, 122), + intArrayOf(12, 75, 47, 26, 76, 48), + intArrayOf(39, 54, 24, 14, 55, 25), + intArrayOf(22, 45, 15, 41, 46, 16), + intArrayOf(6, 151, 121, 14, 152, 122), + intArrayOf(6, 75, 47, 34, 76, 48), + intArrayOf(46, 54, 24, 10, 55, 25), + intArrayOf(2, 45, 15, 64, 46, 16), + intArrayOf(17, 152, 122, 4, 153, 123), + intArrayOf(29, 74, 46, 14, 75, 47), + intArrayOf(49, 54, 24, 10, 55, 25), + intArrayOf(24, 45, 15, 46, 46, 16), + intArrayOf(4, 152, 122, 18, 153, 123), + intArrayOf(13, 74, 46, 32, 75, 47), + intArrayOf(48, 54, 24, 14, 55, 25), + intArrayOf(42, 45, 15, 32, 46, 16), + intArrayOf(20, 147, 117, 4, 148, 118), + intArrayOf(40, 75, 47, 7, 76, 48), + intArrayOf(43, 54, 24, 22, 55, 25), + intArrayOf(10, 45, 15, 67, 46, 16), + intArrayOf(19, 148, 118, 6, 149, 119), + intArrayOf(18, 75, 47, 31, 76, 48), + intArrayOf(34, 54, 24, 34, 55, 25), + intArrayOf(20, 45, 15, 61, 46, 16) + ) + + fun getRSBlocks(typeNumber: Int, errorCorrectionLevel: ErrorCorrectionLevel): Array = + RS_BLOCK_TABLE[(typeNumber - 1) * 4 + errorCorrectionLevel.ordinal] + .let { rsBlock -> + if (rsBlock.size == 3) { + val block = RSBlock(rsBlock[1], rsBlock[2]) + Array(rsBlock[0]) { block } + } else { + val blocksSize = rsBlock[0] + rsBlock[3] + val firstBlock = RSBlock(rsBlock[1], rsBlock[2]) + val secondBlock = RSBlock(rsBlock[4], rsBlock[5]) + + Array(blocksSize) { + if (it < rsBlock[0]) { + firstBlock + } else { + secondBlock + } + } + } + } + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/DelicateQRCodeApi.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/DelicateQRCodeApi.kt new file mode 100644 index 0000000..87d4911 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/DelicateQRCodeApi.kt @@ -0,0 +1,32 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +@RequiresOptIn( + message = "This API may negatively impact QR code functionality", + level = RequiresOptIn.Level.WARNING +) +annotation class DelicateQRCodeApi \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Neighbors.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Neighbors.kt new file mode 100644 index 0000000..de6bf13 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Neighbors.kt @@ -0,0 +1,91 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable + + +/** + * Status of the neighbor QR code pixels or eyes + * */ + +@Immutable +class Neighbors( + val topLeft : Boolean = false, + val topRight : Boolean = false, + val left : Boolean = false, + val top : Boolean = false, + val right : Boolean = false, + val bottomLeft: Boolean = false, + val bottom: Boolean = false, + val bottomRight: Boolean = false, +) { + + companion object { + val Empty = Neighbors() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Neighbors + + if (topLeft != other.topLeft) return false + if (topRight != other.topRight) return false + if (left != other.left) return false + if (top != other.top) return false + if (right != other.right) return false + if (bottomLeft != other.bottomLeft) return false + if (bottom != other.bottom) return false + if (bottomRight != other.bottomRight) return false + + return true + } + + override fun hashCode(): Int { + var result = topLeft.hashCode() + result = 31 * result + topRight.hashCode() + result = 31 * result + left.hashCode() + result = 31 * result + top.hashCode() + result = 31 * result + right.hashCode() + result = 31 * result + bottomLeft.hashCode() + result = 31 * result + bottom.hashCode() + result = 31 * result + bottomRight.hashCode() + return result + } +} +val Neighbors.hasAny : Boolean + get() = topLeft || topRight || left || top || + right || bottomLeft || bottom || bottomRight + +val Neighbors.hasAllNearest + get() = top && bottom && left && right + +val Neighbors.hasAll : Boolean + get() = topLeft && topRight && left && top && + right && bottomLeft && bottom && bottomRight + diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBallShape.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBallShape.kt new file mode 100644 index 0000000..b5677ce --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBallShape.kt @@ -0,0 +1,89 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Path + +/** + * Style of the qr-code eye internal ball. + * */ +interface QrBallShape : QrShapeModifier { + + companion object { + val Default : QrBallShape = square() + } +} + +fun QrBallShape.Companion.square(size : Float = 1f) : QrBallShape = + object : QrBallShape, QrShapeModifier by SquareShape(size) {} + +fun QrBallShape.Companion.circle(size : Float = 1f) : QrBallShape = + object : QrBallShape, QrShapeModifier by CircleShape(size) {} + +fun QrBallShape.Companion.roundCorners( + radius: Float, + topLeft: Boolean = true, + bottomLeft: Boolean = true, + topRight: Boolean = true, + bottomRight: Boolean = true, +) : QrBallShape = object : QrBallShape, QrShapeModifier by RoundCornersShape( + cornerRadius = radius, + topLeft = topLeft, + bottomLeft = bottomLeft, + topRight = topRight, + bottomRight = bottomRight, + withNeighbors = false +) {} + +fun QrBallShape.Companion.asPixel(pixelShape: QrPixelShape) : QrBallShape = + AsPixelBallShape(pixelShape) + + + +@Immutable +private class AsPixelBallShape( + private val pixelShape: QrPixelShape +) : QrBallShape { + + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + + val matrix = QrCodeMatrix(3, QrCodeMatrix.PixelType.DarkPixel) + + repeat(3){ i -> + repeat(3){ j -> + addPath( + pixelShape.newPath( + size / 3, + matrix.neighbors(i,j) + ), + Offset(size/3 * i, size/3 * j) + ) + } + } + } +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBrush.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBrush.kt new file mode 100644 index 0000000..d65df2e --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrBrush.kt @@ -0,0 +1,229 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.Painter +import `in`.procyk.compose.qrcode.options.QrBrushMode.Separate +import `in`.procyk.compose.qrcode.toImageBitmap +import kotlin.random.Random + +enum class QrBrushMode { + + /** + * If applied to QR code pattern, the whole pattern will be combined to the single [Path] + * and then painted using produced [Brush] with large size and [Neighbors.Empty]. + * + * Balls and frames with [QrBrush.Unspecified] will also be joined with this path. + * If balls or frames have specified [QrBrush] then [Neighbors] parameter will be passed + * according to eye position + * */ + Join, + + /** + * If applied to QR code pattern, each QR code pixel will be painted separately and for each + * pixel new [Brush] will be created. In this scenario [Neighbors] parameter for pixels will + * be chosen according to the actual pixel neighbors. + * + * Balls and frames with [QrBrush.Unspecified] will be painted with [QrBrush.Default]. + * */ + Separate +} + +/** + * Color [Brush] factory for a QR code part. + * */ +interface QrBrush { + + /** + * Brush [mode] indicates the way this brush is applied to the QR code part. + * */ + val mode: QrBrushMode + + /** + * Factory method of the [Brush] for the element with given [size] and [neighbors]. + * */ + fun brush(size: Float, neighbors: Neighbors): Brush + + companion object { + + /** + * Delegates painting to other most suitable brush + * */ + val Unspecified : QrBrush = solid(Color.Unspecified) + + /** + * Default solid black brush + * */ + val Default : QrBrush = solid(Color.Black) + } +} + + +/** + * Check if this brush is not specified + * */ +val QrBrush.isUnspecified + get() = this === QrBrush.Unspecified || this is Solid && this.color.isUnspecified + +/** + * Check if this brush is specified + * */ +val QrBrush.isSpecified : Boolean + get() = !isUnspecified + +/** + * [SolidColor] brush from [color] + * */ +fun QrBrush.Companion.solid(color: Color) : QrBrush = Solid(color) + +/** + * Any Compose brush constructed in [builder] with specific QR code part size. + * This can be gradient brushes like, shader brush or any other. + * + * Example: + * ``` + * QrBrush.brush { + * Brush.linearGradient( + * 0f to Color.Red, + * 1f to Color.Blue, + * end = Offset(it,it) + * ) + * } + * ``` + * */ +fun QrBrush.Companion.brush( + builder: (size : Float) -> Brush +) : QrBrush = BrushColor(builder) + +/** + * Random solid color picked from given [probabilities]. + * Custom source of [random]ness can be used. + * + * Note: This brush uses [Separate] brush mode. + * + * Example: + * ``` + * QrBrush.random( + * 0.05f to Color.Red, + * 1f to Color.Black + * ) + * ``` + * */ +fun QrBrush.Companion.random( + vararg probabilities: Pair, + random: Random = Random(13) +) : QrBrush = Random(probabilities.toList(), random) + +/** + * Shader brush that resizes the image [painter] to the required size. + * [painter] resolution should be square for better result. + * */ +fun QrBrush.Companion.image( + painter: Painter, + alpha : Float = 1f, + colorFilter: ColorFilter? = null +) : QrBrush = Image(painter, alpha, colorFilter) + + + +@Immutable +private class Image( + private val painter: Painter, + private val alpha : Float = 1f, + private val colorFilter: ColorFilter? = null +) : QrBrush { + + override val mode: QrBrushMode + get() = QrBrushMode.Join + + private var cachedBrush: Brush? = null + private var cachedSize: Int = -1 + + override fun brush(size: Float, neighbors: Neighbors): Brush { + + val intSize = size.toInt() + + if (cachedBrush != null && cachedSize == intSize) { + return cachedBrush!! + } + + val bmp = painter.toImageBitmap(intSize, intSize, alpha, colorFilter) + + cachedBrush = ShaderBrush(ImageShader(bmp, TileMode.Decal, TileMode.Decal)) + cachedSize = intSize + + return cachedBrush!! + } +} + +@Immutable +private class Solid(val color: Color) : QrBrush by BrushColor({ SolidColor(color) }) + +@Immutable +private class BrushColor(private val builder: (size : Float) -> Brush) : QrBrush { + override val mode: QrBrushMode + get() = QrBrushMode.Join + + override fun brush(size: Float, neighbors: Neighbors): Brush = this.builder(size) +} + +@Immutable +private class Random( + private val probabilities: List>, + private val random : Random +) : QrBrush { + + private val _probabilities = mutableListOf,Color>>() + + init { + require(probabilities.isNotEmpty()) { + "Random color list can't be empty" + } + (listOf(0f) + probabilities.map { it.first }).reduceIndexed { index, sum, i -> + _probabilities.add(sum..(sum + i) to probabilities[index - 1].second) + sum + i + } + } + + + override val mode: QrBrushMode = QrBrushMode.Separate + + override fun brush(size: Float, neighbors: Neighbors): Brush { + val random = random.nextFloat() * _probabilities.last().first.endInclusive + + val idx = _probabilities.binarySearch { + when { + random < it.first.start -> 1 + random > it.first.endInclusive -> -1 + else -> 0 + } + } + + return SolidColor(probabilities[idx].second) + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeMatrix.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeMatrix.kt new file mode 100644 index 0000000..e4067fe --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeMatrix.kt @@ -0,0 +1,94 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +class QrCodeMatrix(val size : Int, initialFill : PixelType = PixelType.Background){ + + constructor(list : List>) : this(list.size) { + types = list.flatten().toMutableList() + } + + enum class PixelType { DarkPixel, LightPixel, Background, Logo } + + private var types = MutableList(size * size) { + initialFill + } + + operator fun get(i : Int, j : Int) : PixelType { + + val outOfBound = when { + i !in 0 until size -> i + j !in 0 until size -> j + else -> null + } + + if (outOfBound != null) + throw IndexOutOfBoundsException( + "Index $outOfBound is out of 0..${size -1} matrix bound" + ) + + return types[i + j * size] + } + + operator fun set(i: Int, j: Int, type: PixelType) { + + val outOfBound = when { + i !in 0 until size -> i + j !in 0 until size -> j + else -> null + } + + if (outOfBound != null) + throw IndexOutOfBoundsException( + "Index $outOfBound is out of 0..${size - 1} matrix bound" + ) + + types[i + j * size] = type + } + + fun copy() : QrCodeMatrix = QrCodeMatrix(size).apply { + types = ArrayList(this@QrCodeMatrix.types) + } +} + +internal fun QrCodeMatrix.neighbors(i : Int, j : Int) : Neighbors { + + fun cmp(i2 : Int, j2 : Int) = kotlin.runCatching { + this[i2,j2] == this[i,j] + }.getOrDefault(false) + + return Neighbors( + topLeft = cmp(i - 1, j - 1), + topRight = cmp(i + 1, j - 1), + left = cmp(i-1, j), + top = cmp(i, j-1), + right = cmp(i+1, j), + bottomLeft = cmp(i-1, j + 1), + bottom = cmp(i, j+1), + bottomRight = cmp(i+1, j + 1) + ) +} + diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeShape.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeShape.kt new file mode 100644 index 0000000..3632e55 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrCodeShape.kt @@ -0,0 +1,223 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import `in`.procyk.compose.qrcode.options.QrCodeShape.Companion.Default +import kotlin.math.* +import kotlin.random.Random + + +/** + * Shape of the QR-code pattern. + * + * Limitations: + * - The [Default] shape is the smallest available. + * Any custom shapes must have bigger size. + * - QR code pattern must always be at the center of the matrix. + * - QR code matrix is always square. + * */ +interface QrCodeShape { + + /** + * How much was QR code size increased in fraction related to default size (1.0). + * */ + val shapeSizeIncrease : Float + + /** + * Transform receiver matrix or create new with bigger size. + * */ + fun QrCodeMatrix.transform() : QrCodeMatrix + + companion object { + val Default = object : QrCodeShape { + override val shapeSizeIncrease: Float = 1f + + override fun QrCodeMatrix.transform(): QrCodeMatrix = this + } + } +} + + +fun QrCodeShape.Companion.circle( + padding : Float = 1.1f, + precise : Boolean = true, + random : Random = Random(233), +) : QrCodeShape = Circle( + padding = padding, + random = random, + precise = precise +) + +fun QrCodeShape.Companion.hexagon( + rotation : Float = 30f, + precise : Boolean = true, + random : Random = Random(233), +) : QrCodeShape = Hexagon( + rotationDegree = rotation, + random = random, + precise = precise +) + + +@Immutable +private class Circle( + private val padding : Float, + private val random : Random, + private val precise : Boolean +) : QrCodeShape { + + override val shapeSizeIncrease: Float = + 1 + (padding * sqrt(2.0) - 1).toFloat() + + override fun QrCodeMatrix.transform(): QrCodeMatrix { + + val padding = padding.coerceIn(1f,2f) + val added = (((size * padding * sqrt(2.0)) - size)/2).roundToInt() + + val newSize = size + 2*added + val newMatrix = QrCodeMatrix(newSize) + + val center = newSize / 2f + + + for (i in 0 until newSize) { + for (j in 0 until newSize) { + + val notInSquare = (i <= added - 1 || + j <= added - 1 || + i >= added + size || + j >= added + size) + + val inLargeCircle = isInCircle(center, i.toFloat(), j.toFloat(), center) + if (notInSquare && inLargeCircle) { + + val inSmallCircle = !precise || isInCircle( + center, + i.toFloat(), + j.toFloat(), + center - 1 + ) + newMatrix[i, j] = if (!inSmallCircle || random.nextBoolean()) + QrCodeMatrix.PixelType.DarkPixel + else QrCodeMatrix.PixelType.LightPixel + } + } + } + + for(i in 0 until size){ + for(j in 0 until size){ + newMatrix[added+i,added+j] = this[i,j] + } + } + return newMatrix + } + + fun isInCircle(center : Float, i : Float, j : Float, radius : Float) : Boolean { + return sqrt((center - i) * (center - i) + (center - j) * (center - j)) < radius + } +} + +@Immutable +private class Hexagon( + rotationDegree : Float, + private val random : Random, + private val precise : Boolean +) : QrCodeShape { + + override val shapeSizeIncrease: Float = 1.6f + + override fun QrCodeMatrix.transform(): QrCodeMatrix { + + val a = sqrt(size * size / 1.268 / 1.268) + + val newSize = (1.575f * size).toInt() + + val newMatrix = QrCodeMatrix(newSize) + + val (x1, y1) = rotate(newSize / 2, newSize / 2) + + repeat(newSize) { i -> + repeat(newSize) { j -> + val (x, y) = rotate(i, j) + + val inLarge = isInHexagon(x, y, x1, y1, a) + + if (inLarge) { + val inSmall = !precise || isInHexagon(x, y, x1, y1, a - 1.1) + + newMatrix[i, j] = if (!inSmall || random.nextBoolean()) + QrCodeMatrix.PixelType.DarkPixel else QrCodeMatrix.PixelType.LightPixel + } + } + } + + val diff = (newSize - size) / 2 + + repeat(size) { i -> + repeat(size) { j -> + newMatrix[diff + i, diff + j] = this[i, j] + } + } + + return newMatrix + } + + private fun isInHexagon(x1 : Double, y1 : Double, x2 : Double, y2 : Double, z : Double) : Boolean + { + val x = abs(x1 - x2) + val y = abs(y1 - y2) + val py1 = z * 0.86602540378 + val px2 = z * 0.5088190451 + val py2 = z * 0.8592582628 + + val p_angle_01 = -x * (py1 - y) - x * y + val p_angle_20 = -y * (px2 - x) + x * (py2 - y) + val p_angle_03 = y * z + val p_angle_12 = -x * (py2 - y) - (px2 - x) * (py1 - y) + val p_angle_32 = (z - x) * (py2 - y) + y * (px2 - x) + val is_inside_1 = (p_angle_01 * p_angle_12 >= 0) && (p_angle_12 * p_angle_20 >= 0) + val is_inside_2 = (p_angle_03 * p_angle_32 >= 0) && (p_angle_32 * p_angle_20 >= 0) + + return is_inside_1 || is_inside_2; + } + + private val rad = rotationDegree * 0.0174533 + + private val si = sin(rad) + private val co = cos(rad) + + private val isModulo60 = rotationDegree.roundToInt() % 60 == 0 + + private fun rotate(x : Int, y : Int) : Pair { + + if (isModulo60){ + return x.toDouble() to y.toDouble() + } + return (x * co - y * si) to (x * si + y * co) + } + +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrColors.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrColors.kt new file mode 100644 index 0000000..2c97f91 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrColors.kt @@ -0,0 +1,52 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable + + +/** + * Colors of QR code elements + * + * @param dark brush of the dark QR code pixels + * @param light brush of the light QR code pixels + * @param ball brush of the QR code eye balls + * @param frame brush of the QR code eye frames + */ +@Immutable +class QrColors( + val dark : QrBrush = QrBrush.Default, + val light : QrBrush = QrBrush.Unspecified, + val ball : QrBrush = QrBrush.Unspecified, + val frame : QrBrush = QrBrush.Unspecified, +) { + fun copy( + dark : QrBrush = this.dark, + light : QrBrush = this.light, + ball : QrBrush = this.ball, + frame : QrBrush = this.frame + ) = QrColors(dark, light, ball, frame) +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrErrorCorrectionLevel.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrErrorCorrectionLevel.kt new file mode 100644 index 0000000..e0f7d2f --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrErrorCorrectionLevel.kt @@ -0,0 +1,68 @@ +@file:Suppress("UNUSED") + +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import `in`.procyk.compose.qrcode.ErrorCorrectionLevel + + +/** + * QR code allows you to read encoded information even if a + * part of the QR code image is damaged. It also allows to have logo + * inside the code as a part of "damage". + * */ +@Immutable +enum class QrErrorCorrectionLevel( + internal val lvl : ErrorCorrectionLevel +) { + + /** + * Minimum possible level will be used. + * */ + Auto(ErrorCorrectionLevel.L), + + /** + * ~7% of QR code can be damaged (or used as logo). + * */ + Low(ErrorCorrectionLevel.L), + + /** + * ~15% of QR code can be damaged (or used as logo). + * */ + Medium(ErrorCorrectionLevel.M), + + /** + * ~25% of QR code can be damaged (or used as logo). + * */ + MediumHigh(ErrorCorrectionLevel.Q), + + /** + * ~30% of QR code can be damaged (or used as logo). + * */ + High(ErrorCorrectionLevel.H) +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrFrameShape.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrFrameShape.kt new file mode 100644 index 0000000..1ec6766 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrFrameShape.kt @@ -0,0 +1,174 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.Path + +/** + * Style of the qr-code eye frame. + */ +interface QrFrameShape : QrShapeModifier { + + companion object { + val Default : QrFrameShape = square() + } +} + +fun QrFrameShape.Companion.square(size: Float = 1f) : QrFrameShape = + SquareFrameShape(size) + +fun QrFrameShape.Companion.circle(size: Float = 1f) : QrFrameShape = + CircleFrameShape(size) + +fun QrFrameShape.Companion.roundCorners( + corner: Float, + width: Float = 1f, + topLeft: Boolean = true, + bottomLeft: Boolean = true, + topRight: Boolean = true, + bottomRight: Boolean = true, +) : QrFrameShape = RoundCornersFrameShape( + corner = corner, + width = width, + topLeft = topLeft, + bottomLeft = bottomLeft, + topRight = topRight, + bottomRight = bottomRight +) + +fun QrFrameShape.Companion.asPixel(pixelShape: QrPixelShape) : QrFrameShape = + AsPixelFrameShape(pixelShape) + + +@Immutable +private class AsPixelFrameShape( + val pixelShape: QrPixelShape +) : QrFrameShape { + + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + + val matrix = QrCodeMatrix(7) + + repeat(7) { i -> + repeat(7) { j -> + matrix[i, j] = if (i == 0 || j == 0 || i == 6 || j == 6) + QrCodeMatrix.PixelType.DarkPixel else QrCodeMatrix.PixelType.Background + } + } + + repeat(7) { i -> + repeat(7) { j -> + if (matrix[i, j] == QrCodeMatrix.PixelType.DarkPixel) + addPath( + pixelShape.newPath( + size / 7, + matrix.neighbors(i, j) + ), + Offset(size / 7 * i, size / 7 * j) + ) + } + } + } +} + +@Immutable +private class SquareFrameShape( + private val size: Float = 1f +) : QrFrameShape { + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + val width = size / 7f * this@SquareFrameShape.size.coerceAtLeast(0f) + + addRect( + Rect(0f, 0f, size, size) + ) + addRect( + Rect(width, width, size - width, size - width) + ) + } +} + + +@Immutable +private class CircleFrameShape( + private val size: Float = 1f +) : QrFrameShape { + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + val width = size / 7f * this@CircleFrameShape.size.coerceAtLeast(0f) + + addOval( + Rect(0f, 0f, size, size) + ) + addOval( + Rect(width, width, size - width, size - width) + ) + } +} + +@Immutable +private class RoundCornersFrameShape( + private val corner: Float, + private val width: Float = 1f, + private val topLeft: Boolean = true, + private val bottomLeft: Boolean = true, + private val topRight: Boolean = true, + private val bottomRight: Boolean = true, +) : QrFrameShape { + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply{ + + val width = size / 7f * width.coerceAtLeast(0f) + + val realCorner = corner.coerceIn(0f, .5f) + val outerCornerSize = realCorner * size + val innerCornerSize = realCorner * (size - 4 * width) + + val outer = CornerRadius(outerCornerSize, outerCornerSize) + val inner = CornerRadius(innerCornerSize, innerCornerSize) + + addRoundRect( + RoundRect( + Rect(0f, 0f, size, size), + topLeft = if (topLeft) outer else CornerRadius.Zero, + topRight = if (topRight) outer else CornerRadius.Zero, + bottomLeft = if (bottomLeft) outer else CornerRadius.Zero, + bottomRight = if (bottomRight) outer else CornerRadius.Zero, + ) + ) + addRoundRect( + RoundRect( + Rect(width, width, size - width, size - width), + topLeft = if (topLeft) inner else CornerRadius.Zero, + topRight = if (topRight) inner else CornerRadius.Zero, + bottomLeft = if (bottomLeft) inner else CornerRadius.Zero, + bottomRight = if (bottomRight) inner else CornerRadius.Zero, + ) + ) + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogo.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogo.kt new file mode 100644 index 0000000..cf77890 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogo.kt @@ -0,0 +1,47 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.painter.Painter + +/** + * Logo (middle image) of the QR code. + * + * @param painter middle image. + * @param size image size in fraction relative to QR code size + * @param padding style and size of the QR code padding. + * Can be used without [painter] if you want to place a logo manually. + * @param shape shape of the logo padding + * */ +@Immutable +data class QrLogo( + val painter: Painter? = null, + val size: Float = 0.25f, + val padding: QrLogoPadding = QrLogoPadding.Empty, + val shape: QrLogoShape = QrLogoShape.Default, +) + diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoPadding.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoPadding.kt new file mode 100644 index 0000000..52c7733 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoPadding.kt @@ -0,0 +1,71 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable + +/** + * Type of padding applied to the logo. + * Helps to highlight the logo inside the QR code pattern. + * Padding can be added regardless of the presence of a logo. + * */ +sealed interface QrLogoPadding { + + /** + * Padding size relatively to the size of logo + * */ + val size : Float + + + /** + * Logo will be drawn on top of QR code without any padding. + * QR code pixels might be visible through transparent logo. + * + * Prefer empty padding if your qr code encodes large amount of data + * to avoid performance issues. + * */ + @Immutable + data object Empty : QrLogoPadding { + override val size: Float get() = 0f + } + + + /** + * Padding will be applied precisely according to the shape of logo + * */ + @Immutable + class Accurate(override val size: Float) : QrLogoPadding + + + /** + * Works like [Accurate] but all clipped pixels will be removed. + * + * WARNING: this padding can cause performance issues + * for QR codes with large amount out data + * */ + @Immutable + class Natural(override val size: Float) : QrLogoPadding +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoShape.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoShape.kt new file mode 100644 index 0000000..95dd3fb --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrLogoShape.kt @@ -0,0 +1,40 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +interface QrLogoShape : QrShapeModifier { + + companion object { + val Default : QrLogoShape = object : QrLogoShape, QrShapeModifier by SquareShape() {} + } + +} + +fun QrLogoShape.Companion.circle() : QrLogoShape = + object : QrLogoShape, QrShapeModifier by CircleShape(1f) {} + +fun QrLogoShape.Companion.roundCorners(radius: Float) : QrLogoShape = + object : QrLogoShape, QrShapeModifier by RoundCornersShape(radius, false) {} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrOptions.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrOptions.kt new file mode 100644 index 0000000..91f9c1b --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrOptions.kt @@ -0,0 +1,107 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import `in`.procyk.compose.qrcode.options.dsl.InternalQrOptionsBuilderScope +import `in`.procyk.compose.qrcode.options.dsl.QrOptionsBuilderScope + +fun QrOptions(block : QrOptionsBuilderScope.() -> Unit) : QrOptions { + val builder = QrOptions.Builder() + InternalQrOptionsBuilderScope(builder).apply(block) + return builder.build() +} + +/** + * Styling options of the QR code + * + * @param shapes shapes of the QR code pattern and its parts + * @param colors colors of the QR code parts + * @param logo middle image + * @param errorCorrectionLevel level of error correction + * @param fourEyed enable fourth eye + * */ +@Immutable +class QrOptions( + val shapes: QrShapes = QrShapes(), + val colors : QrColors = QrColors(), + val logo : QrLogo = QrLogo(), + val errorCorrectionLevel: QrErrorCorrectionLevel = QrErrorCorrectionLevel.Auto, + val fourEyed : Boolean = false, +) { + + fun copy( + shapes: QrShapes = this.shapes, + colors : QrColors = this.colors, + logo : QrLogo = this.logo, + errorCorrectionLevel: QrErrorCorrectionLevel = this.errorCorrectionLevel, + fourthEyeEnabled : Boolean = this.fourEyed, + ) = QrOptions( + shapes = shapes, + colors = colors, + logo = logo, + errorCorrectionLevel = errorCorrectionLevel, + fourEyed = fourthEyeEnabled + ) + + + override fun equals(other: Any?): Boolean { + if (other !is QrOptions) + return false + + return shapes == other.shapes && + colors == other.colors && + logo == other.logo && + errorCorrectionLevel == other.errorCorrectionLevel && + fourEyed == other.fourEyed + } + + override fun hashCode(): Int { + return (((((shapes.hashCode()) * 31) + + colors.hashCode()) * 31 + + logo.hashCode()) * 31 + + errorCorrectionLevel.hashCode()) * 31 + + fourEyed.hashCode() + } + + internal class Builder { + + var shapes: QrShapes = QrShapes() + var colors: QrColors = QrColors() + var logo: QrLogo = QrLogo() + var errorCorrectionLevel: QrErrorCorrectionLevel = QrErrorCorrectionLevel.Auto + var fourthEyeEnabled: Boolean = false + + fun build(): QrOptions = QrOptions( + shapes = shapes, + colors = colors, + logo = logo, + errorCorrectionLevel = errorCorrectionLevel, + fourEyed = fourthEyeEnabled + ) + } +} + diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrPixelShape.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrPixelShape.kt new file mode 100644 index 0000000..0da55ea --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrPixelShape.kt @@ -0,0 +1,51 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +/** + * Style of the qr-code pixels. + * */ +fun interface QrPixelShape : QrShapeModifier { + + companion object { + val Default : QrPixelShape = square() + } +} + +fun QrPixelShape.Companion.square(size: Float = 1f) : QrPixelShape = + object : QrPixelShape, QrShapeModifier by SquareShape(size) {} + +fun QrPixelShape.Companion.circle(size: Float = 1f) : QrPixelShape = + object : QrPixelShape, QrShapeModifier by CircleShape(size) {} + +fun QrPixelShape.Companion.roundCorners(radius : Float = .5f) : QrPixelShape = + object : QrPixelShape, QrShapeModifier by RoundCornersShape(radius,true) {} + +fun QrPixelShape.Companion.verticalLines(width : Float = 1f) : QrPixelShape = + object : QrPixelShape, QrShapeModifier by VerticalLinesShape(width) {} + +fun QrPixelShape.Companion.horizontalLines(width : Float = 1f) : QrPixelShape = + object : QrPixelShape, QrShapeModifier by HorizontalLinesShape(width) {} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifier.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifier.kt new file mode 100644 index 0000000..3f2f289 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifier.kt @@ -0,0 +1,46 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd + +fun interface QrShapeModifier { + + /** + * Modify current path or create new one. + * + * Receiver path is empty and reused for optimization. + * Most benefit this optimization gives when the shape is used for pixels with [QrBrushMode.Separate]. + * + * Note: parent path has [EvenOdd] fill type! And this path will inherit it. + * */ + fun Path.path(size : Float, neighbors: Neighbors) : Path +} + +internal fun QrShapeModifier.newPath(size: Float, neighbors: Neighbors) : Path = Path().apply { + path(size, neighbors) +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifiers.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifiers.kt new file mode 100644 index 0000000..0322d29 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapeModifiers.kt @@ -0,0 +1,146 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.* +import androidx.compose.ui.graphics.Path + + +@Immutable +internal class SquareShape( + val size: Float = 1f +) : QrShapeModifier { + + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + val s = size * this@SquareShape.size.coerceIn(0f, 1f) + val offset = (size - s) / 2 + + addRect( + Rect( + Offset(offset, offset), + Size(s, s) + ) + ) + } +} + +@Immutable +internal class CircleShape( + val size: Float +) : QrShapeModifier { + + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + val s = size * this@CircleShape.size.coerceIn(0f, 1f) + val offset = (size - s) / 2 + + addOval( + Rect( + Offset(offset, offset), + Size(s, s) + ) + ) + } +} + +@Immutable +internal class RoundCornersShape( + val cornerRadius : Float, + val withNeighbors : Boolean, + val topLeft: Boolean = true, + val bottomLeft: Boolean = true, + val topRight: Boolean = true, + val bottomRight: Boolean = true, +) : QrShapeModifier { + + + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + + val corner = (cornerRadius.coerceIn(0f, .5f) * size).let { CornerRadius(it, it) } + + addRoundRect( + RoundRect( + Rect(0f, 0f, size, size), + topLeft = if (topLeft && (withNeighbors.not() || neighbors.top.not() && neighbors.left.not())) + corner else CornerRadius.Zero, + topRight = if (topRight && (withNeighbors.not() || neighbors.top.not() && neighbors.right.not())) + corner else CornerRadius.Zero, + bottomRight = if (bottomRight && (withNeighbors.not() || neighbors.bottom.not() && neighbors.right.not())) + corner else CornerRadius.Zero, + bottomLeft = if (bottomLeft && (withNeighbors.not() || neighbors.bottom.not() && neighbors.left.not())) + corner else CornerRadius.Zero + ) + ) + } + +} + +@Immutable +internal class VerticalLinesShape( + private val width : Float +) : QrShapeModifier { + + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + + val padding = (size * (1 - width.coerceIn(0f, 1f))) + + if (neighbors.top) { + addRect(Rect(Offset(padding, 0f), Size(size - padding * 2, size / 2))) + } else { + addArc(Rect(Offset(padding, 0f), Size(size - padding * 2, size)), 180f, 180f) + } + + if (neighbors.bottom) { + addRect(Rect(Offset(padding, size / 2), Size(size - padding * 2, size / 2))) + } else { + addArc(Rect(Offset(padding, 0f), Size(size - padding * 2, size)), 0f, 180f) + } + } +} + +@Immutable +internal class HorizontalLinesShape( + private val width : Float +) : QrShapeModifier { + + override fun Path.path(size: Float, neighbors: Neighbors): Path = apply { + + val padding = (size * (1 - width.coerceIn(0f, 1f))) + + if (neighbors.left) { + addRect(Rect(Offset(0f, padding), Size(size / 2, size - padding * 2))) + } else { + addArc(Rect(Offset(0f, padding), Size(size, size - padding * 2)), 90f, 180f) + + } + + if (neighbors.right) { + addRect(Rect(Offset(size / 2, padding), Size(size / 2, size - padding * 2))) + } else { + addArc(Rect(Offset(0f, padding), Size(size, size - padding * 2)), -90f, 180f) + } + } +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapes.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapes.kt new file mode 100644 index 0000000..44d274a --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/QrShapes.kt @@ -0,0 +1,65 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.runtime.Immutable + +/** + * Shapes of QR code elements + * + * @param code shape of the QR code pattern. + * @param darkPixel shape of the dark QR code pixels + * @param lightPixel shape of the light QR code pixels + * @param ball shape of the QR code eye balls + * @param frame shape of the QR code eye frames + * @param centralSymmetry if true, [ball] and [frame] shapes will be turned + * to the center according to the current corner + * */ +@Immutable +class QrShapes( + val code: QrCodeShape = QrCodeShape.Default, + val darkPixel: QrPixelShape = QrPixelShape.Default, + val lightPixel : QrPixelShape = QrPixelShape.Default, + val ball : QrBallShape = QrBallShape.Default, + val frame : QrFrameShape = QrFrameShape.Default, + val centralSymmetry: Boolean = true +) { + fun copy( + code: QrCodeShape = this.code, + darkPixel: QrPixelShape = this.darkPixel, + lightPixel : QrPixelShape = this.lightPixel, + ball : QrBallShape = this.ball, + frame : QrFrameShape = this.frame, + centralSymmetry: Boolean = this.centralSymmetry + ) = QrShapes( + code = code, + darkPixel = darkPixel, + lightPixel = lightPixel, + ball = ball, + frame = frame, + centralSymmetry = centralSymmetry + ) +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Util.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Util.kt new file mode 100644 index 0000000..9a7e658 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/Util.kt @@ -0,0 +1,39 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter + +object EmptyPainter : Painter( ){ + override val intrinsicSize: Size + get() = Size.Zero + + override fun DrawScope.onDraw() { + } + +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrColorsBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrColorsBuilderScope.kt new file mode 100644 index 0000000..7e56765 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrColorsBuilderScope.kt @@ -0,0 +1,67 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import `in`.procyk.compose.qrcode.options.QrBrush +import `in`.procyk.compose.qrcode.options.QrOptions + +internal class InternalQrColorsBuilderScope( + private val builder: QrOptions.Builder +) : QrColorsBuilderScope { + + override var dark: QrBrush + get() = builder.colors.dark + set(value) = with(builder) { + colors = colors.copy( + dark = value + ) + } + + override var light: QrBrush + get() = builder.colors.light + set(value) = with(builder) { + colors = colors.copy( + light = value + ) + } + + override var frame: QrBrush + get() = builder.colors.frame + set(value) = with(builder) { + colors = colors.copy( + frame = value + ) + } + + + override var ball: QrBrush + get() = builder.colors.ball + set(value) = with(builder) { + colors = colors.copy( + ball = value + ) + } +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrLogoBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrLogoBuilderScope.kt new file mode 100644 index 0000000..eeef52f --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrLogoBuilderScope.kt @@ -0,0 +1,58 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import androidx.compose.ui.graphics.painter.Painter +import `in`.procyk.compose.qrcode.options.QrLogoPadding +import `in`.procyk.compose.qrcode.options.QrLogoShape +import `in`.procyk.compose.qrcode.options.QrOptions + +internal class InternalQrLogoBuilderScope( + private val builder: QrOptions.Builder, +) : QrLogoBuilderScope { + + override var painter: Painter? + get() = builder.logo.painter + set(value) = with(builder) { + logo = logo.copy(painter = value) + } + override var size: Float + get() = builder.logo.size + set(value) = with(builder) { + logo = logo.copy(size = value) + } + + override var padding: QrLogoPadding + get() = builder.logo.padding + set(value) = with(builder) { + logo = logo.copy(padding = value) + } + override var shape: QrLogoShape + get() = builder.logo.shape + set(value) = with(builder) { + logo = logo.copy(shape = value) + } +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrOptionsBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrOptionsBuilderScope.kt new file mode 100644 index 0000000..1413bde --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrOptionsBuilderScope.kt @@ -0,0 +1,67 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import `in`.procyk.compose.qrcode.options.DelicateQRCodeApi +import `in`.procyk.compose.qrcode.options.QrErrorCorrectionLevel +import `in`.procyk.compose.qrcode.options.QrOptions + + +internal class InternalQrOptionsBuilderScope( + private val builder: QrOptions.Builder +) : QrOptionsBuilderScope { + + override var errorCorrectionLevel: QrErrorCorrectionLevel + get() = builder.errorCorrectionLevel + set(value) { + builder.errorCorrectionLevel = value + } + + @DelicateQRCodeApi + override var fourEyed: Boolean + get() = builder.fourthEyeEnabled + set(value) { + builder.fourthEyeEnabled = value + } + + override fun shapes( + centralSymmetry : Boolean, + block: QrShapesBuilderScope.() -> Unit + ) { + InternalQrShapesBuilderScope(builder,centralSymmetry) + .apply(block) + } + + override fun colors(block: QrColorsBuilderScope.() -> Unit) { + InternalQrColorsBuilderScope(builder).apply(block) + } + + + override fun logo(block: QrLogoBuilderScope.() -> Unit) { + InternalQrLogoBuilderScope(builder) + .apply(block) + } +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrShapesBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrShapesBuilderScope.kt new file mode 100644 index 0000000..f0b2edb --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/InternalQrShapesBuilderScope.kt @@ -0,0 +1,82 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import `in`.procyk.compose.qrcode.options.* + + +internal class InternalQrShapesBuilderScope( + private val builder: QrOptions.Builder, + centralSymmetry : Boolean, +) : QrShapesBuilderScope { + + init { + builder.shapes = builder.shapes.copy( + centralSymmetry = centralSymmetry + ) + } + + override var pattern: QrCodeShape + get() = builder.shapes.code + set(value) = with(builder){ + shapes = shapes.copy( + code = value + ) + } + + + override var darkPixel: QrPixelShape + get() = builder.shapes.darkPixel + set(value) = with(builder){ + shapes = shapes.copy( + darkPixel = value + ) + } + + override var lightPixel: QrPixelShape + get() = builder.shapes.lightPixel + set(value) = with(builder){ + shapes = shapes.copy( + lightPixel = value + ) + } + + override var ball: QrBallShape + get() = builder.shapes.ball + set(value) = with(builder){ + shapes = shapes.copy( + ball = value + ) + } + + override var frame: QrFrameShape + get() = builder.shapes.frame + set(value) = with(builder){ + shapes = shapes.copy( + frame = value + ) + } +} \ No newline at end of file diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrColorsBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrColorsBuilderScope.kt new file mode 100644 index 0000000..ab0c29f --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrColorsBuilderScope.kt @@ -0,0 +1,43 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import `in`.procyk.compose.qrcode.options.QrBrush + +/** + * Colors of QR code elements + * + * @property dark Brush of the dark QR code pixels + * @property light Brush of the light QR code pixels + * @property ball Brush of the QR code eye balls + * @property frame Brush of the QR code eye frames + */ +sealed interface QrColorsBuilderScope { + var dark: QrBrush + var light: QrBrush + var frame: QrBrush + var ball: QrBrush +} diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrLogoBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrLogoBuilderScope.kt new file mode 100644 index 0000000..fa1fcb2 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrLogoBuilderScope.kt @@ -0,0 +1,47 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import androidx.compose.ui.graphics.painter.Painter +import `in`.procyk.compose.qrcode.options.QrLogoPadding +import `in`.procyk.compose.qrcode.options.QrLogoShape + +/** + * Logo (middle image) of the QR code. + * + * @property painter Middle image. + * @property size Image size in fraction relative to QR code size + * @property padding Style and size of the QR code padding. + * Can be used without [painter] if you want to place a logo manually. + * @property shape Shape of the logo padding + * */ +sealed interface QrLogoBuilderScope { + var painter: Painter? + var size : Float + var padding : QrLogoPadding + var shape: QrLogoShape +} + diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrOptionsBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrOptionsBuilderScope.kt new file mode 100644 index 0000000..8e8e741 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrOptionsBuilderScope.kt @@ -0,0 +1,64 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import `in`.procyk.compose.qrcode.options.DelicateQRCodeApi +import `in`.procyk.compose.qrcode.options.QrErrorCorrectionLevel +import `in`.procyk.compose.qrcode.options.QrErrorCorrectionLevel.Auto + +sealed interface QrOptionsBuilderScope { + + /** + * Level of error correction. + * [Auto] by default + * */ + var errorCorrectionLevel: QrErrorCorrectionLevel + + /** + * Enable 4th qr code eye. False by default + * */ + @DelicateQRCodeApi + var fourEyed : Boolean + + /** + * Shapes of the QR code pattern and its parts. + * */ + fun shapes(centralSymmetry : Boolean = true, block: QrShapesBuilderScope.() -> Unit) + + /** + * Colors of QR code parts. + * */ + fun colors(block: QrColorsBuilderScope.() -> Unit) + + /** + * Middle image. + * */ + fun logo(block: QrLogoBuilderScope.() -> Unit) +} + + + + diff --git a/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrShapesBuilderScope.kt b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrShapesBuilderScope.kt new file mode 100644 index 0000000..71f18b1 --- /dev/null +++ b/qr-code/src/commonMain/kotlin/in/procyk/compose/qrcode/options/dsl/QrShapesBuilderScope.kt @@ -0,0 +1,49 @@ +/** + * Based on QRose by Alexander Zhirkevich from [Github](https://github.com/alexzhirkevich/qrose) + * + * MIT License + * + * Copyright (c) 2023 Alexander Zhirkevich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package `in`.procyk.compose.qrcode.options.dsl + +import `in`.procyk.compose.qrcode.options.QrBallShape +import `in`.procyk.compose.qrcode.options.QrCodeShape +import `in`.procyk.compose.qrcode.options.QrFrameShape +import `in`.procyk.compose.qrcode.options.QrPixelShape + +/** + * Shapes of QR code elements + * + * @property pattern Shape of the QR code pattern. + * @property darkPixel Shape of the dark QR code pixels + * @property lightPixel Shape of the light QR code pixels + * @property ball Shape of the QR code eye balls + * @property frame Shape of the QR code eye frames + * */ +sealed interface QrShapesBuilderScope { + var darkPixel: QrPixelShape + var lightPixel: QrPixelShape + var ball: QrBallShape + var frame: QrFrameShape + var pattern: QrCodeShape +} + diff --git a/qr-code/src/nonAndroidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt b/qr-code/src/nonAndroidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt new file mode 100644 index 0000000..c071fe8 --- /dev/null +++ b/qr-code/src/nonAndroidMain/kotlin/in/procyk/compose/qrcode/PlatformByteArray.kt @@ -0,0 +1,21 @@ +package `in`.procyk.compose.qrcode + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asSkiaBitmap +import `in`.procyk.compose.qrcode.ImageFormat.JPEG +import `in`.procyk.compose.qrcode.ImageFormat.PNG +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Image + +actual fun ImageBitmap.toByteArray(format: ImageFormat): ByteArray { + val data = Image + .makeFromBitmap(asSkiaBitmap()) + .encodeToData(format.toEncodedImageFormat()) + ?: error("This painter cannot be encoded to $format") + return data.bytes +} + +private fun ImageFormat.toEncodedImageFormat(): EncodedImageFormat = when (this) { + PNG -> EncodedImageFormat.PNG + JPEG -> EncodedImageFormat.JPEG +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 462b54f..71ed151 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ includeBuild("convention-plugins") include("camera-permission") include("camera-qr") include("util") +include("qr-code") pluginManagement { repositories {