From 9242c6a76571b083fdc5fde493403f3f438eb77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=9D=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D0=B2?= Date: Fri, 6 Jun 2025 03:17:18 +0300 Subject: [PATCH 1/5] Fix and improve parse color function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now this function supports multiple hex color formats with optional hash prefix: 3 digits (RGB): Each digit is expanded to two digits (e.g., "F0A" → "FF00AA") 4 digits (ARGB): Each digit is expanded to two digits (e.g., "8F0A" → "88FF00AA") 6 digits (RRGGBB): Standard RGB format with full alpha (255) 8 digits (AARRGGBB): Full ARGB format with explicit alpha channel --- .../dev/icerock/moko/graphics/ColorHEX.kt | 74 +++++++++++++++---- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt b/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt index 0487719..b9c2a99 100644 --- a/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt +++ b/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt @@ -4,21 +4,67 @@ package dev.icerock.moko.graphics +/** + * Parses a hexadecimal color string into a [Color] object. + * + * This function supports multiple hex color formats with optional hash prefix: + * - **3 digits (RGB)**: Each digit is expanded to two digits (e.g., "F0A" → "FF00AA") + * - **4 digits (ARGB)**: Each digit is expanded to two digits (e.g., "8F0A" → "88FF00AA") + * - **6 digits (RRGGBB)**: Standard RGB format with full alpha (255) + * - **8 digits (AARRGGBB)**: Full ARGB format with explicit alpha channel + * + * The hash prefix (#) is optional and will be automatically removed if present. + * All input is converted to uppercase for consistent parsing. + * + * @param colorHEX + * The hexadecimal color string to parse. Can include optional '#' prefix. + * Supports formats: RGB, ARGB, RRGGBB, AARRGGBB (case-insensitive). + * + * @return A [Color] object with ARGB values in the range 0-255. + * + * @throws IllegalArgumentException + * if the input string is not a valid hex color format or contains invalid hexadecimal characters. + * + */ @Suppress("MagicNumber") -fun Color.Companion.parseColor(colorHEX: String): Color { - require(colorHEX[0] != '#') { "Unknown color" } +public fun Color.Companion.parseColor(colorHEX: String): Color { + val clean = colorHEX.removePrefix("#").uppercase() - var colorARGB = colorHEX.substring(1).toLong(16) - if (colorHEX.length == 7) { - colorARGB = colorARGB or 0x00000000ff000000 - } else { - require(colorHEX.length != 9) { "Unknown color" } - } + return when (clean.length) { + 3 -> { + // RGB -> RRGGBB + val r = clean[0].digitToInt(16) * 17 + val g = clean[1].digitToInt(16) * 17 + val b = clean[2].digitToInt(16) * 17 + Color(red = r, green = g, blue = b, alpha = 255) + } + + 4 -> { + // ARGB + val a = clean[0].digitToInt(radix = 16) * 17 + val r = clean[1].digitToInt(16) * 17 + val g = clean[2].digitToInt(16) * 17 + val b = clean[3].digitToInt(16) * 17 + Color(alpha = a, red = r, green = g, blue = b) + } - return Color( - alpha = (colorARGB.shr(24) and 0xFF).toInt(), - red = (colorARGB.shr(16) and 0xFF).toInt(), - green = (colorARGB.shr(8) and 0xFF).toInt(), - blue = (colorARGB.shr(0) and 0xFF).toInt(), - ) + 6 -> { + // RRGGBB + val r = clean.substring(0, 2).toInt(16) + val g = clean.substring(2, 4).toInt(16) + val b = clean.substring(4, 6).toInt(16) + Color(red = r, green = g, blue = b, alpha = 255) + } + + 8 -> { + // AARRGGBB + val a = clean.substring(0, 2).toInt(16) + val r = clean.substring(2, 4).toInt(16) + val g = clean.substring(4, 6).toInt(16) + val b = clean.substring(6, 8).toInt(16) + Color(alpha = a, red = r, green = g, blue = b) + } + + else -> throw IllegalArgumentException("Invalid Hex color: $colorHEX") + } } From efb16cfde30743a4a46e0d7a9257bd8be0b1373d Mon Sep 17 00:00:00 2001 From: alobynya Date: Mon, 16 Jun 2025 13:28:04 +0700 Subject: [PATCH 2/5] add unit test --- .github/workflows/compilation-check.yml | 2 + README.md | 2 +- gradle/libs.versions.toml | 5 +- sample/mpp-library/build.gradle.kts | 3 ++ .../com/icerockdev/library/GraphicsTests.kt | 47 +++++++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt diff --git a/.github/workflows/compilation-check.yml b/.github/workflows/compilation-check.yml index d1b1b48..fc5c2df 100644 --- a/.github/workflows/compilation-check.yml +++ b/.github/workflows/compilation-check.yml @@ -20,6 +20,8 @@ jobs: java-version: 17 - name: Check build run: ./gradlew detektWithoutTests build publishToMavenLocal + - name: Check unit tests + run: ./gradlew test - name: Install pods run: cd sample/ios-app && pod install if: matrix.os == 'macOS-latest' diff --git a/README.md b/README.md index 7c6bef8..6c53cec 100755 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ allprojects { project build.gradle ```groovy dependencies { - commonMainApi("dev.icerock.moko:graphics:0.9.0") + commonMainApi("dev.icerock.moko:graphics:0.10.1") } ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74fba13..9290c5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,13 @@ [versions] androidAppCompatVersion = "1.6.1" androidAnnotationVersion = "1.8.0" -mokoGraphicsVersion = "0.10.0" +mokoGraphicsVersion = "0.10.1" +mokoTestVersion = "0.6.1" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } annotation = { module = "androidx.annotation:annotation", version.ref = "androidAnnotationVersion" } mokoGraphics = { module = "dev.icerock.moko:graphics", version.ref = "mokoGraphicsVersion" } +moko-test-core = { module = "dev.icerock.moko:test-core", version.ref = "mokoTestVersion" } +moko-test-robolectric = { module = "dev.icerock.moko:test-robolectric", version.ref = "mokoTestVersion" } diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 5634658..53afea5 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -27,6 +27,9 @@ kotlin { dependencies { commonMainApi(projects.graphics) + + commonTestImplementation(libs.moko.test.core) + commonTestImplementation(libs.moko.test.robolectric) } android { diff --git a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt new file mode 100644 index 0000000..c518392 --- /dev/null +++ b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt @@ -0,0 +1,47 @@ +package com.icerockdev.library + +import dev.icerock.moko.graphics.Color +import dev.icerock.moko.graphics.parseColor +import dev.icerock.moko.test.cases.TestCases +import kotlin.test.Test +import kotlin.test.assertEquals + +class GraphicsTests : TestCases() { + + override val rules: List + get() = emptyList() + + @Test + fun parseTest() { + val colorRGB = Color.parseColor("#88AAFF") + val colorFull = Color.parseColor("#FF88AAFF") + val colorRGBShort = Color.parseColor("#8AF") + val colorAlphaShort = Color.parseColor("#F8AF") + val color2Full = Color.parseColor("#7788AAFF") + val color2AlphaShort = Color.parseColor("#78AF") + assertEquals( + expected = colorRGB.argb, + actual = 4287146751 + ) + assertEquals( + expected = colorFull.argb, + actual = 4287146751 + ) + assertEquals( + expected = colorRGBShort.argb, + actual = 4287146751 + ) + assertEquals( + expected = colorAlphaShort.argb, + actual = 4287146751 + ) + assertEquals( + expected = color2AlphaShort.argb, + actual = 2005445375 + ) + assertEquals( + expected = color2Full.argb, + actual = 2005445375 + ) + } +} \ No newline at end of file From e21489a253a63b40b67efea42f7db89a5e305c93 Mon Sep 17 00:00:00 2001 From: alobynya Date: Mon, 16 Jun 2025 15:01:34 +0700 Subject: [PATCH 3/5] fix unit test --- gradle/libs.versions.toml | 5 +- sample/mpp-library/build.gradle.kts | 3 +- .../com/icerockdev/library/GraphicsTests.kt | 46 ++++++++++++------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9290c5f..d3f16da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,12 +2,9 @@ androidAppCompatVersion = "1.6.1" androidAnnotationVersion = "1.8.0" mokoGraphicsVersion = "0.10.1" -mokoTestVersion = "0.6.1" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } annotation = { module = "androidx.annotation:annotation", version.ref = "androidAnnotationVersion" } mokoGraphics = { module = "dev.icerock.moko:graphics", version.ref = "mokoGraphicsVersion" } -moko-test-core = { module = "dev.icerock.moko:test-core", version.ref = "mokoTestVersion" } -moko-test-robolectric = { module = "dev.icerock.moko:test-robolectric", version.ref = "mokoTestVersion" } - +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 53afea5..a1f257d 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -28,8 +28,7 @@ kotlin { dependencies { commonMainApi(projects.graphics) - commonTestImplementation(libs.moko.test.core) - commonTestImplementation(libs.moko.test.robolectric) + commonTestImplementation(libs.kotlin.test) } android { diff --git a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt index c518392..b73f6e8 100644 --- a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt +++ b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt @@ -2,46 +2,60 @@ package com.icerockdev.library import dev.icerock.moko.graphics.Color import dev.icerock.moko.graphics.parseColor -import dev.icerock.moko.test.cases.TestCases import kotlin.test.Test import kotlin.test.assertEquals -class GraphicsTests : TestCases() { - - override val rules: List - get() = emptyList() +class GraphicsTests { @Test - fun parseTest() { + fun parseWithoutAlphaTest() { val colorRGB = Color.parseColor("#88AAFF") val colorFull = Color.parseColor("#FF88AAFF") val colorRGBShort = Color.parseColor("#8AF") val colorAlphaShort = Color.parseColor("#F8AF") - val color2Full = Color.parseColor("#7788AAFF") - val color2AlphaShort = Color.parseColor("#78AF") assertEquals( expected = colorRGB.argb, - actual = 4287146751 + actual = 0xFF88AAFF ) assertEquals( expected = colorFull.argb, - actual = 4287146751 + actual = 0xFF88AAFF ) assertEquals( expected = colorRGBShort.argb, - actual = 4287146751 + actual = 0xFF88AAFF ) assertEquals( expected = colorAlphaShort.argb, - actual = 4287146751 + actual = 0xFF88AAFF ) + } + + @Test + fun parseWithAlphaTest() { + val colorFull = Color.parseColor("#7788AAFF") + val colorAlphaShort = Color.parseColor("#78AF") assertEquals( - expected = color2AlphaShort.argb, - actual = 2005445375 + expected = colorAlphaShort.argb, + actual = 0x7788AAFF ) assertEquals( - expected = color2Full.argb, - actual = 2005445375 + expected = colorFull.argb, + actual = 0x7788AAFF + ) + } + + @Test + fun parseWithoutPrefixTest() { + val colorFull = Color.parseColor("7788AAFF") + val colorAlphaShort = Color.parseColor("78AF") + assertEquals( + expected = colorAlphaShort.argb, + actual = 0x7788AAFF + ) + assertEquals( + expected = colorFull.argb, + actual = 0x7788AAFF ) } } \ No newline at end of file From bac7c0a8e0722503ec6be2717c5f3e0364f77675 Mon Sep 17 00:00:00 2001 From: alobynya Date: Mon, 16 Jun 2025 15:02:44 +0700 Subject: [PATCH 4/5] fix unit test naming --- .../com/icerockdev/library/GraphicsTests.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt index b73f6e8..13a61e0 100644 --- a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt +++ b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt @@ -47,15 +47,25 @@ class GraphicsTests { @Test fun parseWithoutPrefixTest() { - val colorFull = Color.parseColor("7788AAFF") - val colorAlphaShort = Color.parseColor("78AF") + val colorRGB = Color.parseColor("88AAFF") + val colorFull = Color.parseColor("FF88AAFF") + val colorRGBShort = Color.parseColor("8AF") + val colorAlphaShort = Color.parseColor("F8AF") assertEquals( - expected = colorAlphaShort.argb, - actual = 0x7788AAFF + expected = colorRGB.argb, + actual = 0xFF88AAFF ) assertEquals( expected = colorFull.argb, - actual = 0x7788AAFF + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorRGBShort.argb, + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorAlphaShort.argb, + actual = 0xFF88AAFF ) } } \ No newline at end of file From 19b76c2adf5769105a85f71e374a08ce59cf5d91 Mon Sep 17 00:00:00 2001 From: alobynya Date: Mon, 16 Jun 2025 15:05:02 +0700 Subject: [PATCH 5/5] add copyright --- .../commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt index 13a61e0..6679649 100644 --- a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt +++ b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + package com.icerockdev.library import dev.icerock.moko.graphics.Color