diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index 3d10f4510..376b34247 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import org.hisp.dhis.common.screens.BadgesScreen +import org.hisp.dhis.common.screens.BarcodeBlockScreen import org.hisp.dhis.common.screens.BottomSheetHeaderScreen import org.hisp.dhis.common.screens.ButtonBlockScreen import org.hisp.dhis.common.screens.ButtonScreen @@ -139,6 +140,7 @@ fun Main() { Components.BADGES -> BadgesScreen() Components.SWITCH -> SwitchScreen() Components.INPUT_RADIO_BUTTON -> InputRadioButtonScreen() + Components.BARCODE_BLOCK -> BarcodeBlockScreen() } } } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/BarcodeBlockScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/BarcodeBlockScreen.kt new file mode 100644 index 000000000..c28d5e1ba --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/BarcodeBlockScreen.kt @@ -0,0 +1,20 @@ +package org.hisp.dhis.common.screens + +import androidx.compose.material3.Divider +import androidx.compose.runtime.Composable +import org.hisp.dhis.mobile.ui.designsystem.component.BarcodeBlock +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.QrCodeBlock +import org.hisp.dhis.mobile.ui.designsystem.component.RowComponentContainer + +@Composable +fun BarcodeBlockScreen() { + ColumnComponentContainer { + BarcodeBlock(data = "1111111111") + Divider() + RowComponentContainer { + BarcodeBlock(data = "3981239131092310") + BarcodeBlock(data = "9038192381902381029831") + } + } +} diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt index e0fbd2e6f..0801b7145 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt @@ -30,4 +30,5 @@ enum class Components(val label: String) { METADATA_AVATAR("Metadata Avatar"), INPUT_RADIO_BUTTON("Input Radio Button"), SWITCH("Switch"), + BARCODE_BLOCK("Barcode Block"), } diff --git a/designsystem/src/androidMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt b/designsystem/src/androidMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt new file mode 100644 index 000000000..a0962e27e --- /dev/null +++ b/designsystem/src/androidMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt @@ -0,0 +1,47 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.internal.barcode + +import android.graphics.Bitmap +import android.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.MultiFormatWriter +import com.google.zxing.WriterException +import com.google.zxing.common.BitMatrix + +internal actual class BarcodeGenerator { + + actual fun generate(data: String): ImageBitmap? { + return try { + val writer = MultiFormatWriter() + val bitMatrix = writer.encode( + data, + BarcodeFormat.CODABAR, + BARCODE_WIDTH, + BARCODE_HEIGHT, + ) + val bitmap = createAndroidBitmap(bitMatrix) + + bitmap.asImageBitmap() + } catch (e: WriterException) { + null + } + } + + private fun createAndroidBitmap(bitMatrix: BitMatrix): Bitmap { + val width = bitMatrix.width + val height = bitMatrix.height + val pixels = IntArray(width * height) + + for (y in 0 until height) { + for (x in 0 until width) { + pixels[y * width + x] = if (bitMatrix[x, y]) Color.BLACK else Color.WHITE + } + } + + return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply { + setPixels(pixels, 0, width, 0, 0, width, height) + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BarcodeBlock.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BarcodeBlock.kt new file mode 100644 index 000000000..db20f6314 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BarcodeBlock.kt @@ -0,0 +1,64 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.internal.barcode.rememberBarcodeGenerator +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +/** + * Container to render barcode image and text for given + * data + * + * @param data: Data to render barcode and text for + */ +@Composable +fun BarcodeBlock( + data: String, + modifier: Modifier = Modifier, + barcodeSize: DpSize = DpSize(312.dp, 76.dp) +) { + if (data.isNotBlank()) { + Column( + modifier = modifier.padding(vertical = Spacing.Spacing16) + .requiredSizeIn(maxWidth = barcodeSize.width) + .testTag("BARCODE_BLOCK_CONTAINER"), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val barcode by rememberBarcodeGenerator(data) + + Box(Modifier.size(barcodeSize)) { + barcode?.let { bitmap -> + Image( + bitmap = bitmap, + contentDescription = null, + modifier = Modifier.matchParentSize(), + contentScale = ContentScale.FillBounds, + ) + } + } + + Text( + text = data, + style = MaterialTheme.typography.titleMedium, + color = TextColor.OnSurface, + textAlign = TextAlign.Center, + ) + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt new file mode 100644 index 000000000..8ad0e581b --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt @@ -0,0 +1,30 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.internal.barcode + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.ImageBitmap + +@Composable +internal fun rememberBarcodeGenerator(value: String): State { + val barcodeGenerator = LocalBarcodeGenerator.current + val result = remember(value) { mutableStateOf(null) } + + LaunchedEffect(value) { + result.value = barcodeGenerator.generate(value) + } + + return result +} + +internal expect class BarcodeGenerator() { + fun generate(data: String): ImageBitmap? +} + +internal const val BARCODE_WIDTH = 512 // px +internal const val BARCODE_HEIGHT = 256 // px + +internal val LocalBarcodeGenerator = staticCompositionLocalOf { BarcodeGenerator() } diff --git a/designsystem/src/desktopMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt b/designsystem/src/desktopMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt new file mode 100644 index 000000000..4bcf513ff --- /dev/null +++ b/designsystem/src/desktopMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/barcode/BarcodeGenerator.kt @@ -0,0 +1,49 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.internal.barcode + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.MultiFormatWriter +import com.google.zxing.WriterException +import com.google.zxing.common.BitMatrix +import java.awt.image.BufferedImage + +internal actual class BarcodeGenerator { + + private val colorBlack = 0xFF000000.toInt() + private val colorWhite = 0xFFFFFFFF.toInt() + + actual fun generate(data: String): ImageBitmap? { + return try { + val writer = MultiFormatWriter() + val bitMatrix = writer.encode( + data, + BarcodeFormat.CODABAR, + BARCODE_WIDTH, + BARCODE_HEIGHT, + ) + val image = createBufferedImage(bitMatrix) + + image.toComposeImageBitmap() + } catch (e: WriterException) { + null + } + } + + private fun createBufferedImage(bitMatrix: BitMatrix): BufferedImage { + val width = bitMatrix.width + val height = bitMatrix.height + val pixels = IntArray(width * height) + + for (y in 0 until height) { + for (x in 0 until width) { + pixels[y * width + x] = if (bitMatrix[x, y]) colorBlack else colorWhite + } + } + + return BufferedImage(width, height, BufferedImage.TYPE_INT_RGB).apply { + setRGB(0, 0, width, height, pixels, 0, width) + } + } +}