Introduction: Make Your Own CNC Plotter Image

About: 55+ years in electronics, computers, and teaching ... now retired.

This instructable explains how photos may be printed on a CNC plotter with the help of "Processing 3" freeware available from

The finished plots make unique gifts ... especially when framed.

Step 1: Theory

The monochrome image, shown in photo 1, comprises rows and rows of tiny "pixels" (picture elements). Each "pixel" has a value between 0 (black) and 255 (white) ... a total of 256 shades.

It is not possible to plot a monochrome image directly as a plotter can only create two colors ... black (ink) and white (no ink).

Theshold images

Plotting each pixel with a value greater than 128 results in an image similar to that in the second photo.

Much of the detail has been lost and the face is barely recognizable.

The Processing 3 code used to create this image is shown below:

  • // ----- simple threshold
  • int threshold = 128;
  • for (int y = 0; y < height; y++){
  • for (int x = 0; x < width; x++){
  • int location = x + y*width;
  • float value = brightness(pixels[location]);
  • if (value > threshold){
  • stroke(255); //draw white pixel
  • point(x, y);
  • } else {
  • stroke(0); //draw black pixel
  • point(x, y);
  • }
  • }
  • }


Another approach is to use dithering where each pixel is compared with 128 and the error term added to the next pixel before it in turn is compared with 128.

The resulting image is shown in photo 3 and at first glance looks identical to photo 1. The difference between these two photos, however, is that each pixel in photo 1 contains one of 256 shades of gray whereas photo 3 only contains pure black and white pixels.

If you examine this image closely you will see that the average number of "black" pixels, per unit area, is zero for white and 256 for solid black. Our eyes are averaging the number of "black" pixels across the image ....

The code for this dithered image is similar to that for the threshold image and is shown below:

  • // ----- one-dimensional dither
  • int threshold = 128;
  • for (int y = 0; y < height; y++) {
  • float error = 0;
  • for (int x = 0; x < width; x++) {
  • int location = x + y*width;
  • float value = brightness(pixels[location])+error;
  • if (value > threshold) {
  • stroke(255); //draw white pixel
  • point(x, y);
  • error = value - 255;
  • } else {
  • stroke(0); //draw black pixel
  • point(x, y);
  • error = value;
  • }
  • }
  • }

The following step describes the averaging method used to create the images in this instructable.

Step 2: Grayscale Images

A problem with dithered images is that there can be upwards of a 100,000 black pixels each of which will wear the pen-lift mechanism and damage the pen.

The number of pen-lift operations can be significantly reduced if we turn the image into an electronic "jigsaw" with different shades of gray associated with each block. The resulting image, for a 5x5 pixel sample size, is shown in photo 1 above.

Each of the electronic "jigsaw" pieces has been shaded using "sinewaves" of different amplitudes and frequencies as shown in the second photo.

The beauty of this method is that the "entry" and "exit" points for each of the electronic "jigsaw" pieces lie on the same horizontal line which means that a pen-lift is not required until the end-of-line. The plotter is continuously laying ink ...

Bi-directional plotting, with no pen-lift, was considered but rejected as any mechanical play in the plotter would be noticeable.

Generating the grayscale

The required grayscale is generated by a drawSinewave (x, y, cycles, amplitude) function.


  • x=horizontal screen location in pixels
  • y=vertical screen location in pixels
  • cycles=number of complete cycles
  • amplitude= the sinewave amplitude

To see how this works, compare the sinewaves in photo2 with each of the step numbers shown in the following code.

  • // ------------------------------------
  • // drawPattern
  • // ------------------------------------
  • void drawPattern(int column, int row, int value) {
  • /* A grayscale is a bit like a staircase ... 16 steps requires 17 risers */
  • // ----- locals
  • int x=column*cellWidth; //horizontal pixel co-ordinate
  • int y=row*cellHeight; //vertical pixel co-ordinate
  • switch (value) {
  • case 0: drawSinewave(x, y, 1, 0.0); break; //step0 (white)
  • case 1: drawSinewave(x, y, 1, 0.0); break; //step1
  • case 2: drawSinewave(x, y, 1, 0.05); break; //step2
  • case 3: drawSinewave(x, y, 1, 0.2); break; //step3
  • case 4: drawSinewave(x, y, 1, 0.3); break; //step4
  • case 5: drawSinewave(x, y, 1, 0.4); break; //step5
  • case 6: drawSinewave(x, y, 1, 0.6); break; //step6
  • case 7: drawSinewave(x, y, 1, 0.8); break; //step7
  • case 8: drawSinewave(x, y, 1, 1.0); break; //step8 (mid-gray)
  • case 9: drawSinewave(x, y, 2, 1.0; break; //step9
  • case 10: drawSinewave(x, y, 3, 1.0); break; //step10
  • case 11: drawSinewave(x, y, 4, 1.0); break; //step11
  • case 12: drawSinewave(x, y, 5, 1.0); break; //step12
  • case 13: drawSinewave(x, y, 6, 1.0); break; //step13
  • case 14: drawSinewave(x, y, 7, 1.0); break; //step14
  • case 15: drawSinewave(x, y, 8, 1.0); break; //step15 (black)
  • case 16: drawSinewave(x, y, 8, 1.0); break;
  • }
  • }

I chose to use sinewaves of differing amplitudes and frequencies for my grayscale but there is no reason why other patterns can't be used. All we are trying to do is fool the eye ...

A minor downside

An A4 drawing can take up to 10 hours (overnight) to print.

The reason for this is that each sinewave comprises a series of straight lines at 30 degree intervals, or 12 co-ordinates per cycle. Dark screen "cells" tend to have more cycles than light colored screen "cells".

For example black, with 8 cycles, generates 96 co-ordinates per screen "cell" ... hence the slow plot speed.

Step 3: Software

The following software must be installed in this sequence before you can plot an image:

Processing 3

Download and install a copy of "Processing 3" from This software allows us to create the image g-code.

"Processing 3" is almost identical to the arduino IDE (integrated development environment) except that the function "loop()" has been replaced by "draw()". A few drawing "primitives" have also been thrown in for good measure, two of which are:

  • point() ... for plotting specific screen points
  • line() ... for drawing lines between two sets of screen co-ordinates.

These drawing primitives allow us to preview our g-code image prior to plotting.


Download a copy of "processing3_terminal.pde" from Step 2 of instructable

Copy the contents of this file into a new "processing3.exe" sketch and save the sketch as "processing3_terminal".

The action of saving the sketch creates a "../Documents/Processing/processing3_terminal/" folder that contains the file "processing3_terminal.pde".


Download the attached file "draw_image_gcode.pde".

Copy the contents of "draw_image_gcode.pde" into a new "processing3.exe" sketch and save it as "draw_image_gcode" without the quotes.

The action of saving the sketch creates a "../Documents/Processing/draw_image_gcode/" folder that contains the file "draw_image_gcode.pde".

Download, and unpack, the attached "" file. This zip file contains two essential test images.

Copy the two image files, "image.jpg" and "grayscale.jpg" into the "../documents/Processing/draw_image_gcode" folder that was created in the above step.

Creating the image g-code

Open, and run, the "draw_image_gcode" sketch using "processing3..exe".

To run a sketch using "processing3.exe" you simply left-click the top-left on-screen button.

All going well a grayscale image should appear on your screen and an "image.ngc" g-code file will appear in your "../documents/Processing/draw_image_gcode" folder.

We now have everything we need to plot our first image ...

Step 4: Plotting Your Image

The following instructions assume that you have an Inkscape compatible plotter attached to your PC.

Inkscape compatible plotters assume that the (0,0) co-ordinate is at the bottom-left corner. If your image comes out upside down and reversed then search "draw_image_gcode.pde" code for any lines that contain "output.print()" and delete any reference to "height".

Suitable Inkscape plotters include:

Step 1

Copy the file "image.ngc" from your ",,/Documents/Processing/draw_image_gcode/" folder to your "../Documents/Processing/processing3_terminal/" folder.

Step 2

Open "processing3.exe" and run the sketch "processing3_terminal.pde" that is in a folder of the same name.

The menu shown in photo 1 should appear on your screen. If not left-click the gray instruction box.

Instructions for fixing communication port errors are detailed in

Step 3

Follow EXACTLY the instructions shown in the screenshot shown in photo 1.

T1 allows you to position the pen over the (0,0) co-ordinate using the 'A', 'S', 'K', and 'L' keys. Press the 'E' key once the pen is correctly positioned.

T2 S0.4 scales your image such that it fits on a sheet of A4 paper. The original "image,jpg" file dimensions were 467 pixels x 620 pixels. Scaling these dimensions by 40% produces a finished image that is 186 mm x 248 mm which will fit on a sheet of 290 mm x 210 mm A4 paper.

Step 4

Right-click the gray instruction box [1] then type "image.ngc" without the quotation marks. Press the enter key and your plotter should start working.

As mentioned large images take a long time to plot ... I do mine overnight.

[1] there is no cursor

Step 5: Plotting Your Second Image

To plot another image, say "grayscale.jpg", we need to change a few lines of code. These code changes are shown below in bold highlight.

It is essential that the dimensions within "size" match your image dimensions.

The "cellWidth", and "output" filename, are not critical.

    • // -------------------------------------
    • // declarations
    • // -------------------------------------
    • PImage src; //declare variables of type PImage
    • PrintWriter output; //instantiate "output" for printing files
    • int cellWidth = 64, //cell width in pixels ... determines the detail
    • //int cellWidth = 5, //cell width in pixels ... determines the detail
    • cellHeight = cellWidth, //make cell square
    • columns,
    • rows;
    • boolean penUp=true; //flag
    • // -------------------------------------
    • // setup
    • // -------------------------------------
    • void setup() {
    • size(1024, 512); //grayscale.jpg dimensions (cellWidth=64)
    • //size(467, 620); //image.jpg dimensions (cellWidth=5)
    • background(255); //screen image background white
    • rows=height/cellHeight; //number of cell rows
    • columns=width/cellWidth; //number of cell columns
    • src = loadImage("grayscale.jpg"); //get grayscale.jpg
    • //src = loadImage("image.jpg"); //get image.jpg
    • src.filter(GRAY); //convert image to monochrome
    • output = createWriter("grayscale.ngc"); //open a file for storing the g-code
    • //output = createWriter("image.ngc"); //open a file for storing the g-code
    • noLoop(); //main loop only runs once
    • }

      Click here   to view my other instructables.