diff --git a/flamenco_worker/commands.py b/flamenco_worker/commands.py index 101779fd8fa145a7c93e827fc912f1c0dde5354a..4e05797f61f0c74e35c565124b10a23f93b9de0b 100644 --- a/flamenco_worker/commands.py +++ b/flamenco_worker/commands.py @@ -1070,11 +1070,24 @@ class EXRSequenceToJPEGCommand(BlenderRenderCommand): if not self.pyscript.exists(): raise FileNotFoundError(f'Resource script {self.pyscript} cannot be found') - exr_directory, err = self._setting(settings, 'exr_directory', True) + exr_glob, err = self._setting(settings, 'exr_glob', False) if err: return err - if not exr_directory: - return '"exr_directory" may not be empty' + + # Only for backward compatibility. Should not be used. + exr_directory, err = self._setting(settings, 'exr_directory', False) + if err: + return err + + if not exr_glob and not exr_directory: + return '"exr_glob" may not be empty' + if exr_glob and exr_directory: + # Normally I would say 'use either one or the other, not both', but + # in this case 'exr_directory' is deprecated and shouldn't be used. + return 'Just pass "exr_glob", do not use "exr_directory"' + + if exr_directory: + settings['exr_glob'] = str(Path(exr_directory) / '*.exr') output_pattern, err = self._setting(settings, 'output_pattern', False, default='preview-######.jpg') @@ -1089,7 +1102,7 @@ class EXRSequenceToJPEGCommand(BlenderRenderCommand): '--python-exit-code', '32', '--python', str(self.pyscript), '--', - '--exr-dir', settings['exr_directory'], + '--exr-glob', settings['exr_glob'], '--output-pattern', settings['output_pattern'], ] diff --git a/flamenco_worker/resources/exr_sequence_to_jpeg.py b/flamenco_worker/resources/exr_sequence_to_jpeg.py index 36466f9c79c659a77a82663a5c92715b7c708e4d..e8770bbe2e1f9c87e994638decf9da53405271ec 100644 --- a/flamenco_worker/resources/exr_sequence_to_jpeg.py +++ b/flamenco_worker/resources/exr_sequence_to_jpeg.py @@ -1,8 +1,11 @@ # This file is supposed to run inside Blender: -# blender thefile.blend --python /path/to/exr_sequence_to_jpeg.py -- --exr-dir /path/to/exr/files +# blender thefile.blend \ +# --python /path/to/exr_sequence_to_jpeg.py \ +# -- --exr-pattern /path/to/exr/files/prefix-*.exr import argparse import pathlib +import re import sys import bpy @@ -10,15 +13,16 @@ import bpy # Find the EXR files to process. dashdash_index = sys.argv.index('--') parser = argparse.ArgumentParser() -parser.add_argument('--exr-dir') +parser.add_argument('--exr-glob') parser.add_argument('--output-pattern') cli_args, _ = parser.parse_known_args(sys.argv[dashdash_index + 1:]) -imgdir = pathlib.Path(cli_args.exr_dir) -exr_files = list(imgdir.glob('*.exr')) +exr_glob = pathlib.Path(cli_args.exr_glob) +imgdir = exr_glob.parent +exr_files = sorted(imgdir.glob(exr_glob.name)) if not exr_files: - raise ValueError(f'No *.exr files found in {cli_args.exr_dir}') + raise ValueError(f'No files found for pattern {exr_glob}') # Create a copy of the scene without data, so we can fill the sequence editor # with an image sequence. @@ -32,11 +36,22 @@ se = scene.sequence_editor_create() # This assumes the files are named '000020.exr' etc. min_frame = float('inf') max_frame = float('-inf') + +# Interpret the last continuous string of digits as frame number. +frame_nr_re = re.compile(r'[0-9]+$') +print(f'Loading {len(exr_files)} EXR files:') for file in exr_files: - frame_num = int(file.stem, 10) + match = frame_nr_re.search(file.stem) + if not match: + raise ValueError(f'Unable to find frame number in filename {file.name}') + frame_num = int(match.group(), 10) min_frame = min(min_frame, frame_num) max_frame = max(max_frame, frame_num) + print(f' - {file} -> frame {frame_num}') se.sequences.new_image(file.name, str(file), 1, frame_num) +print(f'Found files for frame range {min_frame}-{max_frame}') +print() +sys.stdout.flush() scene.frame_start = min_frame scene.frame_end = max_frame diff --git a/tests/test_commands_exr_sequence_to_jpeg.py b/tests/test_commands_exr_sequence_to_jpeg.py new file mode 100644 index 0000000000000000000000000000000000000000..2b55d5d65ba7982bb3f34fc45bbfaa5e26554b93 --- /dev/null +++ b/tests/test_commands_exr_sequence_to_jpeg.py @@ -0,0 +1,95 @@ +from pathlib import Path +import subprocess +import tempfile +from unittest import mock + +from unittest.mock import patch + +from tests.test_runner import AbstractCommandTest + + +class BlenderRenderProgressiveTest(AbstractCommandTest): + thisfile = Path(__file__).as_posix() + + def setUp(self): + super().setUp() + + from flamenco_worker.commands import EXRSequenceToJPEGCommand + + self.cmd = EXRSequenceToJPEGCommand( + worker=self.fworker, + task_id='12345', + command_idx=0, + ) + + def test_exr_glob(self): + from tests.mock_responses import CoroMock + + filepath = Path(__file__).parent.as_posix() + settings = { + # Point blender_cmd to this file so that we're sure it exists. + 'blender_cmd': f'{self.thisfile!r} --with --cli="args for CLI"', + 'filepath': filepath, + 'exr_glob': '/some/path/to/files-*.exr', + 'output_pattern': 'preview-######', + } + + cse = CoroMock(...) + cse.coro.return_value.wait = CoroMock(return_value=0) + cse.coro.return_value.pid = 47 + with patch('asyncio.create_subprocess_exec', new=cse) as mock_cse: + self.loop.run_until_complete(self.cmd.run(settings)) + + mock_cse.assert_called_once_with( + self.thisfile, + '--with', + '--cli=args for CLI', + '--enable-autoexec', + '-noaudio', + '--background', + filepath, + '--python-exit-code', '32', + '--python', str(self.cmd.pyscript), + '--', + '--exr-glob', '/some/path/to/files-*.exr', + '--output-pattern', 'preview-######', + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + def test_exr_directory(self): + from tests.mock_responses import CoroMock + + filepath = Path(__file__).parent.as_posix() + settings = { + # Point blender_cmd to this file so that we're sure it exists. + 'blender_cmd': f'{self.thisfile!r} --with --cli="args for CLI"', + 'filepath': filepath, + 'exr_directory': '/some/path/to/exr', + 'output_pattern': 'preview-######', + } + + cse = CoroMock(...) + cse.coro.return_value.wait = CoroMock(return_value=0) + cse.coro.return_value.pid = 47 + with patch('asyncio.create_subprocess_exec', new=cse) as mock_cse: + self.loop.run_until_complete(self.cmd.run(settings)) + + mock_cse.assert_called_once_with( + self.thisfile, + '--with', + '--cli=args for CLI', + '--enable-autoexec', + '-noaudio', + '--background', + filepath, + '--python-exit-code', '32', + '--python', str(self.cmd.pyscript), + '--', + '--exr-glob', '/some/path/to/exr/*.exr', + '--output-pattern', 'preview-######', + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + )