Skip to content
Snippets Groups Projects
blender_theme_as_c.py 10.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env python3
    
    # ***** BEGIN GPL LICENSE BLOCK *****
    #
    # This program is free software; you can redistribute it and/or
    # modify it under the terms of the GNU General Public License
    # as published by the Free Software Foundation; either version 2
    # of the License, or (at your option) any later version.
    #
    # This program is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU General Public License for more details.
    #
    # You should have received a copy of the GNU General Public License
    # along with this program; if not, write to the Free Software Foundation,
    # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    #
    # ***** END GPL LICENCE BLOCK *****
    
    # <pep8 compliant>
    
    """
    Generates 'userdef_default_theme.c' from a 'userpref.blend' file.
    
    Pass your user preferenes blend file to this script to update the C source file.
    
    eg:
    
        ./source/tools/utils/blender_theme_as_c.py ~/.config/blender/2.80/config/userpref.blend
    
    .. or find the latest:
    
        ./source/tools/utils/blender_theme_as_c.py $(find ~/.config/blender -name "userpref.blend" | sort | tail -1)
    """
    
    C_SOURCE_HEADER = r'''/*
     * Generated by 'source/tools/utils/blender_theme_as_c.py'
     *
     * Do not hand edit this file!
     */
    
    #include "DNA_userdef_types.h"
    
    #include "BLO_readfile.h"
    
    
    /* clang-format off */
    
    
    #ifdef __LITTLE_ENDIAN__
    #  define RGBA(c) {((c) >> 24) & 0xff, ((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
    #  define RGB(c)  {((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
    #else
    #  define RGBA(c) {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff, ((c) >> 24) & 0xff}
    #  define RGB(c)  {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff}
    #endif
    
    '''
    
    # TODO, support arrays properly,
    # these variables hardly change so hard code for now.
    TARM_WORKAROUND = '''\t\t{
    \t\t\t.solid = RGBA(0x9a0000ff),
    \t\t\t.select = RGBA(0xbd1111ff),
    \t\t\t.active = RGBA(0xf70a0aff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0xf74018ff),
    \t\t\t.select = RGBA(0xf66913ff),
    \t\t\t.active = RGBA(0xfa9900ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x1e9109ff),
    \t\t\t.select = RGBA(0x59b70bff),
    \t\t\t.active = RGBA(0x83ef1dff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x0a3694ff),
    \t\t\t.select = RGBA(0x3667dfff),
    \t\t\t.active = RGBA(0x5ec1efff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0xa9294eff),
    \t\t\t.select = RGBA(0xc1416aff),
    \t\t\t.active = RGBA(0xf05d91ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x430c78ff),
    \t\t\t.select = RGBA(0x543aa3ff),
    \t\t\t.active = RGBA(0x8764d5ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x24785aff),
    \t\t\t.select = RGBA(0x3c9579ff),
    \t\t\t.active = RGBA(0x6fb6abff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x4b707cff),
    \t\t\t.select = RGBA(0x6a8691ff),
    \t\t\t.active = RGBA(0x9bc2cdff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0xf4c90cff),
    \t\t\t.select = RGBA(0xeec236ff),
    \t\t\t.active = RGBA(0xf3ff00ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x1e2024ff),
    \t\t\t.select = RGBA(0x484c56ff),
    \t\t\t.active = RGBA(0xffffffff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x6f2f6aff),
    \t\t\t.select = RGBA(0x9845beff),
    \t\t\t.active = RGBA(0xd330d6ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x6c8e22ff),
    \t\t\t.select = RGBA(0x7fb022ff),
    \t\t\t.active = RGBA(0xbbef5bff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x8d8d8dff),
    \t\t\t.select = RGBA(0xb0b0b0ff),
    \t\t\t.active = RGBA(0xdededeff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x834326ff),
    \t\t\t.select = RGBA(0x8b5811ff),
    \t\t\t.active = RGBA(0xbd6a11ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x08310eff),
    \t\t\t.select = RGBA(0x1c430bff),
    \t\t\t.active = RGBA(0x34622bff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x000000ff),
    \t\t\t.select = RGBA(0x000000ff),
    \t\t\t.active = RGBA(0x000000ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x000000ff),
    \t\t\t.select = RGBA(0x000000ff),
    \t\t\t.active = RGBA(0x000000ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x000000ff),
    \t\t\t.select = RGBA(0x000000ff),
    \t\t\t.active = RGBA(0x000000ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x000000ff),
    \t\t\t.select = RGBA(0x000000ff),
    \t\t\t.active = RGBA(0x000000ff),
    \t\t},
    \t\t{
    \t\t\t.solid = RGBA(0x000000ff),
    \t\t\t.select = RGBA(0x000000ff),
    \t\t\t.active = RGBA(0x000000ff),
    \t\t},
    '''
    
    
    def round_float_32(f):
        from struct import pack, unpack
        return unpack("f", pack("f", f))[0]
    
    
    def repr_f32(f):
        f_round = round_float_32(f)
        f_str = repr(f)
        f_str_frac = f_str.partition(".")[2]
        if not f_str_frac:
            return f_str
        for i in range(1, len(f_str_frac)):
            f_test = round(f, i)
            f_test_round = round_float_32(f_test)
            if f_test_round == f_round:
                return "%.*f" % (i, f_test)
        return f_str
    
    
    
    # Avoid maintaining multiple blendfile modules
    import sys
    
    sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
    
    
    source_dst = os.path.join(
        os.path.dirname(__file__),
        "..", "..", "..",
        "release", "datafiles", "userdef", "userdef_default_theme.c"
    )
    
    
    dna_rename_defs_h = os.path.join(
        os.path.dirname(__file__),
        "..", "..", "..",
        "source", "blender", "makesdna", "intern", "dna_rename_defs.h"
    )
    
    
    def dna_rename_defs(blend):
        """
        """
        from blendfile import DNAName
        import re
        re_dna_struct_rename_elem = re.compile(
            r'DNA_STRUCT_RENAME_ELEM+\('
            r'([a-zA-Z0-9_]+)' ',\s*'
            r'([a-zA-Z0-9_]+)' ',\s*'
            r'([a-zA-Z0-9_]+)' '\)',
        )
        with open(dna_rename_defs_h, 'r', encoding='utf-8') as fh:
            data = fh.read()
        for l in data.split('\n'):
            m = re_dna_struct_rename_elem.match(l)
            if m is not None:
                struct_name, member_storage, member_runtime = m.groups()
                struct_name = struct_name.encode('utf-8')
                member_storage = member_storage.encode('utf-8')
                member_runtime = member_runtime.encode('utf-8')
                dna_struct = blend.structs[blend.sdna_index_from_id[struct_name]]
                for field in dna_struct.fields:
                    dna_name = field.dna_name
                    if member_storage == dna_name.name_only:
                        field.dna_name = dna_name = DNAName(dna_name.name_full)
                        del dna_struct.field_from_name[dna_name.name_only]
                        dna_name.name_full = dna_name.name_full.replace(member_storage, member_runtime)
                        dna_name.name_only = member_runtime
                        dna_struct.field_from_name[dna_name.name_only] = field
    
    
    
    def theme_data(userpref_filename):
        import blendfile
        blend = blendfile.open_blend(userpref_filename)
    
        dna_rename_defs(blend)
    
        u = next((c for c in blend.blocks if c.code == b'USER'), None)
        # theme_type = b.sdna_index_from_id[b'bTheme']
        t = u.get_pointer((b'themes', b'first'))
        t.refine_type(b'bTheme')
        return blend, t
    
    
    def is_ignore_dna_name(name):
    
        if name.startswith(b'_'):
    
        elif name in {
                b'active_theme_area',
        }:
            return True
    
        else:
            return False
    
    
    def write_member(fw, indent, b, theme, ls):
        path_old = ()
    
        for key, value in ls:
            key = key if type(key) is tuple else (key,)
            path_new = key[:-1]
    
            if tuple(path_new) != tuple(path_old):
                if path_old:
                    p = len(path_old) - 1
                    while p >= 0 and (p >= len(path_new) or path_new[p] != path_old[p]):
                        indent = p + 1
                        fw('\t' * indent)
                        fw('},\n')
                        p -= 1
                    del p
    
                p = 0
                for p in range(min(len(path_old), len(path_new))):
                    if path_old[p] != key[p]:
                        break
                    else:
                        p = p + 1
    
                for i, c in enumerate(path_new[p:]):
                    indent = p + i + 1
                    fw('\t' * indent)
                    attr = c.decode('ascii')
                    fw(f'.{attr} = {{\n')
    
            # Evil, tarm array workaround.
            if key[0] == b'tarm':
                if path_old[0] != b'tarm':
                    fw(TARM_WORKAROUND)
                path_old = path_new
                continue
    
            if not is_ignore_dna_name(key[-1]):
                indent = '\t' * (len(path_new) + 1)
                attr = key[-1].decode('ascii')
                if isinstance(value, float):
                    if value != 0.0:
                        value_repr = repr_f32(value)
                        fw(f'{indent}.{attr} = {value_repr}f,\n')
                elif isinstance(value, int):
                    if value != 0:
                        fw(f'{indent}.{attr} = {value},\n')
                elif isinstance(value, bytes):
                    if set(value) != {0}:
                        if len(value) == 3:
                            value_repr = "".join(f'{ub:02x}' for ub in value)
                            fw(f'{indent}.{attr} = RGB(0x{value_repr}),\n')
                        elif len(value) == 4:
                            value_repr = "".join(f'{ub:02x}' for ub in value)
                            fw(f'{indent}.{attr} = RGBA(0x{value_repr}),\n')
                        else:
                            value = value.rstrip(b'\x00')
                            is_ascii = True
                            for ub in value:
                                if not (ub >= 32 and ub < 127):
                                    is_ascii = False
                                    break
                            if is_ascii:
                                value_repr = value.decode('ascii')
                                fw(f'{indent}.{attr} = "{value_repr}",\n')
                            else:
    
    Campbell Barton's avatar
    Campbell Barton committed
                                value_repr = "".join(f'{ub:02x}' for ub in value)
    
                                fw(f'{indent}.{attr} = {{{value_repr}}},\n')
                else:
                    fw(f'{indent}.{attr} = {value},\n')
            path_old = path_new
    
    
    def convert_data(blend, theme, f):
        fw = f.write
        fw(C_SOURCE_HEADER)
        fw('const bTheme U_theme_default = {\n')
        ls = list(theme.items_recursive_iter(use_nil=False))
        write_member(fw, 1, blend, theme, ls)
    
        fw('};\n')
    
        fw('\n')
        fw('/* clang-format on */\n')
    
    def file_remove_empty_braces(source_dst):
        with open(source_dst, 'r', encoding='utf-8') as fh:
            data = fh.read()
        # Remove:
        #     .foo = { }
        import re
    
    Campbell Barton's avatar
    Campbell Barton committed
    
    
        def key_replace(match):
            return ""
        data_prev = None
    
        # Braces may become empty by removing nested
    
        while data != data_prev:
            data_prev = data
            data = re.sub(
                r'\s+\.[a-zA-Z_0-9]+\s+=\s+\{\s*\},',
                key_replace, data, re.MULTILINE
            )
    
    
        # Use two spaces instead of tabs.
        data = data.replace('\t', '  ')
    
    
        with open(source_dst, 'w', encoding='utf-8') as fh:
            fh.write(data)
    
    
    
    def main():
        import sys
        blend, theme = theme_data(sys.argv[-1])
        with open(source_dst, 'w', encoding='utf-8') as fh:
            convert_data(blend, theme, fh)
    
    
        # grr, msvc doesn't like empty braces
        file_remove_empty_braces(source_dst)
    
    
    
    if __name__ == "__main__":
        main()