//// Micro QR Code encoder (ISO/IEC 18004 §6 + Annex K).
////
//// Supports M1 (11×11) through M4 (17×17). Reuses the standard QR Reed-Solomon
//// engine, bitstream, and matrix primitives; mask selection, format info and
//// data placement follow the Micro QR rules.
import gleam/list
import gleam/option.{type Option, None, Some}
import qrkit/error.{
type EncodeError, DataExceedsCapacity, IncompatibleOptions, InvalidVersion,
}
import qrkit/internal/bitstream
import qrkit/internal/matrix
import qrkit/internal/mode
import qrkit/internal/reed_solomon
import qrkit/internal/util
import qrkit/types.{
type ErrorCorrection, type Mode, type ModePreference, Alphanumeric, Byte,
Kanji, Low, Medium, Numeric, Quartile,
}
pub opaque type Encoded {
Encoded(
version: Int,
width: Int,
height: Int,
mask: Int,
rows: List(List(Bool)),
)
}
pub fn version(encoded: Encoded) -> Int {
let Encoded(version, _, _, _, _) = encoded
version
}
pub fn width(encoded: Encoded) -> Int {
let Encoded(_, width, _, _, _) = encoded
width
}
pub fn height(encoded: Encoded) -> Int {
let Encoded(_, _, height, _, _) = encoded
height
}
pub fn rows(encoded: Encoded) -> List(List(Bool)) {
let Encoded(_, _, _, _, rows) = encoded
rows
}
pub fn mask(encoded: Encoded) -> Int {
let Encoded(_, _, _, mask, _) = encoded
mask
}
const finder_size: Int = 7
const min_version: Int = 1
const max_version: Int = 4
const format_xor_mask: Int = 0x4445
const format_info_table: List(Int) = [
0x4445, 0x4172, 0x4E2B, 0x4B1C, 0x55AE, 0x5099, 0x5FC0, 0x5AF7, 0x6793, 0x62A4,
0x6DFD, 0x68CA, 0x7678, 0x734F, 0x7C16, 0x7921, 0x06DE, 0x03E9, 0x0CB0, 0x0987,
0x1735, 0x1202, 0x1D5B, 0x186C, 0x2508, 0x203F, 0x2F66, 0x2A51, 0x34E3, 0x31D4,
0x3E8D, 0x3BBA,
]
/// Encode `text` into a Micro QR code.
///
/// `requested_version` is `Some(N)` when the caller explicitly pinned the
/// version via `qrkit.with_exact_version(N)` (or the legacy
/// `with_min_version(N)` alias), in which case the encoder uses N as a strict
/// version (any mode/ECC/capacity mismatch surfaces as an `Error`). When
/// `requested_version` is `None`, the encoder picks the smallest version that
/// fits the payload.
pub fn encode(
text: String,
ecc: ErrorCorrection,
requested_version: Option(Int),
preference: ModePreference,
) -> Result(Encoded, EncodeError) {
use _ <- result_try(validate_requested(requested_version))
use _ <- result_try(validate_ecc(ecc))
use selected_mode <- result_try(select_mode(text, preference))
use chosen_version <- result_try(resolve_version(
text,
selected_mode,
ecc,
requested_version,
))
use codewords <- result_try(create_codewords(
text,
selected_mode,
ecc,
chosen_version,
))
case build_matrix(chosen_version, ecc, codewords) {
Ok(#(best_mask, final_matrix)) ->
Ok(Encoded(
chosen_version,
matrix.width(final_matrix),
matrix.height(final_matrix),
best_mask,
matrix.rows(final_matrix),
))
Error(error) -> Error(error)
}
}
fn validate_requested(
requested_version: Option(Int),
) -> Result(Nil, EncodeError) {
case requested_version {
None -> Ok(Nil)
Some(value) -> validate_version(value)
}
}
/// Resolve the version the encoder should use.
///
/// - When `requested_version` is `None`, walk versions from `min_version`
/// upward and pick the first that fits the payload (the historical
/// "smallest fit" behaviour).
/// - When `requested_version` is `Some(N)`, use N as a strict floor: if N
/// cannot encode the mode, support the ECC level, or hold the payload,
/// return a typed `Error` instead of silently promoting to N+1.
fn resolve_version(
text: String,
selected_mode: Mode,
ecc: ErrorCorrection,
requested_version: Option(Int),
) -> Result(Int, EncodeError) {
case requested_version {
None -> do_find_version(text, selected_mode, ecc, min_version)
Some(n) -> check_version(text, selected_mode, ecc, n)
}
}
/// Confirm a single, caller-requested Micro QR version. Returns the version
/// when mode + ECC + capacity all check out; otherwise surfaces the precise
/// `EncodeError` that explains the mismatch.
fn check_version(
text: String,
selected_mode: Mode,
ecc: ErrorCorrection,
candidate: Int,
) -> Result(Int, EncodeError) {
case mode_supported(selected_mode, candidate) {
False ->
Error(IncompatibleOptions(
"Micro QR M"
<> int_to_str(candidate)
<> " does not support the "
<> mode_name(selected_mode)
<> " mode",
))
True ->
case data_capacity_bits(candidate, ecc) {
Error(error) -> Error(error)
Ok(capacity) ->
case encoded_bits(text, selected_mode, candidate) {
Error(error) -> Error(error)
Ok(required) ->
case required <= capacity {
True -> Ok(candidate)
False -> Error(DataExceedsCapacity(required, capacity))
}
}
}
}
}
fn mode_name(selected_mode: Mode) -> String {
case selected_mode {
Numeric -> "Numeric"
Alphanumeric -> "Alphanumeric"
Byte -> "Byte"
Kanji -> "Kanji"
}
}
/// Return the side length of a Micro QR version (M1..M4 → 11..17).
pub fn symbol_size(version: Int) -> Result(Int, EncodeError) {
case version >= min_version && version <= max_version {
True -> Ok(version * 2 + 9)
False -> Error(InvalidVersion(version))
}
}
/// Return the maximum data-and-header bit capacity for `(version, ecc)`.
pub fn data_capacity_bits(
version: Int,
ecc: ErrorCorrection,
) -> Result(Int, EncodeError) {
case version, ecc {
1, Low -> Ok(20)
2, Low -> Ok(40)
2, Medium -> Ok(32)
3, Low -> Ok(84)
3, Medium -> Ok(68)
4, Low -> Ok(128)
4, Medium -> Ok(112)
4, Quartile -> Ok(80)
_, _ ->
Error(IncompatibleOptions(
"Micro QR M"
<> int_to_str(version)
<> " does not support the requested error correction level",
))
}
}
/// Number of error-correction codewords for `(version, ecc)`.
pub fn ec_codewords(
version: Int,
ecc: ErrorCorrection,
) -> Result(Int, EncodeError) {
case version, ecc {
1, Low -> Ok(2)
2, Low -> Ok(5)
2, Medium -> Ok(6)
3, Low -> Ok(6)
3, Medium -> Ok(8)
4, Low -> Ok(8)
4, Medium -> Ok(10)
4, Quartile -> Ok(14)
_, _ ->
Error(IncompatibleOptions(
"Micro QR M"
<> int_to_str(version)
<> " does not support the requested error correction level",
))
}
}
fn validate_version(version: Int) -> Result(Nil, EncodeError) {
case version >= min_version && version <= max_version {
True -> Ok(Nil)
False -> Error(InvalidVersion(version))
}
}
fn validate_ecc(ecc: ErrorCorrection) -> Result(Nil, EncodeError) {
case ecc {
Low | Medium | Quartile -> Ok(Nil)
_ ->
Error(IncompatibleOptions(
"Micro QR does not support the High error correction level",
))
}
}
fn select_mode(
text: String,
preference: ModePreference,
) -> Result(Mode, EncodeError) {
case preference {
types.ForceByte -> Ok(Byte)
types.Auto -> Ok(detect_uniform_mode(util.characters(text), Numeric))
}
}
fn detect_uniform_mode(chars: List(String), best: Mode) -> Mode {
case chars {
[] -> best
[char, ..rest] ->
detect_uniform_mode(rest, refine_mode(best, classify_char(char)))
}
}
fn classify_char(char: String) -> Mode {
case mode.is_numeric_char(char) {
True -> Numeric
False ->
case mode.is_alphanumeric_char(char) {
True -> Alphanumeric
False ->
case mode.is_kanji_char(char) {
True -> Kanji
False -> Byte
}
}
}
}
fn refine_mode(current: Mode, next: Mode) -> Mode {
case current, next {
Byte, _ -> Byte
_, Byte -> Byte
Kanji, _ -> Byte
_, Kanji -> Byte
Alphanumeric, Numeric -> Alphanumeric
Numeric, Alphanumeric -> Alphanumeric
_, _ -> next
}
}
fn do_find_version(
text: String,
selected_mode: Mode,
ecc: ErrorCorrection,
candidate: Int,
) -> Result(Int, EncodeError) {
case candidate > max_version {
True -> Error(DataExceedsCapacity(0, 0))
False -> {
case mode_supported(selected_mode, candidate) {
False -> do_find_version(text, selected_mode, ecc, candidate + 1)
True ->
case data_capacity_bits(candidate, ecc) {
Error(_) -> do_find_version(text, selected_mode, ecc, candidate + 1)
Ok(capacity) ->
case encoded_bits(text, selected_mode, candidate) {
Error(_) ->
do_find_version(text, selected_mode, ecc, candidate + 1)
Ok(required) ->
case required <= capacity {
True -> Ok(candidate)
False ->
do_find_version(text, selected_mode, ecc, candidate + 1)
}
}
}
}
}
}
}
fn encoded_bits(
text: String,
selected_mode: Mode,
version: Int,
) -> Result(Int, EncodeError) {
let mode_bits_count = mode_indicator_bits(version)
case mode_supported(selected_mode, version) {
False ->
Error(IncompatibleOptions(
"Mode not supported by Micro QR M" <> int_to_str(version),
))
True ->
case char_count_bits_for_mode(selected_mode, version) {
Error(error) -> Error(error)
Ok(count_bits) -> {
let data_bits = mode.data_bits_length(text, selected_mode)
Ok(mode_bits_count + count_bits + data_bits)
}
}
}
}
fn mode_supported(selected_mode: Mode, version: Int) -> Bool {
case version, selected_mode {
1, Numeric -> True
1, _ -> False
2, Numeric | 2, Alphanumeric -> True
2, _ -> False
_, _ -> True
}
}
fn mode_indicator_bits(version: Int) -> Int {
version - 1
}
fn mode_indicator_value(selected_mode: Mode) -> Int {
case selected_mode {
Numeric -> 0
Alphanumeric -> 1
Byte -> 2
Kanji -> 3
}
}
fn char_count_bits_for_mode(
selected_mode: Mode,
version: Int,
) -> Result(Int, EncodeError) {
case version, selected_mode {
1, Numeric -> Ok(3)
2, Numeric -> Ok(4)
2, Alphanumeric -> Ok(3)
3, Numeric -> Ok(5)
3, Alphanumeric -> Ok(4)
3, Byte -> Ok(4)
3, Kanji -> Ok(3)
4, Numeric -> Ok(6)
4, Alphanumeric -> Ok(5)
4, Byte -> Ok(5)
4, Kanji -> Ok(4)
_, _ ->
Error(IncompatibleOptions(
"Mode is not supported by Micro QR M" <> int_to_str(version),
))
}
}
fn create_codewords(
text: String,
selected_mode: Mode,
ecc: ErrorCorrection,
version: Int,
) -> Result(List(Int), EncodeError) {
use capacity <- result_try(data_capacity_bits(version, ecc))
use count_bits <- result_try(char_count_bits_for_mode(selected_mode, version))
let count_value = mode.character_count(text, selected_mode)
use payload <- result_try(mode.encode(text, selected_mode, at_index: 0))
let stream =
bitstream.new()
|> append_mode_indicator(selected_mode, version)
|> bitstream.append_bits(count_value, size: count_bits)
|> bitstream.append_bytes(payload)
let total_bits = bitstream.length_bits(stream)
case total_bits > capacity {
True -> Error(DataExceedsCapacity(total_bits, capacity))
False -> {
let terminator_bits = terminator_size(version, capacity - total_bits)
let with_terminator =
bitstream.append_bits(stream, 0, size: terminator_bits)
let padded = pad_to_capacity(with_terminator, version, ecc)
use data_bytes <- result_try(Ok(bitstream.to_byte_list(padded)))
use ec_bytes <- result_try(compute_ec(data_bytes, version, ecc))
Ok(list.append(data_bytes, ec_bytes))
}
}
}
fn append_mode_indicator(
stream: bitstream.BitStream,
selected_mode: Mode,
version: Int,
) -> bitstream.BitStream {
let bits = mode_indicator_bits(version)
case bits {
0 -> stream
_ ->
bitstream.append_bits(stream, mode_indicator_value(selected_mode), bits)
}
}
fn terminator_size(version: Int, remaining: Int) -> Int {
let target = version * 2 + 1
case remaining < target {
True -> remaining
False -> target
}
}
fn pad_to_capacity(
stream: bitstream.BitStream,
version: Int,
ecc: ErrorCorrection,
) -> bitstream.BitStream {
let aligned = bitstream.pad_to_byte_boundary(stream)
let data_bytes_count = case data_codewords(version, ecc) {
Ok(value) -> value
Error(_) -> 0
}
pad_alternating(aligned, data_bytes_count, 0)
}
fn pad_alternating(
stream: bitstream.BitStream,
target_bytes: Int,
index: Int,
) -> bitstream.BitStream {
let current_bytes = list.length(bitstream.to_byte_list(stream))
case current_bytes >= target_bytes {
True -> stream
False -> {
let byte = case index % 2 == 0 {
True -> 0xEC
False -> 0x11
}
pad_alternating(
bitstream.append_byte(stream, byte),
target_bytes,
index + 1,
)
}
}
}
/// Total number of full data codewords for `(version, ecc)`. Half-byte cases
/// (M1-L, M3-L, M3-M) round up to the next byte; the half-codeword behaviour is
/// applied during matrix placement.
pub fn data_codewords(
version: Int,
ecc: ErrorCorrection,
) -> Result(Int, EncodeError) {
case data_capacity_bits(version, ecc) {
Ok(bits) -> Ok({ bits + 7 } / 8)
Error(error) -> Error(error)
}
}
fn compute_ec(
data: List(Int),
version: Int,
ecc: ErrorCorrection,
) -> Result(List(Int), EncodeError) {
case ec_codewords(version, ecc) {
Ok(degree) -> Ok(reed_solomon.encode(data, degree))
Error(error) -> Error(error)
}
}
fn has_half_codeword(version: Int, ecc: ErrorCorrection) -> Bool {
case version, ecc {
1, Low -> True
3, Low -> True
3, Medium -> True
_, _ -> False
}
}
fn build_matrix(
chosen_version: Int,
ecc: ErrorCorrection,
codewords: List(Int),
) -> Result(#(Int, matrix.Matrix), EncodeError) {
case symbol_size(chosen_version) {
Error(error) -> Error(error)
Ok(size) -> {
let base =
matrix.new(size, size)
|> draw_finder(size)
|> draw_timing(size)
|> reserve_format_info(size)
let with_data = place_codewords(base, codewords, chosen_version, ecc)
let #(best_mask, masked) =
choose_best_mask(with_data, chosen_version, ecc, size)
Ok(#(best_mask, place_format_info(masked, chosen_version, ecc, best_mask)))
}
}
}
fn draw_finder(target: matrix.Matrix, _size: Int) -> matrix.Matrix {
util.range(0, finder_size)
|> list.fold(target, fn(acc, row) {
util.range(0, finder_size)
|> list.fold(acc, fn(acc2, col) { draw_finder_module(acc2, row, col) })
})
}
fn draw_finder_module(
target: matrix.Matrix,
row: Int,
col: Int,
) -> matrix.Matrix {
let dark = case row == finder_size || col == finder_size {
True -> False
False ->
row == 0
|| row == finder_size - 1
|| col == 0
|| col == finder_size - 1
|| { row >= 2 && row <= 4 && col >= 2 && col <= 4 }
}
matrix.set(target, row, col, dark, reserved: True)
}
fn draw_timing(target: matrix.Matrix, size: Int) -> matrix.Matrix {
util.range(finder_size + 1, size - 1)
|> list.fold(target, fn(acc, index) {
let dark = index % 2 == 0
acc
|> matrix.set(0, index, dark, reserved: True)
|> matrix.set(index, 0, dark, reserved: True)
})
}
fn reserve_format_info(target: matrix.Matrix, _size: Int) -> matrix.Matrix {
let horizontal =
util.range(1, 8)
|> list.fold(target, fn(acc, col) {
matrix.set(acc, 8, col, False, reserved: True)
})
util.range(1, 7)
|> list.fold(horizontal, fn(acc, row) {
matrix.set(acc, row, 8, False, reserved: True)
})
}
fn place_codewords(
target: matrix.Matrix,
codewords: List(Int),
version: Int,
ecc: ErrorCorrection,
) -> matrix.Matrix {
let half = has_half_codeword(version, ecc)
let data_count = case data_codewords(version, ecc) {
Ok(value) -> value
Error(_) -> 0
}
let bits = codewords_to_bits(codewords, data_count, half, 0, [])
let positions =
data_positions(target, matrix.width(target) - 1, matrix.height(target) - 1)
write_bits(target, positions, bits)
}
fn codewords_to_bits(
codewords: List(Int),
data_count: Int,
half_codeword: Bool,
index: Int,
acc: List(Bool),
) -> List(Bool) {
case codewords {
[] -> list.reverse(acc)
[byte, ..rest] -> {
let bit_count = case half_codeword && index + 1 == data_count {
True -> 4
False -> 8
}
// Write bit_count bits starting from the high end (MSB first) so a half
// codeword contributes bits 7..4 — ISO/IEC 18004 §6.4.10.
let next_acc = append_byte_bits(byte, 7, bit_count, acc)
codewords_to_bits(rest, data_count, half_codeword, index + 1, next_acc)
}
}
}
fn append_byte_bits(
byte: Int,
bit_position: Int,
remaining: Int,
acc: List(Bool),
) -> List(Bool) {
case remaining <= 0 {
True -> acc
False ->
append_byte_bits(byte, bit_position - 1, remaining - 1, [
bit_at(byte, bit_position),
..acc
])
}
}
fn bit_at(value: Int, index: Int) -> Bool {
value / power_of_two(index) % 2 == 1
}
fn power_of_two(index: Int) -> Int {
case index <= 0 {
True -> 1
False -> 2 * power_of_two(index - 1)
}
}
fn data_positions(
target: matrix.Matrix,
col: Int,
row: Int,
) -> List(#(Int, Int)) {
do_data_positions(target, col, row, -1, [])
}
fn do_data_positions(
target: matrix.Matrix,
col: Int,
row: Int,
inc: Int,
acc: List(#(Int, Int)),
) -> List(#(Int, Int)) {
case col <= 0 {
True -> list.reverse(acc)
False -> {
let #(next_acc, last_row, next_inc) =
scan_column_pair(target, col, row, inc, acc)
do_data_positions(target, col - 2, last_row, next_inc, next_acc)
}
}
}
fn scan_column_pair(
target: matrix.Matrix,
col: Int,
row: Int,
inc: Int,
acc: List(#(Int, Int)),
) -> #(List(#(Int, Int)), Int, Int) {
let with_right = maybe_take(target, row, col, acc)
let with_both = maybe_take(target, row, col - 1, with_right)
let next_row = row + inc
case next_row < 0 || next_row >= matrix.height(target) {
True -> #(with_both, row, 0 - inc)
False -> scan_column_pair(target, col, next_row, inc, with_both)
}
}
fn maybe_take(
target: matrix.Matrix,
row: Int,
col: Int,
acc: List(#(Int, Int)),
) -> List(#(Int, Int)) {
case col < 0 {
True -> acc
False ->
case matrix.is_reserved(target, row, col) {
True -> acc
False -> [#(row, col), ..acc]
}
}
}
fn write_bits(
target: matrix.Matrix,
positions: List(#(Int, Int)),
bits: List(Bool),
) -> matrix.Matrix {
case positions, bits {
[#(row, col), ..rest_positions], [bit, ..rest_bits] ->
write_bits(
matrix.set(target, row, col, bit, reserved: False),
rest_positions,
rest_bits,
)
_, _ -> target
}
}
fn choose_best_mask(
target: matrix.Matrix,
version: Int,
ecc: ErrorCorrection,
size: Int,
) -> #(Int, matrix.Matrix) {
do_choose_mask(target, version, ecc, size, 0, 0, target, -1)
}
fn do_choose_mask(
target: matrix.Matrix,
version: Int,
ecc: ErrorCorrection,
size: Int,
candidate: Int,
best_mask: Int,
best_matrix: matrix.Matrix,
best_score: Int,
) -> #(Int, matrix.Matrix) {
case candidate > 3 {
True -> #(best_mask, best_matrix)
False -> {
let with_data = apply_mask(target, candidate)
let placed = place_format_info(with_data, version, ecc, candidate)
let score = micro_penalty(placed, size)
case best_score < 0 || score > best_score {
True ->
do_choose_mask(
target,
version,
ecc,
size,
candidate + 1,
candidate,
placed,
score,
)
False ->
do_choose_mask(
target,
version,
ecc,
size,
candidate + 1,
best_mask,
best_matrix,
best_score,
)
}
}
}
}
fn apply_mask(target: matrix.Matrix, mask: Int) -> matrix.Matrix {
do_apply_mask(target, mask, 0, 0)
}
fn do_apply_mask(
target: matrix.Matrix,
mask: Int,
row: Int,
col: Int,
) -> matrix.Matrix {
case row >= matrix.height(target) {
True -> target
False ->
case col >= matrix.width(target) {
True -> do_apply_mask(target, mask, row + 1, 0)
False ->
case matrix.is_reserved(target, row, col) {
True -> do_apply_mask(target, mask, row, col + 1)
False ->
do_apply_mask(
matrix.xor(target, row, col, micro_mask_at(mask, row, col)),
mask,
row,
col + 1,
)
}
}
}
}
fn micro_mask_at(mask: Int, row: Int, col: Int) -> Bool {
case mask {
0 -> row % 2 == 0
1 -> { row / 2 + col / 3 } % 2 == 0
2 -> { row * col % 2 + row * col % 3 } % 2 == 0
_ -> { { row + col } % 2 + row * col % 3 } % 2 == 0
}
}
/// Annex K evaluation: count dark modules on the right column and bottom row
/// of the symbol; pick the mask that maximises the score
/// `min(rp1, rp2) * 16 + max(rp1, rp2)` (ISO/IEC 18004 §G.2 / Annex K).
fn micro_penalty(target: matrix.Matrix, size: Int) -> Int {
let last = size - 1
let dark_right = count_dark_edge(target, last, 1, last, True)
let dark_bottom = count_dark_edge(target, 1, last, last, False)
let high = case dark_right >= dark_bottom {
True -> dark_right
False -> dark_bottom
}
let low = case dark_right >= dark_bottom {
True -> dark_bottom
False -> dark_right
}
low * 16 + high
}
fn count_dark_edge(
target: matrix.Matrix,
axis: Int,
from: Int,
to: Int,
vertical: Bool,
) -> Int {
do_count_dark_edge(target, axis, from, to, vertical, 0)
}
fn do_count_dark_edge(
target: matrix.Matrix,
axis: Int,
current: Int,
to: Int,
vertical: Bool,
acc: Int,
) -> Int {
case current > to {
True -> acc
False -> {
let dark = case vertical {
True -> matrix.get(target, current, axis)
False -> matrix.get(target, axis, current)
}
let next_acc = case dark {
True -> acc + 1
False -> acc
}
do_count_dark_edge(target, axis, current + 1, to, vertical, next_acc)
}
}
}
fn place_format_info(
target: matrix.Matrix,
version: Int,
ecc: ErrorCorrection,
mask: Int,
) -> matrix.Matrix {
let bits = format_bits(version, ecc, mask)
let horizontal = place_horizontal_format(target, bits)
place_vertical_format(horizontal, bits)
}
fn place_horizontal_format(target: matrix.Matrix, bits: Int) -> matrix.Matrix {
util.range(0, 7)
|> list.fold(target, fn(acc, index) {
let col = index + 1
let dark = bit_at(bits, 14 - index)
matrix.set(acc, 8, col, dark, reserved: True)
})
}
fn place_vertical_format(target: matrix.Matrix, bits: Int) -> matrix.Matrix {
util.range(0, 6)
|> list.fold(target, fn(acc, index) {
let row = 7 - index
let dark = bit_at(bits, 6 - index)
matrix.set(acc, row, 8, dark, reserved: True)
})
}
fn format_bits(version: Int, ecc: ErrorCorrection, mask: Int) -> Int {
case symbol_number(version, ecc) {
Ok(number) -> {
let index = number * 4 + mask
util.at_or(format_info_table, index, default: format_xor_mask)
}
Error(_) -> format_xor_mask
}
}
fn symbol_number(version: Int, ecc: ErrorCorrection) -> Result(Int, EncodeError) {
case version, ecc {
1, Low -> Ok(0)
2, Low -> Ok(1)
2, Medium -> Ok(2)
3, Low -> Ok(3)
3, Medium -> Ok(4)
4, Low -> Ok(5)
4, Medium -> Ok(6)
4, Quartile -> Ok(7)
_, _ ->
Error(IncompatibleOptions(
"Micro QR M"
<> int_to_str(version)
<> " does not support the requested error correction level",
))
}
}
fn int_to_str(value: Int) -> String {
case value {
1 -> "1"
2 -> "2"
3 -> "3"
4 -> "4"
_ -> "?"
}
}
fn result_try(
result: Result(a, EncodeError),
callback: fn(a) -> Result(b, EncodeError),
) -> Result(b, EncodeError) {
case result {
Ok(value) -> callback(value)
Error(error) -> Error(error)
}
}