From ba522fa051143ce15a1371918ee043e902ac07e2 Mon Sep 17 00:00:00 2001 From: Zachary Levy Date: Tue, 21 Apr 2026 15:35:55 -0700 Subject: [PATCH] QR code improvements --- draw/draw_qr/draw_qr.odin | 216 +++++++++++++++++++++++++++--------- draw/examples/textures.odin | 12 +- qrcode/generate.odin | 128 +++++++++++++-------- 3 files changed, 247 insertions(+), 109 deletions(-) diff --git a/draw/draw_qr/draw_qr.odin b/draw/draw_qr/draw_qr.odin index 9fb3a0f..e5b1d84 100644 --- a/draw/draw_qr/draw_qr.odin +++ b/draw/draw_qr/draw_qr.odin @@ -3,76 +3,188 @@ package draw_qr import draw ".." import "../../qrcode" -// A registered QR code texture, ready for display via draw.rectangle_texture. -QR :: struct { - texture_id: draw.Texture_Id, - size: int, // modules per side (e.g. 21..177) +// ----------------------------------------------------------------------------- +// Layer 1 — pure: encoded QR buffer → RGBA pixels + descriptor +// ----------------------------------------------------------------------------- + +// Returns the number of bytes to_texture will write for the given encoded +// QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf). +texture_size :: #force_inline proc(qrcode_buf: []u8) -> int { + size := qrcode.get_size(qrcode_buf) + return size * size * 4 } -// Encode text as a QR code and register the result as an R8 texture. -// The texture uses Nearest_Clamp sampling by default (sharp module edges). -// Returns ok=false if encoding or registration fails. +// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to +// texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the +// caller should pass to draw.register_texture alongside texture_buf. +// +// Returns ok=false when: +// - qrcode_buf is invalid (qrcode.get_size returns 0). +// - texture_buf is smaller than to_texture_size(qrcode_buf). @(require_results) -create_from_text :: proc( +to_texture :: proc( + qrcode_buf: []u8, + texture_buf: []u8, + dark: draw.Color = draw.BLACK, + light: draw.Color = draw.WHITE, +) -> ( + desc: draw.Texture_Desc, + ok: bool, +) { + size := qrcode.get_size(qrcode_buf) + if size == 0 do return {}, false + if len(texture_buf) < size * size * 4 do return {}, false + + for y in 0 ..< size { + for x in 0 ..< size { + i := (y * size + x) * 4 + c := dark if qrcode.get_module(qrcode_buf, x, y) else light + texture_buf[i + 0] = c[0] + texture_buf[i + 1] = c[1] + texture_buf[i + 2] = c[2] + texture_buf[i + 3] = c[3] + } + } + + return draw.Texture_Desc { + width = u32(size), + height = u32(size), + depth_or_layers = 1, + type = .D2, + format = .R8G8B8A8_UNORM, + usage = {.SAMPLER}, + mip_levels = 1, + kind = .Static, + }, + true +} + +// ----------------------------------------------------------------------------- +// Layer 2 — raw: pre-encoded QR buffer → registered GPU texture +// ----------------------------------------------------------------------------- + +// Allocates pixel buffer via temp_allocator, decodes qrcode_buf into it, and +// registers with the GPU. The pixel allocation is freed before return. +// +// Returns ok=false when: +// - qrcode_buf is invalid (qrcode.get_size returns 0). +// - temp_allocator fails to allocate the pixel buffer. +// - GPU texture registration fails. +@(require_results) +register_texture_from_raw :: proc( + qrcode_buf: []u8, + dark: draw.Color = draw.BLACK, + light: draw.Color = draw.WHITE, + temp_allocator := context.temp_allocator, +) -> ( + texture: draw.Texture_Id, + ok: bool, +) { + tex_size := texture_size(qrcode_buf) + if tex_size == 0 do return draw.INVALID_TEXTURE, false + + pixels, alloc_err := make([]u8, tex_size, temp_allocator) + if alloc_err != nil do return draw.INVALID_TEXTURE, false + defer delete(pixels, temp_allocator) + + desc := to_texture(qrcode_buf, pixels, dark, light) or_return + return draw.register_texture(desc, pixels) +} + +// ----------------------------------------------------------------------------- +// Layer 3 — text → registered GPU texture +// ----------------------------------------------------------------------------- + +// Encodes text as a QR Code and registers the result as an RGBA texture. +// +// Returns ok=false when: +// - temp_allocator fails to allocate. +// - The text cannot fit in any version within [min_version, max_version] at the given ECL. +// - GPU texture registration fails. +@(require_results) +register_texture_from_text :: proc( text: string, ecl: qrcode.Ecc = .Low, min_version: int = qrcode.VERSION_MIN, max_version: int = qrcode.VERSION_MAX, mask: Maybe(qrcode.Mask) = nil, boost_ecl: bool = true, + dark: draw.Color = draw.BLACK, + light: draw.Color = draw.WHITE, + temp_allocator := context.temp_allocator, ) -> ( - qr: QR, + texture: draw.Texture_Id, ok: bool, ) { - qrcode_buf: [qrcode.BUFFER_LEN_MAX]u8 - encode_ok := qrcode.encode(text, qrcode_buf[:], ecl, min_version, max_version, mask, boost_ecl) - if !encode_ok do return {}, false - return create(qrcode_buf[:]) + qrcode_buf, alloc_err := make([]u8, qrcode.buffer_len_for_version(max_version), temp_allocator) + if alloc_err != nil do return draw.INVALID_TEXTURE, false + defer delete(qrcode_buf, temp_allocator) + + qrcode.encode_auto( + text, + qrcode_buf, + ecl, + min_version, + max_version, + mask, + boost_ecl, + temp_allocator, + ) or_return + + return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator) } -// Register an already-encoded QR code buffer as an R8 texture. -// qrcode_buf must be the output of qrcode.encode (byte 0 = side length, remaining = bit-packed modules). +// ----------------------------------------------------------------------------- +// Layer 4 — binary → registered GPU texture +// ----------------------------------------------------------------------------- + +// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture. +// +// Returns ok=false when: +// - temp_allocator fails to allocate. +// - The payload cannot fit in any version within [min_version, max_version] at the given ECL. +// - GPU texture registration fails. @(require_results) -create :: proc(qrcode_buf: []u8) -> (qr: QR, ok: bool) { - size := qrcode.get_size(qrcode_buf) - if size == 0 do return {}, false +register_texture_from_binary :: proc( + bin_data: []u8, + ecl: qrcode.Ecc = .Low, + min_version: int = qrcode.VERSION_MIN, + max_version: int = qrcode.VERSION_MAX, + mask: Maybe(qrcode.Mask) = nil, + boost_ecl: bool = true, + dark: draw.Color = draw.BLACK, + light: draw.Color = draw.WHITE, + temp_allocator := context.temp_allocator, +) -> ( + texture: draw.Texture_Id, + ok: bool, +) { + qrcode_buf, alloc_err := make([]u8, qrcode.buffer_len_for_version(max_version), temp_allocator) + if alloc_err != nil do return draw.INVALID_TEXTURE, false + defer delete(qrcode_buf, temp_allocator) - // Build R8 pixel buffer: 0 = light, 255 = dark - pixels := make([]u8, size * size, context.temp_allocator) - for y in 0 ..< size { - for x in 0 ..< size { - pixels[y * size + x] = 255 if qrcode.get_module(qrcode_buf, x, y) else 0 - } - } + qrcode.encode_auto( + bin_data, + qrcode_buf, + ecl, + min_version, + max_version, + mask, + boost_ecl, + temp_allocator, + ) or_return - id, reg_ok := draw.register_texture( - draw.Texture_Desc { - width = u32(size), - height = u32(size), - depth_or_layers = 1, - type = .D2, - format = .R8_UNORM, - usage = {.SAMPLER}, - mip_levels = 1, - kind = .Static, - }, - pixels, - ) - if !reg_ok do return {}, false - - return QR{texture_id = id, size = size}, true + return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator) } -// Release the GPU texture. -destroy :: proc(qr: ^QR) { - draw.unregister_texture(qr.texture_id) - qr.texture_id = draw.INVALID_TEXTURE - qr.size = 0 -} +// ----------------------------------------------------------------------------- +// Clay integration helper +// ----------------------------------------------------------------------------- -// Convenience: build a Clay_Image_Data for embedding a QR in Clay layouts. -// Uses Nearest_Clamp sampling (set via Sampler_Preset at draw time, not here) and Fit mode -// to preserve the QR's square aspect ratio. -clay_image :: proc(qr: QR, tint: draw.Color = draw.WHITE) -> draw.Clay_Image_Data { - return draw.clay_image_data(qr.texture_id, fit = .Fit, tint = tint) +// Default fit=.Fit preserves the QR's square aspect; override as needed. +clay_image :: #force_inline proc( + texture: draw.Texture_Id, + tint: draw.Color = draw.WHITE, +) -> draw.Clay_Image_Data { + return draw.clay_image_data(texture, fit = .Fit, tint = tint) } diff --git a/draw/examples/textures.odin b/draw/examples/textures.odin index ca53ba3..a89be7d 100644 --- a/draw/examples/textures.odin +++ b/draw/examples/textures.odin @@ -79,8 +79,8 @@ textures :: proc() { // ------------------------------------------------------------------------- // QR code texture (R8_UNORM — see rendering note below) // ------------------------------------------------------------------------- - qr, _ := draw_qr.create_from_text("https://odin-lang.org/") - defer draw_qr.destroy(&qr) + qr_texture, _ := draw_qr.register_texture_from_text("https://x.com/miiilato/status/1880241066471051443") + defer draw.unregister_texture(qr_texture) spin_angle: f32 = 0 @@ -161,16 +161,12 @@ textures :: proc() { // ===================================================================== ROW2_Y :: f32(190) - // QR code (R8_UNORM texture, nearest sampling) - // NOTE: R8_UNORM samples as (r, 0, 0, 1) in Metal's default swizzle. - // With WHITE tint: dark modules (R=1) → red, light modules (R=0) → black. - // The result is a red-on-black QR code. The white bg rect below is - // occluded by the fully-opaque texture but kept for illustration. + // QR code (RGBA texture with baked colors, nearest sampling) draw.rectangle(base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, {255, 255, 255, 255}) // white bg draw.rectangle_texture( base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, - qr.texture_id, + qr_texture, sampler = .Nearest_Clamp, ) draw.text( diff --git a/qrcode/generate.odin b/qrcode/generate.odin index 9014b56..8261021 100644 --- a/qrcode/generate.odin +++ b/qrcode/generate.odin @@ -117,7 +117,7 @@ NUM_ERROR_CORRECTION_BLOCKS := [4][41]i8{ // - 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( +encode_text_manual :: proc( text: string, temp_buffer, qrcode: []u8, ecl: Ecc, @@ -130,7 +130,7 @@ encode_text_explicit_temp :: proc( ) { text_len := len(text) if text_len == 0 { - return encode_segments_advanced_explicit_temp( + return encode_segments_advanced_manual( nil, ecl, min_version, @@ -162,7 +162,7 @@ encode_text_explicit_temp :: proc( seg.data = temp_buffer[:text_len] } segs := [1]Segment{seg} - return encode_segments_advanced_explicit_temp( + return encode_segments_advanced_manual( segs[:], ecl, min_version, @@ -211,13 +211,9 @@ encode_text_auto :: proc( 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) + return encode_text_manual(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. // @@ -234,7 +230,7 @@ encode_text :: proc { // 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( +encode_binary_manual :: proc( data_and_temp: []u8, data_len: int, qrcode: []u8, @@ -256,7 +252,7 @@ encode_binary :: proc( seg.num_chars = data_len seg.data = data_and_temp[:data_len] segs := [1]Segment{seg} - return encode_segments_advanced( + return encode_segments_advanced_manual( segs[:], ecl, min_version, @@ -268,6 +264,55 @@ encode_binary :: proc( ) } +// Encodes arbitrary binary data to a QR Code using byte mode, +// automatically allocating and freeing the temp buffer. +// +// Parameters: +// bin_data - [in] Payload bytes (aliased by the internal segment; not modified). +// 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 payload cannot fit in any version within [min_version, max_version] at the given ECL. +// - The temp_allocator fails to allocate. +@(require_results) +encode_binary_auto :: proc( + bin_data: []u8, + 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, +) { + seg: Segment + seg.mode = .Byte + seg.bit_length = calc_segment_bit_length(.Byte, len(bin_data)) + if seg.bit_length == LENGTH_OVERFLOW { + qrcode[0] = 0 + return false + } + seg.num_chars = len(bin_data) + seg.data = bin_data + segs := [1]Segment{seg} + return encode_segments_advanced_auto( + segs[:], + ecl, + min_version, + max_version, + mask, + boost_ecl, + qrcode, + temp_allocator, + ) +} + // Encodes the given segments to a QR Code using default parameters // (VERSION_MIN..VERSION_MAX, auto mask, boost ECL). // @@ -282,17 +327,8 @@ encode_binary :: proc( // 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, - ) +encode_segments_manual :: proc(segs: []Segment, ecl: Ecc, temp_buffer, qrcode: []u8) -> (ok: bool) { + return encode_segments_advanced_manual(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. @@ -328,13 +364,9 @@ encode_segments_auto :: proc( return false } defer delete(temp_buffer, temp_allocator) - return encode_segments_explicit_temp(segs, ecl, temp_buffer, qrcode) + return encode_segments_manual(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. // @@ -353,7 +385,7 @@ encode_segments :: proc { // - 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( +encode_segments_advanced_manual :: proc( segs: []Segment, ecl: Ecc, min_version, max_version: int, @@ -490,7 +522,7 @@ encode_segments_advanced_auto :: proc( return false } defer delete(temp_buffer, temp_allocator) - return encode_segments_advanced_explicit_temp( + return encode_segments_advanced_manual( segs, ecl, min_version, @@ -502,18 +534,17 @@ encode_segments_advanced_auto :: proc( ) } -encode_segments_advanced :: proc { - encode_segments_advanced_explicit_temp, - encode_segments_advanced_auto, +encode_manual :: proc { + encode_text_manual, + encode_binary_manual, + encode_segments_manual, + encode_segments_advanced_manual, } -encode :: proc { - encode_text_explicit_temp, +encode_auto :: proc { encode_text_auto, - encode_binary, - encode_segments_explicit_temp, + encode_binary_auto, encode_segments_auto, - encode_segments_advanced_explicit_temp, encode_segments_advanced_auto, } @@ -981,7 +1012,7 @@ min_buffer_size :: proc { min_buffer_size_segments, } -// Text path: auto-selects numeric/alphanumeric/byte mode the same way encode_text does. +// Text path: auto-selects numeric/alphanumeric/byte mode the same way encode_text_manual does. // // Returns ok=false when: // - The text exceeds QR Code capacity for every version in the range at the given ECL. @@ -1162,7 +1193,6 @@ calc_segment_buffer_size :: proc(mode: Mode, num_chars: int) -> int { 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 @@ -2487,7 +2517,7 @@ test_min_buffer_size_text :: proc(t: ^testing.T) { testing.expect(t, planned > 0) qrcode: [BUFFER_LEN_MAX]u8 temp: [BUFFER_LEN_MAX]u8 - ok := encode_text(text, temp[:], qrcode[:], Ecc.Low) + ok := encode_text_manual(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) @@ -2538,7 +2568,7 @@ test_min_buffer_size_binary :: proc(t: ^testing.T) { testing.expect(t, size > 0) testing.expect(t, size <= buffer_len_for_version(2)) - // Verify agreement with encode_binary + // Verify agreement with encode_binary_manual { data_len :: 100 planned, planned_ok := min_buffer_size(data_len, .Medium) @@ -2549,7 +2579,7 @@ test_min_buffer_size_binary :: proc(t: ^testing.T) { for i in 0 ..< data_len { dat[i] = u8(i) } - ok := encode_binary(dat[:], data_len, qrcode[:], .Medium) + ok := encode_binary_manual(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) @@ -2609,7 +2639,7 @@ test_min_buffer_size_segments :: proc(t: ^testing.T) { // Verify against actual encode qrcode: [BUFFER_LEN_MAX]u8 temp: [BUFFER_LEN_MAX]u8 - ok := encode_segments(segs[:], Ecc.Low, temp[:], qrcode[:]) + ok := encode_segments_manual(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) @@ -2631,7 +2661,7 @@ test_encode_text_auto :: proc(t: ^testing.T) { 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) + ok_explicit := encode_text_manual(text, temp[:], qr_explicit[:], .Low) testing.expect(t, ok_explicit) qr_auto: [BUFFER_LEN_MAX]u8 @@ -2650,7 +2680,7 @@ test_encode_text_auto :: proc(t: ^testing.T) { text :: "314159265358979323846264338327950288419716939937510" qr_explicit: [BUFFER_LEN_MAX]u8 temp: [BUFFER_LEN_MAX]u8 - ok_explicit := encode_text_explicit_temp(text, temp[:], qr_explicit[:], .Medium) + ok_explicit := encode_text_manual(text, temp[:], qr_explicit[:], .Medium) testing.expect(t, ok_explicit) qr_auto: [BUFFER_LEN_MAX]u8 @@ -2669,7 +2699,7 @@ test_encode_text_auto :: proc(t: ^testing.T) { 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) + ok_explicit := encode_text_manual(text, temp[:], qr_explicit[:], .Quartile) testing.expect(t, ok_explicit) qr_auto: [BUFFER_LEN_MAX]u8 @@ -2695,7 +2725,7 @@ test_encode_text_auto :: proc(t: ^testing.T) { 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) + ok_explicit := encode_text_manual(text, temp[:], qr_explicit[:], .High, mask = .M3) testing.expect(t, ok_explicit) qr_auto: [BUFFER_LEN_MAX]u8 @@ -2732,7 +2762,7 @@ test_encode_segments_auto :: proc(t: ^testing.T) { qr_explicit: [BUFFER_LEN_MAX]u8 temp: [BUFFER_LEN_MAX]u8 - ok_explicit := encode_segments_explicit_temp(segs[:], .Low, temp[:], qr_explicit[:]) + ok_explicit := encode_segments_manual(segs[:], .Low, temp[:], qr_explicit[:]) testing.expect(t, ok_explicit) qr_auto: [BUFFER_LEN_MAX]u8 @@ -2764,7 +2794,7 @@ test_encode_segments_advanced_auto :: proc(t: ^testing.T) { qr_explicit: [BUFFER_LEN_MAX]u8 temp: [BUFFER_LEN_MAX]u8 - ok_explicit := encode_segments_advanced_explicit_temp( + ok_explicit := encode_segments_advanced_manual( segs[:], .Medium, VERSION_MIN, @@ -2795,7 +2825,7 @@ test_encode_segments_advanced_auto :: proc(t: ^testing.T) { qr_explicit: [BUFFER_LEN_MAX]u8 temp: [BUFFER_LEN_MAX]u8 - ok_explicit := encode_segments_advanced_explicit_temp( + ok_explicit := encode_segments_advanced_manual( segs[:], .High, 1,