DEV Community

datalog
datalog

Posted on

Adding a 14 KB Kanji Table to a JavaScript QR Generator

In my previous post, I described how I reduced a QR Kanji lookup table by 36% using dynamic programming.

That table is now integrated into datalog/qrcode-generator. It provides the character mapping needed for Kanji mode without an external encoding package.

The integration also gave me a few useful test cases for choosing encoding modes, including characters I had not expected Kanji mode to support.

Adding Kanji support

Kanji mode encodes supported characters in 13 bits each, compared with the 24 bits typically needed for Japanese characters in UTF-8.

The table provides the character mapping. The segment optimizer chooses modes based on character costs and segment headers.

A space stays a space

Consider this input:

東京 大阪
Enter fullscreen mode Exit fullscreen mode

The ordinary space (U+0020) cannot be encoded in Kanji mode. The encoder preserves it and uses another mode for that part of the input.

For this example, the optimizer produces:

Kanji("東京") → Alphanumeric(" ") → Kanji("大阪")
Enter fullscreen mode Exit fullscreen mode

A single space is valid in Alphanumeric mode, so it gets its own segment.

All bit counts below use the segment-header widths for QR versions 1–9. They exclude the terminator, padding, and error correction.

Segment Header bits Payload bits Total
Kanji: 東京 12 26 38
Alphanumeric: space 13 6 19
Kanji: 大阪 12 26 38
Total 37 58 95

Encoding the whole string as UTF-8 requires 13 bytes: three for each Japanese character and one for the space. Including the Byte segment header, that is 116 bits.

Splitting the text saves 21 bits, even after paying for three segment headers.

There is another option if you control the input. JIS X 0208 includes the ideographic space (U+3000), which can be encoded in Kanji mode:

東京 大阪
Enter fullscreen mode Exit fullscreen mode

With that space, the whole string fits in one Kanji segment: 12 + 5 × 13 = 77 bits.

The ideographic space is usually wider than an ordinary space and can look out of place in other text. It is also a different Unicode character, so the generator does not substitute it automatically.

When switching modes pays off

Keeping ordinary spaces means accounting for segment overhead. Whether switching modes saves bits depends on the surrounding characters.

JIS X 0208, the character set used by QR Kanji mode, also includes Greek and Cyrillic letters.

Supported letters from these blocks can use the same Kanji encoding path. For example, consider ΑΒΓ:

Kanji:  12 + 3 × 13 = 51 bits
Byte:   12 + 3 × 16 = 60 bits
Enter fullscreen mode Exit fullscreen mode

The saving is smaller than for the Japanese example: three bits per letter instead of eleven. That leaves less room to cover additional segment headers.

Adding an ordinary space shows the difference:

ΑΒΓ ΔΕΖ
Enter fullscreen mode Exit fullscreen mode

For this input, the optimizer keeps the whole string in Byte mode.

Encoding Total bits
One Byte segment 116
Kanji → Alphanumeric space → Kanji 121

Both choices preserve exactly the same text. Splitting it saves payload bits but spends more on headers.

Longer runs can change the result. For this input, with ten Greek letters on each side of the space:

αβγδεζηθικ λμνξοπρστυ
Enter fullscreen mode Exit fullscreen mode

the optimizer selects:

Kanji("αβγδεζηθικ")
Alphanumeric(" ")
Kanji("λμνξοπρστυ")
Enter fullscreen mode Exit fullscreen mode

That takes 303 bits, compared with 340 bits for one UTF-8 Byte segment.

The space adds segment overhead. Whether splitting is worthwhile depends on how much the surrounding runs save.

Choosing segments

The optimizer uses dynamic programming with four modes: Numeric, Alphanumeric, Byte, and Kanji.

It walks through the input, keeping a running cost for each mode. Continuing in the current mode adds the character cost; switching modes also adds a new segment header.

Costs are represented in sixths of a bit. Numeric mode encodes groups of three digits in ten bits, while Alphanumeric mode encodes pairs of characters in eleven bits. Using sixths lets the optimizer represent their average character costs with integers and account for rounding when a segment ends.

The encoder then works backward through the stored history to reconstruct the selected segments. With four modes, that history uses O(N) memory, where N is the number of input code points.

This lets the encoder weigh character savings against segment overhead without requiring the caller to split the text.

From bits to a QR symbol

Saving bits does not always produce a smaller QR grid. Each QR version has a fixed capacity, so the data must fit into a lower version before the grid gets smaller.

The generator also tries to increase the error-correction level without increasing the selected version. A shorter bitstream can leave room for stronger error correction, even when the grid dimensions stay the same.

You can try these examples in the interactive demo and inspect the selected segments.

The source code is available at datalog/qrcode-generator.

Top comments (0)