Skip to content

Commit

Permalink
kn: Add support for experimental writer api (#764)
Browse files Browse the repository at this point in the history
* kn: Allow reading barcode without instantiate a BarcodeReader object

Example:
```kotlin
val barcode = BarcodeReader.read(iv)
```
```kotlin
val barcode = BarcodeReader.read(iv, opts)
```

* kn: Correct the arrangement of parameter for assert functions
* kn: Add support for experimental barcode writer
* kn: Add documentation for experimental writer api
* kn: Mark Barcode constructors as ExperimentalWriterApi
  • Loading branch information
ISNing committed May 8, 2024
1 parent 03e9c12 commit d0c1f34
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: true

- name: Checkout toolchain initializer repository
uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish-kn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: true

- name: Checkout toolchain initializer repository
uses: actions/checkout@v4
Expand Down
40 changes: 38 additions & 2 deletions wrappers/kn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ to your `build.gradle.kts` file in the `dependencies` section of `nativeMain` so

## Use

### Reading

A trivial use case looks like this:

```kotlin
Expand All @@ -23,9 +25,9 @@ import zxingcpp.ImageView
val data: ByteArray = ... // the image data
val width: Int = ... // the image width
val height: Int = ... // the image height
val format = ImageFormat.Lum // ImageFormat.Lum assumes grey scale image data
val format: ImageFormat = ImageFormat.Lum // ImageFormat.Lum assumes grey scale image data

val image = ImageView(data, width, height, format)
val image: ImageView = ImageView(data, width, height, format)
val barcodeReader = BarcodeReader().apply {
formats = setOf(BarcodeFormat.EAN13, BarcodeFormat.QRCode)
tryHarder = true
Expand All @@ -40,6 +42,40 @@ barcodeReader.read(image).joinToString("\n") { barcode: Barcode ->

Here you have to load your image into memory by yourself and pass the decoded data to the constructor of `ImageView`.

### Writing

A trivial use case looks like this:

```kotlin
import zxingcpp.*

val text: String = "Hello, World!"
val format = BarcodeFormat.QRCode

@OptIn(ExperimentalWriterApi::class)
val cOpts = CreatorOptions(format) // more options, see documentation

@OptIn(ExperimentalWriterApi::class)
val barcode = Barcode(text, cOpts)
// or
@OptIn(ExperimentalWriterApi::class)
val barcode2 = Barcode(text.encodeToByteArray(), format)

@OptIn(ExperimentalWriterApi::class)
val wOpts = WriterOptions().apply {
sizeHint = 400
// more options, see documentation
}

@OptIn(ExperimentalWriterApi::class)
val svg: String = barcode.toSVG(wOpts)
@OptIn(ExperimentalWriterApi::class)
val image: Image = barcode.toImage(wOpts)
```

> Note: The Writer api is still experimental and may change in future versions.
> You will have to opt-in `zxingcpp.ExperimentalWriterApi` to use it.
## Build locally

1. Install JDK, CMake and Android NDK(With `$ANDROID_NDK` correctly configured) and ensure their
Expand Down
5 changes: 4 additions & 1 deletion wrappers/kn/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ krossCompile {
packageName = "zxingcpp.cinterop"
includeDirs.from(buildDir)
headers = listOf("$sourceDir/src/ZXingC.h")
compilerOpts += "-DZXING_EXPERIMENTAL_API=ON"
}
cmake.apply {
val buildDir = "$cmakeDir/{projectName}/{targetName}"
Expand All @@ -102,7 +103,9 @@ krossCompile {
} + CustomCMakeCacheEntries(
mapOf(
"ZXING_READERS" to "ON",
"ZXING_WRITERS" to "OFF",
"ZXING_WRITERS" to "NEW",
"ZXING_EXPERIMENTAL_API" to "ON",
"ZXING_USE_BUNDLED_ZINT" to "ON",
"ZXING_C_API" to "ON",
)
)).asCMakeParams
Expand Down
35 changes: 35 additions & 0 deletions wrappers/kn/src/nativeMain/kotlin/zxingcpp/Barcode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,29 @@ fun ZXing_Position.toKObject(): Position = Position(
bottomLeft.toKObject(),
)

class BarcodeConstructionException(message: String?) : Exception("Failed to construct barcode: $message")

@OptIn(ExperimentalForeignApi::class)
class Barcode(val cValue: CValuesRef<ZXing_Barcode>) {

@ExperimentalWriterApi
constructor(text: String, opts: CreatorOptions) : this(
ZXing_CreateBarcodeFromText(text, text.length, opts.cValue)
?: throw BarcodeConstructionException(ZXing_LastErrorMsg()?.toKStringNullPtrHandledAndFree())
)

@ExperimentalWriterApi
constructor(text: String, format: BarcodeFormat) : this(text, CreatorOptions(format))

@ExperimentalWriterApi
constructor(bytes: ByteArray, opts: CreatorOptions) : this(
ZXing_CreateBarcodeFromBytes(bytes.refTo(0), bytes.size, opts.cValue)
?: throw BarcodeConstructionException(ZXing_LastErrorMsg()?.toKStringNullPtrHandledAndFree())
)

@ExperimentalWriterApi
constructor(bytes: ByteArray, format: BarcodeFormat) : this(bytes, CreatorOptions(format))

val isValid: Boolean
get() = ZXing_Barcode_isValid(cValue)
val errorMsg: String? by lazy {
Expand Down Expand Up @@ -132,6 +153,20 @@ class Barcode(val cValue: CValuesRef<ZXing_Barcode>) {
}
}

@OptIn(ExperimentalForeignApi::class)
@ExperimentalWriterApi
fun Barcode.toSVG(opts: WriterOptions? = null): String = cValue.usePinned {
ZXing_WriteBarcodeToSVG(it.get(), opts?.cValue)?.toKStringNullPtrHandledAndFree()
?: throw BarcodeWritingException(ZXing_LastErrorMsg()?.toKStringNullPtrHandledAndFree())
}

@OptIn(ExperimentalForeignApi::class)
@ExperimentalWriterApi
fun Barcode.toImage(opts: WriterOptions? = null): Image = cValue.usePinned {
ZXing_WriteBarcodeToImage(it.get(), opts?.cValue)?.toKObject()
?: throw BarcodeWritingException(ZXing_LastErrorMsg()?.toKStringNullPtrHandledAndFree())
}

@OptIn(ExperimentalForeignApi::class)
fun CValuesRef<ZXing_Barcode>.toKObject(): Barcode = Barcode(this)

Expand Down
11 changes: 8 additions & 3 deletions wrappers/kn/src/nativeMain/kotlin/zxingcpp/BarcodeReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ internal fun CPointer<ByteVar>?.toKStringNullPtrHandledAndFree(): String? = (thi
@OptIn(ExperimentalForeignApi::class)
class BarcodeReader : ReaderOptions() {
@Throws(BarcodeReadingException::class)
fun read(imageView: ImageView): List<Barcode> =
ZXing_ReadBarcodes(imageView.cValue, cValue)?.let { cValues -> cValues.toKObject().also { ZXing_Barcodes_delete(cValues) } }
?: throw BarcodeReadingException(ZXing_LastErrorMsg()?.toKStringNullPtrHandledAndFree())
fun read(imageView: ImageView): List<Barcode> = Companion.read(imageView, this)

companion object {
@Throws(BarcodeReadingException::class)
fun read(imageView: ImageView, opts: ReaderOptions? = null): List<Barcode> =
ZXing_ReadBarcodes(imageView.cValue, opts?.cValue)?.let { cValues -> cValues.toKObject().also { ZXing_Barcodes_delete(cValues) } }
?: throw BarcodeReadingException(ZXing_LastErrorMsg()?.toKStringNullPtrHandledAndFree())
}
}

class BarcodeReadingException(message: String?) : Exception("Failed to read barcodes: $message")
Expand Down
69 changes: 69 additions & 0 deletions wrappers/kn/src/nativeMain/kotlin/zxingcpp/BarcodeWriter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2024 ISNing
*/
// SPDX-License-Identifier: Apache-2.0

package zxingcpp

import cnames.structs.ZXing_CreatorOptions
import cnames.structs.ZXing_WriterOptions
import kotlinx.cinterop.*
import zxingcpp.cinterop.*
import kotlin.experimental.ExperimentalNativeApi
import kotlin.native.ref.createCleaner

// TODO: Remove this annotation when the API is stable
@RequiresOptIn(level = RequiresOptIn.Level.ERROR, message = "The Writer API is experimental and may change in the future.")
@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalWriterApi

class BarcodeWritingException(message: String?) : Exception("Failed to write barcode: $message")

@ExperimentalWriterApi
@OptIn(ExperimentalForeignApi::class)
open class CreatorOptions(format: BarcodeFormat) {
var format: BarcodeFormat
get() = ZXing_CreatorOptions_getFormat(cValue).parseIntoBarcodeFormat().first()
set(value) = ZXing_CreatorOptions_setFormat(cValue, value.rawValue)
var readerInit: Boolean
get() = ZXing_CreatorOptions_getReaderInit(cValue)
set(value) = ZXing_CreatorOptions_setReaderInit(cValue, value)
var forceSquareDataMatrix: Boolean
get() = ZXing_CreatorOptions_getForceSquareDataMatrix(cValue)
set(value) = ZXing_CreatorOptions_setForceSquareDataMatrix(cValue, value)
var ecLevel: String
get() = ZXing_CreatorOptions_getEcLevel(cValue)?.toKStringNullPtrHandledAndFree() ?: ""
set(value) = ZXing_CreatorOptions_setEcLevel(cValue, value)

val cValue: CValuesRef<ZXing_CreatorOptions>? = ZXing_CreatorOptions_new(format.rawValue)

@Suppress("unused")
@OptIn(ExperimentalNativeApi::class)
private val cleaner = createCleaner(cValue) { ZXing_CreatorOptions_delete(it) }
}

@ExperimentalWriterApi
@OptIn(ExperimentalForeignApi::class)
open class WriterOptions {
var scale: Int
get() = ZXing_WriterOptions_getScale(cValue)
set(value) = ZXing_WriterOptions_setScale(cValue, value)
var sizeHint: Int
get() = ZXing_WriterOptions_getSizeHint(cValue)
set(value) = ZXing_WriterOptions_setSizeHint(cValue, value)
var rotate: Int
get() = ZXing_WriterOptions_getRotate(cValue)
set(value) = ZXing_WriterOptions_setRotate(cValue, value)
var withHRT: Boolean
get() = ZXing_WriterOptions_getWithHRT(cValue)
set(value) = ZXing_WriterOptions_setWithHRT(cValue, value)
var withQuietZones: Boolean
get() = ZXing_WriterOptions_getWithQuietZones(cValue)
set(value) = ZXing_WriterOptions_setWithQuietZones(cValue, value)

val cValue: CValuesRef<ZXing_WriterOptions>? = ZXing_WriterOptions_new()

@Suppress("unused")
@OptIn(ExperimentalNativeApi::class)
private val cleaner = createCleaner(cValue) { ZXing_WriterOptions_delete(it) }
}
28 changes: 28 additions & 0 deletions wrappers/kn/src/nativeMain/kotlin/zxingcpp/ImageView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package zxingcpp

import cnames.structs.ZXing_Image
import cnames.structs.ZXing_ImageView
import kotlinx.cinterop.*
import zxingcpp.cinterop.*
Expand Down Expand Up @@ -41,6 +42,7 @@ class ImageView(
private val pinnedDataCleaner = createCleaner(pinnedData) { it.unpin() }
}


@OptIn(ExperimentalForeignApi::class)
enum class ImageFormat(internal val cValue: ZXing_ImageFormat) {
None(ZXing_ImageFormat_None),
Expand All @@ -57,3 +59,29 @@ enum class ImageFormat(internal val cValue: ZXing_ImageFormat) {
@OptIn(ExperimentalForeignApi::class)
fun ZXing_ImageFormat.parseIntoImageFormat(): ImageFormat? =
ImageFormat.entries.firstOrNull { it.cValue == this }

@ExperimentalWriterApi
@OptIn(ExperimentalForeignApi::class)
class Image(val cValue: CValuesRef<ZXing_Image>) {
val data: ByteArray
get() = ZXing_Image_data(cValue)?.run {
readBytes(width * height).also { ZXing_free(this) }
}?.takeUnless { it.isEmpty() } ?: throw OutOfMemoryError()
val width: Int get() = ZXing_Image_width(cValue)
val height: Int get() = ZXing_Image_height(cValue)
val format: ImageFormat
get() = ZXing_Image_format(cValue).parseIntoImageFormat() ?: error(
"Unknown format ${ZXing_Image_format(cValue)} for image, " +
"this is an internal error, please report it to the library maintainers."
)

@Suppress("unused")
@OptIn(ExperimentalNativeApi::class)
val cValueCleaner = createCleaner(cValue) { ZXing_Image_delete(it) }

fun toImageView(): ImageView = ImageView(data, width, height, format)
}

@ExperimentalWriterApi
@OptIn(ExperimentalForeignApi::class)
fun CValuesRef<ZXing_Image>.toKObject(): Image = Image(this)
56 changes: 49 additions & 7 deletions wrappers/kn/src/nativeTest/kotlin/Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,55 @@ class BarcodeReaderTest {

assertNotNull(res)
assert(res.isValid)
assertEquals(res.format, BarcodeFormat.EAN8)
assertEquals(res.text, expected)
assertContentEquals(res.bytes, expected.encodeToByteArray())
assertEquals(BarcodeFormat.EAN8, res.format)
assertEquals(expected, res.text)
assertContentEquals(expected.encodeToByteArray(), res.bytes)
assert(!res.hasECI)
assertEquals(res.contentType, ContentType.Text)
assertEquals(res.orientation, 0)
assertEquals(res.position.topLeft, PointI(4, 0))
assertEquals(res.lineCount, 1)
assertEquals(ContentType.Text, res.contentType)
assertEquals(0, res.orientation)
assertEquals(PointI(4, 0), res.position.topLeft)
assertEquals(1, res.lineCount)
}

@Test
@OptIn(ExperimentalNativeApi::class, ExperimentalWriterApi::class)
fun `create write and read barcode with text`() {
val text = "I have the best words."
val barcode = Barcode(text, BarcodeFormat.DataMatrix)
val image = barcode.toImage()

val res = BarcodeReader.read(image.toImageView()).firstOrNull()

assertNotNull(res)
assert(res.isValid)
assertEquals(BarcodeFormat.DataMatrix, res.format)
assertEquals(text, res.text)
assertContentEquals(text.encodeToByteArray(), res.bytes)
assert(!res.hasECI)
assertEquals(ContentType.Text, res.contentType)
assertEquals(0, res.orientation)
assertEquals(PointI(1, 1), res.position.topLeft)
assertEquals(0, res.lineCount)
}

@Test
@OptIn(ExperimentalNativeApi::class, ExperimentalWriterApi::class)
fun `create write and read barcode with bytes`() {
val text = "I have the best words."
val barcode = Barcode(text.encodeToByteArray(), BarcodeFormat.DataMatrix)
val image = barcode.toImage()

val res = BarcodeReader.read(image.toImageView()).firstOrNull()

assertNotNull(res)
assert(res.isValid)
assertEquals(BarcodeFormat.DataMatrix, res.format)
assertEquals(text, res.text)
assertContentEquals(text.encodeToByteArray(), res.bytes)
assert(res.hasECI)
assertEquals(ContentType.Binary, res.contentType)
assertEquals(0, res.orientation)
assertEquals(PointI(1, 1), res.position.topLeft)
assertEquals(0, res.lineCount)
}
}

0 comments on commit d0c1f34

Please sign in to comment.