Library / SDK
alexzhirkevich/qrose avatar
alexzhirkevich/qrose

QRose: barcode generation for Kotlin, with vector output instead of bitmaps

A pure-Kotlin barcode generation library with a customizable graphical implementation for Compose Multiplatform

539 stars29 forksKotlinMIT

At a glance

What is it?
QRose is a pure-Kotlin library that generates QR codes, Data Matrix, Aztec, PDF417 and the one-dimensional barcode families, and renders them through Compose Multiplatform as scalable vector graphics. It suits a Kotlin team that needs a code on Android, iOS, desktop or the web from one implementation, and the styling options go well beyond the default black squares.
Who is it for?
QRose is worth adopting if your Kotlin project has to show a barcode on more than one platform, because the encoders exist for every Kotlin target and the drawing layer is Compose Multiplatform rather than a per-platform library, and because the output is vector graphics so the same code scales from a label to a billboard without a bitmap.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the library is, and the six reasons it gives

QRose is a pure-Kotlin barcode generation library with a customizable graphical implementation for Compose Multiplatform, and the repository carries the platform badges for Android, iOS, the JVM, macOS and the web.

The list of reasons it gives is worth reading because each one is a design constraint rather than a slogan.

Lightweight means it does not bring any dependencies except compose.ui. For a barcode library, that is a strong claim, because the obvious implementations pull in an imaging library, a font for text rendering and a PDF writer, and a Compose-based one can avoid all three by producing vector geometry instead.

Multiformat lists the supported symbologies: QR, Data Matrix, Aztec, PDF417, UPC, EAN, Code 128, 93 and 39, Codabar and ITF. That is the difference between a QR code helper and a barcode library, and it matters for anyone who has to print a product code as well as a link.

Multiplatform means encoders are available for each Kotlin target, with graphical components available for all targets supported by Compose Multiplatform. The encoder modules additionally support all Kotlin native targets and Node, which is the detail that matters for server-side use, and the documentation is explicit about it.

Efficient is stated as the ability to declare and render codes synchronously right from the composition in 60+ frames per second. Scalable means no raster bitmaps, only scalable vector graphics.

The last two are the same design decision seen from two angles. A barcode is a grid of modules, and the natural output is a bitmap, which means a fixed size, a fixed resolution and a resampling artefact when it is scaled. Producing the geometry instead means the code is drawn at whatever size the layout asks for, on any density, and the claim about frames per second is a consequence of doing the work once and letting the graphics layer draw the result rather than encoding and rasterising on every frame.

The licence is MIT, the primary language is Kotlin, and the recent releases are 1.3.1 from 2026-10-05, 1.3.0 from 2026-09-16 and 1.2.1 from 2026-09-10, with the last push to the repository on 2026-10-06.

The module split, and why it is worth reading the coordinates

The installation is on Maven Central under io.github.alexzhirkevich, and the dependency block is worth reading in full because the module names tell you the structure of the library.

toml
[versions]
qrose="<version>"

[libraries]
# For QR code painter
qrose-qr = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
# For 2D matrix & stacked code painters (Data Matrix, Aztec, PDF417)
qrose-matrix = { module = "io.github.alexzhirkevich:qrose-matrix", version.ref = "qrose" }
# For single-dimension barcode painters (UPC, EAN, Code128, ...)
qrose-oned = { module = "io.github.alexzhirkevich:qrose-oned", version.ref = "qrose" }

So there are three painter modules, split by the geometry of the code rather than by the symbology. qrose-qr is the QR painter, qrose-matrix is for 2D matrix and stacked code painters covering Data Matrix, Aztec and PDF417, and qrose-oned is for the single-dimension family of UPC, EAN and Code 128 and the rest.

That split is a direct consequence of the multiplatform goal. A one-dimensional barcode is a set of bars, which is a different drawing problem from a two-dimensional grid of modules, and a stacked code like PDF417 is a third. Splitting the artefacts means an application that only draws EAN codes does not pull in the QR painter and its dependencies, and the module structure in the repository tree mirrors it, with qrose-core, qrose-encoder-core, qrose-encoder-matrix, qrose-encoder-oned, qrose-matrix, qrose-oned and the umbrella qrose module.

The encoder modules are the second half of the story, and they are for people who do not want Compose at all:

toml
# For matrix encoders without Compose (optional, included in qrose-matrix)
qrose-encoder-matrix = { module = "io.github.alexzhirkevich:qrose-encoder-matrix", version.ref = "qrose" }
# For barcode encoders without Compose (optional, included in qrose-oned)
qrose-encoder-oned = { module = "io.github.alexzhirkevich:qrose-encoder-oned", version.ref = "qrose" }

The parenthetical is the useful part. The encoder modules are included in the corresponding painter modules, so a project that wants the matrix encoders without Compose can depend on the encoder module directly, and a project that wants everything can depend on the painter module and get the encoders anyway.

Creating a code, and the two ways to do it

The usage documentation splits into a set of named sections, and the basic one is the shortest path to a working code.

You can create a code right in composition using rememberQrCodePainter, rememberDataMatrixPainter, rememberAztecPainter, rememberPdf417Painter and rememberBarcodePainter, or you can use QrCodePainter, DataMatrixPainter, AztecPainter, Pdf417Painter and BarcodePainter to create it outside of Compose.

kotlin
Image(
    painter = rememberQrCodePainter("https://example.com"),
    contentDescription = "QR code referring to the example.com website"
)

Image(
    painter = rememberBarcodePainter("9780201379624", BarcodeType.EAN13),
    contentDescription = "EAN barcode for some product"
)

The two forms exist because the composable form is the one that composes well, since it remembers the painter across recompositions, while the plain painter class is the one that works in a non-Compose context, in a Gradle task or a test. A library that only offered the composable would be unusable outside a UI, and a library that only offered the class would lose the caching that the composable version gives you.

The contentDescription on the Image is worth noticing, because a barcode is an image that carries information a screen reader cannot see. Nothing in the library forces you to provide one, and nothing in the library can check that the description says something useful, but a team that treats accessibility as part of the component contract will put the payload in the description, and a team that does not will ship an image that says QR code.

The barcode example also shows the type argument, BarcodeType.EAN13, because the one-dimensional formats need to know which symbology they are encoding, and an EAN-13 and a UPC-A are not the same number of digits.

Styling, custom shapes, and the one limit you cannot change

The design section is where QRose departs from the usual barcode library, and the options are the ones you would want for a branded code.

The parameter list covers the ball shape, the dark pixel shape, the frame shape, the dark brush, the frame brush, and a logo with its own painter, padding, shape and size:

kotlin
val qrcodePainter = rememberQrCodePainter(
    data = "https://example.com",
    ballShape = QrBallShape.circle(),
    darkPixelShape = QrPixelShape.roundCorners(),
    frameShape = QrFrameShape.roundCorners(.25f),
    darkBrush = QrBrush.brush { size ->
        Brush.linearGradient(
            0f to Color.Red,
            1f to Color.Blue,
            end = Offset(size, size)
        )
    },
    frameBrush = QrBrush.solid(Color.Black),
    logoPainter = painterResource(Res.drawable.logo),
    logoPadding = QrLogoPadding.Natural(.1f),
    logoShape = QrLogoShape.circle(),
    logoSize = 0.2f,
)

Every visual decision is a separate parameter rather than a single style object, and the brushes are not limited to solid colours, since a brush is a function of the size, which is how a gradient gets applied to the code without baking it into a bitmap.

The same set of options is also available as a DSL constructor, with logo, shapes and colors blocks, which reads better when most of the configuration is the default.

The customisation section goes further and lets you write your own shape by implementing the interface, and the example is a circle drawn as an oval:

kotlin
class MyCircleBallShape : QrBallShape {
    override fun Path.path(size: Float, neighbors: Neighbors): Path = apply {
        addOval(Rect(0f,0f, size, size))
    }
}

And then the note that matters: a path here uses PathFillType.EvenOdd that cannot be changed.

That is the one hard limit in the design, and it is worth understanding rather than discovering. A QR code is a grid of modules, and the shapes are drawn as paths that overlap the finder patterns and the timing rows, which is why even-odd filling is the right rule for the interior. A shape that needs non-zero filling to look right cannot be expressed, and a designer who wants a solid overlapping shape has to work around it rather than pick a different fill mode.

Payloads, export, and the encoder path without Compose

Two smaller features complete the picture.

The first is data types. QR codes can hold various payload types, Text, Wi-Fi, E-mail and vCard among them, and the QrData object performs those encodings:

kotlin
val wifiData : String = QrData.wifi(ssid = "My Network", psk = "12345678")

val wifiCode = rememberQrCodePainter(wifiData)

A Wi-Fi payload with a pre-shared key is the example that has legal weight, because a QR code containing a network password printed on a poster is a well-known way to hand out guest access without typing it, and it is also a well-known way to leak a network password to anybody who photographs the poster. A library that makes the payload easy to build makes both mistakes easier, so the payload choice is worth a thought rather than a shrug.

The second is export. QR codes can be exported to PNG, JPEG and WEBP using the toByteArray function, and the example shows the size and the format as arguments:

kotlin
val painter : Painter = QrCodePainter(
    data = "https://example.com",
    options =  QrOptions { 
        colors {
            //...
        }
    }
)

val bytes : ByteArray = painter.toByteArray(1024, 1024, ImageFormat.PNG)

Note the pixel size is explicit here, which is the one place where the vector advantage is given up, because a raster format has a fixed resolution.

The last section is the encoder path for people with no Compose. Using the qrose-encoder-matrix and qrose-encoder-oned modules you can get barcode bit matrices or arrays without the Compose graphical implementation, and the QroseEncoders object is used as an encoder factory for all types of codes. The extension functions read like the symbology they create:

kotlin
val encoder : MatrixCodeEncoder = QroseEncoders.QR()
val code : Matrix2D = encoder.encode("https://example.com")

That is the path a server-side service or a Node script would take, and it is the reason the encoder modules exist separately and are documented as supporting all Kotlin native targets and Node.

Editorial conclusion

QRose is worth adopting if your Kotlin project has to show a barcode on more than one platform, because the encoders exist for every Kotlin target and the drawing layer is Compose Multiplatform rather than a per-platform library, and because the output is vector graphics so the same code scales from a label to a billboard without a bitmap. It is the wrong tool if you need to read barcodes rather than write them, and it is the wrong tool if you are on a stack that is not Kotlin. Before you add it, check three things: that you pick the right module, because the painter modules and the encoder modules are separate artefacts, with qrose-qr, qrose-matrix and qrose-oned for drawing and qrose-encoder-matrix and qrose-encoder-oned for the bit matrices without Compose, and the encoder ones are already included in the matrix and oned painter modules; that you know it does not depend on anything except compose.ui, which is the reason it is light, and that you are therefore supplying the Compose dependency yourself on a target where it is not already present; and that you check the styling limits before you design a logo, because the path used for a custom shape is filled with the even-odd rule and that cannot be changed.

Frequently asked questions

Which barcode formats does QRose support?

The supported formats are QR, Data Matrix, Aztec, PDF417, UPC, EAN, Code 128, Code 93, Code 39, Codabar and ITF. The painter modules are split by geometry rather than by symbology, with qrose-qr for QR, qrose-matrix for the 2D and stacked codes and qrose-oned for the single-dimension family, and the encoder modules exist separately for use without Compose.

Can I use QRose without Compose Multiplatform?

Yes. The qrose-encoder-matrix and qrose-encoder-oned modules give you the bit matrices or arrays without the graphical implementation, and the QroseEncoders object acts as a factory, for example QroseEncoders.QR() returning a MatrixCodeEncoder whose encode method returns a Matrix2D. Those encoder modules also support all Kotlin native targets and Node.

How customizable are the codes QRose produces?

You can set the ball, dark pixel and frame shapes, the dark and frame brushes including gradients, and a logo with its own painter, padding, shape and size, either through parameters or through a DSL constructor. You can also implement your own shape, with the note that the path is filled using PathFillType.EvenOdd and that this cannot be changed.

Official sources

  1. alexzhirkevich/qrose on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/alexzhirkevich-qrose.svg)](https://hysenlabs.com/projects/alexzhirkevich-qrose)

Was this useful?