cadboss.dev

Read every word of a CAD drawing, in pure Rust.

cadboss reads DXF drawings, pulls out every piece of text, the metadata and the structure, and hands it to a search index as Markdown or JSON. One core, a CLI, and pythonic bindings.

$pip install cadboss$cargo install cadboss-cli

Prebuilt abi3 wheels for CPython 3.12 and later, no toolchain required.

cadbossCAD reader in Rust
Reads
DXF, ASCII and binary
Versions
R12 (AC1009) to R2018 (AC1032)
Writes
Markdown and JSON
Ships as
Rust crates, a CLI, a Python package
License
MIT OR Apache-2.0

What comes out

Text grouped the way a reader asks for it.

A drawing carries more words than it looks: the title block, the tags on every pump and valve, the notes, the dimension overrides, the layer and block names, the paths of the xrefs and images it pulls in, the hyperlinks on entities. cadboss sorts them into five groups and keeps the metadata beside them.

The samples are real output for plant.dxf, a copy of the repository’s demo drawing: a pump-station layout with two tagged pumps, a title block on its own sheet, a stacked tolerance, a multi-line note, an xref, an image and a hyperlink.

title-block

TEXT and MTEXT inside a block whose name contains TITLE, TBLK, SCHRIFTFELD or CARTOUCHE, or placed in the bottom-right quarter of a paper layout.

cadboss text plant.dxf --group title-block, all lines

DRAWN
CHECKED
SCALE
REV

annotations

Every other TEXT, MTEXT and dimension text, with position and height after the insert transform.

cadboss text plant.dxf --group annotations, 3 of 7 lines

6000
300+0.1/-0.2
PUMP ROOM

attributes

TAG=value for every ATTRIB of every INSERT, bytes kept when the value does not decode.

cadboss text plant.dxf --group attributes --layout Model, all lines

TAG=P-101
DUTY=DUTY
TAG=P-102
DUTY=STANDBY

references

Xref paths, image paths and PE_URL hyperlinks.

cadboss text plant.dxf --group references, all lines

photos/pump-room.png
../xrefs/site-plan.dxf
https://example.com/specs/pipe-class-1A.pdf

names

Layer and block names.

cadboss text plant.dxf --group names, 4 of 10 lines

EQUIPMENT
PIPING
TITLEBLOCK
SITE-PLAN

metadata

Version, codepage, units, author, created and updated dates, extents and per-type entity counts, next to the layer and layout tables, the block names and the report.

cadboss info plant.dxf, 3 of 7 lines

Format: DXF AC1032
Units: Millimeters
Extents: (0, 0) to (12500, 8000)

Items are ordered by layout tab, then layer, then top to bottom and left to right. The properties group exists in the model and stays empty for DXF.

The command line

Read a drawing from the shell.

cargo install cadboss-cli installs the cadboss binary. It prints the metadata, the text by group or layout, Markdown or JSON; cadboss q runs a jq program over the JSON export without writing it to disk, and cadboss obj prints the raw group codes of one handle. Skipped content is reported on stderr.

info, text and q on plant.dxf
A terminal session on plant.dxf: cadboss info prints format DXF AC1032, millimeters, 7 layers, 3 blocks, 3 layouts, 15 entities and the extents; cadboss text with --group attributes --layout Model prints the TAG and DUTY of pumps P-101 and P-102; --group references prints an image path, an xref path and a hyperlink; cadboss q prints the codepage ANSI_1252.
q and obj
$ cadboss q plant.dxf '.text.annotations[] | select(.source == "Dimension") | .text' -r
6000
300+0.1/-0.2
12000
$ cadboss q plant.dxf '.layouts[] | "\(.name) \(.kind) \(.entity_count)"' -r
Model Model 14
Layout1 Paper 0
Sheet 1 Paper 1
$ cadboss obj plant.dxf 140
0	LINE
5	140
330	109
100	AcDbEntity
8	PIPING
100	AcDbLine
10	3000.0
20	4300.0
30	0.0
11	3000.0
21	7500.0
31	0.0
1001	PE_URL
1002	{
1000	https://example.com/specs/pipe-class-1A.pdf
1002	}

Handle 140 is the piping line that carries the hyperlink in the references group. Every command with its output.

Python

The same reader, in Python.

pip install cadboss installs prebuilt abi3 wheels for CPython 3.12 and later. One Document class carries the metadata, the layouts, the layers, the text groups and the report.

Document.open reads the file into one buffer after checking its size against max_bytes; Document.load copies the bytes it is given. Pass budget={"max_entities": 100_000} to either.

The Python API, with output

readme.py
import cadboss

doc = cadboss.Document.open("plant.dxf")                 # or Document.load(raw_bytes)
print(doc.format, doc.version, doc.units)
for layout in doc.layouts:                              # name, kind ("Model" or "Paper"), entity_count
    print(layout.name, layout.kind, layout.entity_count)
for item in doc.text("attributes"):                     # title-block, annotations, names, attributes, references, all
    print(item.layout, item.layer, item.text, item.height)
for skipped in doc.report():                            # what the reader could not decode, counted
    print(skipped.kind, skipped.reason, skipped.count)
open("plant.md", "w").write(doc.markdown(title="Pump plan"))
output, first line
$ python3 readme.py | head -n 1
DXF AC1032 Millimeters

Damaged and huge files

Malformed input is the normal input.

Searching a plant’s drawings means getting the text out of files that are hundreds of megabytes, often damaged, and written by a dozen different exporters. cadboss decodes every record in place and returns what it could read plus a report of what it could not.

Budgets
Bytes, tokens, entities, layouts, nesting depth and string length each have a cap. The reader stops at the cap and says so.
The skipped report
Unknown entities, broken records, stray vertices, truncated sections and exceeded budgets become Skipped { kind, reason, count } notes, never errors.
No panics
Panics are denied by the workspace lints, and a cargo-fuzz target seeded from the fixtures runs on each push.
One pass per layout
The ENTITIES section is bucketed once; counts, extents and text come out of one walk per layout, so a 100-layout file costs the same as a 1-layout file.
Memory bounded by the file
The CLI memory-maps the input and lexes it into a flat token tape. No DOM, and no copy of the drawing that is bigger than the drawing.
plant.dxf, cut at byte 6000, and a budget
$ head -c 6000 plant.dxf > truncated.dxf
$ cadboss info truncated.dxf
Format: DXF AC1032
Units: Millimeters
Layers: 7
Blocks: 3
Layouts: 2 (Model, Layout1)
Entities: 4
Extents: (0, 0) to (12500, 8000)
Report: 1 section skipped (truncated), 1 entity skipped (unknown)
warning: 1 section skipped (truncated), 1 entity skipped (unknown)
$ cadboss info plant.dxf --budget max_entities=5
Format: DXF AC1032
Units: Millimeters
Layers: 7
Blocks: 3
Layouts: 3 (Model, Layout1, Sheet 1)
Entities: 6
Extents: (0, 0) to (12500, 8000)
Report: 4 entities skipped (entity_limit)
warning: 4 entities skipped (entity_limit)

The cut file still yields its 7 layers, 3 blocks and 4 entities; the truncated section and an unknown entity land in the report.

Memory on a 300 MB drawing with 1.7 million mixed entities

PathPeak RSSAnonymousTimeGates
cadboss text (memory-mapped)1.76x input0.76x input1.3 sanonymous ≤ 1.0x, RSS ≤ 2.0x
Document.open(path).markdown() (one owned buffer)1.87x input1.87x input1.4 sanonymous ≤ 2.0x, RSS ≤ 2.5x

Measured, not estimated: benchmarks/memory.py generates the drawing and gates every run of make bench. Apple M3 Pro, release build. The shapes that hurt most are flat: 40 000 text entities on 40 000 distinct layers render to Markdown in 0.04 s, 200 000 lines across 100 paper layouts export to JSON in 0.12 s, and json --entities on a 26 MB drawing peaks at 49 MB because the entity arrays stream instead of building a tree. The benchmarks.

Scope

DXF today. The rest is planned.

Read today

In the CLI, the crates and the Python package.

  • DXF, ASCII and binary
  • R12 (AC1009) through R2018 (AC1032)
  • Codepages ANSI_1252 and ISO-8859-1
  • MTEXT formatting stripped, dimension overrides substituted

Planned

In this order. Not shipped.

  1. 1.DWG
  2. 2.A PNG per 2D layout
  3. 3.STEP and IFC
  4. 4.The CJK codepages

Not yet read

Parts of a DXF file that are not extracted yet.

  • MULTILEADER, ACAD_TABLE and ATTDEF text
  • Codepages 1250, 1251 and 1253 to 1258

Every command accepts --threads; it is reserved and currently unused.

FAQ

Questions developers ask.

Does cadboss read DWG files?
Not yet. cadboss reads DXF today, ASCII and binary, R12 (AC1009) through R2018 (AC1032). DWG is the first item on the planned list, followed by a PNG per 2D layout, STEP and IFC, and the CJK codepages.
How does it handle a 300 MB drawing?
The CLI memory-maps the file and lexes it into a flat token tape instead of building a tree. On a generated 300 MB drawing with 1.7 million mixed entities, cadboss text peaks at 1.76x the input size in RSS, 0.76x of it anonymous memory, in 1.3 s; Document.open(path).markdown() in Python peaks at 1.87x in 1.4 s (Apple M3 Pro, release build). Every run of make bench checks those ratios against fixed gates.
What happens to text it cannot decode?
It is kept as bytes instead of a placeholder: {"_bytes": ...} with the bytes in base64 in JSON, and a bytes object in Python. The plain-text output of cadboss text prints a marker such as [non-decodable text, 9 bytes] in its place.
Is it safe to run on malformed input?
Malformed input is the normal input. Unknown entities, broken records, stray vertices, truncated sections and exceeded budgets become Skipped { kind, reason, count } notes in the report, never errors. Panics are denied by the workspace lints, a cargo-fuzz target runs on each push, and budgets cap bytes, tokens, entities, layouts, nesting depth and string length.
How do I search a plant's drawings with it?
Run cadboss json or cadboss md on each file and send the output to your search index. Text comes out grouped as title block, annotations, attributes, references and names, ordered by layout tab, then layer, then top to bottom and left to right, and items carry their layout, layer, position and height where the drawing has them. cadboss q runs a jq program over the same JSON when you need one field.
What license is cadboss under?
MIT or Apache-2.0, at your option. The license texts are LICENSE-MIT and LICENSE-APACHE in the repository.