mirror of
https://github.com/blender/blender
synced 2026-11-01 03:16:58 +03:00
This commit adds support to the autosave functionality to handle images and allows autosave to work in Texture paint. This requires two main changes: * Packing all edited & editable images. * Allow autosave to function when the most recent step is an image undo step. The majority of this PR addresses the first change. For all images that are to be autosaved, we pack to a new field on the `Image`, `autosave_packedfiles` and mark the datablock with a new flag, `IMA_AUTOSAVE_TEMPPACK`. This field is used to prepopulate the `Image` datablock runtime cache on file load, which then marks the ImBuf as dirty, so the user will be informed to either save or discard changes. If an image is saved, packed, or reloaded, the autosave information (both the flag and the autosave packed file list) is cleared. Additionally, the autosave information is cleared when the cache is populated when loading the file. As a similar area of concern, we also do this when creating the recovery file, `quit.blend` Part of #134518 Pull Request: https://projects.blender.org/blender/blender/pulls/152453
262 lines
9.7 KiB
Python
262 lines
9.7 KiB
Python
# SPDX-FileCopyrightText: 2026 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
import unittest
|
|
|
|
# ./blender.bin --background --python tests/python/bl_blendfile_autosave.py -- --output-dir=/tmp/
|
|
import bpy
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
|
|
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
from bl_blendfile_utils import TestHelper
|
|
|
|
|
|
class TestBlendFileAutosave(TestHelper):
|
|
def __init__(self, args):
|
|
super().__init__(args)
|
|
|
|
@staticmethod
|
|
def find_autosave_path(base_dir, base_filename):
|
|
pattern = re.compile("{}_[0-9]+_autosave.blend".format(base_filename))
|
|
|
|
paths = os.listdir(base_dir)
|
|
for path in paths:
|
|
if pattern.match(path):
|
|
return os.path.join(base_dir, path)
|
|
|
|
return None
|
|
|
|
def test_autosave_restore(self):
|
|
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
|
|
orig_mesh = bpy.data.meshes.new("OrigCubeMesh")
|
|
orig_mesh.use_fake_user = True
|
|
|
|
output_dir = self.args.output_dir
|
|
self.ensure_path(output_dir)
|
|
bpy.context.preferences.filepaths.temporary_directory = output_dir
|
|
|
|
output_path = os.path.join(output_dir, "blendfile_autosave.blend")
|
|
|
|
orig_data = self.blender_data_to_tuple(bpy.data, "orig_data")
|
|
|
|
bpy.ops.wm.save_as_mainfile(filepath=output_path, check_existing=False, compress=False)
|
|
|
|
new_mesh = bpy.data.meshes.new("NewCubeMesh")
|
|
new_mesh.use_fake_user = True
|
|
|
|
autosave_data = self.blender_data_to_tuple(bpy.data, "autosave_data")
|
|
|
|
bpy.ops.wm.save_auto_save()
|
|
bpy.ops.wm.open_mainfile(filepath=output_path, load_ui=False)
|
|
|
|
read_data = self.blender_data_to_tuple(bpy.data, "read_data")
|
|
|
|
self.assertEqual(read_data, orig_data)
|
|
self.assertNotEqual(read_data, autosave_data)
|
|
self.assertIn("OrigCubeMesh", bpy.data.meshes)
|
|
self.assertNotIn("NewCubeMesh", bpy.data.meshes)
|
|
|
|
autosave_path = self.find_autosave_path(output_dir, "blendfile_autosave")
|
|
self.assertTrue(autosave_path)
|
|
self.assertNotEqual(output_path, autosave_path)
|
|
|
|
retval = bpy.ops.wm.recover_auto_save(filepath=autosave_path)
|
|
self.assertEqual(retval, {'FINISHED'})
|
|
|
|
recover_data = self.blender_data_to_tuple(bpy.data, "recover_data")
|
|
self.assertEqual(recover_data, autosave_data)
|
|
self.assertIn("OrigCubeMesh", bpy.data.meshes)
|
|
self.assertIn("NewCubeMesh", bpy.data.meshes)
|
|
|
|
|
|
class TestBlendFileImageAutosave(TestHelper):
|
|
def __init__(self, args):
|
|
super().__init__(args)
|
|
|
|
def setUpClass(self):
|
|
self.ensure_path(self.args.output_dir)
|
|
self.temp_dir = tempfile.TemporaryDirectory(dir=self.args.output_dir)
|
|
|
|
def tearDownClass(self):
|
|
self.temp_dir.cleanup()
|
|
|
|
@staticmethod
|
|
def find_autosave_path(base_dir, base_filename):
|
|
pattern = re.compile("{}_[0-9]+_autosave.blend".format(base_filename))
|
|
|
|
paths = os.listdir(base_dir)
|
|
for path in paths:
|
|
if pattern.match(path):
|
|
return os.path.join(base_dir, path)
|
|
|
|
return None
|
|
|
|
@staticmethod
|
|
def modify_image(image):
|
|
width, height = image.size
|
|
color = (1.0, 1.0, 1.0, 1.0)
|
|
|
|
pixels = width * height
|
|
for pixel in range(pixels):
|
|
for channel in range(4):
|
|
image.pixels[pixel * 4 + channel] = color[channel]
|
|
|
|
def check_image(self, image, expected_color):
|
|
width, height = image.size
|
|
|
|
pixels = width * height
|
|
|
|
actual_data = list(image.pixels[:])
|
|
expected_data = [0.0] * pixels * 4
|
|
for pixel in range(pixels):
|
|
for channel in range(4):
|
|
expected_data[pixel * 4 + channel] = expected_color[channel]
|
|
|
|
self.assertEqual(actual_data, expected_data)
|
|
|
|
def test_generated_image_restore(self):
|
|
self.ensure_path(self.args.output_dir)
|
|
with tempfile.TemporaryDirectory(dir=self.args.output_dir) as output_dir:
|
|
bpy.context.preferences.filepaths.temporary_directory = output_dir
|
|
output_path = os.path.join(output_dir, "blendfile_autosave_generated_image.blend")
|
|
|
|
# Immediately create the file so we can ensure we're working on a
|
|
# "generated" and not packed image with autosave
|
|
bpy.ops.wm.save_as_mainfile(filepath=output_path, check_existing=False, compress=False)
|
|
|
|
orig_image = bpy.data.images.new("GeneratedImage", 2, 2, alpha=True)
|
|
orig_image.use_fake_user = True
|
|
|
|
self.modify_image(orig_image)
|
|
self.check_image(orig_image, (1.0, 1.0, 1.0, 1.0))
|
|
|
|
bpy.ops.wm.save_auto_save()
|
|
bpy.ops.wm.open_mainfile(filepath=output_path, load_ui=False)
|
|
|
|
# File should not have image, only autosave should have image
|
|
self.assertNotIn("GeneratedImage", bpy.data.images)
|
|
|
|
autosave_path = self.find_autosave_path(output_dir, "blendfile_autosave_generated_image")
|
|
self.assertTrue(autosave_path)
|
|
self.assertNotEqual(output_path, autosave_path)
|
|
retval = bpy.ops.wm.recover_auto_save(filepath=autosave_path)
|
|
self.assertEqual(retval, {'FINISHED'})
|
|
|
|
self.assertIn("GeneratedImage", bpy.data.images)
|
|
self.check_image(bpy.data.images["GeneratedImage"], (1.0, 1.0, 1.0, 1.0))
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=output_path, load_ui=False)
|
|
self.assertNotIn("GeneratedImage", bpy.data.images)
|
|
|
|
def test_packed_image_restore(self):
|
|
self.ensure_path(self.args.output_dir)
|
|
with tempfile.TemporaryDirectory(dir=self.args.output_dir) as output_dir:
|
|
bpy.context.preferences.filepaths.temporary_directory = output_dir
|
|
|
|
input_blendfile_path = os.path.join(self.args.src_test_dir, "autosave_image_test.blend")
|
|
bpy.ops.wm.open_mainfile(filepath=input_blendfile_path)
|
|
|
|
output_path = os.path.join(output_dir, "blendfile_autosave_packed_image.blend")
|
|
bpy.ops.wm.save_as_mainfile(filepath=output_path, check_existing=False, compress=False)
|
|
|
|
self.modify_image(bpy.data.images["PackedImage"])
|
|
self.check_image(bpy.data.images["PackedImage"], (1.0, 1.0, 1.0, 1.0))
|
|
|
|
bpy.ops.wm.save_auto_save()
|
|
bpy.ops.wm.open_mainfile(filepath=output_path, load_ui=False)
|
|
|
|
self.assertIn("PackedImage", bpy.data.images)
|
|
self.check_image(bpy.data.images["PackedImage"], (0.0, 0.0, 0.0, 1.0))
|
|
|
|
autosave_path = self.find_autosave_path(output_dir, "blendfile_autosave_packed_image")
|
|
self.assertTrue(autosave_path)
|
|
self.assertNotEqual(output_path, autosave_path)
|
|
retval = bpy.ops.wm.recover_auto_save(filepath=autosave_path)
|
|
self.assertEqual(retval, {'FINISHED'})
|
|
|
|
# Loading the autosave file should show the "temporary" changes
|
|
self.assertIn("PackedImage", bpy.data.images)
|
|
self.check_image(bpy.data.images["PackedImage"], (1.0, 1.0, 1.0, 1.0))
|
|
|
|
def test_external_image_restore(self):
|
|
self.ensure_path(self.args.output_dir)
|
|
with tempfile.TemporaryDirectory(dir=self.args.output_dir) as output_dir:
|
|
bpy.context.preferences.filepaths.temporary_directory = output_dir
|
|
|
|
input_blendfile_path = os.path.join(self.args.src_test_dir, "autosave_image_test.blend")
|
|
bpy.ops.wm.open_mainfile(filepath=input_blendfile_path)
|
|
|
|
output_path = os.path.join(output_dir, "blendfile_autosave_external_image.blend")
|
|
bpy.ops.wm.save_as_mainfile(filepath=output_path, check_existing=False, compress=False)
|
|
|
|
self.modify_image(bpy.data.images["ExternalImage"])
|
|
self.check_image(bpy.data.images["ExternalImage"], (1.0, 1.0, 1.0, 1.0))
|
|
|
|
bpy.ops.wm.save_auto_save()
|
|
bpy.ops.wm.open_mainfile(filepath=output_path, load_ui=False)
|
|
|
|
self.assertIn("ExternalImage", bpy.data.images)
|
|
self.check_image(bpy.data.images["ExternalImage"], (0.0, 0.0, 0.0, 1.0))
|
|
|
|
autosave_path = self.find_autosave_path(output_dir, "blendfile_autosave_external_image")
|
|
self.assertTrue(autosave_path)
|
|
self.assertNotEqual(output_path, autosave_path)
|
|
retval = bpy.ops.wm.recover_auto_save(filepath=autosave_path)
|
|
self.assertEqual(retval, {'FINISHED'})
|
|
|
|
# Loading the autosave file should show the "temporary" changes
|
|
self.assertIn("ExternalImage", bpy.data.images)
|
|
self.check_image(bpy.data.images["ExternalImage"], (1.0, 1.0, 1.0, 1.0))
|
|
|
|
# Loading the actual file should show the original data
|
|
bpy.data.images["ExternalImage"].reload()
|
|
self.check_image(bpy.data.images["ExternalImage"], (0.0, 0.0, 0.0, 1.0))
|
|
|
|
return
|
|
|
|
|
|
TESTS = (
|
|
TestBlendFileAutosave,
|
|
TestBlendFileImageAutosave
|
|
)
|
|
|
|
|
|
def argparse_create():
|
|
import argparse
|
|
|
|
# When --help or no args are given, print this help
|
|
description = "Test basic IO of autosaving a .blend file."
|
|
parser = argparse.ArgumentParser(description=description)
|
|
parser.add_argument(
|
|
"--src-test-dir",
|
|
dest="src_test_dir",
|
|
help="Root tests directory to search for blendfiles",
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
dest="output_dir",
|
|
help="Where to output temp saved files",
|
|
required=True,
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def main():
|
|
args = argparse_create().parse_args()
|
|
|
|
# Don't write thumbnails into the home directory.
|
|
bpy.context.preferences.filepaths.file_preview_type = 'NONE'
|
|
|
|
for Test in TESTS:
|
|
Test(args).run_all_tests()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
|
main()
|