Skip to content

kn: Add support for experimental writer api #764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy