How do I reduce the constant lag of this plugin when loading PNG image data?

So I have this plugin that imports PNG images into instances that uses compressed strings in attributes to store the Image data for my pixel game engine module

Importing images under 150x150 doesn’t seem to lag much at all, but when importing PNG images any higher than 200x200, that’s when performance really tips. The framerate tends to stay below 40 or even lower at some parts, and processing each image can take about half a minute. This is a huge problem for mass importing images for videos/GIFs.

Here’s the current process for importing a PNG image file:

  1. Get an array of all the individual pixels (Color3 values) from the PNG image file via CloneTrooper1019’s PNG Module which uses binary contents.

  2. Loop through all those pixels and insert the pixel color and pixel alpha values into their own arrays.

  3. Loop through both the colors array and the alphas arrays and convert the values into two large compressed strings with a string compressor module.

  4. Create the save object with attributes containing the compressed string data


How can I improve and optimse this procedure? Help would be greatly appreciated as always!


Here’s a roblox file of the whole plugin including the modules used
ImportFromPNGPlugin.rbxm (24.5 KB)

Here’s the main plugin code with most of the performance issues:

local StudioService = game:GetService("StudioService")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local SelectionService = game:GetService("Selection")
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")
local CoreGui = game:GetService("CoreGui")

local PNGReader = require(script.Parent.png)
local StringCompressor = require(script.Parent.StringCompressor)

local ResolutionChunkLimit = Vector2.new(256, 256)
local InstantCreate = false

local Importing = false
local Canceled = false

-- Toolbar (use the same toolbar if one already exist)
local Toolbar = plugin:CreateToolbar("CanvasDraw Tools")
local ToolbarCombiner = CoreGui:FindFirstChild("CanvasDrawToolsToolbar")

-- Button
local CreateSaveObjectButton = Toolbar:CreateButton("Create SaveObjects", "Create SaveObjects from PNG files on your computer (Image resolution limit: 256 x 256)", "rbxassetid://10461213070", "Create SaveObjects")

local MainFolder = script.Parent
local MessagesGui = MainFolder:WaitForChild("CanvasDrawMessageGui")

local CancelButton = MessagesGui:WaitForChild("CancelButton")
local TimeLeftText = MessagesGui:WaitForChild("TimeText")
local SizeText = MessagesGui:WaitForChild("SizeText")

local MFloor = math.floor


-- Main

local CurrentEstimatedTimeLeft = nil

local FastWaitCount = 0

local function FastWait(Count) -- Avoid lag spikes
	if FastWaitCount >= Count then
		FastWaitCount = 0
		RunService.Heartbeat:Wait()
	else
		FastWaitCount += 1
	end
end

local function RoundDecimals(Number, Places)
	local mult = 10 ^ Places
	Number = math.floor(Number * mult) / mult
	return Number
end

local function ConvertColoursToListString(Colours)
	local ColoursListString = ""

	for i, Colour in pairs(Colours) do
		if Canceled then
			break
		end
		
		local ColourR = MFloor(Colour.R * 255)
		local ColourG = MFloor(Colour.G * 255)
		local ColourB = MFloor(Colour.B * 255)

		local StringSegment = tostring(ColourR) .. "," .. tostring(ColourG) .. "," .. tostring(ColourB)
		ColoursListString = ColoursListString .. StringSegment
		
		if i ~= #Colours then
			ColoursListString = ColoursListString .. "S"
		end

		if not InstantCreate then
			FastWait(200)
		end
	end

	return ColoursListString
end

local function ConvertAlphasToListString(Alphas)
	local AlphasListString = ""

	for i, Alpha in pairs(Alphas) do
		if Canceled then
			break
		end
		
		AlphasListString = AlphasListString .. Alpha
		
		if i ~= #Alphas then
			AlphasListString = AlphasListString .. "S"
		end

		if not InstantCreate then
			FastWait(500)
		end
	end

	return AlphasListString
end

local function CreateMessageText(TextLabel, LifeTime)
	local NewLabel = TextLabel:Clone()
	NewLabel.Name = "TemporaryMessageText"
	NewLabel.Parent = MessagesGui
	NewLabel.Visible = true
	
	Debris:AddItem(NewLabel, LifeTime)
end


CreateSaveObjectButton.Click:Connect(function()
	if not Importing then
		Importing = true
		local ImageFilesArray = StudioService:PromptImportFiles({"png"})
		
		local CreatedSaveObjects = {}
		
		local CurrentTotalFileSize = 0
		
		local SaveObjParent = SelectionService:Get()[1]
		
		local function ParentSaveObjects(LoadStartTime)
			if not Canceled then
				SelectionService:Set(CreatedSaveObjects)

				MessagesGui.SuccessText.Text = "<b>SaveObject</b> successfully created! (Parent: " .. SaveObjParent.Name .. ")"
				CreateMessageText(MessagesGui.SuccessText, 5)
				
				local GeneratedFileSizeText = ""
				
				if CurrentTotalFileSize > 1048575 then 
					GeneratedFileSizeText = tostring(RoundDecimals(CurrentTotalFileSize / 1048575, 2)) .. " MB"
				elseif CurrentTotalFileSize > 1023 then 
					GeneratedFileSizeText = tostring(RoundDecimals(CurrentTotalFileSize / 1023, 2)) .. " KB"
				else
					GeneratedFileSizeText = tostring(RoundDecimals(CurrentTotalFileSize, 2)) .. " Byes"
				end
				
				print("Finished creating SaveObject(s)! (Time taken: " .. os.clock() - LoadStartTime .. "s)")
				print("Total File Size of Generated SaveObjects: " .. GeneratedFileSizeText)
			end
		end
		
		CancelButton.Visible = true
		TimeLeftText.Visible = true
		SizeText.Visible = true
	
		if ImageFilesArray and #ImageFilesArray > 0 then
			local LoadStartTime = os.clock()
			
			local function ProcessImageFile(ImageFile, ImageIndex)
				local ProcessSingleStartTime = os.clock()
				
				local ImageData = PNGReader.new(ImageFile:GetBinaryContents()) -- Extract the image data from the file

				if ImageData then
					if ImageData.Width > ResolutionChunkLimit.X then
						warn("Failed to load image (Resolution too large. Please keep the image resolution under 256 x 256)")
						return  
					end

					if ImageData.Height > ResolutionChunkLimit.Y then
						warn("Failed to load image (Resolution too large. Please keep the image resolution under 256 x 256)")
						return
					end

					MessagesGui.LoadingText.Text = "Loading <b>" .. ImageFile.Name .. "</b> (" .. ImageIndex .." out of " .. #ImageFilesArray .. "). Please wait..."
					MessagesGui.LoadingText.Visible = true
					
					if CurrentEstimatedTimeLeft then
						if CurrentEstimatedTimeLeft > 60 then
							TimeLeftText.Text = " Estimated Time Left <b>" .. math.round(CurrentEstimatedTimeLeft / 60) .." mins</b>"
						else
							TimeLeftText.Text = " Estimated Time Left <b>" .. CurrentEstimatedTimeLeft .." secs</b>"
						end
					else
						TimeLeftText.Text = "Estimated Time Left <b>Calculating...</b>"
					end

					-- Get all colours from image
					local ImageColours = {}
					local ImageAlphas = {}

					for Y = 1, ImageData.Height do
						for X = 1, ImageData.Width do
							local PixelColour, Alpha = ImageData:GetPixel(X, Y)
							table.insert(ImageColours, PixelColour)
							table.insert(ImageAlphas, Alpha)
						end
					end
					--print("Creating SaveObject Data. Please wait...")
					local ColoursDataString = ConvertColoursToListString(ImageColours)
					local AlphasDataString = ConvertAlphasToListString(ImageAlphas)
					
					CurrentTotalFileSize += #ColoursDataString + #AlphasDataString
					
					if CurrentTotalFileSize > 1048575 then 
						SizeText.Text = "Total Size: <b>" .. RoundDecimals(CurrentTotalFileSize / 1048575, 1) .. " MB</b>"
					elseif CurrentTotalFileSize > 1023 then 
						SizeText.Text = "Total Size: <b>" .. RoundDecimals(CurrentTotalFileSize / 1023, 1) .. " KB</b>"
					else
						SizeText.Text = "Total Size: <b>" .. RoundDecimals(CurrentTotalFileSize, 1) .. " B</b>"
					end
					
					MessagesGui.LoadingText.Visible = false
					
					if not Canceled then
						local CompressedColoursDataString = StringCompressor.Compress(ColoursDataString)
						local CompressedAlphasDataString = StringCompressor.Compress(AlphasDataString)

						-- Create the save object
						local NewSaveObject = Instance.new("Folder")
						NewSaveObject.Name = ImageFile.Name

						NewSaveObject:SetAttribute("ImageColours", CompressedColoursDataString)
						NewSaveObject:SetAttribute("ImageAlphas", CompressedAlphasDataString)
						NewSaveObject:SetAttribute("ImageResolution", Vector2.new(ImageData.Width, ImageData.Height))
						
						if SaveObjParent then
							NewSaveObject.Parent = SaveObjParent
						else
							NewSaveObject.Parent = workspace
							SaveObjParent = workspace
						end

						table.insert(CreatedSaveObjects, NewSaveObject)
					end
					CurrentEstimatedTimeLeft = math.round((os.clock() - ProcessSingleStartTime) * (#ImageFilesArray - ImageIndex))
				else
					CreateMessageText(MessagesGui.FailToImportText, 10)
					warn("Failed to create SaveObect. The chosen PNG file is invalid")
				end
			end
			
			for i, ImageFile in pairs(ImageFilesArray) do -- Loop through each PNG file
				ProcessImageFile(ImageFile, i)
			end
			
			ParentSaveObjects(LoadStartTime)
		else
			CreateMessageText(MessagesGui.CanceledText, 2)
			print("Canceled")
		end
	end
	
	CurrentEstimatedTimeLeft = nil
	
	Importing = false
	Canceled = false
	CancelButton.Visible = false
	SizeText.Visible = false
	TimeLeftText.Visible = false
end)

CancelButton.MouseButton1Click:Connect(function()
	if Importing then
		Canceled = true
	end
end)

plugin.Unloading:Connect(function()
	MessagesGui.Parent = MainFolder
end)

MessagesGui.Parent = CoreGui