Material texture path change Python

i am trying to copy all texture pictures, used in project, to one folder and than change the path to this folder.
As a starting point I used this topic: Access To Material Texture Path
and thanks to @Alberto_Tono I was able to find a copy all external used jepg files in my materials to one folder.
From The Building Coder: https://thebuildingcoder.typepad.com/blog/2019/04/set-material-texture-path-in-editscope.html
I found out there is a way to change path in material to my new path. But it is writen in C# and I’am still learning python. Is there a way you can help me to figure out what i am doing wrong?
Here is my code in python (for applying in Revit I’m using pyRevit)

import clr
import rpw
import sys
import os
import ntpath
from shutil import copy
clr.AddReference("RevitApi")
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import TaskDialog
from rpw.ui.forms import Console

doc = __revit__.ActiveUIDocument.Document

#Save file:
try:
folder = rpw.ui.forms.select_folder()
filePath = folder + '\\'+ doc.Title + '.rvt'
opt = SaveAsOptions()
opt.OverwriteExistingFile = True
#doc.SaveAs(filePath, opt)
except:
sys.exit()

#name of file from path
def fileName(path):
head, tail = ntpath.split(path)
return tail or ntpath.basename(head)

def newfolder(name, folder):
name = folder+"\\"+name
newfolder.path = name
if not os.path.exists(name):
    os.makedirs(name)

#Create New Folder
newfolder("Materials",folder)

#select material
materials = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Materials).\
        WhereElementIsNotElementType().ToElements()

for material in materials:
appearanceAssetId = material.AppearanceAssetId
assetElem = doc.GetElement(appearanceAssetId)
try:
    with Transaction(doc, "Change jpeg path") as t:
        t.Start()
        with Visual.AppearanceAssetEditScope(assetElem.Document) as editScope:
            editableAsset = editScope.Start(assetElem.Id)
            assetProperty = editableAsset.FindByName("generic_diffuse")
            connectedAsset = assetProperty.GetConnectedProperty(0)
            if connectedAsset.Name == "UnifiedBitmapSchema":
                assetPath = connectedAsset["unifiedbitmap_Bitmap"]
                assetPathValue = assetPath.Value
                #Copy material to new folder
                copy(assetPathValue,newfolder.path)
                texturePath = "{}\\{}"
                texturePathFormat = texturePath.format(folder,fileName(assetPathValue))
                #NOT WORKING
                assetPathValue = texturePathFormat
            editScope.Commit(True)
        t.Commit()
except:
    print("passing")
    pass

Hi @Mo_Ody,

You can also use the Material Change Texture Path node in Genius Loci package.
It is ready to be used, but with Dynamo not pyRevit. :grinning:

3 Likes

Dear @mo_ody

We could work together on the Python code and then publish the solution here to make it open source . We at www.sfcdi.org would love to support you.

Best regards

It is not 100% solved, but it is a start:

  • I used Dynamo, but installed the RevitPythonWrapper package so you wont have to change much when trying it in pyRevit.
  • Revit returns in some cases the relative path when looking for the bitmap path . I´ve added the default material library to the path. with os.path.join(default_lib, asset_path), which could cause problems if the files are not there. Jeremy gives some good pointer at the end of this blog post.
  • I had the issue of returning joined, duplicated paths from GetSingleConnectedAsset(). It´s solved for now by using the split() method. I assume there is cleaner way to receive only 1 path.

There is not much wrong with your code. You should put break points inside your code and return it partially to see if data goes from one method to the next in the right format. To debug your scripts faster, you could look into Exceptions and consider using Dynamo to “prototype” your scripts:

Capture

Here is the script:

import sys
rpw_path = IN[0]
sys.path.append(rpw_path)

from rpw import db, ui, revit, DB
from shutil import copy
import os

doc = revit.doc


def new_folder(name, folder):
    name = "\\".join((folder, name))
    new_folder.path = name
    if not os.path.exists(name):
        os.makedirs(name)


def copy_materials(document):
    """
    Copy all texture pictures used in project

    TODO:
        - Relative path handling
    """
    out = []	

    # Destination folder
    dest_folder = ui.forms.select_folder()
    new_folder("Materials", dest_folder)
    default_lib = "C:/Program Files (x86)/Common Files/Autodesk Shared/Materials/Textures"

    materials = db.Collector(of_class="Material", is_type=False).elements

    with db.Transaction("Change image path") as t:
        for material in materials:
            try:
                appearance_id = material.AppearanceAssetId
                asset = document.GetElement(appearance_id)
                if asset:
                    with DB.Visual.AppearanceAssetEditScope(document) as editScope:
                        edit_asset = editScope.Start(asset.Id)
                        generic_diff = edit_asset.FindByName("generic_diffuse")
                        if generic_diff:
                            connect_asset = generic_diff.GetSingleConnectedAsset()
                            if connect_asset:
                                asset_path = connect_asset["unifiedbitmap_Bitmap"].Value.split("|")[0]

                                # Copy files and handle relative path
                                if not os.path.exists(asset_path):
                                    asset_path = os.path.join(default_lib, asset_path)
                                copy(asset_path, new_folder.path)

                                # Change texture path
                                _, file_name = os.path.split(asset_path)
                                texture_path = os.path.join(new_folder.path, file_name)
                                connect_asset["unifiedbitmap_Bitmap"].Value = texture_path
                                out.append(texture_path)
                        editScope.Commit(True)
            except Exception as e:
                out.append((file_name, texture_path, e))
    return out

OUT = copy_materials(doc)
4 Likes

Hi guys thanks for help.
@Kibar I tried it up. And it’s working quite well. I had to change asset_path = asset_path.replace("/\","/") because of incorrect file path
I don’t know if your dynamo works 100% correct, because it gives me a error mesage. But when I run my code it’s like 70% success. The biggest issue is shared Autoced library where it copy image but not change the file path. but when I created a new RVT file with new materials everything worked as it should.

what I wanted to do first was to create a file with the rvt file with all the materials I could send (for example via email )and it going to work properly. So I tried create relative path to material like: /Material/name_of_jpeg.jpg

test_path = os.path.relpath(texture_path,newfolder.filePath)

but I think it’s not possible. Maybe I’ll just create a dynamo script to change a path in another user computer. Or Is there a better way?

1 Like

I had the same issue setting the new value.
I ran some properties and methods of AssetPropertyString and it returns that the new value is invalid. I checked if it is a string and if the path of that string exists.

I won’t be able to look into it more until tonight/tomorrow. I have it set in pyRevit now aswell and will post it here later.

@Alberto_Tono 's suggestion of working with sfcdi could be very helpful here, especially if you want to extend the functionality (other properties such as bump, or some kind of image batch manager).

1 Like

Hey @Mo_Ody,
Sorry for coming back at this so late.
I have been working on it in pyRevit and tried to find the error by splitting it into functions and making changes to how the path to the bitmap is retrieved.

The exception is raised on only 1 image in my case and it is the same image when I use it on a different project. My guess is that Revit prevents “deleting”/changing the asset property, because the value is the correct path as String. The reason could be related to this post on stack overflow.

Here is the Exception:

Autodesk.Revit.Exceptions.ArgumentException: The input value is invalid for this AssetPropertyString property.
Parameter name: value
at Autodesk.Revit.DB.Visual.AssetPropertyString.set_Value(String value)
at CallSite.Target(Closure , CallSite , Object , Object )
at material_bitmap_copy_and_set$3884(Closure , PythonFunction , Object , Object )

You can skip the exception by checking the property for IsValidValue() as commented out in the script.

Here is the content of my script.py file:

from System import Environment
from rpw import db, ui, revit, DB, exceptions
from pyrevit import forms, script
from shutil import copy
import os


def main():
    material_collector = db.Collector(of_class="Material", is_type=False)
    materials = material_collector.get_elements()

    # Input destination folder
    destination_folder = forms.pick_folder(title="DESTINATION FOLDER")
    n_folder = os.path.join(destination_folder, "Materials")
    if not os.path.exists(n_folder):
        os.makedirs(n_folder)

    # Effective Output
    max_value = len(materials)
    with forms.ProgressBar(title="Material...\
            ({value} of {max_value})", cancellable="True") as pb:
        with db.Transaction("Copy and change rendering texture path") as t:
            for count, material in enumerate(materials):

                # ProgressBar update
                if pb.cancelled:
                    break
                else:
                    pb.update_progress(count, max_value)

                # Copy materials to new folder
                # Then set material asset to copy
                material_bitmap_copy_and_set(material, n_folder)


def file_copy_by_path(file_path, destination_folder):
    """
    Copies a file to a folder
    and returns the new path

    file_path -- str (absolute path)
    destination_folder -- str (absolute path)
    """

    try:
        copy(file_path, destination_folder)
    except EnvironmentError as e:
        # File exists at destination
        pass
    except Exception as e:
        # Other exceptions
        print(e)

    file_name = os.path.basename(file_path)
    new_path = os.path.join(destination_folder, file_name)

    return new_path


def material_bitmap_copy_and_set(material, new_folder):
    asset_id = material.AppearanceAssetId

    if asset_id.InvalidElementId != -1:
        # Color and Transparency only appearances
        # will return an invalid element ids (-1)
        asset_element = material.Document.GetElement(asset_id)
        if asset_element is not None:
            with DB.Visual.AppearanceAssetEditScope(asset_element.Document) as edit_scope:

                # Return an editable copy of the appearance asset
                editable_asset = edit_scope.Start(asset_element.Id)
                generic_diff = DB.Visual.Generic.GenericDiffuse
                generic_diff_property = editable_asset.FindByName(generic_diff)
                if generic_diff_property is not None:

                    # Find the connected asset
                    connected_asset = generic_diff_property.GetSingleConnectedAsset()
                    if connected_asset is not None:

                        # Find the target asset property
                        uni_bitmap = DB.Visual.UnifiedBitmap.UnifiedbitmapBitmap
                        bitmap_property = connected_asset.FindByName(uni_bitmap)

                        # Get the absolute path to the asset property
                        bitmap_path = get_bitmap(bitmap_property.Value)

                        # Copy bitmap to new locatioon
                        new_bitmap_path = file_copy_by_path(bitmap_path, new_folder)

                        try:
                            # if bitmap_property.IsValidValue(new_bitmap_path):
                            bitmap_property.Value = new_bitmap_path
                            print("SUCCESS for [{}]".format(material.Name))
                            print("\tNew bitmap-> {}".format(new_bitmap_path))
                            print(20 * "-")
                            edit_scope.Commit(True)

                        except exceptions.RevitExceptions.ArgumentException as e:
                            print("FAIL for [{}]".format(material.Name))
                            print("\tValue not accepted->({})".format(new_bitmap_path))
                            print("\tException->: {}".format(e))
                            print("\tCurrent bitmap->: {}".format(bitmap_path))
                            print(20 * "-")
                            edit_scope.Cancel()

                        except Exception as e:
                            # Other exceptions
                            print("FAIL for {}".format(material.Name))
                            print("\tException->: {}".format(bitmap_path))
                            print(20 * "-")
                            edit_scope.Cancel()


def get_bitmap(property_value):
    """
    Returns the absolute path of the given
    AsserPropertyString.Value
    --> Path is relative of inside default Material Library

    Input raltive path like:
    '1/path/file | 2/path/file | ...'

    Output:
    'absolute/path/to/file'
    """

    bitmap_paths = property_value.split("|")
    abs_path = []
    library_paths = revit.app.GetLibraryPaths().Values

    for bitmap_path in bitmap_paths:
        if os.path.isabs(bitmap_path):
            # abs_path = bitmap_path
            abs_path = os.path.abspath(bitmap_path)
            break
        else:
            for lib_path in library_paths:
                if os.path.dirname(os.path.relpath(bitmap_path)) in os.path.abspath(lib_path):
                    abs_path = os.path.join(os.path.abspath(lib_path), os.path.basename(bitmap_path))
                    break

    return abs_path


if __name__ == "__main__":
    main()
3 Likes

woow.thanks a lot! As a beginner, I learned a lot from this code. :nerd_face:
But this script didn’t work for me. an error popped up immediately after it started:


So I went through the code and found out that the error is causing the get_bitmap function specifically abs_path was returning [ ] for not absolute path

and I also don’t know if using lib_path in this get_bitmap function has sense, because it’s a Revit folder in which there is no autodesk shared library and also no images at all.

so I changed it the way you showed me before:

def get_bitmap(property_value):
"""
Returns the absolute path of the given
AsserPropertyString.Value
--> Path is relative of inside default Material Library

Input raltive path like:
'1/path/file | 2/path/file | ...'

Output:
'absolute/path/to/file'
"""
default_lib = "C:\Program Files (x86)\Common Files\Autodesk Shared\Materials\Textures"
bitmap_paths = property_value.split("|")
abs_path = []
library_paths = revit.app.GetLibraryPaths().Values

for bitmap_path in bitmap_paths:
    if os.path.isabs(bitmap_path):
        # abs_path = bitmap_path
        abs_path = os.path.abspath(bitmap_path) 
    else:
        abs_path = os.path.join(default_lib,bitmap_path)
             
return abs_pat

and it’s worked, but success is comparable to the previous code :smile: but it will probably be the error you are talking about


anyway, thank you very much you helped me a lot

1 Like

Great to see that you´ve found your way through the code :smiley: .

Yes, it was an attempt on iterating through the library path, and it needs some fixing.

Thanks @Kibar . I tried out the code and got through some projects, but on one of them, I received this error. Do you know why?

It´s been a while since i´ve seen the code, so i went through it and cleaned it up a bit to make it easier to debug…

from System import Environment
from rpw import db, ui, revit, DB, exceptions
from pyrevit import forms, script
from shutil import copy
import os

def main():
    materials = get_materials()
    new_folder = create_folder()

    max_value = len(materials)
    with forms.ProgressBar(title="Material...\
            ({value} of {max_value})", cancellable="True") as pb:
        with db.Transaction("Copy and change rendering texture path") as t:
            for count, material in enumerate(materials):
                if pb.cancelled:
                    break
                else:
                    pb.update_progress(count, max_value)

                change_rendering_texture_path(material, new_folder)


def change_rendering_texture_path(material, new_folder):
    """
    see: https://thebuildingcoder.typepad.com/blog/2019/04/set-material-texture-path-in-editscope.html

    TODO:
        - RevitException.ArgumentException:
          Does not accept some values to be assigned as asset_bitmap.Value.
          Can be skipped [if asset_bitmap.IsValidValue(new_bitmap_path):]
      
    Copies the material generic appearance image to the new folder.
    Assigns the copy to the material.

    material -- Material Class object
    new_folder -- str (absolute path)
    """
    asset_element = get_asset_element(material)
    if asset_element:
        with DB.Visual.AppearanceAssetEditScope(revit.doc) as scope:
            active_asset = scope.Start(asset_element.Id)
            asset_diffuse = active_asset.FindByName(
                DB.Visual.Generic.GenericDiffuse)
            if asset_diffuse:
                asset = asset_diffuse.GetSingleConnectedAsset()
                if asset:
                    asset_bitmap = asset.FindByName(
                        DB.Visual.UnifiedBitmap.UnifiedbitmapBitmap)
                    bitmap_path = get_bitmap_path(asset_bitmap)
                    new_bitmap_path = copy_by_path(bitmap_path, new_folder)

                    try:
                        asset_bitmap.Value = new_bitmap_path
                        print("Material texture path for [{}] changed.".format(
                            material.Name))
                        print("New texture path-> {}".format(new_bitmap_path))
                        print(20 * "-")
                        scope.Commit(True)
                    except exceptions.RevitExceptions.ArgumentException as e:
                        print("FAILED to change path for [{}]".format(
                            material.Name))
                        print("\tValue not accepted->({})".format(
                            new_bitmap_path))
                        print("\tException->: {}".format(e.Message))
                        print(20 * "-")
                        scope.Cancel()
                    except Exception as e:
                        # Other exceptions
                        print("FAIL for [{}]".format(material.Name))
                        print("\tException->: {}".format(e.Message))
                        print(20 * "-")
                        scope.Cancel()


def get_materials():
    """ Collect materials from active document """
    material_collector = db.Collector(of_class="Material", is_type=False)
    return material_collector.get_elements()


def create_folder():
    """ Takes user input to create a 'Materials' folder. """
    destination_folder = forms.pick_folder(title="Folder to save Materials TO")
    new_folder = os.path.join(destination_folder, "Materials")
    if not os.path.exists(new_folder):
        os.makedirs(new_folder)
    return new_folder


def get_asset_element(material):
    """
    Gets the AppearanceAssetElement of the given material.
    If element is color or transparency only, returns None

    material - Material Class object
    """
    asset_id = material.AppearanceAssetId
    if asset_id.InvalidElementId == -1:
        return None
    else:
        return revit.doc.GetElement(asset_id)


def get_bitmap_path(bitmap_property):
    """
    Returns the absolute path of the given AssetPropertyString
    --> Path is relative to default Material Library

    Input raltive path like:
    '1/path/file' or
    '1/path/file | 2/path/file | ...'

    Output:
    'absolute/path/to/file'
    """
    bitmap_paths = bitmap_property.Value.split("|")
    library_paths = revit.app.GetLibraryPaths().Values
    for bitmap_path in bitmap_paths:
        if os.path.isabs(bitmap_path):
            return os.path.abspath(bitmap_path)
        else:
            for lib_path in library_paths:
                bit = os.path.dirname(os.path.relpath(bitmap_path))
                lib = os.path.abspath(lib_path)
                if bit.lower() in lib.lower():
                    return os.sep.join(
                        [lib, os.path.basename(bitmap_path)])


def copy_by_path(file_path, destination_folder):
    """
    Copies a file to a folder and returns the new path

    file_path -- str (absolute path)
    destination_folder -- str (absolute path)
    """
    try:
        copy(file_path, destination_folder)
    except EnvironmentError as e:
        # File exists at destination
        pass
    except Exception as e:
        # Other exceptions
        print(e)
    file_name = os.path.basename(file_path)
    new_path = os.path.join(destination_folder, file_name)
    return new_path


if __name__ == "__main__":
    main()

I had a similar issue and made some minor changes in how it searches for the bitmap path in the library paths (it´s now in the “get_bitmap_path” function).

See if it resolved your issue as well.

NOTE:
I still couldn´t figure out why some paths are not valid as AssetPropertyString.Value, even though they are strings. For now it´s just printing it as “failed”.

2 Likes

Thanks for cleaning up! I tried it on a couple projects/templates I had success with and I’m getting an error, len() of unsized object. I’ll try to find out why.

image

2 Likes

Hello Kibar!
I read about what they discuss here and I was extremely new to Dynamo. But a task was given to me and I know that it is the basics of the basics for you: Where and how to read assets, textures of my project with Python script? I could post a code here so I can study it. I already program in Python. Sorry for my English.

Hi @rcrd.albuquerque,
This thread got very much into pyRevit and python programming. I think it fits more into the RevitAPI Forum. The steps in the post above can be confusing and I´ll see if I can put together a Dynamo graph with minimal python use soon.

There are Dynamo packages that can help you access Assets:

I´d recommend you create a new post here (if you´re using Dynamo) or the RevitAPI Forum (if you´re using only code), and be more specific about your goal (what did you try, what type of asset are you looking for, what are you going to do with it, … ).

Oh thank you very much! I will follow your guidelines and visit the informed website (revitAPI).

1 Like