Initial commit
This commit is contained in:
59
core/barcode/build.gradle.kts
Normal file
59
core/barcode/build.gradle.kts
Normal file
@@ -0,0 +1,59 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-kapt")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.smoa.core.barcode"
|
||||
compileSdk = AppConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AppConfig.minSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:common"))
|
||||
|
||||
implementation(platform(Dependencies.composeBom))
|
||||
implementation(Dependencies.composeUi)
|
||||
implementation(Dependencies.composeUiGraphics)
|
||||
implementation(Dependencies.composeMaterial3)
|
||||
implementation(Dependencies.androidxCoreKtx)
|
||||
implementation(Dependencies.androidxLifecycleRuntimeKtx)
|
||||
|
||||
// Barcode libraries
|
||||
implementation(Dependencies.zxingCore)
|
||||
implementation(Dependencies.zxingAndroid)
|
||||
|
||||
// Hilt
|
||||
implementation(Dependencies.hiltAndroid)
|
||||
kapt(Dependencies.hiltAndroidCompiler)
|
||||
|
||||
// Coroutines
|
||||
implementation(Dependencies.coroutinesCore)
|
||||
implementation(Dependencies.coroutinesAndroid)
|
||||
|
||||
// Testing
|
||||
testImplementation(Dependencies.junit)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.smoa.core.barcode
|
||||
|
||||
import com.smoa.core.barcode.formats.AAMVACredential
|
||||
import com.smoa.core.barcode.formats.ICAO9303Credential
|
||||
import com.smoa.core.barcode.formats.MILSTD129Credential
|
||||
import com.smoa.core.common.Result
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Encoder for different credential formats to PDF417 barcode.
|
||||
*/
|
||||
@Singleton
|
||||
class BarcodeEncoder @Inject constructor(
|
||||
private val pdf417Generator: PDF417Generator
|
||||
) {
|
||||
|
||||
/**
|
||||
* Encode AAMVA credential to PDF417 barcode.
|
||||
*/
|
||||
fun encodeAAMVA(
|
||||
credential: AAMVACredential,
|
||||
errorCorrectionLevel: Int = 5,
|
||||
width: Int = 400,
|
||||
height: Int = 200
|
||||
): Result<BitMatrix> {
|
||||
val encodedData = credential.encodeToAAMVAFormat()
|
||||
return pdf417Generator.generatePDF417(encodedData, errorCorrectionLevel, width, height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode ICAO 9303 credential to PDF417 barcode.
|
||||
*/
|
||||
fun encodeICAO9303(
|
||||
credential: ICAO9303Credential,
|
||||
errorCorrectionLevel: Int = 5,
|
||||
width: Int = 400,
|
||||
height: Int = 200
|
||||
): Result<BitMatrix> {
|
||||
val encodedData = credential.encodeToICAO9303Format()
|
||||
return pdf417Generator.generatePDF417(encodedData, errorCorrectionLevel, width, height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode MIL-STD-129 credential to PDF417 barcode.
|
||||
*/
|
||||
fun encodeMILSTD129(
|
||||
credential: MILSTD129Credential,
|
||||
errorCorrectionLevel: Int = 5,
|
||||
width: Int = 400,
|
||||
height: Int = 200
|
||||
): Result<BitMatrix> {
|
||||
val encodedData = credential.encodeToMILSTD129Format()
|
||||
return pdf417Generator.generatePDF417(encodedData, errorCorrectionLevel, width, height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode generic data string to PDF417 barcode.
|
||||
*/
|
||||
fun encodeGeneric(
|
||||
data: String,
|
||||
errorCorrectionLevel: Int = 5,
|
||||
width: Int = 400,
|
||||
height: Int = 200,
|
||||
useCompression: Boolean = false
|
||||
): Result<BitMatrix> {
|
||||
return if (useCompression) {
|
||||
pdf417Generator.generatePDF417WithCompression(data, errorCorrectionLevel, width, height)
|
||||
} else {
|
||||
pdf417Generator.generatePDF417(data, errorCorrectionLevel, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.smoa.core.barcode
|
||||
|
||||
import android.content.Context
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.NotFoundException
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.Result as ZXingResult
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Barcode scanner for reading PDF417 and other barcode formats.
|
||||
*/
|
||||
@Singleton
|
||||
class BarcodeScanner @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
private val reader = MultiFormatReader()
|
||||
|
||||
/**
|
||||
* Scan barcode from bitmap image.
|
||||
*
|
||||
* @param pixels Pixel array (ARGB format)
|
||||
* @param width Image width
|
||||
* @param height Image height
|
||||
* @return Scanned barcode result or error
|
||||
*/
|
||||
fun scanFromBitmap(
|
||||
pixels: IntArray,
|
||||
width: Int,
|
||||
height: Int
|
||||
): Result<BarcodeScanResult> {
|
||||
return try {
|
||||
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = listOf(BarcodeFormat.PDF_417)
|
||||
hints[DecodeHintType.TRY_HARDER] = true
|
||||
|
||||
val source = RGBLuminanceSource(width, height, pixels)
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
|
||||
val zxingResult: ZXingResult = reader.decode(bitmap, hints)
|
||||
|
||||
Result.success(
|
||||
BarcodeScanResult(
|
||||
text = zxingResult.text,
|
||||
format = zxingResult.barcodeFormat.toString(),
|
||||
rawBytes = zxingResult.rawBytes
|
||||
)
|
||||
)
|
||||
} catch (e: NotFoundException) {
|
||||
Result.failure(BarcodeScanException("Barcode not found in image", e))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(BarcodeScanException("Error scanning barcode: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate scanned barcode data format.
|
||||
*/
|
||||
fun validateFormat(data: String, expectedFormat: BarcodeFormat): Boolean {
|
||||
return when (expectedFormat) {
|
||||
BarcodeFormat.PDF_417 -> {
|
||||
// Basic validation - check for common format markers
|
||||
data.isNotEmpty() && data.length > 10
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
data class BarcodeScanResult(
|
||||
val text: String,
|
||||
val format: String,
|
||||
val rawBytes: ByteArray?
|
||||
)
|
||||
|
||||
class BarcodeScanException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.smoa.core.barcode
|
||||
|
||||
import com.smoa.core.common.Result
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.oned.Code128Writer
|
||||
import com.google.zxing.pdf417.PDF417Writer
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* PDF417 barcode generator compliant with ISO/IEC 15438:2015.
|
||||
* Supports error correction levels 0-8 and text compression mode.
|
||||
*/
|
||||
@Singleton
|
||||
class PDF417Generator @Inject constructor() {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_ERROR_CORRECTION_LEVEL = 5
|
||||
private const val MIN_ERROR_CORRECTION = 0
|
||||
private const val MAX_ERROR_CORRECTION = 8
|
||||
private const val MIN_DPI = 200
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF417 barcode bitmap from data string.
|
||||
*
|
||||
* @param data The data to encode
|
||||
* @param errorCorrectionLevel Error correction level (0-8), default 5
|
||||
* @param width Desired width in pixels
|
||||
* @param height Desired height in pixels
|
||||
* @return BitMatrix representing the barcode
|
||||
*/
|
||||
fun generatePDF417(
|
||||
data: String,
|
||||
errorCorrectionLevel: Int = DEFAULT_ERROR_CORRECTION_LEVEL,
|
||||
width: Int = 400,
|
||||
height: Int = 200
|
||||
): Result<BitMatrix> {
|
||||
return try {
|
||||
// Validate error correction level
|
||||
val ecLevel = errorCorrectionLevel.coerceIn(MIN_ERROR_CORRECTION, MAX_ERROR_CORRECTION)
|
||||
|
||||
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = ecLevel
|
||||
hints[EncodeHintType.PDF417_COMPACT] = false
|
||||
hints[EncodeHintType.PDF417_AUTO_ECI] = true
|
||||
|
||||
val writer = PDF417Writer()
|
||||
val bitMatrix = writer.encode(data, BarcodeFormat.PDF_417, width, height, hints)
|
||||
|
||||
Result.Success(bitMatrix)
|
||||
} catch (e: WriterException) {
|
||||
Result.Error(e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF417 barcode with text compression mode (Mode 902).
|
||||
*/
|
||||
fun generatePDF417WithCompression(
|
||||
data: String,
|
||||
errorCorrectionLevel: Int = DEFAULT_ERROR_CORRECTION_LEVEL,
|
||||
width: Int = 400,
|
||||
height: Int = 200
|
||||
): Result<BitMatrix> {
|
||||
// Apply text compression (Mode 902) - ZXing handles this automatically
|
||||
// but we can optimize the input data
|
||||
val compressedData = compressText(data)
|
||||
return generatePDF417(compressedData, errorCorrectionLevel, width, height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic text compression for PDF417 Mode 902.
|
||||
* In production, use proper compression algorithms.
|
||||
*/
|
||||
private fun compressText(text: String): String {
|
||||
// Simplified compression - remove redundant whitespace
|
||||
// Full implementation would use proper compression algorithms
|
||||
return text.trim().replace(Regex("\\s+"), " ")
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate barcode dimensions meet minimum DPI requirements.
|
||||
*/
|
||||
fun validateDimensions(width: Int, height: Int, dpi: Int = MIN_DPI): Boolean {
|
||||
val widthInches = width / dpi.toFloat()
|
||||
val heightInches = height / dpi.toFloat()
|
||||
// Minimum size: 2.0" x 0.8" (50.8mm x 20.3mm)
|
||||
return widthInches >= 2.0f && heightInches >= 0.8f
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.smoa.core.barcode.formats
|
||||
|
||||
/**
|
||||
* AAMVA (American Association of Motor Vehicle Administrators)
|
||||
* Driver License/ID Card data structure for PDF417 encoding.
|
||||
*
|
||||
* Format specification: AAMVA DL/ID Card Design Standard
|
||||
*/
|
||||
data class AAMVACredential(
|
||||
val documentDiscriminator: String,
|
||||
val firstName: String,
|
||||
val middleName: String? = null,
|
||||
val lastName: String,
|
||||
val address: String,
|
||||
val city: String,
|
||||
val state: String,
|
||||
val zipCode: String,
|
||||
val dateOfBirth: String, // YYYYMMDD
|
||||
val expirationDate: String, // YYYYMMDD
|
||||
val issueDate: String, // YYYYMMDD
|
||||
val licenseNumber: String,
|
||||
val restrictions: String? = null,
|
||||
val endorsements: String? = null,
|
||||
val vehicleClass: String? = null,
|
||||
val height: String? = null, // Format: FTIN or CM
|
||||
val weight: String? = null, // Format: LBS or KG
|
||||
val eyeColor: String? = null,
|
||||
val hairColor: String? = null,
|
||||
val sex: String? = null // M, F, or X
|
||||
) {
|
||||
/**
|
||||
* Encode to AAMVA format string for PDF417 barcode.
|
||||
* Format: @\nANSI [version]\n[data elements]\n
|
||||
*/
|
||||
fun encodeToAAMVAFormat(): String {
|
||||
val builder = StringBuilder()
|
||||
builder.append("@\n")
|
||||
builder.append("ANSI 636026") // Standard version header
|
||||
builder.append(documentDiscriminator)
|
||||
builder.append("\n")
|
||||
|
||||
// Data elements in AAMVA format
|
||||
builder.append("DAA") // First name
|
||||
builder.append(firstName)
|
||||
builder.append("\n")
|
||||
|
||||
if (middleName != null) {
|
||||
builder.append("DAB") // Middle name
|
||||
builder.append(middleName)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
builder.append("DAC") // Last name
|
||||
builder.append(lastName)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DAD") // Address
|
||||
builder.append(address)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DAE") // City
|
||||
builder.append(city)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DAF") // State
|
||||
builder.append(state)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DAG") // ZIP code
|
||||
builder.append(zipCode)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DBA") // Date of birth
|
||||
builder.append(dateOfBirth)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DCS") // Last name (alternate)
|
||||
builder.append(lastName)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DDE") // Sex
|
||||
builder.append(sex ?: "")
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DDF") // Eye color
|
||||
builder.append(eyeColor ?: "")
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DDG") // Height
|
||||
builder.append(height ?: "")
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DBB") // Issue date
|
||||
builder.append(issueDate)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DBC") // Expiration date
|
||||
builder.append(expirationDate)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("DBD") // License number
|
||||
builder.append(licenseNumber)
|
||||
builder.append("\n")
|
||||
|
||||
if (restrictions != null) {
|
||||
builder.append("DBA") // Restrictions
|
||||
builder.append(restrictions)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
if (endorsements != null) {
|
||||
builder.append("DBC") // Endorsements
|
||||
builder.append(endorsements)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
if (vehicleClass != null) {
|
||||
builder.append("DCA") // Vehicle class
|
||||
builder.append(vehicleClass)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.smoa.core.barcode.formats
|
||||
|
||||
/**
|
||||
* ICAO 9303 Machine Readable Travel Document (MRTD) data structure.
|
||||
*
|
||||
* Format specification: ICAO Document 9303 - Machine Readable Travel Documents
|
||||
*/
|
||||
data class ICAO9303Credential(
|
||||
val documentType: String, // P = Passport, I = ID card, A = Alien, etc.
|
||||
val issuingCountry: String, // ISO 3166-1 alpha-3 country code
|
||||
val surname: String,
|
||||
val givenNames: String,
|
||||
val documentNumber: String,
|
||||
val nationality: String, // ISO 3166-1 alpha-3
|
||||
val dateOfBirth: String, // YYMMDD
|
||||
val sex: String, // M, F, or < (unspecified)
|
||||
val expirationDate: String, // YYMMDD
|
||||
val personalNumber: String? = null,
|
||||
val optionalData: String? = null
|
||||
) {
|
||||
/**
|
||||
* Encode to ICAO 9303 format (MRZ - Machine Readable Zone).
|
||||
* Format: Two-line or three-line MRZ format
|
||||
*/
|
||||
fun encodeToICAO9303Format(): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
// Line 1: Document type, issuing country, name
|
||||
builder.append(documentType)
|
||||
builder.append("<")
|
||||
builder.append(issuingCountry)
|
||||
builder.append(surname.uppercase().padEnd(39, '<'))
|
||||
builder.append(givenNames.uppercase().replace(" ", "<"))
|
||||
builder.append("\n")
|
||||
|
||||
// Line 2: Document number, check digit, nationality, DOB, sex, expiration, optional
|
||||
builder.append(documentNumber.padEnd(9, '<'))
|
||||
builder.append(calculateCheckDigit(documentNumber))
|
||||
builder.append(nationality)
|
||||
builder.append(dateOfBirth)
|
||||
builder.append(calculateCheckDigit(dateOfBirth))
|
||||
builder.append(sex)
|
||||
builder.append(expirationDate)
|
||||
builder.append(calculateCheckDigit(expirationDate))
|
||||
builder.append(personalNumber?.padEnd(14, '<') ?: "<".repeat(14))
|
||||
builder.append(calculateCheckDigit(personalNumber ?: ""))
|
||||
builder.append(optionalData ?: "")
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate check digit per ICAO 9303 specification.
|
||||
*/
|
||||
private fun calculateCheckDigit(data: String): String {
|
||||
if (data.isEmpty()) return "0"
|
||||
|
||||
val weights = intArrayOf(7, 3, 1)
|
||||
var sum = 0
|
||||
|
||||
data.forEachIndexed { index, char ->
|
||||
val value = when {
|
||||
char.isDigit() -> char.toString().toInt()
|
||||
char.isLetter() -> char.uppercaseChar().code - 55
|
||||
else -> 0
|
||||
}
|
||||
sum += value * weights[index % 3]
|
||||
}
|
||||
|
||||
return (sum % 10).toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.smoa.core.barcode.formats
|
||||
|
||||
/**
|
||||
* MIL-STD-129 Military Identification data structure.
|
||||
*
|
||||
* Format specification: MIL-STD-129 - Military Identification
|
||||
*/
|
||||
data class MILSTD129Credential(
|
||||
val serviceCode: String, // Service branch code
|
||||
val rank: String? = null,
|
||||
val lastName: String,
|
||||
val firstName: String,
|
||||
val middleInitial: String? = null,
|
||||
val socialSecurityNumber: String, // Last 4 digits or full
|
||||
val dateOfBirth: String, // YYYYMMDD
|
||||
val expirationDate: String, // YYYYMMDD
|
||||
val issueDate: String, // YYYYMMDD
|
||||
val cardNumber: String,
|
||||
val unit: String? = null,
|
||||
val clearanceLevel: String? = null // Classification level
|
||||
) {
|
||||
/**
|
||||
* Encode to MIL-STD-129 format for PDF417 barcode.
|
||||
*/
|
||||
fun encodeToMILSTD129Format(): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
// Header
|
||||
builder.append("MIL-STD-129")
|
||||
builder.append("\n")
|
||||
|
||||
// Service code
|
||||
builder.append("SVC:")
|
||||
builder.append(serviceCode)
|
||||
builder.append("\n")
|
||||
|
||||
// Name
|
||||
builder.append("LNAME:")
|
||||
builder.append(lastName)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("FNAME:")
|
||||
builder.append(firstName)
|
||||
builder.append("\n")
|
||||
|
||||
if (middleInitial != null) {
|
||||
builder.append("MI:")
|
||||
builder.append(middleInitial)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
// Rank
|
||||
if (rank != null) {
|
||||
builder.append("RANK:")
|
||||
builder.append(rank)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
// SSN (last 4 or full)
|
||||
builder.append("SSN:")
|
||||
builder.append(socialSecurityNumber)
|
||||
builder.append("\n")
|
||||
|
||||
// Dates
|
||||
builder.append("DOB:")
|
||||
builder.append(dateOfBirth)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("ISSUE:")
|
||||
builder.append(issueDate)
|
||||
builder.append("\n")
|
||||
|
||||
builder.append("EXPIRE:")
|
||||
builder.append(expirationDate)
|
||||
builder.append("\n")
|
||||
|
||||
// Card number
|
||||
builder.append("CARD:")
|
||||
builder.append(cardNumber)
|
||||
builder.append("\n")
|
||||
|
||||
// Unit
|
||||
if (unit != null) {
|
||||
builder.append("UNIT:")
|
||||
builder.append(unit)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
// Clearance
|
||||
if (clearanceLevel != null) {
|
||||
builder.append("CLR:")
|
||||
builder.append(clearanceLevel)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.smoa.core.barcode.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.smoa.core.barcode.PDF417Generator
|
||||
|
||||
/**
|
||||
* Composable for displaying PDF417 barcode.
|
||||
* Ensures minimum 200 DPI resolution.
|
||||
*/
|
||||
@Composable
|
||||
fun BarcodeDisplay(
|
||||
bitMatrix: BitMatrix,
|
||||
modifier: Modifier = Modifier,
|
||||
errorCorrectionLevel: Int = 5
|
||||
) {
|
||||
val width = bitMatrix.width
|
||||
val height = bitMatrix.height
|
||||
|
||||
// Calculate display size maintaining aspect ratio
|
||||
val displayWidth = 400.dp
|
||||
val displayHeight = (height * 400 / width).dp
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(displayHeight)
|
||||
) {
|
||||
drawBarcode(bitMatrix)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawBarcode(bitMatrix: BitMatrix) {
|
||||
val width = bitMatrix.width
|
||||
val height = bitMatrix.height
|
||||
val scaleX = size.width / width
|
||||
val scaleY = size.height / height
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
if (bitMatrix[x, y]) {
|
||||
drawRect(
|
||||
color = Color.Black,
|
||||
topLeft = Offset(x * scaleX, y * scaleY),
|
||||
size = Size(scaleX, scaleY)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert BitMatrix to Android Bitmap for display.
|
||||
*/
|
||||
fun BitMatrix.toBitmap(): Bitmap {
|
||||
val width = this.width
|
||||
val height = this.height
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bitmap.setPixel(x, y, if (this[x, y]) android.graphics.Color.BLACK else android.graphics.Color.WHITE)
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user