mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
682 lines
24 KiB
Python
682 lines
24 KiB
Python
import os
|
|
import shutil
|
|
import collections
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
def backwards_id_conversion(data_by_layer_id):
|
|
"""Convert layer ids to strings from integers."""
|
|
for key in tuple(data_by_layer_id.keys()):
|
|
if not isinstance(key, str):
|
|
data_by_layer_id[str(key)] = data_by_layer_id.pop(key)
|
|
|
|
|
|
def get_frame_filename_template(frame_end, filename_prefix=None, ext=None):
|
|
"""Get file template with frame key for rendered files.
|
|
|
|
This is simple template contains `{frame}{ext}` for sequential outputs
|
|
and `single_file{ext}` for single file output. Output is rendered to
|
|
temporary folder so filename should not matter as integrator change
|
|
them.
|
|
"""
|
|
frame_padding = 4
|
|
frame_end_str_len = len(str(frame_end))
|
|
if frame_end_str_len > frame_padding:
|
|
frame_padding = frame_end_str_len
|
|
|
|
ext = ext or ".png"
|
|
filename_prefix = filename_prefix or ""
|
|
|
|
return "{}{{frame:0>{}}}{}".format(filename_prefix, frame_padding, ext)
|
|
|
|
|
|
def get_layer_pos_filename_template(range_end, filename_prefix=None, ext=None):
|
|
filename_prefix = filename_prefix or ""
|
|
new_filename_prefix = filename_prefix + "pos_{pos}."
|
|
return get_frame_filename_template(range_end, new_filename_prefix, ext)
|
|
|
|
|
|
def _calculate_pre_behavior_copy(
|
|
range_start, exposure_frames, pre_beh,
|
|
layer_frame_start, layer_frame_end,
|
|
output_idx_by_frame_idx
|
|
):
|
|
"""Calculate frames before first exposure frame based on pre behavior.
|
|
|
|
Function may skip whole processing if first exposure frame is before
|
|
layer's first frame. In that case pre behavior does not make sense.
|
|
|
|
Args:
|
|
range_start(int): First frame of range which should be rendered.
|
|
exposure_frames(list): List of all exposure frames on layer.
|
|
pre_beh(str): Pre behavior of layer (enum of 4 strings).
|
|
layer_frame_start(int): First frame of layer.
|
|
layer_frame_end(int): Last frame of layer.
|
|
output_idx_by_frame_idx(dict): References to already prepared frames
|
|
and where result will be stored.
|
|
"""
|
|
# Check if last layer frame is after range end
|
|
if layer_frame_start < range_start:
|
|
return
|
|
|
|
first_exposure_frame = min(exposure_frames)
|
|
# Skip if last exposure frame is after range end
|
|
if first_exposure_frame < range_start:
|
|
return
|
|
|
|
# Calculate frame count of layer
|
|
frame_count = layer_frame_end - layer_frame_start + 1
|
|
|
|
if pre_beh == "none":
|
|
# Just fill all frames from last exposure frame to range end with None
|
|
for frame_idx in range(range_start, layer_frame_start):
|
|
output_idx_by_frame_idx[frame_idx] = None
|
|
|
|
elif pre_beh == "hold":
|
|
# Keep first frame for whole time
|
|
for frame_idx in range(range_start, layer_frame_start):
|
|
output_idx_by_frame_idx[frame_idx] = first_exposure_frame
|
|
|
|
elif pre_beh in ("loop", "repeat"):
|
|
# Loop backwards from last frame of layer
|
|
for frame_idx in reversed(range(range_start, layer_frame_start)):
|
|
eq_frame_idx_offset = (
|
|
(layer_frame_end - frame_idx) % frame_count
|
|
)
|
|
eq_frame_idx = layer_frame_end - eq_frame_idx_offset
|
|
output_idx_by_frame_idx[frame_idx] = eq_frame_idx
|
|
|
|
elif pre_beh == "pingpong":
|
|
half_seq_len = frame_count - 1
|
|
seq_len = half_seq_len * 2
|
|
for frame_idx in reversed(range(range_start, layer_frame_start)):
|
|
eq_frame_idx_offset = (layer_frame_start - frame_idx) % seq_len
|
|
if eq_frame_idx_offset > half_seq_len:
|
|
eq_frame_idx_offset = (seq_len - eq_frame_idx_offset)
|
|
eq_frame_idx = layer_frame_start + eq_frame_idx_offset
|
|
output_idx_by_frame_idx[frame_idx] = eq_frame_idx
|
|
|
|
|
|
def _calculate_post_behavior_copy(
|
|
range_end, exposure_frames, post_beh,
|
|
layer_frame_start, layer_frame_end,
|
|
output_idx_by_frame_idx
|
|
):
|
|
"""Calculate frames after last frame of layer based on post behavior.
|
|
|
|
Function may skip whole processing if last layer frame is after range_end.
|
|
In that case post behavior does not make sense.
|
|
|
|
Args:
|
|
range_end(int): Last frame of range which should be rendered.
|
|
exposure_frames(list): List of all exposure frames on layer.
|
|
post_beh(str): Post behavior of layer (enum of 4 strings).
|
|
layer_frame_start(int): First frame of layer.
|
|
layer_frame_end(int): Last frame of layer.
|
|
output_idx_by_frame_idx(dict): References to already prepared frames
|
|
and where result will be stored.
|
|
"""
|
|
# Check if last layer frame is after range end
|
|
if layer_frame_end >= range_end:
|
|
return
|
|
|
|
last_exposure_frame = max(exposure_frames)
|
|
# Skip if last exposure frame is after range end
|
|
# - this is probably irrelevant with layer frame end check?
|
|
if last_exposure_frame >= range_end:
|
|
return
|
|
|
|
# Calculate frame count of layer
|
|
frame_count = layer_frame_end - layer_frame_start + 1
|
|
|
|
if post_beh == "none":
|
|
# Just fill all frames from last exposure frame to range end with None
|
|
for frame_idx in range(layer_frame_end + 1, range_end + 1):
|
|
output_idx_by_frame_idx[frame_idx] = None
|
|
|
|
elif post_beh == "hold":
|
|
# Keep last exposure frame to the end
|
|
for frame_idx in range(layer_frame_end + 1, range_end + 1):
|
|
output_idx_by_frame_idx[frame_idx] = last_exposure_frame
|
|
|
|
elif post_beh in ("loop", "repeat"):
|
|
# Loop backwards from last frame of layer
|
|
for frame_idx in range(layer_frame_end + 1, range_end + 1):
|
|
eq_frame_idx = frame_idx % frame_count
|
|
output_idx_by_frame_idx[frame_idx] = eq_frame_idx
|
|
|
|
elif post_beh == "pingpong":
|
|
half_seq_len = frame_count - 1
|
|
seq_len = half_seq_len * 2
|
|
for frame_idx in range(layer_frame_end + 1, range_end + 1):
|
|
eq_frame_idx_offset = (frame_idx - layer_frame_end) % seq_len
|
|
if eq_frame_idx_offset > half_seq_len:
|
|
eq_frame_idx_offset = seq_len - eq_frame_idx_offset
|
|
eq_frame_idx = layer_frame_end - eq_frame_idx_offset
|
|
output_idx_by_frame_idx[frame_idx] = eq_frame_idx
|
|
|
|
|
|
def _calculate_in_range_frames(
|
|
range_start, range_end,
|
|
exposure_frames, layer_frame_end,
|
|
output_idx_by_frame_idx
|
|
):
|
|
"""Calculate frame references in defined range.
|
|
|
|
Function may skip whole processing if last layer frame is after range_end.
|
|
In that case post behavior does not make sense.
|
|
|
|
Args:
|
|
range_start(int): First frame of range which should be rendered.
|
|
range_end(int): Last frame of range which should be rendered.
|
|
exposure_frames(list): List of all exposure frames on layer.
|
|
layer_frame_end(int): Last frame of layer.
|
|
output_idx_by_frame_idx(dict): References to already prepared frames
|
|
and where result will be stored.
|
|
"""
|
|
# Calculate in range frames
|
|
in_range_frames = []
|
|
for frame_idx in exposure_frames:
|
|
if range_start <= frame_idx <= range_end:
|
|
output_idx_by_frame_idx[frame_idx] = frame_idx
|
|
in_range_frames.append(frame_idx)
|
|
|
|
if in_range_frames:
|
|
first_in_range_frame = min(in_range_frames)
|
|
# Calculate frames from first exposure frames to range end or last
|
|
# frame of layer (post behavior should be calculated since that time)
|
|
previous_exposure = first_in_range_frame
|
|
for frame_idx in range(first_in_range_frame, range_end + 1):
|
|
if frame_idx > layer_frame_end:
|
|
break
|
|
|
|
if frame_idx in exposure_frames:
|
|
previous_exposure = frame_idx
|
|
else:
|
|
output_idx_by_frame_idx[frame_idx] = previous_exposure
|
|
|
|
# There can be frames before first exposure frame in range
|
|
# First check if we don't alreade have first range frame filled
|
|
if range_start in output_idx_by_frame_idx:
|
|
return
|
|
|
|
first_exposure_frame = max(exposure_frames)
|
|
last_exposure_frame = max(exposure_frames)
|
|
# Check if is first exposure frame smaller than defined range
|
|
# if not then skip
|
|
if first_exposure_frame >= range_start:
|
|
return
|
|
|
|
# Check is if last exposure frame is also before range start
|
|
# in that case we can't use fill frames before out range
|
|
if last_exposure_frame < range_start:
|
|
return
|
|
|
|
closest_exposure_frame = first_exposure_frame
|
|
for frame_idx in exposure_frames:
|
|
if frame_idx >= range_start:
|
|
break
|
|
if frame_idx > closest_exposure_frame:
|
|
closest_exposure_frame = frame_idx
|
|
|
|
output_idx_by_frame_idx[closest_exposure_frame] = closest_exposure_frame
|
|
for frame_idx in range(range_start, range_end + 1):
|
|
if frame_idx in output_idx_by_frame_idx:
|
|
break
|
|
output_idx_by_frame_idx[frame_idx] = closest_exposure_frame
|
|
|
|
|
|
def _cleanup_frame_references(output_idx_by_frame_idx):
|
|
"""Cleanup frame references to frame reference.
|
|
|
|
Cleanup not direct references to rendered frame.
|
|
```
|
|
// Example input
|
|
{
|
|
1: 1,
|
|
2: 1,
|
|
3: 2
|
|
}
|
|
// Result
|
|
{
|
|
1: 1,
|
|
2: 1,
|
|
3: 1 // Changed reference to final rendered frame
|
|
}
|
|
```
|
|
Result is dictionary where keys leads to frame that should be rendered.
|
|
"""
|
|
for frame_idx in tuple(output_idx_by_frame_idx.keys()):
|
|
reference_idx = output_idx_by_frame_idx[frame_idx]
|
|
# Skip transparent frames
|
|
if reference_idx is None or reference_idx == frame_idx:
|
|
continue
|
|
|
|
real_reference_idx = reference_idx
|
|
_tmp_reference_idx = reference_idx
|
|
while True:
|
|
_temp = output_idx_by_frame_idx[_tmp_reference_idx]
|
|
if _temp == _tmp_reference_idx:
|
|
real_reference_idx = _tmp_reference_idx
|
|
break
|
|
_tmp_reference_idx = _temp
|
|
|
|
if real_reference_idx != reference_idx:
|
|
output_idx_by_frame_idx[frame_idx] = real_reference_idx
|
|
|
|
|
|
def _cleanup_out_range_frames(output_idx_by_frame_idx, range_start, range_end):
|
|
"""Cleanup frame references to frames out of passed range.
|
|
|
|
First available frame in range is used
|
|
```
|
|
// Example input. Range 2-3
|
|
{
|
|
1: 1,
|
|
2: 1,
|
|
3: 1
|
|
}
|
|
// Result
|
|
{
|
|
2: 2, // Redirect to self as is first that reference out range
|
|
3: 2 // Redirect to first redirected frame
|
|
}
|
|
```
|
|
Result is dictionary where keys leads to frame that should be rendered.
|
|
"""
|
|
in_range_frames_by_out_frames = collections.defaultdict(set)
|
|
out_range_frames = set()
|
|
for frame_idx in tuple(output_idx_by_frame_idx.keys()):
|
|
# Skip frames that are already out of range
|
|
if frame_idx < range_start or frame_idx > range_end:
|
|
out_range_frames.add(frame_idx)
|
|
continue
|
|
|
|
reference_idx = output_idx_by_frame_idx[frame_idx]
|
|
# Skip transparent frames
|
|
if reference_idx is None:
|
|
continue
|
|
|
|
# Skip references in range
|
|
if reference_idx < range_start or reference_idx > range_end:
|
|
in_range_frames_by_out_frames[reference_idx].add(frame_idx)
|
|
|
|
for reference_idx in tuple(in_range_frames_by_out_frames.keys()):
|
|
frame_indexes = in_range_frames_by_out_frames.pop(reference_idx)
|
|
new_reference = None
|
|
for frame_idx in frame_indexes:
|
|
if new_reference is None:
|
|
new_reference = frame_idx
|
|
output_idx_by_frame_idx[frame_idx] = new_reference
|
|
|
|
# Finally remove out of range frames
|
|
for frame_idx in out_range_frames:
|
|
output_idx_by_frame_idx.pop(frame_idx)
|
|
|
|
|
|
def calculate_layer_frame_references(
|
|
range_start, range_end,
|
|
layer_frame_start,
|
|
layer_frame_end,
|
|
exposure_frames,
|
|
pre_beh, post_beh
|
|
):
|
|
"""Calculate frame references for one layer based on it's data.
|
|
|
|
Output is dictionary where key is frame index referencing to rendered frame
|
|
index. If frame index should be rendered then is referencing to self.
|
|
|
|
```
|
|
// Example output
|
|
{
|
|
1: 1, // Reference to self - will be rendered
|
|
2: 1, // Reference to frame 1 - will be copied
|
|
3: 1, // Reference to frame 1 - will be copied
|
|
4: 4, // Reference to self - will be rendered
|
|
...
|
|
20: 4 // Reference to frame 4 - will be copied
|
|
21: None // Has reference to None - transparent image
|
|
}
|
|
```
|
|
|
|
Args:
|
|
range_start(int): First frame of range which should be rendered.
|
|
range_end(int): Last frame of range which should be rendered.
|
|
layer_frame_start(int)L First frame of layer.
|
|
layer_frame_end(int): Last frame of layer.
|
|
exposure_frames(list): List of all exposure frames on layer.
|
|
pre_beh(str): Pre behavior of layer (enum of 4 strings).
|
|
post_beh(str): Post behavior of layer (enum of 4 strings).
|
|
"""
|
|
# Output variable
|
|
output_idx_by_frame_idx = {}
|
|
# Skip if layer does not have any exposure frames
|
|
if not exposure_frames:
|
|
return output_idx_by_frame_idx
|
|
|
|
# First calculate in range frames
|
|
_calculate_in_range_frames(
|
|
range_start, range_end,
|
|
exposure_frames, layer_frame_end,
|
|
output_idx_by_frame_idx
|
|
)
|
|
# Calculate frames by pre behavior of layer
|
|
_calculate_pre_behavior_copy(
|
|
range_start, exposure_frames, pre_beh,
|
|
layer_frame_start, layer_frame_end,
|
|
output_idx_by_frame_idx
|
|
)
|
|
# Calculate frames by post behavior of layer
|
|
_calculate_post_behavior_copy(
|
|
range_end, exposure_frames, post_beh,
|
|
layer_frame_start, layer_frame_end,
|
|
output_idx_by_frame_idx
|
|
)
|
|
# Cleanup of referenced frames
|
|
_cleanup_frame_references(output_idx_by_frame_idx)
|
|
|
|
# Remove frames out of range
|
|
_cleanup_out_range_frames(output_idx_by_frame_idx, range_start, range_end)
|
|
|
|
return output_idx_by_frame_idx
|
|
|
|
|
|
def calculate_layers_extraction_data(
|
|
layers_data,
|
|
exposure_frames_by_layer_id,
|
|
behavior_by_layer_id,
|
|
range_start,
|
|
range_end,
|
|
skip_not_visible=True,
|
|
filename_prefix=None,
|
|
ext=None
|
|
):
|
|
"""Calculate extraction data for passed layers data.
|
|
|
|
```
|
|
{
|
|
<layer_id>: {
|
|
"frame_references": {...},
|
|
"filenames_by_frame_index": {...}
|
|
},
|
|
...
|
|
}
|
|
```
|
|
|
|
Frame references contains frame index reference to rendered frame index.
|
|
|
|
Filename by frame index represents filename under which should be frame
|
|
stored. Directory is not handled here because each usage may need different
|
|
approach.
|
|
|
|
Args:
|
|
layers_data(list): Layers data loaded from TVPaint.
|
|
exposure_frames_by_layer_id(dict): Exposure frames of layers stored by
|
|
layer id.
|
|
behavior_by_layer_id(dict): Pre and Post behavior of layers stored by
|
|
layer id.
|
|
range_start(int): First frame of rendered range.
|
|
range_end(int): Last frame of rendered range.
|
|
skip_not_visible(bool): Skip calculations for hidden layers (Skipped
|
|
by default).
|
|
filename_prefix(str): Prefix before filename.
|
|
ext(str): Extension which filenames will have ('.png' is default).
|
|
|
|
Returns:
|
|
dict: Prepared data for rendering by layer position.
|
|
"""
|
|
# Make sure layer ids are strings
|
|
# backwards compatibility when layer ids were integers
|
|
backwards_id_conversion(exposure_frames_by_layer_id)
|
|
backwards_id_conversion(behavior_by_layer_id)
|
|
|
|
layer_template = get_layer_pos_filename_template(
|
|
range_end, filename_prefix, ext
|
|
)
|
|
output = {}
|
|
for layer_data in layers_data:
|
|
if skip_not_visible and not layer_data["visible"]:
|
|
continue
|
|
|
|
orig_layer_id = layer_data["layer_id"]
|
|
layer_id = str(orig_layer_id)
|
|
|
|
# Skip if does not have any exposure frames (empty layer)
|
|
exposure_frames = exposure_frames_by_layer_id[layer_id]
|
|
if not exposure_frames:
|
|
continue
|
|
|
|
layer_position = layer_data["position"]
|
|
layer_frame_start = layer_data["frame_start"]
|
|
layer_frame_end = layer_data["frame_end"]
|
|
|
|
layer_behavior = behavior_by_layer_id[layer_id]
|
|
|
|
pre_behavior = layer_behavior["pre"]
|
|
post_behavior = layer_behavior["post"]
|
|
|
|
frame_references = calculate_layer_frame_references(
|
|
range_start, range_end,
|
|
layer_frame_start,
|
|
layer_frame_end,
|
|
exposure_frames,
|
|
pre_behavior, post_behavior
|
|
)
|
|
# All values in 'frame_references' reference to a frame that must be
|
|
# rendered out
|
|
frames_to_render = set(frame_references.values())
|
|
# Remove 'None' reference (transparent image)
|
|
if None in frames_to_render:
|
|
frames_to_render.remove(None)
|
|
|
|
# Skip layer if has nothing to render
|
|
if not frames_to_render:
|
|
continue
|
|
|
|
# All filenames that should be as output (not final output)
|
|
filename_frames = (
|
|
set(range(range_start, range_end + 1))
|
|
| frames_to_render
|
|
)
|
|
filenames_by_frame_index = {}
|
|
for frame_idx in filename_frames:
|
|
filenames_by_frame_index[frame_idx] = layer_template.format(
|
|
pos=layer_position,
|
|
frame=frame_idx
|
|
)
|
|
|
|
# Store objects under the layer id
|
|
output[orig_layer_id] = {
|
|
"frame_references": frame_references,
|
|
"filenames_by_frame_index": filenames_by_frame_index
|
|
}
|
|
return output
|
|
|
|
|
|
def create_transparent_image_from_source(src_filepath, dst_filepath):
|
|
"""Create transparent image of same type and size as source image."""
|
|
img_obj = Image.open(src_filepath)
|
|
painter = ImageDraw.Draw(img_obj)
|
|
painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0))
|
|
img_obj.save(dst_filepath)
|
|
|
|
|
|
def fill_reference_frames(frame_references, filepaths_by_frame):
|
|
# Store path to first transparent image if there is any
|
|
for frame_idx, ref_idx in frame_references.items():
|
|
# Frame referencing to self should be rendered and used as source
|
|
# and reference indexes with None can't be filled
|
|
if ref_idx is None or frame_idx == ref_idx:
|
|
continue
|
|
|
|
# Get destination filepath
|
|
src_filepath = filepaths_by_frame[ref_idx]
|
|
dst_filepath = filepaths_by_frame[frame_idx]
|
|
|
|
if hasattr(os, "link"):
|
|
os.link(src_filepath, dst_filepath)
|
|
else:
|
|
shutil.copy(src_filepath, dst_filepath)
|
|
|
|
|
|
def copy_render_file(src_path, dst_path):
|
|
"""Create copy file of an image."""
|
|
if hasattr(os, "link"):
|
|
os.link(src_path, dst_path)
|
|
else:
|
|
shutil.copy(src_path, dst_path)
|
|
|
|
|
|
def cleanup_rendered_layers(filepaths_by_layer_id):
|
|
"""Delete all files for each individual layer files after compositing."""
|
|
# Collect all filepaths from data
|
|
all_filepaths = []
|
|
for filepaths_by_frame in filepaths_by_layer_id.values():
|
|
all_filepaths.extend(filepaths_by_frame.values())
|
|
|
|
# Loop over loop
|
|
for filepath in set(all_filepaths):
|
|
if filepath is not None and os.path.exists(filepath):
|
|
os.remove(filepath)
|
|
|
|
|
|
def composite_rendered_layers(
|
|
layers_data, filepaths_by_layer_id,
|
|
range_start, range_end,
|
|
dst_filepaths_by_frame, cleanup=True
|
|
):
|
|
"""Composite multiple rendered layers by their position.
|
|
|
|
Result is single frame sequence with transparency matching content
|
|
created in TVPaint. Missing source filepaths are replaced with transparent
|
|
images but at least one image must be rendered and exist.
|
|
|
|
Function can be used even if single layer was created to fill transparent
|
|
filepaths.
|
|
|
|
Args:
|
|
layers_data(list): Layers data loaded from TVPaint.
|
|
filepaths_by_layer_id(dict): Rendered filepaths stored by frame index
|
|
per layer id. Used as source for compositing.
|
|
range_start(int): First frame of rendered range.
|
|
range_end(int): Last frame of rendered range.
|
|
dst_filepaths_by_frame(dict): Output filepaths by frame where final
|
|
image after compositing will be stored. Path must not clash with
|
|
source filepaths.
|
|
cleanup(bool): Remove all source filepaths when done with compositing.
|
|
"""
|
|
# Prepare layers by their position
|
|
# - position tells in which order will compositing happen
|
|
layer_ids_by_position = {}
|
|
for layer in layers_data:
|
|
layer_position = layer["position"]
|
|
layer_ids_by_position[layer_position] = layer["layer_id"]
|
|
|
|
# Sort layer positions
|
|
sorted_positions = tuple(reversed(sorted(layer_ids_by_position.keys())))
|
|
# Prepare variable where filepaths without any rendered content
|
|
# - transparent will be created
|
|
transparent_filepaths = set()
|
|
# Store first final filepath
|
|
first_dst_filepath = None
|
|
for frame_idx in range(range_start, range_end + 1):
|
|
dst_filepath = dst_filepaths_by_frame[frame_idx]
|
|
src_filepaths = []
|
|
for layer_position in sorted_positions:
|
|
layer_id = layer_ids_by_position[layer_position]
|
|
filepaths_by_frame = filepaths_by_layer_id[layer_id]
|
|
src_filepath = filepaths_by_frame.get(frame_idx)
|
|
if src_filepath is not None:
|
|
src_filepaths.append(src_filepath)
|
|
|
|
if not src_filepaths:
|
|
transparent_filepaths.add(dst_filepath)
|
|
continue
|
|
|
|
# Store first destination filepath to be used for transparent images
|
|
if first_dst_filepath is None:
|
|
first_dst_filepath = dst_filepath
|
|
|
|
if len(src_filepaths) == 1:
|
|
src_filepath = src_filepaths[0]
|
|
if cleanup:
|
|
os.rename(src_filepath, dst_filepath)
|
|
else:
|
|
copy_render_file(src_filepath, dst_filepath)
|
|
|
|
else:
|
|
composite_images(src_filepaths, dst_filepath)
|
|
|
|
# Store first transparent filepath to be able copy it
|
|
transparent_filepath = None
|
|
for dst_filepath in transparent_filepaths:
|
|
if transparent_filepath is None:
|
|
create_transparent_image_from_source(
|
|
first_dst_filepath, dst_filepath
|
|
)
|
|
transparent_filepath = dst_filepath
|
|
else:
|
|
copy_render_file(transparent_filepath, dst_filepath)
|
|
|
|
# Remove all files that were used as source for compositing
|
|
if cleanup:
|
|
cleanup_rendered_layers(filepaths_by_layer_id)
|
|
|
|
|
|
def composite_images(input_image_paths, output_filepath):
|
|
"""Composite images in order from passed list.
|
|
|
|
Raises:
|
|
ValueError: When entered list is empty.
|
|
"""
|
|
if not input_image_paths:
|
|
raise ValueError("Nothing to composite.")
|
|
|
|
img_obj = None
|
|
for image_filepath in input_image_paths:
|
|
_img_obj = Image.open(image_filepath)
|
|
if img_obj is None:
|
|
img_obj = _img_obj
|
|
else:
|
|
img_obj.alpha_composite(_img_obj)
|
|
img_obj.save(output_filepath)
|
|
|
|
|
|
def rename_filepaths_by_frame_start(
|
|
filepaths_by_frame, range_start, range_end, new_frame_start
|
|
):
|
|
"""Change frames in filenames of finished images to new frame start."""
|
|
|
|
# Calculate frame end
|
|
new_frame_end = range_end + (new_frame_start - range_start)
|
|
# Create filename template
|
|
filename_template = get_frame_filename_template(
|
|
max(range_end, new_frame_end)
|
|
)
|
|
|
|
# Use different ranges based on Mark In and output Frame Start values
|
|
# - this is to make sure that filename renaming won't affect files that
|
|
# are not renamed yet
|
|
if range_start < new_frame_start:
|
|
source_range = range(range_end, range_start - 1, -1)
|
|
output_range = range(new_frame_end, new_frame_start - 1, -1)
|
|
else:
|
|
# This is less possible situation as frame start will be in most
|
|
# cases higher than Mark In.
|
|
source_range = range(range_start, range_end + 1)
|
|
output_range = range(new_frame_start, new_frame_end + 1)
|
|
|
|
# Skip if source first frame is same as destination first frame
|
|
new_dst_filepaths = {}
|
|
for src_frame, dst_frame in zip(source_range, output_range):
|
|
src_filepath = os.path.normpath(filepaths_by_frame[src_frame])
|
|
dirpath, src_filename = os.path.split(src_filepath)
|
|
dst_filename = filename_template.format(frame=dst_frame)
|
|
dst_filepath = os.path.join(dirpath, dst_filename)
|
|
|
|
if src_filename != dst_filename:
|
|
os.rename(src_filepath, dst_filepath)
|
|
|
|
new_dst_filepaths[dst_frame] = dst_filepath
|
|
|
|
return new_dst_filepaths
|