uncad-cli 0.8.1

CLI for uncad (CAD file summary, JSON export of the parsed model, SVG/PNG rendering, read-only verbs as JSON and as MCP tools).
uncad-cli-0.8.1 is not a library.

uncad

CI License: GPL v3

An open-source Rust library that parses CAD files (DWG/DXF) into a model and exports that model as JSON, SVG or PNG. Read-only: it does not write DWG or DXF, and does not convert between them.

Quick start

git submodule update --init   # test fixtures only -- not needed to build (see "Platform")
cargo build --workspace
cargo test --workspace
let db = uncad::parse("drawing.dwg")?;   // same model for DWG and DXF (entities + tables)
println!("{} entities", db.entities.len());

// The header variables that give the numbers a meaning come beside the model:
let (db, header) = uncad::parse_with_header("drawing.dxf")?;
println!("units: {:?}", header.units());   // $INSUNITS; None when the file does not say

// Bytes already in memory (from the network, an archive, ...):
let bytes = std::fs::read("drawing.dwg")?;
let db = uncad::parse_bytes(&bytes, uncad::Format::Dwg)?;

let json = db.to_json(uncad::ToJsonOptions { pretty: true })?;   // the model, serialized as-is
std::fs::write("drawing.json", json)?;

// Rendering is the iron-render-cad crate's (uncad-cli uses it):
let result = iron_render_cad::to_svg(&db, iron_render_cad::ToSvgOptions::default());
std::fs::write("drawing.svg", result.svg)?;

CLI

The binary is uncad. From a clone, cargo install --path crates/uncad-cli puts it on your PATH; the lines below run it through cargo run instead.

cargo run -p uncad-cli -- drawing.dwg                            # summary: entity count per type
cargo run -p uncad-cli -- drawing.dwg -o drawing.json --pretty   # export the parsed model
cargo run -p uncad-cli -- drawing.dwg -o drawing.svg             # render to SVG (model space)
cargo run -p uncad-cli -- drawing.dwg -o drawing.png             # render to PNG (via SVG)
cargo run -p uncad-cli -- drawing.dwg -o drawing.png --scale 2   # rasterize at twice the size
cargo run -p uncad-cli -- drawing.dwg -o drawing.png --fit 4000  # the longer side 4000 px
cargo run -p uncad-cli -- drawing.dwg -o drawing.png --fit 4000 --stroke 2 --paper dark  # 2 px lines, black page
cargo run -p uncad-cli -- drawing.dwg -o detail.svg --window 0,0,500,300  # just this rectangle (drawing units)
cargo run -p uncad-cli -- drawing.dwg -o drawing.svg --no-trim   # draw far-away outliers too
cargo run -p uncad-cli -- drawing.dwg -o sheet.svg --space paper # sheet borders / title blocks
cargo run -p uncad-cli -- drawing.dwg -o all.svg --space all     # every space in one document
cargo run -p uncad-cli -- export drawing.dwg -o pkg/             # images + JSON an LLM or a vision model reads

A render is drawn on a white page by default, where pure white (ACI 7) is drawn black. --paper dark draws it on a black page instead: pure white stays white, pure black is drawn white, and every other color is the file's -- the light ones a white page washes out included. Lines are about 1/6000 of the picture's diagonal unless --stroke <px> sets their width in pixels (PNG only).

Questions about a drawing, each answered as one line of JSON -- the result structure of iron-scout-cad or iron-diff-cad, serialized as it is:

uncad summarize drawing.dwg                                   # what the drawing contains
uncad summarize drawing.dwg --type circle --layer pipes       # ... and the circles on a layer, each with its box
uncad summarize drawing.dwg --id 812 --detail                 # ... and one entity's model record (the fields `set` takes)
uncad hit-test drawing.dwg --x 120 --y 45 --tolerance 0.5     # the entities at a point
uncad diff rev-a.dwg rev-b.dwg                                # exact numeric difference (by reference ID when the files show one drawing)
uncad diff a.dxf b.dxf --matching geometry --length-tolerance 0.01
uncad diff a.dxf b.dxf --matching geometry --min-similarity 1.5  # ... pairing only equal shapes
uncad diff rev-a.dwg rev-b.dwg --omit within,unstated         # only what changed beyond tolerance

A change set lists every field that differs, including those that moved within tolerance and those one drawing does not state (a drawing saved again in a newer format states values the older format had no place for). --omit leaves either out -- for an agent reading the answer into a limited context -- and the answer counts what it left out in omitted, so nothing left out reads as unchanged.

One verb edits: set changes one field of one entity and writes the result as a new model JSON file -- never over an existing file, never into its input -- and answers with the difference it made, as diff reports it. The field and the value are named as the model JSON names them (the value is JSON: a number, a string in quotes, an object). Every verb reads model JSON as a drawing, so edits chain, and the last state can be compared with the first -- or rendered, since every command but export (which reads the drawing's header) takes model JSON:

uncad set part.dwg --id 42 --path radius --value 6.5 -o step1.json
uncad set step1.json --id 42 --path center.x --value 10 -o step2.json
uncad diff part.dwg step2.json                                # both edits, and nothing else
uncad step2.json -o step2.svg                                 # what the edited state looks like

redline draws that difference on top of the first drawing: the first drawing exactly as it renders alone, and over it, in red (or --proposal-color), what each changed entity became, what was removed (dashed), and a revision cloud around every change. A change whose counterpart is uncertain gets a dashed cloud and no geometry. When the drawing itself uses colors close to the one the changes are drawn in, the answer lists them and a warning suggests another. It writes an SVG or PNG file -- never over an existing one -- and answers with what it marked and what it could not show, and why. --paper and --stroke mean what they mean for a render; a stroke is in pixels of the picture, so it takes a PNG -- an SVG, which has none, refuses it:

uncad redline part.dwg step2.json -o proposal.svg
uncad redline part.dwg step2.json -o proposal.png --fit 2000 --omit within  # 2000 px, beyond tolerance only
uncad redline plan.dwg step2.json -o detail.png --fit 1200 --frame changes  # just the changes, and around them
uncad redline plan.dwg step2.json -o proposal.svg --proposal-color '#0057b8'  # a drawing already in red
uncad redline plan.dwg step2.json -o review.png --fit 1200 --stroke 3 --paper dark  # 3 px lines, black page

uncad mcp serves summarize, hit-test, diff, set and redline as Model Context Protocol tools over stdio, for an agent to call. A tool's answer is byte for byte what the command prints; a warning the command writes to stderr is a further content block. Each call reads its files again -- the server keeps no state -- and calls run one at a time.

{ "mcpServers": { "uncad": { "command": "uncad", "args": ["mcp"] } } }

Scope

  1. DWG — read through LibreDWG (GPLv3+), bound directly via Rust FFI (bindgen). All versions.
  2. DXF — ASCII and binary, R2007 and later included, read by the pure-Rust undxf crate (MIT) rather than by LibreDWG; chosen by file extension (parse) or by the caller (parse_bytes). A drawing's DXF and DWG read to the same model except where the two formats state different things (docs/CAVEATS.md, "DXF").
  3. Output — the parsed model as JSON (CadDatabase::to_json, from uncad-model). SVG and PNG come from the iron-render-cad crate (MIT), which uncad-cli uses. Writing DWG/DXF is not offered; the write API that existed in 0.1.0 was removed (see CHANGELOG.md).

The model itself -- CadDatabase, Entity, Tables and their JSON form -- is the uncad-model crate (MIT), re-exported here as uncad::model / uncad::tables / uncad::json. This crate is one backend that fills it; anything that only needs to read a drawing depends on the model crate alone and inherits nothing from this crate's license.

Platform

Pure Rust plus native FFI. WebAssembly and the browser are not targets — the intended consumers are libraries and binaries (CLI, server, desktop app).

bindgen needs libclang, so LLVM/Clang has to be installed (Windows: winget install LLVM.LLVM, Ubuntu: apt install libclang-dev). The LibreDWG C sources are vendored into crates/libredwg-sys/vendor/libredwg/, so building does not need the lib/libredwg submodule. Running cargo test --workspace does: the real-file tests under crates/uncad/tests/ and uncad-cli's tests/documented_invocations.rs read fixtures from that submodule's test/test-data/. Clone with git clone --recurse-submodules, or run git submodule update --init in an existing clone. Builds and tests pass on Linux (x86_64-unknown-linux-gnu) as well as Windows.

License

GPLv3-or-later. LibreDWG (GPLv3+) is the only third-party component linked in, and its license carries over. The vendored copy carries local patches; crates/libredwg-sys/NOTICE.md lists each one and is their modification notice, inside the crate so that it reaches the published tarball. Copyright and license details for third-party components are in docs/THIRD_PARTY_NOTICES.md.

Repository layout

lib/libredwg/            LibreDWG upstream, as a git submodule. Not used by the build --
                         it is the source vendor/ is regenerated from, and where the
                         real-file test fixtures (test/test-data/) come from
crates/
  libredwg-sys/          raw FFI (cc + bindgen). vendor/libredwg/ holds the subset of C
                         sources actually compiled (for publishing to crates.io), with
                         local patches marked "uncad local patch" and listed in
                         NOTICE.md; shim/ holds the C accessors for opaque types, and
                         vendor-config/config.h stands in for autotools
  uncad/                 the safe API: parse() / parse_bytes() -> uncad_model::CadDatabase,
                         and the drawing's Header beside it from parse_with_header()
  uncad-cli/             the CLI binary (uncad)
crates/*/tests/          integration tests against the public API. crates/*/examples/ are
                         manual-check tools, and #[cfg(test)] blocks inside src/*.rs are
                         unit tests -- docs/ARCHITECTURE.md's "Test layout" says which
                         belongs where
scripts/                 sync-libredwg-vendor.sh -- regenerates vendor/ after a submodule
                         update
samples/                 gitignored except its README -- drop any DWG/DXF in here for
                         manual testing, no license clearance needed. No automated tests
                         read from it
docs/                    architecture, known limitations, third-party notices

Further reading:

  • docs/ARCHITECTURE.md — crate layout, build system, test layout, the FFI/bindgen boundary, thread safety, the entity model
  • docs/CAVEATS.md — entity type coverage, known limitations and bugs, cross-platform notes