How do I write base64 encoded images to disk?

I’m surprised this question isn’t a little more common. I’m working with a frontend React programmer who is building our iPhone app. He is uploading images to my code (running on the server). He base64 encodes the image and that uploads it to my code. I then have the data in a map of items, and I loop over the items. Each profile can have 4 images. I try to decode the base64 images and then write them to disk.

I write the data to disk, but when I then try to look at the image through the web browser, it is broken. I get an icon suggesting it is broken.

So maybe I’m decoding things wrong? Or is spit converting the binary back to text?

Here is my code, I’m wondering which step I’m getting wrong?

At least for now, all of the test data I have the base64 encoded data starts with:

/9j/

So I assume it is all JPEG.

Actually I know it is JPEG because if I hardcode a data src for a img tag in HTML, it does render correctly, using the encoded base64.

But my attempts to decode and write to disk are failing somehow, the data is getting mangled.

(defn encode [to-encode]
  (.encode (Base64/getEncoder) (.getBytes to-encode)))

(defn decode [to-decode]
  (String. (.decode (Base64/getDecoder) to-decode)))


(defn item-image [item-id image]
  (if (nil? image)
    nil
  (if (= (subs image 0 4) "http")
    image
    (if (= (subs image 0 4) "/9j/")
      (do 
        (spit (str (System/getenv "path_to_images") item-id ".jpeg")  (decode image))
        (str "https://api.left-field.co/images/" item-id ".jpeg"))
      (do 
        (spit (str (System/getenv "path_to_images") item-id ".png")  (decode image))
        (str "https://api.left-field.co/images/" item-id ".png"))))))

      
    
(defn item-images [item]
  (when (map? item)
  (let [
        item (assoc item :image1 (item-image (:item-id item) (:image1 item)))
        item (assoc item :image2 (item-image (:item-id item) (:image2 item)))
        item (assoc item :image3 (item-image (:item-id item) (:image3 item)))
        item (assoc item :image4 (item-image (:item-id item) (:image4 item)))
        ]
    (update-items item))))


(defn item-images-all []
  (try
  (doseq [x (vals @items)]
    (log/log "item-images-all")
    (log/log x)
    (item-images x))
  (catch Exception e (log/log e))))

I’m surprised this question isn’t a little more common.

Because there’s very little to it. It would be akin to asking how to write JSON data to a file - there’s nothing special about JSON data when it’s already serialized into a string, you just write it to a text file and that’s it.

As for the problem, you got a few things wrong:

  1. (String. ...) should be in encode, if anywhere
  2. You can’t use spit because it will convert its argument into a string. And you can’t convert just any random binary data to a string and expect things to work out
  3. Don’t check for /9j/ in the data - there should be a proper Content-Type header when the data is sent (update: I just realized that it can work only if you do (4) below. But it won’t work with b64-encoded data, so maybe the client should send the content type as a part of the body)
  4. If it’s an option, don’t use base64 at all and just use a multipart file upload instead. With proper libraries, you’ll get a byte array in the end, or even a file stored in the location of your choice

Some basic b64 round-trip demonstration that works:

(require '[clojure.java.io :as io])
(import '(java.util Base64 Arrays))

(defn encode [^bytes to-encode]
  (String. (.encode (Base64/getEncoder) to-encode)))

(defn decode [^String to-decode]
  (.decode (Base64/getDecoder) to-decode))

(let [path "/home/p-himik/Pictures/The Egg Hammer.jpg"
      input-data (with-open [in (io/input-stream (io/file path))]
                   (.readAllBytes in))]
  (Arrays/equals ^bytes input-data
                 ^bytes (-> input-data
                            (encode)
                            (decode))))
3 Likes

This was very helpful.