Need help getting RichText formatting colors from a TextLabel onto multiple ImageLabels that represent each letter

I have a system in place that allows me to have custom fonts and to move or recolor each piece individually since it recreates the text of a TextLabel into a frame full of ImageLabels for each letter. (Which it pulls from a sprite sheet)

So think of it this way I’m basically coding roblox’s rich text formatting system from scratch (specifically for color), where I have an ImageLabel for each letter and I want to recolor the letters based on a string of rich formatted text that I copied from a TextLabel.

So here’s an example string:


Here’s what the setup looks like in studio
image
and here’s what that looks like displayed in-game (it also has a wavy effect by tweening each letter)
image

Now I want to recolor these letters to match that rich formatted text example above. How would I go about that?
I tried splitting the string but it never worked quite properly. I need a way to get JUST the RGB values, a way to get just the words EFFECTED by the RGB values, and to also find the start and end positions of the (characters/graphemes) of the words that will be recolored, a start and end position basically, and then what I would do is loop through that start and end position with a for loop and find the numbered frames with matching names and change the ImageColor of the ImageLabels inside those frames. I just can’t find an efficient way to pull apart the formatted string to get data for what characters should be what color.

i made a function for it takes rich text and a frame with imagelabel as input and parses the rich text to find color tag

local function applyRichTextColors(richText, frameWithImageLabels)
    local function parseAndApply()
        local plainText = ""
        local currentIndex = 1

        while true do
            local tagStart, tagEnd = richText:find("<font%s+color=\"rgb%((%d+,%d+,%d+)%)\">", currentIndex)
            if not tagStart then
                plainText = plainText .. richText:sub(currentIndex)
                break
            end

            plainText = plainText .. richText:sub(currentIndex, tagStart - 1)
            
            local r, g, b = richText:match("(%d+),(%d+),(%d+)", tagStart)
            local color = Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))

            local contentStart = tagEnd + 1
            local contentEnd = richText:find("</font>", contentStart, true) - 1
            local content = richText:sub(contentStart, contentEnd)

            local startPosition = #plainText + 1
            local endPosition = #plainText + #content

            for i = startPosition, endPosition do
                local frame = frameWithImageLabels:FindFirstChild(tostring(i))
                if frame and frame:IsA("Frame") then
                    local imageLabel = frame:FindFirstChildOfClass("ImageLabel")
                    if imageLabel then
                        imageLabel.ImageColor3 = color
                    end
                end
            end

            plainText = plainText .. content
            currentIndex = contentEnd + 7  -- 7 is the length of "</font>" so i don't forget 
        end

        return plainText
    end

    local plainText = parseAndApply()
    return plainText
end

local richText = '<font color="rgb(255,0,0)">This</font> is a <font color="rgb(0,255,0)">heavy work</font> in <font color="rgb(0,0,255)">progress</font>.'
local frameWithImageLabels = --Your frame containing the ImageLabels
local plainText = applyRichTextColors(richText, frameWithImageLabels)
print("Plain text:", plainText)
1 Like

THANK YOU, I’ll give this a try soon

So it’s not unformatting the text properly, the plainText string it returns still has the rich text formatting on it. Also the letter images my system creates are based on an unformatted version of the rich text. What I am trying to do is get the rich text unformatted while having a system in place that can find what letters to recolor in the unformatted text. So basically keep score of the start and end positions of each section of the string that is being effected by the formatting (so stuff between the > and <), remove the formatting while also removing what was removed from the formatting from that score, to then find what graphemes/letters to recolor.

I also tried printing your rgb values and its not printing anything in the output at all
image

can you try this

local function applyRichTextColors(richText, frameWithImageLabels)
    local function parseAndApply()
        local plainText = ""
        local colorSegments = {}
        local currentIndex = 1

        while true do
            local tagStart, tagEnd = richText:find("<font%s+color=\"rgb%((%d+),(%d+),(%d+)%)\">", currentIndex)
            if not tagStart then
                plainText = plainText .. richText:sub(currentIndex)
                break
            end

            plainText = plainText .. richText:sub(currentIndex, tagStart - 1)
            
            local r, g, b = richText:match("(%d+),(%d+),(%d+)", tagStart)
            local color = Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))

            local contentStart = tagEnd + 1
            local contentEnd = richText:find("</font>", contentStart, true) - 1
            local content = richText:sub(contentStart, contentEnd)

            table.insert(colorSegments, {
                startPosition = #plainText + 1,
                endPosition = #plainText + #content,
                color = color
            })

            plainText = plainText .. content
            currentIndex = contentEnd + 7
        end

        return plainText, colorSegments
    end

    local plainText, colorSegments = parseAndApply()

    for _, segment in ipairs(colorSegments) do
        for i = segment.startPosition, segment.endPosition do
            local frame = frameWithImageLabels:FindFirstChild(tostring(i))
            if frame and frame:IsA("Frame") then
                local imageLabel = frame:FindFirstChildOfClass("ImageLabel")
                if imageLabel then
                    imageLabel.ImageColor3 = segment.color
                end
            end
        end
    end

    return plainText
end

local richText = '<font color="rgb(255,0,0)">This</font> is a <font color="rgb(0,255,0)">heavy work</font> in <font color="rgb(0,0,255)">progress</font>.'
local frameWithImageLabels = -- Frame withen ImageLabels
local plainText = applyRichTextColors(richText, frameWithImageLabels)
print("Plain text:", plainText)

local r, g, b = richText:match("rgb%((%d+),(%d+),(%d+)%)")
if r and g and b then
    print("RGB values:", r, g, b)
else
    print("No RGB values found")
end
1 Like

I already have it implemented into the dialog system, when I view my log (which uses regular text labels with the rich text formatting) it shows the colors that the text should be. It’s not recoloring them, its not printing that they it found colors, and the plain text it is printing is actually the formatted text.

it seems it won’t parsing the rich text format I improved the regex and added some debugging could u provide me those ? if it isn’t solved

local function applyRichTextColors(richText, frameWithImageLabels)
    local function parseAndApply()
        local plainText = ""
        local colorSegments = {}
        local currentIndex = 1

        while true do
            local tagStart, tagEnd = richText:find("<font%s+color=\"rgb%((%d+),%s*(%d+),%s*(%d+)%)\">", currentIndex)
            if not tagStart then
                plainText = plainText .. richText:sub(currentIndex)
                break
            end

            plainText = plainText .. richText:sub(currentIndex, tagStart - 1)
            
            local r, g, b = richText:match("(%d+),%s*(%d+),%s*(%d+)", tagStart)
            local color = Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))

            local contentStart = tagEnd + 1
            local contentEnd = richText:find("</font>", contentStart, true)
            if not contentEnd then break end
            contentEnd = contentEnd - 1
            local content = richText:sub(contentStart, contentEnd)

            table.insert(colorSegments, {
                startPosition = #plainText + 1,
                endPosition = #plainText + #content,
                color = color
            })

            plainText = plainText .. content
            currentIndex = contentEnd + 7
        end

        return plainText, colorSegments
    end

    local plainText, colorSegments = parseAndApply()

    for _, segment in ipairs(colorSegments) do
        for i = segment.startPosition, segment.endPosition do
            local frame = frameWithImageLabels:FindFirstChild(tostring(i))
            if frame and frame:IsA("Frame") then
                local imageLabel = frame:FindFirstChildOfClass("ImageLabel")
                if imageLabel then
                    imageLabel.ImageColor3 = segment.color
                    print("Applied color", segment.color, "to letter at position", i)
                end
            end
        end
    end

    return plainText
end

local richText = '<font color="rgb(255,0,0)">This</font> is a <font color="rgb(0,255,0)">heavy work</font> in <font color="rgb(0,0,255)">progress</font>.'
local frameWithImageLabels = -- Your frame containing ImageLabels
local plainText = applyRichTextColors(richText, frameWithImageLabels)
print("Plain text:", plainText)

for match in richText:gmatch("rgb%((%d+),%s*(%d+),%s*(%d+)%)") do
    print("RGB values found:", match)
end
1 Like

You’re getting closer although its still having issues, here’s what happened and what it’s printing

I also edited this section of the code since it only would’ve worked for one word and not every word (reposting cause it didnt have comments on it)

if frameWithImageLabels ~= nil then
		for _, segment in ipairs(colorSegments) do
			for i = segment.startPosition, segment.endPosition do
				local letters = frameWithImageLabels:GetDescendants()
				for q = 1, #letters do --descendants of TextLabelDisplay
					if letters[q]:IsA("Frame") then
						if string.find(letters[q].Name, "Word") == nil then --if not a word frame, but a letter frame (which is named after the character/grapheme number)
							local frame = letters[q]
							local imageLabel = frame:FindFirstChildOfClass("ImageLabel")
							if imageLabel then
								imageLabel.ImageColor3 = segment.color
								print("Applied color", segment.color, "to letter at position", i)
							end
						end
					end
				end
			end
		end
	end

Update

local function applyRichTextColors(richText, frameWithImageLabels)
    local function parseAndApply()
        local plainText = ""
        local colorSegments = {}
        local currentIndex = 1

        while true do
            local tagStart, tagEnd = richText:find("<font%s+color=\"rgb%((%d+),%s*(%d+),%s*(%d+)%)\">", currentIndex)
            if not tagStart then
                plainText = plainText .. richText:sub(currentIndex):gsub("</?font>", "")
                break
            end

            plainText = plainText .. richText:sub(currentIndex, tagStart - 1):gsub("</?font>", "")
            
            local r, g, b = richText:match("(%d+),%s*(%d+),%s*(%d+)", tagStart)
            local color = Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))

            local contentStart = tagEnd + 1
            local contentEnd = richText:find("</font>", contentStart, true)
            if not contentEnd then break end
            contentEnd = contentEnd - 1
            local content = richText:sub(contentStart, contentEnd)

            table.insert(colorSegments, {
                startPosition = #plainText + 1,
                endPosition = #plainText + #content,
                color = color
            })

            plainText = plainText .. content
            currentIndex = contentEnd + 7
        end

        return plainText, colorSegments
    end

    local plainText, colorSegments = parseAndApply()

    if frameWithImageLabels then
        for _, segment in ipairs(colorSegments) do
            for i = segment.startPosition, segment.endPosition do
                local letters = frameWithImageLabels:GetDescendants()
                for _, letter in ipairs(letters) do
                    if letter:IsA("Frame") and not string.find(letter.Name, "Word") then
                        local imageLabel = letter:FindFirstChildOfClass("ImageLabel")
                        if imageLabel and tonumber(letter.Name) == i then
                            imageLabel.ImageColor3 = segment.color
                            print("Applied color", segment.color, "to letter at position", i, "frame", letter.Name)
                        end
                    end
                end
            end
        end
    end

    return plainText
end

local richText = '<font color="rgb(255,0,0)">This</font> is a <font color="rgb(0,255,0)">heavy work</font> in <font color="rgb(0,0,255)">progress</font>.'
local frameWithImageLabels = -- Your frame containing ImageLabels
local plainText = applyRichTextColors(richText, frameWithImageLabels)
print("Plain text:", plainText)

for match in richText:gmatch("rgb%((%d+),%s*(%d+),%s*(%d+)%)") do
    print("RGB values found:", match)
end

provide me the debugging in-case

1 Like

It’s not removing one of the > and one of the <, which is causing issues in my dialog system, and it also seems to be recoloring things wrong?

1 Like

It does the coloring correctly now, although it is still not removing the > and > from the text when unformatting it, which causes it to display wrong with my dialog system and cuts off everything after the >

look at the plaintext print

1 Like

got it i added a gsub for that

local function applyRichTextColors(richText, frameWithImageLabels)
    local function parseAndApply()
        local plainText = ""
        local colorSegments = {}
        local currentIndex = 1

        while true do
            local tagStart, tagEnd = richText:find("<font%s+color=\"rgb%((%d+),%s*(%d+),%s*(%d+)%)\">", currentIndex)
            if not tagStart then
                plainText = plainText .. richText:sub(currentIndex):gsub("</?font>", "")
                break
            end

            plainText = plainText .. richText:sub(currentIndex, tagStart - 1):gsub("</?font>", "")
            
            local r, g, b = richText:match("(%d+),%s*(%d+),%s*(%d+)", tagStart)
            local color = Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))

            local contentStart = tagEnd + 1
            local contentEnd = richText:find("</font>", contentStart, true)
            if not contentEnd then break end
            contentEnd = contentEnd - 1
            local content = richText:sub(contentStart, contentEnd)

            table.insert(colorSegments, {
                startPosition = #plainText + 1,
                endPosition = #plainText + #content,
                color = color
            })

            plainText = plainText .. content
            currentIndex = contentEnd + 7
        end

        plainText = plainText:gsub("<[^>]+>", "")

        return plainText, colorSegments
    end

    local plainText, colorSegments = parseAndApply()

    if frameWithImageLabels then
        for _, segment in ipairs(colorSegments) do
            for i = segment.startPosition, segment.endPosition do
                local letters = frameWithImageLabels:GetDescendants()
                for _, letter in ipairs(letters) do
                    if letter:IsA("Frame") and not string.find(letter.Name, "Word") then
                        local imageLabel = letter:FindFirstChildOfClass("ImageLabel")
                        if imageLabel and tonumber(letter.Name) == i then
                            imageLabel.ImageColor3 = segment.color
                            print("Applied color", segment.color, "to letter at position", i, "frame", letter.Name)
                        end
                    end
                end
            end
        end
    end

    return plainText
end

local richText = '<font color="rgb(255,0,0)">This</font> is a <font color="rgb(0,255,0)">heavy work</font> in <font color="rgb(0,0,255)">progress</font>.'
local frameWithImageLabels = -- Your frame containing ImageLabels
local plainText = applyRichTextColors(richText, frameWithImageLabels)
print("Plain text:", plainText)

for match in richText:gmatch("rgb%((%d+),%s*(%d+),%s*(%d+)%)") do
    print("RGB values found:", match)
end
1 Like

I ran this and the two > are still there

1 Like

Just Updated

local function applyRichTextColors(richText, frameWithImageLabels)
    local function parseAndApply()
        local plainText = ""
        local colorSegments = {}
        local currentIndex = 1

        local cleanText = richText:gsub("<[^>]+>", "")

        while true do
            local tagStart, tagEnd = richText:find("<font%s+color=\"rgb%((%d+),%s*(%d+),%s*(%d+)%)\">", currentIndex)
            if not tagStart then
                plainText = plainText .. cleanText:sub(currentIndex)
                break
            end

            plainText = plainText .. cleanText:sub(currentIndex, tagStart - 1)
            
            local r, g, b = richText:match("(%d+),%s*(%d+),%s*(%d+)", tagStart)
            local color = Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))

            local contentStart = tagEnd + 1
            local contentEnd = richText:find("</font>", contentStart, true)
            if not contentEnd then break end
            contentEnd = contentEnd - 1
            local content = cleanText:sub(#plainText + 1, #plainText + contentEnd - contentStart + 1)

            table.insert(colorSegments, {
                startPosition = #plainText + 1,
                endPosition = #plainText + #content,
                color = color
            })

            plainText = plainText .. content
            currentIndex = contentEnd + 7
        end

        return plainText, colorSegments
    end

    local plainText, colorSegments = parseAndApply()

    if frameWithImageLabels then
        for _, segment in ipairs(colorSegments) do
            for i = segment.startPosition, segment.endPosition do
                local letters = frameWithImageLabels:GetDescendants()
                for _, letter in ipairs(letters) do
                    if letter:IsA("Frame") and not string.find(letter.Name, "Word") then
                        local imageLabel = letter:FindFirstChildOfClass("ImageLabel")
                        if imageLabel and tonumber(letter.Name) == i then
                            imageLabel.ImageColor3 = segment.color
                            print("Applied color", segment.color, "to letter at position", i, "frame", letter.Name)
                        end
                    end
                end
            end
        end
    end

    return plainText
end

local richText = '<font color="rgb(255,0,0)">This</font> is a <font color="rgb(0,255,0)">heavy work</font> in <font color="rgb(0,0,255)">progress</font>.'
local frameWithImageLabels = -- Your frame containing ImageLabels
local plainText = applyRichTextColors(richText, frameWithImageLabels)
print("Plain text:", plainText)

for match in richText:gmatch("rgb%((%d+),%s*(%d+),%s*(%d+)%)") do
    print("RGB values found:", match)
end
print("Final plain text (should have no HTML tags):", plainText)

could you try again?

1 Like

reallllly close but it cut off the k at the end of the plaintext

It specifically only cuts off that k, when used in other text it keeps the last character there

could u try out this

local function applyRichTextColors(richText, frameWithImageLabels)
    local function parseAndApply()
        local plainText = ""
        local colorSegments = {}
        local currentIndex = 1

        local cleanText = richText:gsub("<[^>]+>", "")

        while true do
            local tagStart, tagEnd = richText:find("<font%s+color=\"rgb%((%d+),%s*(%d+),%s*(%d+)%)\">", currentIndex)
            if not tagStart then
                plainText = plainText .. cleanText:sub(currentIndex)
                break
            end

            plainText = plainText .. cleanText:sub(currentIndex, tagStart - 1)
            
            local r, g, b = richText:match("(%d+),%s*(%d+),%s*(%d+)", tagStart)
            local color = Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))

            local contentStart = tagEnd + 1
            local contentEnd = richText:find("</font>", contentStart, true)
            if not contentEnd then 
                plainText = plainText .. cleanText:sub(contentStart)
                break 
            end
            contentEnd = contentEnd - 1
            local content = cleanText:sub(#plainText + 1, #plainText + contentEnd - contentStart + 1)

            table.insert(colorSegments, {
                startPosition = #plainText + 1,
                endPosition = #plainText + #content,
                color = color
            })

            plainText = plainText .. content
            currentIndex = contentEnd + 7
        end

        if #plainText < #cleanText then
            plainText = plainText .. cleanText:sub(#plainText + 1)
        end

        return plainText, colorSegments
    end

    local plainText, colorSegments = parseAndApply()

    if frameWithImageLabels then
        for _, segment in ipairs(colorSegments) do
            for i = segment.startPosition, segment.endPosition do
                local letters = frameWithImageLabels:GetDescendants()
                for _, letter in ipairs(letters) do
                    if letter:IsA("Frame") and not string.find(letter.Name, "Word") then
                        local imageLabel = letter:FindFirstChildOfClass("ImageLabel")
                        if imageLabel and tonumber(letter.Name) == i then
                            imageLabel.ImageColor3 = segment.color
                            print("Applied color", segment.color, "to letter at position", i, "frame", letter.Name)
                        end
                    end
                end
            end
        end
    end

    print("Clean text length:", #cleanText)
    print("Plain text length:", #plainText)
    print("Final plain text (should have no HTML tags):", plainText)

    return plainText
end

local richText = '<font color="rgb(255,0,0)">This</font> is a <font color="rgb(0,255,0)">heavy work</font> in <font color="rgb(0,0,255)">progress</font>.'
local frameWithImageLabels = -- Your frame containing ImageLabels
local plainText = applyRichTextColors(richText, frameWithImageLabels)
print("Plain text:", plainText)

for match in richText:gmatch("rgb%((%d+),%s*(%d+),%s*(%d+)%)") do
    print("RGB values found:", match)
end
1 Like