Hi,

I just want to share a little adventure.

For context: I was not very satisfied with the snippet
[https://lsr.di.unimi.it/LSR/Item?id=1153](https://lsr.di.unimi.it/LSR/Item?id=1153)
because its interface is

```
\markup \qr-code "<big block of data generated externally>"
```

rather than

```
\markup \qr-code "https://my-url.com";
```

I was too lazy to reimplement QR-code encoding (with
error-correcting codes etc.). I decided to take a look
at getting the QR code data directly from Python. Of
course, you can do that with a script and the `system` or
`system*` function to run the `python` command. But
partly out of curiosity, and partly because that
has some problems (like complicated permission issues in
sandboxed environments), I was interested in doing that
by actually using Python in-process, much like Scheme
is being used, which is possible with Guile's FFI
and Python's C API.

The sad outcome is that this requires Guile 3 features
(`load-foreign-library`, with its `#:global?` argument),
while LilyPond is currently on Guile 2, so nobody can use
it yet apart from a few people who, like me, run a version
of LilyPond self-compiled with Guile 3.

Still, seeing that this sort of thing is even possible, and will eventually
become "really" possible when LilyPond (hopefully) switches
to Guile 3, leaves me pensive.

```
\version "2.25.2"

#(use-modules (system foreign)
              (system foreign-library)
              (srfi srfi-111))

run-python =
#(let ()
   (define void (@ (system foreign) void))
   (define python (load-foreign-library "libpython3.11" #:global? #t))
   (define-syntax-rule (wrap fn (args ...) ret)
     (define fn
       (pointer->procedure
        ret
        (dynamic-func (symbol->string 'fn) python)
        (list args ...))))
   (wrap Py_NewRef ('*) '*)
   (wrap Py_DecRef ('*) void)
   (wrap PyRun_String ('* int '* '*) '*)
   (wrap Py_Initialize () void)
   (wrap PyImport_AddModule ('*) '*)
   (wrap PyModule_GetDict ('*) '*)
   (wrap PyObject_Str ('*) '*)
   (wrap PyUnicode_FromString ('*) '*)
   (wrap PyUnicode_AsUTF8 ('*) '*)
   (wrap PyErr_Print () void)
   (wrap PyDict_GetItemString ('* '*) '*)
   (wrap PyDict_SetItemString ('* '* '*) int)
   (define Py_file_input 257)
   (define _initialized (Py_Initialize))
   (define __main__ (string->pointer "__main__" "UTF-8"))
   (define lyval (string->pointer "lyval" "UTF-8"))
   (define lyinput (string->pointer "lyinput" "UTF-8"))
   (define main-module
     (let ((ret (PyImport_AddModule __main__)))
       (when (null-pointer? ret)
         (PyErr_Print)
         (error "Python initialization of __main__ failed"))
       ret))
   (define main-dict (PyModule_GetDict main-module))
   (define (run-python code input)
     (let ((c-string (string->pointer code "UTF-8"))
           (c-input (string->pointer input "UTF-8")))
       (PyDict_SetItemString main-dict lyinput (PyUnicode_FromString c-input))
       (let ((result-obj (PyRun_String c-string Py_file_input main-dict 
main-dict)))
         ;; FIXME: handling exception still prints the Python backtrace
         (when (null-pointer? result-obj)
           (PyErr_Print)
           (error "Error while running Python code"))
         (let ((lyval-ret (PyDict_GetItemString main-dict lyval)))
           (if (null-pointer? lyval-ret)
               (error "lyval not defined by code")
               (let ((lyval-str (PyUnicode_AsUTF8 lyval-ret)))
                 (if (null-pointer? lyval-str)
                     (error "non-string lyval not supported")
                     ;; not sure what should(n't) be decreffed
                     (pointer->string lyval-str -1 "UTF-8"))))))))
   run-python)

#(define (get-qrcode-matrix url)
   (run-python
    "
import pyqrcode
qr = pyqrcode.create(lyinput)
lyval = qr.text(quiet_zone=0)
"
            url))


#(define (index-map f . lsts)
"Applies @code{f} to corresponding elements of @code{lists}, just as @code{map},
providing an additional counter starting at zero.  @code{f} needs to have the
counter in its arguments like @code{(index-map (lambda (i arg) <body>) lists)}"
   (let loop ((lsts lsts)
              (acc '())
              (i 0))
     (if (any null? lsts)
         (reverse! acc)
         (loop (map cdr lsts)
               (cons (apply f i (map car lsts))
                     acc)
               (1+ i)))))

#(define-markup-command (qr-code layout props url) (string?)
   #:properties ((width 10))
   (let* ((data (get-qrcode-matrix url))
          ;; Return lines in reversed order, since translating in Y-axis
          ;; uses increasing values. Meaning lines will be stacked upwards.
          (lines (reverse
                   (remove
                     string-null?
                     (map string-trim-both (string-split data #\newline)))))
          (n (length lines))
          (square-width (/ width n))
          (box (make-filled-box-stencil `(0 . ,square-width)
                                        `(0 . ,square-width))))

     ;; Build the final qr-code-stencil from line-stencils list
     (apply ly:stencil-add
            ;; Get a list of line-stencils
            (index-map
             (lambda (i line)
               ;; Build a line-stencil from square-stencils list
               (apply ly:stencil-add
                      ;; Get a list of (already translated) square-stencils
                      ;; per line
                      (index-map
                       (lambda (j char)
                         (ly:stencil-translate
                          (stencil-with-color
                           box
                           (case char
                            ((#\0)
                             white)
                            ((#\1)
                             black)
                            (else
                             (ly:warning
                               "unrecognized character ~a, should be 0 or 1"
                               char)
                             red)))
                          (cons (* j square-width)
                                (* i square-width))))
                      (string->list line))))
             lines))))



\markup \qr-code "https://lilypond.org";
```

![qrcode.png](/home/jean/qrcode.png)

Attachment: signature.asc
Description: This is a digitally signed message part

Reply via email to