From 4cb2f24899dd9560b8e34dd575859b6840949818 Mon Sep 17 00:00:00 2001 From: Zachary Levy Date: Mon, 20 Apr 2026 14:53:30 -0700 Subject: [PATCH] QR Code generation --- .zed/tasks.json | 27 +- qrcode/examples/examples.odin | 305 ++++ qrcode/generate.odin | 2832 +++++++++++++++++++++++++++++++++ 3 files changed, 3163 insertions(+), 1 deletion(-) create mode 100644 qrcode/examples/examples.odin create mode 100644 qrcode/generate.odin diff --git a/.zed/tasks.json b/.zed/tasks.json index 34251b1..e08acae 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -32,9 +32,14 @@ "command": "odin test phased_executor -out=out/debug/test_phased_executor", "cwd": "$ZED_WORKTREE_ROOT", }, + { + "label": "Test qrcode", + "command": "odin test qrcode -out=out/debug/test_qrcode", + "cwd": "$ZED_WORKTREE_ROOT", + }, { "label": "Test all", - "command": "odin test many_bits -out=out/debug/test_many_bits && odin test ring -out=out/debug/test_ring && odin test levsort -out=out/debug/test_levsort && odin test levsync -out=out/debug/test_levsync && odin test levmath -out=out/debug/test_levmath && odin test phased_executor -out=out/debug/test_phased_executor", + "command": "odin test many_bits -out=out/debug/test_many_bits && odin test ring -out=out/debug/test_ring && odin test levsort -out=out/debug/test_levsort && odin test levsync -out=out/debug/test_levsync && odin test levmath -out=out/debug/test_levmath && odin test phased_executor -out=out/debug/test_phased_executor && odin test qrcode -out=out/debug/test_qrcode", "cwd": "$ZED_WORKTREE_ROOT", }, // --------------------------------------------------------------------------------------------------------------------- @@ -65,6 +70,26 @@ "command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-custom", "cwd": "$ZED_WORKTREE_ROOT", }, + { + "label": "Run qrcode basic example", + "command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- basic", + "cwd": "$ZED_WORKTREE_ROOT", + }, + { + "label": "Run qrcode variety example", + "command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- variety", + "cwd": "$ZED_WORKTREE_ROOT", + }, + { + "label": "Run qrcode segment example", + "command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- segment", + "cwd": "$ZED_WORKTREE_ROOT", + }, + { + "label": "Run qrcode mask example", + "command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- mask", + "cwd": "$ZED_WORKTREE_ROOT", + }, // --------------------------------------------------------------------------------------------------------------------- // ----- Other ------------------------ // --------------------------------------------------------------------------------------------------------------------- diff --git a/qrcode/examples/examples.odin b/qrcode/examples/examples.odin new file mode 100644 index 0000000..4db3d59 --- /dev/null +++ b/qrcode/examples/examples.odin @@ -0,0 +1,305 @@ +package examples + +import "core:fmt" +import "core:mem" +import "core:os" + +import qr ".." + +main :: proc() { + //----- Tracking allocator ---------------------------------- + { + tracking_temp_allocator := false + // Temp + track_temp: mem.Tracking_Allocator + if tracking_temp_allocator { + mem.tracking_allocator_init(&track_temp, context.temp_allocator) + context.temp_allocator = mem.tracking_allocator(&track_temp) + } + // Default + track: mem.Tracking_Allocator + mem.tracking_allocator_init(&track, context.allocator) + context.allocator = mem.tracking_allocator(&track) + defer { + // Temp allocator + if tracking_temp_allocator { + if len(track_temp.allocation_map) > 0 { + fmt.eprintf("=== %v allocations not freed - temp allocator: ===\n", len(track_temp.allocation_map)) + for _, entry in track_temp.allocation_map { + fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location) + } + } + if len(track_temp.bad_free_array) > 0 { + fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array)) + for entry in track_temp.bad_free_array { + fmt.eprintf("- %p @ %v\n", entry.memory, entry.location) + } + } + mem.tracking_allocator_destroy(&track_temp) + } + // Default allocator + if len(track.allocation_map) > 0 { + fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map)) + for _, entry in track.allocation_map { + fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location) + } + } + if len(track.bad_free_array) > 0 { + fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array)) + for entry in track.bad_free_array { + fmt.eprintf("- %p @ %v\n", entry.memory, entry.location) + } + } + mem.tracking_allocator_destroy(&track) + } + } + + args := os.args + if len(args) < 2 { + fmt.eprintln("Usage: examples ") + fmt.eprintln("Available examples: basic, variety, segment, mask") + os.exit(1) + } + + switch args[1] { + case "basic": basic() + case "variety": variety() + case "segment": segment() + case "mask": mask() + case: + fmt.eprintf("Unknown example: %v\n", args[1]) + fmt.eprintln("Available examples: basic, variety, segment, mask") + os.exit(1) + } +} + +// ------------------------------------------------------------------------------------------------- +// Utilities +// ------------------------------------------------------------------------------------------------- + +// Prints the given QR Code to the console. +print_qr :: proc(qrcode: []u8) { + size := qr.get_size(qrcode) + border :: 4 + for y in -border ..< size + border { + for x in -border ..< size + border { + fmt.print("##" if qr.get_module(qrcode, x, y) else " ") + } + fmt.println() + } + fmt.println() +} + +// ------------------------------------------------------------------------------------------------- +// Demo: Basic +// ------------------------------------------------------------------------------------------------- + +// Creates a single QR Code, then prints it to the console. +basic :: proc() { + text :: "Hello, world!" + ecl :: qr.Ecc.Low + + qrcode: [qr.BUFFER_LEN_MAX]u8 + ok := qr.encode(text, qrcode[:], ecl) + if ok do print_qr(qrcode[:]) +} + +// ------------------------------------------------------------------------------------------------- +// Demo: Variety +// ------------------------------------------------------------------------------------------------- + +// Creates a variety of QR Codes that exercise different features of the library. +variety :: proc() { + qrcode: [qr.BUFFER_LEN_MAX]u8 + + { // Numeric mode encoding (3.33 bits per digit) + ok := qr.encode("314159265358979323846264338327950288419716939937510", qrcode[:], qr.Ecc.Medium) + if ok do print_qr(qrcode[:]) + } + + { // Alphanumeric mode encoding (5.5 bits per character) + ok := qr.encode("DOLLAR-AMOUNT:$39.87 PERCENTAGE:100.00% OPERATIONS:+-*/", qrcode[:], qr.Ecc.High) + if ok do print_qr(qrcode[:]) + } + + { // Unicode text as UTF-8 + ok := qr.encode( + "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1wa\xE3\x80\x81" + + "\xE4\xB8\x96\xE7\x95\x8C\xEF\xBC\x81\x20\xCE\xB1\xCE\xB2\xCE\xB3\xCE\xB4", + qrcode[:], + qr.Ecc.Quartile, + ) + if ok do print_qr(qrcode[:]) + } + + { // Moderately large QR Code using longer text (from Lewis Carroll's Alice in Wonderland) + ok := qr.encode( + "Alice was beginning to get very tired of sitting by her sister on the bank, " + + "and of having nothing to do: once or twice she had peeped into the book her sister was reading, " + + "but it had no pictures or conversations in it, 'and what is the use of a book,' thought Alice " + + "'without pictures or conversations?' So she was considering in her own mind (as well as she could, " + + "for the hot day made her feel very sleepy and stupid), whether the pleasure of making a " + + "daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly " + + "a White Rabbit with pink eyes ran close by her.", + qrcode[:], + qr.Ecc.High, + ) + if ok do print_qr(qrcode[:]) + } +} + +// ------------------------------------------------------------------------------------------------- +// Demo: Segment +// ------------------------------------------------------------------------------------------------- + +// Creates QR Codes with manually specified segments for better compactness. +segment :: proc() { + qrcode: [qr.BUFFER_LEN_MAX]u8 + + { // Illustration "silver" + silver0 :: "THE SQUARE ROOT OF 2 IS 1." + silver1 :: "41421356237309504880168872420969807856967187537694807317667973799" + + // Encode as single text (auto mode selection) + { + concat :: silver0 + silver1 + ok := qr.encode(concat, qrcode[:], qr.Ecc.Low) + if ok do print_qr(qrcode[:]) + } + + // Encode as two manual segments (alphanumeric + numeric) for better compactness + { + seg_buf0: [qr.BUFFER_LEN_MAX]u8 + seg_buf1: [qr.BUFFER_LEN_MAX]u8 + segs := [2]qr.Segment{qr.make_alphanumeric(silver0, seg_buf0[:]), qr.make_numeric(silver1, seg_buf1[:])} + ok := qr.encode(segs[:], qr.Ecc.Low, qrcode[:]) + if ok do print_qr(qrcode[:]) + } + } + + { // Illustration "golden" + golden0 :: "Golden ratio \xCF\x86 = 1." + golden1 :: "6180339887498948482045868343656381177203091798057628621354486227052604628189024497072072041893911374" + golden2 :: "......" + + // Encode as single text (auto mode selection) + { + concat :: golden0 + golden1 + golden2 + ok := qr.encode(concat, qrcode[:], qr.Ecc.Low) + if ok do print_qr(qrcode[:]) + } + + // Encode as three manual segments (byte + numeric + alphanumeric) for better compactness + { + golden0_str: string = golden0 + golden0_bytes := transmute([]u8)golden0_str + seg_buf0: [qr.BUFFER_LEN_MAX]u8 + seg_buf1: [qr.BUFFER_LEN_MAX]u8 + seg_buf2: [qr.BUFFER_LEN_MAX]u8 + segs := [3]qr.Segment { + qr.make_bytes(golden0_bytes, seg_buf0[:]), + qr.make_numeric(golden1, seg_buf1[:]), + qr.make_alphanumeric(golden2, seg_buf2[:]), + } + ok := qr.encode(segs[:], qr.Ecc.Low, qrcode[:]) + if ok do print_qr(qrcode[:]) + } + } + + { // Illustration "Madoka": kanji, kana, Cyrillic, full-width Latin, Greek characters + // Encode as text (auto mode — byte mode) + { + madoka :: + "\xE3\x80\x8C\xE9\xAD\x94\xE6\xB3\x95\xE5" + + "\xB0\x91\xE5\xA5\xB3\xE3\x81\xBE\xE3\x81" + + "\xA9\xE3\x81\x8B\xE2\x98\x86\xE3\x83\x9E" + + "\xE3\x82\xAE\xE3\x82\xAB\xE3\x80\x8D\xE3" + + "\x81\xA3\xE3\x81\xA6\xE3\x80\x81\xE3\x80" + + "\x80\xD0\x98\xD0\x90\xD0\x98\xE3\x80\x80" + + "\xEF\xBD\x84\xEF\xBD\x85\xEF\xBD\x93\xEF" + + "\xBD\x95\xE3\x80\x80\xCE\xBA\xCE\xB1\xEF" + + "\xBC\x9F" + ok := qr.encode(madoka, qrcode[:], qr.Ecc.Low) + if ok do print_qr(qrcode[:]) + } + + // Encode with manual kanji mode (13 bits per character) + { + //odinfmt: disable + kanji_chars :: [29]int{ + 0x0035, 0x1002, 0x0FC0, 0x0AED, 0x0AD7, + 0x015C, 0x0147, 0x0129, 0x0059, 0x01BD, + 0x018D, 0x018A, 0x0036, 0x0141, 0x0144, + 0x0001, 0x0000, 0x0249, 0x0240, 0x0249, + 0x0000, 0x0104, 0x0105, 0x0113, 0x0115, + 0x0000, 0x0208, 0x01FF, 0x0008, + } + //odinfmt: enable + + seg_buf: [qr.BUFFER_LEN_MAX]u8 + for &b in seg_buf { + b = 0 + } + + seg: qr.Segment + seg.mode = .Kanji + seg.num_chars = len(kanji_chars) + seg.bit_length = 0 + for ch in kanji_chars { + for j := 12; j >= 0; j -= 1 { + seg_buf[seg.bit_length >> 3] |= u8(((ch >> uint(j)) & 1)) << uint(7 - (seg.bit_length & 7)) + seg.bit_length += 1 + } + } + seg.data = seg_buf[:(seg.bit_length + 7) / 8] + + segs := [1]qr.Segment{seg} + ok := qr.encode(segs[:], qr.Ecc.Low, qrcode[:]) + if ok do print_qr(qrcode[:]) + } + } +} + +// ------------------------------------------------------------------------------------------------- +// Demo: Mask +// ------------------------------------------------------------------------------------------------- + +// Creates QR Codes with the same size and contents but different mask patterns. +mask :: proc() { + qrcode: [qr.BUFFER_LEN_MAX]u8 + + { // Project Nayuki URL + ok: bool + + ok = qr.encode("https://www.nayuki.io/", qrcode[:], qr.Ecc.High) + if ok do print_qr(qrcode[:]) + + ok = qr.encode("https://www.nayuki.io/", qrcode[:], qr.Ecc.High, mask = qr.Mask.M3) + if ok do print_qr(qrcode[:]) + } + + { // Chinese text as UTF-8 + text :: + "\xE7\xB6\xAD\xE5\x9F\xBA\xE7\x99\xBE\xE7\xA7\x91\xEF\xBC\x88\x57\x69\x6B\x69\x70" + + "\x65\x64\x69\x61\xEF\xBC\x8C\xE8\x81\x86\xE8\x81\xBD\x69\x2F\xCB\x8C\x77\xC9\xAA" + + "\x6B\xE1\xB5\xBB\xCB\x88\x70\x69\xCB\x90\x64\x69\x2E\xC9\x99\x2F\xEF\xBC\x89\xE6" + + "\x98\xAF\xE4\xB8\x80\xE5\x80\x8B\xE8\x87\xAA\xE7\x94\xB1\xE5\x85\xA7\xE5\xAE\xB9" + + "\xE3\x80\x81\xE5\x85\xAC\xE9\x96\x8B\xE7\xB7\xA8\xE8\xBC\xAF\xE4\xB8\x94\xE5\xA4" + + "\x9A\xE8\xAA\x9E\xE8\xA8\x80\xE7\x9A\x84\xE7\xB6\xB2\xE8\xB7\xAF\xE7\x99\xBE\xE7" + + "\xA7\x91\xE5\x85\xA8\xE6\x9B\xB8\xE5\x8D\x94\xE4\xBD\x9C\xE8\xA8\x88\xE7\x95\xAB" + + ok: bool + + ok = qr.encode(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M0) + if ok do print_qr(qrcode[:]) + + ok = qr.encode(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M1) + if ok do print_qr(qrcode[:]) + + ok = qr.encode(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M5) + if ok do print_qr(qrcode[:]) + + ok = qr.encode(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M7) + if ok do print_qr(qrcode[:]) + } +} diff --git a/qrcode/generate.odin b/qrcode/generate.odin new file mode 100644 index 0000000..9014b56 --- /dev/null +++ b/qrcode/generate.odin @@ -0,0 +1,2832 @@ +package qrcode + +import "core:slice" + + +// ------------------------------------------------------------------------------------------------- +// Types +// ------------------------------------------------------------------------------------------------- + +// The error correction level in a QR Code symbol. +Ecc :: enum { + Low, // The QR Code can tolerate about 7% erroneous codewords + Medium, // The QR Code can tolerate about 15% erroneous codewords + Quartile, // The QR Code can tolerate about 25% erroneous codewords + High, // The QR Code can tolerate about 30% erroneous codewords +} + +// The mask pattern used in a QR Code symbol. +Mask :: enum u8 { + M0, + M1, + M2, + M3, + M4, + M5, + M6, + M7, +} + +// Describes how a segment's data bits are interpreted. +Mode :: enum { + Numeric = 0x1, + Alphanumeric = 0x2, + Byte = 0x4, + Kanji = 0x8, + Eci = 0x7, +} + +// A segment of character/binary/control data in a QR Code symbol. +Segment :: struct { + mode: Mode, + num_chars: int, + data: []u8, // Slice into caller-provided buffer. Not owned. + bit_length: int, +} + +// ------------------------------------------------------------------------------------------------- +// Constants +// ------------------------------------------------------------------------------------------------- + +VERSION_MIN :: 1 +VERSION_MAX :: 40 + +// The worst-case number of bytes needed to store one QR Code, up to and including version 40. +BUFFER_LEN_MAX :: 3918 // buffer_len_for_version(VERSION_MAX) + +// Returns the number of bytes needed to store any QR Code up to and including the given version. +buffer_len_for_version :: #force_inline proc(n: int) -> int { + size := n * 4 + 17 + return (size * size + 7) / 8 + 1 +} + +// ------------------------------------------------------------------------------------------------- +// Private constants +// ------------------------------------------------------------------------------------------------- + +@(private) +LENGTH_OVERFLOW :: -1 +@(private) +REED_SOLOMON_DEGREE_MAX :: 30 +@(private) +PENALTY_N1 :: 3 +@(private) +PENALTY_N2 :: 3 +@(private) +PENALTY_N3 :: 40 +@(private) +PENALTY_N4 :: 10 + +//odinfmt: disable +// For generating error correction codes. Index 0 is padding (set to illegal value). +@(private) +ECC_CODEWORDS_PER_BLOCK := [4][41]i8{ + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +} + +@(private) +NUM_ERROR_CORRECTION_BLOCKS := [4][41]i8{ + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +} +//odinfmt: enable + + +// ------------------------------------------------------------------------------------------------- +// Encode procedures +// ------------------------------------------------------------------------------------------------- + +// Encodes the given text string to a QR Code, automatically selecting +// numeric, alphanumeric, or byte mode based on content. +// +// Parameters: +// text - [in] UTF-8 encoded input string. +// temp_buffer - [scratch] Temporary workspace, clobbered during encoding. Contents are +// undefined after return. Must not alias qrcode. +// qrcode - [out] On success, contains the encoded QR Code (byte 0 = side length, +// remaining bytes = module bits). On failure, qrcode[0] is set to 0. +// +// Both temp_buffer and qrcode must have length >= buffer_len_for_version(max_version). +// +// Returns ok=false when: +// - The text cannot fit in any version within [min_version, max_version] at the given ECL. +// - The encoded segment data exceeds the buffer capacity. +@(require_results) +encode_text_explicit_temp :: proc( + text: string, + temp_buffer, qrcode: []u8, + ecl: Ecc, + min_version: int = VERSION_MIN, + max_version: int = VERSION_MAX, + mask: Maybe(Mask) = nil, + boost_ecl: bool = true, +) -> ( + ok: bool, +) { + text_len := len(text) + if text_len == 0 { + return encode_segments_advanced_explicit_temp( + nil, + ecl, + min_version, + max_version, + mask, + boost_ecl, + temp_buffer, + qrcode, + ) + } + buf_len := buffer_len_for_version(max_version) + + seg: Segment + encode: { + if is_numeric(text) { + if calc_segment_buffer_size(.Numeric, text_len) > buf_len do break encode + seg = make_numeric(text, temp_buffer) + } else if is_alphanumeric(text) { + if calc_segment_buffer_size(.Alphanumeric, text_len) > buf_len do break encode + seg = make_alphanumeric(text, temp_buffer) + } else { + seg.bit_length = calc_segment_bit_length(.Byte, text_len) + if seg.bit_length == LENGTH_OVERFLOW || text_len > buf_len do break encode + for i in 0 ..< text_len { + temp_buffer[i] = text[i] + } + seg.mode = .Byte + seg.num_chars = text_len + seg.data = temp_buffer[:text_len] + } + segs := [1]Segment{seg} + return encode_segments_advanced_explicit_temp( + segs[:], + ecl, + min_version, + max_version, + mask, + boost_ecl, + temp_buffer, + qrcode, + ) + } + qrcode[0] = 0 + return false +} + +// Encodes text to a QR Code, automatically allocating and freeing the temp buffer. +// +// Parameters: +// qrcode - [out] On success, contains the encoded QR Code. On failure, qrcode[0] is +// set to 0. +// temp_allocator - Allocator used for the internal scratch buffer. Freed before return. +// +// Returns ok=false when: +// - The text cannot fit in any version within [min_version, max_version] at the given ECL. +// - The temp_allocator fails to allocate. +@(require_results) +encode_text_auto :: proc( + text: string, + qrcode: []u8, + ecl: Ecc, + min_version: int = VERSION_MIN, + max_version: int = VERSION_MAX, + mask: Maybe(Mask) = nil, + boost_ecl: bool = true, + temp_allocator := context.temp_allocator, +) -> ( + ok: bool, +) { + size, size_ok := min_buffer_size(text, ecl, min_version, max_version) + if !size_ok { + qrcode[0] = 0 + return false + } + temp_buffer, alloc_err := make([]u8, size, temp_allocator) + if alloc_err != nil { + qrcode[0] = 0 + return false + } + defer delete(temp_buffer, temp_allocator) + return encode_text_explicit_temp(text, temp_buffer, qrcode, ecl, min_version, max_version, mask, boost_ecl) +} + +encode_text :: proc { + encode_text_explicit_temp, + encode_text_auto, +} + +// Encodes arbitrary binary data to a QR Code using byte mode. +// +// Parameters: +// data_and_temp - [in,scratch] The first data_len bytes are the input payload. The entire +// slice is used as temporary workspace during encoding and its +// contents are undefined after return. Must not alias qrcode. +// data_len - Number of input bytes in data_and_temp. +// qrcode - [out] On success, contains the encoded QR Code. On failure, +// qrcode[0] is set to 0. +// +// Both data_and_temp and qrcode must have length >= buffer_len_for_version(max_version). +// +// Returns ok=false when: +// - The payload cannot fit in any version within [min_version, max_version] at the given ECL. +@(require_results) +encode_binary :: proc( + data_and_temp: []u8, + data_len: int, + qrcode: []u8, + ecl: Ecc, + min_version: int = VERSION_MIN, + max_version: int = VERSION_MAX, + mask: Maybe(Mask) = nil, + boost_ecl: bool = true, +) -> ( + ok: bool, +) { + seg: Segment + seg.mode = .Byte + seg.bit_length = calc_segment_bit_length(.Byte, data_len) + if seg.bit_length == LENGTH_OVERFLOW { + qrcode[0] = 0 + return false + } + seg.num_chars = data_len + seg.data = data_and_temp[:data_len] + segs := [1]Segment{seg} + return encode_segments_advanced( + segs[:], + ecl, + min_version, + max_version, + mask, + boost_ecl, + data_and_temp, + qrcode, + ) +} + +// Encodes the given segments to a QR Code using default parameters +// (VERSION_MIN..VERSION_MAX, auto mask, boost ECL). +// +// Parameters: +// segs - [in] Slice of segments to encode. Segment data buffers may alias temp_buffer. +// temp_buffer - [scratch] Temporary workspace, clobbered during encoding. Must not alias qrcode. +// qrcode - [out] On success, contains the encoded QR Code. On failure, qrcode[0] is +// set to 0. +// +// Both temp_buffer and qrcode must have length >= BUFFER_LEN_MAX. +// +// Returns ok=false when: +// - The total segment data exceeds the capacity of version 40 at the given ECL. +@(require_results) +encode_segments_explicit_temp :: proc(segs: []Segment, ecl: Ecc, temp_buffer, qrcode: []u8) -> (ok: bool) { + return encode_segments_advanced_explicit_temp( + segs, + ecl, + VERSION_MIN, + VERSION_MAX, + nil, + true, + temp_buffer, + qrcode, + ) +} + +// Encodes segments to a QR Code using default parameters, automatically allocating the temp buffer. +// +// Parameters: +// segs - [in] Slice of segments to encode. +// qrcode - [out] On success, contains the encoded QR Code. On failure, qrcode[0] is +// set to 0. +// temp_allocator - Allocator used for the internal scratch buffer. Freed before return. +// +// qrcode must have length >= BUFFER_LEN_MAX. +// +// Returns ok=false when: +// - The total segment data exceeds the capacity of version 40 at the given ECL. +// - The temp_allocator fails to allocate. +@(require_results) +encode_segments_auto :: proc( + segs: []Segment, + ecl: Ecc, + qrcode: []u8, + temp_allocator := context.temp_allocator, +) -> ( + ok: bool, +) { + size, size_ok := min_buffer_size(segs, ecl) + if !size_ok { + qrcode[0] = 0 + return false + } + temp_buffer, alloc_err := make([]u8, size, temp_allocator) + if alloc_err != nil { + qrcode[0] = 0 + return false + } + defer delete(temp_buffer, temp_allocator) + return encode_segments_explicit_temp(segs, ecl, temp_buffer, qrcode) +} + +encode_segments :: proc { + encode_segments_explicit_temp, + encode_segments_auto, +} + +// Encodes the given segments to a QR Code with full control over version range, mask, and ECL boosting. +// +// Parameters: +// segs - [in] Slice of segments to encode. Segment data buffers may alias temp_buffer. +// temp_buffer - [scratch] Temporary workspace, clobbered during encoding. Must not alias qrcode. +// qrcode - [out] On success, contains the encoded QR Code. On failure, qrcode[0] is +// set to 0. +// mask - nil for automatic mask selection, or a specific Mask value. +// boost_ecl - If true, the ECL may be raised above ecl when the data fits without +// increasing the version. +// +// Both temp_buffer and qrcode must have length >= buffer_len_for_version(max_version). +// +// Returns ok=false when: +// - The total segment data exceeds the capacity of every version in [min_version, max_version] +// at the given ECL. +@(require_results) +encode_segments_advanced_explicit_temp :: proc( + segs: []Segment, + ecl: Ecc, + min_version, max_version: int, + mask: Maybe(Mask), + boost_ecl: bool, + temp_buffer, qrcode: []u8, +) -> ( + ok: bool, +) { + assert( + VERSION_MIN <= min_version && min_version <= max_version && max_version <= VERSION_MAX, + "version range out of bounds", + ) + + ecl := ecl + + // Find the minimal version number to use + version, data_used_bits: int + for version = min_version;; version += 1 { + data_capacity_bits := get_num_data_codewords(version, ecl) * 8 + data_used_bits = get_total_bits(segs, version) + if data_used_bits != LENGTH_OVERFLOW && data_used_bits <= data_capacity_bits { + break + } + if version >= max_version { + qrcode[0] = 0 + return false + } + } + assert(data_used_bits != LENGTH_OVERFLOW, "data bits overflowed") + + // Increase the error correction level while the data still fits + if boost_ecl { + for e in Ecc.Medium ..= Ecc.High { + if data_used_bits <= get_num_data_codewords(version, e) * 8 { + ecl = e + } + } + } + + // Concatenate all segments to create the data bit string + buf_len := buffer_len_for_version(version) + slice.zero(qrcode[:buf_len]) + bit_len := 0 + for &seg in segs { + append_bits_to_buffer(uint(seg.mode), 4, qrcode, &bit_len) + append_bits_to_buffer(uint(seg.num_chars), num_char_count_bits(seg.mode, version), qrcode, &bit_len) + for j in 0 ..< seg.bit_length { + bit := uint((seg.data[j >> 3] >> uint(7 - (j & 7))) & 1) + append_bits_to_buffer(bit, 1, qrcode, &bit_len) + } + } + assert(bit_len == data_used_bits, "bit length mismatch after segment concatenation") + + // Add terminator and pad up to a byte if applicable + data_capacity_bits := get_num_data_codewords(version, ecl) * 8 + assert(bit_len <= data_capacity_bits, "data exceeds capacity") + terminator_bits := min(data_capacity_bits - bit_len, 4) + append_bits_to_buffer(0, terminator_bits, qrcode, &bit_len) + append_bits_to_buffer(0, (8 - bit_len % 8) % 8, qrcode, &bit_len) + assert(bit_len % 8 == 0, "bit length not byte-aligned after padding") + + // Pad with alternating bytes until data capacity is reached + pad_byte: u8 = 0xEC + for bit_len < data_capacity_bits { + append_bits_to_buffer(uint(pad_byte), 8, qrcode, &bit_len) + pad_byte ~= 0xEC ~ 0x11 + } + + // Compute ECC, draw modules + add_ecc_and_interleave(qrcode, version, ecl, temp_buffer) + initialize_function_modules(version, qrcode) + draw_codewords(temp_buffer, get_num_raw_data_modules(version) / 8, qrcode) + draw_light_function_modules(qrcode, version) + initialize_function_modules(version, temp_buffer) + + // Do masking + chosen_mask: Mask + if m, m_ok := mask.?; m_ok { + chosen_mask = m + } else { + // Automatically choose best mask + min_penalty := max(int) + for msk in Mask { + apply_mask(temp_buffer, qrcode, msk) + draw_format_bits(ecl, msk, qrcode) + penalty := get_penalty_score(qrcode) + if penalty < min_penalty { + chosen_mask = msk + min_penalty = penalty + } + apply_mask(temp_buffer, qrcode, msk) // Undo mask via XOR + } + } + apply_mask(temp_buffer, qrcode, chosen_mask) + draw_format_bits(ecl, chosen_mask, qrcode) + return true +} + +// Encodes segments with full control over parameters, automatically allocating the temp buffer. +// +// Parameters: +// segs - [in] Slice of segments to encode. +// qrcode - [out] On success, contains the encoded QR Code. On failure, qrcode[0] is +// set to 0. +// temp_allocator - Allocator used for the internal scratch buffer. Freed before return. +// +// qrcode must have length >= buffer_len_for_version(max_version). +// +// Returns ok=false when: +// - The total segment data exceeds the capacity of every version in [min_version, max_version] +// at the given ECL. +// - The temp_allocator fails to allocate. +@(require_results) +encode_segments_advanced_auto :: proc( + segs: []Segment, + ecl: Ecc, + min_version, max_version: int, + mask: Maybe(Mask), + boost_ecl: bool, + qrcode: []u8, + temp_allocator := context.temp_allocator, +) -> ( + ok: bool, +) { + size, size_ok := min_buffer_size(segs, ecl, min_version, max_version) + if !size_ok { + qrcode[0] = 0 + return false + } + temp_buffer, alloc_err := make([]u8, size, temp_allocator) + if alloc_err != nil { + qrcode[0] = 0 + return false + } + defer delete(temp_buffer, temp_allocator) + return encode_segments_advanced_explicit_temp( + segs, + ecl, + min_version, + max_version, + mask, + boost_ecl, + temp_buffer, + qrcode, + ) +} + +encode_segments_advanced :: proc { + encode_segments_advanced_explicit_temp, + encode_segments_advanced_auto, +} + +encode :: proc { + encode_text_explicit_temp, + encode_text_auto, + encode_binary, + encode_segments_explicit_temp, + encode_segments_auto, + encode_segments_advanced_explicit_temp, + encode_segments_advanced_auto, +} + +// ------------------------------------------------------------------------------------------------- +// Error correction code generation +// ------------------------------------------------------------------------------------------------- + +// Appends error correction bytes to each block of data, then interleaves bytes from all blocks. +@(private) +add_ecc_and_interleave :: proc(data: []u8, version: int, ecl: Ecc, result: []u8) { + assert(VERSION_MIN <= version && version <= VERSION_MAX, "version out of bounds") + num_blocks := int(NUM_ERROR_CORRECTION_BLOCKS[int(ecl)][version]) + block_ecc_len := int(ECC_CODEWORDS_PER_BLOCK[int(ecl)][version]) + raw_codewords := get_num_raw_data_modules(version) / 8 + data_len := get_num_data_codewords(version, ecl) + num_short_blocks := num_blocks - raw_codewords % num_blocks + short_block_data_len := raw_codewords / num_blocks - block_ecc_len + + rsdiv: [REED_SOLOMON_DEGREE_MAX]u8 + reed_solomon_compute_divisor(block_ecc_len, rsdiv[:block_ecc_len]) + + dat_offset := 0 + for i in 0 ..< num_blocks { + dat_len := short_block_data_len + (0 if i < num_short_blocks else 1) + ecc := data[data_len:] // Temporary storage + reed_solomon_compute_remainder( + data[dat_offset:dat_offset + dat_len], + rsdiv[:block_ecc_len], + ecc[:block_ecc_len], + ) + k := i + for j in 0 ..< dat_len { + if j == short_block_data_len { + k -= num_short_blocks + } + result[k] = data[dat_offset + j] + k += num_blocks + } + k = data_len + i + for j in 0 ..< block_ecc_len { + result[k] = ecc[j] + k += num_blocks + } + dat_offset += dat_len + } +} + +// Returns the number of 8-bit data codewords (not ECC) for the given version and ECC level. +@(private) +get_num_data_codewords :: proc(version: int, ecl: Ecc) -> int { + e := int(ecl) + return( + get_num_raw_data_modules(version) / 8 - + int(ECC_CODEWORDS_PER_BLOCK[e][version]) * int(NUM_ERROR_CORRECTION_BLOCKS[e][version]) \ + ) +} + +// Returns the number of data bits that can be stored in a QR Code of the given version number. +@(private) +get_num_raw_data_modules :: proc(ver: int) -> int { + assert(VERSION_MIN <= ver && ver <= VERSION_MAX, "version out of bounds") + result := (16 * ver + 128) * ver + 64 + if ver >= 2 { + num_align := ver / 7 + 2 + result -= (25 * num_align - 10) * num_align - 55 + if ver >= 7 { + result -= 36 + } + } + assert(208 <= result && result <= 29648, "raw data modules out of expected range") + return result +} + +// ------------------------------------------------------------------------------------------------- +// Reed-Solomon ECC generator +// ------------------------------------------------------------------------------------------------- + +@(private) +reed_solomon_compute_divisor :: proc(degree: int, result: []u8) { + assert(1 <= degree && degree <= REED_SOLOMON_DEGREE_MAX, "reed-solomon degree out of range") + slice.zero(result[:degree]) + result[degree - 1] = 1 + + root: u8 = 1 + for _ in 0 ..< degree { + for j in 0 ..< degree { + result[j] = reed_solomon_multiply(result[j], root) + if j + 1 < degree { + result[j] ~= result[j + 1] + } + } + root = reed_solomon_multiply(root, 0x02) + } +} + +@(private) +reed_solomon_compute_remainder :: proc(data: []u8, generator: []u8, result: []u8) { + degree := len(generator) + assert(1 <= degree && degree <= REED_SOLOMON_DEGREE_MAX, "reed-solomon degree out of range") + slice.zero(result[:degree]) + for i in 0 ..< len(data) { + factor := data[i] ~ result[0] + for j in 0 ..< degree - 1 { + result[j] = result[j + 1] + } + result[degree - 1] = 0 + for j in 0 ..< degree { + result[j] ~= reed_solomon_multiply(generator[j], factor) + } + } +} + +// Returns the product of two field elements modulo GF(2^8/0x11D). +@(private) +reed_solomon_multiply :: proc(x, y: u8) -> u8 { + z: u8 = 0 + for i := int(7); i >= 0; i -= 1 { + z = u8((uint(z) << 1) ~ ((uint(z) >> 7) * 0x11D)) + z ~= ((y >> uint(i)) & 1) * x + } + return z +} + +// ------------------------------------------------------------------------------------------------- +// Drawing function modules +// ------------------------------------------------------------------------------------------------- + +// Clears the QR Code grid and marks every function module as dark. +@(private) +initialize_function_modules :: proc(version: int, qrcode: []u8) { + qrsize := version * 4 + 17 + total_bytes := (qrsize * qrsize + 7) / 8 + 1 + slice.zero(qrcode[:total_bytes]) + qrcode[0] = u8(qrsize) + + fill_rectangle(6, 0, 1, qrsize, qrcode) + fill_rectangle(0, 6, qrsize, 1, qrcode) + + fill_rectangle(0, 0, 9, 9, qrcode) + fill_rectangle(qrsize - 8, 0, 8, 9, qrcode) + fill_rectangle(0, qrsize - 8, 9, 8, qrcode) + + align_pat_pos: [7]u8 + num_align := get_alignment_pattern_positions(version, &align_pat_pos) + for i in 0 ..< num_align { + for j in 0 ..< num_align { + if !((i == 0 && j == 0) || (i == 0 && j == num_align - 1) || (i == num_align - 1 && j == 0)) { + fill_rectangle(int(align_pat_pos[i]) - 2, int(align_pat_pos[j]) - 2, 5, 5, qrcode) + } + } + } + + if version >= 7 { + fill_rectangle(qrsize - 11, 0, 3, 6, qrcode) + fill_rectangle(0, qrsize - 11, 6, 3, qrcode) + } +} + +@(private) +draw_light_function_modules :: proc(qrcode: []u8, version: int) { + qrsize := get_size(qrcode) + + for i := 7; i < qrsize - 7; i += 2 { + set_module_bounded(qrcode, 6, i, false) + set_module_bounded(qrcode, i, 6, false) + } + + for dy in -4 ..= 4 { + for dx in -4 ..= 4 { + dist := abs(dx) + if abs(dy) > dist { + dist = abs(dy) + } + if dist == 2 || dist == 4 { + set_module_unbounded(qrcode, 3 + dx, 3 + dy, false) + set_module_unbounded(qrcode, qrsize - 4 + dx, 3 + dy, false) + set_module_unbounded(qrcode, 3 + dx, qrsize - 4 + dy, false) + } + } + } + + align_pat_pos: [7]u8 + num_align := get_alignment_pattern_positions(version, &align_pat_pos) + for i in 0 ..< num_align { + for j in 0 ..< num_align { + if (i == 0 && j == 0) || (i == 0 && j == num_align - 1) || (i == num_align - 1 && j == 0) { + continue + } + for dy in -1 ..= 1 { + for dx in -1 ..= 1 { + set_module_bounded(qrcode, int(align_pat_pos[i]) + dx, int(align_pat_pos[j]) + dy, dx == 0 && dy == 0) + } + } + } + } + + if version >= 7 { + rem := version + for _ in 0 ..< 12 { + rem = (rem << 1) ~ ((rem >> 11) * 0x1F25) + } + bits := version << 12 | rem + assert(bits >> 18 == 0, "version bits overflow") + + for i in 0 ..< 6 { + for j in 0 ..< 3 { + k := qrsize - 11 + j + set_module_bounded(qrcode, k, i, (bits & 1) != 0) + set_module_bounded(qrcode, i, k, (bits & 1) != 0) + bits >>= 1 + } + } + } +} + +@(private) +draw_format_bits :: proc(ecl: Ecc, mask: Mask, qrcode: []u8) { + format_table := [4]int{1, 0, 3, 2} + data := format_table[int(ecl)] << 3 | int(mask) + rem := data + for _ in 0 ..< 10 { + rem = (rem << 1) ~ ((rem >> 9) * 0x537) + } + bits := (data << 10 | rem) ~ 0x5412 + assert(bits >> 15 == 0, "format bits overflow") + + for i in 0 ..= 5 { + set_module_bounded(qrcode, 8, i, get_bit(bits, uint(i))) + } + set_module_bounded(qrcode, 8, 7, get_bit(bits, 6)) + set_module_bounded(qrcode, 8, 8, get_bit(bits, 7)) + set_module_bounded(qrcode, 7, 8, get_bit(bits, 8)) + for i in 9 ..< 15 { + set_module_bounded(qrcode, 14 - i, 8, get_bit(bits, uint(i))) + } + + qrsize := get_size(qrcode) + for i in 0 ..< 8 { + set_module_bounded(qrcode, qrsize - 1 - i, 8, get_bit(bits, uint(i))) + } + for i in 8 ..< 15 { + set_module_bounded(qrcode, 8, qrsize - 15 + i, get_bit(bits, uint(i))) + } + set_module_bounded(qrcode, 8, qrsize - 8, true) +} + +@(private) +get_alignment_pattern_positions :: proc(version: int, result: ^[7]u8) -> int { + if version == 1 { + return 0 + } + num_align := version / 7 + 2 + step := (version * 8 + num_align * 3 + 5) / (num_align * 4 - 4) * 2 + pos := version * 4 + 10 + for i := num_align - 1; i >= 1; i -= 1 { + result[i] = u8(pos) + pos -= step + } + result[0] = 6 + return num_align +} + +@(private) +fill_rectangle :: proc(left, top, width, height: int, qrcode: []u8) { + for dy in 0 ..< height { + for dx in 0 ..< width { + set_module_bounded(qrcode, left + dx, top + dy, true) + } + } +} + +// ------------------------------------------------------------------------------------------------- +// Drawing data modules and masking +// ------------------------------------------------------------------------------------------------- + +@(private) +draw_codewords :: proc(data: []u8, data_len: int, qrcode: []u8) { + qrsize := get_size(qrcode) + i := 0 + right := qrsize - 1 + for right >= 1 { + if right == 6 { + right = 5 + } + for vert in 0 ..< qrsize { + for j in 0 ..< 2 { + x := right - j + upward := ((right + 1) & 2) == 0 + y := (qrsize - 1 - vert) if upward else vert + if !get_module_bounded(qrcode, x, y) && i < data_len * 8 { + dark := get_bit(int(data[i >> 3]), uint(7 - (i & 7))) + set_module_bounded(qrcode, x, y, dark) + i += 1 + } + } + } + right -= 2 + } + assert(i == data_len * 8, "not all codeword bits were consumed") +} + +@(private) +apply_mask :: proc(function_modules: []u8, qrcode: []u8, mask: Mask) { + qrsize := get_size(qrcode) + for y in 0 ..< qrsize { + for x in 0 ..< qrsize { + if get_module_bounded(function_modules, x, y) { + continue + } + invert: bool + switch mask { + case .M0: invert = (x + y) % 2 == 0 + case .M1: invert = y % 2 == 0 + case .M2: invert = x % 3 == 0 + case .M3: invert = (x + y) % 3 == 0 + case .M4: invert = (x / 3 + y / 2) % 2 == 0 + case .M5: invert = x * y % 2 + x * y % 3 == 0 + case .M6: invert = (x * y % 2 + x * y % 3) % 2 == 0 + case .M7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0 + } + val := get_module_bounded(qrcode, x, y) + set_module_bounded(qrcode, x, y, val ~ invert) // XOR booleans + } + } +} + +@(private) +get_penalty_score :: proc(qrcode: []u8) -> int { + qrsize := get_size(qrcode) + result := 0 + + // Adjacent modules in row having same color, and finder-like patterns + for y in 0 ..< qrsize { + run_color := false + run_x := 0 + run_history: [7]int + for x in 0 ..< qrsize { + if get_module_bounded(qrcode, x, y) == run_color { + run_x += 1 + if run_x == 5 { + result += PENALTY_N1 + } else if run_x > 5 { + result += 1 + } + } else { + finder_penalty_add_history(run_x, &run_history, qrsize) + if !run_color { + result += finder_penalty_count_patterns(&run_history, qrsize) * PENALTY_N3 + } + run_color = get_module_bounded(qrcode, x, y) + run_x = 1 + } + } + result += finder_penalty_terminate_and_count(run_color, run_x, &run_history, qrsize) * PENALTY_N3 + } + + // Adjacent modules in column having same color, and finder-like patterns + for x in 0 ..< qrsize { + run_color := false + run_y := 0 + run_history: [7]int + for y in 0 ..< qrsize { + if get_module_bounded(qrcode, x, y) == run_color { + run_y += 1 + if run_y == 5 { + result += PENALTY_N1 + } else if run_y > 5 { + result += 1 + } + } else { + finder_penalty_add_history(run_y, &run_history, qrsize) + if !run_color { + result += finder_penalty_count_patterns(&run_history, qrsize) * PENALTY_N3 + } + run_color = get_module_bounded(qrcode, x, y) + run_y = 1 + } + } + result += finder_penalty_terminate_and_count(run_color, run_y, &run_history, qrsize) * PENALTY_N3 + } + + // 2*2 blocks of modules having same color + for y in 0 ..< qrsize - 1 { + for x in 0 ..< qrsize - 1 { + color := get_module_bounded(qrcode, x, y) + if color == get_module_bounded(qrcode, x + 1, y) && + color == get_module_bounded(qrcode, x, y + 1) && + color == get_module_bounded(qrcode, x + 1, y + 1) { + result += PENALTY_N2 + } + } + } + + // Balance of dark and light modules + dark := 0 + for y in 0 ..< qrsize { + for x in 0 ..< qrsize { + if get_module_bounded(qrcode, x, y) { + dark += 1 + } + } + } + total := qrsize * qrsize + k := (abs(dark * 20 - total * 10) + total - 1) / total - 1 + assert(0 <= k && k <= 9, "penalty balance factor out of range") + result += k * PENALTY_N4 + return result +} + +@(private) +finder_penalty_count_patterns :: proc(run_history: ^[7]int, qrsize: int) -> int { + n := run_history[1] + assert(n <= qrsize * 3, "finder penalty run exceeds limit") + core := + n > 0 && run_history[2] == n && run_history[3] == n * 3 && run_history[4] == n && run_history[5] == n + return( + (1 if core && run_history[0] >= n * 4 && run_history[6] >= n else 0) + + (1 if core && run_history[6] >= n * 4 && run_history[0] >= n else 0) \ + ) +} + +@(private) +finder_penalty_terminate_and_count :: proc( + current_run_color: bool, + current_run_length: int, + run_history: ^[7]int, + qrsize: int, +) -> int { + current_run_length := current_run_length + if current_run_color { + finder_penalty_add_history(current_run_length, run_history, qrsize) + current_run_length = 0 + } + current_run_length += qrsize + finder_penalty_add_history(current_run_length, run_history, qrsize) + return finder_penalty_count_patterns(run_history, qrsize) +} + +@(private) +finder_penalty_add_history :: proc(current_run_length: int, run_history: ^[7]int, qrsize: int) { + current_run_length := current_run_length + if run_history[0] == 0 { + current_run_length += qrsize + } + // Shift history right by 1 + for i := 5; i >= 0; i -= 1 { + run_history[i + 1] = run_history[i] + } + run_history[0] = current_run_length +} + +// ------------------------------------------------------------------------------------------------- +// Basic QR Code information +// ------------------------------------------------------------------------------------------------- + +// Returns the minimum buffer size (in bytes) needed for both temp_buffer and qrcode +// to encode the given content at the given ECC level within the given version range. +// +// Returns ok=false when: +// - The content exceeds QR Code capacity for every version in [min_version, max_version] +// at the given ECL. +min_buffer_size :: proc { + min_buffer_size_text, + min_buffer_size_binary, + min_buffer_size_segments, +} + +// Text path: auto-selects numeric/alphanumeric/byte mode the same way encode_text does. +// +// Returns ok=false when: +// - The text exceeds QR Code capacity for every version in the range at the given ECL. +// - The encoded segment data would exceed the QR grid buffer size (theoretical edge case). +@(require_results) +min_buffer_size_text :: proc( + text: string, + ecl: Ecc, + min_version: int = VERSION_MIN, + max_version: int = VERSION_MAX, +) -> ( + size: int, + ok: bool, +) { + text_len := len(text) + if text_len == 0 { + return buffer_len_for_version(min_version), true + } + + mode: Mode = .Byte + if is_numeric(text) { + mode = .Numeric + } else if is_alphanumeric(text) { + mode = .Alphanumeric + } + + bit_len := calc_segment_bit_length(mode, text_len) + if bit_len == LENGTH_OVERFLOW do return 0, false + + for v in min_version ..= max_version { + total := 4 + num_char_count_bits(mode, v) + bit_len + if total <= get_num_data_codewords(v, ecl) * 8 { + buf_size := buffer_len_for_version(v) + // Segment data is written into temp_buffer, so it must also fit the segment buffer. + seg_buf := calc_segment_buffer_size(mode, text_len) + if seg_buf < 0 || seg_buf > buf_size do return 0, false + return buf_size, true + } + } + return 0, false +} + +// Binary path: always byte mode, only needs the payload length. +// +// Returns ok=false when: +// - The payload cannot fit in any version within [min_version, max_version] at the given ECL. +@(require_results) +min_buffer_size_binary :: proc( + data_len: int, + ecl: Ecc, + min_version: int = VERSION_MIN, + max_version: int = VERSION_MAX, +) -> ( + size: int, + ok: bool, +) { + if data_len == 0 { + return buffer_len_for_version(min_version), true + } + + bit_len := calc_segment_bit_length(.Byte, data_len) + if bit_len == LENGTH_OVERFLOW do return 0, false + + for v in min_version ..= max_version { + total := 4 + num_char_count_bits(.Byte, v) + bit_len + if total <= get_num_data_codewords(v, ecl) * 8 { + return buffer_len_for_version(v), true + } + } + return 0, false +} + +// Segments path: sums per-segment bit costs across candidate versions. +// +// Returns ok=false when: +// - The combined segment data exceeds QR Code capacity for every version in the range +// at the given ECL. +@(require_results) +min_buffer_size_segments :: proc( + segs: []Segment, + ecl: Ecc, + min_version: int = VERSION_MIN, + max_version: int = VERSION_MAX, +) -> ( + size: int, + ok: bool, +) { + if len(segs) == 0 { + return buffer_len_for_version(min_version), true + } + + for v in min_version ..= max_version { + data_used_bits := get_total_bits(segs, v) + if data_used_bits != LENGTH_OVERFLOW && data_used_bits <= get_num_data_codewords(v, ecl) * 8 { + return buffer_len_for_version(v), true + } + } + return 0, false +} + +// Returns the side length of the given QR Code (range [21, 177]). +get_size :: #force_inline proc(qrcode: []u8) -> int { + return int(qrcode[0]) +} + +// Returns the color of the module at the given coordinates (false=light, true=dark). +// Out-of-bounds coordinates return false. +get_module :: proc(qrcode: []u8, x, y: int) -> bool { + qrsize := int(qrcode[0]) + return (0 <= x && x < qrsize && 0 <= y && y < qrsize) && get_module_bounded(qrcode, x, y) +} + +@(private) +get_module_bounded :: #force_inline proc(qrcode: []u8, x, y: int) -> bool { + qrsize := int(qrcode[0]) + index := y * qrsize + x + return get_bit(int(qrcode[(index >> 3) + 1]), uint(index & 7)) +} + +@(private) +set_module_bounded :: #force_inline proc(qrcode: []u8, x, y: int, is_dark: bool) { + qrsize := int(qrcode[0]) + index := y * qrsize + x + bit_index := uint(index & 7) + byte_index := (index >> 3) + 1 + if is_dark { + qrcode[byte_index] |= u8(1) << bit_index + } else { + qrcode[byte_index] &= ~(u8(1) << bit_index) + } +} + +@(private) +set_module_unbounded :: #force_inline proc(qrcode: []u8, x, y: int, is_dark: bool) { + qrsize := int(qrcode[0]) + if 0 <= x && x < qrsize && 0 <= y && y < qrsize { + set_module_bounded(qrcode, x, y, is_dark) + } +} + +@(private) +get_bit :: #force_inline proc(x: int, i: uint) -> bool { + return ((x >> i) & 1) != 0 +} + +// ------------------------------------------------------------------------------------------------- +// Segment handling +// ------------------------------------------------------------------------------------------------- + +// Tests whether the given string can be encoded in numeric mode. +is_numeric :: proc(text: string) -> bool { + for i in 0 ..< len(text) { + if text[i] < '0' || text[i] > '9' { + return false + } + } + return true +} + +// Tests whether the given string can be encoded in alphanumeric mode. +// Valid characters: 0-9, A-Z (uppercase), space, $ % * + - . / : +is_alphanumeric :: proc(text: string) -> bool { + for i in 0 ..< len(text) { + if alphanumeric_index(text[i]) < 0 do return false + } + return true +} + +// Returns the number of bytes needed for the data buffer of a segment. +// Returns -1 when num_chars exceeds the representable range for the given mode +// (i.e. the resulting bit length would exceed 32767). +calc_segment_buffer_size :: proc(mode: Mode, num_chars: int) -> int { + temp := calc_segment_bit_length(mode, num_chars) + if temp == LENGTH_OVERFLOW { + return -1 + } + assert(0 <= temp && temp <= 32767, "segment bit length out of range") + return (temp + 7) / 8 +} + +@(private) +calc_segment_bit_length :: proc(mode: Mode, num_chars: int) -> int { + if num_chars < 0 || num_chars > 32767 { + return LENGTH_OVERFLOW + } + result := num_chars + switch mode { + case .Numeric: result = (result * 10 + 2) / 3 + case .Alphanumeric: result = (result * 11 + 1) / 2 + case .Byte: result *= 8 + case .Kanji: result *= 13 + case .Eci: + assert(num_chars == 0, "ECI mode requires num_chars == 0") + result = 3 * 8 + } + assert(result >= 0, "negative bit length") + if result > 32767 { + return LENGTH_OVERFLOW + } + return result +} + +// Returns a segment representing the given binary data encoded in byte mode. +// +// Parameters: +// data - [in] Source bytes to encode. May be nil/empty. +// buf - [out] Buffer to receive the copied data. Must have length >= len(data). +// The segment's data pointer will reference this buffer. +make_bytes :: proc(data: []u8, buf: []u8) -> Segment { + result: Segment + result.mode = .Byte + result.bit_length = calc_segment_bit_length(.Byte, len(data)) + assert(result.bit_length != LENGTH_OVERFLOW, "segment data too long") + result.num_chars = len(data) + if len(data) > 0 { + copy(buf, data) + } + result.data = buf[:len(data)] + return result +} + +// Returns a segment representing the given decimal digits encoded in numeric mode. +// Every character in digits must be '0'-'9'. +// +// Parameters: +// digits - [in] String of decimal digit characters. +// buf - [out] Buffer to receive the packed bit data. Must have length >= +// calc_segment_buffer_size(.Numeric, len(digits)). The segment's data +// pointer will reference this buffer. +make_numeric :: proc(digits: string, buf: []u8) -> Segment { + result: Segment + digit_len := len(digits) + result.mode = .Numeric + bit_len := calc_segment_bit_length(.Numeric, digit_len) + assert(bit_len != LENGTH_OVERFLOW, "segment data too long") + result.num_chars = digit_len + bytes_needed := (bit_len + 7) / 8 + if bit_len > 0 { + slice.zero(buf[:bytes_needed]) + } + result.bit_length = 0 + + accum_data: uint = 0 + accum_count := 0 + for i in 0 ..< digit_len { + c := digits[i] + assert('0' <= c && c <= '9', "non-digit character in numeric segment") + accum_data = accum_data * 10 + uint(c - '0') + accum_count += 1 + if accum_count == 3 { + append_bits_to_buffer(accum_data, 10, buf, &result.bit_length) + accum_data = 0 + accum_count = 0 + } + } + if accum_count > 0 { + append_bits_to_buffer(accum_data, accum_count * 3 + 1, buf, &result.bit_length) + } + assert(result.bit_length == bit_len, "encoded bit length mismatch") + result.data = buf[:bytes_needed] + return result +} + +// Returns a segment representing the given text encoded in alphanumeric mode. +// Every character must be in: 0-9 A-Z (uppercase) space $ % * + - . / : +// +// Parameters: +// text - [in] String of alphanumeric characters. +// buf - [out] Buffer to receive the packed bit data. Must have length >= +// calc_segment_buffer_size(.Alphanumeric, len(text)). The segment's data +// pointer will reference this buffer. +make_alphanumeric :: proc(text: string, buf: []u8) -> Segment { + result: Segment + text_len := len(text) + result.mode = .Alphanumeric + bit_len := calc_segment_bit_length(.Alphanumeric, text_len) + assert(bit_len != LENGTH_OVERFLOW, "segment data too long") + result.num_chars = text_len + bytes_needed := (bit_len + 7) / 8 + if bit_len > 0 { + slice.zero(buf[:bytes_needed]) + } + result.bit_length = 0 + + accum_data: uint = 0 + accum_count := 0 + for i in 0 ..< text_len { + idx := alphanumeric_index(text[i]) + assert(idx >= 0, "character not in alphanumeric charset") + accum_data = accum_data * 45 + uint(idx) + accum_count += 1 + if accum_count == 2 { + append_bits_to_buffer(accum_data, 11, buf, &result.bit_length) + accum_data = 0 + accum_count = 0 + } + } + if accum_count > 0 { + append_bits_to_buffer(accum_data, 6, buf, &result.bit_length) + } + assert(result.bit_length == bit_len, "encoded bit length mismatch") + result.data = buf[:bytes_needed] + return result +} + +// Returns a segment representing an Extended Channel Interpretation (ECI) designator +// with the given assignment value (must be in [0, 999999]). +// +// Parameters: +// buf - [out] Buffer to receive the packed ECI data. Must have length >= 3. +// The segment's data pointer will reference this buffer. +make_eci :: proc(assign_val: int, buf: []u8) -> Segment { + result: Segment + result.mode = .Eci + result.num_chars = 0 + result.bit_length = 0 + if assign_val < 0 { + assert(false, "ECI assignment value must be non-negative") + } else if assign_val < (1 << 7) { + slice.zero(buf[:1]) + append_bits_to_buffer(uint(assign_val), 8, buf, &result.bit_length) + } else if assign_val < (1 << 14) { + slice.zero(buf[:2]) + append_bits_to_buffer(2, 2, buf, &result.bit_length) + append_bits_to_buffer(uint(assign_val), 14, buf, &result.bit_length) + } else if assign_val < 1000000 { + slice.zero(buf[:3]) + append_bits_to_buffer(6, 3, buf, &result.bit_length) + append_bits_to_buffer(uint(assign_val >> 10), 11, buf, &result.bit_length) + append_bits_to_buffer(uint(assign_val & 0x3FF), 10, buf, &result.bit_length) + } else { + assert(false, "ECI assignment value must be < 1000000") + } + result.data = buf[:(result.bit_length + 7) / 8] + return result +} + +// ------------------------------------------------------------------------------------------------- +// Private helpers +// ------------------------------------------------------------------------------------------------- + +@(private) +append_bits_to_buffer :: proc(val: uint, num_bits: int, buffer: []u8, bit_len: ^int) { + assert(0 <= num_bits && num_bits <= 16 && val >> uint(num_bits) == 0, "invalid bit count or value overflow") + for i := num_bits - 1; i >= 0; i -= 1 { + buffer[bit_len^ >> 3] |= u8(((val >> uint(i)) & 1)) << uint(7 - (bit_len^ & 7)) + bit_len^ += 1 + } +} + +@(private) +get_total_bits :: proc(segs: []Segment, version: int) -> int { + result := 0 + for &seg in segs { + num_chars := seg.num_chars + bit_length := seg.bit_length + assert(0 <= num_chars && num_chars <= 32767, "segment num_chars out of range") + assert(0 <= bit_length && bit_length <= 32767, "segment bit_length out of range") + ccbits := num_char_count_bits(seg.mode, version) + assert(0 <= ccbits && ccbits <= 16, "char count bit width out of range") + if num_chars >= (1 << uint(ccbits)) { + return LENGTH_OVERFLOW + } + result += 4 + ccbits + bit_length + if result > 32767 { + return LENGTH_OVERFLOW + } + } + assert(0 <= result && result <= 32767, "total bits out of range") + return result +} + +@(private) +num_char_count_bits :: proc(mode: Mode, version: int) -> int { + assert(VERSION_MIN <= version && version <= VERSION_MAX, "version out of bounds") + i := (version + 7) / 17 + switch mode { + case .Numeric: + t := [3]int{10, 12, 14} + return t[i] + case .Alphanumeric: + t := [3]int{9, 11, 13} + return t[i] + case .Byte: + t := [3]int{8, 16, 16} + return t[i] + case .Kanji: + t := [3]int{8, 10, 12} + return t[i] + case .Eci: return 0 + } + unreachable() +} + +// Returns the index of c in the alphanumeric charset (0-44), or -1 if not found. +@(private) +alphanumeric_index :: proc(c: u8) -> int { + switch c { + case '0' ..= '9': return int(c - '0') + case 'A' ..= 'Z': return int(c - 'A') + 10 + case ' ': return 36 + case '$': return 37 + case '%': return 38 + case '*': return 39 + case '+': return 40 + case '-': return 41 + case '.': return 42 + case '/': return 43 + case ':': return 44 + } + return -1 +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Tests ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +import "core:math/rand" +import "core:testing" + +// Helper: reference implementation of add_ecc_and_interleave for testing. +// This uses a straightforward block-by-block approach with dynamic allocation, +// then interleaves column-by-column. The result is written to the provided buffer. +add_ecc_and_interleave_reference :: proc(data: []u8, version: int, ecl: Ecc, result: []u8) { + num_blocks := int(NUM_ERROR_CORRECTION_BLOCKS[int(ecl)][version]) + block_ecc_len := int(ECC_CODEWORDS_PER_BLOCK[int(ecl)][version]) + raw_codewords := get_num_raw_data_modules(version) / 8 + num_short_blocks := num_blocks - raw_codewords % num_blocks + short_block_data_len := raw_codewords / num_blocks - block_ecc_len + + gen: [REED_SOLOMON_DEGREE_MAX]u8 + reed_solomon_compute_divisor(block_ecc_len, gen[:block_ecc_len]) + + blocks := make([][]u8, num_blocks) + defer { + for &b in blocks { + delete(b) + } + delete(blocks) + } + + k := 0 + for i in 0 ..< num_blocks { + dat_len := short_block_data_len + (0 if i < num_short_blocks else 1) + block_len := dat_len + block_ecc_len + blocks[i] = make([]u8, block_len) + copy(blocks[i][:dat_len], data[k:k + dat_len]) + reed_solomon_compute_remainder( + data[k:k + dat_len], + gen[:block_ecc_len], + blocks[i][dat_len:dat_len + block_ecc_len], + ) + k += dat_len + } + + k = 0 + for i in 0 ..= short_block_data_len { + for j in 0 ..< num_blocks { + if i == short_block_data_len && j < num_short_blocks { + continue + } + result[k] = blocks[j][i] + k += 1 + } + } + for i in 0 ..< block_ecc_len { + for j in 0 ..< num_blocks { + dat_len := short_block_data_len + (0 if j < num_short_blocks else 1) + result[k] = blocks[j][dat_len + i] + k += 1 + } + } +} + +@(test) +test_append_bits_to_buffer :: proc(t: ^testing.T) { + // Sub-test 1: single byte buffer + { + buf: [1]u8 + bit_len := 0 + append_bits_to_buffer(0, 0, buf[:], &bit_len) + testing.expect_value(t, bit_len, 0) + testing.expect_value(t, buf[0], u8(0x00)) + append_bits_to_buffer(1, 1, buf[:], &bit_len) + testing.expect_value(t, bit_len, 1) + testing.expect_value(t, buf[0], u8(0x80)) + append_bits_to_buffer(0, 1, buf[:], &bit_len) + testing.expect_value(t, bit_len, 2) + testing.expect_value(t, buf[0], u8(0x80)) + append_bits_to_buffer(5, 3, buf[:], &bit_len) + testing.expect_value(t, bit_len, 5) + testing.expect_value(t, buf[0], u8(0xA8)) + append_bits_to_buffer(6, 3, buf[:], &bit_len) + testing.expect_value(t, bit_len, 8) + testing.expect_value(t, buf[0], u8(0xAE)) + } + // Sub-test 2: multi-byte buffer + { + buf: [6]u8 + bit_len := 0 + append_bits_to_buffer(16942, 16, buf[:], &bit_len) + testing.expect_value(t, bit_len, 16) + testing.expect_value(t, buf[0], u8(0x42)) + testing.expect_value(t, buf[1], u8(0x2E)) + testing.expect_value(t, buf[2], u8(0x00)) + testing.expect_value(t, buf[3], u8(0x00)) + testing.expect_value(t, buf[4], u8(0x00)) + testing.expect_value(t, buf[5], u8(0x00)) + append_bits_to_buffer(10, 7, buf[:], &bit_len) + testing.expect_value(t, bit_len, 23) + testing.expect_value(t, buf[0], u8(0x42)) + testing.expect_value(t, buf[1], u8(0x2E)) + testing.expect_value(t, buf[2], u8(0x14)) + testing.expect_value(t, buf[3], u8(0x00)) + testing.expect_value(t, buf[4], u8(0x00)) + testing.expect_value(t, buf[5], u8(0x00)) + append_bits_to_buffer(15, 4, buf[:], &bit_len) + testing.expect_value(t, bit_len, 27) + testing.expect_value(t, buf[0], u8(0x42)) + testing.expect_value(t, buf[1], u8(0x2E)) + testing.expect_value(t, buf[2], u8(0x15)) + testing.expect_value(t, buf[3], u8(0xE0)) + testing.expect_value(t, buf[4], u8(0x00)) + testing.expect_value(t, buf[5], u8(0x00)) + append_bits_to_buffer(26664, 15, buf[:], &bit_len) + testing.expect_value(t, bit_len, 42) + testing.expect_value(t, buf[0], u8(0x42)) + testing.expect_value(t, buf[1], u8(0x2E)) + testing.expect_value(t, buf[2], u8(0x15)) + testing.expect_value(t, buf[3], u8(0xFA)) + testing.expect_value(t, buf[4], u8(0x0A)) + testing.expect_value(t, buf[5], u8(0x00)) + } +} + +@(test) +test_add_ecc_and_interleave :: proc(t: ^testing.T) { + for ver in VERSION_MIN ..= VERSION_MAX { + for ecl_int in 0 ..< 4 { + ecl := Ecc(ecl_int) + data_len := get_num_data_codewords(ver, ecl) + raw_codewords := get_num_raw_data_modules(ver) / 8 + + data: [BUFFER_LEN_MAX]u8 + data_copy: [BUFFER_LEN_MAX]u8 + result: [BUFFER_LEN_MAX]u8 + expected: [BUFFER_LEN_MAX]u8 + + for i in 0 ..< data_len { + data[i] = u8(rand.uint32()) + } + copy(data_copy[:data_len], data[:data_len]) + + add_ecc_and_interleave_reference(data_copy[:data_len], ver, ecl, expected[:]) + add_ecc_and_interleave(data[:], ver, ecl, result[:]) + + for i in 0 ..< raw_codewords { + testing.expect_value(t, result[i], expected[i]) + } + } + } +} + +@(test) +test_get_num_data_codewords :: proc(t: ^testing.T) { + //odinfmt: disable + vers := [?]int{3, 3, 3, 6, 7, 9, 9, 12, 15, 16, 19, 21, 22, 22, 22, 24, 24, 28, 30, 32, 33, 33, 35, 35, 35, 36, 37, 39, 40} + ecls := [?]int{1, 2, 3, 0, 0, 0, 1, 3, 0, 2, 3, 0, 0, 1, 3, 0, 3, 0, 3, 3, 0, 3, 0, 1, 2, 3, 3, 1, 1} + exps := [?]int{44, 34, 26, 136, 156, 232, 182, 158, 523, 325, 341, 932, 1006, 782, 442, 1174, 514, 1531, 745, 845, 2071, 901, 2306, 1812, 1286, 1054, 1096, 2216, 2334} + //odinfmt: enable + + for i in 0 ..< len(vers) { + testing.expect_value(t, get_num_data_codewords(vers[i], Ecc(ecls[i])), exps[i]) + } +} + +@(test) +test_get_num_raw_data_modules :: proc(t: ^testing.T) { + //odinfmt: disable + vers := [?]int{1, 2, 3, 6, 7, 12, 15, 18, 22, 26, 32, 37, 40} + exps := [?]int{208, 359, 567, 1383, 1568, 3728, 5243, 7211, 10068, 13652, 19723, 25568, 29648} + //odinfmt: enable + + for i in 0 ..< len(vers) { + testing.expect_value(t, get_num_raw_data_modules(vers[i]), exps[i]) + } +} + +@(test) +test_reed_solomon_compute_divisor :: proc(t: ^testing.T) { + // Degree 1 + { + gen: [1]u8 + reed_solomon_compute_divisor(1, gen[:]) + testing.expect_value(t, gen[0], u8(0x01)) + } + // Degree 2 + { + gen: [2]u8 + reed_solomon_compute_divisor(2, gen[:]) + testing.expect_value(t, gen[0], u8(0x03)) + testing.expect_value(t, gen[1], u8(0x02)) + } + // Degree 5 + { + gen: [5]u8 + reed_solomon_compute_divisor(5, gen[:]) + testing.expect_value(t, gen[0], u8(0x1F)) + testing.expect_value(t, gen[1], u8(0xC6)) + testing.expect_value(t, gen[2], u8(0x3F)) + testing.expect_value(t, gen[3], u8(0x93)) + testing.expect_value(t, gen[4], u8(0x74)) + } + // Degree 30 + { + gen: [REED_SOLOMON_DEGREE_MAX]u8 + reed_solomon_compute_divisor(30, gen[:30]) + testing.expect_value(t, gen[0], u8(0xD4)) + testing.expect_value(t, gen[1], u8(0xF6)) + testing.expect_value(t, gen[5], u8(0xC0)) + testing.expect_value(t, gen[12], u8(0x16)) + testing.expect_value(t, gen[13], u8(0xD9)) + testing.expect_value(t, gen[20], u8(0x12)) + testing.expect_value(t, gen[27], u8(0x6A)) + testing.expect_value(t, gen[29], u8(0x96)) + } +} + +@(test) +test_reed_solomon_compute_remainder :: proc(t: ^testing.T) { + // Sub-test 1: empty data, degree 3 → all zeros + { + gen: [3]u8 + reed_solomon_compute_divisor(3, gen[:]) + rem: [3]u8 + empty: [0]u8 + reed_solomon_compute_remainder(empty[:], gen[:], rem[:]) + testing.expect_value(t, rem[0], u8(0x00)) + testing.expect_value(t, rem[1], u8(0x00)) + testing.expect_value(t, rem[2], u8(0x00)) + } + // Sub-test 2: data={0,1}, degree 4 → remainder == generator + { + gen: [4]u8 + reed_solomon_compute_divisor(4, gen[:]) + data := [?]u8{0x00, 0x01} + rem: [4]u8 + reed_solomon_compute_remainder(data[:], gen[:], rem[:]) + testing.expect_value(t, rem[0], gen[0]) + testing.expect_value(t, rem[1], gen[1]) + testing.expect_value(t, rem[2], gen[2]) + testing.expect_value(t, rem[3], gen[3]) + } + // Sub-test 3: 5-byte data, degree 5 + { + gen: [5]u8 + reed_solomon_compute_divisor(5, gen[:]) + data := [?]u8{0x03, 0x3A, 0x60, 0x12, 0xC7} + rem: [5]u8 + reed_solomon_compute_remainder(data[:], gen[:], rem[:]) + testing.expect_value(t, rem[0], u8(0xCB)) + testing.expect_value(t, rem[1], u8(0x36)) + testing.expect_value(t, rem[2], u8(0x16)) + testing.expect_value(t, rem[3], u8(0xFA)) + testing.expect_value(t, rem[4], u8(0x9D)) + } + // Sub-test 4: 43-byte data, degree 30 + { + gen: [REED_SOLOMON_DEGREE_MAX]u8 + reed_solomon_compute_divisor(30, gen[:30]) + //odinfmt: disable + data := [?]u8{ + 0x03, 0x3A, 0x60, 0x12, 0xC7, 0x40, 0x04, 0x00, + 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, + 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, + 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, + 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, + 0xEC, 0x11, 0xEC, + } + //odinfmt: enable + rem: [REED_SOLOMON_DEGREE_MAX]u8 + reed_solomon_compute_remainder(data[:], gen[:30], rem[:30]) + testing.expect_value(t, rem[0], u8(0xFE)) + testing.expect_value(t, rem[1], u8(0x71)) + testing.expect_value(t, rem[2], u8(0xFA)) + testing.expect_value(t, rem[3], u8(0xFE)) + testing.expect_value(t, rem[9], u8(0xB9)) + testing.expect_value(t, rem[10], u8(0xC9)) + testing.expect_value(t, rem[15], u8(0xAC)) + testing.expect_value(t, rem[16], u8(0xFA)) + testing.expect_value(t, rem[19], u8(0x07)) + testing.expect_value(t, rem[20], u8(0x63)) + testing.expect_value(t, rem[25], u8(0xB5)) + testing.expect_value(t, rem[29], u8(0xCB)) + } +} + +@(test) +test_reed_solomon_multiply :: proc(t: ^testing.T) { + //odinfmt: disable + cases := [?][3]u8{ + {0x00, 0x00, 0x00}, + {0x01, 0x01, 0x01}, + {0x02, 0x02, 0x04}, + {0x00, 0x6E, 0x00}, + {0xB2, 0xDD, 0xE6}, + {0x41, 0x11, 0x25}, + {0xB0, 0x1F, 0x11}, + {0x05, 0x75, 0xBC}, + {0x52, 0xB5, 0xAE}, + {0xA8, 0x20, 0xA4}, + {0x0E, 0x44, 0x9F}, + {0xD4, 0x13, 0xA0}, + {0x31, 0x10, 0x37}, + {0x6C, 0x58, 0xCB}, + {0xB6, 0x75, 0x3E}, + {0xFF, 0xFF, 0xE2}, + } + //odinfmt: enable + for tc in cases { + testing.expect_value(t, reed_solomon_multiply(tc[0], tc[1]), tc[2]) + } +} + +@(test) +test_initialize_function_modules :: proc(t: ^testing.T) { + for ver in VERSION_MIN ..= VERSION_MAX { + qrcode: [BUFFER_LEN_MAX]u8 + initialize_function_modules(ver, qrcode[:]) + + size := ver * 4 + 17 + testing.expect_value(t, get_size(qrcode[:]), size) + + has_light := false + has_dark := false + for y in 0 ..< size { + for x in 0 ..< size { + if get_module(qrcode[:], x, y) { + has_dark = true + } else { + has_light = true + } + } + } + testing.expect(t, has_light) + testing.expect(t, has_dark) + } +} + +@(test) +test_get_alignment_pattern_positions :: proc(t: ^testing.T) { + // {version, num, positions...} + { + result: [7]u8 + num := get_alignment_pattern_positions(1, &result) + testing.expect_value(t, num, 0) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(2, &result) + testing.expect_value(t, num, 2) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(18)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(3, &result) + testing.expect_value(t, num, 2) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(22)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(6, &result) + testing.expect_value(t, num, 2) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(34)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(7, &result) + testing.expect_value(t, num, 3) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(22)) + testing.expect_value(t, result[2], u8(38)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(8, &result) + testing.expect_value(t, num, 3) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(24)) + testing.expect_value(t, result[2], u8(42)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(16, &result) + testing.expect_value(t, num, 4) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(26)) + testing.expect_value(t, result[2], u8(50)) + testing.expect_value(t, result[3], u8(74)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(25, &result) + testing.expect_value(t, num, 5) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(32)) + testing.expect_value(t, result[2], u8(58)) + testing.expect_value(t, result[3], u8(84)) + testing.expect_value(t, result[4], u8(110)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(32, &result) + testing.expect_value(t, num, 6) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(34)) + testing.expect_value(t, result[2], u8(60)) + testing.expect_value(t, result[3], u8(86)) + testing.expect_value(t, result[4], u8(112)) + testing.expect_value(t, result[5], u8(138)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(33, &result) + testing.expect_value(t, num, 6) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(30)) + testing.expect_value(t, result[2], u8(58)) + testing.expect_value(t, result[3], u8(86)) + testing.expect_value(t, result[4], u8(114)) + testing.expect_value(t, result[5], u8(142)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(39, &result) + testing.expect_value(t, num, 7) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(26)) + testing.expect_value(t, result[2], u8(54)) + testing.expect_value(t, result[3], u8(82)) + testing.expect_value(t, result[4], u8(110)) + testing.expect_value(t, result[5], u8(138)) + testing.expect_value(t, result[6], u8(166)) + } + { + result: [7]u8 + num := get_alignment_pattern_positions(40, &result) + testing.expect_value(t, num, 7) + testing.expect_value(t, result[0], u8(6)) + testing.expect_value(t, result[1], u8(30)) + testing.expect_value(t, result[2], u8(58)) + testing.expect_value(t, result[3], u8(86)) + testing.expect_value(t, result[4], u8(114)) + testing.expect_value(t, result[5], u8(142)) + testing.expect_value(t, result[6], u8(170)) + } +} + +@(test) +test_get_set_module :: proc(t: ^testing.T) { + qrcode: [BUFFER_LEN_MAX]u8 + initialize_function_modules(23, qrcode[:]) + size := 23 * 4 + 17 // 109 + + // Clear all modules to light + for y in 0 ..< size { + for x in 0 ..< size { + set_module_bounded(qrcode[:], x, y, false) + } + } + // Check all are light + for y in 0 ..< size { + for x in 0 ..< size { + testing.expect(t, !get_module(qrcode[:], x, y)) + } + } + + // Set all modules to dark + for y in 0 ..< size { + for x in 0 ..< size { + set_module_bounded(qrcode[:], x, y, true) + } + } + // Check all are dark + for y in 0 ..< size { + for x in 0 ..< size { + testing.expect(t, get_module(qrcode[:], x, y)) + } + } + + // Set out-of-bounds to light (should not change anything) + set_module_unbounded(qrcode[:], -1, -1, false) + set_module_unbounded(qrcode[:], -1, 0, false) + set_module_unbounded(qrcode[:], 0, -1, false) + set_module_unbounded(qrcode[:], size, 5, false) + set_module_unbounded(qrcode[:], 72, size, false) + // Check all still dark + for y in 0 ..< size { + for x in 0 ..< size { + testing.expect(t, get_module(qrcode[:], x, y)) + } + } + + // Set specific modules to light, verify pattern + set_module_bounded(qrcode[:], 3, 8, false) + set_module_bounded(qrcode[:], 61, 49, false) + testing.expect(t, !get_module(qrcode[:], 3, 8)) + testing.expect(t, get_module(qrcode[:], 2, 8)) + testing.expect(t, get_module(qrcode[:], 4, 8)) + testing.expect(t, !get_module(qrcode[:], 61, 49)) + testing.expect(t, get_module(qrcode[:], 60, 49)) + testing.expect(t, get_module(qrcode[:], 62, 49)) +} + +@(test) +test_get_set_module_randomly :: proc(t: ^testing.T) { + qrcode: [BUFFER_LEN_MAX]u8 + initialize_function_modules(1, qrcode[:]) + size :: 21 // version 1: 1*4+17 + + modules: [size][size]bool + for y in 0 ..< size { + for x in 0 ..< size { + modules[y][x] = get_module(qrcode[:], x, y) + } + } + + for _ in 0 ..< 100000 { + x := int(rand.uint32() % u32(size * 2)) - size / 2 + y := int(rand.uint32() % u32(size * 2)) - size / 2 + is_in_bounds := 0 <= x && x < size && 0 <= y && y < size + expected: bool + if is_in_bounds { + expected = modules[y][x] + } + testing.expect(t, get_module(qrcode[:], x, y) == expected) + if is_in_bounds { + new_val := rand.uint32() % 2 == 0 + set_module_bounded(qrcode[:], x, y, new_val) + modules[y][x] = new_val + } + } +} + +@(test) +test_is_alphanumeric :: proc(t: ^testing.T) { + testing.expect(t, is_alphanumeric("")) + testing.expect(t, is_alphanumeric("0")) + testing.expect(t, is_alphanumeric("A")) + testing.expect(t, !is_alphanumeric("a")) + testing.expect(t, is_alphanumeric(" ")) + testing.expect(t, is_alphanumeric(".")) + testing.expect(t, is_alphanumeric("*")) + testing.expect(t, !is_alphanumeric(",")) + testing.expect(t, !is_alphanumeric("|")) + testing.expect(t, !is_alphanumeric("@")) + testing.expect(t, is_alphanumeric("XYZ")) + testing.expect(t, !is_alphanumeric("XYZ!")) + testing.expect(t, is_alphanumeric("79068")) + testing.expect(t, is_alphanumeric("+123 ABC$")) + { + b := [1]u8{0x01} + testing.expect(t, !is_alphanumeric(string(b[:]))) + } + { + b := [1]u8{0x7F} + testing.expect(t, !is_alphanumeric(string(b[:]))) + } + { + b := [1]u8{0x80} + testing.expect(t, !is_alphanumeric(string(b[:]))) + } + { + b := [1]u8{0xC0} + testing.expect(t, !is_alphanumeric(string(b[:]))) + } + { + b := [1]u8{0xFF} + testing.expect(t, !is_alphanumeric(string(b[:]))) + } +} + +@(test) +test_is_numeric :: proc(t: ^testing.T) { + testing.expect(t, is_numeric("")) + testing.expect(t, is_numeric("0")) + testing.expect(t, !is_numeric("A")) + testing.expect(t, !is_numeric("a")) + testing.expect(t, !is_numeric(" ")) + testing.expect(t, !is_numeric(".")) + testing.expect(t, !is_numeric("*")) + testing.expect(t, !is_numeric(",")) + testing.expect(t, !is_numeric("|")) + testing.expect(t, !is_numeric("@")) + testing.expect(t, !is_numeric("XYZ")) + testing.expect(t, !is_numeric("XYZ!")) + testing.expect(t, is_numeric("79068")) + testing.expect(t, !is_numeric("+123 ABC$")) + { + b := [1]u8{0x01} + testing.expect(t, !is_numeric(string(b[:]))) + } + { + b := [1]u8{0x7F} + testing.expect(t, !is_numeric(string(b[:]))) + } + { + b := [1]u8{0x80} + testing.expect(t, !is_numeric(string(b[:]))) + } + { + b := [1]u8{0xC0} + testing.expect(t, !is_numeric(string(b[:]))) + } + { + b := [1]u8{0xFF} + testing.expect(t, !is_numeric(string(b[:]))) + } +} + +@(test) +test_calc_segment_buffer_size :: proc(t: ^testing.T) { + // NUMERIC: {num_chars, expected} + { + cases := [?][2]int { + {0, 0}, + {1, 1}, + {2, 1}, + {3, 2}, + {4, 2}, + {5, 3}, + {6, 3}, + {1472, 614}, + {2097, 874}, + {5326, 2220}, + {9828, 4095}, + {9829, 4096}, + {9830, 4096}, + {9831, -1}, + {9832, -1}, + {12000, -1}, + {28453, -1}, + {55555, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_buffer_size(.Numeric, tc[0]), tc[1]) + } + } + // ALPHANUMERIC + { + cases := [?][2]int { + {0, 0}, + {1, 1}, + {2, 2}, + {3, 3}, + {4, 3}, + {5, 4}, + {6, 5}, + {1472, 1012}, + {2097, 1442}, + {5326, 3662}, + {5955, 4095}, + {5956, 4095}, + {5957, 4096}, + {5958, -1}, + {5959, -1}, + {12000, -1}, + {28453, -1}, + {55555, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_buffer_size(.Alphanumeric, tc[0]), tc[1]) + } + } + // BYTE + { + cases := [?][2]int { + {0, 0}, + {1, 1}, + {2, 2}, + {3, 3}, + {1472, 1472}, + {2097, 2097}, + {4094, 4094}, + {4095, 4095}, + {4096, -1}, + {4097, -1}, + {5957, -1}, + {12000, -1}, + {28453, -1}, + {55555, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_buffer_size(.Byte, tc[0]), tc[1]) + } + } + // KANJI + { + cases := [?][2]int { + {0, 0}, + {1, 2}, + {2, 4}, + {3, 5}, + {1472, 2392}, + {2097, 3408}, + {2519, 4094}, + {2520, 4095}, + {2521, -1}, + {5957, -1}, + {2522, -1}, + {12000, -1}, + {28453, -1}, + {55555, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_buffer_size(.Kanji, tc[0]), tc[1]) + } + } + // ECI + { + testing.expect_value(t, calc_segment_buffer_size(.Eci, 0), 3) + } +} + +@(test) +test_calc_segment_bit_length :: proc(t: ^testing.T) { + // NUMERIC: {num_chars, expected} + { + cases := [?][2]int { + {0, 0}, + {1, 4}, + {2, 7}, + {3, 10}, + {4, 14}, + {5, 17}, + {6, 20}, + {1472, 4907}, + {2097, 6990}, + {5326, 17754}, + {9828, 32760}, + {9829, 32764}, + {9830, 32767}, + {9831, -1}, + {9832, -1}, + {12000, -1}, + {28453, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_bit_length(.Numeric, tc[0]), tc[1]) + } + } + // ALPHANUMERIC + { + cases := [?][2]int { + {0, 0}, + {1, 6}, + {2, 11}, + {3, 17}, + {4, 22}, + {5, 28}, + {6, 33}, + {1472, 8096}, + {2097, 11534}, + {5326, 29293}, + {5955, 32753}, + {5956, 32758}, + {5957, 32764}, + {5958, -1}, + {5959, -1}, + {12000, -1}, + {28453, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_bit_length(.Alphanumeric, tc[0]), tc[1]) + } + } + // BYTE + { + cases := [?][2]int { + {0, 0}, + {1, 8}, + {2, 16}, + {3, 24}, + {1472, 11776}, + {2097, 16776}, + {4094, 32752}, + {4095, 32760}, + {4096, -1}, + {4097, -1}, + {5957, -1}, + {12000, -1}, + {28453, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_bit_length(.Byte, tc[0]), tc[1]) + } + } + // KANJI + { + cases := [?][2]int { + {0, 0}, + {1, 13}, + {2, 26}, + {3, 39}, + {1472, 19136}, + {2097, 27261}, + {2519, 32747}, + {2520, 32760}, + {2521, -1}, + {5957, -1}, + {2522, -1}, + {12000, -1}, + {28453, -1}, + } + for tc in cases { + testing.expect_value(t, calc_segment_bit_length(.Kanji, tc[0]), tc[1]) + } + } + // ECI + testing.expect_value(t, calc_segment_bit_length(.Eci, 0), 24) +} + +@(test) +test_make_bytes :: proc(t: ^testing.T) { + // Empty + { + seg := make_bytes(nil, nil) + testing.expect_value(t, seg.mode, Mode.Byte) + testing.expect_value(t, seg.num_chars, 0) + testing.expect_value(t, seg.bit_length, 0) + testing.expect(t, seg.data == nil) + } + // Single byte {0x00} + { + data := [1]u8{0x00} + buf: [1]u8 + seg := make_bytes(data[:], buf[:]) + testing.expect_value(t, seg.mode, Mode.Byte) + testing.expect_value(t, seg.num_chars, 1) + testing.expect_value(t, seg.bit_length, 8) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0x00)) + } + // Three bytes {0xEF, 0xBB, 0xBF} + { + data := [3]u8{0xEF, 0xBB, 0xBF} + buf: [3]u8 + seg := make_bytes(data[:], buf[:]) + testing.expect_value(t, seg.mode, Mode.Byte) + testing.expect_value(t, seg.num_chars, 3) + testing.expect_value(t, seg.bit_length, 24) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0xEF)) + testing.expect_value(t, seg.data[1], u8(0xBB)) + testing.expect_value(t, seg.data[2], u8(0xBF)) + } +} + +@(test) +test_make_numeric :: proc(t: ^testing.T) { + // Empty + { + seg := make_numeric("", nil) + testing.expect_value(t, seg.mode, Mode.Numeric) + testing.expect_value(t, seg.num_chars, 0) + testing.expect_value(t, seg.bit_length, 0) + } + // "9" + { + buf: [1]u8 + seg := make_numeric("9", buf[:]) + testing.expect_value(t, seg.mode, Mode.Numeric) + testing.expect_value(t, seg.num_chars, 1) + testing.expect_value(t, seg.bit_length, 4) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0x90)) + } + // "81" + { + buf: [1]u8 + seg := make_numeric("81", buf[:]) + testing.expect_value(t, seg.mode, Mode.Numeric) + testing.expect_value(t, seg.num_chars, 2) + testing.expect_value(t, seg.bit_length, 7) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0xA2)) + } + // "673" + { + buf: [2]u8 + seg := make_numeric("673", buf[:]) + testing.expect_value(t, seg.mode, Mode.Numeric) + testing.expect_value(t, seg.num_chars, 3) + testing.expect_value(t, seg.bit_length, 10) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0xA8)) + testing.expect_value(t, seg.data[1], u8(0x40)) + } + // "3141592653" + { + buf: [5]u8 + seg := make_numeric("3141592653", buf[:]) + testing.expect_value(t, seg.mode, Mode.Numeric) + testing.expect_value(t, seg.num_chars, 10) + testing.expect_value(t, seg.bit_length, 34) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0x4E)) + testing.expect_value(t, seg.data[1], u8(0x89)) + testing.expect_value(t, seg.data[2], u8(0xF4)) + testing.expect_value(t, seg.data[3], u8(0x24)) + testing.expect_value(t, seg.data[4], u8(0xC0)) + } +} + +@(test) +test_make_alphanumeric :: proc(t: ^testing.T) { + // Empty + { + seg := make_alphanumeric("", nil) + testing.expect_value(t, seg.mode, Mode.Alphanumeric) + testing.expect_value(t, seg.num_chars, 0) + testing.expect_value(t, seg.bit_length, 0) + } + // "A" + { + buf: [1]u8 + seg := make_alphanumeric("A", buf[:]) + testing.expect_value(t, seg.mode, Mode.Alphanumeric) + testing.expect_value(t, seg.num_chars, 1) + testing.expect_value(t, seg.bit_length, 6) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0x28)) + } + // "%:" + { + buf: [2]u8 + seg := make_alphanumeric("%:", buf[:]) + testing.expect_value(t, seg.mode, Mode.Alphanumeric) + testing.expect_value(t, seg.num_chars, 2) + testing.expect_value(t, seg.bit_length, 11) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0xDB)) + testing.expect_value(t, seg.data[1], u8(0x40)) + } + // "Q R" + { + buf: [3]u8 + seg := make_alphanumeric("Q R", buf[:]) + testing.expect_value(t, seg.mode, Mode.Alphanumeric) + testing.expect_value(t, seg.num_chars, 3) + testing.expect_value(t, seg.bit_length, 17) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0x96)) + testing.expect_value(t, seg.data[1], u8(0xCD)) + testing.expect_value(t, seg.data[2], u8(0x80)) + } +} + +@(test) +test_make_eci :: proc(t: ^testing.T) { + // 127 + { + buf: [3]u8 + seg := make_eci(127, buf[:]) + testing.expect_value(t, seg.mode, Mode.Eci) + testing.expect_value(t, seg.num_chars, 0) + testing.expect_value(t, seg.bit_length, 8) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0x7F)) + } + // 10345 + { + buf: [3]u8 + seg := make_eci(10345, buf[:]) + testing.expect_value(t, seg.mode, Mode.Eci) + testing.expect_value(t, seg.num_chars, 0) + testing.expect_value(t, seg.bit_length, 16) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0xA8)) + testing.expect_value(t, seg.data[1], u8(0x69)) + } + // 999999 + { + buf: [3]u8 + seg := make_eci(999999, buf[:]) + testing.expect_value(t, seg.mode, Mode.Eci) + testing.expect_value(t, seg.num_chars, 0) + testing.expect_value(t, seg.bit_length, 24) + testing.expect(t, seg.data != nil) + testing.expect_value(t, seg.data[0], u8(0xCF)) + testing.expect_value(t, seg.data[1], u8(0x42)) + testing.expect_value(t, seg.data[2], u8(0x3F)) + } +} + +@(test) +test_get_total_bits :: proc(t: ^testing.T) { + // Empty segments + { + empty: []Segment + testing.expect_value(t, get_total_bits(empty, 1), 0) + testing.expect_value(t, get_total_bits(empty, 40), 0) + } + // Single BYTE segment: {.BYTE, 3, nil, 24} + { + segs := [?]Segment{{mode = .Byte, num_chars = 3, data = nil, bit_length = 24}} + testing.expect_value(t, get_total_bits(segs[:], 2), 36) + testing.expect_value(t, get_total_bits(segs[:], 10), 44) + testing.expect_value(t, get_total_bits(segs[:], 39), 44) + } + // Mixed segments: ECI + NUMERIC + ALPHANUMERIC + KANJI + { + segs := [?]Segment { + {mode = .Eci, num_chars = 0, data = nil, bit_length = 8}, + {mode = .Numeric, num_chars = 7, data = nil, bit_length = 24}, + {mode = .Alphanumeric, num_chars = 1, data = nil, bit_length = 6}, + {mode = .Kanji, num_chars = 4, data = nil, bit_length = 52}, + } + testing.expect_value(t, get_total_bits(segs[:], 9), 133) + testing.expect_value(t, get_total_bits(segs[:], 21), 139) + testing.expect_value(t, get_total_bits(segs[:], 27), 145) + } + // Large BYTE segment: {.BYTE, 4093, nil, 32744} + { + segs := [?]Segment{{mode = .Byte, num_chars = 4093, data = nil, bit_length = 32744}} + testing.expect_value(t, get_total_bits(segs[:], 1), -1) + testing.expect_value(t, get_total_bits(segs[:], 10), 32764) + testing.expect_value(t, get_total_bits(segs[:], 27), 32764) + } + // 5 segments: 4x {.NUMERIC,2047,nil,6824} + 1x {.NUMERIC,1617,nil,5390} + { + segs := [?]Segment { + {mode = .Numeric, num_chars = 2047, data = nil, bit_length = 6824}, + {mode = .Numeric, num_chars = 2047, data = nil, bit_length = 6824}, + {mode = .Numeric, num_chars = 2047, data = nil, bit_length = 6824}, + {mode = .Numeric, num_chars = 2047, data = nil, bit_length = 6824}, + {mode = .Numeric, num_chars = 1617, data = nil, bit_length = 5390}, + } + testing.expect_value(t, get_total_bits(segs[:], 1), -1) + testing.expect_value(t, get_total_bits(segs[:], 10), 32766) + testing.expect_value(t, get_total_bits(segs[:], 27), -1) + } + // 10 segments: 9x {.KANJI,255,nil,3315} + 1x {.ALPHANUMERIC,511,nil,2811} + { + segs := [?]Segment { + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Kanji, num_chars = 255, data = nil, bit_length = 3315}, + {mode = .Alphanumeric, num_chars = 511, data = nil, bit_length = 2811}, + } + testing.expect_value(t, get_total_bits(segs[:], 9), 32767) + testing.expect_value(t, get_total_bits(segs[:], 26), -1) + testing.expect_value(t, get_total_bits(segs[:], 40), -1) + } +} + +@(test) +test_min_buffer_size_text :: proc(t: ^testing.T) { + // Empty string fits in version 1 at any ECL + { + size, ok := min_buffer_size("", .Low) + testing.expect(t, ok) + testing.expect_value(t, size, buffer_len_for_version(VERSION_MIN)) + } + { + size, ok := min_buffer_size("", .High) + testing.expect(t, ok) + testing.expect_value(t, size, buffer_len_for_version(VERSION_MIN)) + } + + // "Hello, world!" is short byte-mode text, should fit in a small version + size, size_ok := min_buffer_size("Hello, world!", .Low) + testing.expect(t, size_ok) + testing.expect(t, size > 0) + testing.expect(t, size <= buffer_len_for_version(3)) + + // Pure numeric string gets numeric mode — more compact than byte + num_size, num_ok := min_buffer_size("314159265358979323846264338327950288419716939937510", .Medium) + byte_size, byte_ok := min_buffer_size_binary(51, .Medium) // same length forced to byte mode + testing.expect(t, num_ok) + testing.expect(t, byte_ok) + testing.expect(t, num_size > 0) + testing.expect(t, num_size <= byte_size) // numeric mode should be at least as compact + + // Alphanumeric string + alnum_size, alnum_ok := min_buffer_size("HELLO WORLD", .Low) + testing.expect(t, alnum_ok) + testing.expect(t, alnum_size > 0) + + // Verify it agrees with what encode_text actually picks + { + text :: "Hello, world!" + planned, planned_ok := min_buffer_size(text, .Low) + testing.expect(t, planned_ok) + testing.expect(t, planned > 0) + qrcode: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok := encode_text(text, temp[:], qrcode[:], Ecc.Low) + testing.expect(t, ok) + actual_version_size := get_size(qrcode[:]) + actual_buf_len := buffer_len_for_version((actual_version_size - 17) / 4) + testing.expect_value(t, planned, actual_buf_len) + } + + // Constrained max_version: short text should fit in v1 + { + hi_size, ok := min_buffer_size("Hi", .Low, max_version = 1) + testing.expect(t, ok) + testing.expect(t, hi_size > 0) + } + + // Constrained max_version: long text should fail in v1 + long_text := "This string is definitely too long to fit in a version 1 QR code at high ECC" + { + _, ok := min_buffer_size(long_text, .High, max_version = 1) + testing.expect(t, !ok) + } + + // min_version raises the floor + { + hi_size, ok := min_buffer_size("Hi", .Low, min_version = 5) + testing.expect(t, ok) + testing.expect_value(t, hi_size, buffer_len_for_version(5)) + } + + // Higher ECL requires more capacity → may need a larger version + low_size, low_ok := min_buffer_size("Hello, world!", .Low) + high_size, high_ok := min_buffer_size("Hello, world!", .High) + testing.expect(t, low_ok) + testing.expect(t, high_ok) + testing.expect(t, high_size >= low_size) +} + +@(test) +test_min_buffer_size_binary :: proc(t: ^testing.T) { + // Empty payload + { + size, ok := min_buffer_size(0, .Low) + testing.expect(t, ok) + testing.expect_value(t, size, buffer_len_for_version(VERSION_MIN)) + } + + // Small payload + size, size_ok := min_buffer_size(10, .Low) + testing.expect(t, size_ok) + testing.expect(t, size > 0) + testing.expect(t, size <= buffer_len_for_version(2)) + + // Verify agreement with encode_binary + { + data_len :: 100 + planned, planned_ok := min_buffer_size(data_len, .Medium) + testing.expect(t, planned_ok) + testing.expect(t, planned > 0) + dat: [BUFFER_LEN_MAX]u8 + qrcode: [BUFFER_LEN_MAX]u8 + for i in 0 ..< data_len { + dat[i] = u8(i) + } + ok := encode_binary(dat[:], data_len, qrcode[:], .Medium) + testing.expect(t, ok) + actual_version_size := get_size(qrcode[:]) + actual_buf_len := buffer_len_for_version((actual_version_size - 17) / 4) + testing.expect_value(t, planned, actual_buf_len) + } + + // Max capacity at LOW: 2953 bytes should fit, something huge should not + { + size, ok := min_buffer_size(2953, .Low) + testing.expect(t, ok) + testing.expect(t, size > 0) + } + { + _, ok := min_buffer_size(3000, .Low) + testing.expect(t, !ok) + } + + // Constrained version + { + _, ok := min_buffer_size(500, .Low, max_version = 5) + testing.expect(t, !ok) + } +} + +@(test) +test_min_buffer_size_segments :: proc(t: ^testing.T) { + // Empty segments + { + size, ok := min_buffer_size([]Segment{}, .Low) + testing.expect(t, ok) + testing.expect_value(t, size, buffer_len_for_version(VERSION_MIN)) + } + + // Single numeric segment + { + buf: [32]u8 + seg := make_numeric("12345", buf[:]) + segs := [1]Segment{seg} + size, size_ok := min_buffer_size(segs[:], .Low) + testing.expect(t, size_ok) + testing.expect(t, size > 0) + testing.expect(t, size <= buffer_len_for_version(1)) + } + + // Mixed segments (alphanumeric + numeric) — the silver demo + { + buf0: [BUFFER_LEN_MAX]u8 + buf1: [BUFFER_LEN_MAX]u8 + segs := [2]Segment { + make_alphanumeric("THE SQUARE ROOT OF 2 IS 1.", buf0[:]), + make_numeric("41421356237309504880168872420969807856967187537694807317667973799", buf1[:]), + } + planned, planned_ok := min_buffer_size(segs[:], .Low) + testing.expect(t, planned_ok) + testing.expect(t, planned > 0) + + // Verify against actual encode + qrcode: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok := encode_segments(segs[:], Ecc.Low, temp[:], qrcode[:]) + testing.expect(t, ok) + actual_version_size := get_size(qrcode[:]) + actual_buf_len := buffer_len_for_version((actual_version_size - 17) / 4) + testing.expect_value(t, planned, actual_buf_len) + } + + // Overflow case: segments that exceed version 40 + { + segs := [1]Segment{{mode = .Byte, num_chars = 3000, data = nil, bit_length = 24000}} + _, ok := min_buffer_size(segs[:], .Low) + testing.expect(t, !ok) + } +} + +@(test) +test_encode_text_auto :: proc(t: ^testing.T) { + // Basic: auto variant produces identical output to explicit_temp + { + text :: "Hello, world!" + qr_explicit: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok_explicit := encode_text_explicit_temp(text, temp[:], qr_explicit[:], .Low) + testing.expect(t, ok_explicit) + + qr_auto: [BUFFER_LEN_MAX]u8 + ok_auto := encode_text_auto(text, qr_auto[:], .Low) + testing.expect(t, ok_auto) + + size := get_size(qr_explicit[:]) + buf_len := buffer_len_for_version((size - 17) / 4) + for i in 0 ..< buf_len { + testing.expect_value(t, qr_auto[i], qr_explicit[i]) + } + } + + // Numeric mode + { + text :: "314159265358979323846264338327950288419716939937510" + qr_explicit: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok_explicit := encode_text_explicit_temp(text, temp[:], qr_explicit[:], .Medium) + testing.expect(t, ok_explicit) + + qr_auto: [BUFFER_LEN_MAX]u8 + ok_auto := encode_text_auto(text, qr_auto[:], .Medium) + testing.expect(t, ok_auto) + + size := get_size(qr_explicit[:]) + buf_len := buffer_len_for_version((size - 17) / 4) + for i in 0 ..< buf_len { + testing.expect_value(t, qr_auto[i], qr_explicit[i]) + } + } + + // Alphanumeric mode + { + text :: "HELLO WORLD" + qr_explicit: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok_explicit := encode_text_explicit_temp(text, temp[:], qr_explicit[:], .Quartile) + testing.expect(t, ok_explicit) + + qr_auto: [BUFFER_LEN_MAX]u8 + ok_auto := encode_text_auto(text, qr_auto[:], .Quartile) + testing.expect(t, ok_auto) + + size := get_size(qr_explicit[:]) + buf_len := buffer_len_for_version((size - 17) / 4) + for i in 0 ..< buf_len { + testing.expect_value(t, qr_auto[i], qr_explicit[i]) + } + } + + // Empty string + { + qr_auto: [BUFFER_LEN_MAX]u8 + ok := encode_text_auto("", qr_auto[:], .Low) + testing.expect(t, ok) + } + + // With specific mask + { + text :: "https://www.nayuki.io/" + qr_explicit: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok_explicit := encode_text_explicit_temp(text, temp[:], qr_explicit[:], .High, mask = .M3) + testing.expect(t, ok_explicit) + + qr_auto: [BUFFER_LEN_MAX]u8 + ok_auto := encode_text_auto(text, qr_auto[:], .High, mask = .M3) + testing.expect(t, ok_auto) + + size := get_size(qr_explicit[:]) + buf_len := buffer_len_for_version((size - 17) / 4) + for i in 0 ..< buf_len { + testing.expect_value(t, qr_auto[i], qr_explicit[i]) + } + } + + // Oversized content returns false + { + long_text := "This string is definitely too long to fit in a version 1 QR code at high ECC" + qr_auto: [BUFFER_LEN_MAX]u8 + ok := encode_text_auto(long_text, qr_auto[:], .High, max_version = 1) + testing.expect(t, !ok) + testing.expect_value(t, qr_auto[0], u8(0)) + } +} + +@(test) +test_encode_segments_auto :: proc(t: ^testing.T) { + // Mixed segments (silver demo): auto matches explicit_temp + { + buf0: [BUFFER_LEN_MAX]u8 + buf1: [BUFFER_LEN_MAX]u8 + segs := [2]Segment { + make_alphanumeric("THE SQUARE ROOT OF 2 IS 1.", buf0[:]), + make_numeric("41421356237309504880168872420969807856967187537694807317667973799", buf1[:]), + } + + qr_explicit: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok_explicit := encode_segments_explicit_temp(segs[:], .Low, temp[:], qr_explicit[:]) + testing.expect(t, ok_explicit) + + qr_auto: [BUFFER_LEN_MAX]u8 + ok_auto := encode_segments_auto(segs[:], .Low, qr_auto[:]) + testing.expect(t, ok_auto) + + size := get_size(qr_explicit[:]) + buf_len := buffer_len_for_version((size - 17) / 4) + for i in 0 ..< buf_len { + testing.expect_value(t, qr_auto[i], qr_explicit[i]) + } + } + + // Empty segments + { + qr_auto: [BUFFER_LEN_MAX]u8 + ok := encode_segments_auto(nil, .Low, qr_auto[:]) + testing.expect(t, ok) + } +} + +@(test) +test_encode_segments_advanced_auto :: proc(t: ^testing.T) { + // Single numeric segment with constrained version range + { + buf: [BUFFER_LEN_MAX]u8 + seg := make_numeric("12345678901234567890", buf[:]) + segs := [1]Segment{seg} + + qr_explicit: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok_explicit := encode_segments_advanced_explicit_temp( + segs[:], + .Medium, + VERSION_MIN, + 10, + nil, + true, + temp[:], + qr_explicit[:], + ) + testing.expect(t, ok_explicit) + + qr_auto: [BUFFER_LEN_MAX]u8 + ok_auto := encode_segments_advanced_auto(segs[:], .Medium, VERSION_MIN, 10, nil, true, qr_auto[:]) + testing.expect(t, ok_auto) + + size := get_size(qr_explicit[:]) + buf_len := buffer_len_for_version((size - 17) / 4) + for i in 0 ..< buf_len { + testing.expect_value(t, qr_auto[i], qr_explicit[i]) + } + } + + // With specific mask and boost_ecl = false + { + buf: [BUFFER_LEN_MAX]u8 + seg := make_alphanumeric("TESTING 123", buf[:]) + segs := [1]Segment{seg} + + qr_explicit: [BUFFER_LEN_MAX]u8 + temp: [BUFFER_LEN_MAX]u8 + ok_explicit := encode_segments_advanced_explicit_temp( + segs[:], + .High, + 1, + 40, + Mask.M5, + false, + temp[:], + qr_explicit[:], + ) + testing.expect(t, ok_explicit) + + qr_auto: [BUFFER_LEN_MAX]u8 + ok_auto := encode_segments_advanced_auto(segs[:], .High, 1, 40, Mask.M5, false, qr_auto[:]) + testing.expect(t, ok_auto) + + size := get_size(qr_explicit[:]) + buf_len := buffer_len_for_version((size - 17) / 4) + for i in 0 ..< buf_len { + testing.expect_value(t, qr_auto[i], qr_explicit[i]) + } + } + + // Oversized: too much data for max_version = 1 + { + buf: [BUFFER_LEN_MAX]u8 + seg := make_alphanumeric("THIS IS A SOMEWHAT LONG STRING THAT WILL NOT FIT", buf[:]) + segs := [1]Segment{seg} + + qr_auto: [BUFFER_LEN_MAX]u8 + ok := encode_segments_advanced_auto(segs[:], .High, 1, 1, nil, true, qr_auto[:]) + testing.expect(t, !ok) + testing.expect_value(t, qr_auto[0], u8(0)) + } +}