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:
-
Get an array of all the individual pixels (Color3 values) from the PNG image file via CloneTrooper1019’s PNG Module which uses binary contents.
-
Loop through all those pixels and insert the pixel color and pixel alpha values into their own arrays.
-
Loop through both the colors array and the alphas arrays and convert the values into two large compressed strings with a string compressor module.
-
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