" +
diff --git a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/PrinterController.kt b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/PrinterController.kt
index 12b412b..195140b 100644
--- a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/PrinterController.kt
+++ b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/PrinterController.kt
@@ -1,6 +1,6 @@
package de.tillhub.printengine
-import de.tillhub.printengine.data.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
import de.tillhub.printengine.data.PrinterInfo
import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.PrintingFontType
diff --git a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoder.kt b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoder.kt
index bade036..cb8d7d6 100644
--- a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoder.kt
+++ b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoder.kt
@@ -1,6 +1,6 @@
package de.tillhub.printengine.barcode
-import de.tillhub.printengine.data.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
interface BarcodeEncoder {
fun encodeAsBitmap(
diff --git a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.kt b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.kt
index 50aeb01..dba30f0 100644
--- a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.kt
+++ b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.kt
@@ -1,6 +1,6 @@
package de.tillhub.printengine.barcode
-import de.tillhub.printengine.data.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
internal expect class BarcodeEncoderImpl() : BarcodeEncoder {
override fun encodeAsBitmap(
diff --git a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/ImageBitmap.kt b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/ImageBitmapExt.kt
similarity index 64%
rename from print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/ImageBitmap.kt
rename to print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/ImageBitmapExt.kt
index 7977b01..90d70e9 100644
--- a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/ImageBitmap.kt
+++ b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/ImageBitmapExt.kt
@@ -1,5 +1,5 @@
package de.tillhub.printengine.data
-expect class ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
expect fun ImageBitmap.encodeToBase64(): String
diff --git a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/PrintCommand.kt b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/PrintCommand.kt
index 6d362ad..a1b9a55 100644
--- a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/PrintCommand.kt
+++ b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/data/PrintCommand.kt
@@ -1,6 +1,6 @@
package de.tillhub.printengine.data
-import de.tillhub.printengine.data.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
import de.tillhub.printengine.helpers.HashHelper
sealed class PrintCommand {
@@ -33,7 +33,7 @@ sealed class PrintCommand {
class Barcode(
val barcode: String,
) : PrintCommand() {
- override fun toString() = "PrintCommand.Text(" +
+ override fun toString() = "PrintCommand.Barcode(" +
"barcode=$barcode" +
")"
diff --git a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlPrinterController.kt b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlPrinterController.kt
index bf34327..81962bc 100644
--- a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlPrinterController.kt
+++ b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlPrinterController.kt
@@ -1,9 +1,9 @@
package de.tillhub.printengine.html
+import androidx.compose.ui.graphics.ImageBitmap
import de.tillhub.printengine.PrinterController
import de.tillhub.printengine.barcode.BarcodeEncoder
import de.tillhub.printengine.barcode.BarcodeType
-import de.tillhub.printengine.data.ImageBitmap
import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.RawPrinterData
import de.tillhub.printengine.html.HtmlUtils.generateImageHtml
diff --git a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlUtils.kt b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlUtils.kt
index d681da7..c5b21d7 100644
--- a/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlUtils.kt
+++ b/print-engine/src/commonMain/kotlin/de/tillhub/printengine/html/HtmlUtils.kt
@@ -1,6 +1,6 @@
package de.tillhub.printengine.html
-import de.tillhub.printengine.data.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
import de.tillhub.printengine.data.encodeToBase64
object HtmlUtils {
diff --git a/print-engine/src/iosMain/kotlin/de/tillhub/printengine/PrinterServiceProvider.ios.kt b/print-engine/src/iosMain/kotlin/de/tillhub/printengine/PrinterServiceProvider.ios.kt
index 0b87de4..60a3951 100644
--- a/print-engine/src/iosMain/kotlin/de/tillhub/printengine/PrinterServiceProvider.ios.kt
+++ b/print-engine/src/iosMain/kotlin/de/tillhub/printengine/PrinterServiceProvider.ios.kt
@@ -1,5 +1,11 @@
package de.tillhub.printengine
+import de.tillhub.printengine.barcode.BarcodeEncoder
+import de.tillhub.printengine.data.ExternalPrinter
+
actual interface PrinterServiceProvider {
- // TODO("not implemented")
+ fun build(
+ printer: ExternalPrinter? = null,
+ barcode: BarcodeEncoder? = null,
+ ): PrintService
}
diff --git a/print-engine/src/iosMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.ios.kt b/print-engine/src/iosMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.ios.kt
index c80d1f1..bf2c6df 100644
--- a/print-engine/src/iosMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.ios.kt
+++ b/print-engine/src/iosMain/kotlin/de/tillhub/printengine/barcode/BarcodeEncoderImpl.ios.kt
@@ -1,14 +1,128 @@
package de.tillhub.printengine.barcode
-import de.tillhub.printengine.data.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import co.touchlab.kermit.Logger
+import kotlinx.cinterop.BetaInteropApi
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.addressOf
+import kotlinx.cinterop.usePinned
+import platform.CoreGraphics.CGAffineTransformMakeScale
+import platform.CoreGraphics.CGRectGetHeight
+import platform.CoreGraphics.CGRectGetWidth
+import platform.CoreGraphics.CGRectMake
+import platform.CoreGraphics.CGSizeMake
+import platform.CoreImage.CIFilter
+import platform.Foundation.NSClassFromString
+import platform.Foundation.NSData
+import platform.Foundation.NSSelectorFromString
+import platform.Foundation.NSString
+import platform.Foundation.NSUTF8StringEncoding
+import platform.Foundation.dataUsingEncoding
+import platform.Foundation.setValue
+import platform.UIKit.UIGraphicsBeginImageContextWithOptions
+import platform.UIKit.UIGraphicsEndImageContext
+import platform.UIKit.UIGraphicsGetImageFromCurrentImageContext
+import platform.UIKit.UIImage
+import platform.UIKit.UIImagePNGRepresentation
+import platform.darwin.NSObject
+import platform.posix.memcpy
+import org.jetbrains.skia.Image as SkiaImage
internal actual class BarcodeEncoderImpl : BarcodeEncoder {
+ @OptIn(ExperimentalForeignApi::class)
actual override fun encodeAsBitmap(
content: String,
type: BarcodeType,
imgWidth: Int,
imgHeight: Int,
): ImageBitmap? {
- TODO("Not yet implemented")
+ val filterName = when (type) {
+ BarcodeType.QR_CODE -> "CIQRCodeGenerator"
+ BarcodeType.CODE_128 -> "CICode128BarcodeGenerator"
+ }
+
+ val filter = createCIFilter(filterName) ?: run {
+ Logger.e("Failed to create CIFilter: $filterName")
+ return null
+ }
+
+ val messageData = (content as NSString).dataUsingEncoding(NSUTF8StringEncoding)
+ ?: return null
+ filter.setValue(messageData, forKey = "inputMessage")
+
+ if (type == BarcodeType.QR_CODE) {
+ filter.setValue("Q", forKey = "inputCorrectionLevel")
+ }
+
+ val outputImage = filter.outputImage ?: run {
+ Logger.e("CIFilter produced no output image")
+ return null
+ }
+
+ // Scale to desired dimensions
+ val extent = outputImage.extent
+ val extentWidth = CGRectGetWidth(extent)
+ val extentHeight = CGRectGetHeight(extent)
+ if (extentWidth <= 0.0 || extentHeight <= 0.0) return null
+
+ val scaleX = imgWidth.toDouble() / extentWidth
+ val scaleY = imgHeight.toDouble() / extentHeight
+ val scaledImage = outputImage.imageByApplyingTransform(
+ CGAffineTransformMakeScale(scaleX, scaleY),
+ )
+
+ // Render CIImage to a bitmap-backed UIImage
+ val ciBasedImage = UIImage(cIImage = scaledImage)
+ val targetSize = CGSizeMake(imgWidth.toDouble(), imgHeight.toDouble())
+
+ UIGraphicsBeginImageContextWithOptions(targetSize, true, 1.0)
+ val renderedImage = try {
+ ciBasedImage.drawInRect(CGRectMake(0.0, 0.0, imgWidth.toDouble(), imgHeight.toDouble()))
+ UIGraphicsGetImageFromCurrentImageContext()
+ } finally {
+ UIGraphicsEndImageContext()
+ }
+
+ val pngData = renderedImage?.let { UIImagePNGRepresentation(it) } ?: run {
+ Logger.e("Failed to create PNG representation")
+ return null
+ }
+
+ // Convert NSData → ByteArray → Skia Image → Compose ImageBitmap
+ val bytes = pngData.toByteArray()
+ return try {
+ val skiaImage = SkiaImage.makeFromEncoded(bytes)
+ try {
+ skiaImage.toComposeImageBitmap()
+ } finally {
+ skiaImage.close()
+ }
+ } catch (e: Exception) {
+ Logger.e("Failed to decode barcode image", e)
+ null
+ }
+ }
+
+ /**
+ * Creates a CIFilter via ObjC runtime +[CIFilter filterWithName:].
+ * K/N bindings only expose CIFilter() and CIFilter(coder:) constructors,
+ * so we must call the factory class method via performSelector.
+ */
+ @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
+ @Suppress("UNCHECKED_CAST")
+ private fun createCIFilter(name: String): CIFilter? {
+ val cls = NSClassFromString("CIFilter") as? NSObject ?: return null
+ val sel = NSSelectorFromString("filterWithName:")
+ return cls.performSelector(sel, withObject = name) as? CIFilter
+ }
+
+ @OptIn(ExperimentalForeignApi::class)
+ private fun NSData.toByteArray(): ByteArray = ByteArray(length.toInt()).apply {
+ if (isNotEmpty()) {
+ usePinned { pinned ->
+ memcpy(pinned.addressOf(0), bytes, length)
+ }
+ }
}
}
diff --git a/print-engine/src/iosMain/kotlin/de/tillhub/printengine/data/ImageBitmap.ios.kt b/print-engine/src/iosMain/kotlin/de/tillhub/printengine/data/ImageBitmap.ios.kt
index 7b67fd6..c8527ee 100644
--- a/print-engine/src/iosMain/kotlin/de/tillhub/printengine/data/ImageBitmap.ios.kt
+++ b/print-engine/src/iosMain/kotlin/de/tillhub/printengine/data/ImageBitmap.ios.kt
@@ -1,7 +1,59 @@
package de.tillhub.printengine.data
-actual class ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.addressOf
+import kotlinx.cinterop.usePinned
+import org.jetbrains.skia.EncodedImageFormat
+import org.jetbrains.skia.ImageInfo
+import platform.posix.memcpy
+import kotlin.io.encoding.Base64
+import kotlin.io.encoding.ExperimentalEncodingApi
+import org.jetbrains.skia.Bitmap as SkiaBitmap
+import org.jetbrains.skia.Image as SkiaImage
+/**
+ * Encodes a Compose [ImageBitmap] to PNG bytes via Skia.
+ * Handles pixel extraction, Skia bitmap/image lifecycle, and cleanup.
+ * Returns null if PNG encoding fails.
+ */
+@OptIn(ExperimentalForeignApi::class)
+fun ImageBitmap.encodeToPngBytes(): ByteArray? {
+ val w = width
+ val h = height
+ val buffer = IntArray(w * h)
+ readPixels(buffer)
+
+ // On little-endian (all Apple platforms), raw int bytes are already in
+ // BGRA order which matches Skia's N32 native format — memcpy directly.
+ val bytes = ByteArray(buffer.size * 4)
+ buffer.usePinned { src ->
+ bytes.usePinned { dst ->
+ memcpy(dst.addressOf(0), src.addressOf(0), bytes.size.toULong())
+ }
+ }
+
+ val imageInfo = ImageInfo.makeN32Premul(w, h)
+ val skiaBitmap = SkiaBitmap().apply {
+ allocPixels(imageInfo)
+ installPixels(imageInfo, bytes, w * 4)
+ }
+
+ return try {
+ val skiaImage = SkiaImage.makeFromBitmap(skiaBitmap)
+ try {
+ skiaImage.encodeToData(EncodedImageFormat.PNG)?.bytes
+ } finally {
+ skiaImage.close()
+ }
+ } finally {
+ skiaBitmap.close()
+ }
+}
+
+@OptIn(ExperimentalEncodingApi::class)
actual fun ImageBitmap.encodeToBase64(): String {
- TODO("Not yet implemented")
+ val pngBytes = encodeToPngBytes()
+ ?: error("Failed to encode image to PNG")
+ return Base64.encode(pngBytes)
}
diff --git a/print-plugins/epson/build.gradle.kts b/print-plugins/epson/build.gradle.kts
index 6c4dffb..9a1a7e1 100644
--- a/print-plugins/epson/build.gradle.kts
+++ b/print-plugins/epson/build.gradle.kts
@@ -1,19 +1,85 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.konan.target.KonanTarget
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.compose)
alias(libs.plugins.maven.publish)
}
+object EpsonSdk {
+ const val VERSION = "2.23.1"
+ const val REPO = "https://github.com/popina/EpsonSDK.git"
+ const val FRAMEWORK_NAME = "print-engine-epson"
+
+ // DEVICE_SLICE → all physical iPhones/iPads (arm64 + legacy armv7)
+ // SIMULATOR_SLICE → Xcode Simulator on Intel & Apple Silicon Macs
+ const val DEVICE_SLICE = "ios-arm64_armv7"
+ const val SIMULATOR_SLICE = "ios-arm64_i386_x86_64-simulator"
+
+ fun sliceFor(target: KonanTarget): String =
+ when (target) {
+ KonanTarget.IOS_ARM64 -> DEVICE_SLICE
+ else -> SIMULATOR_SLICE
+ }
+}
+
+val epsonXcframeworkDir = "${project.projectDir}/nativeInterop/libs/ios/libepos2.xcframework"
+
+val downloadEpsonSdk by tasks.registering {
+ group = "setup"
+ description = "Downloads Epson ePOS SDK xcframework if not already present"
+ outputs.dir(epsonXcframeworkDir)
+ onlyIf { !file(epsonXcframeworkDir).exists() }
+
+ doLast {
+ val tmpDir =
+ layout.buildDirectory
+ .dir("tmp/epson-sdk")
+ .get()
+ .asFile
+ tmpDir.deleteRecursively()
+ tmpDir.mkdirs()
+
+ logger.lifecycle("Downloading Epson ePOS SDK v${EpsonSdk.VERSION} ...")
+ providers
+ .exec {
+ commandLine(
+ "git",
+ "clone",
+ "--depth",
+ "1",
+ "--branch",
+ EpsonSdk.VERSION,
+ EpsonSdk.REPO,
+ tmpDir.absolutePath,
+ )
+ }.result
+ .get()
+
+ file(epsonXcframeworkDir).parentFile.mkdirs()
+ copy {
+ from("${tmpDir.absolutePath}/libepos2.xcframework")
+ into(epsonXcframeworkDir)
+ }
+
+ tmpDir.deleteRecursively()
+ logger.lifecycle("Epson ePOS SDK installed at $epsonXcframeworkDir")
+ }
+}
+
+tasks.configureEach {
+ if (name.startsWith("cinteropEpos2")) dependsOn(downloadEpsonSdk)
+}
+
kotlin {
compilerOptions {
- // removes warnings for expect/actual classes
freeCompilerArgs.add("-Xexpect-actual-classes")
}
androidTarget {
- // Keep JVM target consistent with Java 17
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
@@ -21,26 +87,45 @@ kotlin {
publishLibraryVariants("release")
dependencies {
- // Core Dependencies
implementation(libs.androidx.core)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
-
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
- // Unit tests
testImplementation(libs.bundles.testing)
testImplementation(libs.bundles.robolectric)
- // Android tests
androidTestImplementation(libs.bundles.testing)
androidTestImplementation(libs.bundles.testing.android)
}
}
- val xcfName = "print-engine-epson"
- iosX64 { binaries.framework { baseName = xcfName } }
- iosArm64 { binaries.framework { baseName = xcfName } }
- iosSimulatorArm64 { binaries.framework { baseName = xcfName } }
+ fun org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.configureEpsonInterop() {
+ val slice = EpsonSdk.sliceFor(konanTarget)
+ binaries.framework { baseName = EpsonSdk.FRAMEWORK_NAME }
+ compilations["main"].cinterops {
+ create("epos2") {
+ definitionFile = file("nativeInterop/cinterop/epos2.def")
+ includeDirs(
+ file("nativeInterop/cinterop/shim"),
+ "$epsonXcframeworkDir/$slice/Headers",
+ )
+ }
+ }
+ binaries.all {
+ linkerOpts(
+ "-L$epsonXcframeworkDir/$slice",
+ "-lepos2",
+ "-framework",
+ "CoreBluetooth",
+ "-framework",
+ "ExternalAccessory",
+ )
+ }
+ }
+
+ iosArm64 { configureEpsonInterop() }
+ iosX64 { configureEpsonInterop() }
+ iosSimulatorArm64 { configureEpsonInterop() }
sourceSets {
val commonMain by getting {
@@ -48,6 +133,7 @@ kotlin {
implementation(project(":print-engine"))
implementation(libs.kotlin.coroutines)
+ implementation(compose.ui)
// Utils
implementation(libs.kermit)
diff --git a/print-plugins/epson/nativeInterop/README.md b/print-plugins/epson/nativeInterop/README.md
new file mode 100644
index 0000000..0017b55
--- /dev/null
+++ b/print-plugins/epson/nativeInterop/README.md
@@ -0,0 +1,39 @@
+# Epson iOS SDK Setup
+
+The `libepos2.xcframework` binary is **not committed to git** due to its size (~71 MB).
+
+## Automatic download
+
+The SDK is downloaded automatically on first iOS build. No manual steps needed:
+
+```bash
+./gradlew :print-plugins:epson:cinteropEpos2IosArm64
+```
+
+Or simply build the project — any task that depends on cinterop will trigger the download.
+
+To re-download (e.g. after version bump), delete the cached copy:
+
+```bash
+rm -rf print-plugins/epson/nativeInterop/libs/ios/libepos2.xcframework
+```
+
+## Manual download
+
+If you prefer to download manually:
+
+```bash
+git clone --depth 1 --branch 2.23.1 https://github.com/popina/EpsonSDK.git /tmp/epson_sdk
+mkdir -p print-plugins/epson/nativeInterop/libs/ios
+cp -r /tmp/epson_sdk/libepos2.xcframework print-plugins/epson/nativeInterop/libs/ios/
+rm -rf /tmp/epson_sdk
+```
+
+## SDK version
+
+| SDK Version | Tested |
+|-------------|--------|
+| 2.23.1 | yes |
+
+The SDK is sourced from the CocoaPod mirror at https://github.com/popina/EpsonSDK.
+Original SDK: https://download.epson-biz.com/modules/pos/index.php?page=soft&pcat=3&scat=50
\ No newline at end of file
diff --git a/print-plugins/epson/nativeInterop/cinterop/epos2.def b/print-plugins/epson/nativeInterop/cinterop/epos2.def
new file mode 100644
index 0000000..874f594
--- /dev/null
+++ b/print-plugins/epson/nativeInterop/cinterop/epos2.def
@@ -0,0 +1,3 @@
+language = Objective-C
+headers = ePOS2.h
+package = com.epson.epos2
diff --git a/print-plugins/epson/nativeInterop/cinterop/shim/UIKit/UIKit.h b/print-plugins/epson/nativeInterop/cinterop/shim/UIKit/UIKit.h
new file mode 100644
index 0000000..da582ec
--- /dev/null
+++ b/print-plugins/epson/nativeInterop/cinterop/shim/UIKit/UIKit.h
@@ -0,0 +1,16 @@
+/**
+ * UIKit shim for cinterop commonization.
+ *
+ * ePOS2.h imports
but only uses UIImage.
+ * Importing the full UIKit framework causes the Kotlin/Native commonizer
+ * to crash on types that differ across iOS targets (e.g. UIMenuSystemElementGroupPreference).
+ *
+ * This shim intercepts the import and pulls in only UIImage,
+ * so cinterop resolves it as the real platform.UIKit.UIImage type.
+ */
+#ifndef EPOS2_UIKIT_SHIM_H
+#define EPOS2_UIKIT_SHIM_H
+
+#import
+
+#endif /* EPOS2_UIKIT_SHIM_H */
diff --git a/print-plugins/epson/src/androidInstrumentedTest/kotlin/de/tillhub/printengine/epson/EpsonServiceProviderTest.kt b/print-plugins/epson/src/androidInstrumentedTest/kotlin/de/tillhub/printengine/epson/EpsonServiceProviderTest.kt
deleted file mode 100644
index e295aa2..0000000
--- a/print-plugins/epson/src/androidInstrumentedTest/kotlin/de/tillhub/printengine/epson/EpsonServiceProviderTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.tillhub.printengine.epson
-
-import android.content.Context
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import de.tillhub.printengine.data.ExternalPrinter
-import io.mockk.mockk
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-// TODO this test is not working with minSdk = 24
-@RunWith(AndroidJUnit4::class)
-class EpsonServiceProviderTest {
- @Test
- fun build_shouldReturnEpsonPrintService() {
- val context: Context = ApplicationProvider.getApplicationContext()
- val printer = mockk(relaxed = true)
-
- val result = EpsonServiceProvider.build(context, printer)
-
- assertTrue(result is EpsonPrintService)
- }
-}
diff --git a/print-plugins/epson/src/androidMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.android.kt b/print-plugins/epson/src/androidMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.android.kt
index 12fd83a..6d492f1 100644
--- a/print-plugins/epson/src/androidMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.android.kt
+++ b/print-plugins/epson/src/androidMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.android.kt
@@ -1,6 +1,7 @@
package de.tillhub.printengine.epson
-import android.graphics.Bitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
import com.epson.epos2.Epos2Exception
import de.tillhub.printengine.PrinterController
import de.tillhub.printengine.data.ExternalPrinter
@@ -61,13 +62,14 @@ internal actual class EpsonPrinterController(
)
}
- actual override fun printImage(image: Bitmap) = executeEpsonCommand {
+ actual override fun printImage(image: ImageBitmap) = executeEpsonCommand {
+ val bitmap = image.asAndroidBitmap()
printerWrapper.addImage(
- image,
+ bitmap,
IMAGE_START_XY,
IMAGE_START_XY,
- image.getWidth(),
- image.getHeight(),
+ bitmap.width,
+ bitmap.height,
EpsonPrinter.COLOR_1,
EpsonPrinter.MODE_MONO,
EpsonPrinter.HALFTONE_DITHER,
diff --git a/print-plugins/epson/src/androidUnitTest/kotlin/de/tillhub/printengine/epson/EpsonPrinterControllerTest.kt b/print-plugins/epson/src/androidUnitTest/kotlin/de/tillhub/printengine/epson/EpsonPrinterControllerTest.kt
index 952e98c..f1aefed 100644
--- a/print-plugins/epson/src/androidUnitTest/kotlin/de/tillhub/printengine/epson/EpsonPrinterControllerTest.kt
+++ b/print-plugins/epson/src/androidUnitTest/kotlin/de/tillhub/printengine/epson/EpsonPrinterControllerTest.kt
@@ -1,6 +1,7 @@
package de.tillhub.printengine.epson
import android.graphics.Bitmap
+import androidx.compose.ui.graphics.asImageBitmap
import com.epson.epos2.Epos2Exception
import com.epson.eposprint.Builder.BARCODE_CODE128
import com.epson.eposprint.Builder.COLOR_1
@@ -18,6 +19,7 @@ import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.PrintingFontType
import de.tillhub.printengine.data.PrintingPaperSpec
import io.kotest.core.spec.style.FunSpec
+import io.kotest.extensions.robolectric.RobolectricTest
import io.kotest.matchers.shouldBe
import io.mockk.Ordering
import io.mockk.Runs
@@ -27,6 +29,7 @@ import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
+@RobolectricTest
class EpsonPrinterControllerTest :
FunSpec({
@@ -95,11 +98,8 @@ class EpsonPrinterControllerTest :
}
test("printImage should call addImage with correct parameters") {
- val bitmap =
- mockk {
- every { width } returns 100
- every { height } returns 100
- }
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
+ val imageBitmap = bitmap.asImageBitmap()
every {
printerWrapper.addImage(
any(),
@@ -115,11 +115,11 @@ class EpsonPrinterControllerTest :
)
} just Runs
- controller.printImage(bitmap)
+ controller.printImage(imageBitmap)
verify {
printerWrapper.addImage(
- bitmap = bitmap,
+ bitmap = any(),
x = 0,
y = 0,
width = 100,
diff --git a/print-plugins/epson/src/androidUnitTest/kotlin/de/tillhub/printengine/epson/EpsonServiceProviderTest.kt b/print-plugins/epson/src/androidUnitTest/kotlin/de/tillhub/printengine/epson/EpsonServiceProviderTest.kt
new file mode 100644
index 0000000..f100835
--- /dev/null
+++ b/print-plugins/epson/src/androidUnitTest/kotlin/de/tillhub/printengine/epson/EpsonServiceProviderTest.kt
@@ -0,0 +1,46 @@
+package de.tillhub.printengine.epson
+
+import android.content.Context
+import io.mockk.mockk
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class EpsonServiceProviderTest {
+
+ private lateinit var context: Context
+
+ @BeforeTest
+ fun before() {
+ context = mockk(relaxed = true)
+ }
+
+ @Test
+ fun buildShouldThrowWhenPrinterIsNull() {
+ // When
+ val exception = assertFailsWith {
+ EpsonServiceProvider.build(context, printer = null)
+ }
+
+ // Then
+ assertEquals(
+ "EpsonServiceProvider requires an ExternalPrinter configuration",
+ exception.message,
+ )
+ }
+
+ @Test
+ fun buildShouldThrowWhenPrinterIsNullWithBarcodeProvided() {
+ // When
+ val exception = assertFailsWith {
+ EpsonServiceProvider.build(context, printer = null, barcode = mockk())
+ }
+
+ // Then
+ assertEquals(
+ "EpsonServiceProvider requires an ExternalPrinter configuration",
+ exception.message,
+ )
+ }
+}
diff --git a/print-plugins/epson/src/commonMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.kt b/print-plugins/epson/src/commonMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.kt
index 107889e..a2d7c0a 100644
--- a/print-plugins/epson/src/commonMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.kt
+++ b/print-plugins/epson/src/commonMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.kt
@@ -1,7 +1,7 @@
package de.tillhub.printengine.epson
+import androidx.compose.ui.graphics.ImageBitmap
import de.tillhub.printengine.PrinterController
-import de.tillhub.printengine.data.ImageBitmap
import de.tillhub.printengine.data.PrinterInfo
import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.PrintingFontType
diff --git a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrintService.ios.kt b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrintService.ios.kt
index fe33bca..219c4f2 100644
--- a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrintService.ios.kt
+++ b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrintService.ios.kt
@@ -1,15 +1,128 @@
package de.tillhub.printengine.epson
+import co.touchlab.kermit.Logger
+import com.epson.epos2.EPOS2_MODEL_ANK
+import com.epson.epos2.EPOS2_TM_H6000
+import com.epson.epos2.EPOS2_TM_L100
+import com.epson.epos2.EPOS2_TM_L90
+import com.epson.epos2.EPOS2_TM_L90LFC
+import com.epson.epos2.EPOS2_TM_M10
+import com.epson.epos2.EPOS2_TM_M30
+import com.epson.epos2.EPOS2_TM_M30II
+import com.epson.epos2.EPOS2_TM_M30III
+import com.epson.epos2.EPOS2_TM_M50
+import com.epson.epos2.EPOS2_TM_P20
+import com.epson.epos2.EPOS2_TM_P20II
+import com.epson.epos2.EPOS2_TM_P60
+import com.epson.epos2.EPOS2_TM_P60II
+import com.epson.epos2.EPOS2_TM_P80
+import com.epson.epos2.EPOS2_TM_P80II
+import com.epson.epos2.EPOS2_TM_T100
+import com.epson.epos2.EPOS2_TM_T20
+import com.epson.epos2.EPOS2_TM_T60
+import com.epson.epos2.EPOS2_TM_T70
+import com.epson.epos2.EPOS2_TM_T81
+import com.epson.epos2.EPOS2_TM_T82
+import com.epson.epos2.EPOS2_TM_T83
+import com.epson.epos2.EPOS2_TM_T83III
+import com.epson.epos2.EPOS2_TM_T88
+import com.epson.epos2.EPOS2_TM_T88VII
+import com.epson.epos2.EPOS2_TM_T90
+import com.epson.epos2.EPOS2_TM_T90KP
+import com.epson.epos2.EPOS2_TM_U220
+import com.epson.epos2.EPOS2_TM_U330
+import com.epson.epos2.EPOS2_TS_100
+import com.epson.epos2.Epos2Printer
+import com.epson.epos2.Epos2PrinterStatusInfo
+import com.epson.epos2.Epos2PtrReceiveDelegateProtocol
import de.tillhub.printengine.PrintService
import de.tillhub.printengine.PrinterController
+import de.tillhub.printengine.data.ExternalPrinter
import de.tillhub.printengine.data.PrinterState
+import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import platform.darwin.NSObject
-internal actual class EpsonPrintService : PrintService() {
- actual override var printController: PrinterController?
- get() = TODO("Not yet implemented")
- set(value) {}
+private val logger = Logger.withTag("EpsonPrintService")
- actual override val printerState: Flow
- get() = TODO("Not yet implemented")
+@OptIn(ExperimentalForeignApi::class)
+internal actual class EpsonPrintService(
+ printer: ExternalPrinter,
+) : PrintService() {
+
+ private val connectionState = MutableStateFlow(PrinterState.CheckingForPrinter)
+ actual override val printerState: Flow = connectionState
+
+ private val receiveDelegate = object : NSObject(), Epos2PtrReceiveDelegateProtocol {
+ override fun onPtrReceive(
+ printerObj: Epos2Printer?,
+ code: Int,
+ status: Epos2PrinterStatusInfo?,
+ printJobId: String?,
+ ) {
+ connectionState.value = EpsonPrinterErrorState.epsonStatusToState(code, status)
+ }
+ }
+
+ private val epsonPrinter: Epos2Printer by lazy {
+ connectionState.value = PrinterState.Preparing
+ val series = printer.info.deviceModel.uppercase().toModel()
+ logger.d { "Initializing Epos2Printer series=$series model=${printer.info.deviceModel}" }
+ Epos2Printer(printerSeries = series, lang = EPOS2_MODEL_ANK).apply {
+ setReceiveEventDelegate(receiveDelegate)
+ connectionState.value = PrinterState.Connected
+ }
+ }
+
+ actual override var printController: PrinterController? =
+ EpsonPrinterController(
+ printerData = printer,
+ epsonPrinter = epsonPrinter,
+ printerState = connectionState,
+ )
+
+ @Suppress("CyclomaticComplexMethod")
+ private fun String.toModel(): Int = when (this.substringBefore('_')) {
+ "TM-M10" -> EPOS2_TM_M10
+ "TM-M30" -> EPOS2_TM_M30
+ "TM-P20" -> EPOS2_TM_P20
+ "TM-P60" -> EPOS2_TM_P60
+ "TM-P60II" -> EPOS2_TM_P60II
+ "TM-P80" -> EPOS2_TM_P80
+ "TM-T20" -> EPOS2_TM_T20
+ "TM-T60" -> EPOS2_TM_T60
+ "TM-T70" -> EPOS2_TM_T70
+ "TM-T81" -> EPOS2_TM_T81
+ "TM-T82" -> EPOS2_TM_T82
+ "TM-T83" -> EPOS2_TM_T83
+ "TM-T88" -> EPOS2_TM_T88
+ "TM-T90" -> EPOS2_TM_T90
+ "TM-T90KP" -> EPOS2_TM_T90KP
+ "TM-U220" -> EPOS2_TM_U220
+ "TM-U330" -> EPOS2_TM_U330
+ "TM-L90" -> EPOS2_TM_L90
+ "TM-H6000" -> EPOS2_TM_H6000
+ "TM-T83III" -> EPOS2_TM_T83III
+ "TM-T100" -> EPOS2_TM_T100
+ "TM-M30II" -> EPOS2_TM_M30II
+ "TS-100" -> EPOS2_TS_100
+ "TM-M50" -> EPOS2_TM_M50
+ "TM-T88VII" -> EPOS2_TM_T88VII
+ "TM-L90LFC" -> EPOS2_TM_L90LFC
+ "TM-L100" -> EPOS2_TM_L100
+ "TM-P20II" -> EPOS2_TM_P20II
+ "TM-P80II" -> EPOS2_TM_P80II
+ "TM-M30III" -> EPOS2_TM_M30III
+ // Models not in SDK 2.23.1 — fall back to closest equivalent
+ "TM-M50II", "TM-M55" -> EPOS2_TM_M50
+ "TM-U220II" -> EPOS2_TM_U220
+ "EU-M30" -> EPOS2_TM_M30
+ "SB-H50" -> EPOS2_TM_T88
+ "TM PRINTER", "" -> EPOS2_TM_T88
+ else -> {
+ logger.w { "Unknown printer model '$this', falling back to TM-T88" }
+ EPOS2_TM_T88
+ }
+ }
}
diff --git a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.ios.kt b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.ios.kt
index 686a012..b397d7e 100644
--- a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.ios.kt
+++ b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterController.ios.kt
@@ -1,60 +1,216 @@
package de.tillhub.printengine.epson
+import androidx.compose.ui.graphics.ImageBitmap
+import com.epson.epos2.EPOS2_ALIGN_CENTER
+import com.epson.epos2.EPOS2_BARCODE_CODE128
+import com.epson.epos2.EPOS2_COLOR_1
+import com.epson.epos2.EPOS2_COMPRESS_AUTO
+import com.epson.epos2.EPOS2_CUT_NO_FEED
+import com.epson.epos2.EPOS2_FONT_A
+import com.epson.epos2.EPOS2_HALFTONE_DITHER
+import com.epson.epos2.EPOS2_HRI_BELOW
+import com.epson.epos2.EPOS2_LEVEL_M
+import com.epson.epos2.EPOS2_MODE_MONO
+import com.epson.epos2.EPOS2_PARAM_DEFAULT
+import com.epson.epos2.EPOS2_PARAM_UNSPECIFIED
+import com.epson.epos2.EPOS2_SUCCESS
+import com.epson.epos2.EPOS2_SYMBOL_QRCODE_MODEL_1
+import com.epson.epos2.EPOS2_TRUE
+import com.epson.epos2.Epos2Printer
import de.tillhub.printengine.PrinterController
-import de.tillhub.printengine.data.ImageBitmap
+import de.tillhub.printengine.data.ExternalPrinter
import de.tillhub.printengine.data.PrinterInfo
import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.PrintingFontType
import de.tillhub.printengine.data.PrintingIntensity
import de.tillhub.printengine.data.RawPrinterData
+import de.tillhub.printengine.data.encodeToPngBytes
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.addressOf
+import kotlinx.cinterop.usePinned
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import platform.Foundation.NSData
+import platform.Foundation.create
+import platform.UIKit.UIImage
-internal actual class EpsonPrinterController : PrinterController {
- actual override fun sendRawData(data: RawPrinterData) {
- TODO("Not yet implemented")
+@Suppress("TooManyFunctions")
+@OptIn(ExperimentalForeignApi::class, kotlinx.cinterop.BetaInteropApi::class)
+internal actual class EpsonPrinterController(
+ private val printerData: ExternalPrinter,
+ private val epsonPrinter: Epos2Printer,
+ private val printerState: MutableStateFlow,
+) : PrinterController {
+
+ actual override fun sendRawData(data: RawPrinterData) = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addTextAlign(EPOS2_ALIGN_CENTER), "addTextAlign")
+ checkSdkResult(epsonPrinter.addCommand(data.bytes.toNSData()), "addCommand")
}
- actual override fun observePrinterState(): Flow {
- TODO("Not yet implemented")
+ actual override fun observePrinterState(): Flow = printerState
+
+ actual override fun setFontSize(fontSize: PrintingFontType) = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addTextAlign(EPOS2_ALIGN_CENTER), "addTextAlign")
+ checkSdkResult(
+ epsonPrinter.addTextFont(
+ when (fontSize) {
+ PrintingFontType.DEFAULT_FONT_SIZE -> EPOS2_FONT_A
+ },
+ ),
+ "addTextFont",
+ )
}
- actual override fun setFontSize(fontSize: PrintingFontType) {
- TODO("Not yet implemented")
+ actual override fun printText(text: String) = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addTextAlign(EPOS2_ALIGN_CENTER), "addTextAlign")
+ checkSdkResult(epsonPrinter.addText("$text\n"), "addText")
}
- actual override fun printText(text: String) {
- TODO("Not yet implemented")
+ actual override fun printBarcode(barcode: String) = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addTextAlign(EPOS2_ALIGN_CENTER), "addTextAlign")
+ // "{B" forces Code Set B — same convention as Android
+ checkSdkResult(
+ epsonPrinter.addBarcode(
+ "{B$barcode",
+ EPOS2_BARCODE_CODE128,
+ EPOS2_HRI_BELOW,
+ EPOS2_FONT_A,
+ BARCODE_MODULE_WIDTH.toLong(),
+ BARCODE_HEIGHT.toLong(),
+ ),
+ "addBarcode",
+ )
+ checkSdkResult(epsonPrinter.addFeedLine(THREE_FEED_LINES.toLong()), "addFeedLine")
}
- actual override fun printBarcode(barcode: String) {
- TODO("Not yet implemented")
+ actual override fun printQr(qrData: String) = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addTextAlign(EPOS2_ALIGN_CENTER), "addTextAlign")
+ checkSdkResult(
+ epsonPrinter.addSymbol(
+ qrData,
+ EPOS2_SYMBOL_QRCODE_MODEL_1,
+ EPOS2_LEVEL_M,
+ QR_DIMENSION.toLong(),
+ QR_DIMENSION.toLong(),
+ EPOS2_PARAM_UNSPECIFIED.toLong(),
+ ),
+ "addSymbol",
+ )
}
- actual override fun printQr(qrData: String) {
- TODO("Not yet implemented")
+ actual override fun printImage(image: ImageBitmap) = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addTextAlign(EPOS2_ALIGN_CENTER), "addTextAlign")
+ val uiImage = image.toUIImage() ?: return@executeEpsonCommand
+ checkSdkResult(
+ epsonPrinter.addImage(
+ uiImage,
+ IMAGE_START_XY.toLong(),
+ IMAGE_START_XY.toLong(),
+ image.width.toLong(),
+ image.height.toLong(),
+ EPOS2_COLOR_1,
+ EPOS2_MODE_MONO,
+ EPOS2_HALFTONE_DITHER,
+ IMAGE_BRIGHTNESS,
+ EPOS2_COMPRESS_AUTO,
+ ),
+ "addImage",
+ )
}
- actual override fun printImage(image: ImageBitmap) {
- TODO("Not yet implemented")
+ actual override fun feedPaper() = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addFeedLine(ONE_FEED_LINE.toLong()), "addFeedLine")
}
- actual override fun feedPaper() {
- TODO("Not yet implemented")
+ actual override fun cutPaper() = executeEpsonCommand {
+ checkSdkResult(epsonPrinter.addCut(EPOS2_CUT_NO_FEED), "addCut")
}
- actual override fun cutPaper() {
- TODO("Not yet implemented")
+ actual override fun setIntensity(intensity: PrintingIntensity) = Unit
+
+ actual override fun start() {
+ var connected = false
+ executeEpsonCommand {
+ val status = epsonPrinter.getStatus()
+ if (status?.connection != EPOS2_TRUE) {
+ checkSdkResult(
+ epsonPrinter.connect(printerData.getTarget(), EPOS2_PARAM_DEFAULT.toLong()),
+ "connect",
+ )
+ }
+ connected = true
+ checkSdkResult(epsonPrinter.sendData(EPOS2_PARAM_DEFAULT.toLong()), "sendData")
+ }
+ epsonPrinter.clearCommandBuffer()
+ if (connected) {
+ try {
+ epsonPrinter.disconnect()
+ } catch (_: Exception) {
+ // Disconnect failure is non-critical; connection will be re-established next print
+ }
+ }
}
- actual override fun setIntensity(intensity: PrintingIntensity) {
- TODO("Not yet implemented")
+ actual override suspend fun getPrinterInfo(): PrinterInfo = printerData.info
+
+ /**
+ * Checks an Epson SDK return code and throws [EpsonSdkException] on failure.
+ * iOS SDK methods return int codes (0 = success) rather than throwing exceptions,
+ * so we must check every return value explicitly.
+ */
+ private fun checkSdkResult(result: Int, operation: String) {
+ if (result != EPOS2_SUCCESS) {
+ throw EpsonSdkException(result, operation)
+ }
}
- actual override fun start() {
- TODO("Not yet implemented")
+ private fun executeEpsonCommand(command: () -> Unit) {
+ try {
+ command.invoke()
+ } catch (e: EpsonSdkException) {
+ printerState.value = EpsonPrinterErrorState.epsonErrorStatusToState(e.errorCode)
+ epsonPrinter.clearCommandBuffer()
+ } catch (e: Exception) {
+ printerState.value = PrinterState.Error.Epson.InternalError
+ epsonPrinter.clearCommandBuffer()
+ }
}
- actual override suspend fun getPrinterInfo(): PrinterInfo {
- TODO("Not yet implemented")
+ private fun ExternalPrinter.getTarget() = "${connectionType.value}:$connectionAddress"
+
+ @OptIn(ExperimentalForeignApi::class)
+ private fun ByteArray.toNSData(): NSData {
+ if (isEmpty()) return NSData()
+ return usePinned { pinned ->
+ NSData.create(bytes = pinned.addressOf(0), length = size.toULong())
+ }
+ }
+
+ @OptIn(ExperimentalForeignApi::class)
+ private fun ImageBitmap.toUIImage(): UIImage? {
+ val pngBytes = encodeToPngBytes() ?: return null
+ val nsData = pngBytes.usePinned { pinned ->
+ NSData.create(bytes = pinned.addressOf(0), length = pngBytes.size.toULong())
+ }
+ return UIImage(data = nsData)
+ }
+
+ private companion object {
+ const val QR_DIMENSION = 240
+ const val BARCODE_HEIGHT = 100
+ const val BARCODE_MODULE_WIDTH = 3
+ const val ONE_FEED_LINE = 1
+ const val THREE_FEED_LINES = 3
+ const val IMAGE_BRIGHTNESS = 1.0
+ const val IMAGE_START_XY = 0
}
}
+
+/**
+ * Exception thrown when an Epson SDK method returns a non-zero error code on iOS.
+ * On Android, the SDK throws Epos2Exception directly; on iOS, we must check return values.
+ */
+internal class EpsonSdkException(
+ val errorCode: Int,
+ val operation: String,
+) : Exception("Epson SDK $operation failed with error code: $errorCode")
diff --git a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt
index 08c0553..133bfac 100644
--- a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt
+++ b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt
@@ -1,10 +1,102 @@
package de.tillhub.printengine.epson
+import com.epson.epos2.Epos2DeviceInfo
+import com.epson.epos2.Epos2Discovery
+import com.epson.epos2.Epos2DiscoveryDelegateProtocol
+import com.epson.epos2.Epos2FilterOption
+import de.tillhub.printengine.data.ConnectionType
import de.tillhub.printengine.data.DiscoveryState
+import de.tillhub.printengine.data.ExternalPrinter
+import de.tillhub.printengine.data.PrinterInfo
+import de.tillhub.printengine.data.PrinterServiceVersion
+import de.tillhub.printengine.data.PrintingFontType
+import de.tillhub.printengine.data.PrintingPaperSpec
import de.tillhub.printengine.external.PrinterDiscovery
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.withContext
+import platform.darwin.NSObject
+@OptIn(ExperimentalForeignApi::class)
actual class EpsonPrinterDiscovery : PrinterDiscovery {
+
+ private companion object {
+ const val CHARACTER_COUNT = 42
+ const val MANUFACTURER_EPSON = "EPSON"
+
+ // ePOS2.h enum values
+ const val EPOS2_SUCCESS = 0
+ const val EPOS2_PORTTYPE_ALL = 0
+ const val EPOS2_TYPE_PRINTER = 1
+ const val EPOS2_MODEL_ALL = 0
+ }
+
actual override val observePrinters: Flow
- get() = TODO("Not yet implemented")
+ get() = callbackFlow {
+ trySend(DiscoveryState.Idle)
+
+ val discoveredPrinters = mutableMapOf()
+
+ val filterOption = Epos2FilterOption().apply {
+ portType = EPOS2_PORTTYPE_ALL
+ deviceType = EPOS2_TYPE_PRINTER
+ deviceModel = EPOS2_MODEL_ALL
+ }
+
+ // Epson SDK dispatches onDiscovery callbacks sequentially on its internal thread
+ val discoveryDelegate = object : NSObject(), Epos2DiscoveryDelegateProtocol {
+ override fun onDiscovery(deviceInfo: Epos2DeviceInfo?) {
+ val info = deviceInfo ?: return
+ if (!info.isValid()) return
+ val printer = createPrinter(info)
+ discoveredPrinters[printer.connectionAddress] = printer
+ trySend(DiscoveryState.Discovering(discoveredPrinters.values.toList()))
+ }
+ }
+
+ val result = withContext(Dispatchers.Main) {
+ Epos2Discovery.start(filterOption, discoveryDelegate)
+ }
+ if (result != EPOS2_SUCCESS) {
+ trySend(DiscoveryState.Error("Discovery start failed with code: $result"))
+ }
+
+ awaitClose {
+ Epos2Discovery.stop()
+ // Reference delegate to prevent Kotlin/Native GC from collecting it
+ // while the Epson SDK still holds a weak reference to it
+ discoveryDelegate.hashCode()
+ }
+ }
+
+ private fun createPrinter(deviceInfo: Epos2DeviceInfo): ExternalPrinter {
+ val target = deviceInfo.target ?: ""
+ val dividerIdx = target.indexOf(':')
+ val protocol = if (dividerIdx >= 0) target.substring(0, dividerIdx) else ""
+ val address = if (dividerIdx >= 0) target.substring(dividerIdx + 1) else target
+
+ return ExternalPrinter(
+ info = PrinterInfo(
+ serialNumber = "n/a",
+ deviceModel = deviceInfo.deviceName ?: "",
+ printerVersion = "n/a",
+ printerPaperSpec = PrintingPaperSpec.External(CHARACTER_COUNT),
+ printingFontType = PrintingFontType.DEFAULT_FONT_SIZE,
+ printerHead = "n/a",
+ printedDistance = 0,
+ serviceVersion = PrinterServiceVersion.Unknown,
+ ),
+ manufacturer = MANUFACTURER_EPSON,
+ connectionAddress = address,
+ connectionType = protocol.toConnectionType(),
+ )
+ }
+
+ private fun String.toConnectionType(): ConnectionType = ConnectionType.entries.find { it.value == this }
+ ?: ConnectionType.LAN
+
+ private fun Epos2DeviceInfo.isValid(): Boolean = deviceType == EPOS2_TYPE_PRINTER && !deviceName.isNullOrEmpty()
}
diff --git a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterErrorState.ios.kt b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterErrorState.ios.kt
new file mode 100644
index 0000000..615fcb3
--- /dev/null
+++ b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterErrorState.ios.kt
@@ -0,0 +1,81 @@
+package de.tillhub.printengine.epson
+
+import com.epson.epos2.EPOS2_AUTOCUTTER_ERR
+import com.epson.epos2.EPOS2_AUTORECOVER_ERR
+import com.epson.epos2.EPOS2_BATTERY_LEVEL_0
+import com.epson.epos2.EPOS2_BATTERY_OVERHEAT
+import com.epson.epos2.EPOS2_CODE_PRINTING
+import com.epson.epos2.EPOS2_CODE_SUCCESS
+import com.epson.epos2.EPOS2_COVER_OPEN
+import com.epson.epos2.EPOS2_ERR_CONNECT
+import com.epson.epos2.EPOS2_ERR_DISCONNECT
+import com.epson.epos2.EPOS2_ERR_MEMORY
+import com.epson.epos2.EPOS2_ERR_PARAM
+import com.epson.epos2.EPOS2_ERR_TIMEOUT
+import com.epson.epos2.EPOS2_FALSE
+import com.epson.epos2.EPOS2_HEAD_OVERHEAT
+import com.epson.epos2.EPOS2_MECHANICAL_ERR
+import com.epson.epos2.EPOS2_MOTOR_OVERHEAT
+import com.epson.epos2.EPOS2_NO_ERR
+import com.epson.epos2.EPOS2_PAPER_EMPTY
+import com.epson.epos2.EPOS2_TRUE
+import com.epson.epos2.EPOS2_UNKNOWN
+import com.epson.epos2.EPOS2_UNRECOVER_ERR
+import com.epson.epos2.EPOS2_WRONG_PAPER
+import com.epson.epos2.Epos2PrinterStatusInfo
+import de.tillhub.printengine.data.PrinterState
+import kotlinx.cinterop.ExperimentalForeignApi
+
+// Epos2UnrecoverError enum values — not commonized by cinterop across iOS targets
+private const val EPOS2_HIGH_VOLTAGE_ERR = 0
+private const val EPOS2_LOW_VOLTAGE_ERR = 1
+
+@OptIn(ExperimentalForeignApi::class)
+internal object EpsonPrinterErrorState {
+
+ fun epsonErrorStatusToState(errorCode: Int): PrinterState = when (errorCode) {
+ EPOS2_ERR_PARAM -> PrinterState.Error.Epson.DataError
+ EPOS2_ERR_CONNECT -> PrinterState.Error.NotAvailable
+ EPOS2_ERR_TIMEOUT -> PrinterState.Error.ConnectionLost
+ EPOS2_ERR_DISCONNECT -> PrinterState.Error.ConnectionLost
+ EPOS2_ERR_MEMORY -> PrinterState.Error.Epson.MemoryError
+ else -> PrinterState.Error.Epson.InternalError
+ }
+
+ fun epsonStatusToState(code: Int, status: Epos2PrinterStatusInfo?): PrinterState = when (code) {
+ EPOS2_CODE_SUCCESS -> PrinterState.Connected
+ EPOS2_CODE_PRINTING -> PrinterState.Busy
+ else -> if (status != null) evaluateStatusInfo(status) else PrinterState.Error.Epson.InternalError
+ }
+
+ private fun evaluateStatusInfo(status: Epos2PrinterStatusInfo): PrinterState = when {
+ status.online == EPOS2_FALSE -> PrinterState.Error.ConnectionLost
+ status.paper == EPOS2_PAPER_EMPTY -> PrinterState.Error.OutOfPaper
+ status.coverOpen == EPOS2_TRUE -> PrinterState.Error.CoverNotClosed
+ status.errorStatus == EPOS2_NO_ERR -> PrinterState.Connected
+ status.errorStatus == EPOS2_MECHANICAL_ERR ||
+ status.errorStatus == EPOS2_UNKNOWN -> PrinterState.Error.Malfunctions
+ status.errorStatus == EPOS2_AUTOCUTTER_ERR -> PrinterState.Error.PaperCutterAbnormal
+ status.errorStatus == EPOS2_UNRECOVER_ERR -> handleUnrecoverableError(status)
+ status.errorStatus == EPOS2_AUTORECOVER_ERR -> handleAutoRecoverableError(status)
+ status.batteryLevel == EPOS2_BATTERY_LEVEL_0 -> PrinterState.Error.VoltageTooLow
+ else -> PrinterState.Connected
+ }
+
+ private fun handleUnrecoverableError(status: Epos2PrinterStatusInfo): PrinterState = when (status.unrecoverError) {
+ EPOS2_LOW_VOLTAGE_ERR,
+ EPOS2_HIGH_VOLTAGE_ERR,
+ -> PrinterState.Error.VoltageTooLow
+ else -> PrinterState.Error.Malfunctions
+ }
+
+ private fun handleAutoRecoverableError(status: Epos2PrinterStatusInfo): PrinterState = when (status.autoRecoverError) {
+ EPOS2_HEAD_OVERHEAT,
+ EPOS2_MOTOR_OVERHEAT,
+ EPOS2_BATTERY_OVERHEAT,
+ -> PrinterState.Error.Overheated
+ EPOS2_WRONG_PAPER -> PrinterState.Error.PaperAbnormal
+ EPOS2_COVER_OPEN -> PrinterState.Error.CoverNotClosed
+ else -> PrinterState.Error.Malfunctions
+ }
+}
diff --git a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonServiceProvider.ios.kt b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonServiceProvider.ios.kt
index e46081e..fecccc5 100644
--- a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonServiceProvider.ios.kt
+++ b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonServiceProvider.ios.kt
@@ -1,5 +1,18 @@
package de.tillhub.printengine.epson
+import de.tillhub.printengine.PrintService
import de.tillhub.printengine.PrinterServiceProvider
+import de.tillhub.printengine.barcode.BarcodeEncoder
+import de.tillhub.printengine.data.ExternalPrinter
-actual object EpsonServiceProvider : PrinterServiceProvider
+actual object EpsonServiceProvider : PrinterServiceProvider {
+ override fun build(
+ printer: ExternalPrinter?,
+ barcode: BarcodeEncoder?,
+ ): PrintService {
+ requireNotNull(printer) {
+ "EpsonServiceProvider requires an ExternalPrinter configuration"
+ }
+ return EpsonPrintService(printer)
+ }
+}
diff --git a/print-plugins/pax/build.gradle.kts b/print-plugins/pax/build.gradle.kts
index 7be22e6..e09a155 100644
--- a/print-plugins/pax/build.gradle.kts
+++ b/print-plugins/pax/build.gradle.kts
@@ -4,6 +4,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinAndroid)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.compose)
alias(libs.plugins.maven.publish)
}
@@ -58,6 +60,7 @@ dependencies {
// Core Dependencies
implementation(libs.androidx.core)
implementation(libs.kotlin.coroutines)
+ implementation(compose.ui)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
// Pax Dependencies
diff --git a/print-plugins/pax/src/test/java/de/tillhub/printengine/pax/PaxPrinterControllerTest.kt b/print-plugins/pax/src/test/java/de/tillhub/printengine/pax/PaxPrinterControllerTest.kt
index 2aadcf6..a42e14e 100644
--- a/print-plugins/pax/src/test/java/de/tillhub/printengine/pax/PaxPrinterControllerTest.kt
+++ b/print-plugins/pax/src/test/java/de/tillhub/printengine/pax/PaxPrinterControllerTest.kt
@@ -1,6 +1,8 @@
package de.tillhub.printengine.pax
import android.graphics.Bitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
import de.tillhub.printengine.barcode.BarcodeEncoder
import de.tillhub.printengine.barcode.BarcodeType
import de.tillhub.printengine.data.PrinterInfo
@@ -27,14 +29,16 @@ import kotlinx.coroutines.flow.first
internal class PaxPrinterControllerTest :
DescribeSpec({
- lateinit var bitmap: Bitmap
+ lateinit var androidBitmap: Bitmap
+ lateinit var bitmap: ImageBitmap
lateinit var printerState: MutableStateFlow
lateinit var printService: DirectPrintService
lateinit var barcodeEncoder: BarcodeEncoder
lateinit var target: PaxPrinterController
beforeSpec {
- bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
+ androidBitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
+ bitmap = androidBitmap.asImageBitmap()
}
beforeTest {
@@ -58,7 +62,7 @@ internal class PaxPrinterControllerTest :
}
afterSpec {
- bitmap.recycle()
+ androidBitmap.recycle()
}
it("getPrinterInfo") {
diff --git a/print-plugins/star/build.gradle.kts b/print-plugins/star/build.gradle.kts
index 24e3607..a1a989e 100644
--- a/print-plugins/star/build.gradle.kts
+++ b/print-plugins/star/build.gradle.kts
@@ -3,6 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.compose)
alias(libs.plugins.maven.publish)
}
@@ -45,6 +47,7 @@ kotlin {
implementation(project(":print-engine"))
implementation(libs.kotlin.coroutines)
+ implementation(compose.ui)
// Utils
implementation(libs.kermit)
diff --git a/print-plugins/star/src/androidMain/kotlin/de/tillhub/printengine/star/StarPrinterController.android.kt b/print-plugins/star/src/androidMain/kotlin/de/tillhub/printengine/star/StarPrinterController.android.kt
index 097afa1..c0d38c7 100644
--- a/print-plugins/star/src/androidMain/kotlin/de/tillhub/printengine/star/StarPrinterController.android.kt
+++ b/print-plugins/star/src/androidMain/kotlin/de/tillhub/printengine/star/StarPrinterController.android.kt
@@ -1,6 +1,7 @@
package de.tillhub.printengine.star
-import android.graphics.Bitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
import com.starmicronics.stario10.StarIO10Exception
import com.starmicronics.stario10.StarPrinter
import com.starmicronics.stario10.starxpandcommand.DocumentBuilder
@@ -78,9 +79,9 @@ internal actual class StarPrinterController(
).styleAlignment(Alignment.Center)
}
- actual override fun printImage(image: Bitmap) {
+ actual override fun printImage(image: ImageBitmap) {
printerBuilder
- .actionPrintImage(ImageParameter(image, IMAGE_WIDTH))
+ .actionPrintImage(ImageParameter(image.asAndroidBitmap(), IMAGE_WIDTH))
.styleAlignment(Alignment.Center)
}
diff --git a/print-plugins/star/src/androidUnitTest/kotlin/de/tillhub/printengine/star/StarPrinterControllerTest.kt b/print-plugins/star/src/androidUnitTest/kotlin/de/tillhub/printengine/star/StarPrinterControllerTest.kt
index 279dc6b..90b8c18 100644
--- a/print-plugins/star/src/androidUnitTest/kotlin/de/tillhub/printengine/star/StarPrinterControllerTest.kt
+++ b/print-plugins/star/src/androidUnitTest/kotlin/de/tillhub/printengine/star/StarPrinterControllerTest.kt
@@ -1,6 +1,7 @@
package de.tillhub.printengine.star
import android.graphics.Bitmap
+import androidx.compose.ui.graphics.asImageBitmap
import com.starmicronics.stario10.StarIO10ErrorCode
import com.starmicronics.stario10.StarIO10Exception
import com.starmicronics.stario10.StarPrinter
@@ -14,6 +15,7 @@ import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.PrintingFontType
import de.tillhub.printengine.data.RawPrinterData
import io.kotest.core.spec.style.FunSpec
+import io.kotest.extensions.robolectric.RobolectricTest
import io.kotest.matchers.shouldBe
import io.mockk.coEvery
import io.mockk.coVerifyOrder
@@ -28,6 +30,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@OptIn(ExperimentalCoroutinesApi::class)
+@RobolectricTest
class StarPrinterControllerTest :
FunSpec({
lateinit var starPrinter: StarPrinter
@@ -130,9 +133,10 @@ class StarPrinterControllerTest :
}
test("printImage adds image with correct parameters") {
- val bitmap = mockk()
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
+ val imageBitmap = bitmap.asImageBitmap()
- controller.printImage(bitmap)
+ controller.printImage(imageBitmap)
verifyOrder {
printerBuilder.actionPrintImage(any())
diff --git a/print-plugins/star/src/commonMain/kotlin/de/tillhub/printengine/star/StarPrinterController.kt b/print-plugins/star/src/commonMain/kotlin/de/tillhub/printengine/star/StarPrinterController.kt
index c55f1c3..fd2ac1d 100644
--- a/print-plugins/star/src/commonMain/kotlin/de/tillhub/printengine/star/StarPrinterController.kt
+++ b/print-plugins/star/src/commonMain/kotlin/de/tillhub/printengine/star/StarPrinterController.kt
@@ -1,7 +1,7 @@
package de.tillhub.printengine.star
+import androidx.compose.ui.graphics.ImageBitmap
import de.tillhub.printengine.PrinterController
-import de.tillhub.printengine.data.ImageBitmap
import de.tillhub.printengine.data.PrinterInfo
import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.PrintingFontType
diff --git a/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarPrinterController.ios.kt b/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarPrinterController.ios.kt
index 1521d88..0f9a4b5 100644
--- a/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarPrinterController.ios.kt
+++ b/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarPrinterController.ios.kt
@@ -1,7 +1,7 @@
package de.tillhub.printengine.star
+import androidx.compose.ui.graphics.ImageBitmap
import de.tillhub.printengine.PrinterController
-import de.tillhub.printengine.data.ImageBitmap
import de.tillhub.printengine.data.PrinterInfo
import de.tillhub.printengine.data.PrinterState
import de.tillhub.printengine.data.PrintingFontType
diff --git a/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarServiceProvider.ios.kt b/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarServiceProvider.ios.kt
index e5cb296..41ab468 100644
--- a/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarServiceProvider.ios.kt
+++ b/print-plugins/star/src/iosMain/kotlin/de/tillhub/printengine/star/StarServiceProvider.ios.kt
@@ -1,5 +1,13 @@
package de.tillhub.printengine.star
+import de.tillhub.printengine.PrintService
import de.tillhub.printengine.PrinterServiceProvider
+import de.tillhub.printengine.barcode.BarcodeEncoder
+import de.tillhub.printengine.data.ExternalPrinter
-actual object StarServiceProvider : PrinterServiceProvider
+actual object StarServiceProvider : PrinterServiceProvider {
+ override fun build(
+ printer: ExternalPrinter?,
+ barcode: BarcodeEncoder?,
+ ): PrintService = StarPrintService()
+}
diff --git a/print-plugins/sunmi/build.gradle.kts b/print-plugins/sunmi/build.gradle.kts
index c528969..f664105 100644
--- a/print-plugins/sunmi/build.gradle.kts
+++ b/print-plugins/sunmi/build.gradle.kts
@@ -3,6 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinAndroid)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.compose)
}
android {
@@ -56,6 +58,7 @@ dependencies {
// Core Dependencies
implementation(libs.androidx.core)
implementation(libs.kotlin.coroutines)
+ implementation(compose.ui)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
// Sunmi Dependencies
diff --git a/print-plugins/sunmi/src/main/java/de/tillhub/printengine/sunmi/SunmiPrinterController.kt b/print-plugins/sunmi/src/main/java/de/tillhub/printengine/sunmi/SunmiPrinterController.kt
index 6e15b98..de5dfb7 100644
--- a/print-plugins/sunmi/src/main/java/de/tillhub/printengine/sunmi/SunmiPrinterController.kt
+++ b/print-plugins/sunmi/src/main/java/de/tillhub/printengine/sunmi/SunmiPrinterController.kt
@@ -1,7 +1,8 @@
package de.tillhub.printengine.sunmi
-import android.graphics.Bitmap
import android.os.RemoteException
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
import co.touchlab.kermit.Logger
import com.sunmi.peripheral.printer.InnerResultCallback
import com.sunmi.peripheral.printer.SunmiPrinterService
@@ -94,9 +95,9 @@ internal class SunmiPrinterController(
printerService.lineWrap(1, printListener)
}
- override fun printImage(image: Bitmap) {
+ override fun printImage(image: ImageBitmap) {
printerService.setAlignment(Alignment.CENTER.value, printListener)
- printerService.printBitmapCustom(image, ImagePrintingMethod.GRAYSCALE.value, printListener)
+ printerService.printBitmapCustom(image.asAndroidBitmap(), ImagePrintingMethod.GRAYSCALE.value, printListener)
printerService.setAlignment(Alignment.LEFT.value, printListener)
printerService.lineWrap(1, printListener)
}
diff --git a/print-plugins/sunmi/src/test/java/de/tillhub/printengine/sunmi/SunmiPrinterControllerTest.kt b/print-plugins/sunmi/src/test/java/de/tillhub/printengine/sunmi/SunmiPrinterControllerTest.kt
index 01ea099..94e23b1 100644
--- a/print-plugins/sunmi/src/test/java/de/tillhub/printengine/sunmi/SunmiPrinterControllerTest.kt
+++ b/print-plugins/sunmi/src/test/java/de/tillhub/printengine/sunmi/SunmiPrinterControllerTest.kt
@@ -2,6 +2,7 @@ package de.tillhub.printengine.sunmi
import android.graphics.Bitmap
import android.os.RemoteException
+import androidx.compose.ui.graphics.asImageBitmap
import com.sunmi.peripheral.printer.SunmiPrinterService
import de.tillhub.printengine.PrinterController
import de.tillhub.printengine.data.PrinterServiceVersion
@@ -103,11 +104,11 @@ internal class SunmiPrinterControllerTest :
test("printImage") {
val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
- printerController.printImage(bitmap)
+ printerController.printImage(bitmap.asImageBitmap())
verify(ordering = Ordering.ORDERED) {
printerService.setAlignment(1, any())
- printerService.printBitmapCustom(bitmap, 2, any())
+ printerService.printBitmapCustom(any(), 2, any())
printerService.setAlignment(0, any())
printerService.lineWrap(1, any())
}
diff --git a/print-plugins/verifone/build.gradle.kts b/print-plugins/verifone/build.gradle.kts
index a088b37..4d44f80 100644
--- a/print-plugins/verifone/build.gradle.kts
+++ b/print-plugins/verifone/build.gradle.kts
@@ -4,6 +4,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinAndroid)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.compose)
alias(libs.plugins.maven.publish)
}
@@ -59,6 +61,7 @@ dependencies {
// Core Dependencies
implementation(libs.androidx.core)
implementation(libs.kotlin.coroutines)
+ implementation(compose.ui)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
// Utils
diff --git a/print-plugins/verifone/src/test/java/de/tillhub/printengine/verifone/VerifonePrintControllerTest.kt b/print-plugins/verifone/src/test/java/de/tillhub/printengine/verifone/VerifonePrintControllerTest.kt
index 460059f..c9906f4 100644
--- a/print-plugins/verifone/src/test/java/de/tillhub/printengine/verifone/VerifonePrintControllerTest.kt
+++ b/print-plugins/verifone/src/test/java/de/tillhub/printengine/verifone/VerifonePrintControllerTest.kt
@@ -1,6 +1,8 @@
package de.tillhub.printengine.verifone
import android.graphics.Bitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
import com.verifone.peripherals.DirectPrintManager
import com.verifone.peripherals.Printer
import de.tillhub.printengine.PrinterController
@@ -29,14 +31,16 @@ import kotlinx.coroutines.flow.first
internal class VerifonePrintControllerTest :
DescribeSpec({
- lateinit var bitmap: Bitmap
+ lateinit var androidBitmap: Bitmap
+ lateinit var bitmap: ImageBitmap
lateinit var printManager: DirectPrintManager
lateinit var printerState: MutableStateFlow
lateinit var barcodeEncoder: BarcodeEncoder
lateinit var printerController: PrinterController
beforeSpec {
- bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ androidBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ bitmap = androidBitmap.asImageBitmap()
}
beforeTest {
@@ -60,7 +64,7 @@ internal class VerifonePrintControllerTest :
}
afterSpec {
- bitmap.recycle()
+ androidBitmap.recycle()
}
it("observePrinterState") {
diff --git a/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt b/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt
index 506f1f6..924e16b 100644
--- a/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt
+++ b/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt
@@ -30,6 +30,7 @@ fun PrinterLayout(
printState: PrinterState,
printers: List,
onPrinterSelected: (ExternalPrinter?) -> Unit,
+ discoveryStatus: String? = null,
) {
Column(
modifier = Modifier.fillMaxSize(),
@@ -41,6 +42,14 @@ fun PrinterLayout(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
)
+ if (discoveryStatus != null) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = discoveryStatus,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
Spacer(modifier = Modifier.height(36.dp))
Button(onClick = { onPrinterSelected(null) }) {
Text(text = "Print sample job")
diff --git a/sample/src/iosMain/kotlin/de/tillhub/printengine/sample/MainViewController.kt b/sample/src/iosMain/kotlin/de/tillhub/printengine/sample/MainViewController.kt
new file mode 100644
index 0000000..890ab7c
--- /dev/null
+++ b/sample/src/iosMain/kotlin/de/tillhub/printengine/sample/MainViewController.kt
@@ -0,0 +1,154 @@
+package de.tillhub.printengine.sample
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.window.ComposeUIViewController
+import de.tillhub.printengine.PrintEngine
+import de.tillhub.printengine.data.DiscoveryState
+import de.tillhub.printengine.data.ExternalPrinter
+import de.tillhub.printengine.data.PrintCommand
+import de.tillhub.printengine.data.PrintJob
+import de.tillhub.printengine.data.PrinterState
+import de.tillhub.printengine.data.PrintingIntensity
+import de.tillhub.printengine.epson.EpsonPrinterDiscovery
+import de.tillhub.printengine.epson.EpsonServiceProvider
+import de.tillhub.printengine.sample.ui.PrinterLayout
+import de.tillhub.printengine.sample.ui.theme.TillhubPrintEngineTheme
+import kotlinx.coroutines.launch
+import platform.UIKit.UIViewController
+
+@Suppress("ktlint:standard:function-naming")
+fun MainViewController(): UIViewController = ComposeUIViewController {
+ val printEngine = remember {
+ PrintEngine.getInstance().also {
+ it.printer.settings.printingIntensity = PrintingIntensity.DEFAULT
+ }
+ }
+
+ val printers = remember { mutableStateListOf() }
+ var initialized by remember { mutableStateOf(false) }
+ var discoveryStatus by remember { mutableStateOf("Searching for printers…") }
+ val scope = rememberCoroutineScope()
+
+ LaunchedEffect(Unit) {
+ printEngine.discoverExternalPrinters(
+ EpsonPrinterDiscovery(),
+ ).collect { state ->
+ when (state) {
+ is DiscoveryState.Idle -> discoveryStatus = "Searching for printers…"
+ is DiscoveryState.Discovering -> {
+ printers.clear()
+ printers.addAll(state.printers)
+ discoveryStatus = "Found ${state.printers.size} printer(s)"
+ }
+ is DiscoveryState.Finished -> {
+ printers.clear()
+ printers.addAll(state.printers)
+ discoveryStatus = "Discovery done: ${state.printers.size} printer(s)"
+ }
+ is DiscoveryState.Error -> {
+ discoveryStatus = "Discovery error: ${state.message}"
+ }
+ }
+ }
+ }
+
+ val printState by printEngine.printer.printerState
+ .collectAsState(PrinterState.CheckingForPrinter)
+
+ TillhubPrintEngineTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ PrinterLayout(
+ printState = printState,
+ discoveryStatus = discoveryStatus,
+ printers = printers,
+ onPrinterSelected = { selectedPrinter ->
+ scope.launch {
+ if (!initialized && selectedPrinter != null) {
+ try {
+ printEngine.initPrinter {
+ EpsonServiceProvider.build(printer = selectedPrinter)
+ }
+ initialized = true
+ } catch (_: Exception) {
+ discoveryStatus = "Failed to initialize printer"
+ return@launch
+ }
+ }
+ if (initialized) {
+ printEngine.printer.startPrintJob(PRINT_JOB)
+ }
+ }
+ },
+ )
+ }
+ }
+}
+
+private val PRINT_JOB = PrintJob(
+ listOf(
+ PrintCommand.Text(
+ " receipt-header \n" +
+ "------------------------------------------\n" +
+ "Receipt: 1\n" +
+ "Salesperson: staff name\n" +
+ "Date and Time: 20.5.2020 20:00\n" +
+ "Branch: 1\n" +
+ "Register: 123\n" +
+ "------------------------------------------\n" +
+ "Customer number: 654321\n" +
+ "------------------------------------------\n" +
+ " receipt-note \n" +
+ "------------------------------------------\n" +
+ "0011 \n" +
+ "multiline long product item name for \n" +
+ "receipt att-desc \n" +
+ "- addon_name \n" +
+ " 10.00 € 1 10.00 €\n" +
+ " note \n" +
+ "voucher-code \n" +
+ " \n" +
+ "------------------------------------------\n" +
+ "Tips 3.00 €\n" +
+ " \n" +
+ "******************************************\n" +
+ "Total (gross): 10.00 €\n" +
+ "******************************************\n" +
+ "Given: \n" +
+ "Cash 10.00 €\n" +
+ "- including tip: 3.00 €\n" +
+ "------------------------------------------\n" +
+ "Change: 10.00 €\n" +
+ "------------------------------------------\n" +
+ "Vat \n" +
+ " 19 % 1.00 €\n" +
+ "------------------------------------------\n" +
+ "Net: 10.00 €\n" +
+ "------------------------------------------\n" +
+ " receipt-footer \n",
+ ),
+ PrintCommand.Text("This is another line"),
+ PrintCommand.Text("-------"),
+ PrintCommand.Text("Barcode working:"),
+ PrintCommand.Barcode("RTC6093739"),
+ PrintCommand.Text("QR code:"),
+ PrintCommand.QrCode("123ABC"),
+ PrintCommand.Text("40 char line:"),
+ PrintCommand.Text("1234567890123456789012345678901234567890"),
+ PrintCommand.FeedPaper,
+ PrintCommand.CutPaper,
+ ),
+)