dxf

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 6 Imported by: 0

README

dxf

CI Go Reference Go Report Card

Write 2D DXF files from Go, for CAM and laser/CNC workflows. No dependencies beyond the standard library.

d := dxf.New(dxf.WithUnits(dxf.Millimeters))
d.AddLayer("CUT", dxf.WithColor(dxf.Red))
d.Circle("CUT", dxf.Point{X: 50, Y: 25}, 4.25)

if err := d.Save("plate.dxf"); err != nil {
    log.Fatal(err)
}
go get github.com/gallowaysoftware/dxf

Why

Go has no maintained DXF writer. If you generate toolpaths, nested cut sheets or engraving layouts from code, the usual options are to shell out to Python and ezdxf, or to hand-roll a DXF emitter and hope your CAM package accepts it.

The hard part is not the entities, it is the R2010 container around them: every object needs a unique handle and a resolvable owner pointer, subclass markers have to appear in a specific order, and a set of tables must exist whether you use them or not. Get any of it wrong and a reader either refuses the file or, worse, opens it with entities quietly missing.

This library gets that container right and keeps the API small.

What it writes

Format AutoCAD R2010 (AC1024)
Entities LINE, CIRCLE, ARC, LWPOLYLINE (with bulge arcs), TEXT
Tables LAYER, LTYPE, STYLE, APPID, DIMSTYLE, BLOCK_RECORD
Geometry 2D — everything is emitted at Z=0 in model space

Not supported: blocks and inserts, dimensions, hatching, splines, ellipses, MTEXT, paper space layouts, or reading DXF. If you need those, you need a full DXF implementation, not this.

Deterministic output

The same drawing always produces the same bytes. There are no embedded timestamps and no generated UUIDs, so DXF output can be committed to a repository and diffed like any other build artifact — regenerating a file that did not change produces no diff at all.

That is deliberate. Most DXF writers stamp $TDCREATE and fresh handle UUIDs into every file, so a rebuild dirties your working tree even when no dimension moved.

Bulges

LWPOLYLINE encodes arcs as a bulge on the vertex the arc leaves: tan(θ/4), where θ is the included angle. Positive sweeps counter-clockwise, negative clockwise, zero draws a straight segment.

BulgeQuarter is the 90° case, which is most of what rounded corners need:

w, h, r := 100.0, 60.0, 8.0
b := dxf.BulgeQuarter
d.Polyline("CUT", []dxf.Vertex{
    {X: r, Y: 0},     {X: w - r, Y: 0, Bulge: b},
    {X: w, Y: r},     {X: w, Y: h - r, Bulge: b},
    {X: w - r, Y: h}, {X: r, Y: h, Bulge: b},
    {X: 0, Y: h - r}, {X: 0, Y: r, Bulge: b},
}, true)

Errors instead of broken files

WriteTo and Save validate before writing a single byte, and fail rather than emit something a reader will mis-handle:

  • an entity on a layer that was never added
  • text in a style that was never defined
  • a line break inside any string value, which would split a group-code pair
d := dxf.New()
d.Circle("MISSING", dxf.Point{}, 1)
_, err := d.WriteTo(&buf)
// dxf: entity 0 (CIRCLE) references undefined layer "MISSING"

TEXT is single-line by definition, so a multi-line label is rejected rather than silently flattened — it would otherwise be engraved into a real part.

Testing

The test suite parses the generated output back into group-code pairs and asserts on structure, rather than matching strings:

  • handles are unique and every owner pointer resolves
  • entities are owned by *Model_Space
  • every mandatory table is present, sections appear in order
  • each entity's group codes, subclass markers and coordinates round-trip
  • a golden file pins the exact bytes, so container changes surface as a diff
  • a fuzz target asserts that any input either errors or produces a file that is whole group-code pairs ending in EOF

Coverage is above 99%. The fuzz target found the line-break bug described above.

go test ./...
go test -fuzz FuzzDrawingStaysWellFormed
go test -bench .

License

MIT — see LICENSE.

Documentation

Overview

Package dxf writes 2D DXF drawings for CAM and laser/CNC workflows.

It produces AutoCAD R2010 (AC1024) files containing layers, text styles and the entity types that 2D toolpath work actually uses: LINE, CIRCLE, ARC, LWPOLYLINE (with bulge arcs) and TEXT. Files are written from scratch with no dependencies beyond the standard library.

Scope

This is a writer, not a reader, and deliberately covers a small surface:

  • Entities: LINE, CIRCLE, ARC, LWPOLYLINE, TEXT.
  • Tables: LAYER, LTYPE, STYLE, APPID, DIMSTYLE, BLOCK_RECORD.
  • Geometry is 2D. Every entity is emitted at Z=0 in model space.

There is no block/insert support, no dimensions, no hatching, no splines and no paper space layout. If you need those, you need a full DXF implementation.

What it does guarantee is that the R2010 container is correct: every object carries a unique handle and a resolvable owner pointer, subclass markers appear in the required order, and the mandatory tables are present. Output is deterministic - the same drawing always produces the same bytes, with no embedded timestamps or generated UUIDs - so files can be committed and diffed.

Usage

d := dxf.New(dxf.WithUnits(dxf.Millimeters))
d.AddLayer("CUT", dxf.WithColor(dxf.Red))
d.Circle("CUT", dxf.Point{X: 50, Y: 50}, 4.25)
if err := d.Save("plate.dxf"); err != nil {
	log.Fatal(err)
}

Bulges turn polyline segments into arcs. A bulge is tan(theta/4) where theta is the included angle, so BulgeQuarter gives a 90 degree arc:

d.Polyline("CUT", []dxf.Vertex{
	{X: 10, Y: 0},
	{X: 40, Y: 0, Bulge: dxf.BulgeQuarter},
	{X: 50, Y: 10},
}, true)
Example
package main

import (
	"bytes"
	"fmt"
	"log"

	"github.com/gallowaysoftware/dxf"
)

func main() {
	d := dxf.New(dxf.WithUnits(dxf.Millimeters))
	d.AddLayer("CUT", dxf.WithColor(dxf.Red))
	d.Circle("CUT", dxf.Point{X: 50, Y: 25}, 4.25)

	var buf bytes.Buffer
	if _, err := d.WriteTo(&buf); err != nil {
		log.Fatal(err)
	}
	fmt.Println("bytes written:", buf.Len() > 0)
}
Output:
bytes written: true

Index

Examples

Constants

This section is empty.

Variables

View Source
var BulgeQuarter = 0.4142135623730951 // tan(pi/8)

BulgeQuarter is the bulge value for a 90 degree arc, the common case when rounding a rectangular corner.

Functions

This section is empty.

Types

type ACI

type ACI int

ACI is an AutoCAD Color Index. Values 1-255 are palette entries; 256 means ByLayer and 0 means ByBlock.

const (
	ByBlock ACI = 0
	Red     ACI = 1
	Yellow  ACI = 2
	Green   ACI = 3
	Cyan    ACI = 4
	Blue    ACI = 5
	Magenta ACI = 6
	White   ACI = 7
	Grey    ACI = 8
	ByLayer ACI = 256
)

The seven standard colours, which are all most CAM setups distinguish.

type Drawing

type Drawing struct {
	// contains filtered or unexported fields
}

Drawing is a DXF document under construction. The zero value is not usable; call New.

func New

func New(opts ...Option) *Drawing

New returns an empty drawing in millimetres. Layer "0" always exists.

func (*Drawing) AddLayer

func (d *Drawing) AddLayer(name string, opts ...LayerOption)

AddLayer defines a layer. Redefining a layer replaces its settings rather than adding a duplicate. Layer "0" is implicit and need not be added.

func (*Drawing) AddTextStyle

func (d *Drawing) AddTextStyle(name, font string)

AddTextStyle defines a text style that Drawing.Text can reference by name. Redefining a style replaces it.

func (*Drawing) Arc

func (d *Drawing) Arc(layer string, centre Point, radius, startDeg, endDeg float64)

Arc appends an ARC entity. Angles are in degrees measured counter-clockwise from the positive X axis, and the arc is always drawn counter-clockwise from start to end.

func (*Drawing) Circle

func (d *Drawing) Circle(layer string, centre Point, radius float64)

Circle appends a CIRCLE entity.

func (*Drawing) Layers

func (d *Drawing) Layers() []Layer

Layers returns the layers defined so far, excluding the implicit "0".

func (*Drawing) Len

func (d *Drawing) Len() int

Len reports how many entities the drawing holds.

func (*Drawing) Line

func (d *Drawing) Line(layer string, a, b Point)

Line appends a LINE entity from a to b.

func (*Drawing) Polyline

func (d *Drawing) Polyline(layer string, verts []Vertex, closed bool)

Polyline appends an LWPOLYLINE. Set closed to join the last vertex back to the first. Per-vertex Bulge values turn segments into arcs.

Example

A rounded rectangle is the common CAM case: four straight runs joined by quarter-arcs, expressed as bulges on alternate vertices.

package main

import (
	"fmt"

	"github.com/gallowaysoftware/dxf"
)

func main() {
	d := dxf.New()
	d.AddLayer("CUT")

	w, h, r := 100.0, 60.0, 8.0
	b := dxf.BulgeQuarter
	d.Polyline("CUT", []dxf.Vertex{
		{X: r, Y: 0}, {X: w - r, Y: 0, Bulge: b},
		{X: w, Y: r}, {X: w, Y: h - r, Bulge: b},
		{X: w - r, Y: h}, {X: r, Y: h, Bulge: b},
		{X: 0, Y: h - r}, {X: 0, Y: r, Bulge: b},
	}, true)

	fmt.Println(d.Len(), "entity")
}
Output:
1 entity

func (*Drawing) Save

func (d *Drawing) Save(path string) error

Save writes the drawing to path, truncating any existing file.

func (*Drawing) Text

func (d *Drawing) Text(layer, s string, at Point, height float64, opts ...TextOption)

Text appends a single-line TEXT entity of the given cap height.

Example
package main

import (
	"fmt"

	"github.com/gallowaysoftware/dxf"
)

func main() {
	d := dxf.New()
	d.AddLayer("ENGRAVE", dxf.WithColor(dxf.Yellow))
	d.AddTextStyle("OpenSans", "OpenSans.ttf")
	d.Text("ENGRAVE", "PART 1", dxf.Point{X: 50, Y: 30}, 12,
		dxf.WithStyle("OpenSans"),
		dxf.WithJustify(dxf.Center, dxf.Middle))
	fmt.Println(d.Len(), "entity")
}
Output:
1 entity

func (*Drawing) TextStyles

func (d *Drawing) TextStyles() []TextStyle

TextStyles returns the text styles defined so far, excluding "Standard".

func (*Drawing) WriteTo

func (d *Drawing) WriteTo(w io.Writer) (int64, error)

WriteTo renders the drawing. It returns an error if an entity references a layer or text style that was never defined, which is the usual way a generated drawing ends up silently wrong.

Example (UndefinedLayer)

Referencing a layer that was never added is reported rather than written out as a silently broken file.

package main

import (
	"bytes"
	"fmt"

	"github.com/gallowaysoftware/dxf"
)

func main() {
	d := dxf.New()
	d.Circle("MISSING", dxf.Point{}, 1)
	_, err := d.WriteTo(&bytes.Buffer{})
	fmt.Println(err)
}
Output:
dxf: entity 0 (CIRCLE) references undefined layer "MISSING"

type HAlign

type HAlign int

HAlign is a TEXT horizontal justification (DXF group 72).

const (
	Left   HAlign = 0
	Center HAlign = 1
	Right  HAlign = 2
)

Horizontal justifications.

type Layer

type Layer struct {
	Name  string
	Color ACI
}

Layer is a drawing layer.

type LayerOption

type LayerOption func(*Layer)

LayerOption configures a layer.

func WithColor

func WithColor(c ACI) LayerOption

WithColor sets a layer's AutoCAD Color Index.

type Option

type Option func(*Drawing)

Option configures a Drawing at construction.

func WithUnits

func WithUnits(u Units) Option

WithUnits sets the drawing's insertion units. The default is Millimeters.

type Point

type Point struct{ X, Y float64 }

Point is a location in the drawing. Geometry is 2D; every entity is emitted at Z=0.

type TextOption

type TextOption func(*entity)

TextOption configures a TEXT entity.

func WithJustify

func WithJustify(h HAlign, v VAlign) TextOption

WithJustify sets how the text is positioned relative to its insertion point. The default is Left/Baseline.

func WithRotation

func WithRotation(deg float64) TextOption

WithRotation rotates the text, in degrees counter-clockwise.

func WithStyle

func WithStyle(name string) TextOption

WithStyle draws the text in a named style previously passed to Drawing.AddTextStyle.

type TextStyle

type TextStyle struct {
	Name string
	Font string
}

TextStyle names a font for TEXT entities. Font is recorded in the file; a viewer substitutes its own if the font is not installed.

type Units

type Units int

Units is the drawing's insertion unit, written to the $INSUNITS header variable. The values are AutoCAD's.

const (
	Unitless    Units = 0
	Inches      Units = 1
	Feet        Units = 2
	Millimeters Units = 4
	Centimeters Units = 5
	Meters      Units = 6
)

Units understood by AutoCAD. Unitless is the DXF default; most CAM workflows want Millimeters or Inches.

type VAlign

type VAlign int

VAlign is a TEXT vertical justification (DXF group 73).

const (
	Baseline VAlign = 0
	Bottom   VAlign = 1
	Middle   VAlign = 2
	Top      VAlign = 3
)

Vertical justifications.

type Vertex

type Vertex struct {
	X, Y  float64
	Bulge float64
}

Vertex is one LWPOLYLINE point. Bulge is tan(theta/4) for the arc leaving this vertex, where theta is the arc's included angle: zero draws a straight segment, positive sweeps counter-clockwise, negative clockwise.

Example

A bulge is tan(theta/4) for the arc's included angle, so any sweep is expressible, not just quarter turns.

package main

import (
	"fmt"
	"math"
)

func main() {
	bulge := func(deg float64) float64 { return math.Tan(deg * math.Pi / 180 / 4) }
	fmt.Printf("90 deg: %.4f\n", bulge(90))
	fmt.Printf("180 deg: %.4f\n", bulge(180))
}
Output:
90 deg: 0.4142
180 deg: 1.0000

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL