Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the file format of pxo to be a ZIP #952

Merged
merged 3 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Translations/Translations.pot
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,14 @@ msgstr ""
msgid "Save as..."
msgstr ""

msgid "Use ZSTD Compression"
#. Checkbox found in the Save project dialog. If enabled, the final blended images are being stored also in the pxo, for each frame.
msgid "Include blended images"
msgstr ""

#. Hint tooltip of the "Include blended images" checkbox found in the Save project dialog.
msgid "If enabled, the final blended images are also being stored in the pxo, for each frame.\n"
"This makes the pxo file larger and is useful for importing by third-party software\n"
"or CLI exporting. Loading pxo files in Pixelorama does not need this option to be enabled.\n"
msgstr ""

msgid "Import"
Expand Down
207 changes: 149 additions & 58 deletions src/Autoload/OpenSave.gd
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,6 @@ func handle_loading_aimg(path: String, frames: Array) -> void:


func open_pxo_file(path: String, untitled_backup := false, replace_empty := true) -> void:
var file := FileAccess.open_compressed(path, FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
if FileAccess.get_open_error() == ERR_FILE_UNRECOGNIZED:
# If the file is not compressed open it raw (pre-v0.7)
file = FileAccess.open(path, FileAccess.READ)
var err := FileAccess.get_open_error()
if err != OK:
Global.error_dialog.set_text(
tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
return

var empty_project := Global.current_project.is_empty() and replace_empty
var new_project: Project
if empty_project:
Expand All @@ -136,50 +123,73 @@ func open_pxo_file(path: String, untitled_backup := false, replace_empty := true
new_project.name = path.get_file()
else:
new_project = Project.new([], path.get_file())

var first_line := file.get_line()
var test_json_conv := JSON.new()
var error := test_json_conv.parse(first_line)
if error != OK:
print("Error, corrupt pxo file")
file.close()
var zip_reader := ZIPReader.new()
var err := zip_reader.open(path)
if err == FAILED:
# Most likely uses the old pxo format, load that
var success := open_v0_pxo_file(path, new_project)
if not success:
return
elif err != OK:
Global.error_dialog.set_text(
tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
return
else:
var data_json := zip_reader.read_file("data.json").get_string_from_utf8()
var test_json_conv := JSON.new()
var error := test_json_conv.parse(data_json)
if error != OK:
print("Error, corrupt pxo file")
zip_reader.close()
return
var result = test_json_conv.get_data()
if typeof(result) != TYPE_DICTIONARY:
print("Error, json parsed result is: %s" % typeof(result))
file.close()
zip_reader.close()
return

new_project.deserialize(result)
for frame in new_project.frames:
for cel in frame.cels:
cel.load_image_data_from_pxo(file, new_project.size)

for frame_index in new_project.frames.size():
var frame := new_project.frames[frame_index]
for cel_index in frame.cels.size():
var cel := frame.cels[cel_index]
if not cel is PixelCel:
continue
var image_data := zip_reader.read_file(
"image_data/frames/%s/layer_%s" % [frame_index + 1, cel_index + 1]
)
var image := Image.create_from_data(
new_project.size.x, new_project.size.y, false, Image.FORMAT_RGBA8, image_data
)
cel.image_changed(image)
if result.has("brushes"):
var brush_index := 0
for brush in result.brushes:
var b_width = brush.size_x
var b_height = brush.size_y
var buffer := file.get_buffer(b_width * b_height * 4)
var b_width: int = brush.size_x
var b_height: int = brush.size_y
var image_data := zip_reader.read_file("image_data/brushes/brush_%s" % brush_index)
var image := Image.create_from_data(
b_width, b_height, false, Image.FORMAT_RGBA8, buffer
b_width, b_height, false, Image.FORMAT_RGBA8, image_data
)
new_project.brushes.append(image)
Brushes.add_project_brush(image)

brush_index += 1
if result.has("tile_mask") and result.has("has_mask"):
if result.has_mask:
var t_width = result.tile_mask.size_x
var t_height = result.tile_mask.size_y
var buffer := file.get_buffer(t_width * t_height * 4)
var image_data := zip_reader.read_file("image_data/tile_map")
var image := Image.create_from_data(
t_width, t_height, false, Image.FORMAT_RGBA8, buffer
t_width, t_height, false, Image.FORMAT_RGBA8, image_data
)
new_project.tiles.tile_mask = image
else:
new_project.tiles.reset_mask()
zip_reader.close()

file.close()
if empty_project:
new_project.change_project()
Global.project_changed.emit()
Expand Down Expand Up @@ -208,8 +218,75 @@ func open_pxo_file(path: String, untitled_backup := false, replace_empty := true
save_project_to_recent_list(path)


func open_v0_pxo_file(path: String, new_project: Project) -> bool:
var file := FileAccess.open_compressed(path, FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
if FileAccess.get_open_error() == ERR_FILE_UNRECOGNIZED:
# If the file is not compressed open it raw (pre-v0.7)
file = FileAccess.open(path, FileAccess.READ)
var err := FileAccess.get_open_error()
if err != OK:
Global.error_dialog.set_text(
tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
return false

var first_line := file.get_line()
var test_json_conv := JSON.new()
var error := test_json_conv.parse(first_line)
if error != OK:
print("Error, corrupt pxo file")
file.close()
return false

var result = test_json_conv.get_data()
if typeof(result) != TYPE_DICTIONARY:
print("Error, json parsed result is: %s" % typeof(result))
file.close()
return false

new_project.deserialize(result)
for frame in new_project.frames:
for cel in frame.cels:
if cel is PixelCel:
var buffer := file.get_buffer(new_project.size.x * new_project.size.y * 4)
var image := Image.create_from_data(
new_project.size.x, new_project.size.y, false, Image.FORMAT_RGBA8, buffer
)
cel.image_changed(image)
elif cel is Cel3D:
# Don't do anything with it, just read it so that the file can move on
file.get_buffer(new_project.size.x * new_project.size.y * 4)

if result.has("brushes"):
for brush in result.brushes:
var b_width = brush.size_x
var b_height = brush.size_y
var buffer := file.get_buffer(b_width * b_height * 4)
var image := Image.create_from_data(
b_width, b_height, false, Image.FORMAT_RGBA8, buffer
)
new_project.brushes.append(image)
Brushes.add_project_brush(image)

if result.has("tile_mask") and result.has("has_mask"):
if result.has_mask:
var t_width = result.tile_mask.size_x
var t_height = result.tile_mask.size_y
var buffer := file.get_buffer(t_width * t_height * 4)
var image := Image.create_from_data(
t_width, t_height, false, Image.FORMAT_RGBA8, buffer
)
new_project.tiles.tile_mask = image
else:
new_project.tiles.reset_mask()
file.close()
return true


func save_pxo_file(
path: String, autosave: bool, use_zstd := true, project := Global.current_project
path: String, autosave: bool, include_blended := false, project := Global.current_project
) -> bool:
if !autosave:
project.name = path.get_file()
Expand All @@ -233,17 +310,11 @@ func save_pxo_file(
# Check if a file with the same name exists. If it does, rename the new file temporarily.
# Needed in case of a crash, so that the old file won't be replaced with an empty one.
var temp_path := path
var dir := DirAccess.open("user://")
if dir.file_exists(path):
if FileAccess.file_exists(path):
temp_path = path + "1"

var file: FileAccess
if use_zstd:
file = FileAccess.open_compressed(temp_path, FileAccess.WRITE, FileAccess.COMPRESSION_ZSTD)
else:
file = FileAccess.open(temp_path, FileAccess.WRITE)

var err := FileAccess.get_open_error()
var zip_packer := ZIPPacker.new()
var err := zip_packer.open(temp_path)
if err != OK:
if temp_path.is_valid_filename():
return false
Expand All @@ -252,37 +323,57 @@ func save_pxo_file(
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
if file: # this would be null if we attempt to save filenames such as "//\\||.pxo"
file.close()
if zip_packer: # this would be null if we attempt to save filenames such as "//\\||.pxo"
zip_packer.close()
return false
zip_packer.start_file("data.json")
zip_packer.write_file(to_save.to_utf8_buffer())
zip_packer.close_file()

if !autosave:
current_save_paths[Global.current_project_index] = path

file.store_line(to_save)
var frame_index := 1
for frame in project.frames:
if not autosave and include_blended:
var blended := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
DrawingAlgos.blend_layers(blended, frame, Vector2i.ZERO, project)
zip_packer.start_file("image_data/final_images/%s" % frame_index)
zip_packer.write_file(blended.get_data())
zip_packer.close_file()
var cel_index := 1
for cel in frame.cels:
cel.save_image_data_to_pxo(file)
var cel_image := cel.get_image()
if is_instance_valid(cel_image) and cel is PixelCel:
zip_packer.start_file("image_data/frames/%s/layer_%s" % [frame_index, cel_index])
zip_packer.write_file(cel_image.get_data())
zip_packer.close_file()
cel_index += 1
frame_index += 1
var brush_index := 0
for brush in project.brushes:
file.store_buffer(brush.get_data())
zip_packer.start_file("image_data/brushes/brush_%s" % brush_index)
zip_packer.write_file(brush.get_data())
zip_packer.close_file()
brush_index += 1
if project.tiles.has_mask:
file.store_buffer(project.tiles.tile_mask.get_data())

file.close()
zip_packer.start_file("image_data/tile_map")
zip_packer.write_file(project.tiles.tile_mask.get_data())
zip_packer.close_file()
zip_packer.close()

if temp_path != path:
# Rename the new file to its proper name and remove the old file, if it exists.
dir.rename(temp_path, path)
DirAccess.rename_absolute(temp_path, path)

if OS.has_feature("web") and !autosave:
file = FileAccess.open(path, FileAccess.READ)
if OS.has_feature("web") and not autosave:
var file := FileAccess.open(path, FileAccess.READ)
if FileAccess.get_open_error() == OK:
var file_data := Array(file.get_buffer(file.get_length()))
var file_data := file.get_buffer(file.get_length())
JavaScriptBridge.download_buffer(file_data, path.get_file())
file.close()
# Remove the .pxo file from memory, as we don't need it anymore
var browser_dir := DirAccess.open(path)
browser_dir.remove(path)
DirAccess.remove_absolute(path)

if autosave:
Global.notification_label("File autosaved")
Expand Down Expand Up @@ -688,7 +779,7 @@ func _on_Autosave_timeout() -> void:
)

store_backup_path(i)
save_pxo_file(backup_save_paths[i], true, true, Global.projects[i])
save_pxo_file(backup_save_paths[i], true, false, Global.projects[i])


## Backup paths are stored in two ways:
Expand Down
10 changes: 0 additions & 10 deletions src/Classes/BaseCel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,6 @@ func deserialize(dict: Dictionary) -> void:
opacity = dict["opacity"]


## Used to save cel image/thumbnail during saving of a pxo file.
func save_image_data_to_pxo(_file: FileAccess) -> void:
return


## Used to load cel image/thumbnail during loading of a pxo file.
func load_image_data_from_pxo(_file: FileAccess, _project_size: Vector2i) -> void:
return


## Used to perform cleanup after a cel is removed.
func on_remove() -> void:
pass
Expand Down
9 changes: 0 additions & 9 deletions src/Classes/Cel3D.gd
Original file line number Diff line number Diff line change
Expand Up @@ -290,15 +290,6 @@ func on_remove() -> void:
viewport.queue_free()


func save_image_data_to_pxo(file: FileAccess) -> void:
file.store_buffer(get_image().get_data())


## Don't do anything with it, just read it so that the file can move on
func load_image_data_from_pxo(file: FileAccess, project_size: Vector2i) -> void:
file.get_buffer(project_size.x * project_size.y * 4)


func instantiate_cel_button() -> Node:
return Global.cel_3d_button_node.instantiate()

Expand Down
12 changes: 0 additions & 12 deletions src/Classes/PixelCel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,6 @@ func update_texture() -> void:
super.update_texture()


func save_image_data_to_pxo(file: FileAccess) -> void:
file.store_buffer(image.get_data())


func load_image_data_from_pxo(file: FileAccess, project_size: Vector2i) -> void:
var buffer := file.get_buffer(project_size.x * project_size.y * 4)
image = Image.create_from_data(
project_size.x, project_size.y, false, Image.FORMAT_RGBA8, buffer
)
image_changed(image)


func instantiate_cel_button() -> Node:
return Global.pixel_cel_button_node.instantiate()

Expand Down
Loading