Android won't export my drawings as png

Hi, I’m porting my game to Android on Godot 4.3. I have a problem with the drawing section regarding exporting the drawing made on TileMapLayer as a png file on Android in the Drawing folder. I tested on Android and I press the cloudy button (the icon with the cloud to export the drawing in .png) but when I go to the Drawing folder in the Picture section, no .png image is created. I heard that you have to check the WRITE EXTERNAL STORAGE permission, which I checked, but it doesn’t export the drawing as a .png image. I only have this problem on Android. The drawings export perfectly as .png on Windows, MacOS and Linux.


Here is the long GDScript script of the scene to make drawings.

extends Node2D

# Références aux nœuds
@onready var tilemap_layer = $TileMapLayer
@onready var petite_brosse_button = $petitebrossebuttton
@onready var moyenne_brosse_button = $moyennebrossebutton
@onready var grosse_brosse_button = $grosbrossebutton
@onready var filly_button = $fillybutton
@onready var gummy_button = $gummybutton
@onready var bomby_button = $bombybutton
@onready var alazar_button = $alazarbutton  # Bouton Undo
@onready var yakiti_button = $yakitibutton  # Bouton Redo
@onready var floppy_button = $floppybutton  # Bouton Sauvegarde
@onready var loady_button = $loadybutton    # Bouton Chargement
@onready var cloudy_button = $cloudybutton  # Bouton Export PNG
@onready var pinco_button = $pincobutton    # Bouton couleur rouge vin
@onready var black_color_button = $blackcolorbutton  # Bouton couleur noire
@onready var winered_color_button = $wineredcolorbutton
@onready var white_color_button = $whitecolorbutton
@onready var yellow_color_button = $yellowcolorbutton
@onready var lilas_color_button = $lilascolorbutton
@onready var orangeyellow_color_button = $orangeyellowcolorbutton
@onready var beige_color_button = $beigecolorbutton
@onready var darkblue_color_button = $darkbluecolorbutton
@onready var greyblue_color_button = $bluegreycolorbutton
@onready var riverblue_color_button = $riverbluecolorbutton
@onready var brown_color_button = $browncolorbutton
@onready var cyan_color_button = $cyancolorbutton
@onready var lightgrey_color_button = $lightgreycolorbutton
@onready var darkgrey_color_button = $darkgreycolorbutton
@onready var red_color_button = $redcolorbutton
@onready var green_color_button = $greencolorbutton
@onready var greenswamp_color_button = $greenswampcolorbutton
@onready var purple_color_button = $purplecolorbutton
@onready var pink_color_button = $pinkcolorbutton
@onready var floridapink_color_button = $floridapinkcolorbutton
@onready var orange_color_button = $orangecolorbutton


# Audio
@onready var petite_brosse_audio = $petitebrossebuttton/AudioStreamPlayer
@onready var moyenne_brosse_audio = $moyennebrossebutton/AudioStreamPlayer
@onready var grosse_brosse_audio = $grosbrossebutton/AudioStreamPlayer
@onready var filly_audio = $fillybutton/AudioStreamPlayer
@onready var gummy_audio = $gummybutton/AudioStreamPlayer
@onready var bomby_audio = $bombybutton/AudioStreamPlayer
@onready var alazar_audio = $alazarbutton/AudioStreamPlayer
@onready var yakiti_audio = $yakitibutton/AudioStreamPlayer
@onready var zemmourtousse_audio = $AudioStreamPlayerZemmourtousse
@onready var floppy_audio = $floppybutton/AudioStreamPlayer  # Audio pour Sauvegarde
@onready var loady_audio = $loadybutton/AudioStreamPlayer    # Audio pour Chargement
@onready var cloudy_audio = $cloudybutton/AudioStreamPlayer  # Audio pour Export PNG
@onready var pinco_audio = $pincobutton/AudioStreamPlayer   # Audio pour bouton rouge vin

# Icônes pour le bouton de remplissage
@onready var filly_icon = preload("res://icone/fillyicon.png")
@onready var filly_icon_deux = preload("res://icone/fillyicondeux.png")

# Variables pour le dessin
var current_brush_size = 1  # 1 = petite, 2 = moyenne, 3 = grosse
var last_tile: Vector2i = Vector2i(-1, -1)
var is_drawing = false
var is_filling = false  # Mode remplissage activé/désactivé
var is_erasing = false  # Mode gomme activé/désactivé
var loading_in_progress = false  # Pour suivre si un chargement est en cours

# Variables pour la gestion des couleurs
var current_color_source_id = 0  # ID de la source (0 pour rouge vin par défaut)
var current_color_name = "default"  # Nom dans l'atlas (default pour rouge vin)

# Historique pour Undo/Redo
var action_history = []  # Pour stocker l'historique des actions
var redo_history = []    # Pour stocker les actions annulées
var current_action = {}  # Pour stocker l'action en cours
var is_action_recording = false  # Pour savoir si on enregistre une action

# État sauvegardé
var saved_state = {}  # Pour stocker l'état sauvegardé du dessin

# Limites de la zone de dessin
var min_bounds: Vector2i = Vector2i(-25, -16)
var max_bounds: Vector2i = Vector2i(24, 14)

func _ready():
	# Connecter les boutons s'ils ne sont pas encore connectés
	if not petite_brosse_button.pressed.is_connected(_on_petite_brosse_pressed):
		petite_brosse_button.pressed.connect(_on_petite_brosse_pressed)
	if not moyenne_brosse_button.pressed.is_connected(_on_moyenne_brosse_pressed):
		moyenne_brosse_button.pressed.connect(_on_moyenne_brosse_pressed)
	if not grosse_brosse_button.pressed.is_connected(_on_grosse_brosse_pressed):
		grosse_brosse_button.pressed.connect(_on_grosse_brosse_pressed)
	if not filly_button.pressed.is_connected(_on_filly_button_pressed):
		filly_button.pressed.connect(_on_filly_button_pressed)
	if not gummy_button.pressed.is_connected(_on_gummy_button_pressed):
		gummy_button.pressed.connect(_on_gummy_button_pressed)
	if not bomby_button.pressed.is_connected(_on_bomby_button_pressed):
		bomby_button.pressed.connect(_on_bomby_button_pressed)
	if not alazar_button.pressed.is_connected(_on_alazar_button_pressed):
		alazar_button.pressed.connect(_on_alazar_button_pressed)
	if not yakiti_button.pressed.is_connected(_on_yakiti_button_pressed):
		yakiti_button.pressed.connect(_on_yakiti_button_pressed)
	if not floppy_button.pressed.is_connected(_on_floppy_button_pressed):
		floppy_button.pressed.connect(_on_floppy_button_pressed)
	if not loady_button.pressed.is_connected(_on_loady_button_pressed):
		loady_button.pressed.connect(_on_loady_button_pressed)
	if not cloudy_button.pressed.is_connected(_on_cloudy_button_pressed):
		cloudy_button.pressed.connect(_on_cloudy_button_pressed)
	if not pinco_button.pressed.is_connected(_on_pinco_button_pressed):
		pinco_button.pressed.connect(_on_pinco_button_pressed)
	if not black_color_button.pressed.is_connected(_on_black_color_button_pressed):
		black_color_button.pressed.connect(_on_black_color_button_pressed)
	if not winered_color_button.pressed.is_connected(_on_winered_colorbutton_pressed):
		winered_color_button.pressed.connect(_on_winered_colorbutton_pressed)
	if not white_color_button.pressed.is_connected(_on_white_color_button_pressed):
		white_color_button.pressed.connect(_on_white_color_button_pressed)
	if not yellow_color_button.pressed.is_connected(_on_yellow_color_button_pressed):
		yellow_color_button.pressed.connect(_on_yellow_color_button_pressed)
	if not lilas_color_button.pressed.is_connected(_on_lilas_color_button_pressed):
		lilas_color_button.pressed.connect(_on_lilas_color_button_pressed)
	if not orangeyellow_color_button.pressed.is_connected(_on_orangeyellow_color_button_pressed):
		orangeyellow_color_button.pressed.connect(_on_orangeyellow_color_button_pressed)
	if not beige_color_button.pressed.is_connected(_on_beige_color_button_pressed):
		beige_color_button.pressed.connect(_on_beige_color_button_pressed)
	if not darkblue_color_button.pressed.is_connected(_on_darkblue_color_button_pressed):
		darkblue_color_button.pressed.connect(_on_darkblue_color_button_pressed)
	if not greyblue_color_button.pressed.is_connected(_on_greyblue_color_button_pressed):
		greyblue_color_button.pressed.connect(_on_greyblue_color_button_pressed)
	if not riverblue_color_button.pressed.is_connected(_on_riverblue_color_button_pressed):
		riverblue_color_button.pressed.connect(_on_riverblue_color_button_pressed)
	if not brown_color_button.pressed.is_connected(_on_brown_color_button_pressed):
		brown_color_button.pressed.connect(_on_brown_color_button_pressed)
	if not cyan_color_button.pressed.is_connected(_on_cyan_color_button_pressed):
		cyan_color_button.pressed.connect(_on_cyan_color_button_pressed)
	if not lightgrey_color_button.pressed.is_connected(_on_lightgrey_color_button_pressed):
		lightgrey_color_button.pressed.connect(_on_lightgrey_color_button_pressed)
	if not darkgrey_color_button.pressed.is_connected(_on_darkgrey_color_button_pressed):
		darkgrey_color_button.pressed.connect(_on_darkgrey_color_button_pressed)
	if not red_color_button.pressed.is_connected(_on_red_color_button_pressed):
		red_color_button.pressed.connect(_on_red_color_button_pressed)
	if not green_color_button.pressed.is_connected(_on_green_color_button_pressed):
		green_color_button.pressed.connect(_on_green_color_button_pressed)
	if not greenswamp_color_button.pressed.is_connected(_on_greenswamp_color_button_pressed):
		greenswamp_color_button.pressed.connect(_on_greenswamp_color_button_pressed)
	if not purple_color_button.pressed.is_connected(_on_purple_color_button_pressed):
		purple_color_button.pressed.connect(_on_purple_color_button_pressed)
	if not pink_color_button.pressed.is_connected(_on_pink_color_button_pressed):
		pink_color_button.pressed.connect(_on_pink_color_button_pressed)
	if not floridapink_color_button.pressed.is_connected(_on_floridapink_color_button_pressed):
		floridapink_color_button.pressed.connect(_on_floridapink_color_button_pressed)
	if not orange_color_button.pressed.is_connected(_on_orange_color_button_pressed):
		orange_color_button.pressed.connect(_on_orange_color_button_pressed)
	
	# Connecter le signal de fin d'audio pour le chargement
	if not loady_audio.finished.is_connected(_on_loady_audio_finished):
		loady_audio.finished.connect(_on_loady_audio_finished)

func _input(event):
	# Gestion du raccourci Ctrl+Z pour Undo
	if event is InputEventKey:
		if event.keycode == KEY_Z and event.pressed and event.ctrl_pressed:
			undo_last_action_with_zemmourtousse()
			return
	
	if event is InputEventMouse:
		var mouse_pos = tilemap_layer.to_local(event.position)
		var tile_pos = tilemap_layer.local_to_map(mouse_pos)

		if event is InputEventMouseButton:
			if event.button_index == MOUSE_BUTTON_LEFT:
				if event.pressed:
					if is_within_bounds(tile_pos):
						if is_filling:
							# Capture l'état avant le remplissage
							start_action("fill")
							capture_tilemap_state()
							flood_fill(tile_pos)
							end_action()
						else:
							is_drawing = true
							last_tile = tile_pos
							# Débuter une nouvelle action de dessin
							start_action("draw")
							draw_brush(tile_pos)
				else:
					if is_drawing:
						is_drawing = false
						last_tile = Vector2i(-1, -1)
						# Terminer l'action de dessin
						end_action()

		elif event is InputEventMouseMotion and is_drawing and not is_filling:
			if tile_pos != last_tile and is_within_bounds(tile_pos):
				if last_tile != Vector2i(-1, -1):
					draw_line_on_tilemap(last_tile, tile_pos)
				last_tile = tile_pos

# Système d'historique
func start_action(action_type: String):
	is_action_recording = true
	current_action = {
		"type": action_type,
		"before": {},
		"after": {}
	}
	capture_tilemap_state()

func capture_tilemap_state():
	# Capture l'état actuel de la tilemap
	current_action["before"] = get_tilemap_state()

func end_action():
	if is_action_recording:
		is_action_recording = false
		current_action["after"] = get_tilemap_state()
		action_history.append(current_action.duplicate())
		# Vider l'historique redo quand une nouvelle action est effectuée
		redo_history.clear()
		current_action = {}

func get_tilemap_state() -> Dictionary:
	var state = {}
	for x in range(min_bounds.x, max_bounds.x + 1):
		for y in range(min_bounds.y, max_bounds.y + 1):
			var pos = Vector2i(x, y)
			var atlas_coords = tilemap_layer.get_cell_atlas_coords(pos)
			var source_id = tilemap_layer.get_cell_source_id(pos)
			if source_id != -1:  # Si la cellule n'est pas vide
				state[str(pos.x) + "," + str(pos.y)] = {
					"source_id": source_id,
					"atlas_coords": atlas_coords
				}
	return state

func apply_tilemap_state(state: Dictionary):
	# Effacer la tilemap d'abord
	for x in range(min_bounds.x, max_bounds.x + 1):
		for y in range(min_bounds.y, max_bounds.y + 1):
			tilemap_layer.erase_cell(Vector2i(x, y))
	
	# Appliquer l'état sauvegardé
	for pos_key in state:
		var pos_parts = pos_key.split(",")
		var pos = Vector2i(int(pos_parts[0]), int(pos_parts[1]))
		var cell_data = state[pos_key]
		tilemap_layer.set_cell(pos, cell_data["source_id"], cell_data["atlas_coords"])

# Boutons Undo/Redo
func _on_alazar_button_pressed():
	cancel_loading_if_in_progress()
	undo_last_action()
	stop_all_audio()
	alazar_audio.play()

func _on_yakiti_button_pressed():
	cancel_loading_if_in_progress()
	redo_last_action()
	stop_all_audio()
	yakiti_audio.play()

func undo_last_action():
	if action_history.size() > 0:
		var last_action = action_history.pop_back()
		redo_history.append(last_action)
		apply_tilemap_state(last_action["before"])

# Fonction spécifique pour Ctrl+Z qui joue un son différent
func undo_last_action_with_zemmourtousse():
	cancel_loading_if_in_progress()
	stop_all_audio()
	zemmourtousse_audio.play()
	
	# Effectuer l'annulation seulement s'il y a des actions à annuler
	if action_history.size() > 0:
		undo_last_action()

func redo_last_action():
	if redo_history.size() > 0:
		var next_action = redo_history.pop_back()
		action_history.append(next_action)
		apply_tilemap_state(next_action["after"])

# Fonctions de sauvegarde et chargement
func _on_floppy_button_pressed():
	cancel_loading_if_in_progress()
	stop_all_audio()
	floppy_audio.play()
	save_drawing()

func _on_loady_button_pressed():
	cancel_loading_if_in_progress()
	stop_all_audio()
	loady_audio.play()
	loading_in_progress = true

func _on_loady_audio_finished():
	if loading_in_progress:
		load_drawing()
		loading_in_progress = false

func save_drawing():
	var save_data = {
		"tilemap_state": get_tilemap_state(),
		"action_history": export_action_history(),
		"redo_history": export_redo_history(),
		"version": "1.0"
	}

	var save_file = FileAccess.open("user://drawing_save.dat", FileAccess.WRITE)
	if save_file:
		save_file.store_var(save_data)
		print("Dessin + historique complet sauvegardé")
	else:
		print("Erreur lors de la sauvegarde.")



func load_drawing():
	var save_file = FileAccess.open("user://drawing_save.dat", FileAccess.READ)
	if save_file:
		var save_data = save_file.get_var()
		
		if save_data is Dictionary and save_data.has("tilemap_state"):
			apply_tilemap_state(save_data["tilemap_state"])
			saved_state = save_data["tilemap_state"]

			# Restaurer les historiques s’ils existent
			if save_data.has("action_history"):
				action_history = save_data["action_history"]
			else:
				action_history.clear()

			if save_data.has("redo_history"):
				redo_history = save_data["redo_history"]
			else:
				redo_history.clear()

			print("Dessin + historique Undo/Redo chargé")

func export_action_history() -> Array:
	var exported = []
	for action in action_history:
		exported.append({
			"type": action["type"],
			"before": action["before"],
			"after": action["after"]
		})
	return exported

func export_redo_history() -> Array:
	var exported = []
	for action in redo_history:
		exported.append({
			"type": action["type"],
			"before": action["before"],
			"after": action["after"]
		})
	return exported



func cancel_loading_if_in_progress():
	if loading_in_progress:
		loading_in_progress = false
		# Le son sera arrêté par stop_all_audio() dans la fonction qui appelle celle-ci

# Boutons brosse
func _on_petite_brosse_pressed():
	cancel_loading_if_in_progress()
	stop_all_audio()
	petite_brosse_audio.play()
	current_brush_size = 1
	is_filling = false
	is_erasing = false
	filly_button.icon = filly_icon
	update_button_visuals()

func _on_moyenne_brosse_pressed():
	cancel_loading_if_in_progress()
	stop_all_audio()
	moyenne_brosse_audio.play()
	current_brush_size = 2
	is_filling = false
	is_erasing = false
	filly_button.icon = filly_icon
	update_button_visuals()

func _on_grosse_brosse_pressed():
	cancel_loading_if_in_progress()
	stop_all_audio()
	grosse_brosse_audio.play()
	current_brush_size = 3
	is_filling = false
	is_erasing = false
	filly_button.icon = filly_icon
	update_button_visuals()

# Bouton remplissage
func _on_filly_button_pressed():
	cancel_loading_if_in_progress()
	is_filling = not is_filling
	is_erasing = false

	if is_filling:
		filly_button.icon = filly_icon_deux
		stop_all_audio()
		filly_audio.play()
	else:
		filly_button.icon = filly_icon
		filly_audio.stop()

# Bouton gomme
func _on_gummy_button_pressed():
	cancel_loading_if_in_progress()
	is_erasing = true
	is_filling = false
	stop_all_audio()
	gummy_audio.play()
	filly_button.icon = filly_icon
	update_button_visuals()

# Bouton d'effacement complet
func _on_bomby_button_pressed():
	cancel_loading_if_in_progress()
	# Capture l'état avant l'effacement
	start_action("clear")
	clear_entire_drawing()
	end_action()
	
	action_history.clear()
	redo_history.clear()
	
	stop_all_audio()
	bomby_audio.play()

# Boutons de couleur
func _on_pinco_button_pressed():
	cancel_loading_if_in_progress()
	stop_all_audio()
	pinco_audio.play()
	current_color_source_id = 0
	current_color_name = "default"


func _on_black_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 1
	current_color_name = "black"

func _on_winered_colorbutton_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 0
	current_color_name = "default"

func _on_white_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 2
	current_color_name = "white"

func _on_yellow_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 3
	current_color_name = "yellow"

func _on_lilas_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 4
	current_color_name = "lilas"

func _on_orangeyellow_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 5
	current_color_name = "orangeyellow"
	

func _on_beige_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 6
	current_color_name = "beige"

func _on_darkblue_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 7
	current_color_name = "darkblue"

func _on_greyblue_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 8
	current_color_name = "greyblue"


func _on_riverblue_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 9
	current_color_name = "riverblue"

func _on_brown_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 10
	current_color_name = "brown"

func _on_cyan_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 11
	current_color_name = "cyan"

func _on_lightgrey_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 12
	current_color_name = "lightgrey"

func _on_darkgrey_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 13
	current_color_name = "darkgrey"

func _on_red_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 14
	current_color_name = "red"

func _on_green_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 15
	current_color_name = "green"

func _on_greenswamp_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 16
	current_color_name = "swampgreen"

func _on_purple_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 17
	current_color_name = "purple"

func _on_pink_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 18
	current_color_name = "pink"

func _on_floridapink_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 19
	current_color_name = "floridapink"

func _on_orange_color_button_pressed():
	cancel_loading_if_in_progress()
	current_color_source_id = 20
	current_color_name = "orange"


# Bouton d'export PNG
func _on_cloudy_button_pressed():
	cancel_loading_if_in_progress()
	stop_all_audio()
	cloudy_audio.play()
	export_drawing_to_png()



func export_drawing_to_png():
	var tile_size = tilemap_layer.tile_set.tile_size
	var tilemap_size = (max_bounds - min_bounds + Vector2i.ONE) * tile_size

	# Créer le viewport pour rendre le TileMap
	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

	# Copier le TileMap dans un conteneur temporaire
	var container := Node2D.new()
	var tilemap_copy := tilemap_layer.duplicate()
	tilemap_copy.position = -min_bounds * tile_size  # Centrer à l'origine
	container.add_child(tilemap_copy)
	viewport.add_child(container)

	# Ajouter le viewport à la scène pour permettre le rendu
	add_child(viewport)
	# Attendre quelques frames pour s'assurer que le rendu est complet
	await get_tree().process_frame
	await get_tree().idle_frame()

	# Récupérer l'image
	var image = viewport.get_texture().get_image()

	# Nettoyage
	remove_child(viewport)
	viewport.queue_free()

	# Construire le chemin d'enregistrement dans le dossier Pictures
	var pictures_dir := OS.get_system_dir(OS.SYSTEM_DIR_PICTURES)
	var drawing_dir := pictures_dir.path_join("Drawing")
	if not DirAccess.dir_exists_absolute(drawing_dir):
		DirAccess.make_dir_recursive_absolute(drawing_dir)

	# Créer un nom de fichier sans caractères invalides
	var timestamp := Time.get_datetime_string_from_system().replace(":", "-")
	var filename := "export_tilemap_" + timestamp + ".png"
	var file_path := drawing_dir.path_join(filename)

	# Sauvegarder l'image au format PNG
	var err = image.save_png(file_path)
	if err == OK:
		print("✅ Export PNG réussi :", file_path)
	else:
		print("❌ Erreur pendant l'export PNG :", err)



func stop_all_audio():
	petite_brosse_audio.stop()
	moyenne_brosse_audio.stop()
	grosse_brosse_audio.stop()
	filly_audio.stop()
	gummy_audio.stop()
	bomby_audio.stop()
	alazar_audio.stop()
	yakiti_audio.stop()
	zemmourtousse_audio.stop()
	floppy_audio.stop()
	loady_audio.stop()
	cloudy_audio.stop()
	pinco_audio.stop()

func update_button_visuals():
	# Exemple visuel : teinte jaune sur le bouton sélectionné
	petite_brosse_button.modulate = Color(1, 1, 1)
	moyenne_brosse_button.modulate = Color(1, 1, 1)
	grosse_brosse_button.modulate = Color(1, 1, 1)
	gummy_button.modulate = Color(1, 1, 1)
	pinco_button.modulate = Color(1, 1, 1)
	black_color_button.modulate = Color(1, 1, 1)

	# Mettre en surbrillance le bouton de couleur actuel
	if current_color_source_id == 0:
		pinco_button.modulate = Color(1, 0.8, 0.8)  # Teinte rouge clair
	elif current_color_source_id == 1:
		black_color_button.modulate = Color(0.8, 0.8, 1)  # Teinte bleue claire

	if is_erasing:
		match current_brush_size:
			1: petite_brosse_button.modulate = Color(1, 1, 0)
			2: moyenne_brosse_button.modulate = Color(1, 1, 0)
			3: grosse_brosse_button.modulate = Color(1, 1, 0)

# Efface entièrement le dessin
func clear_entire_drawing():
	for x in range(min_bounds.x, max_bounds.x + 1):
		for y in range(min_bounds.y, max_bounds.y + 1):
			tilemap_layer.erase_cell(Vector2i(x, y))

# Dessin
func draw_brush(center_pos: Vector2i):
	var tile_source = current_color_source_id
	var atlas_coords = Vector2i(0, 0)
	
	# Si en mode gomme, on efface les cellules
	if is_erasing:
		match current_brush_size:
			1:
				tilemap_layer.erase_cell(center_pos)
			2:
				for y in range(-1, 2):
					for x in range(-1, 2):
						var pos = center_pos + Vector2i(x, y)
						if is_within_bounds(pos):
							tilemap_layer.erase_cell(pos)
			3:
				for y in range(-2, 3):
					for x in range(-2, 3):
						var pos = center_pos + Vector2i(x, y)
						if is_within_bounds(pos):
							tilemap_layer.erase_cell(pos)
	else:
		# Mode dessin normal
		match current_brush_size:
			1:
				tilemap_layer.set_cell(center_pos, tile_source, atlas_coords)
			2:
				for y in range(-1, 2):
					for x in range(-1, 2):
						var pos = center_pos + Vector2i(x, y)
						if is_within_bounds(pos):
							tilemap_layer.set_cell(pos, tile_source, atlas_coords)
			3:
				for y in range(-2, 3):
					for x in range(-2, 3):
						var pos = center_pos + Vector2i(x, y)
						if is_within_bounds(pos):
							tilemap_layer.set_cell(pos, tile_source, atlas_coords)

func draw_line_on_tilemap(start: Vector2i, end: Vector2i):
	var points = get_line_points(start, end)
	for point in points:
		draw_brush(point)

func get_line_points(start: Vector2i, end: Vector2i) -> Array:
	var points = []
	var dx = abs(end.x - start.x)
	var dy = abs(end.y - start.y)
	var sx = 1 if start.x < end.x else -1
	var sy = 1 if start.y < end.y else -1
	var err = dx - dy
	var x = start.x
	var y = start.y

	while true:
		points.append(Vector2i(x, y))
		if x == end.x and y == end.y:
			break
		var e2 = 2 * err
		if e2 > -dy:
			err -= dy
			x += sx
		if e2 < dx:
			err += dx
			y += sy
	return points

# Flood fill
func flood_fill(start_pos: Vector2i):
	var tile_source = current_color_source_id
	var atlas_coords = Vector2i(0, 0)

	var original_source_id = tilemap_layer.get_cell_source_id(start_pos)
	var original_atlas_coords = tilemap_layer.get_cell_atlas_coords(start_pos)
	
	# Ne pas remplir si déjà la même couleur
	if original_source_id == tile_source && original_atlas_coords == atlas_coords:
		return

	var queue = [start_pos]
	var visited = {}

	while queue.size() > 0:
		var pos = queue.pop_front()
		if not is_within_bounds(pos):
			continue
		if visited.has(pos):
			continue
		visited[pos] = true

		var cell_source_id = tilemap_layer.get_cell_source_id(pos)
		var cell_atlas_coords = tilemap_layer.get_cell_atlas_coords(pos)
		
		if cell_source_id != original_source_id || cell_atlas_coords != original_atlas_coords:
			continue

		tilemap_layer.set_cell(pos, tile_source, atlas_coords)

		queue.append(pos + Vector2i(1, 0))
		queue.append(pos + Vector2i(-1, 0))
		queue.append(pos + Vector2i(0, 1))
		queue.append(pos + Vector2i(0, -1))

# Limites
func is_within_bounds(pos: Vector2i) -> bool:
	return pos.x >= min_bounds.x and pos.x <= max_bounds.x and pos.y >= min_bounds.y and pos.y <= max_bounds.y

func set_bounds(min_pos: Vector2i, max_pos: Vector2i):
	min_bounds = min_pos
	max_bounds = max_pos

HEY! Can anyone help me? This is a problem with the function to export the drawing to .png on Android.

func export_drawing_to_png():
	var tile_size = tilemap_layer.tile_set.tile_size
	var tilemap_size = (max_bounds - min_bounds + Vector2i.ONE) * tile_size

	# Créer le viewport pour rendre le TileMap
	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

	# Copier le TileMap dans un conteneur temporaire
	var container := Node2D.new()
	var tilemap_copy := tilemap_layer.duplicate()
	tilemap_copy.position = -min_bounds * tile_size  # Centrer à l'origine
	container.add_child(tilemap_copy)
	viewport.add_child(container)

	# Ajouter le viewport à la scène pour permettre le rendu
	add_child(viewport)
	# Attendre quelques frames pour s'assurer que le rendu est complet
	await get_tree().process_frame
	await get_tree().idle_frame()

	# Récupérer l'image
	var image = viewport.get_texture().get_image()

	# Nettoyage
	remove_child(viewport)
	viewport.queue_free()

	# Construire le chemin d'enregistrement dans le dossier Pictures
	var pictures_dir := OS.get_system_dir(OS.SYSTEM_DIR_PICTURES)
	var drawing_dir := pictures_dir.path_join("Drawing")
	if not DirAccess.dir_exists_absolute(drawing_dir):
		DirAccess.make_dir_recursive_absolute(drawing_dir)

	# Créer un nom de fichier sans caractères invalides
	var timestamp := Time.get_datetime_string_from_system().replace(":", "-")
	var filename := "export_tilemap_" + timestamp + ".png"
	var file_path := drawing_dir.path_join(filename)

	# Sauvegarder l'image au format PNG
	var err = image.save_png(file_path)
	if err == OK:
		print("✅ Export PNG réussi :", file_path)
	else:
		print("❌ Erreur pendant l'export PNG :", err)

What are the results of the err print? Does the filepath look correct? Does it fail, what is the error_string?

If it does error, print the error_string instead of th err number

print("❌ Erreur pendant l'export PNG :", error_string(err))

I don’t know the results of the debug errors because I’m running the game by exporting an .apk on Android, and the Godot editor is on one of my Windows PCs.

On Windows, Linux, and MacOS, the image exports fine to Picture/Drawing, but not on Android.

On Android, the Drawing file is created in Picture. When I go to the Drawing folder, no image appears, even though I clicked the correct button to export the drawing as a PNG.

I did check the Read External Storage, Write External Storage, and Manage External Storage permissions, but no image will be exported to the Drawing folder.

From what I’ve heard, exporting an image to a .png on Android is different from exporting an image to a .png for Windows, MacOS, and Linux.

Finally I managed to export my drawing in .png to Android. However, the Drawing folder is found in Document and not Pictures. Here is the code that needed to be modified to export to Android.

func request_storage_permissions():
	if OS.get_name() == "Android":
		# In Godot 4.3, permissions are handled differently
		# You need to declare them in the export template or project settings
		OS.request_permissions()

func export_drawing_to_png():
	var tile_size = tilemap_layer.tile_set.tile_size
	var tilemap_size = (max_bounds - min_bounds + Vector2i.ONE) * tile_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  # Center at the origin
	container.add_child(tilemap_copy)
	viewport.add_child(container)
	
	# Add the viewport to the scene so it gets rendered
	add_child(viewport)
	await RenderingServer.frame_post_draw  # Wait for the rendering to complete
	
	# Retrieve the image from the viewport texture
	var image = viewport.get_texture().get_image()
	
	# Clean up
	remove_child(viewport)
	viewport.queue_free()
	
	# Build the save path - Use user:// for Android compatibility
	var base_dir: String
	if OS.get_name() == "Android":
		# On Android, use the app's internal storage or documents directory
		base_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
		if base_dir.is_empty():
			# Fallback to user:// directory (app's internal storage)
			base_dir = OS.get_user_data_dir()
	else:
		# On other platforms, use the Pictures directory
		base_dir = OS.get_system_dir(OS.SYSTEM_DIR_PICTURES)
	
	var drawing_dir := base_dir.path_join("Drawing")
	
	# Ensure the "Drawing" folder exists
	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
	
	# Create a filename with a valid timestamp
	var timestamp := Time.get_datetime_string_from_system().replace(":", "-").replace(" ", "_")
	var filename := "export_tilemap_" + timestamp + ".png"
	var file_path := drawing_dir.path_join(filename)
	
	print("📁 Attempting to save to:", file_path)
	
	# Save the image as PNG
	var err := image.save_png(file_path)
	if err == OK:
		print("✅ Export PNG succeeded:", file_path)
		# On Android, also try to copy to a more accessible location
		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)

On the other hand, I would like to put the Drawing folder in Picture and not Document.