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))) | |||