Defold PNG - Inspect, load and save PNG files

Not really sure how useful this is but I thought I’d share it anyway. Defold PNG is a Defold native extension that allows you to inspect, load and save PNG files. Usage:

-- load my.png from disk
local f = io.open("my.png", "rb")
local bytes = f:read("*a")

-- read the PNG header
local info = png.info(bytes)
print(info.width)
print(info.height)
print(info.colortype) -- png.RGB, png.RGBA, png.GREY, png.GREY_ALPHA or png.PALETTE
print(info.bitdepth)

-- decode the PNG to a Defold buffer in RGB(A) format
local buf, w, h = png.decode_rgb(bytes)
local buf, w, h = png.decode_rgba(bytes)

-- use buffer as a sprite texture
local resource_path = go.get("#sprite", "texture0")
local header = { width = w, height = h, type = resource.TEXTURE_TYPE_2D, format = resource.TEXTURE_FORMAT_RGBA, num_mip_maps = 1 }
resource.set_texture(resource_path, header, buf)

-- create 100x100 raw pixels of random colors
local w, h = 100, 100
local pixels = ""
for i=1,w*h do
	pixels = pixels .. string.char(math.random(1,255), math.random(1,255), math.random(1,255), 255)
end

--- encode raw RGB(A) pixels into a PNG
local bytes = png.encode_rgb(pixels, w, h)
local bytes = png.encode_rgba(pixels, w, h)

-- write bytes to foo.png
local f = io.open("foo.png", "wb")
f:write(bytes)
f:flush()
f:close()
19 Likes

Very useful, thanks! How difficult would it be to do something similar for GIFs? Because my game is 100% deterministic a super-stretch goal of mine is to record player input and when they solve a puzzle play back the key inputs to create a GIF of their solution that they can then tweet/whatever. Would be happy to look into extensions and do it myself if/when the time comes, just wondering if it’s even feasible.

4 Likes

You would probably want to save the game state and only make the GIF after with a custom render script. Use a render target to render everything to a texture, downscale each frame, then batch process them to produce the GIF. There are still mystery steps which I am not clear on doing still but with buffers and native extensions I think it should all be possible right now. Could also save every x frame textures downscaled to memory too and then produce the GIF at game over based on last few frames with a few seconds/msecs inbetween or based on when moves are made.

2 Likes

a game that generates replay gifs may go quite viral, from where I am.
Love the idea

6 Likes

Well this is pretty awesome, thanks a lot for making it! Just a little bit faster than decoding PNGs with pure Lua, haha. As a test I loaded a 17MB PNG (about 5000x3000px) and it decoded in just a tad over 1 second. This definitely opens up some possibilities.

One question though: is there some way to get a loaded PNG onto a GUI node? gui.new_texture and all the related functions require a string for the texture buffer . . .

Yes, but not using my extension. You can use:

local file = io.open("my.png", "rb")
local bytes = file:read("*a")
local png = image.load(bytes)
gui.new_texture("my.png", png.width, png.height, "rgba", png.buffer)

Complete example: publicexamples/examples/loadtexture/load_texture/loadtexture.gui_script at master · britzl/publicexamples · GitHub

Now, I could ofc add support for returning decoded image as a string instead of a buffer, but I reasoned that since image.load() exists there was no need for it.

6 Likes

Ohhh, bingo. Boy do I feel sheepish. I was trying to use resource.load or something when I tried this before. Thanks!

3 Likes

Amazing… I did the search “save to png” without any hope and found this!

How I can do the reverse: Get a render target and save as PNG? Respecting alpha channel.

1 Like

https://www.defold.com/community/projects/56489/ might help.

1 Like

Also here is a similar extension, but works with JPG too.

6 Likes

I use the extension to create wallpapers from my level backgrounds. Since they are biiiig, it takes forever.

What are my options? It’s a hidden objects game. Players must be able to zoom in pretty far and therefore the images are large, north (some very far north) of 2500×3500px. Right now I create the wallpapers by getting user’s window size, cropping from a defined centre, then scaling down the image to fit. Obviously smaller images would fix my issue, but let’s assume I would like to keep them as big as possible (but do lecture me anyway if I am way overboard). Right now I calculate something like twenty seconds per wallpaper, haven’t measured for sure but it FEELS like forever.

Can you restate what you are doing exactly? It’s not super clear to me.

1 Like

I use png.info to read the image dimensions, go over buffer and stream, to finally use png.encode_rgba to create and save the final wallpaper image. (I could post the script if it helps)
The operation takes a long time. Does the buffer take so long, the stream, or something else?

Sidenote, there apparently is another possible solution by rendering the buffer onto a new target. I must admit that render script changes scare me - I found them difficult to get to work - so I tried something else first, but if that’s the better solution I’ll look at it.

Can you share the piece of code doing this?


function M.unlock_wallpaper(level_id)
	local entry = M.get_level_entry(level_id)
	if not entry then return end

	local bytes = sys.load_resource(entry.bg)
	if not bytes then return end

	local info = png.info(bytes)

	local screen_w, screen_h = window.get_size()
	local crop_rect = M.compute_center_crop(level_id, screen_w, screen_h, info.width, info.height)
	if not crop_rect then return end

	local src_buf = png.decode_rgba(bytes)
	if not src_buf then return end

	local x, y, w, h = crop_rect.x, crop_rect.y, crop_rect.w, crop_rect.h

	local src = buffer.get_stream(src_buf, hash("pixels"))
	local out_buf = buffer.create(w * h, {
		{ name = hash("pixels"), type = buffer.VALUE_TYPE_UINT8, count = 4 }
	})
	local out = buffer.get_stream(out_buf, hash("pixels"))

	local out_i = 1
	for row = 0, h - 1 do
		local src_row = (y + (h - 1 - row)) * info.width + x
		local src_i = src_row * 4 + 1
		local copy_bytes = w * 4
		for i = 0, copy_bytes - 1 do
			out[out_i + i] = src[src_i + i]
		end
		out_i = out_i + copy_bytes
	end

	local target_w, target_h = window.get_size()

	-- never upscale (only shrink)
	if target_w > w then target_w = w end
	if target_h > h then target_h = h end

	local png_bytes

	if target_w ~= w or target_h ~= h then
		local src_scaled = buffer.get_stream(out_buf, hash("pixels"))

		local scaled_buf = buffer.create(target_w * target_h, {
			{ name = hash("pixels"), type = buffer.VALUE_TYPE_UINT8, count = 4 }
		})
		local dst = buffer.get_stream(scaled_buf, hash("pixels"))

		for yy = 0, target_h - 1 do
			local sy = math.floor(yy * h / target_h)
			for xx = 0, target_w - 1 do
				local sx = math.floor(xx * w / target_w)

				local si = (sy * w + sx) * 4 + 1
				local di = (yy * target_w + xx) * 4 + 1

				dst[di]     = src_scaled[si]
				dst[di + 1] = src_scaled[si + 1]
				dst[di + 2] = src_scaled[si + 2]
				dst[di + 3] = src_scaled[si + 3]
			end
		end

		local scaled_pixels = buffer.get_bytes(scaled_buf, hash("pixels"))		
		png_bytes = png.encode_rgb(scaled_pixels, target_w, target_h)
	else
		local pixels = buffer.get_bytes(out_buf, hash("pixels"))
		png_bytes = png.encode_rgb(pixels, w, h)
	end

	local filename = ("wallpaper_%s.png"):format(level_id)
	local path = sys.get_save_file("A Thousand Bees Wallpapers", filename)	

	local f = io.open(path, "wb")
	if not f then return end
	f:write(png_bytes)
	f:flush()
	f:close()

	print("WALLPAPER: unlocked ->", path)
end
```

I calculate the centers from the individual images. I do all this to accomodate diff monitor sizes, so I don’t have to add umpteen premade wallpapers (althought taht is the last resort).

The above function is called from a gui.pick_node event. It currently takes 46s to render the PNG image which is 3508×2480px. I use rgb, but used rgba before. No difference in rendering time.

46 seconds sounds a lot, even if you are creating buffers, iterating millions of pixels, and writing a file to disk from Lua.

Have you measured the individual steps? You can use socket.gettime() to get an accurate millisecond timestamp. Print the value at various steps of the process and calculate the difference. What is taking the most time?

1 Like

It’s important to note that each of these array accesses are a Lua<->C communication, which is quite costly. And doing it multiple times for each pixel in such large images, I guess that’s where the time is spent!

Also, I suggest using socket.gettime() to time the various pieces of code.

Personally, I would do this in either Lua or C.
Not sure if the png module has an option to decode to a byte string (and not a buffer). I guess it should be possible, as it can accept a byte string for encoding :thinking:

The two loops cost the most time. I fiddled a bit with the math and finally cleanly use only RGB instead of RGBA, which brought he whole process down to ~34 seconds (for the smaller end of my images at least).

As @Mathias_Westerdahl says, presumably the Lua-C switches cost so much, but I really don’t know how to get out of that without adding something external, and I am not C-savvy.

It seems to me that the solution would be to have optional fields in png.decode(bytes, x, y, w, h) to decode only a subregion if possible.