moon-qrcode

    Pure-MoonBit QR Code Model 2 encoder with optimal segmentation, ASCII/SVG renderers, payload templates, a native CLI and a typed JS/npm API | 纯 MoonBit 实现的 QR Code Model 2 编码器,支持最优分段、ASCII/SVG 渲染、载荷模板、原生 CLI 和带类型的 JS/npm API

    qrcode
    qr
    encoder
    iso18004
    svg
    Download zip
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    10 days ago
    Downloads
    35

    #MoonQR

    CI

    English | 中文

    A pure-MoonBit QR Code encoder for ISO/IEC 18004 Model 2 symbols. The encoder produces the finished module matrix; its core, renderers and payload templates are dependency-free, and a thin CLI is included on top.

    • Versions 1–40, error-correction levels L / M / Q / H
    • Numeric, alphanumeric and UTF-8 byte segments with optimal dynamic-programming auto-segmentation
    • Raw Bytes payloads and caller-composed segment lists
    • Automatic version selection and all eight data masks with Annex J scoring
    • Reed–Solomon over GF(28) (0x11D), BCH format/version information
    • Immutable QrCode matrix (size, get, modules, to_bytes)
    • ASCII, half-height and SVG renderers; SVG merges dark runs into one <path>
    • Payload templates: URL, WiFi, tel, mailto, SMS, geo, vCard and text
    • Flat Uint8Array ABI, a typed ESM/npm wrapper and a native CLI with stdin and JSON output
    • Machine-checked capacity contracts (moon prove + Z3) and a Python qrcode reference cross-check

    #Install

    moon add allenli178/moon-qrcode

    Requires MoonBit 0.1.20261023 or newer.

    #Quick start

    ///|
    test "README quick start" {
    let qr = @moon-qrcode.encode("HELLO WORLD", ec_level=@moon-qrcode.EcLevel::M)
    assert_eq(qr.version().value(), 1)
    assert_eq(qr.size(), 21)
    assert_true(qr.get(0, 0))
    let svg = @moon-qrcode.to_svg(qr, quiet_zone=4, module_size=4)
    assert_true(svg.contains("<svg"))
    }

    encode picks the smallest version in min_version..max_version that fits the selected EC level, then picks the mask with the lowest penalty.

    OptionDefaultMeaning
    text—payload string (UTF-8 in byte mode)
    ec_levelMerror-correction level
    min_version / max_version1 / 40version search range
    modeautoforce Numeric, Alphanumeric or Byte
    maskautoforce a mask M0–M7

    #Reading the matrix

    FunctionResult
    size()side length in modules (4 * version + 17)
    get(row, col)Bool for one module
    modules()row-major Array[Bool] copy
    to_bytes()row-major Array[Byte], 1 = dark
    version() / ec_level() / mask()metadata

    Errors are checked: DataTooLong carries the needed bits, capacity, version and EC level, and QrError::message() renders an actionable diagnostic.

    #Raw bytes and custom segments

    encode_bytes encodes arbitrary binary data as one Byte segment, preserving bytes that are not valid UTF-8. encode_segments accepts a caller-composed segment list and encodes it in order.

    ///|
    test "README payload options" {
    let binary = @moon-qrcode.encode_bytes(
    b"\xff\x00\x80",
    ec_level=@moon-qrcode.EcLevel::L,
    )
    assert_eq(binary.version().value(), 1)
    let segments : Array[@moon-qrcode.Segment] = [
    @moon-qrcode.Segment::new(@moon-qrcode.Mode::Numeric, "12345"),
    @moon-qrcode.Segment::from_bytes(b"\xff\x00"),
    ]
    let qr = @moon-qrcode.encode_segments(
    segments,
    ec_level=@moon-qrcode.EcLevel::M,
    )
    assert_eq(qr.version().value(), 1)
    }

    In auto mode encode runs a dynamic-programming segmentation over all three modes and all version groups, so mixed payloads get the cheapest encoding rather than greedy class runs. Supplying mode keeps the deterministic greedy split from split_segments.

    #Rendering

    ///|
    test "README rendering" {
    let qr = @moon-qrcode.encode("https://moonbitlang.com")
    let ascii = @moon-qrcode.to_ascii(qr, quiet_zone=2)
    let half = @moon-qrcode.to_ascii_half(qr)
    assert_true(ascii.length() > 0)
    assert_true(half.length() > 0)
    let custom = @moon-qrcode.to_svg(qr, dark="navy", light="#ffffff")
    assert_true(custom.contains("fill=\"navy\""))
    }

    • to_ascii(qr, quiet_zone?, scale?) uses double-width blocks; scale repeats each module.
    • to_ascii_half(qr, quiet_zone?) packs two module rows per terminal row.
    • to_svg(qr, quiet_zone?, module_size?, dark?, light?) returns a complete SVG document. Horizontal dark runs are merged into a single <path>; colors are validated and url(...) is rejected. Invalid colors raise QrError::InvalidColor.

    #Payload templates

    ///|
    test "README templates" {
    let wifi = @moon-qrcode.Content::wifi("Cafe", "latte")
    assert_eq(wifi.to_text(), "WIFI:T:WPA;S:Cafe;P:latte;;")
    let card = @moon-qrcode.Content::vcard(
    "Ada Lovelace",
    email="ada@example.com",
    )
    let qr = @moon-qrcode.encode_content(card, ec_level=@moon-qrcode.EcLevel::H)
    assert_true(qr.size() >= 21)
    }

    Available constructors: Content::wifi(ssid, password, auth?), Content::mailto(address, subject?, body?), Content::sms(number, message?), Content::tel(number), Content::geo(lat, lng), Content::vcard(name, phone?, email?, org?), plus Text and Url. to_text() serializes a payload; encode_content() serializes and encodes it.

    #CLI

    # run from the module checkout moon run cmd/moonqr --target native -- "HELLO WORLD" -l H -o hello.svg # release binary: _build/native/release/build/cmd/moonqr/moonqr.exe moon build cmd/moonqr --target native --release

    USAGE: moonqr <payload> [options] moonqr - [options] < file moonqr [options] -- <payload> -l, --level <L|M|Q|H> error correction level (default M) -v, --version <1..40> force an explicit version --min-version <n> smallest version considered --max-version <n> largest version considered -m, --mode <name> numeric | alphanumeric | byte (default auto) --mask <0..7> force a mask pattern -q, -qz, --quiet-zone <n> quiet zone width in modules (default 4) -s, --scale <n> ASCII scale factor (default 1) --module-size <n> SVG module size in pixels (default 4) -f, --format <name> ascii | half | svg | json (default: by extension) --json JSON metadata and module rows -o, --output <path> write to a file instead of stdout --svg / --half format shorthands --dark / --light SVG colors -h, --help show help -V, --cli-version show the CLI version

    A payload of - reads standard input; trailing newlines are stripped. Use -- before a payload that starts with a dash. Diagnostics go to stderr, so stdout stays clean for pipes and files.

    Examples:

    moonqr "https://moonbitlang.com" --svg -o site.svg moonqr "0123456789" -v 2 -m numeric -s 2 moonqr "payload" --min-version 5 --max-version 10 -l Q -f half echo -n "HELLO" | moonqr - -l H --svg cat payload.txt | moonqr - -o code.svg moonqr --json "HELLO" | jq '.version, .size' moonqr -- --payload-starting-with-dash

    --json (or -f json, or a .json output path) emits {"version":1,"ecLevel":"L","mask":7,"size":21,"modules":["…", …]} where each module row is a string of 0/1 characters.

    #JS / wasm flat API

    The js package exports a primitive-typed buffer ABI so hosts never pay for JSON or nested arrays. Build it with:

    moon build js --target js --release # _build/js/release/build/js/js.js + .d.ts moon build js --target wasm-gc --release # _build/wasm-gc/release/build/js/js.wasm

    import { encode_flat_buffer } from "./_build/js/release/build/js/js.js"; // ec_level: 0=L, 1=M, 2=Q, 3=H const result = encode_flat_buffer("HELLO WORLD", 1, 1, 40); if (result.$tag === 1) { const buf = result._0; // Uint8Array const size = (buf[0] << 8) | buf[1]; // side length in modules const version = buf[2]; const mask = buf[3]; const modules = buf.subarray(4); // size * size bytes, 1 = dark }

    Buffer layout: [size_hi, size_lo, version, mask, modules...]. The JS binding type-checks as Uint8Array; errors surface as Result ($tag === 0). encode_bytes_flat_buffer takes binary data, encode_bytes_to_modules returns the module array directly, and flat_error_message renders a readable error.

    #Typed ESM/npm wrapper

    npm/ wraps the JS build in a typed ESM API:

    npm ci --prefix npm npm test --prefix npm # builds dist/moonbit.js, runs Node + tsc checks

    import { encode, encodeBytes } from "./npm/index.js"; const qr = encode("HELLO WORLD", { ecLevel: "H", mask: 3 }); const binary = encodeBytes(new Uint8Array([0xff, 0x00]), { ecLevel: "L" });

    encode(text, options?) and encodeBytes(data, options?) return { size, version, ecLevel, mask, modules }; encodeToBytes returns the Uint8Array matrix; invalid options throw QrCodeError. The package is marked private: true and is not published to the npm registry yet.

    Measured artifact sizes (release, this repository):

    ArtifactRawgzip
    wasm-gc encoder + flat API (js.wasm)33.9 KiB12.9 KiB
    JS ESM encoder + flat API (js.js)100.9 KiB14.0 KiB
    native release CLI (moonqr.exe)354.2 KiB—

    #Correctness and testing

    moon test --target wasm-gc moon test --target wasm moon test --target js moon test --target native moon fmt --check && moon info # formal capacity contracts (requires z3) moon prove core/capacity_model # reference cross-check (Python 3 + qrcode==8.2) python3 -m pip install -r tools/fuzz/requirements.txt moon build cmd/moonqr --target native --release python3 tools/fuzz/verify.py # CLI smoke tests python3 tools/cli/smoke.py # ZXing decode check (Node 18+) npm ci --prefix tools/zxing node tools/zxing/verify.js # npm wrapper tests npm ci --prefix npm npm test --prefix npm # native benchmarks moon bench benchmarks --target native --release

    • 88 unit and README tests across core, render, content, js and the root facade, plus seven native benchmarks.
    • Matrix digests are pinned from the Python qrcode reference implementation: 72 byte-mode vectors across versions/EC levels/masks, 32 alphanumeric and 8 numeric vectors (112 total), plus full-matrix snapshots.
    • tools/fuzz/verify.py compares 203 forced version/EC/mode/mask matrices against Python qrcode 8.2 on every run.
    • Four capacity contracts (total_codewords, char_count_bits, numeric_capacity, byte_capacity) are proved with moon prove and Z3.
    • GF(28), BCH divisibility/known values, RS parity vectors, capacity accounting, greedy and optimal segmentation, error paths and SVG/ASCII output are covered.
    • The same test suite passes on wasm, wasm-gc, JS and native targets, so output is bit-identical across the four backends.
    • Core coverage is 95.5% (moon coverage analyze); remaining uncovered lines are defensive invariant checks and unreachable enum arms.
    • Twelve generated symbols (all EC levels, UTF-8, forced masks and versions, long payloads, payload templates) decode correctly with the ZXing reference implementation.
    • CI runs the test matrix on Ubuntu and macOS, plus size, ZXing, coverage, npm, fuzz, benchmark and formal-proof jobs.

    Physical-camera tests on iOS/Android/WeChat are still manual; ZXing decoding, matrix-level cross-checking and capacity proofs are the automated gates.

    #Scope

    Implemented: Model 2 versions 1–40, L/M/Q/H, numeric/alphanumeric/byte, optimal dynamic-programming auto-segmentation, raw bytes and custom segments, masks, renderers, templates, a stdin/JSON CLI, the flat JS/wasm ABI, a typed npm wrapper, capacity proofs and reference fuzzing.

    Deliberately excluded: decoding, PNG/JPEG/PDF export, logos/styling, Micro QR, Kanji mode, ECI and Structured Append. Those, plus a published npm package and physical-camera automation, are candidates for 1.0.

    #Project layout

    core/ encoder: types, capacity, segments, bit buffer, RS, BCH, matrix, penalty core/capacity_model/ capacity arithmetic with `moon prove` contracts benchmarks/ native benchmark suite render/ ASCII and SVG renderers content/ payload templates js/ flat Uint8Array ABI + ESM/wasm-gc link exports npm/ typed ESM wrapper (private) + Node/TypeScript tests cmd/moonqr CLI tools/zxing ZXing decode verification harness (Node) tools/fuzz Python qrcode matrix cross-check tools/cli CLI smoke tests

    core, core/capacity_model, render, content and js use only the MoonBit standard library. The CLI adds moonbitlang/x for filesystem and process support.

    #License

    Apache-2.0.

    Content

    Payload templates (URL, WiFi, vCard, tel, mailto, SMS, geo).

    载荷模板(URL、WiFi、vCard、电话、邮件、短信、地理坐标)。

    EcLevel

    Encoder facade: value types, capacity helpers and the single encode entry point.

    编码器门面:值类型、容量辅助函数和统一的 encode 入口。

    Mask

    Encoder facade: value types, capacity helpers and the single encode entry point.

    编码器门面:值类型、容量辅助函数和统一的 encode 入口。

    Mode

    Encoder facade: value types, capacity helpers and the single encode entry point.

    编码器门面:值类型、容量辅助函数和统一的 encode 入口。

    QrCode

    Encoder facade: value types, capacity helpers and the single encode entry point.

    编码器门面:值类型、容量辅助函数和统一的 encode 入口。

    QrError

    Encoder facade: value types, capacity helpers and the single encode entry point.

    编码器门面:值类型、容量辅助函数和统一的 encode 入口。

    Segment

    Encoder facade: value types, capacity helpers and the single encode entry point.

    编码器门面:值类型、容量辅助函数和统一的 encode 入口。

    Version

    Encoder facade: value types, capacity helpers and the single encode entry point.

    编码器门面:值类型、容量辅助函数和统一的 encode 入口。

    bits_needed

    fn bits_needed(segments : Array[
    Segment
    ], version : Int) -> Int

    Total bits needed for the segments at a given version, headers included.

    给定版本下所有段的总比特数,包含段头。

    block_count

    fn block_count(version : Int, ec :
    EcLevel
    ) -> Int

    char_count_bits

    fn char_count_bits(version : Int, mode :
    Mode
    ) -> Int

    character_capacity

    fn character_capacity(version : Int, ec :
    EcLevel
    , mode :
    Mode
    ) -> Int

    Maximum number of characters of the given mode that fit in a version/EC pair.

    给定版本/纠错等级下,指定模式可容纳的最大字符数。

    data_capacity_bits

    fn data_capacity_bits(version : Int, ec :
    EcLevel
    ) -> Int

    data_codewords

    fn data_codewords(version : Int, ec :
    EcLevel
    ) -> Int

    ecc_codewords

    fn ecc_codewords(version : Int, ec :
    EcLevel
    ) -> Int

    encode

    Encodes text as a QR Model 2 symbol.

    将 text 编码为 QR Model 2 符号。

    ec_level defaults to M, version range to 1..40, mode is chosen automatically, and the mask is selected by minimizing the Annex J penalty.

    ec_level 默认为 M,版本范围为 1..40,模式自动选择, 掩码通过最小化 Annex J 惩罚分选出。

    Example

    test "encode hello world" {
    let qr = @core.encode("HELLO WORLD") // V1-M
    assert_eq(qr.size(), 21)
    assert_eq(qr.version().value(), 1)
    }

    encode_bytes

    Encodes arbitrary bytes as one Byte segment. Unlike encode, no UTF-8 interpretation is applied, so binary payloads are preserved exactly.

    将任意字节编码为单个 Byte 段。与 encode 不同,这里不做 UTF-8 解释, 因此二进制载荷会被精确保留。

    Example

    test "encode raw bytes" {
    let qr = @core.encode_bytes(b"\xff\x00\x80")
    assert_eq(qr.version().value(), 1)
    }

    encode_content

    Convenience wrapper: render a template and encode it.

    便捷包装:渲染模板并编码。

    encode_segments

    Encodes an explicit sequence of segments, preserving the caller's mode and ordering choices. Empty segments are dropped. Numeric and alphanumeric segments are validated against their repertoires.

    编码显式给出的段序列,保留调用方的模式与顺序选择。空段会被丢弃; 数字与字母数字段会按其字符集校验。

    Example

    test "encode custom segments" {
    let segments = [
    @core.Segment::new(@core.Mode::Numeric, "123"),
    @core.Segment::new(@core.Mode::Byte, "abc"),
    ]
    let qr = @core.encode_segments(segments)
    assert_eq(qr.version().value(), 1)
    }

    format_info

    BCH(15,5) format information, masked with 0x5412 as required by ISO 7.9.

    BCH(15,5) 格式信息,按 ISO 7.9 要求与 0x5412 异或掩码。

    gf_mul

    fn gf_mul(a : Byte, b : Byte) -> Byte

    Multiplication in GF(2^8).

    GF(2^8) 上的乘法。

    is_valid_color

    fn is_valid_color(color : String) -> Bool

    Accepts hex colors, named CSS colors, and rgb()/rgba()/hsl() style values. Rejects anything containing markup or characters that could escape an attribute.

    接受十六进制颜色、CSS 命名颜色以及 rgb()/rgba()/hsl() 格式。 拒绝包含标记或可能逃逸属性的字符。

    rs_encode

    fn rs_encode(msg : ArrayView[Byte], nsym : Int) -> Array[Byte]

    Reed-Solomon parity bytes for msg with nsym ECC codewords.

    对 msg 计算 nsym 个 ECC 校验字节的 Reed-Solomon 编码。

    split_segments

    Splits text into greedy class runs. Forcing mode produces a single segment and rejects characters outside that mode's repertoire.

    将文本拆分为贪心分类段。强制指定 mode 时生成单个段, 并拒绝该模式字符集之外的字符。

    to_ascii

    fn to_ascii(qr :
    QrCode
    , quiet_zone? : Int, scale? : Int) -> String

    Terminal rendering. Each module is two characters wide so the result looks roughly square in monospaced fonts. quiet_zone is measured in modules and scale repeats every module scale times.

    终端渲染。每个模块占两个字符宽度,在等宽字体下近似方形。 quiet_zone 以模块为单位,scale 控制每个模块的重复次数。

    to_ascii_half

    fn to_ascii_half(qr :
    QrCode
    , quiet_zone? : Int) -> String

    Half-height rendering: two module rows are packed into one text row using upper/lower block characters. Good for narrow terminals.

    半高渲染:使用上下半块字符将两行模块压缩到一行终端输出。 适合窄终端。

    to_svg

    fn to_svg(qr :
    QrCode
    , quiet_zone? : Int, module_size? : Int, dark? : String, light? : String) -> String raise
    QrError

    Renders the symbol as an SVG document. Consecutive dark modules in each row are merged into horizontal runs inside a single <path>, which keeps high-version output small.

    将 QR 码渲染为 SVG 文档。每行连续的深色模块合并为水平行程, 统一写入一个 <path>,以控制高版本码的输出体积。

    total_codewords

    fn total_codewords(version : Int) -> Int

    Number of data + ECC modules available for codewords, divided by 8. Derived from ISO Table 1; remainder bits are not counted.

    可用于码字的数据 + ECC 模块数除以 8。 源自 ISO Table 1;不计余留位。

    version_info

    fn version_info(version : Int) -> Int?

    BCH(18,6) version information for versions 7..40; None below 7.

    BCH(18,6) 版本信息,适用于版本 7..40;低于 7 时返回 None。

    Source Files