Initial commit

This commit is contained in:
defiQUG
2025-12-26 10:48:33 -08:00
commit 97f75e144f
270 changed files with 35886 additions and 0 deletions

View 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)
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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
}