From ab9f8d06939e01f9924e757063f43b350b8524eb Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Wed, 21 Oct 2020 12:25:27 -0400 Subject: [PATCH 1/3] project_data: New keyframe scaler implementation - Move the bulk of rescale_keyframes to a separate file, and implement it as a factory class KeyframeScaler. Instances of the class are callables which have been assigned a scaling factor. Calling the scaler instance on a project data dict will apply the factor. - Break up the scaling code into multiple private methods, optimized to be as DRY as possible. Avoid deep nesting and repetition. --- src/classes/keyframe_scaler.py | 87 ++++++++++++++++++++++++++++++++++ src/classes/project_data.py | 56 ++++------------------ 2 files changed, 96 insertions(+), 47 deletions(-) create mode 100644 src/classes/keyframe_scaler.py diff --git a/src/classes/keyframe_scaler.py b/src/classes/keyframe_scaler.py new file mode 100644 index 0000000000..1870785f46 --- /dev/null +++ b/src/classes/keyframe_scaler.py @@ -0,0 +1,87 @@ +""" + @file + @brief Process project data, scaling keyframe X coordinates by the given factor + @author Noah Figg + @author Jonathan Thomas + @author Olivier Girard + @author FeRD (Frank Dana) + + @section LICENSE + + Copyright (c) 2008-2020 OpenShot Studios, LLC + (http://www.openshotstudios.com). This file is part of + OpenShot Video Editor (http://www.openshot.org), an open-source project + dedicated to delivering high quality video editing and animation solutions + to the world. + + OpenShot Video Editor is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenShot Video Editor is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenShot Library. If not, see . + """ + + +class KeyframeScaler: + """This factory class produces scaler objects which, when called, + will apply the assigned scaling factor to the keyframe points + in a project data dictionary. Keyframe X coordinate values are + multiplied by the scaling factor, except X=1 (because the first + frame never changes)""" + + def _scale_value(self, value: float) -> int: + """Scale value by some factor, except for 1 (leave that alone)""" + if value == 1.0: + return value + # Round to nearest INT + return round(value * self._scale_factor) + + def _update_prop(self, prop: dict): + """Find the keyframe points in a property and scale""" + # Create a list of lists of keyframe points for this prop + if "red" in prop: + # It's a color, one list of points for each channel + keyframes = [prop[color].get("Points", []) for color in prop] + else: + # Not a color, just a single list of points + keyframes = [prop.get("Points", [])] + for k in keyframes: + # Scale the X coordinate (frame #) by the stored factor + [point["co"].update({ + "X": self._scale_value(point["co"].get("X", 0.0)) + }) + for point in k if "co" in point] + + def _process_item(self, item: dict): + """Process all the dict sub-members of the current dict""" + dict_props = [ + item[prop] for prop in item + if isinstance(item[prop], dict) + ] + for prop in dict_props: + self._update_prop(prop) + + def __call__(self, data: dict) -> dict: + """Apply the stored scaling factor to a project data dict""" + # Look for keyframe objects in clips + for clip in data.get('clips', []): + self._process_item(clip) + # Also update any effects applied to the clip + for effect in clip.get("effects", []): + self._process_item(effect) + # Look for keyframe objects in project effects (transitions) + for effect in data.get('effects', []): + self._process_item(effect) + # return the scaled project data + return data + + def __init__(self, factor: float): + """Store the scale factor assigned to this instance""" + self._scale_factor = factor diff --git a/src/classes/project_data.py b/src/classes/project_data.py index bf00c03af1..754107c847 100644 --- a/src/classes/project_data.py +++ b/src/classes/project_data.py @@ -41,6 +41,8 @@ from classes.assets import get_assets_path from windows.views.find_file import find_missing_file +from .keyframe_scaler import KeyframeScaler + class ProjectDataStore(JsonDataStore, UpdateInterface): """ This class allows advanced searching of data structure, implements changes interface """ @@ -406,53 +408,13 @@ def scale_keyframe_value(self, original_value, scale_factor): def rescale_keyframes(self, scale_factor): """Adjust all keyframe coordinates from previous FPS to new FPS (using a scale factor) and return scaled project data without modifing the current project.""" - log.info('Scale all keyframes by a factor of %s' % scale_factor) - - # Create copy of active project data - data = copy.deepcopy(self._data) - - # Rescale the the copied project data - # Loop through all clips (and look for Keyframe objects) - # Scale the X coordinate by factor (which represents the frame #) - for clip in data.get('clips', []): - for attribute in clip: - if type(clip.get(attribute)) == dict and "Points" in clip.get(attribute): - for point in clip.get(attribute).get("Points"): - if "co" in point: - point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor) - if type(clip.get(attribute)) == dict and "red" in clip.get(attribute): - for color in clip.get(attribute): - for point in clip.get(attribute).get(color).get("Points"): - if "co" in point: - point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor) - for effect in clip.get("effects", []): - for attribute in effect: - if type(effect.get(attribute)) == dict and "Points" in effect.get(attribute): - for point in effect.get(attribute).get("Points"): - if "co" in point: - point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor) - if type(effect.get(attribute)) == dict and "red" in effect.get(attribute): - for color in effect.get(attribute): - for point in effect.get(attribute).get(color).get("Points"): - if "co" in point: - point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor) - - # Loop through all effects/transitions (and look for Keyframe objects) - # Scale the X coordinate by factor (which represents the frame #) - for effect in data.get('effects', []): - for attribute in effect: - if type(effect.get(attribute)) == dict and "Points" in effect.get(attribute): - for point in effect.get(attribute).get("Points"): - if "co" in point: - point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor) - if type(effect.get(attribute)) == dict and "red" in effect.get(attribute): - for color in effect.get(attribute): - for point in effect.get(attribute).get(color).get("Points"): - if "co" in point: - point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor) - - # return the copied and scaled project data - return data + # + log.info('Scale all keyframes by a factor of %s', scale_factor) + # Create a scaler instance + scaler = KeyframeScaler(factor=scale_factor) + # Create copy of active project data and scale + scaled = scaler(copy.deepcopy(self._data)) + return scaled def read_legacy_project_file(self, file_path): """Attempt to read a legacy version 1.x openshot project file""" From 24f9d3fd874ab73f3a625a2e46ecae8226d3ed5c Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Mon, 9 Nov 2020 22:20:41 -0500 Subject: [PATCH 2/3] project_data: Remove unused function --- src/classes/project_data.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/classes/project_data.py b/src/classes/project_data.py index 754107c847..672dbe4037 100644 --- a/src/classes/project_data.py +++ b/src/classes/project_data.py @@ -396,15 +396,6 @@ def load(self, file_path, clear_thumbnails=True): from classes.app import get_app get_app().updates.load(self._data) - def scale_keyframe_value(self, original_value, scale_factor): - """Scale keyframe X coordinate by some factor, except for 1 (leave that alone)""" - if original_value == 1.0: - # This represents the first frame of a clip (so we want to maintain that) - return original_value - else: - # Round to nearest INT - return round(original_value * scale_factor) - def rescale_keyframes(self, scale_factor): """Adjust all keyframe coordinates from previous FPS to new FPS (using a scale factor) and return scaled project data without modifing the current project.""" From 3ad93c7b429413d2231e3d012cb347ac545baeba Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Mon, 9 Nov 2020 22:21:05 -0500 Subject: [PATCH 3/3] keyframe_scaler: Rename to _scale_x_value --- src/classes/keyframe_scaler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/classes/keyframe_scaler.py b/src/classes/keyframe_scaler.py index 1870785f46..af7c94f27c 100644 --- a/src/classes/keyframe_scaler.py +++ b/src/classes/keyframe_scaler.py @@ -1,9 +1,7 @@ """ @file @brief Process project data, scaling keyframe X coordinates by the given factor - @author Noah Figg @author Jonathan Thomas - @author Olivier Girard @author FeRD (Frank Dana) @section LICENSE @@ -36,7 +34,7 @@ class KeyframeScaler: multiplied by the scaling factor, except X=1 (because the first frame never changes)""" - def _scale_value(self, value: float) -> int: + def _scale_x_value(self, value: float) -> int: """Scale value by some factor, except for 1 (leave that alone)""" if value == 1.0: return value @@ -55,7 +53,7 @@ def _update_prop(self, prop: dict): for k in keyframes: # Scale the X coordinate (frame #) by the stored factor [point["co"].update({ - "X": self._scale_value(point["co"].get("X", 0.0)) + "X": self._scale_x_value(point["co"].get("X", 0.0)) }) for point in k if "co" in point]