
Scan, decode, generate and render QR, Aztec, Data Matrix, PDF417 and 1D barcodes to SVG or PNG; includes charset tables, ZXing-style API and Compose drawing utilities.
Scan, decode, generate and render barcodes from commonMain on Kotlin
Multiplatform. The API is ZXing's, so code you already wrote for Android reads
the same here.
Documentation · a guide per task, plus the generated API reference.
KiteQR is one library and it runs on every Kotlin Multiplatform target. There is
no expect/actual, no import java.*, and nothing on the classpath but
kotlin-stdlib. It reads and writes QR codes, Aztec, Data Matrix, PDF417 and
the 1D barcode families. It also renders a finished barcode to SVG or PNG. The
character-set tables ship inside the library, so Shift_JIS, GB18030, Big5 and
the rest work on every target.
import io.github.yuroyami.kiteqr.Qr
import io.github.yuroyami.kiteqr.RGBLuminanceSource
import io.github.yuroyami.kiteqr.qrcode.decoder.ErrorCorrectionLevel
import io.github.yuroyami.kiteqr.render.toPng
import io.github.yuroyami.kiteqr.render.toSvg
val matrix = Qr.encode("HELLO WORLD", ErrorCorrectionLevel.Q)
val svg: String = matrix.toSvg()
val png: ByteArray = matrix.toPng(scale = 4)
// pixels is an IntArray of packed ARGB, w * h long.
val text: String? = Qr.decode(RGBLuminanceSource(w, h, pixels)).getText()toSvg and toPng live in io.github.yuroyami.kiteqr.render, not alongside
Qr, so they need their own imports.
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.yuroyami:kiteqr:0.1.0")
// Optional Compose Multiplatform binding.
implementation("io.github.yuroyami:kiteqr-compose:0.1.0")
}
}
}The core's only dependency is kotlin-stdlib. The same artifact works in a
plain Android or JVM project. You do not have to be using Kotlin Multiplatform.
KiteQR has two API shapes over the same engine, and they do not look alike.
Qr is a QR-only facade. It gives you two functions and Kotlin default
arguments. Every other entry point uses Java-style accessors instead:
getText(), getWidth(), and a raw Map<EncodeHintType, *> of hints in place
of named parameters.
The two shapes also disagree about what a BitMatrix holds. Qr.encode returns
a tight matrix: one cell per module, no quiet zone, no scaling.
MultiFormatWriter.encode returns a matrix already scaled to the width and
height you asked for, and that matrix already has a margin. The type is the
same, but the contract is not. The render extensions below expect the tight
form. Pass quietZone = 0 when you render a writer's output, or the image ends
up with two margins.
val matrix = Qr.encode(
contents = "https://example.com",
ecLevel = ErrorCorrectionLevel.Q, // L < M < Q < H redundancy
characterSet = "Shift_JIS", // forces an ECI; null to let the encoder decide
version = null, // pin 1..40, or null to auto-size
)Those four parameters are everything the facade offers. For anything else, use
QRCodeWriter or Encoder and build the hint map yourself. That covers a fixed
mask pattern (EncodeHintType.QR_MASK_PATTERN), a GS1 payload
(EncodeHintType.GS1_FORMAT), and the byte-mode minimal-encoding optimizer
(EncodeHintType.QR_COMPACT). The minimal encoder stays off unless you set
QR_COMPACT. Qr.encode never sets it, so it uses the ordinary mode chooser.
Numeric, alphanumeric, byte and Kanji modes all work. The test suite encodes Kanji through the generated Shift_JIS table and reads it back unchanged.
import io.github.yuroyami.kiteqr.BarcodeFormat
import io.github.yuroyami.kiteqr.MultiFormatWriter
val ean = MultiFormatWriter().encode("9780201379624", BarcodeFormat.EAN_13, 480, 120)
val dm = MultiFormatWriter().encode("payload", BarcodeFormat.DATA_MATRIX, 240, 240)Per-format options go in the optional fifth argument, a hints map
(EncodeHintType.MARGIN, ERROR_CORRECTION, CHARACTER_SET, and the
format-specific ones).
import io.github.yuroyami.kiteqr.BinaryBitmap
import io.github.yuroyami.kiteqr.MultiFormatReader
import io.github.yuroyami.kiteqr.common.HybridBinarizer
val result = MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)))
println("${result.getBarcodeFormat()}: ${result.getText()}")source is any LuminanceSource. KiteQR ships RGBLuminanceSource for packed
ARGB, GrayscaleLuminanceSource for raw luminance bytes,
PlanarYUVLuminanceSource for camera frames, and InvertedLuminanceSource.
LuminanceSource is an abstract class, so subclass it for anything else.
Qr.decode is the shorthand for the QR-only case. It throws NotFoundException
when it finds nothing.
ResultParser.parseResult(result) turns the decoded string into a typed
ParsedResult: URL, Wi-Fi, vCard, geo, calendar, email, SMS and the rest.
QRCodeMultiReader and GenericMultipleBarcodeReader read several codes from
one image.
val svg = matrix.toSvg(moduleSize = 10, quietZone = 4, dark = "#101418", light = "#FFFFFF")
val png = matrix.toPng(scale = 8, quietZone = 4, dark = 0x101418, light = 0xFFFFFF)toSvg emits the whole symbol as one <path> with shape-rendering="crispEdges".
toPng writes 8-bit truecolor PNG through KiteQR's own encoder. Both are common
code with no platform behind them. Pass light = null to toSvg for a
transparent background.
kiteqr-compose adds three things: a QrCode composable, a
DrawScope.drawQrCode extension, and BitMatrix.toImageBitmap.
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.github.yuroyami.kiteqr.compose.QrCode
QrCode(matrix, modifier = Modifier.size(240.dp), dark = Color.Black, light = Color.White, quietZone = 4)Those five parameters are the whole composable. It fills the modifier's bounds and stays square and centered. It merges adjacent dark modules into horizontal runs, so there are no anti-aliasing seams.
| Format | Read | Write |
|---|---|---|
QR_CODE |
yes | yes |
AZTEC |
yes | yes |
DATA_MATRIX |
yes | yes |
PDF_417 |
yes | yes |
CODE_128, CODE_93, CODE_39
|
yes | yes |
CODABAR, ITF
|
yes | yes |
EAN_13, EAN_8, UPC_A, UPC_E
|
yes | yes |
MAXICODE |
yes | no |
RSS_14, RSS_EXPANDED
|
yes | no |
UPC_EAN_EXTENSION |
yes | no |
The four read-only formats have no encoder.
A barcode names its character set with an ECI code (Extended Channel
Interpretation). KiteQR covers 27 CharacterSetECI entries: the fifteen
ISO-8859 variants, four windows-125x, Cp437, US-ASCII, UTF-8, UTF-16BE,
Shift_JIS, GB18030, Big5 and EUC-KR.
The two artifacts do not build for the same set. kiteqr-compose supports only
the targets that Compose Multiplatform supports, and its Android minSdk is
higher.
| Target | kiteqr |
kiteqr-compose |
|---|---|---|
androidTarget |
minSdk 21 | minSdk 24 |
jvm |
yes | yes |
iosArm64, iosSimulatorArm64
|
yes | yes |
iosX64 |
yes | no |
macosArm64 |
yes | yes |
tvosArm64, tvosSimulatorArm64
|
yes | no |
watchosArm32, watchosArm64, watchosSimulatorArm64, watchosDeviceArm64
|
yes | no |
linuxX64, linuxArm64, mingwX64
|
yes | no |
androidNativeArm32, androidNativeArm64, androidNativeX86, androidNativeX64
|
yes | no |
js(IR) |
browser + Node | browser |
wasmJs |
browser + Node | browser |
wasmWasi |
Node | no |
That is 22 targets for kiteqr and 7 for kiteqr-compose. Neither builds
macosX64, tvosX64 or watchosX64.
The detection code has only ever been tested on images this library rendered itself, never on photos. There are 22 tests over about 43,800 lines of code. Every decode test encodes a matrix, renders it, and reads it back. That input is pixel-perfect, axis-aligned and evenly lit.
So FinderPatternFinder, AlignmentPatternFinder, WhiteRectangleDetector,
PerspectiveTransform and HybridBinarizer have never run against a camera
frame. They have never run against a skewed, blurred or damaged symbol either.
This is the largest correctness risk in the project. Test KiteQR against your
own images before you depend on it.
KiteQR declares 22 targets and the metadata compiles. No test has been executed on Native, JS, Wasm or Android in this tree. The only GitHub Actions workflow builds and deploys the documentation. Nothing runs the test suite on a push, on any target.
Result accessor is nullable: getText(), getRawBytes(),
getResultPoints(), getBarcodeFormat() and getResultMetadata() all return
a nullable type.PDF417HighLevelEncoderTestAdapter is a public object in commonMain whose
own KDoc says it exists solely for unit tests. It will ship in the published
API until it is made internal.Z) are read as UTC,
because common Kotlin has no host timezone, and
CalendarParsedResult.getDisplayResult() renders a fixed yyyy-MM-dd HH:mm:ss
instead of a locale-aware format. See
docs/compatibility.md.Apache-2.0. KiteQR is a derivative work of ZXing, which is also Apache-2.0. See NOTICE for full attribution.
Scan, decode, generate and render barcodes from commonMain on Kotlin
Multiplatform. The API is ZXing's, so code you already wrote for Android reads
the same here.
Documentation · a guide per task, plus the generated API reference.
KiteQR is one library and it runs on every Kotlin Multiplatform target. There is
no expect/actual, no import java.*, and nothing on the classpath but
kotlin-stdlib. It reads and writes QR codes, Aztec, Data Matrix, PDF417 and
the 1D barcode families. It also renders a finished barcode to SVG or PNG. The
character-set tables ship inside the library, so Shift_JIS, GB18030, Big5 and
the rest work on every target.
import io.github.yuroyami.kiteqr.Qr
import io.github.yuroyami.kiteqr.RGBLuminanceSource
import io.github.yuroyami.kiteqr.qrcode.decoder.ErrorCorrectionLevel
import io.github.yuroyami.kiteqr.render.toPng
import io.github.yuroyami.kiteqr.render.toSvg
val matrix = Qr.encode("HELLO WORLD", ErrorCorrectionLevel.Q)
val svg: String = matrix.toSvg()
val png: ByteArray = matrix.toPng(scale = 4)
// pixels is an IntArray of packed ARGB, w * h long.
val text: String? = Qr.decode(RGBLuminanceSource(w, h, pixels)).getText()toSvg and toPng live in io.github.yuroyami.kiteqr.render, not alongside
Qr, so they need their own imports.
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.yuroyami:kiteqr:0.1.0")
// Optional Compose Multiplatform binding.
implementation("io.github.yuroyami:kiteqr-compose:0.1.0")
}
}
}The core's only dependency is kotlin-stdlib. The same artifact works in a
plain Android or JVM project. You do not have to be using Kotlin Multiplatform.
KiteQR has two API shapes over the same engine, and they do not look alike.
Qr is a QR-only facade. It gives you two functions and Kotlin default
arguments. Every other entry point uses Java-style accessors instead:
getText(), getWidth(), and a raw Map<EncodeHintType, *> of hints in place
of named parameters.
The two shapes also disagree about what a BitMatrix holds. Qr.encode returns
a tight matrix: one cell per module, no quiet zone, no scaling.
MultiFormatWriter.encode returns a matrix already scaled to the width and
height you asked for, and that matrix already has a margin. The type is the
same, but the contract is not. The render extensions below expect the tight
form. Pass quietZone = 0 when you render a writer's output, or the image ends
up with two margins.
val matrix = Qr.encode(
contents = "https://example.com",
ecLevel = ErrorCorrectionLevel.Q, // L < M < Q < H redundancy
characterSet = "Shift_JIS", // forces an ECI; null to let the encoder decide
version = null, // pin 1..40, or null to auto-size
)Those four parameters are everything the facade offers. For anything else, use
QRCodeWriter or Encoder and build the hint map yourself. That covers a fixed
mask pattern (EncodeHintType.QR_MASK_PATTERN), a GS1 payload
(EncodeHintType.GS1_FORMAT), and the byte-mode minimal-encoding optimizer
(EncodeHintType.QR_COMPACT). The minimal encoder stays off unless you set
QR_COMPACT. Qr.encode never sets it, so it uses the ordinary mode chooser.
Numeric, alphanumeric, byte and Kanji modes all work. The test suite encodes Kanji through the generated Shift_JIS table and reads it back unchanged.
import io.github.yuroyami.kiteqr.BarcodeFormat
import io.github.yuroyami.kiteqr.MultiFormatWriter
val ean = MultiFormatWriter().encode("9780201379624", BarcodeFormat.EAN_13, 480, 120)
val dm = MultiFormatWriter().encode("payload", BarcodeFormat.DATA_MATRIX, 240, 240)Per-format options go in the optional fifth argument, a hints map
(EncodeHintType.MARGIN, ERROR_CORRECTION, CHARACTER_SET, and the
format-specific ones).
import io.github.yuroyami.kiteqr.BinaryBitmap
import io.github.yuroyami.kiteqr.MultiFormatReader
import io.github.yuroyami.kiteqr.common.HybridBinarizer
val result = MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)))
println("${result.getBarcodeFormat()}: ${result.getText()}")source is any LuminanceSource. KiteQR ships RGBLuminanceSource for packed
ARGB, GrayscaleLuminanceSource for raw luminance bytes,
PlanarYUVLuminanceSource for camera frames, and InvertedLuminanceSource.
LuminanceSource is an abstract class, so subclass it for anything else.
Qr.decode is the shorthand for the QR-only case. It throws NotFoundException
when it finds nothing.
ResultParser.parseResult(result) turns the decoded string into a typed
ParsedResult: URL, Wi-Fi, vCard, geo, calendar, email, SMS and the rest.
QRCodeMultiReader and GenericMultipleBarcodeReader read several codes from
one image.
val svg = matrix.toSvg(moduleSize = 10, quietZone = 4, dark = "#101418", light = "#FFFFFF")
val png = matrix.toPng(scale = 8, quietZone = 4, dark = 0x101418, light = 0xFFFFFF)toSvg emits the whole symbol as one <path> with shape-rendering="crispEdges".
toPng writes 8-bit truecolor PNG through KiteQR's own encoder. Both are common
code with no platform behind them. Pass light = null to toSvg for a
transparent background.
kiteqr-compose adds three things: a QrCode composable, a
DrawScope.drawQrCode extension, and BitMatrix.toImageBitmap.
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.github.yuroyami.kiteqr.compose.QrCode
QrCode(matrix, modifier = Modifier.size(240.dp), dark = Color.Black, light = Color.White, quietZone = 4)Those five parameters are the whole composable. It fills the modifier's bounds and stays square and centered. It merges adjacent dark modules into horizontal runs, so there are no anti-aliasing seams.
| Format | Read | Write |
|---|---|---|
QR_CODE |
yes | yes |
AZTEC |
yes | yes |
DATA_MATRIX |
yes | yes |
PDF_417 |
yes | yes |
CODE_128, CODE_93, CODE_39
|
yes | yes |
CODABAR, ITF
|
yes | yes |
EAN_13, EAN_8, UPC_A, UPC_E
|
yes | yes |
MAXICODE |
yes | no |
RSS_14, RSS_EXPANDED
|
yes | no |
UPC_EAN_EXTENSION |
yes | no |
The four read-only formats have no encoder.
A barcode names its character set with an ECI code (Extended Channel
Interpretation). KiteQR covers 27 CharacterSetECI entries: the fifteen
ISO-8859 variants, four windows-125x, Cp437, US-ASCII, UTF-8, UTF-16BE,
Shift_JIS, GB18030, Big5 and EUC-KR.
The two artifacts do not build for the same set. kiteqr-compose supports only
the targets that Compose Multiplatform supports, and its Android minSdk is
higher.
| Target | kiteqr |
kiteqr-compose |
|---|---|---|
androidTarget |
minSdk 21 | minSdk 24 |
jvm |
yes | yes |
iosArm64, iosSimulatorArm64
|
yes | yes |
iosX64 |
yes | no |
macosArm64 |
yes | yes |
tvosArm64, tvosSimulatorArm64
|
yes | no |
watchosArm32, watchosArm64, watchosSimulatorArm64, watchosDeviceArm64
|
yes | no |
linuxX64, linuxArm64, mingwX64
|
yes | no |
androidNativeArm32, androidNativeArm64, androidNativeX86, androidNativeX64
|
yes | no |
js(IR) |
browser + Node | browser |
wasmJs |
browser + Node | browser |
wasmWasi |
Node | no |
That is 22 targets for kiteqr and 7 for kiteqr-compose. Neither builds
macosX64, tvosX64 or watchosX64.
The detection code has only ever been tested on images this library rendered itself, never on photos. There are 22 tests over about 43,800 lines of code. Every decode test encodes a matrix, renders it, and reads it back. That input is pixel-perfect, axis-aligned and evenly lit.
So FinderPatternFinder, AlignmentPatternFinder, WhiteRectangleDetector,
PerspectiveTransform and HybridBinarizer have never run against a camera
frame. They have never run against a skewed, blurred or damaged symbol either.
This is the largest correctness risk in the project. Test KiteQR against your
own images before you depend on it.
KiteQR declares 22 targets and the metadata compiles. No test has been executed on Native, JS, Wasm or Android in this tree. The only GitHub Actions workflow builds and deploys the documentation. Nothing runs the test suite on a push, on any target.
Result accessor is nullable: getText(), getRawBytes(),
getResultPoints(), getBarcodeFormat() and getResultMetadata() all return
a nullable type.PDF417HighLevelEncoderTestAdapter is a public object in commonMain whose
own KDoc says it exists solely for unit tests. It will ship in the published
API until it is made internal.Z) are read as UTC,
because common Kotlin has no host timezone, and
CalendarParsedResult.getDisplayResult() renders a fixed yyyy-MM-dd HH:mm:ss
instead of a locale-aware format. See
docs/compatibility.md.Apache-2.0. KiteQR is a derivative work of ZXing, which is also Apache-2.0. See NOTICE for full attribution.