zumen for Python
Read 2D drawings of Japanese construction and render them, with the Rust implementation of
zumen. The result is the same Scene model as the
@zumenjs/core viewer in the browser.
Status: early release (0.x), for Python 3.12 or newer. SXF (SFC and P21, also compressed as P2Z and SFZ), Jw_cad JWW, DXF and DWG (AutoCAD 1.0 to 2018) are supported.
Download a wheel for your platform from the GitHub release
and install it with pip install path/to/zumen-0.1.1-….whl. The release includes wheels for Linux x86_64 and
aarch64 (glibc 2.28 or newer), macOS arm64, and a source distribution for other platforms.
import zumen
scene = zumen.read("plan.jww") # the format is detected from the contents
print(scene.version, len(scene), scene.sheet)
png = scene.render_png(dpi=150) # for thumbnails or as an image for vision models
pdf = scene.render_pdf() # one page, the sheet at its real size, with searchable text
svg = scene.render_svg() # millimeters, with text as <text>
part = scene.render_png(dpi=300, region=(100, 50, 200, 120)) # paper millimeters
json_text = scene.to_json() # pass to sceneFromJson in @zumenjs/core
Texts and entities, in paper millimeters with the Y axis up:
for text in scene.texts(region=(100, 50, 200, 120)): # omit region for the whole sheet
print(text["id"], text["text"], text["x"], text["y"], text["height"], text["angle"])
entity = scene.entity("#120") # id, kind, type, source, layers, bbox, text, properties, values, placement
entity["kind"], entity["values"]["radius"] # "circle", 25.0: the same keys in every format
rows = scene.entities(hidden_layers=[]) # [] includes the layers that the drawing hides
table = scene.to_arrow() # a pyarrow.Table, for pandas, Polars or DuckDB (pip install "zumen[arrow]")
scene.unit is the length unit of the drawing's own coordinates, those of values ("millimeter" in SXF and JWW,
the unit of $INSUNITS in DXF and DWG, such as "inch" or "meter"). Entity ids come from the file (an SXF instance
number, a JWW record, a DXF or DWG handle, with the placements or block references the entity sits in) and stay the
same for the same bytes in every version, so they can be stored and looked up again with scene.entity(id)
(Entity ids).
texts joins the parts of a text that share a line and puts a line break between lines.
properties are the label and value pairs that the format gives, as in the viewer's property panel, and values
maps their stable keys (layer, start, radius, text …) to the unformatted values, such as numbers and (x, y).
kind (line, text, dimension …) and the keys are the same in every format, while type and the labels are
Japanese (the list). scene.metadata_values does the same for the metadata.
geometry=True adds the shapes drawn, in drawing order, so that an application or an AI model can compute with
the coordinates of lines, outlines and hatched areas:
entity = scene.entity("#120", geometry=True) # also scene.entities(region=..., geometry=True)
for shape in entity["geometry"]:
shape["shape"] # "polyline", "segments", "fill", "hatch", "point" or "text"
shape["points"] # polyline: [(x, y), ...] in paper millimeters, with shape["closed"]
shape["real"] # the same coordinates in real-world millimeters, or None outside a placement
Segments have segments (pairs of points), fills and hatches rings (the outline and its holes; hatch lines are
left out), and points and text parts one point (for text, the lower left, with text, height and angle).
Arcs, circles, ellipses and splines come as the polyline drawn (arcs, circles and ellipses within 0.02 mm of the
curve on the sheet); their center, radius and angles are in values. real goes through the placement of the
entity (an SXF sub-figure or part, a JWW layer group, a DXF viewport or model space), so it is in the coordinates
of the drawing itself: the real-world coordinates of JWW, those of the SXF sub-figure or part, and DXF model space
in millimeters, also for drawings in inches.
render returns the image with the conversion between pixels and paper millimeters and the box of
every entity drawn, so that an application can tell which entity a vision model refers to:
image = scene.render(size=(2000, 2000)) # or dpi=..., region=... as in render_png
image.png, image.width, image.height, image.scale # scale: pixels per paper millimeter
x, y = image.to_paper(640, 480) # and image.to_pixel(x, y)
for entity in image.entities_at(640, 480): # boxes that contain the pixel, smallest first
print(entity["id"], entity["type"], entity["box"], entity["text"])
A Rendering shows itself as an image in Jupyter.
Drawings of every format can be written as DXF (AutoCAD 2013, UTF-8). DXF and DWG keep their handles, layers, styles, blocks and layouts, and read again, the DXF draws the same scene. SXF and JWW are mapped into the AutoCAD drawing model: model space at real size, and the sheet as a layout whose viewports show each SXF sub-figure and each JWW layer group scale where the drawing had it, with frames and title blocks in paper space. The layout draws the same as the original:
converted = zumen.convert("plan.jww", "dxf") # or bytes; converted.data holds the DXF
converted.save("plan.dxf")
for notice in converted.notices: # what could not be written as it was, such as images, by kind and count
print(notice["code"], notice["message"], notice["count"])
They can also be written as SXF (SFC of SXF Ver.3.1). The sheet of the layout that the drawing opens in becomes the SXF sheet, each viewport a sub-figure at real size placed at its scale, and each block a drawing part. What SXF has no feature for, such as multiline texts, leaders and dimensions, is written as the lines, fills and texts that zumen draws for it, and read again, the SFC draws the same:
zumen.convert("plan.dwg", "sfc").save("plan.sfc")
zumen.convert("plan.dxf", "p21", version="2.0").save("plan.p21") # SXF Ver.2.0 as P21 (also "sfz" and "p2z")
DWG (AutoCAD 2013) and JWW (Jw_cad 8) are written from the same model: zumen.convert("plan.sfc", "dwg") and
zumen.convert("plan.dxf", "jww"). In JWW, the layout is the sheet and the model space in each viewport is a layer
group at the viewport's scale.
Editing drawings
zumen.edit changes a drawing in place: it applies a list of operations and keeps everything else as it was, in the
same format, version and text encoding, including what zumen cannot read (extended data, objects, unread entities).
DXF (R12 to 2018, ASCII and binary), DWG (R13 to 2018), JWW and SXF (SFC, P21, SFZ and P2Z) can be edited.
operations = [ # the operations may also be a JSON string
{"op": "add-layer", "name": "CHECK", "color": "#ff0000"},
{"op": "add", "kind": "line", "layer": "CHECK", "start": [0, 0], "end": [5000, 0]},
{"op": "add", "kind": "text", "layer": "CHECK", "position": [100, 200], "text": "確認済み", "height": 350},
{"op": "update", "id": "3F2", "set": {"end": [5200, 0], "lineType": "DASHED"}},
{"op": "delete", "id": "3F5"},
]
edited = zumen.edit("plan.dxf", operations) # or bytes
edited.created # ["CHECK", "4A1", "4A2"]: what add-layer and add created, in order
edited.save("plan.dxf")
- Entities are chosen by the
idof the scene and described by thekindand the property keys thatScenereports. Coordinates are the drawing's own, therawof properties (Scene.entity(id)["values"]), not paper millimeters. Every id in one edit names an entity of the drawing as it was read. Ids that are places in the file (#12, in DXF without handles) change when an edit adds or removes records before them, so read the edited drawing again. addtakescontainer:"model"(the default),"layout:<name>"or"block:<name>"in DXF and DWG,"group:<0-F or name>"(a layer group) in JWW, and"sheet"(the default) or"figure:<name>"in SXF.- Kinds that can be added:
line,polyline(vertices,bulges,closed),arc,circle,ellipse,ellipse-arc,point,text(MTEXT when it has line breaks) andblock-reference(block,position,scale,rotation). Every kind takeslayer,color(a color number,"#rrggbb","bylayer"or"byblock"),lineTypeandlineWidth(millimeters). - If any operation cannot be applied, nothing is written and
zumen.EditErroris raised withcode(such as"not-found","unknown-key"or"inside-block") andoperation, the number of the operation from 0. - Values that the format cannot hold exactly, such as a true color in an old DXF, are written as the nearest one and
listed in
edited.notices. - Checking that the file is the version that was read (a checksum) is left to the application.
zumen.create("dxf", operations, units="mm") starts a new drawing (AutoCAD 2013 DXF) and applies the operations.
"dwg" starts an AutoCAD 2013 DWG, "jww" an A3 JWW, and "sfc", "p21", "sfz" or "p2z" an A3 SXF sheet; JWW
and SXF lengths are in millimeters, so they take only units="mm".
Comparing revisions
zumen.compare matches entities by type, shape and text, not by id, so revisions that were saved again,
or saved in another format, compare too:
changes = zumen.compare(zumen.read("plan-rev1.jww"), zumen.read("plan-rev2.jww"))
changes.removed, changes.added # entities, as Scene.entities returns them
for pair in changes.changed: # the same entity changed, such as a dimension value
print(pair["before"]["text"], "->", pair["after"]["text"])
changes.regions # boxes around nearby changes, in the earlier revision's paper millimeters
changes.scene.render_png(dpi=150) # red before, blue after, gray unchanged
tolerance (0.01 paper millimeters by default) sets how close coordinates count as the same, style=True also
counts changes of color, line weight, line type and layer, and a revision that moved as a whole is moved back by
the texts both revisions share (changes.offset; align=False turns this off).
Title block
title_block guesses the drawing number, revision, title, project, scale and date, so that drawings can be filed
by number and revision:
block = scene.title_block()
block["number"], block["revision"], block["title"] # None when not found
for field in block["fields"]: # every candidate, the most likely first for each key
print(field["key"], field["value"], field["source"], field["label"], field["entity"])
It reads attribute tags (DWGNO, REV, TITLE), labels such as 図面番号 or DWG. NO. with the text to their right
or below, labels with their value in the same text (縮尺 1/100), the sheet name and the metadata (the title block
attributes of SXF). The values are guesses from the drawing's texts; check them before relying on them.
Rooms and areas
regions finds the regions that drawn lines close, such as rooms, with their areas:
for room in scene.regions(layers=[layer["id"] for layer in scene.layers if "壁" in layer["name"]]):
real = room["real"] # None outside a placement drawn at a scale
print([text["text"] for text in room["texts"]], real and real["area"] / 1e6, "m²")
Lines, polylines, arcs, circles, splines and solids are split where they cross or meet, ends closer than tolerance
(0.01 paper millimeters) are joined, and every face they close is a region: its outline (counterclockwise) and
holes, area (without the holes), outline_area and perimeter on the sheet, the same in real-world millimeters
in real, the boundary entities and the texts inside. Lines that end nowhere close nothing, and closed shapes
that touch no other line (columns, furniture) are holes. layers keeps the lines on some layers (walls), kinds
changes the kinds used, and min_area leaves out smaller regions. A room whose walls have an opening drawn as a gap
joins the next one (decision 0015).
MCP server
zumen-mcp lets AI applications that speak the Model Context Protocol open, render, search, measure and compare drawings
on this computer. It serves over stdio and only reads. Regions are given in paper millimeters (region) or as
fractions of the frame (frame_region), as in zumen.tools.
pip install "zumen[mcp]"
claude mcp add zumen -- zumen-mcp --root ~/drawings # Claude Code; other clients take the same command
| Tool | What it returns |
|---|---|
open_drawing |
Format, sheet, views, layers with entity counts, entity types, the placements drawn at a scale (with their box and the conversion from paper to real-world millimeters), notices, metadata and the drawing number, revision, title, project, scale and date from the title block |
render_drawing |
A PNG of the frame or a region (1,568 pixels on the long edge by default), the scale to convert pixels to paper millimeters, and the texts drawn with their pixel boxes |
find_texts |
Texts that contain a query, ignoring full-width and half-width forms, case and spaces |
find_entities |
Entities in a region, by kind, type or layer, with counts by type and kind; with geometry, their shapes as get_entity gives them |
get_entity |
One entity with its properties (key, label and value), its values by key, the scale of its placement and its geometry: the shapes drawn, in paper millimeters and, inside a placement, in real-world millimeters |
measure_distance |
The distance between two points on the sheet and in real-world millimeters |
find_regions |
The regions that the drawn lines close, such as rooms: areas in square meters, perimeters, outlines and holes, the entities that bound them and the texts inside, filtered by layer, text and area |
compare_drawings |
What changed between two revisions: an image of the changes, the regions that changed and the changed entities with their texts |
Geometry is the agent's to interpret: the server gives the shapes as drawn and their coordinates, and the regions
that lines close with the entities that bound them, and leaves deciding what is a wall or a room to the client. real is left out where it equals the coordinates on the sheet (DXF model
space), and one call returns at most max_points points (1,000 by default, at most 10,000; a point with real counts
twice), so that a page of entities stays well below what clients accept from a tool. A page of find_entities with
geometry ends before an entity whose shapes would pass the budget; next_offset goes on from there.
--root DIR (repeatable) limits the directories the server may read. --max-bytes (256 MiB by default, after
unpacking P2Z and SFZ), --max-entities (2,000,000) and --timeout (60 seconds) limit each drawing it reads. Texts, layer names and properties come from the drawing file; clients should treat them as
data, not as instructions. The tools are the functions of zumen.tools.
Tools for AI agents
Applications that give an agent tools of their own, with or without MCP, call the functions behind the MCP tools in
zumen.tools. Each takes a Scene and returns plain data that json.dumps writes as it is, with keys described by
the TypedDict classes of the module (a later version may add keys, but does not remove or rename them); on Python
3.12 and later, pydantic.TypeAdapter(tools.EntityDetail).json_schema() gives the JSON Schema of a result. They only
read and report what the drawing holds; interpreting it is left to the agent.
from zumen import tools
scene = zumen.read(data, max_bytes=200_000_000, max_entities=1_000_000, timeout=30)
info = tools.summary(scene) # format, unit, sheet, bounds (the frame), layers, kinds, placements, title block
png, image = tools.render(scene, frame_region=[0.5, 0, 1, 0.5]) # the top right quarter, 1,568 pixels long
page = tools.find_texts(scene, query="平面図", limit=50) # texts with their position, height and box
walls = tools.find_entities(scene, kind="polyline", layer="壁", geometry=True) # with their shapes
entity = tools.get_entity(scene, page["texts"][0]["id"]) # properties, values, placement and shapes
tools.measure(scene, [10, 20], [110, 20]) # on the sheet, and in real-world millimeters through the scale
rooms = tools.find_regions(scene, layer="壁", named=True) # rooms with their areas in square meters
| Function | Result | What it returns |
|---|---|---|
summary |
Summary |
Format, the length unit of values, sheet, bounds (the frame), views, layers with entity counts, counts by type and kind, the placements drawn at a scale, notices, metadata and the title block fields |
render |
bytes, RenderInfo |
A PNG of the frame or a region, the region shown, the scale between pixels and paper millimeters, and the texts drawn with their pixel boxes |
find_texts |
TextPage |
Texts that contain a query (ignoring full-width and half-width forms, case and spaces), with position, height, angle and box |
find_entities |
EntityPage |
Entities in a region by kind, Japanese type or layer, with counts, and their shapes when asked |
get_entity |
EntityDetail |
One entity: properties (key, label, value), values by key, placement with its scale and to_real, and its shapes |
measure |
MeasurementInfo |
A distance on the sheet and in real-world millimeters |
find_regions |
RegionPage |
Closed regions (rooms) with their real-world areas in square meters, outlines, boundary entities and texts inside |
compare_revisions |
bytes, ComparisonInfo |
An image of the changes between two revisions, the regions that changed and the changed entities |
- Coordinates. Paper millimeters with the Y axis up, rounded to 0.001: boxes
[min_x, min_y, max_x, max_y], points[x, y]. The frame is the drawing'sbounds, the box around the sheet and every entity, which a render without a region shows. Functions that takeregionalso takeframe_region=[left, top, right, bottom], fractions of the frame from 0 to 1 with the origin at the top left, the way regions of pages and images are usually given;tools.paper_regionandtools.frame_region_ofconvert between the two. Shapes inside a placement repeat their coordinates in real-world millimeters (real), and the placement'sto_realconverts any point. - Sizes. Every result is bounded by its arguments: lists stop at
limit(at most 1,000) and give thetotalandnext_offset, texts and other strings from the drawing atmax_text_lengthcharacters (1,000 by default; the item then hastruncated: true), shapes atmax_pointspoints for the call (1,000 by default, at most 10,000; shapes are left out whole and counted ingeometry_omitted), and images at 4,096 pixels on the long edge. - Reading.
tools.Drawingsreads files from allowed directories with limits on size, entities and time, and keeps the last eight scenes until their files change. AScenedoes not change once read, so applications can keep scenes in their own cache and call the functions from threads (asyncio.to_thread); reading, rendering and comparing release the GIL. Errors that the agent can act on raisetools.DrawingErrorwith an English message. - Ids. Entity ids come from the file and stay the same for the same bytes, so an application can cite an entity
by its id (next to the file's digest) and fetch it again later with
get_entity. - Texts, names and properties come from the drawing file: agents should treat them as data, not as instructions.
Drawings from others
Drawings received from others are not trusted: a file can be huge, unpack to gigabytes (P2Z and SFZ are ZIP
archives) or grow without end when blocks contain each other. read stops part way at the limits you give and
raises LimitError, a ReadError whose limit says which limit was reached:
try:
scene = zumen.read(path, max_bytes=200_000_000, max_entities=1_000_000, timeout=30)
except zumen.LimitError as error:
print(error.limit, error) # "bytes", "entities" or "time", and a message for the user
max_bytes counts the size after unpacking P2Z and SFZ (2 GiB at most without it), and a file larger than
max_bytes is not read into memory. max_entities counts the entities of the scene; work that ends in fewer
entities, such as dimension blocks inside each other, is bounded by timeout (seconds) and by the readers' own
limits. Without limits nothing changes.
Reading and rendering release the GIL. Japanese text is drawn with the bundled BIZ UDGothic (SIL Open Font License 1.1).
Metadata
Release files for zumen 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| zumen-0.1.1.tar.gz | 4.0 MB | With provenance | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zumen-0.1.1-cp312-abi3-manylinux_2_28_x86_64.whl | CPython 3.12 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| zumen-0.1.1-cp312-abi3-manylinux_2_28_aarch64.whl | CPython 3.12 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| zumen-0.1.1-cp312-abi3-macosx_11_0_arm64.whl | CPython 3.12 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 22.1 MB
Release files / zumen-0.1.1.tar.gz
| Download URL | zumen-0.1.1.tar.gz |
|---|---|
| Size | 4.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
90f1f79e27bc25985da211acbf075ae54e8f3d438d09ed92c75e5c4df77cf933
|
|
BLAKE2b-256 checksum How to use checksums |
d7646bab4a78257076521767a336a0e338c3c67fdcff5f1f0dd59b0320e37efb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / zumen-0.1.1-cp312-abi3-manylinux_2_28_x86_64.whl
| Download URL | zumen-0.1.1-cp312-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 6.2 MB |
| Tags | CPython 3.12 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
e31d4f7b789af11929fca3cb5957c8256c58b3669779645b094517533afff17f
|
|
BLAKE2b-256 checksum How to use checksums |
7b124032f27d1be9aad43532a572c237e13477e8d0c9a881f9cc34e83d499387
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / zumen-0.1.1-cp312-abi3-manylinux_2_28_aarch64.whl
| Download URL | zumen-0.1.1-cp312-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 6.0 MB |
| Tags | CPython 3.12 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
5da027a93e5c178401d38926c06ac739db8869b821b9cfc0ca473a9ef6dc5941
|
|
BLAKE2b-256 checksum How to use checksums |
4fa8b558adc27596769c7826a4aa00f0e19137cebbeb3a7496d0f2937ec87728
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / zumen-0.1.1-cp312-abi3-macosx_11_0_arm64.whl
| Download URL | zumen-0.1.1-cp312-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 5.9 MB |
| Tags | CPython 3.12 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
5d21f453e1c53515318d3fe76aa07bbdbfc97bb96167628025520b434c998fb3
|
|
BLAKE2b-256 checksum How to use checksums |
d8a5661238a80aecae841348982acfa9f6fac4e94f1f42b06545369ac5b5775d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for zumen 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 4 files in this release have been uploaded with provenance metadata.
Attestation for zumen-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.