Marc Dunand
WorkCVAboutContact
1 / 8
Major Works AnyHersheyWater ClockInk DripCDMScribblerBlick TypologyGenTruchetBall and LineLightsailWhat We Share Experiments CandleshiftDouble-Ended DrawingHow Do We See Art?Seven Days of DrawingEmotion Data PostcardThe 1x1080p DisplayFlockfinderSpirograph²JeremiahTDF3sLook Down
Major Works
  • AnyHershey
  • Water Clock
  • Ink Drip
  • CDM
  • Scribbler
  • Blick Typology
  • GenTruchet
  • Ball and Line
  • Lightsail
  • What We Share
Experiments
  • Candleshift
  • Double-Ended Drawing
  • How Do We See Art?
  • Seven Days of Drawing
  • Emotion Data Postcard
  • The 1x1080p Display
  • Flockfinder
  • Spirograph²
  • Jeremiah
  • TDF3s
  • Look Down

AnyHershey, 2026

Python, Inkscape, scikit-image, vpype

Digital tool for artists and fabricators who want to create text for plotters and CNCs

Download for Windows

In late 2025, I noticed a gap in the tooling available for digital artists and fabricators who create text with CNC-based systems like pen plotters. Back in 1967, A. V. Hershey created a series of fonts called ‘Hershey fonts’, which allow CNC pen plotters to write characters using the same strokes as a person. However, a limitation of Hershey fonts is that every character has to be designed by hand.

This is okay for a language like English that has a small character set, but becomes problematic in other cases. These include writing systems with a large character set (see Lingdong Huang’s work for hersheying Chinese), writing systems that are evolving (Japan added 196 characters to its standard Jōyō kanji set in 2010), and the many writing systems where no one has created a Hershey font.

I created AnyHershey to address all of these issues and to hershify text that contains multiple writing systems in one go. Rather than relying on hardcoded characters, you can input writing in any script in any font installed on your computer and automatically hershify it. This allows artists and fabricators to create CNC-ready text on demand in any language.

Behind the Project

Inspiration

A pen plotter or laser cutter draws along lines. Regular fonts store each character as a filled outline, so plotting one traces around the edge of every stroke and the letters come out hollow. The usual fix is a font where each character is made of lines drawn once. The best known are the Hershey fonts, designed by A. V. Hershey at the U.S. Naval Weapons Laboratory in 1967, and from here on I call any font like this a Hershey font.

Three versions of the word plot: solid black letters in Arial; the same letters drawn as outlines only, so every stroke is a thin loop; and the word in a Hershey font, where each stroke is a single line

Left: Arial, a regular font. Middle: what a plotter draws from it. Right: Hershey's Roman Simplex (as included in vpype), made of single lines.

I ran into this while building Babble, a project of mine that writes out what people say on a pen plotter live. Its beta testers spoke English, Chinese, and French, so Babble needed a way to turn multiple alphabets into text automatically. Furthermore, in order to keep up with the speed at which people spoke, I needed to draw as few strokes as possible, so existing tools which turned regular fonts into outlines were more than doubling the amount of time required to draw letters. This was a perfect storm: I needed a tool that could generate Hershey text in any alphabet, and I couldn't find a tool online that could do this. This directly resulted in the AnyHershey project, which released for Windows in January 2026.

How it works

AnyHershey takes the shape of every character from a regular font, then finds the line running down the middle of each stroke. Each step is shown below on 永 ("eternity"), a character calligraphy students practice because it contains all eight basic types of stroke in Chinese regular script.

1.Outlines. AnyHershey writes the text into an SVG file and has Inkscape convert it to paths: the filled outline of every character in the preset's font.

2.Mask. Inkscape exports the outlines as an image, which AnyHershey thresholds into black and white to create a binary mask.

3.Skeleton. scikit-image's skeletonize function (Lee's method) removes pixels from the edges of the mask until every stroke is one pixel wide. What is left, along the middle of each stroke, is the skeleton.

4.Strokes. AnyHershey traces the skeleton's pixels into lines, the strokes a plotter will draw, in millimeters.

5.Plotting order. vpype, an existing library for preparing plotter files, joins strokes whose ends touch, reorders them to cut down travel with the pen raised, then removes points that barely change a stroke's shape.

Diagram in four panels showing the Chinese character 永: its font outline filled in gray, the same shape as a solid black mask, the mask in gray with a thin black skeleton running down the middle of each stroke, and the five traced strokes as black lines with a circle at each end

Steps 1 to 4 on 永 in Microsoft YaHei. Circles mark the two ends of each of the five strokes.

For tracing, each skeleton pixel is counted by how many of its eight neighbors are also skeleton pixels: one neighbor makes it an end, three or more make it a junction, where strokes meet. AnyHershey starts a stroke at every end and junction and follows the skeleton pixel by pixel until no unvisited pixel is left to step to. Closed loops, like the ring of an "o", have no ends or junctions, so they are picked up afterward. When a stroke reaches a junction, it continues onto whichever neighboring pixel changes its direction the least, so a stroke keeps going straight through a crossing instead of turning onto a branch.

Close-up pixel grid of part of 永: light gray squares for the mask, darker gray squares for the one pixel wide skeleton with three branches meeting at a single black junction pixel, and white lines through the pixel centers for the traced strokes

A close-up of 永's skeleton where three branches meet, one square per mask pixel. The traced strokes (white) run through pixel centers.

Tracing finds strokes in no useful order and keeps a point at every pixel. For the words "born free and equal" in the Latin preset, that is 33 strokes and 5,998 points, and a plotter drawing them in that order would travel 870 mm with its pen raised. After vpype, the same words are 27 strokes and 205 points, with 94 mm of pen-up travel.

Two copies of the words born free and equal drawn as Hershey text. In the first, dashed lines for pen-up moves criss-cross the whole line of text many times; in the second, after vpype, the dashed moves are short hops from one letter to the next

Dashed lines are pen-up moves between strokes, in drawing order.

Notes from building it

Some of the problems I ran into building AnyHershey apply to anyone skeletonizing text or other vector art. The numbers come from running the current code (Python 3.11, Pillow 12.1, NumPy 2.4, scikit-image 0.26, Inkscape 1.4.2).

Use existing tools for text layout and plotting order. Getting from a string of characters to glyph outlines takes more than looking up one shape per character. Arabic letters change shape depending on their neighbors and join together. In Devanagari, consonants combine into conjuncts, and some vowel signs are drawn before the consonant they follow in the text. Inkscape lays text out with Pango and HarfBuzz, which handle both. Pillow's basic layout places one glyph per character, breaking both scripts: مرحبا بالعالم ("hello world") comes out as disconnected letters in reverse order, the conjunct क्ष in क्षत्रिय falls apart into क् and ष, and the vowel sign ि lands after य instead of before it.

Two rows comparing text without and with shaping. Arabic: on the left, separate unjoined letters in reversed order; on the right, the joined Arabic phrase. Devanagari: on the left, the word split into separate letters with visible virama marks and a misplaced vowel sign; on the right, the correctly formed word

The same two strings in the same fonts (Segoe UI and Kalam), laid out by Pillow without shaping and by Inkscape.

The same goes for plotting order. vpype already does the joining, reordering, and simplifying a plotter needs, cutting the pen-up travel above from 870 mm to 94 mm. No need to reinvent the wheel.

Fill glyph outlines with the nonzero rule. Font outlines are meant to be filled with the nonzero winding rule, under which a pixel is ink if any contour surrounds it, allowing text contours to overlap. In Segoe UI's lam-alef (لا), for example, the lam and the alef are separate contours that cross. AnyHershey's first mask builder filled each contour with XOR, which is the even-odd rule. A pixel covered by two contours flips back to paper, so the overlap becomes a hole that the skeleton has to go around. The XOR mask gives 9 strokes for لا, where Inkscape's nonzero fill gives 2.

Three versions of the Arabic lam-alef ligature: its two contours drawn half-transparent, crossing near the bottom where the overlap shows darker; the XOR-filled mask, with a small white hole where the contours overlap; and Inkscape's filled mask, solid everywhere

Left: the two contours of لا, half-transparent so the overlap shows darker. Middle: the XOR mask, with a hole where they overlap. Right: Inkscape's fill.

Pick the mask resolution from the thinnest gap. The mask's resolution decides which lines survive skeletonizing. 龘 is written with 48 brush strokes (it is three copies of 龍, "dragon"). In Microsoft JhengHei at 8 mm, the gaps between its brush strokes are as narrow as 0.08 mm, and the brush strokes themselves are 0.25 mm wide (median). At 12 px/mm the narrowest gaps are a single pixel, and AnyHershey's output breaks into pieces. The output's stroke count stops changing at 45 px/mm, where the narrowest gap is about 4 pixels wide.

Six Hershey renderings of the character 龘 at 12, 20, 30, 45, 60, and 90 pixels per millimeter. At 12 and 20 the strokes are broken into short dashes; from 45 up the three renderings look the same

龘 at six mask resolutions, with the stroke count after vpype. The Chinese (Traditional) preset uses 60 px/mm.

This resolution is cheap for individual characters, but it can become a slowdown on page-sized chunks of very intricate text.

Presets for common writing systems

I wanted AnyHershey to be useful to as many people as possible. It has presets for the four most widely used writing systems: Latin (English, Spanish, etc.), Chinese (Simplified and Traditional characters), Arabic, and Devanagari (Hindi, Marathi, Nepali, etc.). The fifth, Bengali, is structurally similar to Devanagari and can use the same preset. There are also presets for Japanese (kanji, hiragana, and katakana) and Korean (hangul).

Anonymous native speakers of each language helped me build every preset. Together we chose the font and adjusted the settings until the output looked correct to them and as close as possible to a neutral, modern typeface. The presets differ most in mask resolution, from 38 px/mm for Latin text to 60 px/mm for Traditional Chinese and Japanese kanji, whose strokes sit closer together.

Stress tests

For each preset, I tested AnyHershey on the most complex characters I could find in its writing system, with the font (rasterized) on top and AnyHershey's output (vectorized) below it. Judge them by the form of the strokes, not their thickness. The output is vector lines with no real width: the thickness on screen is only there so the lines can be seen, and on a plotter or laser cutter it comes from the pen or the beam. That is also why dots come out as small points.

Latin letters with stacked accents and symbols, as the font and as AnyHershey's Hershey version

Latin (English, Spanish, etc.), Arial: Vietnamese letters with two stacked accents, the digraph Dž, capital ẞ, and & @ §.

Six complex simplified Chinese characters, as the font and as AnyHershey's Hershey version

Chinese (Simplified), Microsoft YaHei: 齉 (36 brush strokes), 爨 (30), 馕, 矗, 罐, 衢.

Six complex traditional Chinese characters, as the font and as AnyHershey's Hershey version

Chinese (Traditional), Microsoft JhengHei: 龘 (48 brush strokes), 靐 (39), 齉 (36), 爨 (30), 鬱 (29), 纛.

Six complex Japanese kanji, as the font and as AnyHershey's Hershey version

Japanese (Kanji), Yu Gothic: 鬱 (29 brush strokes), 麤 (33), 驫 (30), 纏, 讒, 鑿.

Hiragana characters with small marks, as the font and as AnyHershey's Hershey version

Japanese (Hiragana), Yu Gothic: kana with the handakuten mark (ぱ, ぽ), small kana, the archaic ゐ and ゑ, and ゔ.

Katakana characters with small marks, as the font and as AnyHershey's Hershey version

Japanese (Katakana), Yu Gothic: ヴ and the rarer ヷ and ヺ, ポ, small kana, the archaic ヰ, and the long-vowel mark ー.

Eight complex Korean syllable blocks, as the font and as AnyHershey's Hershey version

Korean (Hangul), Malgun Gothic: syllable blocks, including doubled consonants and two-consonant final clusters.

Devanagari conjuncts, shri, and om, as the font and as AnyHershey's Hershey version

Hindi-family (Devanagari), Kalam: conjuncts of up to five consonants, श्री, and ॐ.

A line of Arabic with vowel marks and many dots, as the font and as AnyHershey's Hershey version

Arabic (Modern Standard), Segoe UI: a phrase marked with short vowels, sukun, shadda, and tanwin ("Arabic is beautiful"), then the dot-heavy ثلاثة ("three") and لا.

Hi and short greetings in Chinese, Japanese, Korean, Hindi, and Arabic, as the font and as AnyHershey's Hershey version

Default preset, Noto Sans CJK SC, on six writing systems. Noto Sans CJK SC has no Devanagari or Arabic, so Inkscape drew those two words in fonts it picked itself.

Natural text conversion

As a demo of a more 'realistic' chunk of text being converted, I use Article 1 of the Universal Declaration of Human Rights in each preset's language. The translations are from the UDHR in Unicode project.

Article 1 of the Universal Declaration of Human Rights in English, written as Hershey text

English, Latin preset (Arial): 245 strokes.

Article 1 of the Universal Declaration of Human Rights in Spanish, written as Hershey text

Spanish, Latin preset (Arial): 225 strokes.

Article 1 of the Universal Declaration of Human Rights in Simplified Chinese, written as Hershey text

Chinese, Simplified preset (Microsoft YaHei): 277 strokes.

Article 1 of the Universal Declaration of Human Rights in Traditional Chinese, written as Hershey text

Chinese, Traditional preset (Microsoft JhengHei): 310 strokes.

Article 1 of the Universal Declaration of Human Rights in Japanese, written as Hershey text

Japanese, Kanji preset (Yu Gothic): 384 strokes.

Article 1 of the Universal Declaration of Human Rights in Korean, written as Hershey text

Korean, Hangul preset (Malgun Gothic): 262 strokes.

Article 1 of the Universal Declaration of Human Rights in Hindi, written as Hershey text

Hindi, Devanagari preset (Kalam): 322 strokes.

Article 1 of the Universal Declaration of Human Rights in Arabic, written as Hershey text

Arabic, Modern Standard preset (Segoe UI): 173 strokes.

Conclusion

AnyHershey turns typed text into Hershey text, in any writing system and any font installed on your computer. It has presets for the five most widely used writing systems, as well as for Japanese and Korean.

My goal is for AnyHershey to be useful to artists who work with vector graphics and need text in a writing system no Hershey font covers, and to anyone who wants to draw characters with a pen plotter, laser cutter, or other CNC machine. That was my situation with Babble: I needed this tool, I couldn't find it online, so I made it myself.

The source code is on GitHub, and the Windows app is on its releases page.

marcdunandc at gmail dot com instagram.com/marccdunand linkedin.com/in/marc-dunand-cmu github.com/MarcDunand openprocessing.org/@u310940