Scrimage is a Scala image library for manipulating and processing of images. The aim of the this library is to provide a quick and easy way to do the kinds of image operations that most people need, such as scaling, rotating, converting between formats and applying filters. It is not intended to provide functionality that might be required by a more "serious" image application - such as face recognition or movement tracking.
A typical use case for this library would be creating thumbnails of images uploaded by users in a web app, or resizing a set of images to have a consistent size, or optimizing PNG uploads by users to apply maximum compression, or applying a grayscale filter in a print application.
Scrimage has a consistent, idiomatic scala, and mostly immutable API that builds on the java.awt.Image methods. I say mostly immutable because for some operations creating a copy of the underlying image would prove expensive (think an in place filter on a 8000 x 6000 image where you do not care about keeping the original in memory). For these kinds of operations Scrimage supports a MutableImage instance where operations that can be performed in place mutate the original instead of returning a copy.
These operations all operate on an existing image, returning a copy of that image. The more complicated operations have a link to more detailed documentation.
Operation | Description |
---|---|
autocrop | Removes any "excess" background, returning just the image proper |
bound | Ensures that the image is no larger than specified dimensions. If the original is bigger, it will be scaled down, otherwise the original is returned. This is useful when you want to ensure images do need exceed a certain size but you don't want to scale up if smaller. |
copy | Creates a new clone of this image with a new pixel buffer. Any operations on the copy do not write back to the original. |
cover | Resizes the canvas to the given dimensions and scales the original image so that it is the minimum size needed to cover the new dimensions without leaving any background visible. This operation is useful if you want to generate an avatar/thumbnail style image from a larger image where having no background is more important than cropping part of the image. Think a facebook style profile thumbnail. |
empty | Creates a new image but without initializing the data buffer to any specific values. |
filled | Creates a new image and initializes the data buffer to the given color. |
fit | Resizes the canvas to the given dimensions and scales the original image so that it is the maximum possible size inside the canvas while maintaining aspect ratio. This operation is useful if you want a group of images to all have the same canvas dimensions while maintaining the original aspect ratios. Think thumbnails on a site like amazon where they are padded with white background. |
flip | Flips the image either horizontally or vertically. |
filter | Returns a new image with the given filter applied. See the filters section for examples of the filters available. Filters can be chained and are applied in sequence. |
pad | Resizes the canvas by adding a number of pixels around the image in a given color. |
resize | Resizes the canvas to the given dimensions. This does not scale the image but simply changes the dimensions of the canvas on which the image is sitting. Specifying a larger size will pad the image with a background color and specifying a smaller size will crop the image. This is the operation most people want when they think of crop. |
rotate | Rotates the image clockwise or anti-clockwise. |
scale | Scales the image to given dimensions. This operation will change both the canvas and the image. This is what most people think of when they want a "resize" operation. |
trim | Removes a specified amount of pixels from each edge, essentially sugar to a crop operation. |
Reading an image, scaling it to 50% using the Bicubic method, and writing out as PNG
val in = ... // input stream
val out = ... // output stream
Image(in).scale(0.5, Bicubic).write(out) // Png is default
Reading an image from a java File, applying a blur filter, then flipping it on the horizontal axis, then writing out as a Jpeg
val inFile = ... // input File
val outFile = ... // output File
Image(inFile).filter(BlurFilter).flipX.write(outFile, Format.Jpeg) // specified Jpeg
Padding an image with a 20 pixel border around the edges in red
val in = ... // input stream
val out = ... // output stream
Image(in).pad(20, Color.Red)
Enlarging the canvas of an image without scaling the image (resize method changes canvas size, scale method scales image)
val in = ... // input stream
val out = ... // output stream
Image(in).resize(600,400)
Scaling an image to a specific size using a fast non-smoothed scale
val in = ... // input stream
val out = ... // output stream
Image(in).scale(300, 200, FastScale)
Writing out a heavily compressed Jpeg thumbnail
val in = ... // input stream
val out = ... // output stream
Image(in).fit(180,120).writer(Format.JPEG).withCompression(50).write(out)
Printing the sizes and ratio of the image
val in = ... // input stream
val out = ... // output stream
val image = Image(in)
println(s"Width: ${image.width} Height: ${image.height} Ratio: ${image.ratio}")
Converting a byte array in JPEG to a byte array in PNG
val in : Array[Byte] = ... // array of bytes in JPEG say
val out = Image(in).write // default is PNG
val out2 = Image(in).write(Format.PNG) // to be explicit about the output format
Coverting an input stream to a maximum compressed PNG
val in : InputStream = ... // some input stream
val out : OutputStream = ... // some output stream
val compressed = Image(in).writer(Format.PNG).withMaxCompression.write(out)
Scrimage supports loading and saving of images in the common web formats (currently png, jpeg, gif, tiff). In addition it extends jav'sa image.io support by giving you an easy way to compress / optimize / interlace the images when saving.
To load an image simply use the Image apply methods on an input stream, file, filepath (String) or a byte array. The format does not matter as the underlying reader will determine that. Eg,
val in = ... // a handle to an input stream
val image = Image(in)
To save a method, Scrimage provides an ImageWriter for each format it supports. An ImageWriter supports saving to a File, filepath (String), byte array, or OutputStream. The quickest way to use an ImageWriter is to call write() on an image, which will get a handle to an ImageWriter with the default configuration and use it for you. Eg,
val image = ... // some image
image.write(new File("/home/sam/spaghetti.png"))
If you want to override the configuration for a writer then you will need to get a handle to the writer itself using the writer() method which returns an ImageWriter instance. From here you can then configure it before writing. A common example would be optimising a PNG to use compression (uses a modified version of PngTastic behind the scenes). Eg,
val image = ... // some image
image.writer(Format.PNG).withCompression(9).write(new File("/home/sam/compressed_spahgetti.png"))
Note the writers are immutable and are created per image.
In version 1.1.0 support for asynchronous operations was added. This is achieved using the AsyncImage class. First, get an instance of AsyncImage from an Image or other source:
val in = ... // input stream
val a = AsyncImage(in)
Then any operations that act on that image return a Future[Image] instead of a standard Image. They will operate on the scala.concurrent implicit execution context.
... given an async image
val filtered = a.filter(VintageFilter) // filtered has type Future[Image]
A more complicated example would be to load all images instead a directory, apply a grayscale filter, and then re-save them out as optimized PNGs.
val dir = new File("/home/sam/images")
dir.listFiles().foreach(file => AsyncImage(file).filter(GrayscaleFilter).onSuccess {
case image => image.writer(Format.PNG).withMaxCompression.write(file)
})
Some noddy benchmarks comparing the speed of rescaling an image. I've compared the basic getScaledInstance method in java.awt.Image with ImgScalr and Scrimage. ImgScalr delegates to awt.Graphics2D for its rendering. Scrimage adapts the methods implemented by Morten Nobel.
The code is inside src/test/scala/com/sksamuel/scrimage/ScalingBenchmark.scala.
The results are for 100 runs of a resize to a fixed width / height.
Library | Fast | High Quality (Method) |
---|---|---|
java.awt.Image.getScaledInstance | 11006ms | 17134ms (Area Averaging) |
ImgScalr | 57ms | 5018ms (ImgScalr.Quality) |
Scrimage | 113ms | 2730ms (Bicubic) |
As you can see, ImgScalr is the fastest for a simple rescale, but Scrimage is much faster than the rest for a high quality scale.
Scrimage is available on maven central. There are several dependencies. One is the core library which is required. The others are filters and canvas. They are split because the image filters is a large jar, and most people just want the basic resize/scale/load/save functionality.
Include the canvas dependency if you want to be able to do AWT style graphics manipulating (drawRect, etc). Include the filters dependency if you need the image filters.
Scrimage is cross compiled for scala 2.11 and 2.10.
If using SBT then you want:
libraryDependencies += "com.sksamuel.scrimage" %% "scrimage-core" % "1.4.1"
libraryDependencies += "com.sksamuel.scrimage" %% "scrimage-canvas" % "1.4.1"
libraryDependencies += "com.sksamuel.scrimage" %% "scrimage-filters" % "1.4.1"
Maven:
<dependency>
<groupId>com.sksamuel.scrimage</groupId>
<artifactId>scrimage-core_2.11</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.sksamuel.scrimage</groupId>
<artifactId>scrimage-canvas_2.11</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.sksamuel.scrimage</groupId>
<artifactId>scrimage-filters_2.11</artifactId>
<version>1.4.1</version>
</dependency>
If you're using maven you'll have to adjust the artifact id with the correct scala version. SBT will do this automatically when you use %% like in the example above.
Scrimage comes with a wide array (or Iterable ;) of filters. Most of these filters I have not written myself, but rather collected from other open source imaging libraries (for compliance with licenses and / or attribution - see file headers), and either re-written them in Scala, wrapped them in Scala, fixed bugs or improved them.
Some filters have options which can be set when creating the filters. All filters are immutable. Most filters have sensible default options as default parameters.
Click on the small images to see an enlarged example.
Scrimage comes with the usual composites built in. This grid shows the effect of compositing palm trees over a US mailbox. The first column is the composite with a value of 0.5f, and the second column with 1f. Note, if you reverse the order of the images then the effects would be reversed.
The code required to perform a composite is simply.
val composed = image1.composite(new XYZComposite(alpha), image2)
Click on an example to see it full screen.
Composite | Alpha 0.5f | Alpha 1f |
---|---|---|
average | ||
blue | ||
color | ||
colorburn | ||
colordodge | ||
diff | ||
green | ||
grow | ||
hue | ||
hard | ||
heat | ||
lighten | ||
negation | ||
luminosity | ||
multiply | ||
negation | ||
normal | ||
overlay | ||
red | ||
reflect | ||
saturation | ||
screen | ||
subtract |
This software is licensed under the Apache 2 license, quoted below.
Copyright 2013 Stephen Samuel
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.