diff --git a/README.org b/README.org index 9cf1937..98eae02 100644 --- a/README.org +++ b/README.org @@ -21,9 +21,10 @@ qrencode RET=. QR Codes are rendered as Unicode text, but there is an option to - export them as bitmap (NetPBM format) by pressing =e= in the QR Code - buffer. There are also some public elisp library functions to - generate QR Codes for use in other elisp code. + export them as bitmap (NetPBM format) or vector graphics (SVG) by + pressing =e= in the QR Code buffer. There are also some public + elisp library functions to generate QR Codes for use in other elisp + code. Currently two NetPBM formats are supported: P1 and P4. The latter is much smaller and the exporter much faster, so highly recommended. @@ -32,6 +33,10 @@ format can be changed. In the next major version release P1 support is likely to get dropped in favour of P4. + SVG export is available if =svg.el= (Emacs 26.1 or newer) is + available. This can be selected by customizing + =qrencode-export-format= to ='svg=. + *** Converting different bitmaps This package only supports exporting to NetPBM format due to the @@ -60,7 +65,7 @@ [[file:https://github.com/ruediger/qrencode-el/actions/workflows/test.yml/badge.svg]] - The code is written in pure Emacs Lisp on GNU Emacs 27. It should + The code is written in pure Emacs Lisp on GNU Emacs 31. It should be backwards compatible to GNU Emacs 25.1 and potentially even earlier with =seq= and =cl-lib= compat libraries. diff --git a/qr-self.svg b/qr-self.svg new file mode 100644 index 0000000..0d52712 --- /dev/null +++ b/qr-self.svg @@ -0,0 +1 @@ + diff --git a/qrencode-tests.el b/qrencode-tests.el index 973a973..a26aa5c 100644 --- a/qrencode-tests.el +++ b/qrencode-tests.el @@ -536,6 +536,13 @@ The expected word for version 7 is the literal from section 7.10 of the 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "))) +(ert-deftest qrencode-svg-path-test () + "Test SVG path." + (skip-unless (featurep 'svg)) + (should (string= (qrencode--svg-path [[0 0] [0 0]] 0) "")) + (should (string= (qrencode--svg-path [[1 1] [1 1]] 0) "M0 0h2v1h-2zM0 1h2v1h-2z")) + (should (string= (qrencode--svg-path [[0 0 1] [0 1 1] [0 0 0]] 0) "M2 0h1v1h-1zM1 1h2v1h-2z"))) + (defun qrencode-tests--get-file (filename) "Helper function returning contents of FILENAME." (with-temp-buffer @@ -553,7 +560,8 @@ The expected word for version 7 is the literal from section 7.10 of the (should (not (null qrencode--raw-qr))) (let ((tmpfile-p1 (make-temp-file "qr-p1" nil ".pbm")) (tmpfile-p4 (make-temp-file "qr-p4" nil ".pbm")) - p1 p4) + (tmpfile-svg (make-temp-file "qr-svg" nil ".svg")) + p1 p4 svg) (unwind-protect (progn (let ((qrencode-export-format 'p1)) @@ -564,18 +572,26 @@ The expected word for version 7 is the literal from section 7.10 of the (qrencode-export-buffer-to-file tmpfile-p4)) (setq p4 (qrencode-tests--get-file tmpfile-p4)) (should (string-prefix-p "P4\n" p4)) + (when (featurep 'svg) + (let ((qrencode-export-format 'svg)) + (qrencode-export-buffer-to-file tmpfile-svg)) + (setq svg (qrencode-tests--get-file tmpfile-svg)) + (should (string-prefix-p " ;; Keywords: qrcode comm -;; Version: 1.5 +;; Version: 1.6-beta1 ;; Package-Requires: ((emacs "25.1")) ;; Package: qrencode ;; URL: https://github.com/ruediger/qrencode-el @@ -44,6 +44,9 @@ (require 'seq) (require 'thingatpt) +;; svg.el is only available in Emacs 26.1 and newer. +(require 'svg nil 'noerror) + ;;; Error correction ;; Reed solomon ECC implementation based on https://research.swtch.com/field @@ -1043,6 +1046,61 @@ Optionally specify PIXEL-SIZE (default is 3)." (dotimes (_ quiet-zone-size) (dotimes (_ bsize) (insert 0)))))) +;; Declare SVG functions in case svg.el is not available. +(declare-function svg-create "svg" (width height &rest args)) +(declare-function svg-rectangle "svg" (svg x y width height &rest args)) +(declare-function svg-node "svg" (svg tag &rest args)) +(declare-function svg-print "svg" (dom)) + +(defun qrencode--svg-path (qr quiet-zone-size) + "Return svg path for QR with QUIET-ZONE-SIZE." + (with-temp-buffer + (let ((size (length qr))) + (dotimes (r size) + (let ((c 0)) + (while (< c size) + (if (= (qrencode--aaref qr c r) 1) + (let ((start c)) + ;; We only bother scanning one row at a time and turning + ;; series of 1's into a rectangle. + (while (and (< c size) (= (qrencode--aaref qr c r) 1)) + (setq c (1+ c))) + (let ((block-len (- c start))) + (insert + ;; `Mx y` move to x,y absolute + ;; `hx` draw x horizontal relative + ;; `v1` draw 1 vertical relative + ;; `z` close + (format "M%d %dh%dv1h-%dz" + (+ start quiet-zone-size) + (+ r quiet-zone-size) + block-len + block-len)))) + (setq c (1+ c))))))) + (buffer-string))) + +(defun qrencode-as-svg (qr &optional pixel-size inverse) + "Return an svg object of QR code. +See documentation of svg.el for how to use the object. +Optional argument PIXEL-SIZE (default is 3) and INVERSE to flip white/dark mode." + (let* ((size (length qr)) + (quiet-zone-size 4) + (pixel-size (or pixel-size 3)) + (size-wqz (+ quiet-zone-size size quiet-zone-size)) + (width-height (* size-wqz pixel-size)) + (background-colour (if inverse "#000" "#fff")) + (fill-colour (if inverse "#fff" "#000")) + (svgimg (svg-create width-height width-height :viewBox (format "0 0 %d %d" size-wqz size-wqz)))) + (svg-rectangle svgimg 0 0 size-wqz size-wqz :fill background-colour) ;; background + (svg-node svgimg 'path :d (qrencode--svg-path qr quiet-zone-size) :fill fill-colour) + svgimg)) + +(defun qrencode--write-as-svg (filename qr &optional pixel-size) + "Write QR as svg to FILENAME." + (with-temp-file filename + (svg-print (qrencode-as-svg qr pixel-size)) + (insert "\n"))) + (defgroup qrencode nil "QREncode: Encoder for QR Codes." :link '(url-link "https://github.com/ruediger/qrencode-el") @@ -1073,9 +1131,10 @@ bitmap format." P1 is the current default. But P4 export is much faster and produces smaller (binary) files. In the next major release the default will change to P4 and P1 support will be removed." - :type '(choice + :type `(choice (const :tag "NetPBM text (P1)" p1) - (const :tag "NetPBM binary (P4)" p4)) + (const :tag "NetPBM binary (P4)" p4) + ,@(when (featurep 'svg) '((const :tag "SVG" svg)))) :package-version '(qrencode . "1.5-beta4") :group 'qrencode) @@ -1094,6 +1153,8 @@ change to P4 and P1 support will be removed." (error "No raw QRCode data found") (let ((qr qrencode--raw-qr)) ; save ref to buffer local var. (pcase qrencode-export-format + ((and 'svg (guard (featurep 'svg))) + (qrencode--write-as-svg filename qr qrencode-export-pixel-size)) ('p4 (qrencode--write-as-netpbm-p4 filename qr qrencode-export-pixel-size)) ((or 'p1 'nil)