DEV Community

Cover image for How Tracing Contours Cut My rMQR SVG Path by 67%
datalog
datalog

Posted on Edited on

How Tracing Contours Cut My rMQR SVG Path by 67%

Whenever I needed vector artwork to survive resizing, rotation, or a trip to the print shop, I had a familiar routine. Open Illustrator, select the pieces, click Unite in Pathfinder, and get on with the job.

Then I caught myself doing that to an rMQR Code generated by my own JavaScript library.

The generator had already worked out exactly which modules were dark. It exported an SVG. And there I was, opening Illustrator to join the pieces it had just drawn. What was I doing?

In my earlier posts, I was trying to save space in the Kanji lookup table and then in the encoded text. This time, the matrix was already finished. I wanted to draw it with less SVG and stop reaching for Pathfinder.

A dot here, a fence over there

The obvious way to draw a QR Code is one square per dark module. It is easy to generate and easy to check. It also means that a solid block of dark modules is described as a pile of little squares, each with its own position and edges.

I tried the usual ways of making the markup smaller. Move the fill color to the root element. Define a square once and reference it with <use>. Eventually I settled on one <path> with a little rectangular subpath for each dark module.

Then I started joining neighbors. If five dark modules sit next to each other in a row, draw one rectangle five modules wide. Do the same down the columns, compare the two strings, and keep the shorter one.

That helped, but the rectangles still met along internal edges. In some of my export and rendering workflows, I could see hairline seams after scaling or transforming artwork built from touching shapes. Sometimes the squares looked like dots. Sometimes the longer strips looked like a picket fence. The geometry said the pieces met. The rendered result occasionally disagreed.

My usual workaround was to make the pieces a tiny bit bigger so they overlapped. The idea felt familiar from print. In offset printing, trapping creates small overlaps to compensate for misregistration. In my SVG, I was borrowing the overlap trick to hide a seam. Adobe’s guide to trapping

It worked well enough that I kept doing it. Then came that next trip to Illustrator. I already had the grid in JavaScript, including the answer to whether every pair of neighbors belonged to the same dark region. Why was I asking a vector editor to figure that out again?

Four ways to draw the same grid

I used the same 43 × 13 rMQR matrix for all four versions. It has 283 dark modules. All three rectangle passes start at the bottom right. The per-module and horizontal passes go row by row, while the vertical pass goes column by column.

Method Subpaths Characters in d
One square per module 283 3,676
Merge vertical runs 152 1,978
Merge horizontal runs 159 2,073
Trace the contours 17 1,195

The counts cover the d attribute alone, before compression. All four methods use z to close the final side. The 17 contour subpaths include the cutouts.

Tracing reduced the path string by about 67% compared with individual squares and 40% compared with the shorter grouped path. For this grid, that was the vertical pass.

What those paths actually draw

All four paths below draw the same solid 3 × 2 block, with its top-left corner at (8, 2).

One square per module Each of the six dark modules gets its own rectangle.

M10,3h1v1h-1zM9,3h1v1h-1zM8,3h1v1h-1zM10,2h1v1h-1zM9,2h1v1h-1zM8,2h1v1h-1z
Enter fullscreen mode Exit fullscreen mode

Vertical runs The two dark modules in each column form one rectangle.

M10,2h1v2h-1zM9,2h1v2h-1zM8,2h1v2h-1z
Enter fullscreen mode Exit fullscreen mode

Horizontal runs The three dark modules in each row form one rectangle.

M8,3h3v1h-3zM8,2h3v1h-3z
Enter fullscreen mode Exit fullscreen mode

Contours The boundary of the whole block forms one closed rectangle.

m11,4h-3v-2h3z
Enter fullscreen mode Exit fullscreen mode

The fill is identical in every case. The difference is how many edges we ask SVG to draw.

Left to right: six squares, three vertical runs, two horizontal runs, one contour.

Parts × modules per part: 6 × 1 = 3 × 2 = 2 × 3 = 1 × 6 = 6 modules.

The difference shows up when a shape extends in both directions. A solid 3 × 2 block takes six squares, two horizontal strips, or three vertical strips. Its outline is one rectangle. The grid already tells us where that outline is.

Keep the edges that matter

For each dark module, contourPath checks its four neighbors. A side belongs to the boundary if it faces a light module or the outside of the matrix. When two dark modules touch, their shared side contributes nothing to the outline.

Left to right: two modules with directed edges; their shared side highlighted; the remaining outer boundary. The opposing edges on the shared side are removed.

I store each surviving edge at its starting vertex on the grid. The four directions fit in a bit mask. The numbers beside the arrows in the diagram are their bit values.

0001  =  1  |  right
0010  =  2  |  down
0100  =  4  |  left
1000  =  8  |  up
Enter fullscreen mode Exit fullscreen mode

A vertex can have more than one outgoing edge, so several bits can be set at once.

At the shared vertex, two edges point out:

0100 | 0001   = 0101
left | right
   4 | 1      = 5
Enter fullscreen mode Exit fullscreen mode

Each edge points in the direction that keeps the dark area on its right. That little rule determines which way the walk goes around the outside and which way it goes around a cutout.

Follow the edges into closed paths

Starting at a vertex with an unused edge, the walk takes that edge, clears its bit, and pushes the next vertex onto a stack. When it reaches a vertex with no outgoing edges left, it pops back along the stack and records the route in reverse.

The stack earns its keep when two dark squares meet at a corner. There can be more than one way out of that vertex. The walk can return to it and take the remaining branch, visiting the same vertex twice in one closed route. When that route is complete, the outer loop looks for another unused edge elsewhere in the grid.

When writing the SVG, I keep the turns. Five steps to the right become h5. Three steps up become v-3. The closing z brings the path back to its starting vertex.

1. At the shared vertex, choose the right edge.
   Clear bit 1: 0101 -> 0100

2. The top and right edges have been followed.
   Choose the bottom edge, pointing left.

3. The right loop is complete.
   At the shared vertex again, choose the left edge.
   Clear bit 4: 0100 -> 0000
Enter fullscreen mode Exit fullscreen mode

The orange point is the shared vertex. Red marks the selected edge, green the edges already followed, and blue the unused edges. Continuing around the left loop joins both loops into one closed route.

A hole is a contour too

Take a 3 × 3 block of dark modules and remove the center. The walk finds an outside boundary and a smaller boundary around the light module.

Following the dark-on-the-right rule takes the outside boundary clockwise and the inside boundary counterclockwise. Both go into the same SVG path. Under SVG’s default nonzero fill rule, their winding contributions cancel inside the hole, so the background shows through. SVG fill rules

Left:  8 dark modules, 1 empty center.
Right: the same shape, traced as two contours.

Outer boundary: clockwise.
Inner boundary: counterclockwise.

Inside the ring: one winding contribution.
Inside the hole: opposite contributions cancel.
Enter fullscreen mode Exit fullscreen mode

In the exported SVG, the foreground is a single filled path.

Try it on your own rMQR Code

Set vrb: 1 to get a square per dark module, or use the default to get the traced path. Both start from the same encoded matrix, so you can compare the SVGs without changing the message or the symbol.

Open the live demo, expand Inside rMQR Code, and turn on Explore SVG contours to see the routes and cutouts.

Top comments (0)