JMagick - Scripted Image Processing with JSON

Jmagick is a JSON-based pseudo-language for scripting image manipulation. It is meant to provide image processing capabilities to other servers via an API. Using the API a client can supply an image ID and a set of image processing operations, which the server will interpret and return a permanent URL to the desired generated image. This is useful in web server environments where web pages are generated dynamicly by script. It eliminates the need for graphic designers to manually tailor and crop and resize every image by hand — give Jmagick the desired processing operations and it will produce images that seemlessly fit into any use on the web every time.

Basic Usage

Usually the requirements are basic and oriented towards web distribution, for example: you have a high resolution photograph that you want to serve on the web at a smaller size; you have a collection of photographs you want to present thumbnails for which need to be cropped because the thumbnail aspect ratio does not match the source aspect ratio; you have a photograph that needs to be rotated 90 degrees; you need to place a watermark over the image.

[{
   "type":"resize",
   "width":300
}]
[{
   "type":"rotate",
   "degrees":90
}]
[{
   "type":"composite",
   "filename":"img/random/logo.png",
   "gravity":"southeast"
}]
[{
   "type":"crop",
   "width":300,
   "gravity":"center"
}]

However, since Jmagick is powered by ImageMagick, a renowned command-line image processing tool, it is capable of much more than just these basic web-oriented uses. Let's say you have a smartphone app which enables users to take photos, apply basic filters to them, and share them with friends. Let's say you want to make your photo look happy:

[{
   "type":"gamma",
   "green":2
}]

or sad:

[{
   "type":"modulate",
   "hue":180
}]

you can do that easily with Jmagick. There are more novel operation types too:

[{
   "type":"swirl",
   "degrees":45
}]
[{
   "type":"roll",
   "x":225,
   "y":150
}]

And of course, you can combine as many operations as you want:

[
   {
      "type":"modulate",
      "hue":180
   },
   {
      "type":"swirl",
      "degrees":45
   }
]

With Jmagick just about any batch image manipulation you need can be done on the fly — and it's extensible too. So if you need a new operation, or you want to define a new function or create a new macro-operation or global variable, that is all possible and easy. (Sorry, the code is closed-source for now!)

Now that you know the basic building blocks of Jmagick, let's move on to Advanced Usage.

Advanced Usage

To demonstrate the more advanced features of Jmagick we will be generating art using Pokemon. Why Pokemon? Because they are widely recognizable serial objects which lend themselves well both to programmatic demonstration and visual appeal. (Note, we will only be using the original 151 Pokemon here.)

Let's say you want to generate an image which selects 25 random Pokemon and overlays them randomly on the canvas:

[{
   "type":"loop",
   "start":1,
   "end":"25",
   "interval":1,
   "operations":[{
      "type":"composite",
      "filename":"img/pokemon/sprite/redblue/front/rand(1,151).png",
      "offset":{
         "x":"rand(-20, 320)",
         "y":"rand(-20, 320)"
      }
   }]
}]

You accomplish this by using the built-in rand function. You can run the Jmagick script repeatedly and it will give you a different image every time. But 25 is not enough. Let's try 300:

[{
   "type":"loop",
   "start":1,
   "end":"300",
   "interval":1,
   "operations":[{
      "type":"composite",
      "filename":"img/pokemon/sprite/redblue/front/rand(1,151).png",
      "offset":{
         "x":"rand(-20, 320)",
         "y":"rand(-20, 320)"
      }
   }]
}]

OK, but this pool of only 151 objects is not visually diverse enough. Let's say we want to randomly choose the Pokemon as well as randomly choose which face, front or back, of that pokemon to use:

[{
   "type":"loop",
   "start":1,
   "end":"600",
   "interval":1,
   "operations":[{
      "type":"composite",
      "filename":"img/pokemon/sprite/redblue/rand(front,back)/rand(1,151).png",
      "offset":{
         "x":"rand(-20, 620)",
         "y":"rand(-20, 470)"
      }
   }]
}]

In addition to choosing a random number within a range, rand can also choose a random element from a list. You can pass an arbitrary number of items to rand. Now let's get a little bit more complicated. In addition to randomly choosing from 151 Pokemon and front or back, let's choose between game version. We've been using Red/Blue for now, let's now add Red/Green and Yellow:

[{
   "type":"loop",
   "start":1,
   "end":"600",
   "interval":1,
   "operations":[{
      "type":"composite",
      "filename":"img/pokemon/sprite/rand(redblue,redgreen,yellow)/rand(front,back)/rand(1,151).png",
      "offset":{
         "x":"rand(-20, 620)",
         "y":"rand(-20, 470)"
      }
   }]
}]

Simple enough, but now let's add Silver and Gold. Silver and Gold complicate the matter because that generation of the game franchise introduced the concept of "shiny" Pokemon, which of course appear different than their non-shiny counterparts. So to add Silver and Gold, we need to also choose from shiny or not-shiny, but only if the the randomly chosen version is Silver or Gold:

[{
   "type":"loop",
   "start":1,
   "end":"600",
   "interval":1,
   "operations":[{
      "type":"composite",
      "filename":"img/pokemon/sprite/rand(redblue,yellow,redgreen,rand(silver,gold)/rand(shiny,normal))/rand(front,back)/rand(1,151).png",
      "offset":{
         "x":"rand(-20, 620)",
         "y":"rand(-20, 470)"
      }
   }]
}]

rand and other functions are nestable. In the case of rand, this ability to nest also gives us some-what of a control flow interface, bizarre although it is. Let's add versions Emerald, Ruby/Sapphire, Black/White, Fire/Leaf and Diamond/Pearl. Diamond/Pearl introduced the concept of male and female, so we will need to take the nesting one step further:

[{
   "type":"loop",
   "start":1,
   "end":"600",
   "interval":1,
   "operations":[{
      "type":"composite",
      "filename":"img/pokemon/sprite/rand(redblue,yellow,redgreen,rand(silver,gold,emerald,blackwhite,fire,ruby,diamondpearl/rand(male,female))/rand(shiny,normal))/rand(front,back)/rand(1,151).png",
      "offset":{
         "x":"rand(-20, 620)",
         "y":"rand(-20, 470)"
      }
   }]
}]

In the previous section we've used a base image to perform our manipulations on, but here we have no base image. Actually the base image is what ImageMagick refers to as a "pseudo file." Pseudo files are generated on the fly by ImageMagick according to set attributes. For the above Pokemon examples we used canvas:white, which is just the most basic psuedo file. For the following examples we will switch to the gradient psuedo file formats for their enhanced artistic expressiveness. Also, we will be dropping versions Red/Blue, Red/Green, Yellow, Silver and Gold because unfortunately the sprites I have for those versions are missing a transparency layer (for now) and thus overlay poorly.

Explaining pseudo files is beyond the scope of this writing, check ImageMagick for a documentation on the available pseudo file formats. We will be moving on quickly now.

In the examples shown above you have seen various operations defined with types like resize and rotate. You've also seen the macro-operation loop, which at run time gets transposed into a list of operations prior to image generation. Some of the more advanced operations and macro-operations, such as loop, accept lists of operations which get applied to child images prior to processing on the main image. You've seen the composite operation used to randomly compose Pokemon onto the image, however we can also apply operations to those Pokemon prior to overlaying them. So let's randomly resize and rotate them:

[{
   "type":"loop",
   "start":1,
   "end":"1200",
   "interval":1,
   "operations":[
      {
         "type":"composite",
         "filename":"img/pokemon/sprite/rand(emerald,blackwhite,fire,ruby,diamondpearl/rand(male,female))/rand(shiny,normal)/rand(front,back)/rand(1,151).png",
         "offset":{
            "x":"rand(-75, 675)",
            "y":"rand(-75, 525)"
         },
         "operations":[
            {
               "type":"resize",
               "height":"rand(20,100)"
            },
            {
               "type":"rotate",
               "degrees":"rand(1, 359)"
            }
         ]
      }
   ]
}]

Now we're getting somewhere! Beyond just simple resizing and web usage, Jmagick is a full artistic suite. Combining everything we know so far, let's create some real Pokemon art now:

[{
   "type":"swirl",
   "degrees":3000
},
{
   "type":"composite",
   "filename":"gradient:black-transparent",
   "attributes":{
      "size":"1920x1080"
   },
   "operations":[{
      "type":"swirl",
      "degrees":-3000
   }]
},
{
   "type":"loop",
   "start":1,
   "end":"4000",
   "interval":1,
   "operations":[
      {
         "type":"composite",
         "filename":"img/pokemon/sprite/rand(emerald,blackwhite,fire,ruby,diamondpearl/rand(male,female))/rand(shiny,normal)/rand(front,back)/rand(1,151).png",
         "offset":{
            "x":"rand(-500, 2420)",
            "y":"rand(-500, 1580)"
         },
         "operations":[
            {
               "type":"resize",
               "height":"rand(75,200)"
            },
            {
               "type":"rotate",
               "degrees":"rand(1, 359)"
            }
         ]
      },
      {
         "type":"swirl",
         "degrees":"2"
      }
   ]
}]

The loop macro-operator is capable of nesting loops as well. When nesting, the special variables $i and $j (and $k, and so on, etc.) become available for finely turned operations, like here:

[{
   "type":"loop",
   "start":0,
   "end":12,
   "interval":2,
   "operations":[{
      "type":"loop",
      "start":1,
      "end":"1000",
      "interval":1,
      "operations":[
         {
            "type":"composite",
            "filename":"img/pokemon/sprite/rand(emerald,blackwhite,fire,ruby,diamondpearl/rand(male,female))/normal/rand(front,back)/rand(4,5,6,37,38,58,59,77,78,126,136,146).png",
            "offset":{
               "x":"rand(-100, 2000)",
               "y":"rand(add(multiply($i,77),-30),add(multiply($i,77),24))"
            },
            "operations":[
               {
                  "type":"resize",
                  "height":"rand(64,77)"
               },
               {
                  "type":"rotate",
                  "degrees":"rand(1, 359)"
               }
            ]
         }
      ]
   }]
},
{
   "type":"loop",
   "start":1,
   "end":"2000",
   "interval":1,
   "operations":[
      {
         "type":"composite",
         "filename":"img/pokemon/sprite/rand(emerald,blackwhite,fire,ruby,diamondpearl/rand(male,female))/normal/rand(front,back)/rand(7,8,9,54,55,60,61,62,72,73,79,80,86,87,90,91,98,99,116,117,118,119,120,121,129,130,131,134,138,139,140,141).png",
         "offset":{
            "x":"rand(-100, 760)",
            "y":"rand(-100, 500)"
         },
         "operations":[
            {
               "type":"resize",
               "height":"rand(64,77)"
            },
            {
               "type":"rotate",
               "degrees":"rand(1, 359)"
            }
         ]
      }
   ]
},
{
   "type":"loop",
   "start":0,
   "end":8,
   "interval":2,
   "operations":[{
      "type":"loop",
      "start":0,
      "end":10,
      "interval":2,
      "operations":[{
         "type":"composite",
         "filename":"img/pokemon/sprite/rand(emerald,blackwhite,fire,ruby,diamondpearl/rand(male,female))/shiny/front/rand(63,64,65,79,80,92,93,94,96,97,102,103,121,122,124,150,151).png",
         "offset":{
            "x":"add(multiply($j,68),23)",
            "y":"add(multiply($i,58),-2)"
         },
         "operations":[
            {
               "type":"resize",
               "height":90
            },{
               "type":"gamma",
               "red":60,
               "green":60,
               "blue":60
            }
         ]
      }]      
   }]
},
{
   "type":"loop",
   "start":0,
   "end":6,
   "interval":2,
   "operations":[{
      "type":"loop",
      "start":0,
      "end":8,
      "interval":2,
      "operations":[{
         "type":"composite",
         "filename":"img/pokemon/sprite/rand(emerald,blackwhite,fire,ruby,diamondpearl/rand(male,female))/shiny/front/rand(63,64,65,79,80,92,93,94,96,97,102,103,121,122,124,150,151).png",
         "offset":{
            "x":"add(multiply($j,68),91)",
            "y":"add(multiply($i,58),66)"
         },
         "operations":[
            {
               "type":"resize",
               "height":90
            },{
               "type":"gamma",
               "red":60,
               "green":60,
               "blue":60
            }
         ]
      }]      
   }]
}]

Let's change the image sources now. Let's use the images as featured on pokemon.com — but alas we encounter an elementary problem! rand has failed us because the file nomenclature for the pokemon.com images uses leading zeros thus breaking our primitive string building. Good thing there is an sprintf function:

[{
   "type":"loop",
   "start":0,
   "end":12,
   "interval":2,
   "operations":[{
      "type":"loop",
      "start":1,
      "end":"1000",
      "interval":1,
      "operations":[
         {
            "type":"composite",
            "filename":"img/pokemon/pokemon.com/v2/png/sprintf(%03d,rand(4,5,6,37,38,58,59,77,78,126,136,146)).png",
            "offset":{
               "x":"rand(-100, 2000)",
               "y":"rand(add(multiply($i,77),-30),add(multiply($i,77),24))"
            },
            "operations":[
               {
                  "type":"resize",
                  "height":"rand(64,77)"
               },
               {
                  "type":"rotate",
                  "degrees":"rand(1, 359)"
               }
            ]
         }
      ]
   }]
},
{
   "type":"loop",
   "start":1,
   "end":"2000",
   "interval":1,
   "operations":[
      {
         "type":"composite",
         "filename":"img/pokemon/pokemon.com/v2/png/sprintf(%03d,rand(7,8,9,54,55,60,61,62,72,73,79,80,86,87,90,91,98,99,116,117,118,119,120,121,129,130,131,134,138,139,140,141)).png",
         "offset":{
            "x":"rand(-100, 760)",
            "y":"rand(-100, 500)"
         },
         "operations":[
            {
               "type":"resize",
               "height":"rand(64,77)"
            },
            {
               "type":"rotate",
               "degrees":"rand(1, 359)"
            }
         ]
      }
   ]
},
{
   "type":"loop",
   "start":0,
   "end":8,
   "interval":2,
   "operations":[{
      "type":"loop",
      "start":0,
      "end":10,
      "interval":2,
      "operations":[{
         "type":"composite",
         "filename":"img/pokemon/pokemon.com/v2/png/sprintf(%03d,rand(63,64,65,79,80,92,93,94,96,97,102,103,121,122,124,150,151)).png",
         "offset":{
            "x":"add(multiply($j,68),23)",
            "y":"add(multiply($i,58),-2)"
         },
         "operations":[
            {
               "type":"resize",
               "height":90
            },{
               "type":"gamma",
               "red":60,
               "green":60,
               "blue":60
            }
         ]
      }]      
   }]
},
{
   "type":"loop",
   "start":0,
   "end":6,
   "interval":2,
   "operations":[{
      "type":"loop",
      "start":0,
      "end":8,
      "interval":2,
      "operations":[{
         "type":"composite",
         "filename":"img/pokemon/pokemon.com/v2/png/sprintf(%03d,rand(63,64,65,79,80,92,93,94,96,97,102,103,121,122,124,150,151)).png",
         "offset":{
            "x":"add(multiply($j,68),91)",
            "y":"add(multiply($i,58),66)"
         },
         "operations":[
            {
               "type":"resize",
               "height":90
            },{
               "type":"gamma",
               "red":60,
               "green":60,
               "blue":60
            }
         ]
      }]      
   }]
}]

You don't need Pokemon, you can use any imagery. You can use US presidents, or World War II airplanes — or flowers, puppies, insects — pizza and pizza toppings! The possibilities are endless. Here, Hope expressed as a function of time:

[{
   "type":"mirror",
   "axis":"x"
},
{
   "type":"composite",
   "filename":"img/random/obama.jpg",
   "gravity":"southeast",
   "operations":[
      {
         "type":"mirror",
         "axis":"x"
      },
      {
         "type":"modulate",
         "hue":90
      },
      {
         "type":"swirl",
         "degrees":90
      },
      {
         "type":"resize",
         "width":787
      }
   ]
},
{
   "type":"composite",
   "filename":"img/random/obama.jpg",
   "gravity":"southeast",
   "operations":[
      {
         "type":"mirror",
         "axis":"x"
      },
      {
         "type":"modulate",
         "hue":180
      },
      {
         "type":"swirl",
         "degrees":180
      },
      {
         "type":"resize",
         "width":393
      }
   ]
},
{
   "type":"composite",
   "filename":"img/random/obama.jpg",
   "gravity":"southeast",
   "operations":[
      {
         "type":"mirror",
         "axis":"x"
      },
      {
         "type":"modulate",
         "hue":270
      },
      {
         "type":"swirl",
         "degrees":360
      },
      {
         "type":"resize",
         "width":196
      }
   ]
}]

You don't need any source imagery at all actually. You can use nothing but pseudo files to create all kinds of geometric imagery, such as these Windows XP wallpapers:

[{
   "type":"loop",
   "start":1,
   "end":100,
   "interval":1,
   "operations":[{
      "type":"composite",
      "filename":"gradient:#sprintf(%06x,rand(0,16777215))-transparent",
      "attributes":{
         "size":"rand(100,1000)xrand(100,1000)"
      },
      "offset":{
         "x":"rand(-500,2420)",
         "y":"rand(-500,1580)"
      },
      "operations":[{
         "type":"rotate",
         "degrees":"rand(1,359)"
      },
      {
         "type":"roll",
         "x":"rand(1,1000)",
         "y":"rand(1,1000)"
      },
      {
         "type":"rotate",
         "degrees":"rand(1,359)"
      }]
   },
   {
      "type":"swirl",
      "degrees":40
   },
   {
      "type":"roll",
      "x":"rand(1,1920)",
      "y":"rand(1,1080)"
   }]
}]

Or you can stick to the basic usage described in the introduction.

You can try Jmagick below. Read the documentation for all the available operations and features. Sorry, I do not take image uploads for now. You can use URLs from images on the internet or pseudo file names for your source images.

Live Demo

Documentation - Coming Soon