Storing Custom Data in DXF Files

This tutorial describes how to store custom data in DXF files using standard DXF features.

Saving data in comments is not covered in this section, because comments are not a reliable way to store information in DXF files and ezdxf does not support adding comments to DXF files. Comments are also ignored by ezdxf and many other DXF libraries when loading DXF files, but there is a ezdxf.comments module to load comments from DXF files.

The DXF data format is a very versatile and flexible data format and supports various ways to store custom data. This starts by setting special header variables, storing XData, AppData and extension dictionaries in DXF entities and objects, storing XRecords in the OBJECTS section and ends by using proxy entities or even extending the DXF format by user defined entities and objects.

This is the common prolog for all Python code examples shown in this tutorial:

import ezdxf

doc = ezdxf.new()
msp = doc.modelspace()

Retrieving User Data

Retrieving the is a simple task by ezdxf, but often not possible in CAD applications without using the scripting features (AutoLISP) or even the SDK.

AutoLISP Resources

Warning

I have no experience with AutoLISP so far and I created this scripts for AutoLISP while writing this tutorial. There may be better ways to accomplish these tasks, and feedback on this is very welcome. Everything is tested with BricsCAD and should also work with the full version of AutoCAD.

Header Section

The HEADER section has tow ways to store custom data.

Predefined User Variables

There are ten predefined user variables, five 16-bit integer variables called $USERI1 up to $USERI5 and five floating point variables (reals) called $USERR1 up to $USERR5. This is very limited and the data maybe will be overwritten by the next application which opens and saves the DXF file. Advantage of this methods is, it works for all supported DXF versions starting at R12.

Settings the data:

doc.header["$USERI1"] = 4711
doc.header["$USERR1"] = 3.141592

Getting the data by ezdxf:

i1 = doc.header["$USERI1"]
r1 = doc.header["$USERR1"]

Getting the data in BricsCAD at the command line:

: USERI1
New current value for USERI1 (-32768 to 32767) <4711>:

Getting the data by AutoLISP:

: (getvar 'USERI1)
4711

Setting the value by AutoLISP:

: (setvar 'USERI1 1234)
1234

Custom Document Properties

This method defines custom document properties, but requires at least DXF R2004. The custom document properties are stored in a CustomVars instance in the custom_vars attribute of the HeaderSection object and supports only string values.

Settings the data:

doc.header.custom_vars.append("MyFirstVar", "First Value")

Getting the data by ezdxf:

my_first_var = doc.header.custom_vars.get("MyFirstVar", "Default Value")

The document property MyFirstVar is available in BricsCAD as FIELD variable:

../_images/custom_header_property.png

AutoLISP script for getting the custom document properties:

(defun C:CUSTOMDOCPROPS (/ Info Num Index Custom)
  (vl-load-com)
  (setq acadObject (vlax-get-acad-object))
  (setq acadDocument (vla-get-ActiveDocument acadObject))

  ;;Get the SummaryInfo
  (setq Info (vlax-get-Property acadDocument 'SummaryInfo))
  (setq Num (vla-NumCustomInfo Info))
  (setq Index 0)
  (repeat Num
    (vla-getCustomByIndex Info Index 'ID 'Value)
    (setq Custom (cons (cons ID Value) Custom))
    (setq Index (1+ Index))
  )  ;repeat

  (if Custom (reverse Custom))
)

Running the script in BricsCAD:

: (load "customdocprops.lsp")
C:CUSTOMDOCPROPS
: CUSTOMDOCPROPS
(("MyFirstVar" . "First Value"))

Meta Data

Starting with version v0.16.4 ezdxf stores some meta data in the DXF file and the AppID EZDXF will be created. Two entries will be added to the MetaData instance, the CREATED_BY_EZDXF for DXF documents created by ezdxf and the entry WRITTEN_BY_EZDXF if the DXF document will be saved by ezdxf. The marker string looks like this "0.17b0 @ 2021-09-18T05:14:37.921826+00:00" and contains the ezdxf version and an UTC timestamp in ISO format.

You can add your own data to the MetaData instance as a string with a maximum of 254 characters and choose a good name which may never be used by ezdxf in the future.

metadata = doc.ezdxf_metadata()
metadata["MY_UNIQUE_KEY"] = "my additional meta data"

print(metadata.get("CREATED_BY_EZDXF"))
print(metadata.get("MY_UNIQUE_KEY"))

The data is stored as XDATA in then BLOCK entity of the model space for DXF R12 and for DXF R2000 and later as a DXF Dictionary in the root dictionary by the key EZDXF_META. See following chapters for accessing such data by AutoLISP.

XDATA

Extended Data (XDATA) is a way to attach arbitrary data to DXF entities. Each application needs a unique AppID registered in the AppID table to add XDATA to an entity. The AppID ACAD is reserved and by using ezdxf the AppID EZDXF is also registered automatically. The total size of XDATA for a single DXF entity is limited to 16kB for AutoCAD. XDATA is supported by all DXF versions and is accessible by AutoLISP.

The valid group codes for extended data are limited to the following values, see also the internals of Extended Data:

Group Code

Description

1000

Strings up to 255 bytes long

1001

(fixed) Registered application name up to 31 bytes long

1002

(fixed) An extended data control string '{' or '}'

1004

Binary data

1005

Database Handle of entities in the drawing database

1010

3D point, in the order X, Y, Z that will not be modified at any transformation of the entity

1011

A WCS point that is moved, scaled, rotated and mirrored along with the entity

1012

A WCS displacement that is scaled, rotated and mirrored along with the entity, but not moved

1013

A WCS direction that is rotated and mirrored along with the entity but not moved and scaled.

1040

A real value

1041

Distance, a real value that is scaled along with the entity

1042

Scale Factor, a real value that is scaled along with the entity

1070

A 16-bit integer (signed or unsigned)

1071

A 32-bit signed (long) integer

Group codes are not unique in the XDATA section and can be repeated, therefore tag order matters.

# register your appid
APPID = "YOUR_UNIQUE_ID"
doc.appids.add(APPID)

# create a DXF entity
line = msp.add_line((0, 0), (1, 0))

# setting the data
line.set_xdata(APPID, [
    # basic types
    (1000, "custom text"),
    (1040, 3.141592),
    (1070, 4711),  # 16bit
    (1071, 1_048_576),  # 32bit
    # points and vectors
    (1010, (10, 20, 30)),
    (1011, (11, 21, 31)),
    (1012, (12, 22, 32)),
    (1013, (13, 23, 33)),
    # scaled distances and factors
    (1041, 10),
    (1042, 10),
])

# getting the data
if line.has_xdata(APPID):
    tags = line.get_xdata(APPID)
    print(f"{str(line)} has {len(tags)} tags of XDATA for AppID {APPID!r}")
    for tag in tags:
        print(tag)

AutoLISP script for getting XDATA for AppID YOUR_UNIQUE_ID:

(defun C:SHOWXDATA (/ entity_list xdata_list)
    (setq entity_list (entget (car (entsel)) '("YOUR_UNIQUE_ID")))
    (setq xdata_list (assoc -3 entity_list))
    (car (cdr xdata_list))
)

Script output:

: SHOWXDATA
Select entity: ("YOUR_UNIQUE_ID" (1000 . "custom text") (1040 . 3.141592) ...

XDATA Helper Classes

The XDataUserList and XDataUserDict are helper classes to manage XDATA content in a simple way.

Both classes store the Python types int, float and str and the ezdxf type Vec3. As the names suggests has the XDataUserList a list-like interface and the XDataUserDict a dict-like interface. This classes can not contain additional container types, but multiple lists and/or dicts can be stored in the same XDATA section for the same AppID.

These helper classes uses a fixed group code for each data type:

1001

strings (max. 255 chars)

1040

floats

1071

32-bit ints

1010

Vec3

Additional required imports for these examples:

from ezdxf.math import Vec3
from ezdxf.entities.xdata import XDataUserDict, XDataUserList

Example for XDataUserDict:

Each XDataUserDict has a unique name, the default name is “DefaultDict” and the default AppID is EZDXF. If you use your own AppID, don’t forget to create the requited AppID table entry like doc.appids.new("MyAppID"), otherwise AutoCAD will not open the DXF file.

doc = ezdxf.new()
msp = doc.modelspace()
line = msp.add_line((0, 0), (1, 0))

with XDataUserDict.entity(line) as user_dict:
    user_dict["CreatedBy"] = "mozman"
    user_dict["Float"] = 3.1415
    user_dict["Int"] = 4711
    user_dict["Point"] = Vec3(1, 2, 3)

If you modify the content of without using the context manager entity(), you have to call commit() by yourself, to transfer the modified data back into the XDATA section.

Getting the data back from an entity:

with XDataUserDict.entity(line) as user_dict:
    print(user_dict)
    # acts like any other dict()
    storage = dict(user_dict)

Example for XDataUserList:

This example stores the data in a XDataUserList named “AppendedPoints”, the default name is “DefaultList” and the default AppID is EZDXF.

with XDataUserList.entity(line, name="AppendedPoints") as user_list:
    user_list.append(Vec3(1, 0, 0))
    user_list.append(Vec3(0, 1, 0))
    user_list.append(Vec3(0, 0, 1))

Now the content of both classes are stored in the same XDATA section for AppID EZDXF. The XDataUserDict is stored by the name “DefaultDict” and the XDataUserList is stored by the name “AppendedPoints”.

Getting the data back from an entity:

with XDataUserList.entity(line, name="AppendedPoints") as user_list:
    print(user_list)
    storage = list(user_list)

print(f"Copy of XDataUserList: {storage}")

See also

Extension Dictionaries

Extension dictionaries are another way to attach custom data to any DXF entity. This method requires DXF R13/14 or later. I will use the short term XDICT for extension dictionaries in this tutorial.

The Extension Dictionary is a regular DXF Dictionary which can store (key, value) pairs where the key is a string and the value is a DXF object from the OBJECTS section. The usual objects to store custom data are DictionaryVar to store simple strings and XRecord to store complex data.

Unlike XDATA, custom data attached by extension dictionary will not be transformed along with the DXF entity!

This example shows how to manage the XDICT and to store simple strings as DictionaryVar objects in the XDICT, to store more complex data go to the next section XRecord.

  1. Get or create the XDICT for an entity:

# create a DXF entity
line = msp.add_line((0, 0), (1, 0))

if line.has_extension_dict:
    # get the extension dictionary
    xdict = line.get_extension_dict()
else:
    # create a new extension dictionary
    xdict = line.new_extension_dict()

2. Add strings as DictionaryVar objects to the XDICT. No AppIDs required, but existing keys will be overridden, so be careful by choosing your keys:

xdict.add_dictionary_var("DATA1", "Your custom data string 1")
xdict.add_dictionary_var("DATA2", "Your custom data string 2")

3. Retrieve the strings from the XDICT as DictionaryVar objects:

print(f"DATA1 is '{xdict['DATA1'].value}'")
print(f"DATA2 is '{xdict['DATA2'].value}'")

The AutoLISP access to DICTIONARIES is possible, but it gets complex and I’m only referring to the AfraLISP Dictionaries and XRecords tutorial.

See also

XRecord

The XRecord object can store arbitrary data like the XDATA section, but is not limited by size and can use all group codes in the range from 1 to 369 for DXF Tags. The XRecord can be referenced by any DXF Dictionary, other XRecord objects (tricky ownership!), the XDATA section (store handle by group code 1005) or any other DXF object by adding the XRecord object to the Extension Dictionary of the DXF entity.

It is recommend to follow the DXF reference to assign appropriate group codes to DXF Tags. My recommendation is shown in the table below, but all group codes from 1 to 369 are valid. I advice against using the group codes 100 and 102 (structure tags) to avoid confusing generic tag loaders. Unfortunately, Autodesk doesn’t like general rules and uses DXF format exceptions everywhere.

1

strings (max. 2049 chars)

2

structure tags as strings like "{" and "}"

10

points and vectors

40

floats

90

integers

330

handles

Group codes are not unique in XRecord and can be repeated, therefore tag order matters.

This example shows how to attach a XRecord object to a LINE entity by Extension Dictionary:

line = msp.add_line((0, 0), (1, 0))
line2 = msp.add_line((0, 2), (1, 2))

if line.has_extension_dict:
    xdict = line.get_extension_dict()
else:
    xdict = line.new_extension_dict()

xrecord = xdict.add_xrecord("DATA1")
xrecord.reset([
    (1, "text1"),  # string
    (40, 3.141592),  # float
    (90, 256),  # 32-bit int
    (10, (1, 2, 0)),  # points and vectors
    (330, line2.dxf.handle)  # handles
])

print(xrecord.tags)

Script output:

[DXFTag(1, 'text1'),
 DXFTag(40, 3.141592),
 DXFTag(90, 256),
 DXFVertex(10, (1.0, 2.0, 0.0)),
 DXFTag(330, '30')]

Unlike XDATA, custom data attached by extension dictionary will not be transformed along with the DXF entity! To react to entity modifications by a CAD applications it is possible to write event handlers by AutoLISP, see the AfraLISP Reactors Tutorial for more information. This is very advanced stuff!

XRecord Helper Classes

The UserRecord and BinaryRecord are helper classes to manage XRECORD content in a simple way. The UserRecord manages the data as plain Python types: dict, list, int, float, str and the ezdxf types Vec2 and Vec3. The top level type for the UserRecord.data attribute has to be a list. The BinaryRecord stores arbitrary binary data as BLOB. These helper classes uses fixed group codes to manage the data in XRECORD, you have no choice to change them.

Additional required imports for these examples:

from pprint import pprint
import ezdxf
from ezdxf.math import Vec3
from ezdxf.urecord import UserRecord, BinaryRecord
from ezdxf.entities import XRecord
import zlib

Example 1: Store entity specific data in the Extension Dictionary:

line = msp.add_line((0, 0), (1, 0))
xdict = line.new_extension_dict()
xrecord = xdict.add_xrecord("MyData")

with UserRecord(xrecord) as user_record:
    user_record.data = [  # top level has to be a list!
        "MyString",
        4711,
        3.1415,
        Vec3(1, 2, 3),
        {
            "MyIntList": [1, 2, 3],
            "MyFloatList": [4.5, 5.6, 7.8],
        },
    ]

Example 1: Get entity specific data back from the Extension Dictionary:

if line.has_extension_dict:
    xdict = line.get_extension_dict()
    xrecord = xdict.get("MyData")
    if isinstance(xrecord, XRecord):
        user_record = UserRecord(xrecord)
        pprint(user_record.data)

If you modify the content of UserRecord.data without using the context manager, you have to call commit() by yourself, to store the modified data back into the XRECORD.

Example 2: Store arbitrary data in DICTIONARY objects. The XRECORD is stored in the named DICTIONARY, called rootdict in ezdxf. This DICTIONARY is the root entity for the tree-like data structure stored in the OBJECTS section, see also the documentation of the ezdxf.sections.objects module.

# Get the existing DICTIONARY object or create a new DICTIONARY object:
my_dict = doc.objects.rootdict.get_required_dict("MyDict")

# Create a new XRECORD object, the DICTIONARY object is the owner of this
# new XRECORD:
xrecord = my_dict.add_xrecord("MyData")

# This example creates the user record without the context manager.
user_record = UserRecord(xrecord)

# Store user data:
user_record.data = [
    "Just another user record",
    4711,
    3.1415,
]
# Store user data in associated XRECORD:
user_record.commit()

Example 2: Get user data back from the DICTIONARY object

my_dict = doc.rootdict.get_required_dict("MyDict")
entity = my_dict["MyData"]
if isinstance(entity, XRecord):
    user_record = UserRecord(entity)
    pprint(user_record.data)

Example 3: Store arbitrary binary data

my_dict = doc.rootdict.get_required_dict("MyDict")
xrecord = my_dict.add_xrecord("MyBinaryData")
with BinaryRecord(xrecord) as binary_record:
    # The content is stored as hex strings (e.g. ABBAFEFE...) in one or more
    # group code 310 tags.
    # A preceding group code 160 tag stores the data size in bytes.
    data = b"Store any binary data, even line breaks\r\n" * 20
    # compress data if required
    binary_record.data = zlib.compress(data, level=9)

Example 3: Get binary data back from the DICTIONARY object

entity = my_dict["MyBinaryData"]
if isinstance(entity, XRecord):
    binary_record = BinaryRecord(entity)
    print("\ncompressed data:")
    pprint(binary_record.data)

    print("\nuncompressed data:")
    pprint(zlib.decompress(binary_record.data))

Hint

Don’t be fooled, the ability to save any binary data such as images, office documents, etc. in the DXF file doesn’t impress AutoCAD, it simply ignores this data, this data only has a meaning for your application!

See also

AppData

Application-Defined Data (AppData) was introduced in DXF R13/14 and is used by AutoCAD internally to store the handle to the Extension Dictionary and the Reactors in DXF entities. Ezdxf supports these kind of data storage for any AppID and the data is preserved by AutoCAD and BricsCAD, but I haven’t found a way to access this data by AutoLISP or even the SDK. So I don’t recommend this feature to store application defined data, because Extended Data (XDATA) and the Extension Dictionary are well documented and safe ways to attach custom data to entities.

# register your appid
APPID = "YOUR_UNIQUE_ID"
doc.appids.add(APPID)

# create a DXF entity
line = msp.add_line((0, 0), (1, 0))

# setting the data
line.set_app_data(APPID, [(300, "custom text"), (370, 4711), (460, 3.141592)])

# getting the data
if line.has_app_data(APPID):
    tags = line.get_app_data(APPID)
    print(f"{str(line)} has {len(tags)} tags of AppData for AppID {APPID!r}")
    for tag in tags:
        print(tag)

Printed output:

LINE(#30) has 3 tags of AppData for AppID 'YOUR_UNIQUE_ID'
(300, 'custom text')
(370, 4711)
(460, 3.141592)