Added quiet zone support to QR rendering

This commit is contained in:
Zachary Levy
2026-05-11 21:36:58 -07:00
parent 7febc2d200
commit 1af8b5cdfb
+39 -27
View File
@@ -1,54 +1,71 @@
package draw_qr package draw_qr
import "core:mem"
import "core:slice"
import draw ".." import draw ".."
import "../../qrcode" import "../../qrcode"
DFT_QR_DARK :: draw.BLACK // Default QR code dark module color. DFT_QR_DARK :: draw.BLACK // Default QR code dark module color.
DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color. DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color.
DFT_QR_BOOST_ECL :: true // Default QR error correction level boost. DFT_QR_BOOST_ECL :: true // Default QR error correction level boost.
DFT_QR_QUIET_ZONE :: 4 // Default light-pixel border on each side; 4 is the QR spec value.
// Returns the number of bytes to_texture will write for the given encoded // Returns the number of bytes to_texture will write. Equals dim*dim*4 where
// QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf). // dim = qrcode.get_size(qrcode_buf) + 2*quiet_zone.
texture_size :: #force_inline proc(qrcode_buf: []u8) -> int { texture_size :: #force_inline proc(qrcode_buf: []u8, quiet_zone: int = DFT_QR_QUIET_ZONE) -> int {
size := qrcode.get_size(qrcode_buf) size := qrcode.get_size(qrcode_buf)
return size * size * 4 if size == 0 || quiet_zone < 0 do return 0
padded_size := size + 2 * quiet_zone
return padded_size * padded_size * 4
} }
// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to // 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 // texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the
// caller should pass to draw.register_texture alongside texture_buf. // caller should pass to draw.register_texture alongside texture_buf.
// //
// quiet_zone adds that many `light` pixels on each side; the spec value is 4.
// Final dimension is qrcode.get_size + 2*quiet_zone on each axis.
//
// Returns ok=false when: // Returns ok=false when:
// - qrcode_buf is invalid (qrcode.get_size returns 0). // - qrcode_buf is invalid (qrcode.get_size returns 0).
// - texture_buf is smaller than texture_size(qrcode_buf). // - quiet_zone is negative.
// - texture_buf is smaller than texture_size(qrcode_buf, quiet_zone).
@(require_results) @(require_results)
to_texture :: proc( to_texture :: proc(
qrcode_buf: []u8, qrcode_buf: []u8,
texture_buf: []u8, texture_buf: []u8,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
) -> ( ) -> (
desc: draw.Texture_Desc, desc: draw.Texture_Desc,
ok: bool, ok: bool,
) { ) {
size := qrcode.get_size(qrcode_buf) size := qrcode.get_size(qrcode_buf)
if size == 0 do return {}, false if size == 0 || quiet_zone < 0 do return
if len(texture_buf) < size * size * 4 do return {}, false padded_size := size + 2 * quiet_zone
if len(texture_buf) < padded_size * padded_size * 4 do return
// Type-pun to []Color so each store is a single 32-bit write.
pixels := mem.slice_data_cast([]draw.Color, texture_buf[:padded_size * padded_size * 4])
// Bulk-fill with light: handles the border and every light QR module at once.
slice.fill(pixels, light)
// Overwrite only the dark modules, offset by the quiet-zone border.
for y in 0 ..< size { for y in 0 ..< size {
row := (y + quiet_zone) * padded_size + quiet_zone
for x in 0 ..< size { for x in 0 ..< size {
i := (y * size + x) * 4 if qrcode.get_module(qrcode_buf, x, y) {
c := dark if qrcode.get_module(qrcode_buf, x, y) else light pixels[row + x] = dark
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 { return draw.Texture_Desc {
width = u32(size), width = u32(padded_size),
height = u32(size), height = u32(padded_size),
depth_or_layers = 1, depth_or_layers = 1,
type = .D2, type = .D2,
format = .R8G8B8A8_UNORM, format = .R8G8B8A8_UNORM,
@@ -71,19 +88,20 @@ register_texture_from_raw :: proc(
qrcode_buf: []u8, qrcode_buf: []u8,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
ok: bool, ok: bool,
) { ) {
tex_size := texture_size(qrcode_buf) tex_size := texture_size(qrcode_buf, quiet_zone)
if tex_size == 0 do return draw.INVALID_TEXTURE, false if tex_size == 0 do return draw.INVALID_TEXTURE, false
pixels, alloc_err := make([]u8, tex_size, temp_allocator) pixels, alloc_err := make([]u8, tex_size, temp_allocator)
if alloc_err != nil do return draw.INVALID_TEXTURE, false if alloc_err != nil do return draw.INVALID_TEXTURE, false
defer delete(pixels, temp_allocator) defer delete(pixels, temp_allocator)
desc := to_texture(qrcode_buf, pixels, dark, light) or_return desc := to_texture(qrcode_buf, pixels, dark, light, quiet_zone) or_return
return draw.register_texture(desc, pixels) return draw.register_texture(desc, pixels)
} }
@@ -103,6 +121,7 @@ register_texture_from_text :: proc(
boost_ecl: bool = DFT_QR_BOOST_ECL, boost_ecl: bool = DFT_QR_BOOST_ECL,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
@@ -123,7 +142,7 @@ register_texture_from_text :: proc(
temp_allocator, temp_allocator,
) or_return ) or_return
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator) return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
} }
// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture. // Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture.
@@ -142,6 +161,7 @@ register_texture_from_binary :: proc(
boost_ecl: bool = DFT_QR_BOOST_ECL, boost_ecl: bool = DFT_QR_BOOST_ECL,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
@@ -162,18 +182,10 @@ register_texture_from_binary :: proc(
temp_allocator, temp_allocator,
) or_return ) or_return
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator) return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
} }
register_texture_from :: proc { register_texture_from :: proc {
register_texture_from_text, register_texture_from_text,
register_texture_from_binary, register_texture_from_binary,
} }
// 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.DFT_TINT,
) -> draw.Clay_Image_Data {
return draw.clay_image_data(texture, fit = .Fit, tint = tint)
}