fivetonine/collage0.1.0Clean, minimal image processing library for Clojure dependencies
| (this space intentionally left almost blank) | |||
Drop in library for (some of) your image processing needs.Collage was developed out of my own need to answer some specific needs in a project I was working on. Even though there are a couple of other libraries out there (mikera's imagez, which builds on imgscalr, a Java library), but I felt like implementing my own in order to gain more experience with Clojure. The feature-set is somewhat similar to the previously mentioned libraries,
adding functionality to paste layers (regular This project aims to
| (ns fivetonine.collage.core (:require [fivetonine.collage.util :as util]) (:import java.awt.image.BufferedImage) (:import java.awt.geom.AffineTransform) (:import java.awt.RenderingHints)) | |||
(declare resize*) (declare paste*) (declare normalise-angle) (declare pi-rotation?) | ||||
(def not-nil? (complement nil?)) | ||||
Core functions | ||||
Rotates image through angle If If | (defn rotate [image theta] (when-not (contains? (set (range -360 450 90)) (normalise-angle theta)) (throw (IllegalArgumentException. "theta has to be an integer multiple of 90."))) (let [old-width (.getWidth image) old-height (.getHeight image) new-width (if (pi-rotation? theta) old-width old-height) new-height (if (pi-rotation? theta) old-height old-width) angle (Math/toRadians theta) new-image (BufferedImage. new-width new-height (.getType image)) graphics (.createGraphics new-image) transform (AffineTransform.)] ;; Given that the rotation happens around the point (0,0) (the top left hand ;; corner of the image), the resulting image needs to be translated back ;; into the "viewport". (condp = (Math/abs (normalise-angle theta)) 0 (.translate transform 0 0) 90 (.translate transform new-width 0) 180 (.translate transform new-width new-height) 270 (.translate transform 0 new-height) 360 (.translate transform 0 0)) (.rotate transform angle) (doto graphics (.drawImage image transform nil) (.dispose)) new-image)) | |||
Flips an image. If direction is If direction is | (defn flip [image direction] (let [width (.getWidth image) height (.getHeight image) new-image (BufferedImage. width height (.getType image)) graphics (.createGraphics new-image) transform (AffineTransform.)] (case direction :horizontal (doto transform (.translate width 0) (.scale -1 1)) :vertical (doto transform (.translate 0 height) (.scale 1 -1))) (doto graphics (.drawImage image transform nil) (.dispose)) new-image)) | |||
Scales an image by a factor If If | (defn scale [image f] (resize* image (* f (-> image .getWidth int)) (* f (-> image .getHeight int)))) | |||
Crops an image. The returned image does not share its data with the original image. | (defn crop [image x y width height] (-> image (.getSubimage x y width height) util/copy)) | |||
Resizes an image. If only With If neither Examples:
| (defn resize [image & {:keys [width height] :as opts}] (let [supported #{:width :height} options (select-keys opts supported) width (options :width) height (options :height)] (when (empty? options) (throw (IllegalArgumentException. "Width or height (or both) has to be provided."))) (cond (and width height) (resize* image (int width) (int height)) (not-nil? width) (let [new-height (* (/ (int width) (.getWidth image)) (.getHeight image))] (resize* image width (int new-height))) (not-nil? height) (let [new-width (* (/ (int height) (.getHeight image)) (.getWidth image))] (resize* image (int new-width) height))))) | |||
Resize the given image to Note: the method of resizing may change in the future as there are better, iterative, solutions to balancing speed vs. quality. See the perils of Image.getScaledInstance(). | (defn resize* [image width height] (let [new-image (BufferedImage. width height (.getType image)) graphics (.createGraphics new-image)] (doto graphics (.setRenderingHint RenderingHints/KEY_INTERPOLATION RenderingHints/VALUE_INTERPOLATION_BICUBIC) (.drawImage image 0 0 width height nil) .dispose) new-image)) | |||
Pastes layer(s) onto image at coordinates
Layers are loaded using Throws Returns the resulting image. | (defn paste [image & layer-defs] (let [args (flatten (seq layer-defs))] (when-not (= 0 (-> args count (rem 3))) (throw (IllegalArgumentException. "Expected layer-defs format [image1 x1 y1 image2 x2 y2 ... ]."))) (let [new-image (util/copy image) layers (partition 3 args)] ;; "reduce" all the layers onto new-image using paste* -- the layers are ;; accumulated onto the image (reduce paste* new-image layers) new-image))) | |||
Paste layer on top of base at position | (defn paste* [base [layer x y]] (let [graphics (.createGraphics base)] (doto graphics (.setRenderingHint RenderingHints/KEY_RENDERING RenderingHints/VALUE_RENDER_QUALITY) (.setRenderingHint RenderingHints/KEY_COLOR_RENDERING RenderingHints/VALUE_COLOR_RENDER_QUALITY) (.drawImage (util/load-image layer) x y nil) (.dispose)) base)) | |||
A helper for applying multiple operations to an image. Example:
Expands to (properly namespaced):
Returns the image which is the result of applying all operations to the input image. | (defmacro with-image [image-resource & operations] `(let [image# (util/load-image ~image-resource)] (-> image# ~@operations))) | |||
Helpers | ||||
Does the rotation through angle | (defn- pi-rotation? [theta] (-> theta (/ 90) (rem 2) (= 0))) | |||
Restrict the rotation angle to the range [-360..360]. | (defn normalise-angle [theta] (rem theta 360)) | |||
(ns fivetonine.collage.util (:require [clojure.java.io :refer [as-file file]]) (:import java.io.File java.net.URI java.net.URL java.awt.image.BufferedImage javax.imageio.ImageIO javax.imageio.IIOImage javax.imageio.ImageWriter javax.imageio.ImageWriteParam fivetonine.collage.Frame)) | ||||
(declare parse-extension) | ||||
Display an image in a Convenience function for viewing an image quickly. | (defn show [^BufferedImage image] (Frame/createImageFrame "Quickview" image)) | |||
Make a deep copy of an image. | (defn copy [image] (let [width (.getWidth image) height (.getHeight image) type (.getType image) new-image (BufferedImage. width height type)] ;; Get data from image and set data in new-image, resulting in a copy ;; This also works for BufferedImages that are obtained by calling ;; .getSubimage on another BufferedImage. (.setData new-image (.getData image)) new-image)) | |||
Store an image on disk. Accepts optional keyword arguments. Examples:
Returns the path to the saved image when saved successfully. | (defn save [^BufferedImage image path & rest] (let [opts (apply hash-map rest) outfile (file path) ext (parse-extension path) ^ImageWriter writer (.next (ImageIO/getImageWritersByFormatName ext)) ^ImageWriteParam write-param (.getDefaultWriteParam writer) iioimage (IIOImage. image nil nil) outstream (ImageIO/createImageOutputStream outfile)] ; Only compress images that can be compressed. PNGs, for example, cannot be ; compressed. (when (.canWriteCompressed write-param) (doto write-param (.setCompressionMode ImageWriteParam/MODE_EXPLICIT) (.setCompressionQuality (get opts :quality 0.8)))) (when (.canWriteProgressive write-param) (let [mode-map {true ImageWriteParam/MODE_DEFAULT false ImageWriteParam/MODE_DISABLED} mode-flag (get opts :progressive)] (doto write-param (.setProgressiveMode (get mode-map mode-flag ImageWriteParam/MODE_COPY_FROM_METADATA))))) (doto writer (.setOutput outstream) (.write nil iioimage write-param) (.dispose)) (.close outstream) path)) | |||
Coerce different image resource representations to BufferedImage. | (defprotocol ImageResource (as-image [x] "Coerce argument to an image.")) | |||
(extend-protocol ImageResource String (as-image [s] (ImageIO/read (as-file s))) File (as-image [f] (ImageIO/read f)) URL (as-image [r] (ImageIO/read r)) BufferedImage (as-image [b] b)) | ||||
Loads an image from resource. | (defn ^BufferedImage load-image [resource] (as-image resource)) | |||
Helpers & experimental | ||||
Parses the image extension from the path. | (defn parse-extension [path] (last (clojure.string/split path #"\."))) | |||
Sanitizes a path. Returns the sanitized path, or throws if sanitization is not possible. | (defn sanitize-path [path] (when-let [scheme (-> path URI. .getScheme)] (if (not (= "file" scheme)) (throw (Exception. "Path must point to a local file.")) (URI. path))) (URI. (str "file://" path))) | |||