That's pretty cool, and sets a template for those who are brave/foolish
enough to build extensions of all sorts in Python.

Nice job!  What version of Lilypond will that appear in?

On Thu, Feb 9, 2023 at 9:40 PM Jean Abou Samra <[email protected]> wrote:

> 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 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";
>
> [image: qrcode.png]
>


-- 
*Jeff Kopmanis*
Medium: https://kopmanis.medium.com
GLAAC: https://www.glaac.org/
University Lowbrow Astronomers: http://umich.edu/~lowbrows
Orange Can Astronomy: https://www.facebook.com/orangecanastronomy/

** Go Green and leave this email on the Screen! **

Reply via email to