Skip to content
Snippets Groups Projects
blender_theme_as_c.py 8.55 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"
    
    #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 os
    import sys
    sys.path.append(os.path.join(
        os.path.dirname(__file__),
        "..", "..", "..",
        "release", "scripts", "addons", "io_blend_utils", "blend",
    ))
    
    source_dst = os.path.join(
        os.path.dirname(__file__),
        "..", "..", "..",
        "release", "datafiles", "userdef", "userdef_default_theme.c"
    )
    del sys
    
    
    def theme_data(userpref_filename):
        import blendfile
        blend = blendfile.open_blend(userpref_filename)
        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'_') or name == b'pad':
            return True
        elif name.startswith(b'pad') and name[3:].isdigit():
            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')
    
    
    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)
    
    
    if __name__ == "__main__":
        main()