UNSOLVED Set the Preview of an Asset using an Image file.

Hi. In the UI of the Asset Browser there is the option to set a preview image using "Update Preview From File ...", and I would like to do the same thing this command does in Python without showing an File Chooser Dialog but by using a known path.

I know that I can get an existing preview image using this code:

metadata = assetDescription.GetMetaData()
previewUrl = metadata.Get(maxon.ASSETMETADATA.ASSET_PREVIEWIMAGEURL)

And setting other meta data of the asset works fine, but I don't know how to properly set/create/update the preview image.

I tried to set it with:

assetDescription.StoreMetaData(maxon.ASSETMETADATA.ASSET_PREVIEWIMAGEURL, maxon.Url("file:///C:/path/to/image.png"), maxon.AssetMetaData.KIND.PERSISTENT)

or

assetDescription.StoreUrlMetaData(maxon.ASSETMETADATA.ASSET_PREVIEWIMAGEURL, maxon.Url("file:///C:/path/to/image.png"), maxon.AssetMetaData.KIND.PERSISTENT)

I also tried to disable the automatic preview before setting ASSET_PREVIEWIMAGEURL using:

metaProperties = metadata.Get(maxon.ASSETMETADATA.MetaProperties)
metaProperties.Set(maxon.ASSET.METAPROPERTIES.BASE.AUTOMATICPREVIEW, False)

The only example code related to the ASSET_PREVIEWIMAGEURL that I was able to find was in the examples_dots.cpp so I tried to use that code and instead of maxon::Url newPreviewUrl = presetAsset.GeneratePreview(previewWidth, dummy, 0) iferr_return; I used newPreviewUrl = maxon.Url("file:///C:/path/to/image.png") in python, but that didn't work either.

I think assetDescription.StoreUrlMetaData and maxon.ASSETMETADATA.ASSET_PREVIEWIMAGEURL is right, but I guess maxon.Url must be something different than a file:/// URL?

Kind regards,
Till

Hello @till-niese,

Thank you for reaching out to us and as a heads-up, I am still on vacation, so the thread will either be managed by Maxime or Manuel. But as an FYI:

  1. The code you are referring to is deliberately called Invoke Asset Implementation and not Generate Previews for example. You are sort of barking up the wrong tree here.
    • maxon::ASSETMETADATA::ASSET_PREVIEWIMAGEURL is the correct attribute to set for static previews.
    • When the preview images have been processed before by the Asset Browser, you must flush the preview image cache for that preview with maxon::ImageUrlCacheInterface::InvalidateCache (not wrapped for Python) as shown in the example.
    • The scheme, e.g., file:///, the URL is in, does not really matter as long as the Asset Browser can open a stream/connection to access it.
  2. To rebuild the thumbnail of an asset, you must use maxon.AssetCreationInterface.AddPreviewRenderAsset.
  3. Which brings us to the last point, the barking up the wrong tree part.
    • Manipulating the asset thumbnail in the Implementing Preset Assets example makes sense because we are using there the ::GeneratePreview method of the asset which we implemented before. We sort of do manually what AddPreviewRenderAsset does. The point was to highlight how to interact with the implementation of an asset type from an asset description of an instance of that type.
    • You could invalidate a preview thumbnail and then just write an arbitrary image URL to that attribute, but the next time, the Asset Browser rebuilds the thumbnails, all your changes would be gone.
    • You could technically attempt to write a ASSET_PREVIEWIMAGEURL as a read-only attribute, but that is very much not recommended and supported, as this could lead to unexpected behavior.

So, to recap:

  • When you want to regenerate the thumbnail of an asset, you must use AddPreviewRenderAsset.
  • It is not intended to replace or mess with the thumbnail rendering output of existing asset types.

My hunch is that you want to exactly do that, replace the thumbnail of an asset with a thumbnail that is not equal to the thumbnail output of that asset type. If that is the case, you might want to describe more precisely what you are trying to achieve practically.

Cheers,
Ferdinand

I confirm that overwriting ASSET_PREVIEWIMAGEURL does not work in Python, and InvalidateCache is not exposed to the Python API and would need some internal change to the Cinema 4D code source to make it public so there is no possible workaround for you for the moment.

Cheers,
Maxime.

Ok nervermind it is possible to overwrite the ASSET_PREVIEWIMAGEURL in Python it's just that AssetDescription.StoreUrlMetaData, AssetDescription.StoreMetaData, AssetDescription.EraseMetaData are bugged in Python. So find bellow a workaround for that (it will be fixed in the next version and StoreUrlMetaData will work out of the box).

import c4d
import maxon

# --- AssetDescriptionInterface monkey patching fix for AssetDescriptionInterface.StoreUrlMetaData() --------------------------
@maxon.interface.MAXON_INTERFACE(maxon.consts.MAXON_REFERENCE_COPY_ON_WRITE, "net.maxon.interface.assetdescription")
class AssetDescriptionInterface(maxon.AssetBaseInterface):

    @maxon.interface.MAXON_FUNCTION_EXTEND("net.maxon.interface.assetdescription.StoreUrlMetaData")
    def StoreUrlMetaData(self, metaId, source, kind):
        return self.GetRepository().StoreUrlMetaData(self, metaId, source, kind)

maxon.AssetDescriptionInterface.StoreUrlMetaData = AssetDescriptionInterface.StoreUrlMetaData
# --- AssetDescriptionInterface monkey patching fix for AssetDescriptionInterface.StoreUrlMetaData() --------------------------


def main():
    repository = maxon.AssetInterface.GetUserPrefsRepository()
    assetId = maxon.Id("Id of the asset")
    lst = repository.FindAssets("net.maxon.node.assettype.nodetemplate", assetId, maxon.Id(),
                  maxon.ASSET_FIND_MODE.LATEST)
    assetDescription = lst[0]

    currentLanguage = maxon.Resource.GetCurrentLanguage()
    name = assetDescription.GetMetaString(maxon.OBJECT.BASE.NAME, currentLanguage)
    print(name)

    metadata = assetDescription.GetMetaData()
    imagepUrl = maxon.Url("C:/Users/m_adam/picture.png")

    # Stores the Url Meta Data, if there was no preview stored previously (aka the default png file for the given category) then it will be refreshed right away
    # Otherwise Cinema 4D need to be restarted
    maxon.AssetDescriptionInterface.StoreUrlMetaData = AssetDescriptionInterface.StoreUrlMetaData
    assetDescription.StoreUrlMetaData(maxon.ASSETMETADATA.ASSET_PREVIEWIMAGEURL, imagepUrl, maxon.AssetMetaData.KIND.PERSISTENT)
    
    # Turn off automatic preview
    metaProperties.Set(maxon.ASSET.METAPROPERTIES.BASE.AUTOMATICPREVIEW, False)
    assetDescription.StoreMetaData(maxon.ASSETMETADATA.MetaProperties, metaProperties, maxon.AssetMetaData.KIND.PERSISTENT)
    

if __name__ == '__main__':
    main()

However the limitation for InvalidateCache not being exposed is still there and as said will need a Cinema 4D update to make it work, so for the moment the only way to see new icons is to restart Cinema 4D.

Cheers,
Maxime.

@m_adam Hi, this partially works for me.

When I manually run your script for the assets we have already created, the preview images are set and displayed after a restart of Cinema 4D.

However if I insert the code directly into our script, then sometimes one object has the preview image correctly set and displayed after a restart. But most of the time it is the preview image created by Cinema 4D itself, which is almost completely black.

To clarify what we do:
Within a batch process (a script executed within Cinema 4D) we create multiple textured objects and add each of those to a newly created database. The object to be added to the Database have Redshift materials.

So if we create for example 9 object within that batch process then all 9 object most of the time have a black image, sometimes one of those has the correct image.

The database is create this way:

        bases = maxon.BaseArray(maxon.AssetRepositoryRef)
        assetDb = AssetRepositoryTypes.AssetDatabase
        assetDb = maxon.RegistryInterface.FindEntryValue(assetDb._registry, assetDb._ids)

        repository = maxon.AssetInterface.CreateRepositoryFromUrl(
            rid, assetDb, bases, maxon.Url(self.databasePath), True, False, False, None)
        if not repository:
            raise RuntimeError("Repository construction failed.")

In a loop we iterate objects and create an Asset from that object, at that part I added the above code.

for objectVaraint in variants:
        assetMetadata = maxon.AssetMetaData()
        assetCategoryId = maxon.Id(self.categoryId)

        # A StoreAssetStruct is a helper data structure for storing assets which bundles up an asset
        # category and storage and lookup repository for the operation.
        storeAssetStruct = maxon.StoreAssetStruct(assetCategoryId, self.repository, self.repository)

        # Use the convenience method AssetCreationInterface.CreateObjectAsset() to create and store an
        # object asset in one operation. The instantiation of a FileAsset for the object is hidden
        # away, and instead we deal directly with the AssetDescription which is representing the object
        # file asset.
        assetDescription = maxon.AssetCreationInterface.CreateObjectAsset(
            objectVaraint, doc, storeAssetStruct, assetId, assetName, assetVersion, assetMetadata, True)

        metadata = assetDescription.GetMetaData()
        
        imagepUrl = maxon.Url("C:/Users/Till Niese/picture.png")

        # Stores the Url Meta Data, if there was no preview stored previously (aka the default png file for the given category) then it will be refreshed right away
        # Otherwise Cinema 4D need to be restarted
        maxon.AssetDescriptionInterface.StoreUrlMetaData = AssetDescriptionInterface.StoreUrlMetaData
        assetDescription.StoreUrlMetaData(maxon.ASSETMETADATA.ASSET_PREVIEWIMAGEURL, imagepUrl, maxon.AssetMetaData.KIND.PERSISTENT)
        
        # Turn off automatic preview
        metaProperties = metadata.Get(maxon.ASSETMETADATA.MetaProperties)
        metaProperties.Set(maxon.ASSET.METAPROPERTIES.BASE.AUTOMATICPREVIEW, False)
        assetDescription.StoreMetaData(maxon.ASSETMETADATA.MetaProperties, metaProperties, maxon.AssetMetaData.KIND.PERSISTENT)

Right after the imports I have added this:

# --- AssetDescriptionInterface monkey patching fix for AssetDescriptionInterface.StoreUrlMetaData() --------------------------
@maxon.interface.MAXON_INTERFACE(maxon.consts.MAXON_REFERENCE_COPY_ON_WRITE, "net.maxon.interface.assetdescription")
class AssetDescriptionInterface(maxon.AssetBaseInterface):

    @maxon.interface.MAXON_FUNCTION_EXTEND("net.maxon.interface.assetdescription.StoreUrlMetaData")
    def StoreUrlMetaData(self, metaId, source, kind):
        return self.GetRepository().StoreUrlMetaData(self, metaId, source, kind)

maxon.AssetDescriptionInterface.StoreUrlMetaData = AssetDescriptionInterface.StoreUrlMetaData

@maxon.MAXON_REGISTRY("net.maxon.registry.assetrepositorytypes")
class AssetRepositoryTypes(maxon.Registry):
    AssetDatabase = maxon.MAXON_DECLARATION("net.maxon.assets.repositorytype.database")

Kind regards,
Till

Hey @till-niese,

As said before, the way you (and Maxime) are treating asset thumbnails is not really intended as you copied there a code example which had a different context. And if you do it like this, you must invalidate the thumbnail caches which you cannot in Python as you lack the interfaces for that.

When you want to invoke a preview rendering, you must use AssetCreationInterface.AddPreviewRenderAsset. All other ways are not intended, and you must use C++ and have a good knowledge of the Asset API for these 'non-intended' ways to work.

But invoking AddPreviewRenderAsset is rarely necessary itself, as all newly added assets are automatically enqueued for thumbnail rendering. Thumbnail rendering is parallelized and wrapped with a job queue, making it impossible to force the Asset API to carry out thumbnailing at a certain point of time, one can only add a job for that (unless one uses the super manual approach I showed in the C++ examples). For the same reason it is also not possible to re-render thumbnails without manually flushing the thumbnail caches.

All in all, asset thumbnailing is fully automated, and the Asset API will make sure that thumbnails are generated when they are required. You can circumvent these mechanisms, for example when you want to regenerate a thumbnail based on metadata changes as shown in the C++ Asset API examples, but you will need C++ for that and be quite low-level.

When you encounter these black images, this is then a bug in the Asset API, as this should not happen. I would be best if you could provide an example scene and/or asset database for that. You can share data confidentially with the mail handle sdk_support(at)maxon(dot)net.

Cheers,
Ferdinand

Hi Maxime and Ferdinand,

Thank you so much for the extensive answers. I am working with @till-niese on this project. First perhaps to clarify what we are trying to do:

  1. We realize that the asset library creates a thumbnail on its own. However, that thumbnail is, in our case, basically just black and useless and even if it weren't, we need a custom thumbnail to match a specific style that has been requested by our customer.
  2. Therefore we are basically trying to replicate the functionality you get when you right-click on the thumbnail in the Asset Browser and Click "Update Thumbnail from File...".
  3. The current version of our automated asset creation does set the thumbnail using maxon.ASSETMETADATA.ASSET_PREVIEWIMAGEURL and that works. However, the automated thumbnail creating is also running (as you pointed out, @ferdinand). So if we set out thumbnail before the automated thumbnail creation is finished, it will get overwritten with what the preview render job comes up with.
  4. We currently work around the problem of our manual thumbnails being overwritten, by adding a pretty ten seconds sleep. That works, but it slows our conversion process down significantly and is likely very brittle.
  5. What is not really a problem for us is when an updated thumbnail does not immediately show in the Asset Browser. It's totally fine if the Asset Browser after our conversion shows the wrong thumbnails, as long as they are all fine after a restart of Cinema 4D.

Bottom line is that it's cool that the asset browser automatically creates thumbnails, but unfortunately those are useless for us because they are not to spec. What would be awesome:

  • Perhaps there is a way to just immediately kill the job that creates the thumbnail from being generated in the background or prevent that job from starting in the first place.
  • If that is not possible, maybe there is a message we could intercept to know the thumbnail has been updated so we can run our workaround right after the thumbnail creation job is finished instead of waiting a hardcoded amount of time.
  • Maybe there is another option that we're not clearly seeing right now?

Thank you again for your help so far! Very much appreciated!

Best
Timm

Hello @tdapper,

I still do not fully understand what you want to do, but in general, my answer does not change much. I do not want to be rude here, but in C++ you have everything you need. And what you want to do here, is fundamentally interfere with how the Asset API works. Such low-level access is traditionally the domain of the C++ API and not the Python API.

Bottom line is that it's cool that the asset browser automatically creates thumbnails [...]

That was less me showing off the features of the asset API and more pointing out its general approach.

Therefore we are basically trying to replicate the functionality you get when you right-click on the thumbnail in the Asset Browser and Click "Update Thumbnail from File...".

'Update Thumbnail from File...' will not prevent thumbnails from being overwritten either. So the only way to do so, would be to make the metadata entry read-only as pointed out in my first posting.

Perhaps there is a way to just immediately kill the job that creates the thumbnail from being generated in the background or prevent that job from starting in the first place.

If that is not possible, maybe there is a message we could intercept to know the thumbnail has been updated so we can run our workaround right after the thumbnail creation job is finished instead of waiting a hardcoded amount of time.

Maybe there is another option that we're not clearly seeing right now?

Neither your first nor second option are possible. The preview thumbnail job queue is non-public and there is no such thing as messages in the maxon API. There are Observables which realize the idea of events, but they are not exposed in Python nor are you able to stop a preview rendering with them.

Again, I do not want to be rude here, but as pointed out in my first posting, what you want to do is not intended. You could get hacky from the Python API, but that is more or less up to you.

982bbbbf-ca43-4f16-a85c-b9b5cb836a6f-image.png
Fig. 1: The physical location of thumbnails for local assets is always the same, net.maxon.asset.previewimageurl.meta.png.

With that knowledge one can infer the future pyhsical location of a thumbnail for a local asset.

    # Create a new object asset.
    asset: maxon.AssetDescription = maxon.AssetCreationInterface.CreateObjectAsset(
        obj, doc, storeAssetStruct, assetId, assetName, assetVersion, assetMetadata, True)

    # Get the physical location of the asset #asset, normally one should not touch 
    # AssetDescriptionInterface.GetUrl(). Then get the directory of that path and infer the
    # preview thumbnail location from it.
    assetUrl: maxon.Url = asset.GetUrl()
    assetPath: maxon.Url = maxon.Url(assetUrl.GetSystemPath())
    previewFile: maxon.Url = assetPath + maxon.Url("net.maxon.asset.previewimageurl.meta.png")

    # Print out the paths, #previewFile won't yet exist at this point, because the thumbnailing is
    # parallelized. 
    print (f"{assetUrl = }")
    print (f"{assetPath = }")
    print (f"{previewFile = }")
    print (f"{os.path.exists(previewFile.GetUrl()) = }")

Example output:

assetUrl = file:///C:/Users/f_hoppe/AppData/Roaming/Maxon/2023.1.3_97ABE84B/userrepository/object_a2c9ff14d748481fb9a8ae03d7bfa9b7/1/asset.c4d
assetPath = file:///C:/Users/f_hoppe/AppData/Roaming/Maxon/2023.1.3_97ABE84B/userrepository/object_a2c9ff14d748481fb9a8ae03d7bfa9b7/1/
previewFile = file:///C:/Users/f_hoppe/AppData/Roaming/Maxon/2023.1.3_97ABE84B/userrepository/object_a2c9ff14d748481fb9a8ae03d7bfa9b7/1/net.maxon.asset.previewimageurl.meta.png
os.path.exists(previewFile.GetUrl()) = False

With that knowledge you could start messing with that file. But that is obviously out of scope of support and a hack. Otherwise you will have to use the C++ API or file a bug report for your thumbnails being black in the first place.

Cheers,
Ferdinand