Scripted drawing with ImageMagick

Silhouette

Article from Issue 263/2022
Author(s):

ImageMagick can do more than just edit existing images. The free software can even be scripted to create simple drawings.

Although you would normally use a bona fide graphics program for drawing and painting, there are definitely situations in which you need to draw regular shapes in an image repeatedly at fixed intervals – as shown here, for example, when creating the silhouette of an imaginary city (Figure 1). This does not require an expensive graphics program with a sophisticated macro language. Using the free and open source ImageMagick software package at the command line is more than up to this task.

Figure 1: Picture of a skyline created with ImageMagick. You can pass in the number of windows and floors as parameters.

To compose more extensive images, you will need the support of a scripting language such as Bash, which uses loops and other control structures to repeatedly insert image content into the graphic. ImageMagick can be found in the package sources of most Linux distributions, but it can also be downloaded for installation from the download section of the project page [1].

Painting by Commands

After completing the install, type

magick logo: logo.gif

at the command line. ImageMagick will create the logo.gif file in the current directory. It shows the magician seen in the upper right corner of Figure 1. You can easily check this by opening the file in a suitable image viewer. Image names ending with a colon are internal test images in ImageMagick. You can create more test images with the rose: and wizard: [2] options.

To demonstrate that you can actually draw at the command line with ImageMagick, see the command in Listing 1.

Listing 1

Rectangle

$ magick -size 500x300 \
  xc:skyblue -fill red \
  -draw 'roundrectangle \
  100,50,400,250,80,60' \
  image.png

What this does is to first set the image size with the -size parameter. Then xc:skyblue paints the background of the image sky blue. Historically, xc: stands for "X Constant Image," but today it stands for canvas. The instruction expects a color name from the X Window System palette, which ImageMagick adopted [3].

You can also specify a gradient, for example, by typing gradient:blue instead of xc:skyblue. The built-in gradient image generator would then create a blue gradient background image for you. For an idea of the possibilities, check out the ImageMagick documentation. The manual goes into the details of plasma canvases [4] and gradients [5], for example.

The -fill parameter is followed by the definition of the drawing color for the subsequent drawing command, which is introduced by -draw and quoted. The last parameter is the name of the image file to be created.

The resulting image is a red rectangle, rounded at the corners, on a light blue background. You create more complex drawings by stringing together several drawing commands (Listing 2, lines 1 to 3).

Listing 2

Multiple Elements

01 $ magick -size 500x300 xc:skyblue -fill red -draw 'roundrectangle 100,150,400,225,55,10' \
02   -draw 'roundrectangle 200,100,350,180,50,10' -fill black -draw 'circle 150,225 180,225' \
03   -draw 'circle 350,225 380,225' auto.png
04 $ magick auto.png -fill red -draw 'polygon 175,150 200,110 200,150' auto-bevel.png
05 $ mogrify -fill red -draw 'polygon 175,150 200,110 200,150' auto.png

Three-Box Car

The result of the first call from Listing 2 is shown in Figure 2: a very simplified image of a notchback based on the three-box principle. By passing in the drawing command from line 4 of Listing 2, you can improve the boxy shape a little. The command angles the passenger compartment a bit at the front.

Figure 2: A notchback created with simple draw commands.

The auto.png file I just created provides the basis for subsequent beveling of the passenger compartment. The command writes the results to the auto-bevel.png file. If you want to avoid constantly recreating files, use the command from the last line of Listing 2 instead. This modifies the auto.png file directly.

Although both methods have advantages for your first steps in getting to know ImageMagick, they have one decisive disadvantage. They are both comparatively slow. As you can imagine, drawing an entire skyscraper this way is complicated. You would have to string together several draw commands, and a separate call would be required for each individual window. And you would have to manually calculate the positions of the windows up front.

Faster with Scripts

The slowness in drawing a skyscraper can remedied by using a script that determines the image size based on the information it receives about the desired number of floors and windows per floor. It also defines a color gradient for the background and saves the image. It then calculates all the drawing instructions for the actual building and the windows and doors and collects them in an XML-formatted Magick Scripting Language (MSL) file.

Finally, the script uses a conjure statement to process the mess of drawing commands. This greatly improves speed over incrementally developing the image using individual mogrify statements. Listing 3 shows an MSL file for a small two-story house with a foundation, five windows, and a front door.

Listing 3

miniHouse.msl

<?xml version="1.0" encoding="UTF-8"?>
<image>
  <read filename='miniHouse.png' />
  <draw fill='sandybrown' primitive='rectangle 250, 1940, 810, 2500' />
  <draw fill='snow4' primitive='rectangle 0, 2500, 1060, 3000' />
  <draw fill='yellow' primitive='rectangle 270, 1970, 370, 2090' />
  <draw fill='yellow' primitive='rectangle 690, 1970, 790, 2090' />
  <draw fill='yellow' primitive='rectangle 440, 1970, 620, 2090' />
  <draw fill='yellow' primitive='rectangle 270, 2250, 370, 2370' />
  <draw fill='yellow' primitive='rectangle 690, 2250, 790, 2370' />
  <draw fill='yellow' primitive='rectangle 440, 2250, 620, 2500' />
  <write filename='miniHouse.png' />
</image>

You call the buildHouse shell script, on which this workshop is based, using the syntax in Listing 4. Its contents are taken unchanged from Listing 5. For the sake of simplicity, the script does not perform an extensive check of the parameters you pass in. In addition, only positioning parameters are used. The call sequence in the command line determines the assignment in the script for this. If you want to improve this script, you need to look into the shell's error checking options and also consider using getopts in the script.

Listing 4

Sample Syntax

$ ./buildhouse <File> <Floors> <WindowsPerFloor>

Listing 5

buildHouse

#/bin/bash
if [ -z "$1" ]; then
  echo "Please specify at least the name of the output file without a file extension as the parameters, optionally also the number of floors and windows per floor."
  exit
fi
rows=5
columns=10
ox=250 # X-offset of the house
oy=2500 # Y-offset of the house
if [ $# -ge 3 ]; then
  rows=`expr $2`
  columns=`expr $3`
  if [ $# -ge 4 ]; then
    ox=`expr $4`
  fi
fi
rh=250 # room height
fh=120 # window height
fb=100 # window width
dh=30 # ceiling height
fa=20 # window distance
ma=300 # center distance/staircase bay width
mslfile="$1.msl"
housewidth=`expr $columns \* \( $fa + $fb \) + $fa + $ma`
househeight=`expr $rows \* \( $rh + $dh \)`
t="$ox, `expr $oy - $househeight`, `expr $housewidth + $ox`, $oy"
center=`expr $housewidth / 2 + $ox`
mahalb=`expr $ma / 2`
center-left=`expr $center - $mahalb + \( 3 \* $fa \)`
center-right=`expr $center + $mahalb - \( 3 \* $fa \)`
magick -size"`expr $ox + $housewidth + $ox`"x3000 gradient:#0000ff-#ffffff -draw "rectangle $t" $1.png
str="0, 2500, `expr $ox + $housewidth + $ox`, 3000"
echo '<?xml version="1.0" encoding="UTF-8"?>' > $mslfile
echo '<image>' >> $mslfile
inputfile="<read filename=\"$1.png\" />"
echo $inputfile >> $mslfile
echo " <draw fill='sandybrown' primitive='rectangle $t' />" >> $mslfile
echo " <draw fill='snow4' primitive='rectangle $str' />" >> $mslfile
top=`expr $oy - $househeight + $dh`
left=$ox
for ((i=0; i < lines; i++)) ; do
  links=`expr $links + $fa`
  for ((j=0; j < columns; j++)) ; do
    varInt=`expr $columns / 2`
    if [ $j -eq $varInt ]; then
      links=`expr $links + $ma`
    fi
    t="$left, $up, `expr $left + $fb`, `expr $up + $fh`"
    echo " <draw fill='yellow' primitive='rectangle $t' />" >> $mslfile
    links=`expr $links + $fb + $fa`
  done
  if [ $i -ne `expr $lines - 1` ]; then
    tm="$center-left, $top, $center-right, `expr $top + $fh`"
    echo " <draw fill='yellow' primitive='rectangle $tm' />" >> $mslfile
  else
    tm="$center-left, $top, $center-right, `expr $top + $rh`"
    echo " <draw fill='yellow' primitive='rectangle $tm' />" >> $mslfile
  fi
  left=$ox
  top=`expr $top + $rh + $dh`
done
outputfile="<write filename=\"$1.png\" />"
echo $outputfile >> $mslfile
echo '</image>' >> $mslfile
conjure msl:$mslfile

When calling the script, you need to specify the file name without a file extension. The .msl and .png extensions are added automatically by the code. If you leave the option for the number of floors and the number of windows per floor empty, the script will use default values. The file name, on the other hand, must always be specified, otherwise the program will abort. You always need to append the number of floors and windows per floor together, otherwise the script will not evaluate the parameters completely and will replace both with default values.

Once you have made all the entries, you can optionally specify a fourth parameter for correcting the boundary distance to the neighboring property if you want to recreate the sample settlement from Figure 1 as shown in Listing 6. Table 1 explains the meaning of the assembly and composite tools, among other things.

Table 1

Command Line Tools in the ImageMagick package

Tool Name

Function

convert

A standard tool from the ImageMagick package, it can convert file formats and scale, blur, crop, denoise, dither, rotate, flip images, and much more.

identify

Outputs a description of the format and characteristics of one or more graphics files.

mogrify

Offers the same functions as convert, but unlike the latter, overwrites the source file.

composite

Overlaps two images.

assemble

Assembles multiple images into one.

compare

Displays the differences between two graphs (as a report of a mathematical analysis and visually).

stream

Copies single or multiple pixel components of an image to another format (mainly intended for very large image files).

display

Displays an image or image sequence via an X server.

import

Creates screenshots in X11. The function optionally saves the entire screen area, the area of a window, or a defined rectangle.

conjure

Interprets scripts in the MSL and executes them.

Listing 6

Building a Skyline

$ ./buildHouse house1 8 8
$ ./buildHouse house2 4 10 0
$ ./buildHouse house3 5 10 0
$ ./buildHouse house4 1 8 250
$ magick composite house1.png house2.png house3.png house4.png -geometry +0 -tile x1 street.png
$ magick composite logo: street.png -gravity NorthEast street-magick.png

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News