Problem with exporting .PNG to HTML5

Godot Version

Godot 4.5.1

Question

Hi, I’m porting one of my games to HTML5 and I’m having a problem.

By default, HTML5 browsers don’t allow exporting .PNG files to system folders for security reasons, and the only option is to export the file via a download. The problem is that when I click the export button, nothing happens and no download folder opens.

func _on_cloudy_button_pressed():
	print("🔴 BUTTON PRESSED!")
	cancel_loading_if_in_progress()
	stop_all_audio()
	cloudy_audio.play()
	print("🔴 CALLING export_drawing_to_png()")
	export_drawing_to_png()
	print("🔴 RETURNED FROM export_drawing_to_png()")

func request_storage_permissions():
	if OS.get_name() == "Android":
		OS.request_permissions()

func export_drawing_to_png():
	print("🟠 === STARTING EXPORT ===")
	print("🟠 OS.get_name():", OS.get_name())
	print("🟠 OS.has_feature('web'):", OS.has_feature("web"))
	
	var tile_size = tilemap_layer.tile_set.tile_size
	var tilemap_size = (max_bounds - min_bounds + Vector2i.ONE) * tile_size
	
	print("🟠 Tilemap size:", tilemap_size)
	
	# Create the viewport for capturing the TileMap rendering
	var viewport := SubViewport.new()
	viewport.size = tilemap_size
	viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ALWAYS
	viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
	viewport.transparent_bg = false
	viewport.disable_3d = true
	
	# Duplicate the TileMap into a temporary container
	var container := Node2D.new()
	var tilemap_copy := tilemap_layer.duplicate()
	tilemap_copy.position = -min_bounds * tile_size
	container.add_child(tilemap_copy)
	viewport.add_child(container)
	
	add_child(viewport)
	print("🟠 Waiting for frame...")
	await RenderingServer.frame_post_draw
	print("🟠 Frame complete")
	
	var image = viewport.get_texture().get_image()
	print("🟠 Image captured, size:", image.get_size() if image else "NULL")
	
	# Clean up
	remove_child(viewport)
	viewport.queue_free()
	
	var timestamp := Time.get_datetime_string_from_system().replace(":", "-").replace(" ", "_")
	var filename := "export_tilemap_" + timestamp + ".png"
	
	print("🟠 Filename:", filename)
	print("🟠 Checking platform...")
	
	# ===== HTML5 CHECK =====
	if OS.has_feature("web"):
		print("🌐 HTML5 DETECTED - Calling export_png_html5()")
		export_png_html5(image, filename)
		print("🌐 Returned from export_png_html5()")
		return
	
	# ===== NATIVE PLATFORMS =====
	print("💻 Native platform - using file save")
	
	var base_dir: String
	if OS.get_name() == "Android":
		base_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
		if base_dir.is_empty():
			base_dir = OS.get_user_data_dir()
	else:
		base_dir = OS.get_system_dir(OS.SYSTEM_DIR_PICTURES)
	
	var drawing_dir := base_dir.path_join("Drawing")
	
	if not DirAccess.dir_exists_absolute(drawing_dir):
		var dir_result = DirAccess.make_dir_recursive_absolute(drawing_dir)
		if dir_result != OK:
			print("❌ Failed to create directory:", drawing_dir, " Error:", dir_result)
			return
	
	var file_path := drawing_dir.path_join(filename)
	print("📁 Attempting to save to:", file_path)
	
	var err := image.save_png(file_path)
	if err == OK:
		print("✅ Export PNG succeeded:", file_path)
		if OS.get_name() == "Android":
			var accessible_path = "/storage/emulated/0/Pictures/Drawing/" + filename
			var copy_err = image.save_png(accessible_path)
			if copy_err == OK:
				print("✅ Also saved to accessible location:", accessible_path)
			else:
				print("⚠️ Could not save to accessible location:", copy_err)
	else:
		print("❌ Error during PNG export:", err)
		print("📁 Tried path:", file_path)

func export_png_html5(image: Image, filename: String):
	var separator = "=".repeat(50)
	print(separator)
	print("HTML5 EXPORT FUNCTION CALLED")
	print(separator)
	
	if image == null:
		print("ERROR: Image is null!")
		return
	
	print("Image size:", image.get_size())
	print("Converting to PNG buffer...")
	
	var png_data := image.save_png_to_buffer()
	print("PNG buffer size:", png_data.size())
	
	if png_data.size() == 0:
		print("ERROR: PNG buffer is empty!")
		return
	
	print("Converting to base64...")
	var base64 := Marshalls.raw_to_base64(png_data)
	print("Base64 length:", base64.length())
	print("First 100 chars:", base64.substr(0, 100))
	
	print("Executing JavaScript alert test...")
	JavaScriptBridge.eval("alert('TEST FROM GODOT - If you see this, JavaScript works!');")
	
	print(separator)

Did you vibe-code that? It does nothing with the image related to downloading it anywhere in the export_png_html5() function so…

You can use JavaScriptBridge.download_buffer() for that.

func _download() -> void:
	var img = preload("res://icon.svg").get_image()
	var png = img.save_png_to_buffer()
	JavaScriptBridge.download_buffer(png, "image.png")
1 Like

That would explain all the icons in the error messages.

Unfortunately, I generate code because I’m not very good at coding, and unfortunately, Godot doesn’t offer a good visual scripting solution like GDevelop. FlowKit came along recently, but it’s still unreliable.

I took note and now it exports to Download (If the game is on browser).

Does anyone know how to close an HTML5 game with a quit button? I’ve heard it’s not possible due to browser security reasons.

You just have them close the page.

I know I have to close the page, but there’s a Quit button in my game. I guess I need to remove it for the web version.

Correct.