# NodeBox

Create visual output with Python programming code
Home Download Reference Tutorial Library Gallery About

# Core Image

## Description

The Core Image library for NodeBox adds image manipulation to NodeBox. It's like having control over Photoshop through simple Python programming commands. Core Image is a Mac OS X specific framework available from Mac OS X 1.4 (Tiger). Core Image uses hardware acceleration whenever possible. You can apply filters and transformations to images with a real-time and interactive response. And, since all of the image manipulations are stored in a script, the process is non-destructive. The source images remain unchanged. The script is a recipe you can reuse on different source images.

## Download

| coreimage.zip (1.4MB) Last updated for NodeBox 1.8.4

## Documentation

- How to get the library up and running
- Creating a new canvas
- Adding new layers to the canvas
- Retrieving and arranging layers
- Moving and transforming layers
- Adjusting layer brightness, contrast and saturation
- Layer opacity and blending modes
- Applying alpha masks
- Layer filters
- Working with layer pixels
- Exporting the canvas to JPEG, GIF, PNG or TIFF
- Drawing the canvas in NodeBox
- Working with helpers and interfaces
- Working with images from MorgueFile
- Working with dynamics and caching
- Known issues and limitations

## How to get the library up and running

Put the downloaded coreimage.py library in the same folder as your script so NodeBox can find the library. It takes some time to load the library the first time.

    coreimage = ximport("coreimage")

## Creating a new canvas

The canvas() command returns a new, empty canvas. It takes two parameters: the width and height of the canvas. You can set an optional quality parameter to "high" to use 128-bit pixel depth but this is unnecessary in most cases.

    canvas = coreimage.canvas(150, 150)

## Adding new layers to the canvas

A canvas is a container of many layers which you can position, scale , rotate, blend, filter, etc. You can create layers from image files, fill colors, linear or radial gradients, paths (ovals, text, ...) and other canvases (i.e. a group of layers).

Consider we have the following images, leaf.jpg and lily.tif.

leaf.jpg | lily.tif

Core Image can use JPEG, GIF, PNG and TIFF images. If you are using images with an alpha channel the best way to import them is as transparent TIFF and not as a PNG (which can have jagged edges in Core Image).

| A canvas with two image layers is easy enough:
    l = canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    canvas.draw(0, 0)
New layers are centered on the canvas by the default. Optional parameters are x and y position in pixels and w and h dimensions in pixels. The command returns the layer object.
| Opaque fill colors are created with the canvas.layer_fill() command which has a color parameter. This can be a standard NodeBox color() but the best results are achieved with the coreimage.color() command. This command takes RGBA color values ranging between 0 and 255 or between 0.0 and 1.0.
    l = canvas.layer_fill(coreimage.color(1.0, 0.0, 0.5))
| The canvas.layer_path() command creates layers from NodeBox paths. It takes four optional parameters: foreground, background, stroke (black, transparent and transparent colors by default) and strokewidth.
    l = canvas.layer_fill(coreimage.color(1.0, 0.0, 0.5))
    font("Georgia-Italic", 40)
    p = textpath("text", 0, 0)
    l = canvas.layer_path(p, foreground=color(1))
    l = canvas.layer_linear_gradient()
    l = canvas.layer_radial_gradient()
Both linear and radial gradients have optional color1 and color2 parameters, which are white and black by default. Additionally, a radial gradient has an optional spread parameter ranging between 0.0 and 1.0. This parameter controls the opacity of the gradient center.
| The canvas.layer_group() command takes a canvas object as input - a group of layers that stick together:
    l = canvas.layer_fill(coreimage.color(0.0))
    group = coreimage.canvas(100,100)
    group.layer("leaf.jpg")
    group.layer("lily.tif")
    l = canvas.layer_group(group)
    l.rotate(45)

Existing layers in a canvas can be duplicated. The copy appears right above the duplicated layer:

    l = canvas.layer("leaf.jpg")
    dupl = l.duplicate()

You can copy layers from one canvas and append them to another canvas:

    offscreen = coreimage.canvas(150,150)
    l = offscreen.layer("leaf.jpg").copy()
    canvas.layers.append(l)

You can check what type of layer you are working with using the following commands:

- layer.is_file(): a layer created from an image file
- layer.is_fill(): an opaque fill color
- layer.is_gradient(): a linear or radial gradient
- layer.is_linear_gradient(): a linear gradient
- layer.is_radial_gradient(): a radial gradient
- layer.is_path(): a layer created from a NodeBox path
- layer.has_layers(): a layer created from a canvas object
- layer.is_mask(): a layer that is part of the alpha mask of another layer

    l = canvas.layer("leaf.jpg")
    print l.is_file()
    >>> True

## Retrieving and arranging layers

All of the layers are stored in the canvas.layers list. With a for-loop you can address each layer in the list:

    for i in range(20):
        canvas.layer("lily.tif")

    for layer in canvas.layers:
        layer.x = random(canvas.w)
        layer.y = random(canvas.h)

Layers can be named and easily retrieved later on. Each of the layer creation commands has an optional name parameter. You can then search the canvas for layer names with the canvas.find() command.

    canvas.layer("leaf.jpg", name="bg")
    canvas.layer("lily.tif")
    l = canvas.find("bg")
    print l.name, l.x, l.y
    >>> bg, 75, 75

Each new layer is added to the top of the canvas. You can change the ordering of a layer with the following commands:

- layer.arrange_up(): moves the layer up one position in the layer stack
- layer.arrange_down(): moves the layer down one position in the layer stack
- layer.arrange_front(): moves the layer to the foreground of the canvas
- layer.arrange_back(): moves the layer to the background of the canvas

    back = canvas.layer("leaf.jpg")
    front = canvas.layer("lily.tif")
    back.arrange_front()

The layer.index() returns the layer's index in the canvas.layers list. Like any list in NodeBox the indices start from zero:

    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    print l.index()
    >>> 1

The layer.hide() excludes the layer from the output:

    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.hide()

## Moving and transforming layers

Individual layers in the canvas can be moved, scaled, rotated, flipped and distorted.

| You can use a layer's x and y properties to move it around. These properties store the layer's position measured in pixels from the top-left of the canvas.
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.x = 0
    l.y = 0
| The same can be achieved using the layer.translate() command. This command accepts two parameters, either absolute pixel values or relative positions between 0.0 and 1.0.
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.translate(1.0, 1.0)
| You can scale images using pixel size or relative size. By supplying two parameters you can scale width and height individually.
    canvas.layer("leaf.jpg")
    for i in range(10):
        l = canvas.layer("lily.tif")
        l.x = i*20
        l.scale(1.0 - i*0.1)
    canvas.layer("leaf.jpg")
    for i in range(10):
        l = canvas.layer("lily.tif")
        l.rotate(i*10)
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif").flip_vertical()
There's also the layer.flip_horizontal() command of course.
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.distort(dy0=-500, dx2=-100)
The layer.distort() command has optional dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3 parameters setting the displacement of the left top, right top, right bottom and left bottom corners of the layer (in pixels or between 0.0 and 1.0).

By default, all layer transformations originate from the layer's center. However, you can change the layer's origin point, have the layer rotate from the bottom right corner for example. Let's turn on some of the visual aids in the Core Image library for a minute:

    canvas.draw(0,0, helper=True)

We can clearly see the layer's bounding box and how it is positioned around the central origin point. The layer rotates from the center. When we call layer.origin_bottom_left() to change the layer's origin point to the bottom left, rotations will originate from over there. Also, now the layer's bottom left is at the center of the canvas instead of the layer's center. Think of the origin point as the layer's anchor/pivot/orbit.

The following commands set the origin point:

- layer.origin_top_left()
- layer.origin_top_right()
- layer.origin_top_center()
- layer.origin_bottom_left()
- layer.origin_bottom_right()
- layer.origin_bottom_center()
- layer.origin_left_center()
- layer.origin_right_center()
- layer.origin_center()

Or you can just use layer.origin() to set it yourself:

    print l.origin(0.23, 1.07)
    >>> (0.23, 1.07)

To get the bounding box coordinates of layer, use layer.bounds():

    print l.bounds()
    >>> (200.0, 78.647456721610681, 409.52033810627165, 288.167787843871)

To get the width and height of a transformed layer, use layer.size(). Note that when you manipulate the layer with filters the actual rendered pixels may fall beyond the given bounds.

    w, h = l.size()
    print w, h
    >>> (209.52033810627165, 209.5203311222603)

## Adjusting layer brightness, contrast and saturation

Aside from placing and transforming multiple layers in a canvas, we can also modify the color information in each layer.

| The layer.adjust_brightness() command takes one parameter ranging between -1.0 and 1.0:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.adjust_brightness(0.25)
    print l.brightness
    >>> 0.25
| The layer.adjust_contrast() command takes one parameter ranging between 0.25 and 4.0:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.adjust_contrast(1.5)
    print l.contrast
    >>> 1.5
| The layer.adjust_saturation() command takes one parameter ranging between 0.0 and 2.0:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.adjust_saturation(2.0)
    print l.saturation
    >>> 2.0
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.desaturate()
    print l.saturation
    >>> 0.0
    l = canvas.layer("leaf.jpg")
    l.invert()
    l = canvas.layer("lily.tif")
    l.invert()

You can set a combination of brightness, contrast and saturation with the layer.adjust() command, which has three optional parameters brightness, contrast and saturation.

## Layer opacity and blending modes

Layers can be composited by adjusting their individual transparency or by applying blend modes to create all sorts of effects.

| The layer.blend() command takes one parameter ranging between 0 and 100 or between 0.0 and 1.0:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.blend(25)
    print l.opacity
    >>> 0.25
    canvas.layer("leaf.jpg")
    for i in range(10):
        l = canvas.layer("lily.tif")
        l.scale(0.5)
        l.x = random(canvas.w)
        l.y = random(canvas.h)
        l.blend(random())
| Screening keeps the pixels that are lighter than those underneath.
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif", x=50)
    l = canvas.layer("lily.tif", x=100)
    l.blend_screen()
| Multiplying keeps the pixels that are darker than those underneath.
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif", x=50)
    l = canvas.layer("lily.tif", x=100)
    l.blend_multiply()
| Overlay uses a combination of screen and multiply.
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif", x=50)
    l = canvas.layer("lily.tif", x=100)
    l.blend_overlay()
| Soft light uses a combination of darken and soften.
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif", x=50)
    l = canvas.layer("lily.tif", x=100)
    l.blend_softlight()
| Hard light uses a combination of screen and multiply.
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif", x=50)
    l = canvas.layer("lily.tif", x=100)
    l.blend_hardlight()
| Applying the hue of the blend layer to layers underneath:
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif")
    l = canvas.layer_fill(coreimage.color(1.0,0.0,0.5))
    l.blend_hue(50)
| Applying the hue and saturation of the blend layer to layers underneath:
    canvas.layer("leaf.jpg")
    canvas.layer("lily.tif")
    l = canvas.layer_fill(coreimage.color(1.0,0.0,0.5))
    l.blend_color(50

Use the layer.blendmode property to check what blending mode the layer is using. The returned value is coreimage.BLEND_SCREEN, or coreimage.BLEND_MULTIPLY, etc.

    print l.blendmode
    print (l.blendmode == coreimage.BLEND_COLOR)
    >>> color
    >>> True

You can set the opacity and blend mode together with the layer.blend() command:

    l.blend(25, mode="color")
    opacity, mode = l.blend
    print opacity, mode
    >>> 0.25, color

The layer.blend_normal() command removes any blend mode from the layer.

## Applying alpha masks

The layer's alpha mask stores information where the layer shines through and where it is opaque. Since the layer.mask property is a canvas object, it's just as easy to work with alpha layers than it is with normal layers. You can position, transform, adjust, blend and filter them just as any other layer.

| Applying a linear gradient alpha mask to the lily:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    m = l.mask.layer_linear_gradient()
    m.rotate(45)
| Using a NodeBox path as a mask:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    font("Georgia-Italic", 50)
    p = textpath("text", 0, 0)
    m = l.mask.layer_path(p)
    m.y += 25
| Using the lily as a mask for a gradient layer:
    canvas.layer_fill(coreimage.color(0.4,0.0,0.2))
    pink = coreimage.color(1.0,0.0,0.5)
    white = coreimage.color(1.0,1.0,1.0)
    l = canvas.layer_radial_gradient(pink, white)
    l.mask.layer("lily.tif")
| Complex mask made up of multiple layers:
    l = c.layer_radial_gradient(pink, white)
    for i in range(3):
        m = l.mask.layer("lily.tif")
        m.scale(1-i*0.4)
        m.rotate(20*i)
        m.blend_softlight()
        m.filter_zoomblur(amount=i*25)

## Layer filters

The Core Image library supports a number of filters that manipulate the pixels in a layer. The layer.filter_blur() and layer.filter_sharpen() are standard filters.

| The layer.filter_blur() takes one parameter ranging between 0.0 and 100.0:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_blur(2)
| Sharpening the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_sharpen(10)
| Applying a zoom blur to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_zoomblur(dx=25)

There are many other filters like the zoom blur above. Each filter command takes its own optional parameters to tweak:

Zoom blur
Simulates the effect of zooming the camera while capturing the layer. Parameters are dx, dy and amount ranging between 0.0 and 200.0.

    layer.filter_zoomblur(amount=20, dx=0, dy=0)

Motion blur
Simulates a directional blur. Parameters are radius between 0.0 and 100.0 and angle.

    layer.filter_motionblur(radius=20, angle=0)

Bump distortion
Creates a bump or pinch in the layer. Parameters are dx, dy, radius between 0.0 and 600.0 and scale between -1.0 and 1.0.

    layer.filter_bumpdistortion(dx=0, dy=0, radius=300, scale=0.5)

Hole distortion
Pushes the layer's pixels outwards. Parameters are dx, dy, and a radius between 0.0 and 1000.0.

    layer.filter_holedistortion(dx=0, dy=0, radius=300)

Twirl
Generates a wave motion in the layer. Parameters are dx, dy, radius between 0.0 and 500.0 and angle.

    layer.filter_twirl(dx=0, dy=0, radius=150, angle=100)

Kaleidoscope
Generates fractal-like output. Parameters are dx, dy and count between 0 and 100.

    layer.filter_kaleidoscope(count=10, dx=0, dy=0)

Perspective tile
Distorts the base layer and copies that on a plane. Parameters are dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3 representing the corners of the layer to distort.

    layer.filter_perspectivetile(dx0=0, dy0=0, dx1=0, dy1=0,
                                 dx2=0, dy2=0, dx3=0, dy3=0)

Starshine
A generator that creates stars and sunbursts on the layer (which is usually a transparent color). Parameters are dx, dy, radius between 0.0 and 300.0, x_scale between 0.0 and 100.0, x_angle, x_width and epsilon between -8.0 and 8.0.

    layer.filter_starshine(dx=0, dy=0, radius=25,
                           x_scale=10, x_angle=0, x_width=0.5, epsilon=-5.0)

Bloom
Adds a hazy glow to the layer by softening the edges. Parameters are radius between 0.0 and 100.0 and intensity between 0.0 and 1.0.

    layer.filter_bloom(radius=10, intensity=1.0)

Lighting
Adds a light source to the layer. Parameters are dx0, dy0, dz0 between 0 and 1000 represting the height of the light source, dx1 and dy1 representing where the light is shining, brightness between 0.0 and 10.0, concentration between 0.0 and 1.0 representing the cone of the light beam and color.

    layer.filter_lighting(dx0=100, dy0=100, dz0=300, dx1=0, dy1=0,
                          brightness=3.0, concentration=0.3,
                          color=None, helper=False)

Shading
Adds texturing to the layer. You can pass it a canvas object to apply as a texture. Parameters are dx, dy, radius between 0.0 and 300.0 and texture which is a canvas object.

    layer.filter_shading(radius=20, texture=None, dx=0, dy=0)

Levels
Provides control over each RGBA color channel in the layer. Parameters are r, g, b and a which can be numbers between 0.0 and 1.0 or four-tuples if you want to do complex things like increase the blue inside the R channel.

    layer.filter_levels(r=1.0, g=1.0, b=.0, a=.0)

| Applying the zoom blur to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_zoomblur(dy=25)
| Applying the motion blur to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_motionblur(angle=45, radius=50)
| Applying bump distortion to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_bumpdistortion(scale=0.75, dy=-35)
| Applying hole distortion to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_holedistortion(radius=40)
| Applying the dreaded twirl filter to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_twirl(radius=150)
| Applying a kaleidoscope to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_kaleidoscope(count=5)
| Applying a perspective tile to the layer:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.scale(0.6)
    l.filter_perspectivetile(dx0=50, dy1=30, dy2=-50)
| Generating starshine:
    canvas.layer("leaf.jpg")
    l = canvas.layer_fill(coreimage.transparent())
    l.filter_starshine(x_width=0.3)
| Applying a bloom filter:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_bloom()
| Applying lighting:
    canvas.layer_fill(color(0))
    l = canvas.layer("lily.tif")
    l.filter_lighting(dx1=-55, dy0=55, dz0=45)
| Applying shading:
    canvas.layer_fill(color(0))
    p = supershape.path(50, 60, 60, 60, 12, 2, 0.7, -2)
    l = canvas.layer_path(p, foreground=color(1))
    l.filter_shading(dx=31, radius=5.0)
    canvas.layer_fill(color(0))
    p = supershape.path(50, 60, 60, 60, 12, 2, 0.7, -2)
    l = canvas.layer_path(p, foreground=color(1))
    t = coreimage.canvas(150, 150)
    t.layer("lily.tif")
    l.filter_shading(dx=31, radius=20.0, texture=t)
| Applying color level adjustments:
    l = canvas.layer("leaf.jpg")
    l.filter_levels(g=0.6, b=1.6)
| Using levels to create a dropshadow:
    canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    l.filter_levels(r=0, g=0, b=0)
    l.filter_blur(2)
    l.blend(75)
    l.x += 10; l.y += 10
    l = canvas.layer("lily.tif")

All the information about filters and their parameters is stored in the canvas renderer. Take a look at:

    print canvas.renderer.aliases.keys()
    >>> ['starshine', 'shading', 'bumpdistortion', 'zoomblur', 'motionblur',
    >>>  'levels', 'twirl', 'perspectivetile', 'lighting',
    >>>  'bloom', 'kaleidoscope']

    print canvas.renderer.aliases["levels"]
    >>> CIColorMatrix

    print canvas.renderer.filters["CIColorMatrix"]
    >>> {'a': 1.0, 'interface': False, 'r': 1.0, 'b': 1.0, 'g': 1.0}

## Working with layer pixels

You can access individual pixels in a layer or retrieve information about the minimum, maximum and average pixel values. Right now you cannot modify the pixel information, only read it. Also remember that pixel operations are memory intensive.

    l = canvas.layer("leaf.jpg")
    p = l.pixels()

    print p.is_rgb()
    >>> True

    print p.has_alpha()
    >>> False

    print "min:", p.min()
    print "max:", p.max()
    print "avg:", p.average()
    >>> min: [6, 7, 0]
    >>> max: [113, 137, 127]
    >>> avg: [46, 78, 48]

    print p.get_pixel(100, 100)
    >>> [22, 29, 7]

As a fun experiment we can transform the pixels to NodeBox vector rectangles. We measure the RGB values in every tenth pixels and apply those colors to a grid of 10x10 rectangles:

The pixels.histogram() extracts a histogram from the layer's pixel data. The return value is a list with either three (R, G and B) or four (R, G, B, A) elements. Each element is a list of 254 elements ranging between 0.0 and 1.0. For example: if pixels.histogram()[0][100] would be 0.0, this means that the layer has no pixels where the red is 100. If pixels.histogram()[1][10] would be 1.0, this means that the layer's pixels usually have a green value of 10.

Let's have a look at the pixel values of the leaf image using another one of the visual aids in the Core Image library:

## Exporting the canvas to JPEG, GIF, PNG or TIFF

It's easy to export the canvas as an compressed image for the web or a TIFF with CMYK colors. If your image has few different colors (e.g. opaque color planes with text) you can use export a lossless GIF for the web. If your image contains photos, use a JPEG with compression. When you are going to paper print your image, use a CMYK TIFF.

| A JPEG for the web with 40% compression:
    canvas = coreimage.canvas(150, 150)
    l = canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    canvas.export_jpg("flower", quality=0.6)
| A TIFF in CMYK with LZW-compression:
    canvas = coreimage.canvas(150, 150)
    l = canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    canvas.export_tif("flower", cmyk=True, lzw=True)
| A PNG image with a transparent background:
    canvas = coreimage.canvas(150, 150)
    l = canvas.layer("lily.tif")
    canvas.export_png("flower")
| A GIF image file:
    canvas = coreimage.canvas(150, 150)
    l = canvas.layer_fill(coreimage.color(1.0, 0.0, 0.5))
    font("Georgia-Italic", 40)
    p = textpath("text", 0, 0)
    l = canvas.layer_path(p, foreground=color(1))
    canvas.export_gif("flower")

Some important things to know:

- The Core Image library is currently not supporting LZW-compression for CMYK TIFF images. You can create a TIFF in RGB with (if you want) a transparent background and use compression, but once you want it in CMYK the LZW parameter is ignored.

- A generic CMYK profile is used. This means that the canvas renderer makes no attempt to correctly remap colors that are outside the CMYK scope: pure RGB red and other high intensity colors may contain pixel errors. This is especially true for layers created from paths. It is your responsibility to create paths in CMYK-safe colors. Usually everything is OK so long as you use paths created with NodeBox color() colors.

## Drawing the canvas in NodeBox

Merging all the layers in a canvas and drawing the result in NodeBox is easy:

    canvas = coreimage.canvas(150, 150)
    l = canvas.layer("leaf.jpg")
    l = canvas.layer("lily.tif")
    canvas.draw(0,0)

The canvas.draw() command takes two parameters: the x and y position.

Note that when you export a PDF that contains a canvas with a transparent background it might have pixel errors.

## Working with helpers and interfaces

Some things in the Core Image library are a little hard to do or grasp without visual feedback. That's why the library has a few helpers and interfaces. There's a helper for the canvas.draw() command that displays rulers and a bounding box for each of the layers. It's already shown in the documentation on transforming layers. There's also a helper for the pixels histogram shown in the documentation on working with pixels.

Furthermore, there's a helper for the lighting filter. The helper makes it a bit easier to determine where the light source is positioned and what parameters like dz0 and concentration do.

With each filter you can use the interface parameter to get NodeBox sliders that control the options for the filter. It's a convenient way to get to know how each filter works.

Notice how each parameter you normally define in code is now replaced by a handy slider.

## Working with images from MorgueFile

The Core Image library includes a tool to download images from MorgueFile. MorgueFile is an online archive of freely contributed photographs. To use MorgueFile images in NodeBox, you need to add the morguefile.py library to the folder where your script is located. You can then query MorgueFile and download images to a specified folder.

The morguefile.query() command returns a list of images on MorgueFile on the given query, It has an optional parameter max specifying the maximum number of images to return.

    morguefile = ximport("morguefile")
    images = morguefile.query("leaf", max=10)

Each item in the returned list has a number of properties:

- img.id: the unique MorgueFile ID for the image
- img.category: the category the image belongs to
- img.author: the name of the author
- img.name: the image's name
- img.url: the URL of the image thumbnail
- img.date: the date the image was added to MorgueFile
- img.views: the number of times the image has been viewed
- img.downloads: the number of times the image has been downloaded

    for img in images:
        print img.name, img.views
    >>> DSCN6707.JPG 8
    >>> keegan_2006_1015random0106.JPG 10
    >>> Img2006-10-08-132310.jpg 611
    >>> climb_a_tree_2006_1015random0086.JPG 17
    >>> missyredboots111.jpg 14
    >>> IMG_6852.jpg 403
    >>> Fallen_Star.JPG 25
    >>> JGS_mF_FloatingInDarkness.jpg 30
    >>> leaf_n.JPG 18
    >>> drift_away__.jpg 103

You can pass any of these image objects to the morguefile.download() command. This command takes two optional parameters: path specifying in what folder to put the image (if the folder doesn't exist it will be created) and thumbnail, which when True downloads a small thumbnail instead of the image itself.

The command returns the pathname of the image, which is the specified path + the name of the author + the name of the image.

    print morguefile.download(images[0], "/images")
    >>> images/dineshtilva_DSCN6707.JPG

You can then start using the image as a canvas layer.

There's also the morguefile.query_by_author() command which looks for images by author name.

## Working with dynamics and caching

You can create a cached screenshot from a layer or a canvas. This means Core Image will render the layer or canvas with all its transformations, masks, adjustments and filters and return it as an object stored in memory instead of drawing it to the screen. You can then reuse that object as a layer in a canvas. This way you can incrementally stack effects in a fast animation.

The layer.render() and canvas.render() commands return a cached screenshot. You can pass this screenshot to the canvas.layer() command. Then you can stack new effects on the transformed layer, take a screenshot of it, apply more filters to it, and so on.

    speed(30)
    def setup():
        global cache
        cache = None

    def draw():
        canvas = coreimage.canvas(WIDTH, HEIGHT)
        global cache
        if not cache:
            cache = canvas.layer("leaf.jpg")
        else:
            cache = cache.render()
            cache = canvas.layer(cache)
            cache.filter_twirl(radius=random(30,40),
                               dx=random(-40,40),
                               dy=random(-40,40))

        canvas.draw()

Try out some other examples bundled in the library download. You may notice that incrementally applying filters and effects to layers makes them blurry.

## Known issues and limitations

The library is currently in alpha stage and has some known issues and limitations:

- Exporting a PDF with a canvas that has a transparent background might contain pixel errors.

- Exporting a CMYK TIFF uses a Generic CMYK profile. This means that the canvas renderer makes no attempt to correctly remap colors that are outside the CMYK scope: pure RGB red and other high intensity colors may contain pixel errors. This is especially true for layers created from paths. It is your responsibility to create paths in CMYK-safe colors. Usually everything is OK so long as you use paths created with NodeBox color() colors.

- Using the layer.distort() command in combination with layer filters displaces the layer from the origin, because the bounding box of a layer does not take filters into account. This means that the layer will no longer correctly display at the position you specified. An easy way to overcome this is to call layer.filters_first(False) to first render the distort and then apply the filter.

- Combining different filters might sometimes produce no output or crash NodeBox. If you are experiencing strange behaviour, try quitting and reopening NodeBox so the Core Image library has a chance to reload itself.

- Some filters are kinda quirky, especially the motion blur filter and perspective tile.

- If you rotate a canvas drawn in NodeBox using transform(CORNER) it rotates from the bottom left corner instead of the top left corner.

- Core Image might run very slow or even halt NodeBox when exporting multiple PDF's or animations from a complex canvas.