1 February 2023

Calling a C Library for QR Generation from Chicken Scheme

I wanted to create a QR code from within Chicken Scheme, but I couldn't find an existing Scheme library or chicken Scheme egg online that would let me do this. Fortunately, Chicken Scheme is pretty easy to integrate with existing C code.

The first thing I did was I wrote a Hello World in Scheme and another Hello World in C, and checked that I could get both of these to run (using the chicken compiler and GCC).

C

#include <stdio.h>

int main(void) {
    printf("Hello World\n");
    return 0;
}
cc hello_world.c
./a.out

Chicken Scheme

(print "Hello World")
chicken-csc hello_world.scm
./hello_world

Once I got both of those running, I downloaded a C library that I found online for generating QR Codes (https://github.com/nayuki/QR-Code-generator/tree/master/c) and I copied the demo for the usage of the library. I ran that completely within C, to check it worked before trying to call it from Chicken Scheme.

Stripped-down version of the demo from https://github.com/nayuki/QR-Code-generator/blob/master/c/qrcodegen-demo.c:

#include <stdint.h>
#include <stdio.h>
#include "qrcodegen.h"

// Prints the given QR Code to the console.
static void printQr(const uint8_t qrcode[]) {
	int size = qrcodegen_getSize(qrcode);
	int border = 4;
	for (int y = -border; y < size + border; y++) {
		for (int x = -border; x < size + border; x++) {
			fputs((qrcodegen_getModule(qrcode, x, y) ? "##" : "  "), stdout);
		}
		fputs("\n", stdout);
	}
	fputs("\n", stdout);
}

void doBasicDemo(void) {
	const char *text = "Hello, world!";                // User-supplied text
	enum qrcodegen_Ecc errCorLvl = qrcodegen_Ecc_LOW;  // Error correction level
	
	// Make and print the QR Code symbol
	uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX];
	uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX];
	bool ok = qrcodegen_encodeText(text, tempBuffer, qrcode, errCorLvl,
		qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
	if (ok)
		printQr(qrcode);
}

int main() {
    doBasicDemo();
    return 0;
}

With qrcodegen.h and qrcodegen.c in a folder lib/:

cc qr_demo.c lib/qrcodegen.c -I lib/
./a.out

With it working in C, I moved the demo code into a library itself and I included that into the Scheme program and called the demo function from within Scheme. I ran the Scheme program and checked that the demo worked from within Scheme.

I removed main, and moved the c file to lib/qrgeninterface.c, and created a header file lib/qrgeninterface.hs as follows:

Header file:

void doBasicDemo();

With the external library and interface files in place, I imported the library and called the function from within the Scheme program.

(import (chicken foreign))
(foreign-declare "#include \"qrgeninterface.h\"")
(foreign-code "doBasicDemo();")

I compiled and ran the Scheme program with the c files included:

chicken-csc qr.scm lib/qrgeninterface.c  lib/qrcodegen.c -I'lib'
./qr

With the demo working, the next step was to figure out how to pass our own data in and get the result back as a U8 vector rather than just printing it to the screen.

First, I created a new function with a char array as an argument, and I checked that the demo still worked with the text passed in as a char array.

New C function:

void genQr(char *text) {
	enum qrcodegen_Ecc errCorLvl = qrcodegen_Ecc_LOW;
	uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX];
	uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX];
	bool ok = qrcodegen_encodeText(text, tempBuffer, qrcode, errCorLvl,
                qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
	if (ok) {
            printQr(qrcode);
        }
}

Header file entry:

void genQr(char*);

In the Scheme file, I added a foreign-lambda definition so that the function could be called from Scheme, and I called it.

(import (chicken foreign))
(foreign-declare "#include \"qrgeninterface.h\"")
(define (gen-qr text)
  ((foreign-lambda void "genQr" c-string) text))
(gen-qr "Hello World!")

Compile and run again

chicken-csc qr.scm lib/qrgeninterface.c  lib/qrcodegen.c -I'lib'
./qr

That worked, so I added another argument to the function to allow a reference to a U8 vector to be passed in, which the function can then write to as an output. I also needed a way of getting the buffer max size from within the Scheme code, as this will be used to determine the vector size, so I added a function to facilitate this.

New header file:

#include <stdint.h>
void genQr(char*, uint8_t*);
int getBufferMaxSize();

The function definitions that were added to the c source file:

void genQr(char *text, uint8_t *qrcode) {
	enum qrcodegen_Ecc errCorLvl = qrcodegen_Ecc_LOW;
	uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX];
	bool ok = qrcodegen_encodeText(text, tempBuffer, qrcode, errCorLvl,
                qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
}

int getBufferMaxSize() {
    return qrcodegen_BUFFER_LEN_MAX;
}

And then in the Scheme file:

(import
  (chicken foreign)
  (srfi-4)) ; u8vector imported from srfi-4

(foreign-declare "#include \"qrgeninterface.h\"")

(define buffer-max-size
    ((foreign-lambda int "getBufferMaxSize")))

(define (gen-qr text buffer)
  ((foreign-lambda void "genQr" c-string u8vector) text buffer))


; To test, I created a buffer and checked that data was being added to it
; by the foreign function call
(let ((buffer (make-u8vector buffer-max-size 0)))
    (display buffer) ; Check the buffer before
    (gen-qr "Hello World!" buffer)
    (display buffer)) ; Check it again and see if data has been added

I ran it and confirmed that something was being put into the buffer.

That thing is not the final QR code PNG, it is a byte vector representing the QR code. There are a couple of functions provided by the library for reading this format. The first is a function to return the width (and therefore also height) of the QR code, qrcodegen_getSize, the second is qrcodegen_getModule, a function which returns true if the QR code pixel at the given coordinate is black (and false if it is white). I added functions to my interface to access these functions.

Addition to header:

// new include
#include <stdbool.h>
// new declarations
int qrcodeSize(uint8_t*);
bool qrcodePixelValue(uint8_t*, int, int);

Addition to source:

int qrcodeSize(uint8_t *qrcode) {
    return qrcodegen_getSize(qrcode);
}

bool qrcodePixelValue(uint8_t *qrcode, int x, int y) {
    return qrcodegen_getModule(qrcode, x, y);
}

Scheme functions:

(define (qrcode-size qr)
  ((foreign-lambda int "qrcodeSize" u8vector) qr))

(define (qrcode-pixel-value qr x y)
  ((foreign-lambda bool "qrcodePixelValue" u8vector int int) qr x y))

And then I checked the functions could be called from Scheme:

(let ((buffer (make-u8vector buffer-max-size 0)))
    (gen-qr "Hello World!" buffer)
    (display "qr size: ")(display (qrcode-size buffer))
    (newline)
    (display "qr pixel at (0,0): ")(display (qrcode-pixel-value buffer 0 0))
    (newline))

I wanted to output as a PNG, so I installed the stb-image-write Egg (http://wiki.call-cc.org/eggref/5/stb-image-write)

I defined constants for black and white:

(define black 0)
(define white 255)

I then wrote a function to create a vector of pixels representing the image, with a desired target resolution and border width.

The vector default value is set to white, and then for each pixel in the square inside the border, three values are calculated: the output vector's index, and the x and y values of the pixel within the QR code created by the qrcodegen library. The value at the index is then set to the black if the pixel value function returns true for that x and y.

(define (scale-n x w0 w1)
       (floor (* x (/ w1 w0))))

(define (make-scaled-bitmap raw border-width image-width)
  (let ((raw-width (qrcode-size raw))
        (new-size (* image-width image-width))
        (inner-width (- image-width (* 2 border-width))))
    (do
      ((out (make-u8vector new-size white))
       (y 0 (+ y 1)))
      ((= y inner-width) out)
      (do ((x 0 (+ x 1)))
        ((= x inner-width))
        (let ((i (+ x border-width (* image-width (+ border-width y)))))
          (u8vector-set! out i
             (if (qrcode-pixel-value
                    raw
                    (scale-n x inner-width raw-width)
                    (scale-n y inner-width raw-width))
                black
                white)))))))

This byte vector can then be written to a file using stb-image-write:

(import (stb-image-write))

(let ((buffer (make-u8vector buffer-max-size 0))
      (image-width 512)
      (border-width 16))
  (gen-qr "Hello World!" buffer)
  (with-output-to-file "/tmp/demo-qr.png"
    (lambda ()
      (write-png
        (make-scaled-bitmap buffer border-width image-width)
        image-width ; width
        image-width ; height
        1)))) ; greyscale, 1 channel

And then the resulting image:

QR Code PNG
> View Final Code
.
├── lib
│   ├── qrcodegen.c
│   ├── qrcodegen.h
│   ├── qrgeninterface.c
│   └── qrgeninterface.h
└── qr.scm

Build Command:

chicken-csc qr.scm lib/qrgeninterface.c  lib/qrcodegen.c -I'lib'

qr.scm

(import
  (chicken foreign)
  (srfi-4)
  (stb-image-write))

(foreign-declare "#include \"qrgeninterface.h\"")

(define black 0)
(define white 255)
(define buffer-max-size
    ((foreign-lambda int "getBufferMaxSize")))

(define (gen-qr text buffer)
  ((foreign-lambda void "genQr" c-string u8vector) text buffer))

(define (qrcode-size qr)
  ((foreign-lambda int "qrcodeSize" u8vector) qr))

(define (qrcode-pixel-value qr x y)
  ((foreign-lambda bool "qrcodePixelValue" u8vector int int) qr x y))

(define (scale-n x w0 w1)
       (floor (* x (/ w1 w0))))

(define (make-scaled-bitmap raw border-width image-width)
  (let ((raw-width (qrcode-size raw))
        (new-size (* image-width image-width))
        (inner-width (- image-width (* 2 border-width))))
    (do
      ((out (make-u8vector new-size white))
       (y 0 (+ y 1)))
      ((= y inner-width) out)
      (do ((x 0 (+ x 1)))
        ((= x inner-width))
        (let ((i (+ x border-width (* image-width (+ border-width y)))))
          (u8vector-set! out i
             (if (qrcode-pixel-value
                    raw
                    (scale-n x inner-width raw-width)
                    (scale-n y inner-width raw-width))
                black
                white)))))))

(let ((buffer (make-u8vector buffer-max-size 0))
      (image-width 512)
      (border-width 16))
  (gen-qr "Hello World!" buffer)
  (with-output-to-file "/tmp/demo.png"
    (lambda ()
      (write-png
        (make-scaled-bitmap buffer border-width image-width)
        image-width ; width
        image-width ; height
        1)))) ; greyscale, 1 channel

qrgeninterface.h

#include <stdbool.h>
#include <stdint.h>
int getBufferMaxSize();
void genQr(char*, uint8_t*);
int qrcodeSize(uint8_t*);
bool qrcodePixelValue(uint8_t*, int, int);

qrgeninterface.c

#include <stdint.h>
#include <stdio.h>
#include "qrcodegen.h"

void genQr(char *text, uint8_t *qrcode) {
	enum qrcodegen_Ecc errCorLvl = qrcodegen_Ecc_LOW;
	uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX];
	bool ok = qrcodegen_encodeText(text, tempBuffer, qrcode, errCorLvl,
                qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
}

int getBufferMaxSize() {
    return qrcodegen_BUFFER_LEN_MAX;
}

int qrcodeSize(uint8_t *qrcode) {
    return qrcodegen_getSize(qrcode);
}

bool qrcodePixelValue(uint8_t *qrcode, int x, int y) {
    return qrcodegen_getModule(qrcode, x, y);
}

The qrcodegen library can be found at https://github.com/nayuki/QR-Code-generator/tree/master/c.

Tags: C Scheme