This readme is for the 4.0.0 release. For earlier releases see here.
Scrimage is immutable, functional, and performant JVM library for manipulation of images. The aim of the this library is to provide a simple and concise way to do the kinds of image operations that are most common, such as scaling to fit a size or bounds, rotating, converting between formats and applying filters. It is not intended to provide functionality that might be required by a more "serious" image processing 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 mostly builds on the functionality provided by java.awt.* along with selected other third party libraries.
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 |
blank | Creates a new image but without initializing the data buffer to any specific values. |
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. |
fill | Creates a new image and initializes the data buffer to the given color. |
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. |
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. |
max | Returns an image that is as large as possible to fit into the specified dimensions whilst maintaining aspect ratio and without adding padding. Note: The difference between this and fit, is that fit will pad out the canvas to the specified dimensions, whereas max will not |
overlay | Returns a new image which is the original image plus a specified one overlaid on top |
pad | Resizes the canvas by adding a number of pixels around the edges 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. |
translate | Returns a new image with the original image translated (moved) the specified number of pixels |
trim | Removes a specified amount of pixels from each edge, essentially sugar to a crop operation. |
underlay | Returns a new image which is the original image overload on top of the specified image |
Reading an image, scaling it to 50% using the Bicubic method, and writing out as PNG
val in = ... // input stream
val out = ... // output stream
ImmutableImage.loader().fromStream(in).scale(0.5, Bicubic).output(out) // an implicit PNG writer is in scope by 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
ImmutableImage.loader().fromFile(inFile).filter(BlurFilter).flipX.output(outFile)(JpegWriter()) // specified Jpeg
Padding an image with a 20 pixel border around the edges in red
val in = ... // input stream
val out = ... // output stream
ImmutableImage.loader().fromStream(in).pad(20, Color.Red)
Enlarging the canvas of an image without scaling the image. Note: the resize methods change the canvas size, and the scale methods are used to scale/resize the actual image. This terminology is consistent with Photoshop.
val in = ... // input stream
val out = ... // output stream
ImmutableImage.loader().fromStream(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
ImmutableImage.loader().fromStream(in).scaleTo(300, 200, FastScale)
Writing out a heavily compressed Jpeg thumbnail
implicit val writer = JpegWriter().withCompression(50)
val in = ... // input stream
val out = ... // output stream
ImmutableImage.loader().fromStream(in).fit(180,120).output(new File("image.jpeg"))
Printing the sizes and ratio of the image
val in = ... // input stream
val out = ... // output stream
val image = ImmutableImage.loader().fromStream(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 = ImmutableImage.loader().fromBytes(in).bytes) // an implicit PNG writer is in scope by default with max compression
Coverting an input stream to a PNG with no compression
implicit val writer = PngWriter.NoCompression
val in : InputStream = ... // some input stream
val out = ImmutableImage.loader().fromStream(in).stream
Scrimage supports loading and saving of images in the common web formats (currently png, jpeg, gif, tiff). In addition it extends javas 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 companion 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 = ImmutableImage.loader().fromStream(in)
To save a method, Scrimage requires an ImageWriter. You can use this implicitly or explicitly. A PngWriter is in scope by default.
val image = ... // some image
image.output(new File("/home/sam/spaghetti.png")) // use implicit writer
image.output(new File("/home/sam/spaghetti.png"))(writer) // use explicit writer
To set your own implicit writer, just define it in scope and it will override the default:
implicit val writer = PngWriter.NoCompression
val image = ... // some image
image.output(new File("/home/sam/spaghetti.png")) // use custom implicit writer instead of default
If you want to override the configuration for a writer then you can do this when you create the writer. Eg:
implicit val writer = JpegWriter().withCompression(50).withProgressive(true)
val image = ... // some image
image.output(new File("/home/sam/compressed_spahgetti.png"))
Scrimage builds on the metadata-extractor project to provide the ability to read metadata.
This can be done in two ways. Firstly, the metadata is attached to the image if it was available when you loaded the image
from the Image.fromStream
, Image.fromResource
, or Image.fromFile
methods. Then you can call image.metadata
to get
a handle to the metadata object.
Secondly, the metadata can be loaded without an Image being needed, by using the methods on ImageMetadata.
Once you have the metadata object, you can invoke directories
or tags
to see the information.
If you are interested in detecting the format of an image (which you don't need to do when simply loading an image, as Scrimage will figure it out for you) then you can use the FormatDetector. The detector recognises PNG, JPEG and GIF.
FormatDetector.detect(bytes) // returns an Option[Format] with the detected format if any
FormatDetector.detect(in) // same thing from an input stream
Apple iPhone's have this annoying "feature" where an image taken when the phone is rotated is not saved as a rotated file. Instead the image is always saved as landscape with a flag set to whether it was portrait or not. Scrimage will detect this flag, if it is present on the file, and correct the orientation for you automatically. Most image readers do this, such as web browsers, but you might have noticed some things do not, such as intellij.
Note: This can be disabled by setting detectOrientation(false)
on the ImmutableImage.loader()
instance.
There is a full list of X11 defined colors in the X11Colorlist
class. These can be used and converted to AWT Color
when you need more than the defaults built into the JDK.
For example,
val awt = X11Colorlist.MistyRose.awt()
Scrimage supports animated GIFs using the StreamingGifWriter
class. First we create an instance of the writer, specifying the delay between images and if we should loop.
Then we open a stream, specifying the output path and the AWT image type.
val writer = StreamingGifWriter(Duration.ofSeconds(2), true)
val gif = writer.prepareStream("/path/to/gif.gif", BufferedImage.TYPE_INT_ARGB)
Next we can add as many images as we want, each an instance of ImmutableImage
. Eg,
gif.writeFrame(image0)
gif.writeFrame(image1)
gif.writeFrame(imageN)
Finally we close the stream and the GIF is persisted to disk.
gif.finish()
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 scrimage-core
library which is required. The others are scrimage-filters
and scrimage-formats-extra
.
And if you're using Scala you can also add scrimage-scala_2.12
or scrimage-scala_2.13
for enhanced scala functions.
They are split because the image filters is a large jar, and most people just want the basic resize/scale/load/save functionality.
Thescrimage-formats-extra
package brings in readers/writers for less common formats such as BMP, Tiff or PCX. These formats are wrappers
around the incredible TwelveMonkeys library
Note: The canvas operations are now part of the core library since 2.0.1
If using gradle, then you want:
implementation("com.sksamuel.scrimage:scrimage-core:<version>")
Else if using SBT then:
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "$version"
Or finally if Maven then use:
<dependency>
<groupId>com.sksamuel.scrimage</groupId>
<artifactId>scrimage-core</artifactId>
<version>${version}</version>
If you are using Scala, then you can add the module "com.sksamuel.scrimage" %% "scrimage-scala_2.x" % "$version"
to
your build. Then adding import com.sksamuel.scrimage.scala._
will bring into scope some useful implicits.
Firstly, an implicit PNGWriter
so you do not have to specify it when outputting images. Secondly, a conversion to / from
java.awt.Color
and Scrimage's RGBColor
. Lastly, forall, foreach and map methods on ImmutableImage
which work with Scala
functions.
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-2020 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.