Skip to content

Commit

Permalink
🚧 Printouts page: QR code
Browse files Browse the repository at this point in the history
Why:
- Continuing the migration from SPA to SSR.
- Evaluated multiple QR code generator libraries for Java, before ending
  up using Naoyuki:
  - QRGen https://github.com/kenglxn/QRGen
    - πŸ‘Ž Produces a 20px border around the SVG, no option to remove it
    - πŸ‘Ž Generates very verbose SVG
    - πŸ‘Ž Not available in Maven Central
    - πŸ‘Ž Brings it lots of transitive dependencies
  - Okapi Barcode https://github.com/woo-j/OkapiBarcode
    - πŸ‘Ž Doesn't set the SVG viewbox
    - πŸ‘Ž Generates verbose SVG
  - Nayuki's QR Code generator https://github.com/nayuki/QR-Code-generator
    - πŸ‘ Generates dense SVG
    - πŸ‘Ž Requires hand-writing the image/SVG generator. We had to
      copy-paste and adapt the examples from
      https://github.com/nayuki/QR-Code-generator/blob/master/java/QrCodeGeneratorDemo.java
    - πŸ’‘ The qrcode.react library we've used previously is based on this
    - πŸ‘ Zero dependencies
  - A few other Java and Clojure libraries were found as well, but they
    didn't seem very well maintained.
- To avoid generating lots of QR codes which will never be used, they
  will be cached for some time. Previously they were cached in the
  user's session, but HTMX makes it easy to cache them in the browser's
  request cache.
  • Loading branch information
luontola committed Jul 8, 2024
1 parent 9ae56fe commit a011515
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 7 deletions.
9 changes: 9 additions & 0 deletions .idea/libraries/Leiningen__io_nayuki_qrcodegen_1_8_0.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
[cprop "0.1.20"]
[hiccup "2.0.0-RC3"]
[hikari-cp "3.1.0"]
[io.nayuki/qrcodegen "1.8.0"]
[liberator "0.15.3"]
[medley "1.4.0"]
[metosin/jsonista "0.3.9"]
Expand Down
55 changes: 55 additions & 0 deletions src-java/territory_bro/QrCodeGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright Β© 2015-2024 Esko Luontola
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

package territory_bro;

import io.nayuki.qrcodegen.QrCode;

import java.util.Objects;

public class QrCodeGenerator {

// Adapted from https://github.com/nayuki/QR-Code-generator/blob/master/java/QrCodeGeneratorDemo.java

/**
* Returns a string of SVG code for an image depicting the specified QR Code, with the specified
* number of border modules. The string always uses Unix newlines (\n), regardless of the platform.
*
* @param qr the QR Code to render (not {@code null})
* @param border the number of border modules to add, which must be non-negative
* @param lightColor the color to use for light modules, in any format supported by CSS, not {@code null}
* @param darkColor the color to use for dark modules, in any format supported by CSS, not {@code null}
* @return a string representing the QR Code as an SVG XML document
* @throws NullPointerException if any object is {@code null}
* @throws IllegalArgumentException if the border is negative
*/
public static String toSvgString(QrCode qr, int border, String lightColor, String darkColor) {
Objects.requireNonNull(qr);
Objects.requireNonNull(lightColor);
Objects.requireNonNull(darkColor);
if (border < 0) {
throw new IllegalArgumentException("Border must be non-negative");
}
StringBuilder sb = new StringBuilder()
// .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
// .append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
.append(String.format("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 %1$d %1$d\" stroke=\"none\">\n",
qr.size + border * 2))
.append("\t<rect width=\"100%\" height=\"100%\" fill=\"").append(lightColor).append("\" shape-rendering=\"crispEdges\"/>\n")
.append("\t<path d=\"");
for (int y = 0; y < qr.size; y++) {
for (int x = 0; x < qr.size; x++) {
if (qr.getModule(x, y)) {
if (!(x == 0 && y == 0)) {
sb.append(" ");
}
sb.append(String.format("M%d,%dh1v1h-1z", x + border, y + border));
}
}
}
return sb.append("\" fill=\"").append(darkColor).append("\" shape-rendering=\"crispEdges\"/>\n")
.append("</svg>\n")
.toString();
}
}
1 change: 1 addition & 0 deletions src/territory_bro/api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@
territory-ids (->> (get-in request [:params :territories])
(mapv UUID/fromString))]
(db/with-db [conn {}]
;; TODO: the ::share-cache can be removed after removing the React site, because HTMX uses the browser's cache
(let [share-cache (or (-> request :session ::share-cache)
{})
shares (into [] (for [territory-id territory-ids]
Expand Down
8 changes: 4 additions & 4 deletions src/territory_bro/ui/printout_templates.clj
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@

[:div {:class (:addresses styles)}
[:div {:class (:qrCode styles)}
;; TODO: QR code using lazy loading with caching
[:svg {:height "128" :width "128" :viewBox "0 0 29 29" :style "width: 100%; height: auto;"}
[:path {:fill "#FFFFFF" :d "M0,0 h29v29H0z" :shape-rendering "crispEdges"}]
[:path {:fill "#000000" :d "M0 0h7v1H0zM11 0h1v1H11zM14 0h1v1H14zM18 0h1v1H18zM22,0 h7v1H22zM0 1h1v1H0zM6 1h1v1H6zM8 1h2v1H8zM13 1h1v1H13zM17 1h1v1H17zM22 1h1v1H22zM28,27 h1v1H28zM0 28h7v1H0zM8 28h1v1H8zM10 28h3v1H10zM14 28h2v1H14zM19 28h1v1H19zM24 28h3v1H24z" :shape-rendering "crispEdges"}]]]
[:div {:hx-target "this"
:hx-swap "outerHTML"
:hx-trigger "load"
:hx-get (str html/*page-path* "/qr-code/" (:id territory))}]]
(:addresses territory)]

[:div {:class (:footer styles)} (i18n/t "TerritoryCard.footer")]]))))
29 changes: 26 additions & 3 deletions src/territory_bro/ui/printouts_page.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
(ns territory-bro.ui.printouts-page
(:require [clojure.string :as str]
[hiccup2.core :as h]
[medley.core :refer [dissoc-in]]
[ring.util.http-response :as http-response]
[ring.util.response :as response]
[territory-bro.api :as api]
[territory-bro.gis.geometry :as geometry]
[territory-bro.infra.json :as json]
Expand All @@ -14,8 +17,10 @@
[territory-bro.ui.layout :as layout]
[territory-bro.ui.map-interaction-help :as map-interaction-help]
[territory-bro.ui.printout-templates :as printout-templates])
(:import (java.time LocalDate ZoneId)
(net.greypanther.natsort CaseInsensitiveSimpleNaturalComparator)))
(:import (io.nayuki.qrcodegen QrCode QrCode$Ecc)
(java.time Duration LocalDate ZoneId)
(net.greypanther.natsort CaseInsensitiveSimpleNaturalComparator)
(territory_bro QrCodeGenerator)))

(def templates
[{:id "TerritoryCard"
Expand Down Expand Up @@ -181,6 +186,10 @@
(defn view! [request]
(view (model! request)))

(defn render-qr-code-svg [^String data]
(let [qr-code (QrCode/encodeText data QrCode$Ecc/MEDIUM)]
(QrCodeGenerator/toSvgString qr-code 0 "white" "black")))

(def routes
["/congregation/:congregation/printouts"
{:middleware [[html/wrap-page-path ::page]]}
Expand All @@ -192,4 +201,18 @@
(html/response)))}
:post {:handler (fn [request]
(-> (view! request)
(html/response)))}}]])
(html/response)))}}]

["/qr-code/:territory"
{:get {:handler (fn [request]
(let [territory (get-in request [:params :territory])
response (api/generate-qr-codes (-> request
(dissoc-in [:params :territory])
(assoc-in [:params :territories] [territory])))
share-url (:url (first (:qrCodes (:body response))))]
(when-not (= 200 (:status response))
(http-response/throw! (assoc response :body "")))
(-> (render-qr-code-svg share-url)
(html/response)
;; avoid generating QR codes unnecessarily while the user is tweaking the settings
(response/header "Cache-Control" (str "private, max-age=" (.toSeconds (Duration/ofHours 12)) ", must-revalidate")))))}}]])
1 change: 1 addition & 0 deletions territory-bro.iml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
<orderEntry type="library" name="Leiningen: hikari-cp:3.1.0" level="project" />
<orderEntry type="library" name="Leiningen: instaparse:1.4.8" level="project" />
<orderEntry type="library" name="Leiningen: io.methvin/directory-watcher:0.17.3" level="project" />
<orderEntry type="library" name="Leiningen: io.nayuki/qrcodegen:1.8.0" level="project" />
<orderEntry type="library" name="Leiningen: javax.activation/javax.activation-api:1.2.0" level="project" />
<orderEntry type="library" name="Leiningen: javax.servlet/javax.servlet-api:3.1.0" level="project" />
<orderEntry type="library" name="Leiningen: javax.xml.bind/jaxb-api:2.4.0-b180830.0359" level="project" />
Expand Down
5 changes: 5 additions & 0 deletions web/src/prints/TerritoryCard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,8 @@
/* make the text flow around the image */
shape-outside: inset(calc(100% - var(--qr-code-size)) 0 0 0);
}

.qrCode > svg {
width: 100%;
height: auto;
}

0 comments on commit a011515

Please sign in to comment.