Converting Decal IDs to Image IDs

Why Conversion is Necessary

If you’ve ever had a script set a texture or image property to a decal asset URI, you’d notice that it fails to load. That’s because decals and images are actually separate asset types, and properties that would expect an image asset will fail to load a decal asset. Decal assets refer to decal instances (similar to how model assets refer to model instances) while image assets refer to actual PNGs. Thus, if you want a system that displays user-submitted decals, you’ll have to convert the decal assets into image assets before you can actually display them.

Most users don’t actually know decals and images are separate things because studios does a good job of hiding that. For example, pasting a decal asset ID into an image asset property will automatically format it into its corresponding image asset. In addition, copying a decal’s asset ID from toolbox will actually give you its image asset ID. The automatic conversion is convenient, but it only works within studios’ property window; using a script to set a decal asset ID will not perform a conversion, so its necessary to have to translate it over to an image first.

Unfortunately, Roblox does not provide a convenient method that translates decal to image, so several “hacks” were made to tackle the issue. I wanted to catalog each method I’ve found along with its pros and cons, but I’ve also put together what I think is the most reliable method at the end. So, here are the 4 different methods of converting a decal to an image:

Using a Thumbnail

Using Roblox’s thumbnail format is probably the most used method. Using it is as simple as replacing your decal asset URI with rbxthumb://type=Asset&id=DECAL_ID&w=420&h=420 (replacing DECAL_ID with your decal asset id). The unique thing about this is that it’s able to be performed entirely on the client; other methods can only be performed on the server (but you can use a remote to request the image ID.

The downfall to this is that the resulting image is capped at a 420x420 resolution, which is a far cry from an image’s max resolution size of 1024x1024. You’re also not actually getting the image ID from this. Still, it’s a fair method if you don’t mind the potential quality loss.

Using a Web API

If you’ve ever wondered how BTRoblox is able to fetch the image asset ID from a decal asset, this is how. The assetdelivery API has an endpoint that fetches the actual content file given an asset ID. For example, providing an image asset ID returns a PNG file, and providing a decal asset ID returns an RBXMX file.
If you’re unaware, an RBXMX file describes a Roblox model, formatted in XML. Giving it a decal returns information describing a decal instance but also includes its Texture property which contains the image asset URI. So doing some string matching, you get this code:

local httpService = game:GetService("HttpService")
local xmlResponse = httpService:GetAsync("https://assetdelivery.roblox.com/v1/asset/?id=DECAL_ID")
local imageUrl = xmlResponse:match("<url>(.-)</url>")

However, as with all methods using Roblox’s web API comes the common issues concerning it. Roblox doesn’t allow scripts to request anything from their domain, so you’d have to use a proxy like RoProxy. That also means you’re putting reliance on a third-party service which can fail, but you can always fall back to another method if that ever happens. You’ll also need to allow HTTP requests to be enabled, which could open up security issues.

Another convenient API called Rbxdecal also converts decal IDs to images. If you want to use that instead, a script for that can look like:

local httpService = game:GetService("HttpService")
local imageId = httpService:GetAsync("https://rbxdecal.glitch.me/DECAL_ID")

It’s a little cleaner, but Rbxdecal does have a rate limit of 65 requests per minute while RoProxy does not.

Inserting the Decal

Because decal assets refer to an instance, you can insert it into your game and grab its Texture property. There are actually 2 ways to do this via code. The first method uses InsertService:LoadAsset. A script for that can look like this:

local insertService = game:GetService("InsertService")
local decalModel = insertService:LoadAsset(DECAL_ID)
local imageUrl = decalModel:FindFirstChildWhichIsA("Decal").Texture

Strangely, decals are not considered to be benign assets. Therefore this method can only work if you or Roblox own the decal.

The second method uses DataModel:GetObjects and is what plugins like Imiji uses. It’s very similar to LoadAsset in use:

local decal = game:GetObjects("rbxassetid://DECAL_ID")[1]
local imageUrl = decal.Texture

This method works on any public decal regardless if you own it or not. Unfortunately, this method is only accessible to the command bar and plugins only. Which means scripts can’t use it. So it’s only beneficial to plugin authors.

I don’t suggest using either of these methods at all because it’s very limited in how it can be used.

Brute Force

This method decrements the decal ID until it hits an image asset by continually checking it with MarketplaceService:GetProductInfo. In my opinion, this is one of the hackiest methods, but I’m including it anyway for completeness.

local marketplaceService = game:GetService("MarketplaceService")
local imageId = DECAL_ID
local creatorId = marketplaceService:GetProductInfo(imageId).Creator.CreatorTargetId
while true do
	imageId -= 1
	local productInfo = marketplaceService:GetProductInfo(imageId)
	if productInfo.AssetTypeId == Enum.AssetType.Image.Value and productInfo.Creator.CreatorTargetId == creatorId then
		break
	end
end
-- after the loop, imageId will be the actual image id

This method doesn’t rely on third parties, works on any public decal, and is able to retain original image qualities. In exchange, this method is very slow. There’s no defined limit on how far off a decal ID is from its image ID, so it could take 50 guess-and-checks until it finds the image ID (I’m exaggerating, but my point still stands. Most ID gaps are around 10 but I’ve seen cases where it can go up to 25). For that reason, I don’t suggest using this method either.

The Best Method?

The web API and thumbnail approach come pretty close to being the best methods (inserting a decal only works in certain cases and brute forcing is slow), but the web API has the potential for failure, and the thumbnail method has a capped resolution. Therefore, I believe having the web API fall back to the thumbnail method to make up for its potential failure is the best method. For the most part, the web API should give back the real image ID but in the rare case it fails, an alternative but lower-quality URI will still be presented.

Written in code, it’ll look like this:

local HttpService = game:GetService("HttpService")

local function decalToImage(decalId: number): string
	-- Do a protected call to HttpService
	local success, response = pcall(HttpService.GetAsync, HttpService, `https://assetdelivery.roproxy.com/v1/asset/?id={decalId}`)
	if success then
		-- Extracts the image URI from the XML file
		return response:match("<url>(.-)</url>")
	else
		-- Falls back on thumbnail if the API fails
		warn(response)
		return `rbxthumb://type=Asset&id={decalId}&w=420&h=420`
	end
end

The only major concern with this is the fact it requires HTTP requests to be enabled, which can open up security vulnerabilities. If you’re uncomfortable with that, stick with only using the thumbnail method.

By the way, if you’re taking in user-submitted decal assets, I suggest using MarketplaceService:GetProductInfo to check if it really is a decal or image asset before calling decalToImage since it assumes you’re giving a valid decal asset ID. You can also use some string patterns to help extract the asset ID given an asset URI. I’ve written this optional, longer version of code that helps account for all that:

Extended Decal to Image
local HttpService = game:GetService("HttpService")
local MarketplaceService = game:GetService("MarketplaceService")

local function decalToImage(decalId: number): string
	-- Do a protected call to HttpService
	local success, response = pcall(HttpService.GetAsync, HttpService, `https://assetdelivery.roproxy.com/v1/asset/?id={decalId}`)
	if success then
		-- Extracts the image url from the XML file
		return response:match("<url>(.-)</url>")
	else
		-- Falls back on thumbnail if the api fails
		warn(response)
		return `rbxthumb://type=Asset&id={decalId}&w=420&h=420`
	end
end

-- Checks the asset type given an asset id
local function safeDecalToImage(decalId: number): string?
	local productInfo = MarketplaceService:GetProductInfo(decalId)
	local assetTypeId = productInfo.AssetTypeId
	
	if assetTypeId == Enum.AssetType.Decal.Value then
		-- Confirmed to be a decal id, convert it to an image
		return decalToImage(decalId)
	elseif assetTypeId == Enum.AssetType.Image.Value then
		-- Decal id is actually an image id, so just return that
		return `rbxassetid://{decalId}`
	end
	
	return nil
end

-- There's 5 common ways to express an asset string, this looks for the asset id in each of those ways
local function extractAssetId(asset: string): number?
	asset = asset:lower() -- Makes the match case-insensitive by lowercasing everything
	local assetId = asset:match("^(%d+)$") or -- DECAL_ID
		asset:match("^rbxassetid://(%d+)$") or -- rbxassetid://DECAL_ID
		asset:match("^http://www%.roblox%.com/asset/%?id=(%d+)$") or -- http://www.roblox.com/asset/?id=DECAL_ID
		asset:match("^https://www%.roblox%.com/library/(%d+)") or -- https://www.roblox.com/library/DECAL_ID
		asset:match("^https://create%.roblox%.com/marketplace/asset/(%d+)") -- https://create.roblox.com/marketplace/asset/DECAL_ID
	
	if assetId then
		-- tonumber errors when given no arguments
		return tonumber(assetId)
	end
	return nil
end

-- Example use
local imageAsset = safeDecalToImage(extractAssetId(textBox.Text))
21 Likes

Really get break down with multiple avenues. This does a great job of preemptively answering “what about this approach” questions and introducing those approaches to fully flesh out the concept.

this is so awesome
i used this in p6 rig and it worked extremely fine
Thanks!!!