Automating 'Hand-Written' Cards
SVGs, Javascript, Typescript, Python, Golang, and a 3D Printer Turned 2D(?) Printer
My fiancée sat down to write 50+ names and addresses for our wedding and luckily I walked into the room soon after she had started, a handful of populated cards sitting on the table. I said stop, give me a few days, and I bet I can automate this and do it with a nice script font. I had used my old 3D printer as a plotter before; how hard could this be?
As always, the code can be found on Gitlab
Day 1
On the Dimensionality of Writing
Is the act of writing something by hand 3D? 2D? 2.5D? 3D movements are necessary but the result is, for all intents and porpoises, 2D. Regardless, my 3D printer was incapable of weilding neither a pen nor a sword so I set off to steal someone else’s hard work. Searching for “MK4 plotter” on www.printables.com yielded a few results, one of which is this beautifully designed attachment that bolts to the extruder of a stock Prusa MK4 allowing it to hold a pen or pencil, exquisite. Isn’t the Internet great?
Learning to Draw
I printed the attachment and loaded up a fine-point felt-tipped pen. Following the instructions on the Printables page, I went to https://jscut.org/jscut.html, uploaded the provided settings.json
and SVG, and downloaded the gcode that popped out the other end. I uploaded the gcode to Octoprint, secured a piece of paper to the build plate using a few magnets, and hit Print. As soon as the printer started moving I realized that the plunge depth of 2.5mm in the default settings was too shallow for where the pen was secured in the mount. To fix this I manually moved the Z axis to 3mm and loosened the brackets that hold the pen in place so it could drop down and touch the paper below. 3mm was now 0mm in the pen coordinate space. After adjusting the settings to match my new coordinate system I tried again and voila, a cat! Sort of. The printer limited the movement because it thought it was trying to move to negative x values, but the basic operation is there.
Learning to Write
The basic workflow was clear: svg --> gcode
. The next step was generating an SVG from text. Likely searching for “text to SVG” I stumbled on a very useful Github pages site https://danmarshall.github.io/google-font-to-svg-path/ that does exactly what it says it does. You select a font from the Google Fonts Library, type what you want, and it spits out an SVG.
The same process for generating gcode was repeated and the text test looked great.
This is all well and good for one-off drawings or pieces of text, but the whole process of manually typing the text, downloading the SVG, uploading to JSCUT, and fiddling with the settings was all very tedious and rife with tripping hazards. I cannot be trusted to not mess up repeating that process dozens of times.
Day 2
All my Homies Hate Web Apps
As it turns out, the SVG to GCode pipeline is not uncommon and is the standard for both CNC routers and pen plotters alike. Searching for “SVG to gcode” led me to yet another Github pages site https://sameer.github.io/svg2gcode/ which in turn led me to the project’s Github repo https://github.com/sameer/svg2gcode.
This is a strange looking repo though, no setup.py
or CMakelists.txt
, I did’t see any .cpp
or even a configure.sh
. Just what exactly is in here? Having never actually written Rust myself, I do know the tell-tale markings of a developer who optimizes for speed and memory safety above all, ease of syntax or street cred be damned. Cargo.lock
and Cargo.toml
were familiar enough and after a quick cargo build
I had a shiny new binary to play with.
$ svg2gcode -h
Command line interface for svg2gcode
Usage: svg2gcode [OPTIONS] [FILE]
Arguments:
[FILE] A file path to an SVG, else reads from stdin
Options:
--tolerance <TOLERANCE>
Curve interpolation tolerance (mm)
--feedrate <FEEDRATE>
Machine feed rate (mm/min)
--dpi <DPI>
Dots per Inch (DPI) Used for scaling visual units (pixels, points, picas, etc.)
--on <TOOL_ON_SEQUENCE>
G-Code for turning on the tool
--off <TOOL_OFF_SEQUENCE>
G-Code for turning off the tool
--begin <BEGIN_SEQUENCE>
G-Code for initializing the machine at the beginning of the program
--end <END_SEQUENCE>
G-Code for stopping/idling the machine at the end of the program
-o, --out <OUT>
Output file path (overwrites old files), else writes to stdout
--settings <SETTINGS>
Provide settings from a JSON file. Overrides command-line arguments
--export <EXPORT>
Export current settings to a JSON file instead of converting
--origin <ORIGIN>
Coordinates for the bottom left corner of the machine
--dimensions <DIMENSIONS>
Override the width and height of the SVG (i.e. 210mm,297mm)
--circular-interpolation <CIRCULAR_INTERPOLATION>
Whether to use circular arcs when generating g-code [possible values: true, false]
--line-numbers <LINE_NUMBERS>
Include line numbers at the beginning of each line [possible values: true, false]
--checksums <CHECKSUMS>
Include checksums at the end of each line [possible values: true, false]
--newline-before-comment <NEWLINE_BEFORE_COMMENT>
Add a newline character before each comment [possible values: true, false]
-h, --help
Print help (see more with '--help')
-V, --version
Print version
Ok so the gcode portion is handled, one less web interface to deal with, but the test plots had also uncovered another issue: names and addresses span multiple lines and the font-to-svg website could only handle single lines. To work around this I had manually generated SVGs for a name, then a street address, then a city + state + zip. It worked, in theory, but only a couple times was I able to align the address box well enough that the text fell where I needed it to. More often than not something was misaligned and with a finite number of cards at hand I did not have the luxury of repeating prints until I got it right.
Nothing to fear! Mr. Marshall, the author of the SVG website, had graciously provided a link to his source code! Isn’t the Internet great? Well yes, but also my understanding of frontend languages begins and ends with hand jamming HTML for my Geocities website in middle schoool. Shout-out https://www.cameronsworld.net/ for keeping the dream alive.
I channeled my inner web dev and squinted at the .ts
and .js
files, hoping to divine some understanding of what I was looking at. After a few minutes nothing jumped out at me so I searched for “svg” and was served this interesting looking function
callMakerjs(font: opentype.Font, text: string, size: number, union: boolean, filled: boolean, kerning: boolean, separate: boolean,
bezierAccuracy: number, units: string, fill: string, stroke: string, strokeWidth: string, strokeNonScaling: boolean, fillRule: FillRule) {
//generate the text using a font
const textModel = new makerjs.models.Text(font, text, size, union, false, bezierAccuracy, { kerning });
if (separate) {
for (const i in textModel.models) {
textModel.models[i].layer = i;
}
}
const svg = makerjs.exporter.toSVG(textModel, {
fill: filled ? fill : undefined,
stroke: stroke ? stroke : undefined,
strokeWidth: strokeWidth ? strokeWidth : undefined,
fillRule: fillRule ? fillRule : undefined,
scalingStroke: !strokeNonScaling,
});
const dxf = makerjs.exporter.toDXF(textModel, { units: units, usePOLYLINE: true });
this.renderDiv.innerHTML = svg;
this.renderDiv.setAttribute('data-dxf', dxf);
this.outputTextarea.value = svg;
}
Sweet, so svg
is created in makerjs
which I assume is in the javascript(?). Hopping over to index.js
I searched for makerjs
and found a few references but after looking at the rest of the code was left with more questions than answers. Are JS people ok?
App.prototype.handleEvents = function () {
this.fileUpload.onchange = this.readUploadedFile;
this.fileUploadRemove.onclick = this.removeUploadedFont;
this.selectFamily.onchange = this.loadVariants;
this.selectVariant.onchange =
this.textInput.onchange =
this.textInput.onkeyup =
this.sizeInput.onkeyup =
this.unionCheckbox.onchange =
this.filledCheckbox.onchange =
this.kerningCheckbox.onchange =
this.separateCheckbox.onchange =
this.bezierAccuracy.onchange =
this.bezierAccuracy.onkeyup =
this.selectUnits.onchange =
this.fillInput.onchange =
this.fillInput.onkeyup =
this.strokeInput.onchange =
this.strokeInput.onkeyup =
this.strokeWidthInput.onchange =
this.strokeWidthInput.onkeyup =
this.strokeNonScalingCheckbox.onchange =
this.fillRuleInput.onchange =
this.renderCurrent;
I am well out of my comfort zone. Copilot tells me that makerjs
is a Javascript package and I eventually find https://maker.js.org/. This is starting to feel like dependency hell when building from source fails. MakerJS is obviously feature rich and well supported, but it feels like killing a gnat with a cannon. I just need to generate an SVG, not build a website to generate CNC paths.
Ok what if I can use the repo I have and hack it to give me the SVG? That feels more approachable and I could lean on Copilot to write the Javascript or Typescript that I need. I at least know that node install
does….something….so I ran that in the local repo and sure enough it installed things. But I need a command line tool, not a web page so how does that work? I phoned a friend or two and learned that it’s frowned upon if not outright illegal for Javascript to have access to the host file system, something about web security it seems. Well, at least I have a good reason to look elsewhere now.
Back to searching and I stumbled on another incredibly feature-rich project called DrawSVG. It’s Python native and even supports custom fonts, exactly what I needed.
Day 4
Not all SVGs Are Created Equal
After spending well over a day trying to get the Javascript/Typescript solution to work I pivoted to DrawSVG and within a few minutes had a single script that could take any custom text and produce an SVG. I could even call svg2gcode
using subprocess
! All my problems were solved and the end was in sight.
import drawsvg as draw
import subprocess
import os
import svgwrite
from svgpathtools import svg2paths
testPrint = True
svgName = "font.svg"
fontStyle = "Tangerine"
textToDraw = "Dr. Steve Brule"
d = draw.Drawing(300, 110, origin="center")
d.embed_google_font(fontStyle, text=set(textToDraw))
#draw.Text(text, size, x, y, center=False, font_family=None)
d.append(draw.Text(textToDraw, 35, 0, 0, center=True, font_family=fontStyle))
d.append(draw.Path())
d.save_svg(svgName)
## Prep for GCode
upCmd = "G0 Z4.5"
downCmd = "G0 Z3.25"
if testPrint:
downCmd = "G0 Z2.9"
feedrate = 3000
dimensions = "50mm,29mm"
origin = "0,0"
dpi = 96
endSequence = "G0 X0 Y0 Z30"
outfile = "script.gcode"
subprocess.run(
[
"svg2gcode",
f'{os.getcwd()}/{svgName}',
"--on",
upCmd,
"--off",
downCmd,
"--feedrate",
str(feedrate),
"-o",
outfile,
"--dimensions",
dimensions,
"--origin",
origin,
"--dpi",
str(dpi),
"--end",
endSequence,
]
)
I opened the SVG and inspected it, everything looks great.
No errors from svg2gcode
, but upon inspection the only code inside are the start and stop commands. What gives?
G21
G90;svg > path
G0 Z2.9
G0 X0 Y0 Z30
I opened the SVG in a text editor (they’re actually just XML) and nothing seemed wrong to my untrained eye. I see my font, the view box size, and the base64 encoded data so why is the gcode empty?
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="300" height="110" viewBox="-150.0 -55.0 300 110">
<style>/*<![CDATA[*/@font-face {
font-family: 'Tangerine';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(data:application/octet-stream;base64,<SNIP FOR FORMATTING>) format('truetype');
}
/*]]>*/</style>
<defs>
</defs>
<text x="0" y="0" font-size="35" font-family="Tangerine" text-anchor="middle" dominant-baseline="central">Dr. Steve Brule</text>
<path d="" />
</svg>
Going back to https://danmarshall.github.io/google-font-to-svg-path/ I generated an SVG with the same font and same text and downloaded that to compare against the one I had just generated locally. The difference was immediately obvious. Here’s a snippet:
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" height="400" width="800">
<path d="M24.648583 15.688944Q24.738542 15.748917 24.693563 15.868861Q24.648583 15.988806 24.543632 16.138736Q24.438681 16.288667 24.28875 16.438597Q24.138819 16.588528 24.018875 16.6485Q23.389167 16.828417 22.86441 17.18825Q22.339653 17.548083 21.889861 18.237764Q21.440069 18.927444 21.035257 19.991951Q20.630444 21.056458 20.240625 22.615736Q19.730861 24.594819 19.131139 26.304028Q18.531417 28.013236 17.841736 29.542528Q18.201569 29.482556 18.531417 29.347618Q18.861264 29.212681 19.041181 29.092736Q19.251083 29.092736 19.161125 29.302639Q18.621375 29.692458"
/>
</svg>
The d
tag in the SVG generated using my Python script is suspiciously empty, and the clue is in the title of the website that generatead the SVG that can be parsed into GCode: path.
It turns out that while the SVG standard is effectively just XML representing a vector font or image (shapes, they’re all just shapes) that there are mutliple ways of representing the same data. The web app that I had used generates paths, and the Python library generates base64 encoded data. svg2gcode
expects to find a path in the d
tag and cannot operate on the encoded data.
Back to square one, I now search for “python text to svg path” and find yet another Github page walking me through the process of converting text to an SVG path: https://catherineh.github.io/programming/2018/02/01/text-to-svg-paths
While thorough and educational, the process is brittle and esoteric. Why do I need
def tuple_to_imag(t):
return t[0] + t[1] * 1j
just to render a path? This isn’t optics or control theory; we don’t need to bring imaginary numbers into this.
Day 5
A Diamond in the Rough
At this point I am running out of ideas and my searches have broadened to just “text to svg path”. I no longer care about the language as long as it isn’t Javacript or Typescript. Surely this is a solved problem for non-web people, right? With all of the CNCs, pen plotters, and other machines running GCode someone must have solved the problem of generating a path from text.
I keep finding the same pages with the same information, desparation is growing, and then I come across a Stack Overflow post for a Golang question about converting text to an SVG path https://stackoverflow.com/questions/64324315/how-change-font-text-to-svg-path. It has a single reply from the maintainer of a Golang library called Canvas and the user answered with a succinct snippet showing not only path creation, but path creation using a custom font file. Isn’t the internet great?
fontFamily := canvas.NewFontFamily("Lato")
if err := fontFamily.LoadFontFile("Lato-Bold.ttf", canvas.FontBold); err != nil {
panic(err)
}
face := fontFamily.Face(14.0, canvas.Black, canvas.FontBold, canvas.FontNormal)
path, err := face.ToPath("Change me to svg path")
if err != nil {
panic(err)
}
tpl := `<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" height="400" width="800">
<path d="%s" />
</svg>`
fmt.Printf(tpl, path.ToSVG())
Light at the End of the Tunnel
10 minutes later and I have a working example based off of Taco’s snippet, replacing the call to fmt.Printf
with file.Writestring
so I can write the SVG off to a file. I have my font loaded, text ready, and even managed to append a new line to the text so the name and address can be rendered as a single SVG. Confidence is at an all time high. go run main.go
and out pops test.svg
. I open the file and am met with yet another road block. The text is rendered correctly, but it is mirrored about the x axis.
I started crawling around the API, tab-completing things and seeing what kinds of operations I could do on the Face
object, but nothing yielded any viable results. I even found a Transform
method that takes an affine tansform matrix as the input, but after rotating 180o the text was no longer visible.
path = path.Transform(canvas.Matrix{
{-1.0, 0.0, 0.0},
{0.0, -1.0, 0.0},
})
The format of the affine transform matrix is effectively a 2D rotation matrix with translations slapped on the side
m := canvas.Matrix{
{ cos_theta -sin_theta, deltaX },
{ sin_theta, cos_theta, deltaY }
}
I assumed maybe the center of rotation didn’t align with the text center and tried sliding it around some but still couldn’t find the text
m := canvas.Matrix{
{-1.0, 0.0, 50.0},
{0.0, -1.0, 50.0},
}
tx, ty, theta, sx, sy, phi := m.Decompose()
fmt.Printf("tx: %f, ty: %f, theta: %f, sx: %f, sy: %f, phi: %f\n", tx, ty, theta, sx, sy, phi)
$ go run main.go
tx: 50.000000, ty: 50.000000, theta: 0.000000, sx: 1.000000, sy: 1.000000, phi: 180.000000
By this point I had opened an issue and was keeping a log of my efforts. Thankfully Taco, the maintainer, is not only super responsive but also just a really good dude. He quickly answered my questions and suggested alternatives as well. Go give Taco a star or at least take a look at his work because it’s one of the better open source packages I’ve dug into.
With Taco’s help I was finally able to get an SVG rendered right side up using a custom font, custom text, and passed in as a command line argument. Mama we made it.
Day 6
Bashing my Head
With my Golang code written all that was left to do was call svg2gcode
from the Go app and my goal of a single binary to produce custom GCode would be realized. Go’s os/exec package supplies the os.Command
doohickey that can run system commands in the background. It’s a neat little POIX-complianat way to abuse other binaries from a Go program, because all the cool kids avoid writing Bash scripts.
os.Command
takes the binary to run and then a variable number of arguments to pass in. I defined all my arguments in a map and passed that in to my command.
const (
upCmd = `"G0 Z4.5"`
downCmd = `"G0 Z3.25"`
testDown = `"G0 Z2.9"`
feedRate = `"3000"`
origin = `"0,0"`
endSequence = `"G0 X0 Y0 Z30"`
dpi = `"96.0"`
dimensions = `"50mm,29mm"`
dY = 32.0
dX = 5.0
)
var args = map[string]string{
"--on": upCmd,
"--off": downCmd,
"--feedrate": feedRate,
"--origin": origin,
"--end": endSequence,
"--dpi": dpi,
"--dimensions": dimensions,
}
for k, v := range args {
argString.WriteString(fmt.Sprintf("%s %s ", k, v))
}
cmd := exec.Command("svg2gcode", infile, argString.String())
output, err := cmd.Output()
if err != nil {
log.Default().Println(err)
}
$ go run main.go
2024/10/31 17:31:13 exit status 2
At this point my “couple day” project is nearing a week and I’m fine not being one of the cool kids, so I wrote a bash script that accepts the svg and a file name for the GCode output. I don’t really care that exec.Command
is failing despite it being annoying; I can work around it.
svg2gcode $1 --begin "G28X G28Y" --on "G0 Z3.25" --off "G0 Z4.5" --feedrate 3000 --origin "33,25" --end "G0 X0 Y0 Z30" --dpi 96 --dimensions "50mm,29mm" -o $2
Calling my bash script from the Go co works flawlessly and finally, finally, I can generate GCode with a single binary (and a bash script). But here’s the thing, I do want to be one of the cool kids, and the allure of a single binary doing all my work for me is just too shiny to ignore, so I start debugging the intelligent way: brute force combinations.
// Attempt #1
for k, v := range args {
argString.WriteString(fmt.Sprintf("%s %s ", k, v))
}
cmd := exec.Command("svg2gcode", infile, argString.String())
fmt.Println("Trying command: ", cmd.String())
output, err := cmd.Output()
fmt.Println(string(output))
if err != nil {
log.Default().Println(err)
}
// Attempt #2
argss := []string{
"svgs/John.svg",
"--origin", "0,0",
"--end", "G0 X0 Y0 Z30",
"--dpi", "96.0",
"--dimensions", "50mm,29mm",
"-o", "gcode/John.gcode",
"--on", "G0 Z4.5",
"--off", "G0 Z3.25",
"--feedrate", "3000",
}
cmd = exec.Command("svg2gcode", argss...)
fmt.Println("Trying command: ", cmd.String())
output, err = cmd.Output()
fmt.Println(string(output))
if err != nil {
log.Default().Println(err)
}
// Attempt #3
ctx := context.Background()
cmd = exec.CommandContext(ctx, "svg2gcode", infile,
"--origin", "0,0",
"--end", `"G0 X0 Y0 Z30"`,
"--dpi", "96.0",
"--dimensions", "50mm,29mm",
"-o", "Jeremy.gcode",
"--on", `"G0 Z4.5"`,
"--off", `"G0 Z3.25"`,
"--feedrate", "3000")
fmt.Println("Trying command: ", cmd.String())
output, err = cmd.Output()
fmt.Println(string(output))
if err != nil {
log.Default().Println(err)
}
// Attempt #4
args["-o"] = outfile
cmd = exec.Command("svg2gcode", fmt.Sprintf("svgs/%s.svg ", outFileStem), "--dpi", args["--dpi"], "--feedrate", args["--feedrate"], "--origin", args["--origin"], "--end", args["--end"], "--dimensions", args["--dimensions"], "-o", args["-o"])
fmt.Println("Trying command: ", cmd.String())
output, err = cmd.Output()
fmt.Println(string(output))
if err != nil {
log.Default().Println(err)
}
$ go run main.go
Trying command: /home/scott/.local/bin/svg2gcode svgs/Jeremy.svg --dimensions "50mm,29mm" --on "G0 Z4.5" --off "G0 Z3.25" --feedrate "3000" --origin "0,0" --end "G0 X0 Y0 Z30" --dpi "96.0"
2024/10/31 17:31:13 exit status 2
Trying command: /home/scott/.local/bin/svg2gcode svgs/John.svg --origin 0,0 --end G0 X0 Y0 Z30 --dpi 96.0 --dimensions 50mm,29mm -o gcode/John.gcode --on G0 Z4.5 --off G0 Z3.25 --feedrate 3000
Trying command: /home/scott/.local/bin/svg2gcode svgs/Jeremy.svg --origin 0,0 --end "G0 X0 Y0 Z30" --dpi 96.0 --dimensions 50mm,29mm -o Jeremy.gcode --on "G0 Z4.5" --off "G0 Z3.25" --feedrate 3000
2024/10/31 17:31:13 exit status 1
Trying command: /home/scott/.local/bin/svg2gcode svgs/Jeremy.svg --dpi "96.0" --feedrate "3000" --origin "0,0" --end "G0 X0 Y0 Z30" --dimensions "50mm,29mm" -o gcode/Jeremy.gcode
2024/10/31 17:31:13 exit status 2
Wait, one of those worked. But why? Attempt number 3 worked, but I have the output file hardcoded. I need both it and the input file to be dynamically named so I can eventually feed this thing a CSV or Excel sheet and have it process everything at once. It felt like I was close, so what happens if I pass in the output file as a variable?
outfile := fmt.Sprintf("gcode/%s.gcode", outFileStem)
infile := fmt.Sprintf("svgs/%s.svg ", outFileStem)
cmd = exec.CommandContext(ctx, "svg2gcode", infile,
"--origin", "0,0",
"--end", `"G0 X0 Y0 Z30"`,
"--dpi", "96.0",
"--dimensions", "50mm,29mm",
"-o", outfile,
"--on", `"G0 Z4.5"`,
"--off", `"G0 Z3.25"`,
"--feedrate", "3000")
$ go run main.go
2024/10/31 18:12:23 exit status 2
What. Why? Why is it fine with one string variable but not the other? Strings are strings and should be fine to pass as arguments to svg2gcode
.
What happens if I call fmt.Sprint
on my already string variables? Nothing should change because the output of fmt.Sprint
is a string.
outfile := fmt.Sprintf("gcode/%s.gcode", outFileStem)
infile := fmt.Sprintf("svgs/%s.svg ", outFileStem)
cmd = exec.CommandContext(ctx, "svg2gcode", fmt.Sprint(infile),
"--origin", "0,0",
"--end", `"G0 X0 Y0 Z30"`,
"--dpi", "96.0",
"--dimensions", "50mm,29mm",
"-o", fmt.Sprint(outfile),
"--on", `"G0 Z4.5"`,
"--off", `"G0 Z3.25"`,
"--feedrate", "3000")
$ go run main.go
It works. It doesn’t fail. I get both an SVG and GCode out the other end and everything is happy. I still do not understand why, and at this point I don’t really care, either. But what I don’t like is leaving bare strings in the argument list, so I pivot back to the original implementation, call fmt.Sprint
on all of them, and nothing yells at me when I run it this time.
args := []string{
fmt.Sprintf("svgs/%s.svg", firstName),
"--origin", fmt.Sprint(origin),
"--end", fmt.Sprint(endSequence),
"--dpi", fmt.Sprint(dpi),
"--dimensions", fmt.Sprint(dimensions),
"--on", fmt.Sprint(downCmd),
"--off", fmt.Sprint(upCmd),
"--feedrate", fmt.Sprint(feedRate),
"--begin", fmt.Sprint(startSequence),
"-o", fmt.Sprint(outfile),
}
Day 7
Finishing Touches and Test Cards
I pulled in Excel support and can read an excel file of the format
NameOne | LastNameOne | NameTwo | LastNameTwo | Address | CityStateZip |
---|---|---|---|---|---|
John | Jane | Doe | 1234 Oak Lane | Las Vegas, NV 45321 |
This is necessary because couples with long names don’t fit on the first line of the address area. I instead chose to split the couples like
John &
Jane Doe
1234 Oak Lane
Las Vegas, NV 45321
which also allows for couples with two different last names. Still though, even allowing for a wide range of names and text spacing, I had a finite amount of cards to work with and only a handful that could go wrong. With that in mind I made a test card in GIMP accurate to the dimensions and spacing of the real ones. Lines pointing to the corners allow me to cut the test card out of a sheet of paper and index it against the corner of the print bed. I know the offset from that corner to the corner of the address area, and I set that delta as the origin in the call to svg2gcode
. Throughout this process I also learned about the G28XY
command in the Marlin firmware which tells the printer to home X and Y only, allowing for homing while the pen is in place even though it sits lower than the nozzle.
As I’m typing this the printer is writing away and I have to get up to swap cards every minute or so, but it’s doing the hard work for us and the text looks great.
A scant week later and an hour long manual job has been automated.