back to index

QR-code laser stencil generator


QR code is a form of matrix barcode. It is now fairly common, with readers in virtually every camera-equipped smartphone either already installed, or an app store visit away.

Application via stamping, printing or attaching a sticker is easy. This is not always practical. Sometimes spray-painting with a stencil is the preferred way; e.g. when decorating textiles (whether by paint or bleach), or for urban graffiti.

QR-code generators usually produce bitmap files. These can be used for laser engraving, but not for laser cutting. Even if the edges are traced and the bitmap is converted to vectors, the result would fall apart after cutting. There are enclosed areas within enclosed areas that'd just fall out. The result, not pretty.

A solution is cutting out individual pixels, leaving spaces around. This forms a mesh with some squares removed. But alas, there seems to be no software able to do this.


Not anymore.

A piece of software was written that takes the qr-code bitmap, detects the vertical and horizontal edges, extracts the pixel values from the image, and generates the vector image, ready for cutting, in either OpenSCAD or DXF format.

Code functionality

The code uses the ImageMagick library for reading the bitmaps and decoding them into an in-memory array.

The image is read, converted to black and white, and placed to memory. Memory arrays are prepared for the row and the column. The rows and columns of the bitmap are scanned, and detected edges are set in row/column arrays. This quantizes the image, detects the pixels in the code. Optional white frame around the code is detected and discarded.

With the image quantized, the pixels of the QR code are now directly accessible.

Now, the code is scanned pixel by pixel, optionally a new white frame is generated. Pixels are optionally inverted. Each black one (or white, if inverted) is outputted as the appropriate vector code to stdout.

Output formats

Output size specification

Output image can be specified either as its entire size (the -size option), or by specifying the width of the pixel plus the size of spacer (the -sw option).

In both cases the -sp option can and should be used to specify the spacer width. The actual spacer width should be specified to be as low as the material, laser setting, and final use allows; the value may have to be determined experimentally.

Partial output

Due to possible large size of the stencil and smaller size of affordable laser cutters, it may be needed to export only part of the code. The sub-area can be specified as the qr-code pixels. The -startx and -starty parameters specify the offset from left and top, the -countx and -county parameters specify the length horizontally and vertically. The info block will calculate the size of the exported area, but making sure it is of the right size for the materials on hand is left on the user.

Size calculator output

The dimensions of the output and pixels are displayed unless said otherwise (-q). If the image is sent to stdout, this info is sent to stderr. If image is not to be shown (-n), the info goes to stdout.

Command syntax


Example: check qr.png for size and other parameters:

 qr2laser qr.png -n

Example: check qr.png for size and other parameters, see the QR code in ASCII:

 qr2laser qr.png

Example: convert qr.png to a 15 cm square, 0.8 mm spacing, in DXF, black as cutout:

 qr2laser qr.png -d -size 150 -sp 0.8 > output.dxf

Example: convert qr.png to a 15 cm square, 0.8 mm spacing, in OpenSCAD, inverted (white as cutout), with 3-pixel frame:

 qr2laser qr.png -s -size 150 -sp 0.8 -i -e 3 > output.scad

Example: convert qr.png using 5.5x5.5 mm pixels, 1 mm spacing, to DXF:

 qr2laser qr.png -d -sw 5.5 -sp 1 > output.dxf

Example: convert qr.png to a 25 cm square, 0.8 mm spacing, in DXF, in two parts, 25 cm wide but only 15 cm high (the height of 12 pixels was found by hand):

qr2laser qrtest.png -size 250 -starty 0 -county 12 > output_part1.dxf
qr2laser qrtest.png -size 250 -starty 12 > output_part2.dxf

Example: convert qr.png using 5.5x5.5 mm pixels, 1 mm spacing, to DXF:

 qr2laser qr.png -d -sw 5.5 -sp 1 > output.dxf

Use for generic bitmaps

Generic pixel art bitmaps may not have good autodetection of pixel edges. There are two mechanisms to force the right size; using -xpix/-ypix to specify the number of pixels in the image, or -xpixsize/-ypixsize for the pixels size in image pixels (this size can have decimals, to cater to rounding errors). Some idea about the pixel sizes can be gained from the debug output, -D, from the EdgeArray, EdgePos and EdgeLen lists.

Colors have to be selected sometimes. Images with too many colors can be downsampled using -quantify option. The colors then can be listed via -listcolors. The matching colors, or color ranges, can be specified for the pixel match via the -rgb option.

G-code generation for 3d printers

In some cases it is needed to draw a bitmap image from individual dots, or eventually lines. The original stencil cutting generator was found to be an excellent platform for adding this functionality.

Many tasks require deposition of a material, e.g. an ink or adhesive, to specific places on a flat surface. A dot by dot drawing with e.g. a pen or a marker is one of the possibilities. A 3d-printer can be adapted for this; the printer head is replaced with a pen or other ink depositing device, and instructed, usually using G-code, to move over the surface and touch the pen tip down wherever a dot is required to be.

The G-code generator takes the input bitmap, uses the same sub-area specifications and color separation options as the stencil generator above, and outputs raw code with the head movements.

Pecking and line-drawing

The basic operation for creating a dot on the substrate is a "peck". If the pixel has to be formed on the coordinates, the head is moved there, then lowered to the substrate and quickly retracted again. The head height can have two values; the lower one, for quicker movements, used when neighbouring pixels are created, and the higher, for longer-range movements. This provides higher speed for clusters of dots while giving height margin for longer moves.

Alternatively, the image can be drawn from lines; instead of retracting after each pixel, the head is lifted only at the end of a sequence of neighbouring pixels in a row.

The peck mode has the advantage of the tool touching the substrate only vertically, without the risk of dragging it around. This allows use of simple methods of fixing the substrate, and use of substrates that would deform under lateral forces (e.g. soft cloth). Also, no lateral forces are exerted on the tool head, allowing the use of 3d printers with flimsy frames without loss of positioning accuracy.

There are two scanning modes for the bitmap:

G-code specific options

The G-code output can be provided in three flavors: -gp (line-by-line "pecking"), -gn (nearest-neighbor pecking), and -gl (line-drawing). Pecking and line drawing scans the image line by line, left-to-right by default, zigzag when -gzz is specified.


The heights of the head position can be specified, in millimeters:


The head movement speeds can be specified, in millimeters per minute:

Custom pixel G-code

By default, the peck operation G-code consists from an instruction to go down to zero position (-gzdown) at downward speed (-gfdown), followed immediately by retraction. By default, the go-down operation is "G1 Z0.000000 F2000".

This operation can be replaced by any custom G-code, using the -gcode option. Vertical pipes can be used as command separators when multiple commands are required in a sequence (e.g. switch on a laser, dwell, switch off laser).

The -gfdown and -gzdown options are rendered irrelevant by the -gcode option.


Dimensions can be specified via the pixel size (-sw), total image width (-size), or DPI (-dpi). The resulting DPI, resolution and size are reported in the output.


For more concise output code, -prec can be used to specify decimal precision for x and y coordinates. By default, it is two decimal places, which equals 10µm resolution. Higher or lower resolution can be specified as desired.


More degrees of freedom of output positioning were required. The -offsx and -offsy options allow moving the output by an offset in millimeters. The -center option moves the output image's center to the [0,0] coordinate, for use on e.g. delta printers with zero in the center of the workspace. If used, the offsets (if specified) are added to the centered image.

-fv and -fh options allow flipping the output horizontally and vertically.


The binary is compiled for Linux, using gcc-4.7.2


cc -std=c99 -o qr2laser `pkg-config --cflags --libs MagickWand` qr2laser.c

Code notes

The C is ugly. Language chosen on the basis of knowing it the best. Uses fixed memory structures to avoid pointer hell, was a scratch-an-itch solution instead of a beauty-queen code candidate. No warranty. License to be determined, something open, no idea yet what flavor.

Tests and drawbacks

Mask of the test QR code, with 8 cm square size, was cut out from a cardboard, using 12mm/s speed and 4mA current. The cutting went without a glitch, on the second pass. (On the first pass, higher current was used and the cardboard burned.) The 0.8mm of default spacer width shown to be a good compromise; enough to hold well while not enough to interfere with the paint too much.

The spray painting shown a problem inherent to stencils; the stencil did not adhere fully to the surface and the aerosol went underneath and scattered, smearing the features. Fortunately, the phone app it was tested with recognized the code well enough to decode it properly.

Two more masks (6x6 and 5x5 cm) were cut from cardboard (a box of instant porridge), using 14 mm/s and 4 mA. The results of the cutting are fairly good. More experiences were acquired with stenciling, more samples were made.

Actual tests shown gross differences in sensitivity of QR-code reading apps to the thin line grids. Some apps had no trouble reading the codes at all. Others weren't able to read almost any.


Example images for QR codes

qr2laser qrtest.png

[][][][][][][] [] [] [][][][][][][] [] [] [] [] [] [] [] [][][] [] [] [] [][][] [] [] [][][] [] [] [] [] [][][] [] [] [][][] [] [] [] [] [][][] [] [] [] [][][] [] [] [] [][][][][][][] [] [] [] [][][][][][][] [] [] [][][][][] [][][][] [][][] [] [] [] [] [][] [][][][][] [][] [] [][] [][] [][] [] [][] [] [][] [] [][] [] [][][] [] [][][] [][] [] [][] [] [] [] [] [][] [][][][][][][] [] [][][] [][][] [] [] [] [][] [][] [] [] [][][] [] [][] [][] [][][][] [][] [] [][][] [] [] [][] [] [] [][][] [] [][][][][] [] [] [] [][][] [][] [] [] [][][][][][][] [] [][][][][] [] Input bitmap dimensions: 400x400 QR code dimensions: 21x21 Total area size: 0.00 Pixel size: -0.76 Pixel spacing: 0.80 Warning: Pixel size too small (-0.761905x-0.761905 mm), limit 2.

qr2laser qrtest.png -d -size 150 -sp 1 > qrtest.dxf

Input bitmap dimensions: 400x400 QR code dimensions: 21x21 Total area size: 150.00 Pixel size: 6.19 Pixel spacing: 1.00

(and converted to PNG, using sw_DXFlaserutils) qrtest.dxf -d 72 -lw 1 -o qrtest.dxf.png

qr2laser qrtest.png -s -size 100 -sp 1 > qrtest.scad

Input bitmap dimensions: 400x400 QR code dimensions: 21x21 Total area size: 100.00 Pixel size: 3.81 Pixel spacing: 1.00

Physical tests

8cm mask, overall

8cm mask

8cm mask, failed burned cut

8cm mask, stencil spraying

8cm mask, sprayed

8cm mask, sprayed

8cm mask, phone test

8cm mask, phone test

5 and 6 cm masks, cut

5 cm mask, cut

6 cm mask, cut

5 and 6 cm masks, cut

5 and 6 cm masks, spraying

5 and 6 cm stencils sprayed

6 cm stencil sprayed

5 cm stencil sprayed

5 and 6 cm stencils sprayed

6 cm stencil sprayed

5 cm stencil sprayed

5 and 6 cm stencils sprayed, with mask

Example images for pixel art bitmaps

Sometimes a trial and error is needed to get the pixel quantization right. Use -xpix and -ypix for forcing the resolution in number of pixels in source image, or -xpixsize and -ypixsize for forcing the size of pixels in source image pixels. If some pixels are wrong, manipulate -xpo and -ypo to move the sample point within the pixel area. Use -i to invert the output, to see the number of pixels framing the object; subtract them using -e parameter with negative values or the -startx, -starty, -countx and -county parameters for outputting only a subset of the image.

Example: Iron Man

A picture with four colors (black, red, yellow, blue), transparency, and one pixel of outer edge ([?:-e -1] to strip them); the bitmap is shaded, so has to be quantized to lower number; 8 colors yield 5 colors, of which 4 are usable; source image shows 16x18 pixels.

Listing colors:

 qr2laser ironman_pixelart2.png -quantize 8 -n -listcolors -q

Extracting colors:

 qr2laser ironman_pixelart2.png -quantize 8 -xpix 16 -ypix 18 -xpo 3 -rgb 0d0500 -e -1 -q
 qr2laser ironman_pixelart2.png -quantize 8 -xpix 16 -ypix 18 -xpo 3 -rgb ff0300 -e -1 -q
 qr2laser ironman_pixelart2.png -quantize 8 -xpix 16 -ypix 18 -xpo 3 -rgb fdfc00 -e -1 -q
 qr2laser ironman_pixelart2.png -quantize 8 -xpix 16 -ypix 18 -xpo 3 -rgb 6db3fc -e -1 -q

List of colors:

Colors: 5 (quant.8)
  0D0500: 7068, match
  D64200: 270
  FF0300: 6636
  FDFC00: 3820
  6DB3FC: 528
Black stencil:
      []            []      
      []            []      
    []                []    
    []                []    
      []  []    []  []      
    [][]  []    []  [][]    
  []    []        []    []  
[]        [][][][]        []
[]    []            []    []
  [][][]            []    []
      []            [][][]  
      []    []      []      
        []  []      []      
        [][][]    []        
Red stencil:
      []  [][][][]  []      
      []    [][]    []      
    [][]            [][]    
  [][][][]        [][][][]  
        [][]    [][]        
        [][]    [][]  [][]  
Yellow stencil:
        []        []        
        [][]    [][]        
        []  [][]  []        
        []  [][]  []        
  [][]                [][]  
        [][]  [][][]        
Blue stencil:

Example: Batman logo

A logo with two colors (black and yellow), transparency, and one pixel of outer edge (-e -1 to strip them); pixels are 16x16 of source pixels, sample position is 1 pixel both directions from the edge:

Listing colors:

 qr2laser batman-sign-pixel-art.png -n -listcolors

Extracting colors:

 qr2laser batman-sign-pixel-art.png -xypixsize 16 -xypo 1 -rgb 000000 -e -1
 qr2laser batman-sign-pixel-art.png -xypixsize 16 -xypo 1 -rgb ffff00 -e -1

List of colors:

Colors: 2 (quant.256)
  000000: 37528, match
  FFFF00: 27628
Black stencil:
        [][]              [][]        
      []                      []      
    []    []    []  []    []    []    
  []    []      [][][]      []    []  
[]    [][]      [][][]      [][]    []
[]  [][][][]  [][][][][]  [][][][]  []
[]  [][][][][][][][][][][][][][][]  []
[]  [][][][][][][][][][][][][][][]  []
[]  [][][][][][][][][][][][][][][]  []
[]  [][][][][][][][][][][][][][][]  []
[]    [][]  []  [][][]  []  [][]    []
  []    []        []        []    []  
    []            []      []    []    
      []                      []      
        [][]              [][]        
Yellow stencil:
      [][]  [][]  []  [][]  [][]      
    [][]  [][][]      [][][]  [][]    
  [][]    [][][]      [][][]    [][]  
  []        []          []        []  
  []                              []  
  []                              []  
  []                              []  
  []                              []  
  [][]    []  []      []  []    [][]  
    [][]  [][][][]  [][][][]  [][]    
      [][][][][][]  [][][]  [][]      

If you have any comments or questions about the topic, please let me know here:
Your name:
Your email:
Leave this empty!
Only spambots enter stuff here.