import numpy as np from xml.dom import minidom from pathlib import Path from src.postprocessing.get_svg_size_pos import get_midpoint_of_path_bbox, get_begin_values_by_starting_pos from src.postprocessing.transform_animation_predictor_output import transform_animation_predictor_output def create_animated_svg(file, animation_ids, model_output, filename_suffix="", save=True): """ Insert multiple animation statements. Args: file (str): Path of SVG file. animation_ids (list[int]): List of element IDs that get animated. model_output (ndarray): Array of 13 dimensional arrays with animation predictor model output. filename_suffix (str): Suffix of animated SVG. Returns: list(float): List of begin values of elements in SVG. xml.dom.minidom.Document: Parsed file with inserted animation statements. """ doc = svg_to_doc(file) begin_values = get_begin_values_by_starting_pos(file, animation_ids, start=1, step=0.25) for i in range(len(animation_ids)): if not (model_output[i][:6] == np.array([0] * 6)).all(): try: # there are some paths that can't be embedded and don't have style attributes output_dict = transform_animation_predictor_output(file, animation_ids[i], model_output[i]) output_dict["begin"] = begin_values[i] if output_dict["type"] == "translate": doc = insert_translate_statement(doc, animation_ids[i], output_dict) if output_dict["type"] == "scale": doc = insert_scale_statement(doc, animation_ids[i], output_dict, file) if output_dict["type"] == "rotate": doc = insert_rotate_statement(doc, animation_ids[i], output_dict) if output_dict["type"] in ["skewX", "skewY"]: doc = insert_skew_statement(doc, animation_ids[i], output_dict) if output_dict["type"] == "fill": doc = insert_fill_statement(doc, animation_ids[i], output_dict) if output_dict["type"] in ["opacity"]: doc = insert_opacity_statement(doc, animation_ids[i], output_dict) except Exception as e: print(f"File {file}, animation ID {animation_ids[i]} can't be animated. {e}") pass if save: filename = file.split('/')[-1].replace(".svg", "") + "_animated" save_animated_svg(doc, filename) return begin_values, doc def svg_to_doc(file): """ Parse an SVG file. Args: file (string): Path of SVG file. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ return minidom.parse(file) def save_animated_svg(doc, filename): """ Save animated SVGs to folder animated_svgs. Args: doc (xml.dom.minidom.Document): Parsed file. filename (str): Name of output file. """ Path("data/animated_svgs").mkdir(parents=True, exist_ok=True) with open('data/animated_svgs/' + filename + '.svg', 'wb') as f: f.write(doc.toprettyxml(encoding="iso-8859-1")) def insert_translate_statement(doc, animation_id, model_output_dict): """ Insert translate statement. Args: doc (xml.dom.minidom.Document): Parsed file. animation_id (int): ID of element that gets animated. model_output_dict (dict): Dictionary containing animation statement. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ pre_animations = [] opacity_dict_1, opacity_dict_2 = create_opacity_pre_animation_dicts(model_output_dict) pre_animations.append(create_animation_statement(opacity_dict_1)) pre_animations.append(create_animation_statement(opacity_dict_2)) animation = create_animation_statement(model_output_dict) doc = insert_animation(doc, animation_id, animation, pre_animations) return doc def insert_scale_statement(doc, animation_id, model_output_dict, file): """ Insert scale statement. Args: doc (xml.dom.minidom.Document): Parsed file. animation_id (int): ID of element that gets animated. model_output_dict (dict): Dictionary containing animation statement. file (str): Path of SVG file. Needed to get midpoint of path bbox to suppress simultaneous translate movement. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ pre_animations = [] opacity_dict_1, opacity_dict_2 = create_opacity_pre_animation_dicts(model_output_dict) pre_animations.append(create_animation_statement(opacity_dict_1)) pre_animations.append(create_animation_statement(opacity_dict_2)) x_midpoint, y_midpoint = get_midpoint_of_path_bbox(file, animation_id) if model_output_dict["from_"] > 1: model_output_dict["from_"] = 2 pre_animation_from = f"-{x_midpoint} -{y_midpoint}" # negative midpoint else: model_output_dict["from_"] = 0 pre_animation_from = f"{x_midpoint} {y_midpoint}" # positive midpoint translate_pre_animation_dict = {"type": "translate", "begin": model_output_dict["begin"], "dur": model_output_dict["dur"], "from_": pre_animation_from, "to": "0 0", "fill": "freeze"} pre_animations.append(create_animation_statement(translate_pre_animation_dict)) animation = create_animation_statement(model_output_dict) + ' additive="sum" ' doc = insert_animation(doc, animation_id, animation, pre_animations) return doc def insert_rotate_statement(doc, animation_id, model_output_dict): """ Insert rotate statement. Args: doc (xml.dom.minidom.Document): Parsed file. animation_id (int): ID of element that gets animated. model_output_dict (dict): Dictionary containing animation statement. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ pre_animations = [] opacity_dict_1, opacity_dict_2 = create_opacity_pre_animation_dicts(model_output_dict) pre_animations.append(create_animation_statement(opacity_dict_1)) pre_animations.append(create_animation_statement(opacity_dict_2)) animation = create_animation_statement(model_output_dict) doc = insert_animation(doc, animation_id, animation, pre_animations) return doc def insert_skew_statement(doc, animation_id, model_output_dict): """ Insert skew statement. Args: doc (xml.dom.minidom.Document): Parsed file. animation_id (int): ID of element that gets animated. model_output_dict (dict): Dictionary containing animation statement. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ pre_animations = [] opacity_dict_1, opacity_dict_2 = create_opacity_pre_animation_dicts(model_output_dict) pre_animations.append(create_animation_statement(opacity_dict_1)) pre_animations.append(create_animation_statement(opacity_dict_2)) animation = create_animation_statement(model_output_dict) doc = insert_animation(doc, animation_id, animation, pre_animations) return doc def insert_fill_statement(doc, animation_id, model_output_dict): """ Insert fill statement. Args: doc (xml.dom.minidom.Document): Parsed file animation_id (int): ID of element that gets animated. model_output_dict (dict): Dictionary containing animation statement. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ pre_animations = [] model_output_dict['dur'] = 2 if model_output_dict['begin'] < 2: model_output_dict['begin'] = 0 else: # Wave pre_animation_dict = {"type": "fill", "begin": 0, "dur": model_output_dict["begin"], "from_": model_output_dict["to"], "to": model_output_dict["from_"], "fill": "remove"} pre_animations.append(create_animation_statement(pre_animation_dict)) animation = create_animation_statement(model_output_dict) doc = insert_animation(doc, animation_id, animation, pre_animations) return doc def insert_opacity_statement(doc, animation_id, model_output_dict): """ Insert opacity statement. Args: doc (xml.dom.minidom.Document): Parsed file. animation_id (int): ID of element that gets animated. model_output_dict (dict): Dictionary containing animation statement. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ pre_animations = [] opacity_pre_animation_dict = {"type": "opacity", "begin": "0", "dur": model_output_dict["begin"], "from_": "0", "to": "0", "fill": "remove"} pre_animations.append(create_animation_statement(opacity_pre_animation_dict)) animation = create_animation_statement(model_output_dict) doc = insert_animation(doc, animation_id, animation, pre_animations) return doc def insert_animation(doc, animation_id, animation, pre_animations=None): """ Insert animation statements including pre-animation statements. Args: doc (xml.dom.minidom.Document): Parsed file. animation_id (int): ID of element that gets animated. animation (string): Animation that needs to be inserted. pre_animations (list): List of animations that needs to be inserted before actual animation. Returns: xml.dom.minidom.Document: Parsed file with inserted animation statement. """ elements = doc.getElementsByTagName('path') + doc.getElementsByTagName('circle') + doc.getElementsByTagName( 'ellipse') + doc.getElementsByTagName('line') + doc.getElementsByTagName( 'polygon') + doc.getElementsByTagName('polyline') + doc.getElementsByTagName( 'rect') + doc.getElementsByTagName('text') for element in elements: if element.getAttribute('animation_id') == str(animation_id): if pre_animations is not None: for i in range(len(pre_animations)): element.appendChild(doc.createElement(pre_animations[i])) element.appendChild(doc.createElement(animation)) return doc def create_animation_statement(animation_dict): """ Set up animation statement from a dictionary. Args: animation_dict (dict): Dictionary that is transformed into animation statement. Returns: str: Animation statement. """ if animation_dict["type"] in ["translate", "scale", "rotate", "skewX", "skewY"]: return _create_animate_transform_statement(animation_dict) elif animation_dict["type"] in ["fill", "opacity"]: return _create_animate_statement(animation_dict) def _create_animate_transform_statement(animation_dict): """ Set up animation statement from model output for ANIMATETRANSFORM animations """ animation = f'animateTransform attributeName = "transform" attributeType = "XML" ' \ f'type = "{animation_dict["type"]}" ' \ f'begin = "{str(animation_dict["begin"])}" ' \ f'dur = "{str(animation_dict["dur"])}" ' \ f'from = "{str(animation_dict["from_"])}" ' \ f'to = "{str(animation_dict["to"])}" ' \ f'fill = "{str(animation_dict["fill"])}"' return animation def _create_animate_statement(animation_dict): """ Set up animation statement from model output for ANIMATE animations """ animation = f'animate attributeName = "{animation_dict["type"]}" ' \ f'begin = "{str(animation_dict["begin"])}" ' \ f'dur = "{str(animation_dict["dur"])}" ' \ f'from = "{str(animation_dict["from_"])}" ' \ f'to = "{str(animation_dict["to"])}" ' \ f'fill = "{str(animation_dict["fill"])}"' return animation def create_opacity_pre_animation_dicts(animation_dict): """ Set up pre_animation statements. Args: animation_dict (dict): Dictionary from animation that is needed to set up opacity pre-animations. Returns: str: Animation Statement. """ opacity_pre_animation_dict_1 = {"type": "opacity", "begin": "0", "dur": animation_dict["begin"], "from_": "0", "to": "0", "fill": "remove"} opacity_pre_animation_dict_2 = {"type": "opacity", "begin": animation_dict["begin"], "dur": "0.5", "from_": "0", "to": "1", "fill": "remove"} return opacity_pre_animation_dict_1, opacity_pre_animation_dict_2