├── fixtures └── images │ ├── .DS_Store │ ├── cloud.png │ └── apartment.jpg ├── resources └── webp-imageio.jar ├── .gitignore ├── test └── fivetonine │ └── collage │ ├── helpers.clj │ ├── util_test.clj │ └── core_test.clj ├── src └── fivetonine │ └── collage │ ├── java │ └── Frame.java │ ├── util.clj │ └── core.clj ├── project.clj ├── README.md ├── LICENSE └── docs └── uberdoc.html /fixtures/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/collage/master/fixtures/images/.DS_Store -------------------------------------------------------------------------------- /fixtures/images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/collage/master/fixtures/images/cloud.png -------------------------------------------------------------------------------- /resources/webp-imageio.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/collage/master/resources/webp-imageio.jar -------------------------------------------------------------------------------- /fixtures/images/apartment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/collage/master/fixtures/images/apartment.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | /native 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | !webp-imageio.jar 9 | *.class 10 | /.lein-* 11 | /.nrepl-port 12 | -------------------------------------------------------------------------------- /test/fivetonine/collage/helpers.clj: -------------------------------------------------------------------------------- 1 | (ns fivetonine.collage.helpers 2 | (:import java.awt.image.BufferedImage)) 3 | 4 | (defn pixel-at 5 | [image x y] 6 | (-> image .getData (.getPixel x y (ints nil)) seq)) 7 | 8 | (defn buf-img 9 | ([w] (BufferedImage. w w BufferedImage/TYPE_INT_ARGB)) 10 | ([w h] (BufferedImage. w h BufferedImage/TYPE_INT_ARGB)) 11 | ([w h t] (BufferedImage. w h t))) 12 | -------------------------------------------------------------------------------- /src/fivetonine/collage/java/Frame.java: -------------------------------------------------------------------------------- 1 | package fivetonine.collage; 2 | 3 | import java.awt.image.BufferedImage; 4 | import javax.swing.JFrame; 5 | import javax.swing.JLabel; 6 | import javax.swing.ImageIcon; 7 | 8 | public class Frame { 9 | public static JFrame createImageFrame(String title, BufferedImage image) { 10 | JFrame frame = new JFrame(title); 11 | ImageIcon icon = new ImageIcon(image); 12 | JLabel imageLabel = new JLabel(icon); 13 | 14 | frame.add(imageLabel); 15 | frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 16 | frame.pack(); 17 | frame.show(); 18 | return frame; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject fivetonine/collage "0.1.0" 2 | :description "Clean, minimal image processing library for Clojure" 3 | :url "https://github.com/karls/collage" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.5.1"]] 7 | :plugins [[lein-marginalia "0.7.1"]] 8 | 9 | ;; :global-vars { *warn-on-reflection* true } 10 | 11 | ;; WebP support 12 | :resource-paths ["resources" "resources/webp-imageio.jar"] 13 | :java-source-paths ["src/fivetonine/collage/java"] 14 | 15 | ;; In order to actually use the WebP format, the JVM uses native code that 16 | ;; needs to be compiled by the user. The JVM loads the native code from the 17 | ;; native library path, which is set here. This may be overridden when 18 | ;; starting the JVM with -Djava.library.path=/your/custom/path/. 19 | ;; See the README for instructions on how to compile the native code. 20 | :jvm-opts [~(str "-Djava.library.path=native/:" (System/getenv "LD_LIBRARY_PATH"))]) 21 | -------------------------------------------------------------------------------- /test/fivetonine/collage/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns fivetonine.collage.util-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.io :refer [as-file]] 4 | [fivetonine.collage.util :refer :all] 5 | [fivetonine.collage.helpers :refer :all]) 6 | (:import java.io.File 7 | java.awt.image.BufferedImage 8 | java.awt.Color 9 | javax.imageio.ImageIO 10 | javax.swing.JFrame 11 | java.net.URI)) 12 | 13 | (def test-image-paths 14 | {"fixtures/images/cloud.png" "fixtures/images/cloud-new.png" 15 | "fixtures/images/apartment.jpg" "fixtures/images/apartment-new.jpg"}) 16 | 17 | (defn cleanup-images [] 18 | (doseq [path (vals test-image-paths)] 19 | (let [file (File. path)] 20 | (when (.isFile file) 21 | (.delete file))))) 22 | 23 | (deftest load-image-test 24 | (let [path (first (keys test-image-paths))] 25 | (are [image] (instance? BufferedImage image) 26 | (load-image path) 27 | (load-image (as-file path)) 28 | (load-image (ImageIO/read (as-file path)))))) 29 | 30 | (deftest sanitize-path-test 31 | (is (instance? URI (sanitize-path "file:///path/to/some/image.png"))) 32 | (is (instance? URI (sanitize-path (first (keys test-image-paths))))) 33 | (is (thrown? Exception (sanitize-path "http://www.foobar.com/image.png")))) 34 | 35 | (deftest save-test 36 | (testing "default options" 37 | (doseq [[path new-path] (seq test-image-paths)] 38 | (let [image (load-image path) 39 | saved-image (save image new-path)] 40 | (is (.exists (File. saved-image))))) 41 | (cleanup-images)) 42 | 43 | (testing "with explicit quality coefficient")) 44 | 45 | (deftest copy-test 46 | (testing "creating a new object" 47 | (let [image (BufferedImage. 1 1 BufferedImage/TYPE_INT_ARGB)] 48 | (is (not (= image (copy image)))))) 49 | 50 | (testing "changing old image doesn't change new image" 51 | (let [image1 (BufferedImage. 1 1 BufferedImage/TYPE_INT_RGB) 52 | graphics1 (.createGraphics image1) 53 | image2 (copy image1) 54 | graphics2 (.createGraphics image2)] 55 | (doto graphics1 56 | (.setPaint (Color/yellow)) 57 | (.fillRect 0 0 1 1) 58 | (.dispose)) 59 | (is (not (= (pixel-at image1 0 0) (pixel-at image2 0 0))))))) 60 | -------------------------------------------------------------------------------- /src/fivetonine/collage/util.clj: -------------------------------------------------------------------------------- 1 | (ns fivetonine.collage.util 2 | (:require [clojure.java.io :refer [as-file file]]) 3 | (:import java.io.File 4 | java.net.URI 5 | java.awt.image.BufferedImage 6 | javax.imageio.ImageIO 7 | javax.imageio.IIOImage 8 | javax.imageio.ImageWriter 9 | javax.imageio.ImageWriteParam 10 | fivetonine.collage.Frame)) 11 | 12 | (declare parse-extension) 13 | 14 | (defn show 15 | "Display an image in a `JFrame`. 16 | 17 | Convenience function for viewing an image quickly." 18 | [^BufferedImage image] 19 | (Frame/createImageFrame "Quickview" image)) 20 | 21 | (defn copy 22 | "Make a deep copy of an image." 23 | [image] 24 | (let [width (.getWidth image) 25 | height (.getHeight image) 26 | type (.getType image) 27 | new-image (BufferedImage. width height type)] 28 | ;; Get data from image and set data in new-image, resulting in a copy 29 | ;; This also works for BufferedImages that are obtained by calling 30 | ;; .getSubimage on another BufferedImage. 31 | (.setData new-image (.getData image)) 32 | new-image)) 33 | 34 | (defn save 35 | "Store an image on disk. 36 | 37 | Returns the path to the saved image when saved successfully." 38 | [^BufferedImage image path & rest] 39 | (let [opts (apply hash-map rest) 40 | outfile (file path) 41 | ext (parse-extension path) 42 | ^ImageWriter writer (.next (ImageIO/getImageWritersByFormatName ext)) 43 | ^ImageWriteParam write-param (.getDefaultWriteParam writer) 44 | iioimage (IIOImage. image nil nil) 45 | outstream (ImageIO/createImageOutputStream outfile)] 46 | ; Only compress images that can be compressed. PNGs, for example, cannot be 47 | ; compressed. 48 | (when (.canWriteCompressed write-param) 49 | (doto write-param 50 | (.setCompressionMode ImageWriteParam/MODE_EXPLICIT) 51 | (.setCompressionQuality (get opts :quality 0.8)))) 52 | (doto writer 53 | (.setOutput outstream) 54 | (.write nil iioimage write-param) 55 | (.dispose)) 56 | (.close outstream) 57 | path)) 58 | 59 | (defprotocol ImageResource 60 | "Coerce different image resource representations to BufferedImage." 61 | (as-image [x] "Coerce argument to an image.")) 62 | 63 | (extend-protocol ImageResource 64 | String 65 | (as-image [s] (ImageIO/read (as-file s))) 66 | 67 | File 68 | (as-image [f] (ImageIO/read f)) 69 | 70 | BufferedImage 71 | (as-image [b] b)) 72 | 73 | (defn ^BufferedImage load-image 74 | "Loads an image from resource." 75 | [resource] 76 | (as-image resource)) 77 | 78 | ;; ## Helpers & experimental 79 | 80 | (defn parse-extension 81 | "Parses the image extension from the path." 82 | [path] 83 | (last (clojure.string/split path #"\."))) 84 | 85 | (defn sanitize-path 86 | "Sanitizes a path. 87 | Returns the sanitized path, or throws if sanitization is not possible." 88 | [path] 89 | (when-let [scheme (-> path URI. .getScheme)] 90 | (if (not (= "file" scheme)) 91 | (throw (Exception. "Path must point to a local file.")) 92 | (URI. path))) 93 | (URI. (str "file://" path))) 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collage 2 | 3 | Collage is a simple-to-use image processing library for Clojure. It's intended 4 | to be a drop-in solution for high-level image processing needs. It draws some 5 | inspiration from Python's [PIL](http://effbot.org/imagingbook/pil-index.htm) and 6 | is somewhat similar to mikera's [imagez](https://github.com/mikera/imagez). 7 | 8 | ### Motivation 9 | Collage grew out of my own need when I was writing another Clojure application. 10 | I wanted to have some high-level image processing functionality available - 11 | functions into which I could just pass some `BufferedImages` and get back 12 | transformed `BufferedImages`. At the time, I resorted to doing Java interop, 13 | which is nice, but Clojure is nicer. I also wanted to get more experience with 14 | Clojure in general. 15 | 16 | ### Project goals 17 | * Ease of use 18 | * Well tested-ness 19 | * Clean, composable internal API 20 | * Learn (more idiomatic) Clojure 21 | 22 | Documentation generated by Marginalia is available on 23 | [Github pages](http://karls.github.io/collage/). 24 | 25 | Available on [Clojars](https://clojars.org/fivetonine/collage). 26 | 27 | ## Usage 28 | 29 | #### Using the `with-image` macro. 30 | ```clj 31 | (:require [fivetonine.collage.core :refer :all]) 32 | 33 | (with-image "/path/to/image.jpg" 34 | (resize :width 1000) 35 | (rotate 90) 36 | (save :quality 0.85)) 37 | ``` 38 | 39 | Loads the image at `/path/to/image.jpg`, resizes it to have width of 1000px 40 | (height is computed automatically), rotates 90 degrees clockwise and saves it 41 | with 85% quality of the original, overwriting the original. 42 | 43 | ```clj 44 | (:require [fivetonine.collage.util :as util]) 45 | (:require [fivetonine.collage.core :refer :all]) 46 | 47 | (def image (util/load-image "/path/to/image.jpg")) 48 | (with-image image 49 | (crop 100 50 200 100) 50 | (save "/path/to/new-image.jpg" :quality 1.0)) 51 | ``` 52 | 53 | Loads an image at `/path/to/image.jpg`. With that image, crops a 200px by 100px 54 | image out of the original, at the point (100,50) in the original image, saves 55 | it with 100% quality at `/path/to/new-image.jpg`. 56 | 57 | #### Vanilla functions. 58 | ```clj 59 | (:require [fivetonine.collage.core :refer :all]) 60 | 61 | (def image (load-image "/path/to/image.png")) 62 | (save (flip image :horizontal)) 63 | ``` 64 | 65 | ## WebP support 66 | 67 | Collage does not provide support for WebP out of the box. Collage includes all 68 | the JVM `ImageIO` API plumbing (in resources/webp-imageio.jar), but 69 | the native binaries are not provided. Note that the `webp` format is reported as 70 | a supported format with `ImageIO.getReaderFormatNames()`. But when trying 71 | to load a `.webp` image, an exception is thrown as the native binary for 72 | actually loading the raw data in is missing. 73 | 74 | The main reason for not providing binaries is that I don't want to 75 | build and maintain all the versions of all the binaries for all the platforms. 76 | 77 | Compiling the binary is fairly straightforward. Some instructions are available 78 | in the luciad's [webp-imageio](https://bitbucket.org/luciad/webp-imageio) repo. 79 | This repo includes code that is used in Collage to provide support for WebP. 80 | I also compiled the native binary that WebP depends on using the instructions 81 | in the same repo. 82 | 83 | If you run into problems compiling the binary or getting all the pieces working 84 | together, please open an issue and describe the situation. 85 | 86 | WebP support is currently tested only on Mac OSX, but it *should* work on other 87 | platforms as well. 88 | 89 | ## Contributing 90 | 91 | Contributions, suggestions and friendly criticism are all welcome. 92 | 93 | ## License 94 | 95 | Copyright © 2013 Karl Sutt 96 | 97 | Distributed under the Eclipse Public License either version 1.0. 98 | -------------------------------------------------------------------------------- /test/fivetonine/collage/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns fivetonine.collage.core-test 2 | (:require [clojure.test :refer :all] 3 | [fivetonine.collage.core :refer :all] 4 | [fivetonine.collage.helpers :refer :all]) 5 | (:import java.awt.image.BufferedImage 6 | java.awt.Color)) 7 | 8 | (deftest scale-test 9 | (let [image (buf-img 100)] 10 | (testing "scale factor < 1.0" 11 | (let [scaled (scale image 0.6)] 12 | (is (= 60 (.getWidth scaled))) 13 | (is (= 60 (.getHeight scaled))))) 14 | 15 | (testing "scale factor > 1.0" 16 | (let [scaled (scale image 1.5)] 17 | (is (= 150 (.getWidth scaled))) 18 | (is (= 150 (.getHeight scaled))))) 19 | 20 | (testing "scale factor = 1.0" 21 | (let [scaled (scale image 1.0)] 22 | (is (= 100 (.getWidth scaled))) 23 | (is (= 100 (.getHeight scaled))))))) 24 | 25 | (deftest crop-test 26 | (let [image (buf-img 100)] 27 | (is (= 50 (.getWidth (crop image 0 0 50 10)))) 28 | (is (= 10 (.getHeight (crop image 0 0 50 10)))) 29 | (is (= 15 (.getWidth (crop image 50 90 15 5)))) 30 | (is (= 5 (.getHeight (crop image 50 90 15 5)))) 31 | ;; going out of bounds 32 | (is (thrown? Exception (crop image 90 0 11 10))) 33 | (is (thrown? Exception (crop image 0 90 10 11))))) 34 | 35 | (deftest resize-test 36 | (let [image (buf-img 800 600)] 37 | (testing "no width and height provided" 38 | (is (thrown? IllegalArgumentException (resize image)))) 39 | 40 | (testing "with width provided" 41 | (let [resized (resize image :width 400)] 42 | (is (= 400 (.getWidth resized))) 43 | (is (= 300 (.getHeight resized))))) 44 | 45 | (testing "with height provided" 46 | (let [resized (resize image :height 300)] 47 | (is (= 400 (.getWidth resized))) 48 | (is (= 300 (.getHeight resized))))) 49 | 50 | (testing "with width and height both provided" 51 | (let [resized (resize image :width 50 :height 30)] 52 | (is (= (.getWidth resized) 50)) 53 | (is (= (.getHeight resized) 30)))))) 54 | 55 | (deftest resize*-test 56 | (let [image (buf-img 100) 57 | resized (resize image :width 50 :height 30)] 58 | (is (= (.getWidth resized) 50)) 59 | (is (= (.getHeight resized) 30)))) 60 | 61 | (deftest paste-test 62 | (let [image (buf-img 10 10)] 63 | (testing "wrong number of layer args" 64 | ;; the values of arguments don't matter in the layer vector, as they're 65 | ;; checked only when it's certain that there is a correct number of them 66 | (is (thrown? IllegalArgumentException (paste image [:foo :bar]))) 67 | (is (thrown? IllegalArgumentException (paste image [:foo :bar :baz :qux])))) 68 | 69 | (testing "no layers" 70 | (is (instance? BufferedImage (paste image []))) 71 | (is (not (= image (paste image []))))) 72 | 73 | (testing "some layers" 74 | (let [layer1 (buf-img 2) 75 | graphics1 (.createGraphics layer1) 76 | layer2 (buf-img 2) 77 | graphics2 (.createGraphics layer2)] 78 | 79 | (is (instance? BufferedImage (paste image [layer1 0 0]))) 80 | (is (instance? BufferedImage (paste image [layer1 0 0 layer2 5 5]))) 81 | (is (not (= image (paste image [layer1 0 0 layer2 5 5])))) 82 | 83 | (doto graphics1 84 | (.setPaint (Color/yellow)) 85 | (.fillRect 0 0 2 2) 86 | (.dispose)) 87 | (doto graphics2 88 | (.setPaint (Color/green)) 89 | (.fillRect 0 0 2 2) 90 | (.dispose)) 91 | 92 | (let [pasted (paste image [layer1 0 0 layer2 9 9])] 93 | (is (= (pixel-at pasted 9 0) (pixel-at pasted 0 9))) 94 | (is (not (= (pixel-at pasted 0 0) (pixel-at pasted 9 9)))) 95 | (is (not (= (pixel-at pasted 0 0) (pixel-at pasted 0 9)))) 96 | (is (not (= (pixel-at pasted 0 9) (pixel-at pasted 9 9))))))))) 97 | 98 | (deftest paste*-test 99 | (let [image (buf-img 10) 100 | layer (buf-img 5) 101 | layer-graphics (.createGraphics layer)] 102 | (doto layer-graphics 103 | (.setPaint (Color/yellow)) 104 | (.fillRect 0 0 5 5) 105 | (.dispose)) 106 | (let [pasted (paste* image [layer 0 0])] 107 | (testing "applying a layer" 108 | (is (= (pixel-at pasted 0 0) (pixel-at pasted 4 0))) 109 | (is (not (= (pixel-at pasted 0 0) (pixel-at pasted 5 0)))))))) 110 | 111 | (deftest rotate-test 112 | (let [dimensions [640 480] 113 | image (buf-img (dimensions 0) (dimensions 1)) 114 | new-dims #(let [r (rotate image %1)] [(.getWidth r) (.getHeight r)])] 115 | (are [theta dim] (= dim (new-dims theta)) 116 | 0 dimensions 117 | 90 (reverse dimensions) 118 | 180 dimensions 119 | 270 (reverse dimensions) 120 | 360 dimensions 121 | 450 (reverse dimensions) 122 | -90 (reverse dimensions) 123 | -180 dimensions 124 | -270 (reverse dimensions) 125 | -360 dimensions 126 | -450 (reverse dimensions)))) 127 | -------------------------------------------------------------------------------- /src/fivetonine/collage/core.clj: -------------------------------------------------------------------------------- 1 | ;; ## Drop in library for (some of) your image processing needs. 2 | ;; 3 | ;; Collage was developed out of my own need to answer some specific needs in a 4 | ;; project I was working on. Even though there are a couple of other libraries 5 | ;; out there (mikera's [imagez](https://github.com/mikera/imagez), which builds 6 | ;; on [imgscalr](https://github.com/thebuzzmedia/imgscalr), a Java library), 7 | ;; but I felt like implementing my own in order to gain more experience with 8 | ;; Clojure. 9 | ;; 10 | ;; The feature-set is somewhat similar to the previously mentioned libraries, 11 | ;; adding functionality to paste layers (regular `BufferedImages`) onto an image 12 | ;; and controlling the quality of the image when saving it to disk. 13 | ;; 14 | ;; ### This project aims to 15 | ;; * Be an easy to use, drop-in solution 16 | ;; * Have a composable internal API 17 | ;; * Be well tested 18 | ;; * Be reasonably idiomatic 19 | ;; 20 | (ns fivetonine.collage.core 21 | (:require [fivetonine.collage.util :as util]) 22 | (:import java.awt.image.BufferedImage) 23 | (:import java.awt.geom.AffineTransform) 24 | (:import java.awt.RenderingHints)) 25 | 26 | (declare resize*) 27 | (declare paste*) 28 | (declare normalise-angle) 29 | (declare pi-rotation?) 30 | 31 | (def not-nil? (complement nil?)) 32 | 33 | ;; ## Core functions 34 | 35 | (defn rotate 36 | "Rotates image through angle `theta`, where `theta` is 37 | an integer multiple of 90. 38 | 39 | If `theta > 0`, the image is rotated clockwise. 40 | 41 | If `theta < 0`, the image is rotated anticlockwise." 42 | [image theta] 43 | (when-not (contains? (set (range -360 450 90)) (normalise-angle theta)) 44 | (throw (IllegalArgumentException. 45 | "theta has to be an integer multiple of 90."))) 46 | (let [old-width (.getWidth image) 47 | old-height (.getHeight image) 48 | new-width (if (pi-rotation? theta) old-width old-height) 49 | new-height (if (pi-rotation? theta) old-height old-width) 50 | angle (Math/toRadians theta) 51 | new-image (BufferedImage. new-width new-height (.getType image)) 52 | graphics (.createGraphics new-image) 53 | transform (AffineTransform.)] 54 | 55 | ;; Given that the rotation happens around the point (0,0) (the top left hand 56 | ;; corner of the image), the resulting image needs to be translated back 57 | ;; into the "viewport". 58 | (condp = (Math/abs (normalise-angle theta)) 59 | 0 (.translate transform 0 0) 60 | 90 (.translate transform new-width 0) 61 | 180 (.translate transform new-width new-height) 62 | 270 (.translate transform 0 new-height) 63 | 360 (.translate transform 0 0)) 64 | (.rotate transform angle) 65 | 66 | (doto graphics 67 | (.drawImage image transform nil) 68 | (.dispose)) 69 | new-image)) 70 | 71 | (defn flip 72 | "Flips an image. 73 | 74 | If direction is `:horizontal`, flips the image around the y-axis. 75 | 76 | If direction is `:vertical`, flips the image around the x-axis." 77 | [image direction] 78 | (let [width (.getWidth image) 79 | height (.getHeight image) 80 | new-image (BufferedImage. width height (.getType image)) 81 | graphics (.createGraphics new-image) 82 | transform (AffineTransform.)] 83 | (case direction 84 | :horizontal (doto transform 85 | (.translate width 0) 86 | (.scale -1 1)) 87 | :vertical (doto transform 88 | (.translate 0 height) 89 | (.scale 1 -1))) 90 | 91 | (doto graphics 92 | (.drawImage image transform nil) 93 | (.dispose)) 94 | 95 | new-image)) 96 | 97 | (defn scale 98 | "Scales an image by a factor `f`. 99 | 100 | If `0.0 < f < 1.0` the image is scaled down. 101 | 102 | If `f > 1.0` the image is scaled up." 103 | [image f] 104 | (resize* image 105 | (* f (-> image .getWidth int)) 106 | (* f (-> image .getHeight int)))) 107 | 108 | (defn crop 109 | "Crops an image. 110 | 111 | `x, y` are the coordinates to top left corner of the area to crop. 112 | `width, height` are the width and height of the area to crop. 113 | 114 | The returned image does not share its data with the original image." 115 | [image x y width height] 116 | (-> image (.getSubimage x y width height) util/copy)) 117 | 118 | (defn resize 119 | "Resizes an image. 120 | 121 | If only `width` or `height` is provided, the resulting image will be `width` 122 | or `height` px wide, respectively. The other dimension will be calculated 123 | automatically to preserve `width/height` ratio. 124 | 125 | With `width` and `height` both provided, the resulting image will be crudely 126 | resized to match the provided values. 127 | 128 | If neither `width` nor `height` are provided, `IllegalArgumentException` is 129 | thrown. 130 | 131 | Examples: 132 | 133 | (resize image :width 100) 134 | (resize image :height 300) 135 | (resize image :width 100 :height 300)" 136 | [image & {:keys [width height] :as opts}] 137 | (let [supported #{:width :height} 138 | options (select-keys opts supported) 139 | width (options :width) 140 | height (options :height)] 141 | (when (empty? options) 142 | (throw (IllegalArgumentException. 143 | "Width or height (or both) has to be provided."))) 144 | (cond 145 | (and width height) (resize* image (int width) (int height)) 146 | 147 | (not-nil? width) 148 | (let [new-height (* (/ (int width) (.getWidth image)) (.getHeight image))] 149 | (resize* image width (int new-height))) 150 | 151 | (not-nil? height) 152 | (let [new-width (* (/ (int height) (.getHeight image)) (.getWidth image))] 153 | (resize* image (int new-width) height))))) 154 | 155 | (defn resize* 156 | "Resize the given image to `width` and `height`. 157 | Used as an internal function by `resize`. 158 | 159 | Note: the method of resizing may change in the future as there are better, 160 | iterative, solutions to balancing speed vs. quality. See 161 | [the perils of Image.getScaledInstance()](https://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html)." 162 | [image width height] 163 | (let [new-image (BufferedImage. width height (.getType image)) 164 | graphics (.createGraphics new-image)] 165 | (doto graphics 166 | (.setRenderingHint RenderingHints/KEY_INTERPOLATION 167 | RenderingHints/VALUE_INTERPOLATION_BICUBIC) 168 | (.drawImage image 0 0 width height nil) 169 | .dispose) 170 | new-image)) 171 | 172 | (defn paste 173 | "Pastes layer(s) onto image at coordinates `x` and `y`. 174 | 175 | `layer-defs` is expected to in the format 176 | 177 | [layer1 x1 y2 layer2 x2 y2 ... ] 178 | 179 | Layers are loaded using `fivetonine.collage.util/load-image`. 180 | Top left corner of a layer will be at `x, y`. 181 | 182 | Throws `IllegalArgumentException` if the number of elements in the list of 183 | layers and coordinates is not divisible by 3. 184 | 185 | Returns the resulting image." 186 | [image & layer-defs] 187 | (let [args (flatten (seq layer-defs))] 188 | (when-not (= 0 (-> args count (rem 3))) 189 | (throw (IllegalArgumentException. 190 | "Expected layer-defs format [image1 x1 y1 image2 x2 y2 ... ]."))) 191 | (let [new-image (util/copy image) 192 | layers (partition 3 args)] 193 | ;; "reduce" all the layers onto new-image using paste* -- the layers are 194 | ;; accumulated onto the image 195 | (reduce paste* new-image layers) 196 | new-image))) 197 | 198 | (defn paste* 199 | "Paste layer on top of base at position `x, y` and return the resulting image. 200 | Used as an internal function by `paste`." 201 | [base [layer x y]] 202 | (let [graphics (.createGraphics base)] 203 | (doto graphics 204 | (.setRenderingHint RenderingHints/KEY_RENDERING 205 | RenderingHints/VALUE_RENDER_QUALITY) 206 | (.setRenderingHint RenderingHints/KEY_COLOR_RENDERING 207 | RenderingHints/VALUE_COLOR_RENDER_QUALITY) 208 | (.drawImage (util/load-image layer) x y nil) 209 | (.dispose)) 210 | base)) 211 | 212 | (defmacro with-image 213 | "A helper for applying multiple operations to an image. 214 | 215 | `image-resource` can be a `String`, a `File` or a `BufferedImage`. 216 | 217 | Example: 218 | 219 | (with-image \"/path/to/image.jpg\" 220 | (scale 0.8) 221 | (rotate 90) 222 | (crop 0 0 100 100)) 223 | 224 | Expands to (properly namespaced): 225 | 226 | (let [image__2336__auto__ (load-image \"/path/to/image.jpg\")] 227 | (clojure.core/-> image__2336__auto__ 228 | (scale 0.8) 229 | (rotate 90) 230 | (crop 0 0 100 100))) 231 | 232 | Returns the image which is the result of applying all operations to the input 233 | image." 234 | [image-resource & operations] 235 | `(let [image# (util/load-image ~image-resource)] 236 | (-> image# ~@operations))) 237 | 238 | ;; ## Helpers 239 | 240 | (defn- pi-rotation? 241 | "Does the rotation through angle `theta` correspond to a rotation that is 242 | a multiple of 180 degrees, meaning that the image preserves the original 243 | width and height." 244 | [theta] 245 | (-> theta (/ 90) (rem 2) (= 0))) 246 | 247 | (defn normalise-angle 248 | "Restrict the rotation angle to the range [-360..360]." 249 | [theta] 250 | (rem theta 360)) 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' from 19 | a Contributor if it was added to the Program by such Contributor itself or 20 | anyone acting on such Contributor's behalf. Contributions do not include 21 | additions to the Program which: (i) are separate modules of software 22 | distributed in conjunction with the Program under their own license 23 | agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement, 34 | including all Contributors. 35 | 36 | 2. GRANT OF RIGHTS 37 | a) Subject to the terms of this Agreement, each Contributor hereby grants 38 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 39 | reproduce, prepare derivative works of, publicly display, publicly perform, 40 | distribute and sublicense the Contribution of such Contributor, if any, and 41 | such derivative works, in source code and object code form. 42 | b) Subject to the terms of this Agreement, each Contributor hereby grants 43 | Recipient a non-exclusive, worldwide, royalty-free patent license under 44 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 45 | transfer the Contribution of such Contributor, if any, in source code and 46 | object code form. This patent license shall apply to the combination of the 47 | Contribution and the Program if, at the time the Contribution is added by 48 | the Contributor, such addition of the Contribution causes such combination 49 | to be covered by the Licensed Patents. The patent license shall not apply 50 | to any other combinations which include the Contribution. No hardware per 51 | se is licensed hereunder. 52 | c) Recipient understands that although each Contributor grants the licenses to 53 | its Contributions set forth herein, no assurances are provided by any 54 | Contributor that the Program does not infringe the patent or other 55 | intellectual property rights of any other entity. Each Contributor 56 | disclaims any liability to Recipient for claims brought by any other entity 57 | based on infringement of intellectual property rights or otherwise. As a 58 | condition to exercising the rights and licenses granted hereunder, each 59 | Recipient hereby assumes sole responsibility to secure any other 60 | intellectual property rights needed, if any. For example, if a third party 61 | patent license is required to allow Recipient to distribute the Program, it 62 | is Recipient's responsibility to acquire that license before distributing 63 | the Program. 64 | d) Each Contributor represents that to its knowledge it has sufficient 65 | copyright rights in its Contribution, if any, to grant the copyright 66 | license set forth in this Agreement. 67 | 68 | 3. REQUIREMENTS 69 | 70 | A Contributor may choose to distribute the Program in object code form under its 71 | own license agreement, provided that: 72 | 73 | a) it complies with the terms and conditions of this Agreement; and 74 | b) its license agreement: 75 | i) effectively disclaims on behalf of all Contributors all warranties and 76 | conditions, express and implied, including warranties or conditions of 77 | title and non-infringement, and implied warranties or conditions of 78 | merchantability and fitness for a particular purpose; 79 | ii) effectively excludes on behalf of all Contributors all liability for 80 | damages, including direct, indirect, special, incidental and 81 | consequential damages, such as lost profits; 82 | iii) states that any provisions which differ from this Agreement are offered 83 | by that Contributor alone and not by any other party; and 84 | iv) states that source code for the Program is available from such 85 | Contributor, and informs licensees how to obtain it in a reasonable 86 | manner on or through a medium customarily used for software exchange. 87 | 88 | When the Program is made available in source code form: 89 | 90 | a) it must be made available under this Agreement; and 91 | b) a copy of this Agreement must be included with each copy of the Program. 92 | Contributors may not remove or alter any copyright notices contained within 93 | the Program. 94 | 95 | Each Contributor must identify itself as the originator of its Contribution, if 96 | any, in a manner that reasonably allows subsequent Recipients to identify the 97 | originator of the Contribution. 98 | 99 | 4. COMMERCIAL DISTRIBUTION 100 | 101 | Commercial distributors of software may accept certain responsibilities with 102 | respect to end users, business partners and the like. While this license is 103 | intended to facilitate the commercial use of the Program, the Contributor who 104 | includes the Program in a commercial product offering should do so in a manner 105 | which does not create potential liability for other Contributors. Therefore, if 106 | a Contributor includes the Program in a commercial product offering, such 107 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 108 | every other Contributor ("Indemnified Contributor") against any losses, damages 109 | and costs (collectively "Losses") arising from claims, lawsuits and other legal 110 | actions brought by a third party against the Indemnified Contributor to the 111 | extent caused by the acts or omissions of such Commercial Contributor in 112 | connection with its distribution of the Program in a commercial product 113 | offering. The obligations in this section do not apply to any claims or Losses 114 | relating to any actual or alleged intellectual property infringement. In order 115 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 116 | Contributor in writing of such claim, and b) allow the Commercial Contributor to 117 | control, and cooperate with the Commercial Contributor in, the defense and any 118 | related settlement negotiations. The Indemnified Contributor may participate in 119 | any such claim at its own expense. 120 | 121 | For example, a Contributor might include the Program in a commercial product 122 | offering, Product X. That Contributor is then a Commercial Contributor. If that 123 | Commercial Contributor then makes performance claims, or offers warranties 124 | related to Product X, those performance claims and warranties are such 125 | Commercial Contributor's responsibility alone. Under this section, the 126 | Commercial Contributor would have to defend claims against the other 127 | Contributors related to those performance claims and warranties, and if a court 128 | requires any other Contributor to pay any damages as a result, the Commercial 129 | Contributor must pay those damages. 130 | 131 | 5. NO WARRANTY 132 | 133 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 134 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 135 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 136 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 137 | Recipient is solely responsible for determining the appropriateness of using and 138 | distributing the Program and assumes all risks associated with its exercise of 139 | rights under this Agreement , including but not limited to the risks and costs 140 | of program errors, compliance with applicable laws, damage to or loss of data, 141 | programs or equipment, and unavailability or interruption of operations. 142 | 143 | 6. DISCLAIMER OF LIABILITY 144 | 145 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 146 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 147 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 148 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 149 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 150 | OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS 151 | GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 152 | 153 | 7. GENERAL 154 | 155 | If any provision of this Agreement is invalid or unenforceable under applicable 156 | law, it shall not affect the validity or enforceability of the remainder of the 157 | terms of this Agreement, and without further action by the parties hereto, such 158 | provision shall be reformed to the minimum extent necessary to make such 159 | provision valid and enforceable. 160 | 161 | If Recipient institutes patent litigation against any entity (including a 162 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 163 | (excluding combinations of the Program with other software or hardware) 164 | infringes such Recipient's patent(s), then such Recipient's rights granted under 165 | Section 2(b) shall terminate as of the date such litigation is filed. 166 | 167 | All Recipient's rights under this Agreement shall terminate if it fails to 168 | comply with any of the material terms or conditions of this Agreement and does 169 | not cure such failure in a reasonable period of time after becoming aware of 170 | such noncompliance. If all Recipient's rights under this Agreement terminate, 171 | Recipient agrees to cease use and distribution of the Program as soon as 172 | reasonably practicable. However, Recipient's obligations under this Agreement 173 | and any licenses granted by Recipient relating to the Program shall continue and 174 | survive. 175 | 176 | Everyone is permitted to copy and distribute copies of this Agreement, but in 177 | order to avoid inconsistency the Agreement is copyrighted and may only be 178 | modified in the following manner. The Agreement Steward reserves the right to 179 | publish new versions (including revisions) of this Agreement from time to time. 180 | No one other than the Agreement Steward has the right to modify this Agreement. 181 | The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation 182 | may assign the responsibility to serve as the Agreement Steward to a suitable 183 | separate entity. Each new version of the Agreement will be given a 184 | distinguishing version number. The Program (including Contributions) may always 185 | be distributed subject to the version of the Agreement under which it was 186 | received. In addition, after a new version of the Agreement is published, 187 | Contributor may elect to distribute the Program (including its Contributions) 188 | under the new version. Except as expressly stated in Sections 2(a) and 2(b) 189 | above, Recipient receives no rights or licenses to the intellectual property of 190 | any Contributor under this Agreement, whether expressly, by implication, 191 | estoppel or otherwise. All rights in the Program not expressly granted under 192 | this Agreement are reserved. 193 | 194 | This Agreement is governed by the laws of the State of New York and the 195 | intellectual property laws of the United States of America. No party to this 196 | Agreement will bring a legal action under this Agreement more than one year 197 | after the cause of action arose. Each party waives its rights to a jury trial in 198 | any resulting litigation. 199 | -------------------------------------------------------------------------------- /docs/uberdoc.html: -------------------------------------------------------------------------------- 1 | 2 | fivetonine/collage -- Marginalia

fivetonine/collage

0.1.0


Clean, minimal image processing library for Clojure

3032 |

dependencies

org.clojure/clojure
1.5.1



(this space intentionally left almost blank)
 

Drop in library for (some of) your image processing needs.

3033 | 3034 |

Collage was developed out of my own need to answer some specific needs in a 3035 | project I was working on. Even though there are a couple of other libraries 3036 | out there (mikera's imagez, which builds 3037 | on imgscalr, a Java library), 3038 | but I felt like implementing my own in order to gain more experience with 3039 | Clojure.

3040 | 3041 |

The feature-set is somewhat similar to the previously mentioned libraries, 3042 | adding functionality to paste layers (regular BufferedImages) onto an image 3043 | and controlling the quality of the image when saving it to disk.

3044 | 3045 |

This project aims to

3046 | 3047 |
    3048 |
  • Be an easy to use, drop-in solution
  • 3049 |
  • Have a composable internal API
  • 3050 |
  • Be well tested
  • 3051 |
  • Be reasonably idiomatic
  • 3052 |
3053 |
(ns fivetonine.collage.core
3054 |   (:require [fivetonine.collage.util :as util])
3055 |   (:import java.awt.image.BufferedImage)
3056 |   (:import java.awt.geom.AffineTransform)
3057 |   (:import java.awt.RenderingHints))
3058 |
(declare resize*)
3059 | (declare paste*)
3060 | (declare normalise-angle)
3061 | (declare pi-rotation?)
3062 |
(def not-nil? (complement nil?))

Core functions

3063 |

Rotates image through angle theta, where theta is 3064 | an integer multiple of 90.

3065 | 3066 |

If theta > 0, the image is rotated clockwise.

3067 | 3068 |

If theta < 0, the image is rotated anticlockwise.

3069 |
(defn rotate
3070 |   [image theta]
3071 |   (when-not (contains? (set (range -360 450 90)) (normalise-angle theta))
3072 |     (throw (IllegalArgumentException.
3073 |             "theta has to be an integer multiple of 90.")))
3074 |   (let [old-width (.getWidth image)
3075 |         old-height (.getHeight image)
3076 |         new-width (if (pi-rotation? theta) old-width old-height)
3077 |         new-height (if (pi-rotation? theta) old-height old-width)
3078 |         angle (Math/toRadians theta)
3079 |         new-image (BufferedImage. new-width new-height (.getType image))
3080 |         graphics (.createGraphics new-image)
3081 |         transform (AffineTransform.)]
3082 |     ;; Given that the rotation happens around the point (0,0) (the top left hand
3083 |     ;; corner of the image), the resulting image needs to be translated back
3084 |     ;; into the "viewport".
3085 |     (condp = (Math/abs (normalise-angle theta))
3086 |       0   (.translate transform 0 0)
3087 |       90  (.translate transform new-width 0)
3088 |       180 (.translate transform new-width new-height)
3089 |       270 (.translate transform 0 new-height)
3090 |       360 (.translate transform 0 0))
3091 |     (.rotate transform angle)
3092 |     (doto graphics
3093 |       (.drawImage image transform nil)
3094 |       (.dispose))
3095 |     new-image))

Flips an image.

3096 | 3097 |

If direction is :horizontal, flips the image around the y-axis.

3098 | 3099 |

If direction is :vertical, flips the image around the x-axis.

3100 |
(defn flip
3101 |   [image direction]
3102 |   (let [width (.getWidth image)
3103 |         height (.getHeight image)
3104 |         new-image (BufferedImage. width height (.getType image))
3105 |         graphics (.createGraphics new-image)
3106 |         transform (AffineTransform.)]
3107 |     (case direction
3108 |       :horizontal (doto transform
3109 |                     (.translate width 0)
3110 |                     (.scale -1 1))
3111 |       :vertical (doto transform
3112 |                   (.translate 0 height)
3113 |                   (.scale 1 -1)))
3114 |     (doto graphics
3115 |       (.drawImage image transform nil)
3116 |       (.dispose))
3117 |     new-image))

Scales an image by a factor f.

3118 | 3119 |

If 0.0 < f < 1.0 the image is scaled down.

3120 | 3121 |

If f > 1.0 the image is scaled up.

3122 |
(defn scale
3123 |   [image f]
3124 |   (resize* image
3125 |            (* f (-> image .getWidth int))
3126 |            (* f (-> image .getHeight int))))

Crops an image.

3127 | 3128 |

x, y are the coordinates to top left corner of the area to crop. 3129 | width, height are the width and height of the area to crop.

3130 | 3131 |

The returned image does not share its data with the original image.

3132 |
(defn crop
3133 |   [image x y width height]
3134 |   (-> image (.getSubimage x y width height) util/copy))

Resizes an image.

3135 | 3136 |

If only width or height is provided, the resulting image will be width 3137 | or height px wide, respectively. The other dimension will be calculated 3138 | automatically to preserve width/height ratio.

3139 | 3140 |

With width and height both provided, the resulting image will be crudely 3141 | resized to match the provided values.

3142 | 3143 |

If neither width nor height are provided, IllegalArgumentException is 3144 | thrown.

3145 | 3146 |

Examples:

3147 | 3148 |
(resize image :width 100)
3149 | (resize image :height 300)
3150 | (resize image :width 100 :height 300)
3151 | 
3152 |
(defn resize
3153 |   [image & {:keys [width height] :as opts}]
3154 |   (let [supported #{:width :height}
3155 |         options (select-keys opts supported)
3156 |         width (options :width)
3157 |         height (options :height)]
3158 |     (when (empty? options)
3159 |       (throw (IllegalArgumentException.
3160 |               "Width or height (or both) has to be provided.")))
3161 |     (cond
3162 |      (and width height) (resize* image (int width) (int height))
3163 |      (not-nil? width)
3164 |      (let [new-height (* (/ (int width) (.getWidth image)) (.getHeight image))]
3165 |        (resize* image width (int new-height)))
3166 |      (not-nil? height)
3167 |      (let [new-width (* (/ (int height) (.getHeight image)) (.getWidth image))]
3168 |        (resize* image (int new-width) height)))))

Resize the given image to width and height. 3169 | Used as an internal function by resize.

3170 | 3171 |

Note: the method of resizing may change in the future as there are better, 3172 | iterative, solutions to balancing speed vs. quality. See 3173 | the perils of Image.getScaledInstance().

3174 |
(defn resize*
3175 |   [image width height]
3176 |   (let [new-image (BufferedImage. width height (.getType image))
3177 |         graphics (.createGraphics new-image)]
3178 |     (doto graphics
3179 |       (.setRenderingHint RenderingHints/KEY_INTERPOLATION
3180 |                          RenderingHints/VALUE_INTERPOLATION_BICUBIC)
3181 |       (.drawImage image 0 0 width height nil)
3182 |       .dispose)
3183 |     new-image))

Pastes layer(s) onto image at coordinates x and y.

3184 | 3185 |

layer-defs is expected to in the format

3186 | 3187 |
[layer1 x1 y2 layer2 x2 y2 ... ]
3188 | 
3189 | 3190 |

Layers are loaded using fivetonine.collage.util/load-image. 3191 | Top left corner of a layer will be at x, y.

3192 | 3193 |

Throws IllegalArgumentException if the number of elements in the list of 3194 | layers and coordinates is not divisible by 3.

3195 | 3196 |

Returns the resulting image.

3197 |
(defn paste
3198 |   [image & layer-defs]
3199 |   (let [args (flatten (seq layer-defs))]
3200 |     (when-not (= 0 (-> args count (rem 3)))
3201 |       (throw (IllegalArgumentException.
3202 |               "Expected layer-defs format [image1 x1 y1 image2 x2 y2 ... ].")))
3203 |     (let [new-image (util/copy image)
3204 |           layers (partition 3 args)]
3205 |       ;; "reduce" all the layers onto new-image using paste* -- the layers are
3206 |       ;; accumulated onto the image
3207 |       (reduce paste* new-image layers)
3208 |       new-image)))

Paste layer on top of base at position x, y and return the resulting image. 3209 | Used as an internal function by paste.

3210 |
(defn paste*
3211 |   [base [layer x y]]
3212 |   (let [graphics (.createGraphics base)]
3213 |     (doto graphics
3214 |       (.setRenderingHint RenderingHints/KEY_RENDERING
3215 |                          RenderingHints/VALUE_RENDER_QUALITY)
3216 |       (.setRenderingHint RenderingHints/KEY_COLOR_RENDERING
3217 |                          RenderingHints/VALUE_COLOR_RENDER_QUALITY)
3218 |       (.drawImage (util/load-image layer) x y nil)
3219 |       (.dispose))
3220 |     base))

A helper for applying multiple operations to an image.

3221 | 3222 |

image-resource can be a String, a File or a BufferedImage.

3223 | 3224 |

Example:

3225 | 3226 |
(with-image "/path/to/image.jpg"
3227 |              (scale 0.8)
3228 |              (rotate 90)
3229 |              (crop 0 0 100 100))
3230 | 
3231 | 3232 |

Expands to (properly namespaced):

3233 | 3234 |
(let [image__2336__auto__ (load-image "/path/to/image.jpg")]
3235 |   (clojure.core/-> image__2336__auto__
3236 |                    (scale 0.8)
3237 |                    (rotate 90)
3238 |                    (crop 0 0 100 100)))
3239 | 
3240 | 3241 |

Returns the image which is the result of applying all operations to the input 3242 | image.

3243 |
(defmacro with-image
3244 |   [image-resource & operations]
3245 |   `(let [image# (util/load-image ~image-resource)]
3246 |      (-> image# ~@operations)))

Helpers

3247 |

Does the rotation through angle theta correspond to a rotation that is 3248 | a multiple of 180 degrees, meaning that the image preserves the original 3249 | width and height.

3250 |
(defn- pi-rotation?
3251 |   [theta]
3252 |   (-> theta (/ 90) (rem 2) (= 0)))

Restrict the rotation angle to the range [-360..360].

3253 |
(defn normalise-angle
3254 |   [theta]
3255 |   (rem theta 360))
 
3256 |
(ns fivetonine.collage.util
3257 |   (:require [clojure.java.io :refer [as-file file]])
3258 |   (:import java.io.File
3259 |            java.net.URI
3260 |            java.awt.image.BufferedImage
3261 |            javax.imageio.ImageIO
3262 |            javax.imageio.IIOImage
3263 |            javax.imageio.ImageWriter
3264 |            javax.imageio.ImageWriteParam
3265 |            fivetonine.collage.Frame))
3266 |
(declare parse-extension)

Display an image in a JFrame.

3267 | 3268 |

Convenience function for viewing an image quickly.

3269 |
(defn show
3270 |   [^BufferedImage image]
3271 |   (Frame/createImageFrame "Quickview" image))

Make a deep copy of an image.

3272 |
(defn copy
3273 |   [image]
3274 |   (let [width (.getWidth image)
3275 |         height (.getHeight image)
3276 |         type (.getType image)
3277 |         new-image (BufferedImage. width height type)]
3278 |     ;; Get data from image and set data in new-image, resulting in a copy
3279 |     ;; This also works for BufferedImages that are obtained by calling
3280 |     ;; .getSubimage on another BufferedImage.
3281 |     (.setData new-image (.getData image))
3282 |     new-image))

Store an image on disk.

3283 | 3284 |

Returns the path to the saved image when saved successfully.

3285 |
(defn save
3286 |   [^BufferedImage image path & rest]
3287 |   (let [opts (apply hash-map rest)
3288 |         outfile (file path)
3289 |         ext (parse-extension path)
3290 |         ^ImageWriter writer (.next (ImageIO/getImageWritersByFormatName ext))
3291 |         ^ImageWriteParam write-param (.getDefaultWriteParam writer)
3292 |         iioimage (IIOImage. image nil nil)
3293 |         outstream (ImageIO/createImageOutputStream outfile)]
3294 |     ; Only compress images that can be compressed. PNGs, for example, cannot be
3295 |     ; compressed.
3296 |     (when (.canWriteCompressed write-param)
3297 |       (doto write-param
3298 |         (.setCompressionMode ImageWriteParam/MODE_EXPLICIT)
3299 |         (.setCompressionQuality (get opts :quality 0.8))))
3300 |     (doto writer
3301 |       (.setOutput outstream)
3302 |       (.write nil iioimage write-param)
3303 |       (.dispose))
3304 |     (.close outstream)
3305 |     path))

Coerce different image resource representations to BufferedImage.

3306 |
(defprotocol ImageResource
3307 |   (as-image [x] "Coerce argument to an image."))
3308 |
(extend-protocol ImageResource
3309 |   String
3310 |   (as-image [s] (ImageIO/read (as-file s)))
3311 | 
3312 |   File
3313 |   (as-image [f] (ImageIO/read f))
3314 | 
3315 |   BufferedImage
3316 |   (as-image [b] b))

Loads an image from resource.

3317 |
(defn ^BufferedImage load-image
3318 |   [resource]
3319 |   (as-image resource))

Helpers & experimental

3320 |

Parses the image extension from the path.

3321 |
(defn parse-extension
3322 |   [path]
3323 |   (last (clojure.string/split path #"\.")))

Sanitizes a path. 3324 | Returns the sanitized path, or throws if sanitization is not possible.

3325 |
(defn sanitize-path
3326 |   [path]
3327 |   (when-let [scheme (-> path URI. .getScheme)]
3328 |     (if (not (= "file" scheme))
3329 |       (throw (Exception. "Path must point to a local file."))
3330 |       (URI. path)))
3331 |   (URI. (str "file://" path)))
 
--------------------------------------------------------------------------------