Logic related to creating image bundles, and some predefined ones. An image bundle contains the data needed to either encode the image inline in a URL (when render-type is :inline), or create the hashes/references needed for an attached image (render-type of :attachment)

(ns metabase.channel.render.image-bundle
  (:require
   [clojure.java.io :as io])
  (:import
   (java.util Arrays)
   (org.apache.commons.io IOUtils)
   (org.fit.cssbox.misc Base64Coder)))
(set! *warn-on-reflection* true)

Generate a hash to be used in a Content-ID

(defn- hash-bytes
  [^bytes img-bytes]
  (Math/abs ^Integer (Arrays/hashCode img-bytes)))

Generate a hash to be used in a Content-ID

(defn- hash-image-url
  [^java.net.URL url]
  (-> url io/input-stream IOUtils/toByteArray hash-bytes))
(defn- content-id-reference [content-id]
  (str "cid:" content-id))
(defn- mb-hash-str [image-hash]
  (str image-hash "@metabase"))
(defn- write-byte-array-to-temp-file
  [^bytes img-bytes]
  (let [f (doto (java.io.File/createTempFile "metabase_channel_image_" ".png")
            .deleteOnExit)]
    (with-open [fos (java.io.FileOutputStream. f)]
      (.write fos img-bytes))
    f))
(defn- byte-array->url [^bytes img-bytes]
  (-> img-bytes write-byte-array-to-temp-file io/as-url))

Takes a PNG byte array and returns a Base64 encoded URI

(defn render-img-data-uri
  [img-bytes]
  (str "data:image/png;base64," (String. (Base64Coder/encode img-bytes))))

Create an image bundle. An image bundle contains the data needed to either encode the image inline (when render-type is :inline), or create the hashes/references needed for an attached image (render-type of :attachment)

(defmulti make-image-bundle
  {:arglists '([render-type url-or-bytes])}
  (fn [render-type url-or-bytes]
    [render-type (class url-or-bytes)]))
(defmethod make-image-bundle [:attachment java.net.URL]
  [render-type, ^java.net.URL url]
  (let [content-id (mb-hash-str (hash-image-url url))]
    {:content-id  content-id
     :image-url   url
     :image-src   (content-id-reference content-id)
     :render-type render-type}))
(defmethod make-image-bundle [:attachment (class (byte-array 0))]
  [render-type image-bytes]
  (let [image-url (byte-array->url image-bytes)
        content-id (mb-hash-str (hash-bytes image-bytes))]
    {:content-id  content-id
     :image-url   image-url
     :image-src   (content-id-reference content-id)
     :render-type render-type}))
(defmethod make-image-bundle [:inline java.net.URL]
  [render-type, ^java.net.URL url]
  {:image-src   (-> url io/input-stream IOUtils/toByteArray render-img-data-uri)
   :image-url   url
   :render-type render-type})
(defmethod make-image-bundle [:inline (Class/forName "[B")]
  [render-type image-bytes]
  {:image-src   (render-img-data-uri image-bytes)
   :render-type render-type})
(def ^:private external-link-url (io/resource "frontend_client/app/assets/img/external_link.png"))
(def ^:private no-results-url    (io/resource "frontend_client/app/assets/img/no_results.png"))
(def ^:private attached-url      (io/resource "frontend_client/app/assets/img/attachment@2x.png"))
(def ^:private external-link-image
  (delay
    (make-image-bundle :attachment external-link-url)))
(def ^:private no-results-image
  (delay
    (make-image-bundle :attachment no-results-url)))
(def ^:private attached-image
  (delay
    (make-image-bundle :attachment attached-url)))

Image bundle for an external link icon.

(defn external-link-image-bundle
  [render-type]
  (case render-type
    :attachment @external-link-image
    :inline     (make-image-bundle render-type external-link-url)))

Image bundle for the 'No results' image.

(defn no-results-image-bundle
  [render-type]
  (case render-type
    :attachment @no-results-image
    :inline     (make-image-bundle render-type no-results-url)))

Image bundle for paperclip 'attachment' image.

(defn attached-image-bundle
  [render-type]
  (case render-type
    :attachment @attached-image
    :inline     (make-image-bundle render-type attached-url)))

Convert an image bundle into an email attachment.

(defn image-bundle->attachment
  [{:keys [render-type content-id image-url]}]
  (case render-type
    :attachment {content-id image-url}
    :inline     nil))