From 360550a6a9c9b25b0acc42323b94de61eb41d997 Mon Sep 17 00:00:00 2001 From: heyarne Date: Thu, 20 Aug 2020 22:30:58 +0200 Subject: [PATCH] Initial commit --- .gitignore | 72 ++++++++++++++++++ deps.edn | 3 + src/heyarne/vanilla_sky/tiptaps.clj | 114 ++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 .gitignore create mode 100644 deps.edn create mode 100644 src/heyarne/vanilla_sky/tiptaps.clj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc4b3e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/clojure,emacs +# Edit at https://www.toptal.com/developers/gitignore?templates=clojure,emacs + +### Clojure ### +pom.xml +pom.xml.asc +*.jar +*.class +/lib/ +/classes/ +/target/ +/checkouts/ +.lein-deps-sum +.lein-repl-history +.lein-plugins/ +.lein-failures +.nrepl-port +.cpcache/ + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +# End of https://www.toptal.com/developers/gitignore/api/clojure,emacs diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..044d9bd --- /dev/null +++ b/deps.edn @@ -0,0 +1,3 @@ +{:deps {clojure2d {:mvn/version "1.4.0-SNAPSHOT"} + enlive {:mvn/version "1.1.6"} + clj-http {:mvn/version "3.10.1"}}} diff --git a/src/heyarne/vanilla_sky/tiptaps.clj b/src/heyarne/vanilla_sky/tiptaps.clj new file mode 100644 index 0000000..3772b40 --- /dev/null +++ b/src/heyarne/vanilla_sky/tiptaps.clj @@ -0,0 +1,114 @@ +(ns heyarne.vanilla-sky.tiptaps) + +;; this file contains the first steps. +;; we want to analyze pictures of cctv cameras. the thing we start with is +;; finding those pictures and loading them so we can manipulate them. + +;; for some reason the generic example (using java.net.URL.) from the enlive +;; tutorial does not work, the pages return a 403 Forbidden, which is why we use +;; clj-http and parse the body directly: + +(require '[clj-http.client :as http]) +(require '[net.cgrand.enlive-html :as html]) + +(defn fetch-url [url] + (future (html/html-snippet (:body (http/get url))))) + +(def some-detail-page + @(fetch-url "https://www.insecam.org/en/view/540433/")) + +(def some-camera-image + (-> + (html/select some-detail-page [:#image0]) + (first) + (html/attr-values :src) + (first))) + +;; cool! so we have the url of a camera image we chose randomly by fair dice +;; roll. we'll eventually have to think of a way to get a good camera image +;; dynamically but we can save that for later. for now let's load the +;; image and see what we can do with it. + +;; with the image we encountered above, server-side push is implemented +;; using the multipart/mixed-replace header. this means that essentially the +;; connection is kept open and as soon as a complete chunk of data is received, +;; a browser would be replacing the currently displayed image with the new one. +;; we're only interested in the first chunk of data, so we need to figure out +;; how we can close the connection afterwards and discard the other ones. + +;; TODO: To read the image the following is done +;; - Find the first boundary +;; - Load all following bytes into a buffer until the buffer appears the next time +;; - Return the buffer + +;; NOTE A more elegant method might be this: +;; - Convert the stream into a lazy sequence of bytes +;; - Partition the lazy sequence whenever you find (str "--" boundary) +;; - Select the part of the sequence you want + +(defn input->byte-seq [input] + (lazy-seq (let [b (.read input)] + ;; -1 marks the end of the stream + (when (not= b -1) + (cons b (input->byte-seq input)))))) + +(comment + ;; Let's test this + (input->byte-seq (java.io.StringReader. "Hello World"))) + +;; This is a helper function we need later. + +(defn find-index + "Returns the index of the first occurence of `el` in `coll` or `nil` if it's + not found." + [el coll] + (first (keep-indexed #(when (= el %2) %1) coll))) + +(defn partition-with-seq + "Partitions `coll` every time `sep` appears. The last item returned is + everything that follows after the last time `sep` was found" + [sep coll] + (lazy-seq + (when (seq coll) + (let [idx (find-index sep (partition (count sep) 1 coll))] + (if idx + (cons (take idx coll) (partition-with-seq sep (drop (+ idx (count sep)) coll))) + (list coll)))))) + +;; if you need a refresher what multipart messages look like: +;; https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + +(defn parse-multipart-alternative [body] + (let [parsed (partition-with-seq (map int [\return \newline \return \newline]) body)] + {:header (apply str (map char (first parsed))) + :body (byte-array (apply concat (rest parsed)))})) + +(defn parse-multipart [request] + (let [content-type (get-in request [:headers "Content-Type"]) + boundary (str "--" (second (re-find #"boundary=\"(.*?)\"" content-type)) "\r\n")] + ;; let's throw in an assert because we have no idea how other servers + ;; implement streaming or wether they implement it at all + (assert (some? boundary) "Could not parse multipart/x-mixed-replace boundary") + (with-open [input (:body request)] + ;; find indices of the bytes between the first and second boundary; the byte + ;; sequence always starts with the boundary, which is why can skip the first + ;; byte and have this find the end index + (let [byte-seq (input->byte-seq input) + boundary-seq (map int boundary)] + ;; the multipart message is prepended by the boundary, so we discard the + ;; first (empty) split + (parse-multipart-alternative (second (partition-with-seq boundary-seq byte-seq))))))) + +(def first-multipart-chunk (parse-multipart (http/get some-camera-image {:as :stream}))) + +;; we need javax to convert the byte array that is contained in the body of the +;; first multipart alternative to a `BufferedImage` that we can use +;; with Clojure2d. + +(defn byte-array->image [bs] + (with-open [in (java.io.ByteArrayInputStream. bs)] + (javax.imageio.ImageIO/read in))) + +(def img (byte-array->image (:body first-multipart-chunk))) + +(require '[clojure2d.core :as c2d])