I would like to export a .blend file in a JSON format (so that I can compare two .blend files by comparing their JSON representations), to do that I've written this script which will convert all the data structures to JSON based on the RNA properties:

"""
Export all the data from the current Blender file to either a JSON file or a pickle of
a pure python data structure.

Must be run inside of Blender! E.g. blender --background --python blend_data.py -- --dest /path/to/file.json
"""

import bpy


# Data-blocks that can be accessed via bpy.data.<type>[name]
data_types = (
    bpy.types.MetaBall,
    bpy.types.WindowManager,
    bpy.types.Brush,
    bpy.types.Screen,
    bpy.types.World,
    bpy.types.Particle,
    bpy.types.ShapeKey,
    bpy.types.Text,
    bpy.types.FreestyleLineStyle,
    bpy.types.GreasePencil,
    bpy.types.Texture,
    bpy.types.Camera,
    bpy.types.Material,
    bpy.types.Image,
    bpy.types.Group,
    bpy.types.Armature,
    bpy.types.Lamp,
    bpy.types.Lattice,
    bpy.types.NodeGroup,
    bpy.types.Sound,
    bpy.types.Mask,
    bpy.types.Mesh,
    bpy.types.MovieClip,
    bpy.types.Speaker,
    bpy.types.Palette,
    bpy.types.Action,
    bpy.types.Curve,
    bpy.types.Object,
    bpy.types.VectorFont,
    bpy.types.Scene,
    bpy.types.Library,
)

data_types_set = set(data_types)

data_groups = [
    'actions',
    'armatures',
    'brushes',
    'cameras',
    'curves',
    'fonts',
    'grease_pencil',
    'groups',
    'images',
    'lamps',
    'lattices',
    'libraries',
    'linestyles',
    'masks',
    'materials',
    'meshes',
    'metaballs',
    'movieclips',
    'node_groups',
    'objects',
    'palettes',
    'particles',
    'scenes',
    'screens',
    'shape_keys',
    'sounds',
    'speakers',
    'texts',
    'textures',
    'window_managers',
    'worlds'
]


class BlendEncoder(object):
    """Encode a .blend file in a json format"""
    def __init__(self, data_groups=data_groups):
        self._data_groups = data_groups
        self._prop_hooks = {}

    def register_prop_encoder(self, datablock, prop, encode_fn):
        self._prop_hooks.setdefault(datablock, {})[prop] = encode_fn

    def encode(self, data):
        """Encode the given blend data into json compatible python"""
        ret = {}
        for data_group in self._data_groups:
            ret[data_group] = {}
            for obj in getattr(data, data_group):
                # Skip datablocks that where loaded from a library.
                if not isinstance(obj, bpy.types.ID) or obj.library is None:
                    ret[data_group][obj.name] = self._encode_struct(obj, obj.rna_type)

        return ret

    def _encode_pointer(self, ptr):
        # TODO: find a better way of representing pointer properties other then the repr string.
        return repr(ptr)

    def _encode_prop(self, data, prop):
        """Serialize an rna property"""

        val = getattr(data, prop.identifier)

        if prop.type == "POINTER":
            if val is not None:
                # If it's a pointer to a data-block type then just return the repr string
                if isinstance(val, data_types):
                    return self._encode_pointer(val)
                else:
                    return self._encode_struct(val, prop.fixed_type)

        elif prop.type == "COLLECTION":
            ret = []
            for item in val:
                if item is not None:
                    # XXX: Faster then isinstance, if a RecursionError is raised revert back to isinstance
                    if type(item) in data_types_set:
                        ret.append(self._encode_pointer(item))
                    else:
                        ret.append(self._encode_struct(item, item.rna_type))
            return ret

        else:  # BOOLEAN, INT, FLOAT, STRING and ENUM
            if prop.type in {"BOOLEAN", "INT", "FLOAT"} and prop.is_array:
                # Check if it's a vector
                if len(val) > 0 and (hasattr(val[0], '__iter__') or hasattr(val[0], '__getitem__')):
                    val = [list(item) for item in val]
                else:
                    val = list(val)
            return val

    def _encode_struct(self, data, struct):
        ret = {}

        # Handle RNA properties
        for prop in struct.properties:
            if not prop.is_skip_save and prop.identifier not in {"rna_type"}:
                types = set(type(data).mro())
                typ = types.intersection(self._prop_hooks)

                if typ and prop.identifier in self._prop_hooks[next(iter(typ))]:
                    val = self._prop_hooks[next(iter(typ))][prop.identifier](self, data, prop)
                else:
                    val = self._encode_prop(data, prop)
                ret[prop.identifier] = val

        # Handle ID properties
        if isinstance(data, bpy.types.ID):
            id_properties = ret['id_properties'] = {}
            for prop, value in data.items():
                # Because custom RNA properties also show up as ID properties skip the ID props
                # that have the same name and value as an RNA prop.
                if not (prop in struct.properties and getattr(data, prop) == value):
                    # The value will either be a basic python type (int, string, etc.) or an IDPropertyGroup
                    # IDPropertyGroup's have the to_dict function which returns purely python representation.
                    if hasattr(value, 'to_dict'):
                        value = value.to_dict()
                    elif hasattr(value, 'to_list'):
                        value = value.to_list()
                    elif isinstance(value, list):
                        for i, item in enumerate(value):
                            if hasattr(item, 'to_dict'):
                                value[i] = item.to_dict()
                    id_properties[prop] = value
        return ret


# Use for computing the number of elements in a collection (e.g. the total number of vertices in a mesh)
def encode_collection_len(encoder, data, prop):
        return len(getattr(data, prop.identifier))


def encode_collection_pointer(encoder, data, prop):
    val = getattr(data, prop.identifier)
    return [encoder._encode_pointer(item) for item in val]


def encode_pointer(encoder, data, prop):
    return encoder._encode_pointer(getattr(data, prop.identifier))


def serialize():
    encoder = BlendEncoder()

    # Setup mesh prop encoders
    encoder.register_prop_encoder(bpy.types.Mesh, 'vertices', encode_collection_len)
    encoder.register_prop_encoder(bpy.types.Mesh, 'edges', encode_collection_len)
    encoder.register_prop_encoder(bpy.types.Mesh, 'polygons', encode_collection_len)
    encoder.register_prop_encoder(bpy.types.Mesh, 'loops', encode_collection_len)

    # Bones
    encoder.register_prop_encoder(bpy.types.Bone, 'parent', encode_pointer)
    encoder.register_prop_encoder(bpy.types.Bone, 'children', encode_collection_pointer)

    encoder.register_prop_encoder(bpy.types.PoseBone, 'parent', encode_pointer)
    encoder.register_prop_encoder(bpy.types.PoseBone, 'child', encode_pointer)

    # Nodes
    encoder.register_prop_encoder(bpy.types.NodeSocket, 'node', encode_pointer)

    # FCurves
    encoder.register_prop_encoder(bpy.types.FCurve, 'group', encode_pointer)

    return encoder.encode(bpy.data)


def main():
    import argparse
    import sys

    # get the args passed to blender after "--", all of which are ignored by
    # blender so scripts may receive their own arguments
    argv = sys.argv

    if "--" not in argv:
        argv = []  # as if no args are passed
    else:
        argv = argv[argv.index("--") + 1:]  # get all args after "--"

    # When --help or no args are given, print this help
    usage_text = (
        "Export the entire blend file in json format."
    )
    parser = argparse.ArgumentParser(description=usage_text, prog="blender --background -P blend-data.py")
    parser.add_argument("-d", "--dest", dest="dest_path", metavar='FILE', required=True,
                        help="Save the generated file to the specified path")
    args = parser.parse_args(argv)

    data = serialize()

    with open(args.dest_path, 'w') as f:
        def default(o):
            if isinstance(o, set):
                return sorted(list(o))
            else:
                raise TypeError

        import json
        json.dump(data, f, indent=None, default=default)


if __name__ == '__main__':
    main()

This script will export all the RNA properties (except for the ones explicitly skipped), but some of those properties are computed at runtime for the python API and not necessary to export (e.g. the pixels in an external image.)

My question is, how can I know if a property was created at runtime and therefore should be skipped?

Edit: If it's not possible to do entirely from python (it's looking like that might be the case) then I'm open to answers that involve changes to the C/C++ codebase as well.

I'll mention it in case you haven't seen it, there is an example about dumping RNA properties on blender (old) wiki , sadly it's not for json format but xml..

I may be misunderstanding this but matrix properties, location,rotation and scale are not runtime properties,they are defined in structs in blender's code and exposed to python through PyRNA.

you may be looking for properties and skip those created by users/addons and custom scripts, those created with bpy.types."sometype"Property() instead..

  • I guess I was mistaken about matrix properties, however, there are still properties that are created by Blender at runtime, I edited my question to include a valid example (the list of pixels in an external image). Also, it looks like the doc you gave is using an old version of the API that is no longer compatible. – Isaac Jul 13 '16 at 17:40

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.