All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jlangch.venice.images.venice Maven / Gradle / Ivy

;;;;   __    __         _
;;;;   \ \  / /__ _ __ (_) ___ ___
;;;;    \ \/ / _ \ '_ \| |/ __/ _ \
;;;;     \  /  __/ | | | | (_|  __/
;;;;      \/ \___|_| |_|_|\___\___|
;;;;
;;;;
;;;; Copyright 2017-2024 Venice
;;;;
;;;; Licensed under the Apache License, Version 2.0 (the "License");
;;;; you may not use this file except in compliance with the License.
;;;; You may obtain a copy of the License at
;;;;
;;;;     http://www.apache.org/licenses/LICENSE-2.0
;;;;
;;;; Unless required by applicable law or agreed to in writing, software
;;;; distributed under the License is distributed on an "AS IS" BASIS,
;;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;;; See the License for the specific language governing permissions and
;;;; limitations under the License.

;;;; Image functions

;;;; Thanks to Riyad Kalla and his 'imgscalr' Java project (Apache-2.0 license).
;;;; https://github.com/rkalla/imgscalr/
;;;;
;;;; This is a Venice rewrite of the 'imgscalr'.


(ns images)

(import :java.awt.Color
        :java.awt.Graphics
        :java.awt.Graphics2D
        :java.awt.Image
        :java.awt.RenderingHints
        :java.awt.Transparency
        :java.awt.color.ColorSpace
        :java.awt.geom.AffineTransform
        :java.awt.geom.Rectangle2D
        :java.awt.image.AreaAveragingScaleFilter
        :java.awt.image.BufferedImage
        :java.awt.image.BufferedImageOp
        :java.awt.image.ColorConvertOp
        :java.awt.image.ColorModel
        :java.awt.image.ConvolveOp
        :java.awt.image.ImagingOpException
        :java.awt.image.IndexColorModel
        :java.awt.image.Kernel
        :java.awt.image.RasterFormatException
        :java.awt.image.RescaleOp)

(import :javax.imageio.ImageIO
        :javax.imageio.stream.FileImageInputStream
        :javax.imageio.spi.IIORegistry
        :javax.imageio.spi.ImageWriterSpi)


;; -----------------------------------------------------------------------------
;; Constants
;; -----------------------------------------------------------------------------

(defonce TYPE_INT_RGB  (. :BufferedImage :TYPE_INT_RGB))
(defonce TYPE_INT_ARGB (. :BufferedImage :TYPE_INT_ARGB))

(defonce resize-modes #{ :resize-auto 
                         :resize-performance 
                         :resize-balanced 
                         :resize-quality 
                         :resize-high-quality })

(defonce fit-modes #{ :fit-best 
                      :fit-exact 
                      :fit-width 
                      :fit-height })




;; -----------------------------------------------------------------------------
;; Imaging operators
;; -----------------------------------------------------------------------------

(def convolve-op-antialias (. :ConvolveOp 
                                 :new 
                                 (. :Kernel :new 3 3 [ 0.00F, 0.08F, 0.00F, 
                                                       0.08F, 0.68F, 0.08F,
                                                       0.00F, 0.08F, 0.00F ])
                                 (. :ConvolveOp :EDGE_NO_OP)
                                 nil))

(def rescale-op-darker (. :RescaleOp  :new 0.9F 0 nil))

(def rescale-op-brighter (. :RescaleOp  :new 1.1F 0 nil))

(def color-convert-op-grayscale (as-> (. :ColorSpace :CS_GRAY) cs
                                      (. :ColorSpace :getInstance cs)
                                      (. :ColorConvertOp :new cs nil)))





;; -----------------------------------------------------------------------------
;; Load / Save
;; -----------------------------------------------------------------------------

(defn 
  ^{ :arglists '(
          "(load file)") 
     :doc """
          Loads an image from a `:java.io.File`, a `:java.io.InputStream`, or a 
          `:java.net.URL`.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg")))
          """)
     :see-also '(
          "images/save" ) }

  load [f]

  (assert (or (io/file? f) 
              (instance-of? :java.io.InputStream f)
              (instance-of? :java.net.URL f)))

  (try 
    (. :ImageIO :read f)
    (catch :Exception e
      (throw (ex :VncException "Faled to load image" e)))))


(defn 
  ^{ :arglists '(
          "(save img format-name f)") 
     :doc """
          Saves an image to 'java.io.File' or an ':java.io.OutputStream'.

          Supported formats:  "png", "jpg", "jpeg", "gif", "bmp", ...
          """
     :examples '(
          """
          (do
            (load-module :images)
            (-> (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg"))
                (images/save img :png (io/file "/Users/foo/Desktop/Pink-Forest.png"))))
          """) 
     :see-also '(
          "images/load"
          "images/format-names" ) }

  save [img format-name f]

  (assert (instance-of? :Image img))
  (assert (or (string? format-name) (keyword? format-name)))
  (assert (or (io/file? f) (instance-of? :java.io.OutputStream f)))

  (when-not (try 
              (. :ImageIO :write img (name format-name) f)
              (catch :Exception e
                (throw (ex :VncException "Failed to write image" e))))
    (throw (ex :VncException 
               """
               Failed to write image. No appropriate writer found for format \
               '~{format-name}'!
               """))))
 


;; -----------------------------------------------------------------------------
;; Image properties
;; -----------------------------------------------------------------------------

(defn 
  ^{ :arglists '(
          "(dimension f)") 
     :doc """
          Returns the images dimensions as a vector [width height]. 'f' may 
          be a `:java.io.File` or a `:java.awt.Image`.

          Note: Do not load a file first to get the dimension, passing a
                `:java.io.File` is much faster!
          """
     :examples '(
          """
          (do
            (load-module :images)
            (images/dimension (io/file "/Users/foo/Desktop/Pink-Forest.jpg")))
          """ ) }

  dimension [f]

  (assert (or (io/file? f) (instance-of? :Image f)))

  (if (instance-of? :Image f)
    [(long (. f :getWidth)) (long (. f :getHeight))] ;; return dimensions
    (let [suffix    (io/file-ext f)
          iterators (. :ImageIO :getImageReadersBySuffix suffix)]
      (let [dim (loop [readers (java-iterator-to-list iterators)]
                  (if-let [reader (first readers)]
                    (if-let [d (read-dimension reader f)]
                      d  ;; return dimensions
                      (recur (rest readers)))))]  ;; try with next reader
        (if dim
          dim
          (throw (ex :VncException 
                     (str "Cannot get image dimensions for unknown image " 
                          f))))))))


(defn- read-dimension [img-reader file]
  (try-with [is (. :FileImageInputStream :new file)]
    (. img-reader :setInput is)
    ;; return dimensions
    [(long (. img-reader :getWidth  (. img-reader :getMinIndex)))
     (long (. img-reader :getHeight (. img-reader :getMinIndex)))]
  (catch :Exception e nil)  ;; this reader does not match the image type
  (finally (. img-reader :dispose))))



;; -----------------------------------------------------------------------------
;; Rotate / Flip
;; -----------------------------------------------------------------------------

(defn 
  ^{ :arglists '(
          "(rotate img angle)") 
     :doc """
          Rotates an image by 0°, 90°, 180°, or 270° clockwise. 
          Returns a new image.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (-> (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg"))
                (images/rotate 90)
                (images/save "jpg" (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg"))))
          """ ) }

  rotate [img angle]

  (assert (instance-of? :Image img))
  (assert (and (long? angle) (contains? #{0, 90, 180, 270} angle)))

  (let [[width height] (dimension img)
        tx             (. :AffineTransform :new)]
    (case angle
      90      (let [new-width  height
                    new-height width]
                      (. tx :translate new-width 0)
                      (. tx :quadrantRotate 1)
                (transform img tx new-width new-height))

      180     (let [new-width  width
                    new-height height]
                       (. tx :translate new-width new-height)
                       (. tx :quadrantRotate 2)
                 (transform img tx new-width new-height))

      270     (let [new-width  height
                    new-height width]
                      (. tx :translate 0 new-height)
                      (. tx :quadrantRotate 3)
                (transform img tx new-width new-height))

      img)))


(defn 
  ^{ :arglists '(
          "(flip img mode)") 
     :doc """
          Flips an image vertically or horizontally. Returns a new image.

          Mode is either :vertical or :horizontal.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (-> (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg"))
                (images/flip :vertical)
                (images/save "jpg" (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg"))))
          """ ) }

  flip [img mode]

  (assert (instance-of? :Image img))
  (assert (and (keyword? mode) (contains? #{:vertical :horizontal} mode)))

  (let [[width height] (dimension img)
        tx             (. :AffineTransform :new)]
    (case mode
      :horizontal (do (. tx :translate width 0)
                      (. tx :scale -1.0 1.0)
                      (transform img tx width height))

      :vertical   (do (. tx :translate 0 height)
                      (. tx :scale 1.0 -1.0)
                      (transform img tx width height))
      img)))


(defn- transform [img tx width height]
  (assert (instance-of? :Image img))
  (assert (and (long? width) (pos? width)))
  (assert (and (long? height) (pos? height)))

  (let [result (create-derived-image img width height)
        g2d    (. result :createGraphics)]
    (. g2d :drawImage img tx nil)
    (. g2d :dispose)
    result))


;; -----------------------------------------------------------------------------
;; Crop / Pad / Resize
;; -----------------------------------------------------------------------------

(defn 
  ^{ :arglists '(
          "(crop img x y width height)") 
     :doc """
          Crops an image. Returns a new image.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (-> (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg"))
                (images/crop 1000 1000 400 200)
                (images/save "jpg" (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg"))))
          """ ) }

  crop [img x y width height]

  (assert (instance-of? :Image img))
  (assert (and (long? x) (long? y) (long? width) (long? height)))
  (assert (and (>= x 0) (>= y 0) (>= width 0) (>= height 0)))

  (let [[img-width img-height] (dimension img)]
    (when (> (+ x width) img-width)
      (throw (ex :VncException 
                 "Invalid crop bounds: x[~{x}] + width[~{width}] < img-width[~{img-width}]")))
    (when (> (+ y height) img-height)
      (throw (ex :VncException 
                 "Invalid crop bounds: y[~{y}]+ height[~{height}] < img-height[~{img-height}]")))

    (let [dest (create-derived-image img width height)
          g    (. dest :getGraphics)]
      (. g :drawImage img 
                      0 0 width height 
                      x y (+ x width) (+ y height)
                      nil)
        (. g :dispose)
      dest)))


(defn 
  ^{ :arglists '(
          "(pad img padding color)") 
     :doc """
          Pads an image. Returns a new image.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (import :java.awt.Color)
            (-> (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg"))
                (images/pad 200 (. :Color :WHITE))
                (images/save "jpg" (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg"))))
          """ ) }

  pad [img padding color]

  (assert (instance-of? :Image img))
  (assert (and (long? padding) (>= padding 0)))
  (assert (instance-of? :Color color))

  (let [[img-width img-height] (dimension img)
        img-alpha?   (not (opaque? img))
        width        (+ img-width (* padding 2))
        height       (+ img-height (* padding 2))
        type         (if (or (color-alpha? color) img-alpha?) TYPE_INT_ARGB TYPE_INT_RGB)       
        dest         (. :BufferedImage :new width height type)
        g            (. dest :getGraphics)]

        ;; draw the padding border
        (. g :setColor color)
        (. g :fillRect 0 0 width padding)
        (. g :fillRect 0 padding padding height)
        (. g :fillRect padding (- height padding) width height)
        (. g :fillRect (- width padding) padding width (- height padding))

        ; draw the centered image
        (. g :drawImage img padding padding nil)
        (. g :dispose)

    dest))

(defn 
  ^{ :arglists '(
          "(resize-fit img size fit-mode)"
          "(resize img size fit-mode resize-mode)" )
     :doc """
          Resizes an image to a new size, a square of width and height the 
          image should fit within the size. 

          Resize modes: 
          
          | [![width: 15%]] | [![width: 85%]] |
          | `:fit-best`     | (default), fit within a square box of size 'size', \
                              keeps width / height ratio  |
          | `:fit-exact`    | fit exactly to a square of size 'size', \
                              causes distortions  |
          | `:fit-width`    | fit to width, keeps width / height ratio |
          | `:fit-height`   | fit to height, keeps width / height ratio |

          Resize modes: 
          
          | [![width: 15%]]        | [![width: 85%]]                         |
          | `:resize-auto`         | (default) chooses best mode             |
          | `:resize-performance`  | resize for best performance             |
          | `:resize-balanced`     | balance between performance and quality |
          | `:resize-quality`      | resize for quality                      |
          | `:resize-high-quality` | resize for high quality                 |

          Returns a new image.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (let [src (io/file "/Users/foo/Desktop/Pink-Forest.jpg")
                  dst (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg")]
              (-> (images/load src)
                  (images/resize-fit 500 :fit-best :resize-balanced)
                  (images/save "jpg" dst))
                  
              (println (io/file-name src) ":" (images/dimension src))
              (println (io/file-name dst) ":" (images/dimension dst))))
          """ )
     :see-also '(
          "images/resize" ) }

  resize-fit

  ([img size fit-mode]
    (resize-fit img size fit-mode :resize-auto))

  ([img size fit-mode resize-mode]
    (assert (instance-of? :Image img))
    (assert (and (long? size) (> size 0)))
    (assert (and (keyword? fit-mode) (contains? fit-modes fit-mode)))
    (assert (and (keyword? resize-mode) (contains? resize-modes resize-mode)))
 
    (let [[img-width img-height] (dimension img)]
      (case fit-mode
        :fit-exact
              (resize img size size resize-mode)

        :fit-width
              (let [factor (/ (double size) (double img-width))]
                (resize img size (scale img-height factor) resize-mode))

        :fit-height
              (let [factor (/ (double size) (double img-height))]
                (resize img (scale img-width factor) size resize-mode))

        :fit-best
              (let [factor-w (/ (double size) (double img-width))
                    factor-h (/ (double size) (double img-height))
                    factor   (cond
                               (and (>= factor-w 1.0) (>= factor-h 1.0)) 
                                 (min factor-w factor-h) ;; upscale
                               (and (>= factor-w 1.0) (< factor-h 1.0))
                                 factor-h ;; downscale
                               (and (< factor-w 1.0) (>= factor-h 1.0))
                                 factor-w ;; downscale
                               (and (< factor-w 1.0) (< factor-h 1.0))
                                 (max factor-w factor-h))] ;; downscale
                               :else 1.0
                    (resize img 
                      (scale img-width factor) 
                      (scale img-height factor)
                      resize-mode))
           
        ;; else
        (throw (ex :VncException 
                    "Internal error: unhandled fit-mode '~{fit-mode}'!"))))))


(defn 
  ^{ :arglists '(
          "(resize img width height)"
          "(resize img width height resize-mode)" )
     :doc """
          Resizes an image to a new width and height. 

          Resize modes: 
          
          | [![width: 15%]]        | [![width: 85%]].                        |
          | `:resize-auto`         | (default) chooses best mode             |
          | `:resize-performance`  | resize for best performance             |
          | `:resize-balanced`     | balance between performance and quality |
          | `:resize-quality`      | resize for quality                      |
          | `:resize-high-quality` | resize for high quality                 |

          Returns a new image.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (let [src (io/file "/Users/foo/Desktop/Pink-Forest.jpg")
                  dst (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg")]
              (-> (images/load src)
                  (images/resize 500 200 :resize-balanced)
                  (images/save "jpg" dst))
                  
              (println (io/file-name src) ":" (images/dimension src))
              (println (io/file-name dst) ":" (images/dimension dst))))
          """ )
     :see-also '(
          "images/resize-fit" ) }

  resize 
  
  ([img width height]
    (resize img width height :auto))

  ([img width height resize-mode]
    (assert (instance-of? :Image img))
    (assert (and (long? width) (> width 0)))
    (assert (and (long? height) (> height 0)))
    (assert (and (keyword? resize-mode) (contains? resize-modes resize-mode)))

    (let [[img-width img-height] (dimension img)
          img-ratio      (/ (double img-height) (double img-width))
          img-landscape? (<= img-ratio 1.0)
          resize-mode    (if (= resize-mode :resize-auto)
                           (scale-method width height img-landscape?)
                           resize-mode)]

      (if (and (== img-width width) (== img-height height))
        img ;; no change in size return original image
        (do
          ;; resize the image
          (case resize-mode
            :resize-performance    
                    (scale-image img width height 
                      (. :RenderingHints :VALUE_INTERPOLATION_NEAREST_NEIGHBOR))

            :resize-balanced
                    (scale-image img width height 
                      (. :RenderingHints :VALUE_INTERPOLATION_BILINEAR))

            :resize-quality        
                    (if (or (> width img-width) (> height img-height))
                      ;; scale up
                      (scale-image img width height 
                                    (. :RenderingHints :VALUE_INTERPOLATION_BICUBIC))
                      ;; scale down             
                      (scale-image img width height 
                                    (. :RenderingHints :VALUE_INTERPOLATION_BICUBIC)))
                              
            :resize-high-quality   
                    (if (or (> width img-width) (> height img-height))
                      ;; scale up
                      (scale-image img width height 
                                    (. :RenderingHints :VALUE_INTERPOLATION_BICUBIC))
                      ;; scale down             
                      (scale-image img width height 
                                    (. :RenderingHints :VALUE_INTERPOLATION_BICUBIC)))
            
            ;; else
            (throw (ex :VncException 
                      "Internal error: unhandled resize-mode '~{resize-mode}'!"))))))))


(defn- scale-image [img width height interpolation-hint]
  (let [dest (create-derived-image img width height)
        g2d  (cast :java.awt.Graphics2D (. dest :getGraphics))
        key  (. :RenderingHints :KEY_INTERPOLATION) ]
    (. g2d :setRenderingHint key interpolation-hint)
    (. g2d :drawImage img 0 0 width height nil)
    (. g2d :dispose)
    dest))


(defn- scale-method [width height img-landscape?]
  (let [length (if img-landscape? width height)]    
    (cond
      (<= length 800)   :quality
      (<= length 1600)  :balanced
      :else             :performance)))


(defn scale [size factor]
  (long (* (double size) (double factor))))



;; -----------------------------------------------------------------------------
;; Apply image OPs
;; -----------------------------------------------------------------------------

(defn 
  ^{ :arglists '(
          "(apply-ops img ops)") 
     :doc """
          Applies one or multiple image operators (:java.awt.image.BufferedImageOp)
          to the image. 
          Returns a new image.

          Examples for image operators:

          ```
          (import :java.awt.color.ColorSpace
                  :java.awt.image.ColorConvertOp
                  :java.awt.image.ConvolveOp
                  :java.awt.image.Kernel
                  :java.awt.image.RescaleOp)

          (def convolve-op-antialias (. :ConvolveOp 
                                        :new 
                                        (. :Kernel :new 3 3 
                                                        [ 0.00F, 0.08F, 0.00F, 
                                                          0.08F, 0.68F, 0.08F,
                                                          0.00F, 0.08F, 0.00F ])
                                        (. :ConvolveOp :EDGE_NO_OP)
                                        nil))

          (def rescale-op-darker (. :RescaleOp :new 0.9F 0 nil))

          (def rescale-op-brighter (. :RescaleOp :new 1.1F 0 nil))

          (def color-convert-op-grayscale (as-> (. :ColorSpace :CS_GRAY) cs
                                                (. :ColorSpace :getInstance cs)
                                                (. :ColorConvertOp :new cs nil)))
          ```
          """
     :examples '(
          """
          ;; make the image brighter
          (do
            (load-module :images)
            (import :java.awt.image.RescaleOp)
            
            (let [op-brighter (. :RescaleOp :new 1.3F 0 nil)]
              (-> (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg"))
                  (images/apply-ops [op-brighter])
                  (images/save "jpg" (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg")))))
          """,
          """
          ;; convert the image to grayscale
          (do
            (load-module :images)
            (import :java.awt.color.ColorSpace)
            (import :java.awt.image.ColorConvertOp)

            (let [op-grayscale (as-> (. :ColorSpace :CS_GRAY) cs
                                     (. :ColorSpace :getInstance cs)
                                     (. :ColorConvertOp :new cs nil))]
              (-> (images/load (io/file "/Users/foo/Desktop/Pink-Forest.jpg"))
                  (images/apply-ops [op-grayscale])
                  (images/save "jpg" (io/file "/Users/foo/Desktop/Pink-Forest-1.jpg")))))
          """,
           ) }

  apply-ops [img ops]

  (assert (instance-of? :Image img))
  (assert (sequential? ops))
  (assert (not-empty? ops))
  (assert (every? #(instance-of? :BufferedImageOp %) ops))

  (let [src (if (rgb-or-argb-image? img) img (copy-to-derived-image img))]
    (loop [src        src
           ops        (filter some? ops)
           reassigned false]
      (let [op            (first ops) 
            result-bounds (. op :getBounds2D src)]
        (when (nil? result-bounds)
          (throw (ex :VncException (str "BufferedImageOp[" op "] get bounds returned nil."))))
        (let [w      (long (. result-bounds :getWidth))
              h      (long (. result-bounds :getHeight))
              dest   (create-derived-image src w h)
              result (. op :filter src dest)]
          (when reassigned (. src :flush))
          (if (<= (count ops) 1)
            result
            (recur result (rest ops) true)))))))



;; -----------------------------------------------------------------------------
;; Utils
;; -----------------------------------------------------------------------------

(defn 
  ^{ :arglists '(
          "(format-names)") 
     :doc """
          Returns a list of format that the image writer supports.
          """
     :examples '(
          """
          (do
            (load-module :images)
            (images/format-names))
          """ ) }

  format-names []

  (. :ImageIO :getWriterFormatNames))



;; -----------------------------------------------------------------------------
;; Internals
;; -----------------------------------------------------------------------------

(defn-  create-derived-image 
  ([src]
    (create-derived-image (long (. img :getWidth)) (long (. img :getHeight))))

  ([src width height]
    (assert (instance-of? :Image src))
    (assert (and (long? width) (pos? width)))
    (assert (and (long? height) (pos? height)))
  
    (let [type  (if (opaque? src) TYPE_INT_RGB TYPE_INT_ARGB)]
      (. :BufferedImage :new width height type))))


(defn- copy-to-derived-image [img]
  (let [width        (long (. img :getWidth))
        height       (long (. img :getHeight))
        type         (if (opaque? img) TYPE_INT_RGB TYPE_INT_ARGB)
        dest         (. :BufferedImage :new width height type)
        gd           (. dest :getGraphics)]
    (. gd :drawImage img 0 0 nil)
    (. gd :dispose)
    dest))


(defn- rgb-or-argb-image? [img]
  (contains? #{TYPE_INT_RGB, TYPE_INT_ARGB} (. img :getType)))


(defn- opaque? [img]
  (== (. img :getTransparency) (. :Transparency :OPAQUE)))


(defn- color-alpha? [color] 
  (not= (long (. color :getAlpha)) 255))




© 2015 - 2024 Weber Informatics LLC | Privacy Policy