commit a386c803e19128ded67b187bc38a06c3363b7a99 Author: OmniLottie Date: Sun Mar 1 21:36:54 2026 +0800 Initial Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..01897ff --- /dev/null +++ b/README.md @@ -0,0 +1,383 @@ + + +

OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens +

+ + +
+      +      +      +      +      +      + +
+ +## šŸ”„šŸ”„šŸ”„ News !! +- [2026/03/02] šŸ‘‹ Upload paper and init project. [Read](https://arxiv.org/abs/2504.06263) +- [2026/03/02] šŸ‘‹ We have released the inference code and model weight. šŸ¤—[Weight](https://huggingface.co/OmniLottie/OmniLottie). +- [2026/03/02] We have released **MMLottieBench** benchmark! Check out [MMLottieBench](https://huggingface.co/datasets/OmniLottie/MMLottieBench). +- [2026/03/02] šŸ‘‹ Release MMLottie-2M Dataser šŸ¤—[MMLottie-2M Dataset](https://huggingface.co/datasets/OmniLottie/MMLottie-2M). +- [2026/03/02] šŸ‘‹ We have released the Huggingface Demo. šŸ¤—[Demo](https://huggingface.co/spaces/OmniLottie/OmniLottie). +- [2026/02/21] OmniLottie is accepted to **CVPR 2026**šŸ”„! See you in Denver! + + +

+ Demo GIF +

+ + +## šŸ“‘ Open-source Plan +- [x] Project Page & Technical Report +- [x] MMLottie-2M Dataset Release +- [x] Inference Code & Model Weight +- [x] Online Demo (Gradio deployed on Huggingface) +- [x] MMLottieBench Benchmark +- [ ] Training Code + + + +## 1. Introduction + +**OmniLottie** is the first family of end-to-end multimodal Lottie generators that leverage pre-trained Vision-Language Models (VLMs), capable of generating complex and detailed Lottie animations from multi-modal instructions including texts, images, and videos. We also introduce MMLottie-2M, a multimodal dataset with two million richly annotated Lottie animations, along with a standardized evaluation protocol for multi-modal vector animation generation tasks. + + +## 2. Models Downloading +| Model | Download link | Size | Update date | +|-----------------------------|-------------------------------|------------|------| +| OmniLottie(4B) | [Huggingface](https://huggingface.co/OmniLottie/OmniLottie) | 8.46 GB | 2026-03-02 | + + + +## 3. Dependencies and Installation +The dependencies configured according to the following instructions provide an environment equipped for inference + +### 3.1 Clone the Repository +```bash +git clone https://github.com/OpenVGLab/OmniLottie +cd OmniLottie +``` + +### 3.2 Create Conda Environment +Create and activate a new conda environment with Python 3.10: +```bash +conda create -n omnilottie python=3.10 +conda activate omnilottie +``` + +### 3.3 Install Dependencies + + +#### Python Dependencies +We have tested our environment with CUDA 12.1. You can install CUDA 12.1 by following the [CUDA Toolkit installation guide](https://developer.nvidia.com/cuda-12-1-0-download-archive). + +Install PyTorch with CUDA 12.1 support: +```bash +pip install torch==2.3.0+cu121 torchvision==0.18.0+cu121 --index-url https://download.pytorch.org/whl/cu121 +``` + +Install remaining dependencies: +```bash +pip install -r requirements.txt +``` + +## 4. Inference + +| | GPU Memory Usage | Time per 256/512/1024/2048/4096 tokens | +| ------------------------------------------------ | ---------------- | ----------------- | +| OmniLottie | 15.2G | 8.34/16.68/33.38/66.74/133.49 seconds | + +**Note: The inference time shown here is measured per OmniLottie Lottie tokens, while the inference time reported in our paper is measured per JSON code tokens for fair comparison with baseline methods.** + +### Quick Start + +**Download Model Weights** + +First, install the Hugging Face CLI tool: +```bash +pip install huggingface-hub +``` + +**Download the model from Hugging Face:** +```bash +# Download OmniLottie model +huggingface-cli download OmniLottie/OmniLottie --local-dir /PATH/TO/OmniLottie +``` + +**Try with Example Data** + +We provide example prompts, images, and videos in the `example/` directory: +- `example/demo.txt` - 37 text prompts +- `example/demo_images/` - 26 images (with corresponding text descriptions) +- `example/demo_video/` - 30 videos + +```bash +# Test with example text prompts +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --batch_text_file example/demo.txt \ + --output_dir ./output_demo_text + +# Test with example images +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --single_image example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.png \ + --output_dir ./output_demo_image + +# Test with example videos +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --single_video example/demo_video/02b8ce2014690a9e30dc25da846e8afb.mp4 \ + --output_dir ./output_demo_video +``` + +### Text-to-Lottie Generation + +Generate Lottie animations from text descriptions: + +**Single prompt:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --single_text "A red ball appearing, bouncing up and down, then fading out, repeating seamlessly" \ + --output_dir ./output_text +``` + +**Batch generation from file:** +```bash +# Create a prompts.txt file with one prompt per line +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --batch_text_file example/demo.txt \ + --output_dir ./output_text +``` + +**Custom generation parameters:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --single_text "a blue bird appearing, pulsing while sliding downward, lingers briefly, then growing back while sliding upward to reset with clear phase changes, repeating seamlessly" \ + --use_sampling \ + --temperature 0.8 \ + --top_p 0.25 \ + --top_k 5 \ + --repetition_penalty 1.01 \ + --output_dir ./output +``` + +**Generate with Best-of-N selection:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --single_text "a light blue piggy bank with a darker blue outline, with a single light blue coin with a dark blue yen symbol (£) appears above the piggy bank, then starts descending towards the piggy bank's opening" \ + --num_candidates 8 \ + --output_dir ./output +``` + +### Text-Image-to-Lottie Generation + +Generate Lottie animations from an image: + +**Single image:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --single_image /path/to/image.png \ + --output_dir ./output_image +``` + +### Video-to-Lottie Generation + +Convert video to Lottie animation: + +**Single video:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --single_video /path/to/video.mp4 \ + --output_dir ./output_video +``` + +### Advanced Options + +**Specify tokenizer path:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --tokenizer_name /PATH/TO/Qwen2.5-VL-3B-Instruct \ + --single_text "Your prompt here" \ + --output_dir ./output +``` + +**Adjust token length:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --maxlen 6072 \ + --text_len 512 \ + --single_text "Your prompt here" \ + --output_dir ./output +``` + +**Filter by task type (when using MMLottieBench dataset):** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --mmlottie_bench_dir /PATH/TO/mmlottie_bench \ + --split real \ + --task_filter text \ + --output_dir ./output +``` + +**Process limited samples with shuffling:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --mmlottie_bench_dir /PATH/TO/mmlottie_bench \ + --split real \ + --max_samples 10 \ + --shuffle \ + --output_dir ./output +``` + +### Interactive Demo + +We provide an interactive generation interface using Gradio: + +- **Local Deployment** + ```bash + python app.py + ``` + +- **Online Demo** + + Try our live demo on [Hugging Face Spaces](https://huggingface.co/spaces/OmniLottie/OmniLottie) + + + + +## 5. Benchmark & Evaluation + +We provide **MMLottieBench** for standardized evaluation of Lottie generation models. + +### Download MMLottieBench + +**Option 1: Using download script:** +```bash +python download_mmlottie_bench.py --output_dir /PATH/TO/mmlottie_bench +``` + +**Option 2: Using Hugging Face CLI:** +```bash +huggingface-cli download OmniLottie/MMLottieBench --repo-type dataset --local-dir /PATH/TO/mmlottie_bench +``` + +**Option 3: Automatic download (in code):** +```python +from datasets import load_dataset +dataset = load_dataset("OmniLottie/MMLottieBench") +``` + +### Benchmark Overview + +MMLottieBench contains **900 samples** split into: +- **Real split**: 450 real-world Lottie animations +- **Synthetic split**: 450 synthetically generated samples + +Each split contains **3 task types** (150 samples each): +- **Text-to-Lottie**: Generate from text descriptions +- **Text-Image-to-Lottie**: Generate from image + text guidance +- **Video-to-Lottie**: Convert video to Lottie animation + +### Run Benchmark Inference + +MMLottieBench provides two splits that can be switched using `--split`: +- `--split real` - Test on 450 real-world Lottie animations +- `--split synthetic` - Test on 450 synthetically generated samples + +**Test on real split (all tasks):** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --mmlottie_bench_dir /PATH/TO/mmlottie_bench \ + --split real \ + --output_dir ./benchmark_results_real +``` + +**Test on synthetic split (all tasks):** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --mmlottie_bench_dir /PATH/TO/mmlottie_bench \ + --split synthetic \ + --output_dir ./benchmark_results_synthetic +``` + +**Test specific task type on real split:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --mmlottie_bench_dir /PATH/TO/mmlottie_bench \ + --split real \ + --mmlottie_task text2lottie \ + --output_dir ./benchmark_results +``` + +**Available task types:** +- `text2lottie` - Text-to-Lottie generation (150 samples per split) +- `text_image2lottie` - Text-Image-to-Lottie generation (150 samples per split) +- `video2lottie` - Video-to-Lottie generation (150 samples per split) + +**Process limited samples with filtering:** +```bash +python inference.py \ + --sketch_weight /PATH/TO/OmniLottie \ + --mmlottie_bench_dir /PATH/TO/mmlottie_bench \ + --split real \ + --max_samples 50 \ + --shuffle \ + --output_dir ./benchmark_results +``` + + + +For detailed usage, see: +- [MMLottieBench Usage Guide](https://huggingface.co/datasets/OmniLottie/MMLottieBench/blob/main/README.md) + + + +## 6. License +OmniLottie is licensed under the [**Apache License 2.0**](https://www.apache.org/licenses/LICENSE-2.0), while MMLottie-2M dataset is under [**Creative Commons Attribution Non Commercial Share Alike 4.0 License**](https://spdx.org/licenses/CC-BY-NC-SA-4.0). You can find the license files in the respective github and HuggingFace repositories. + + + +## Citation + +```bibtex +@article{yang2025omnilottie, + title={OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens}, + author={Yiying Yang and Wei Cheng and Sijin Chen and Xianfang Zeng and Jiaxu Zhang and Liao Wang and Gang Yu and Xinjun Ma and Yu-Gang Jiang}, + journal={arXiv preprint arxiv:2504.06263}, + year={2025} +} +``` + +## Acknowledgments +We thank the following excellent open-source works: + +[IconShop](https://icon-shop.github.io/): is the first advanced work that leverages LLMs to generate monochrome, icon-level SVGs. We referred to its parametric implementation. + +Here is the list of highly related concurrent works: + +[LLM4SVG](https://arxiv.org/abs/2412.11102): treats SVG coordinates as number strings and predicts decimal part for higher spatial accuracy. + +[StarVector](https://starvector.github.io/): equips LLM with an image encoder for Image-to-SVG generation. + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=OpenVGLab/OmniLottie&type=Date)](https://www.star-history.com/#OpenVGLab/OmniLottie&Date) + diff --git a/app.py b/app.py new file mode 100644 index 0000000..89e88bd --- /dev/null +++ b/app.py @@ -0,0 +1,976 @@ +import gradio as gr +import json +import torch +import os +import numpy as np +import random +import re +import tempfile +import base64 +import threading +import time +from PIL import Image as PILImage +from decord import VideoReader, cpu + +from decoder import LottieDecoder +from transformers import AutoProcessor +from qwen_vl_utils import process_vision_info +from lottie.objects.lottie_tokenize import LottieTensor +from lottie.objects.lottie_param import ( + from_sequence, ShapeLayer, NullLayer, PreCompLayer, TextLayer, + SolidColorLayer, Font, Chars, + shape_layer_to_json, null_layer_to_json, precomp_layer_to_json, + text_layer_to_json, solid_layer_to_json, font_to_json, char_to_json +) + +SYSTEM_PROMPT = "You are a Lottie animation expert." +VIDEO_PROMPT = "Turn this video into Lottie code." +LOTTIE_BOS = 192398 +LOTTIE_EOS = 192399 +PAD_TOKEN = 151643 + +model = None +processor = None +device = None + +generation_lock = threading.Lock() + +def load_model_once(): + global model, processor, device + + if model is not None: + return model, processor, device + + checkpoint_path = "/PATH/TO/OmniLottie" + + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + print(f"Loading model from {checkpoint_path}...") + model = LottieDecoder(pix_len=4560, text_len=1500) + + model_file = os.path.join(checkpoint_path, 'pytorch_model.bin') + if os.path.exists(model_file): + model.load_state_dict(torch.load(model_file, map_location='cpu')) + else: + raise FileNotFoundError(f"Model file not found: {model_file}") + + model = model.to(device).eval() + + processor = AutoProcessor.from_pretrained( + "Qwen/Qwen2.5-VL-3B-Instruct", + padding_side="left" + ) + + print(f"āœ… Model loaded on {device}") + + return model, processor, device + +def simplify_to_animation_description(text): + if not text or not isinstance(text, str): + return text + + prefixes = [ + r'^The video features?\s+', r'^The scene shows?\s+', + r'^An animation of\s+', r'^There is\s+', r'^It shows?\s+' + ] + + for pattern in prefixes: + text = re.sub(pattern, '', text, flags=re.IGNORECASE) + + if text: + text = text[0].upper() + text[1:] + + return text.strip() + +def add_random_background(img): + if img.mode != 'RGBA': + return img.convert('RGB') + + light_colors = [(255, 255, 255), (245, 245, 245), (250, 250, 250)] + bg_color = random.choice(light_colors) + background = PILImage.new('RGB', img.size, bg_color) + background.paste(img, (0, 0), img) + return background + +def load_frames_from_video(video_path, num_frames=8, max_size=336): + import os + ext = os.path.splitext(video_path)[1].lower() + + frames = [] + + if ext in ('.gif', '.webp'): + try: + img = PILImage.open(video_path) + total_frames = getattr(img, 'n_frames', 1) + + if total_frames < 1: + raise ValueError(f"No frames in {ext.upper()}: {video_path}") + + indices = np.linspace(0, total_frames - 1, min(num_frames, total_frames)).astype(int) + + for idx in indices: + img.seek(idx) + frame = img.convert('RGB') + if max(frame.size) > max_size: + frame.thumbnail((max_size, max_size), PILImage.LANCZOS) + frames.append(frame) + + img.close() + + except Exception as e: + raise ValueError(f"Failed to load {ext.upper()}: {str(e)}") + + else: + try: + vr = VideoReader(video_path, ctx=cpu(0)) + total_frames = len(vr) + + if total_frames < 1: + raise ValueError(f"Video has no frames: {video_path}") + + indices = np.linspace(0, total_frames - 1, num_frames).astype(int) + frames_np = vr.get_batch(indices).asnumpy() + + for f in frames_np: + img = PILImage.fromarray(f) + if max(img.size) > max_size: + img.thumbnail((max_size, max_size), PILImage.LANCZOS) + frames.append(img) + + except Exception as e: + raise ValueError(f"Failed to load video: {str(e)}") + + while len(frames) < num_frames: + frames.append(frames[-1].copy()) + + return frames + +def build_messages(task_type, text_prompt=None, image=None, video_frames=None): + messages = [{"role": "system", "content": SYSTEM_PROMPT}] + + if task_type == "text": + text = simplify_to_animation_description(text_prompt) + messages.append({ + "role": "user", + "content": [{"type": "text", "text": f"Generate Lottie code: {text}"}] + }) + + elif task_type == "image": + text = simplify_to_animation_description(text_prompt) + messages.append({ + "role": "user", + "content": [ + {"type": "image", "image": image}, + {"type": "text", "text": f"Animate this image: {text}"} + ] + }) + + elif task_type == "video": + messages.append({ + "role": "user", + "content": [ + {"type": "video", "video": video_frames, "fps": 8.0}, + {"type": "text", "text": VIDEO_PROMPT} + ] + }) + + return messages + +def prepare_inference_input(processor, messages, device): + text_input = processor.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True + ) + + image_inputs, video_inputs = process_vision_info(messages) + + inputs = processor( + text=[text_input], + images=image_inputs if image_inputs else None, + videos=video_inputs if video_inputs else None, + padding=False, + return_tensors="pt" + ) + + input_ids = inputs['input_ids'] + attention_mask = inputs['attention_mask'] + target_len = 1500 + + if input_ids.shape[1] < target_len: + pad_len = target_len - input_ids.shape[1] + input_ids = torch.cat([ + torch.full((1, pad_len), PAD_TOKEN, dtype=torch.long), + input_ids + ], dim=1) + attention_mask = torch.cat([ + torch.zeros((1, pad_len), dtype=torch.long), + attention_mask + ], dim=1) + + result = { + 'input_ids': input_ids.to(device), + 'attention_mask': attention_mask.to(device), + 'pixel_values': inputs.get('pixel_values').to(device) if inputs.get('pixel_values') is not None else None, + 'image_grid_thw': inputs.get('image_grid_thw').to(device) if inputs.get('image_grid_thw') is not None else None, + 'pixel_values_videos': inputs.get('pixel_values_videos').to(device) if inputs.get('pixel_values_videos') is not None else None, + 'video_grid_thw': inputs.get('video_grid_thw').to(device) if inputs.get('video_grid_thw') is not None else None, + } + + return result + +def generate_lottie(model, inputs, max_tokens, device, use_sampling=False, temperature=0.95, top_p=0.25, top_k=5): + """ē”Ÿęˆ Lottie tokens""" + model.transformer.rope_deltas = None + position_ids, _ = model.transformer.get_rope_index( + input_ids=inputs['input_ids'], + attention_mask=inputs['attention_mask'], + image_grid_thw=inputs.get('image_grid_thw'), + video_grid_thw=inputs.get('video_grid_thw'), + ) + position_ids = position_ids * inputs['attention_mask'][None, ] + + kwargs = { + 'input_ids': inputs['input_ids'], + 'attention_mask': inputs['attention_mask'], + 'pixel_values': inputs.get('pixel_values'), + 'image_grid_thw': inputs.get('image_grid_thw'), + 'pixel_values_videos': inputs.get('pixel_values_videos'), + 'video_grid_thw': inputs.get('video_grid_thw'), + 'position_ids': position_ids, + 'max_new_tokens': max_tokens, + 'eos_token_id': LOTTIE_EOS, + 'pad_token_id': PAD_TOKEN, + 'use_cache': True, + } + + if use_sampling: + kwargs.update({'do_sample': True, 'temperature': temperature, 'top_p': top_p, 'top_k': top_k}) + else: + kwargs.update({'do_sample': False, 'num_beams': 1}) + + with torch.no_grad(): + outputs = model.transformer.generate(**kwargs) + + input_len = inputs['input_ids'].shape[1] + generated_ids = outputs[0][input_len:].tolist() + + del outputs, kwargs, position_ids + + if generated_ids and generated_ids[0] == LOTTIE_BOS: + generated_ids = generated_ids[1:] + if LOTTIE_EOS in generated_ids: + generated_ids = generated_ids[:generated_ids.index(LOTTIE_EOS)] + + return generated_ids + +def fix_lottie_json(anim): + + anim_ip = int(round(anim.get("ip", 0))) + anim_op = int(round(anim.get("op", 16))) + anim["ip"] = anim_ip + anim["op"] = anim_op + anim["fr"] = int(round(anim.get("fr", 8))) + anim["ddd"] = int(anim.get("ddd", 0)) + + def fix_t_recursive(obj): + if isinstance(obj, dict): + if obj.get("a") == 1 and isinstance(obj.get("k"), list): + for kf in obj["k"]: + if isinstance(kf, dict) and "t" in kf: + kf["t"] = int(round(kf["t"])) + for v in obj.values(): + fix_t_recursive(v) + elif isinstance(obj, list): + for item in obj: + fix_t_recursive(item) + + fix_t_recursive(anim) + + max_x = float(anim.get("w", 512)) + max_y = float(anim.get("h", 512)) + + def collect_pos(layer): + nonlocal max_x, max_y + p = layer.get("ks", {}).get("p", {}) + if isinstance(p, dict): + if p.get("a", 0) == 0: + pv = p.get("k", [0, 0]) + if isinstance(pv, list) and len(pv) >= 2: + max_x = max(max_x, float(pv[0])) + max_y = max(max_y, float(pv[1])) + else: + for kf in p.get("k", []): + if isinstance(kf, dict): + for sv in (kf.get("s", []), kf.get("e", [])): + if isinstance(sv, list) and len(sv) >= 2: + max_x = max(max_x, float(sv[0])) + max_y = max(max_y, float(sv[1])) + for sub in layer.get("layers", []): + collect_pos(sub) + + for layer in anim.get("layers", []): + collect_pos(layer) + + anim["w"] = max(512, int((max_x * 1.1 + 15) // 16 * 16)) + anim["h"] = max(512, int((max_y * 1.1 + 15) // 16 * 16)) + + valid_inds = set() + for layer in anim.get("layers", []): + if "ind" in layer: + valid_inds.add(int(layer["ind"])) + + def clean_shapes(shapes): + if not isinstance(shapes, list): + return shapes + cleaned = [] + for sh in shapes: + if not isinstance(sh, dict): + continue + if sh.get("ty") == "gr": + sh["it"] = clean_shapes(sh.get("it", [])) + if not sh["it"]: + continue + has_tr = any(item.get("ty") == "tr" for item in sh["it"] if isinstance(item, dict)) + if not has_tr: + sh["it"].append({ + "ty": "tr", "nm": "", + "a": {"a": 0, "k": [0, 0], "ix": 1}, + "p": {"a": 0, "k": [0, 0], "ix": 2}, + "s": {"a": 0, "k": [100, 100], "ix": 3}, + "r": {"a": 0, "k": 0, "ix": 6}, + "o": {"a": 0, "k": 100, "ix": 7}, + "sk": {"a": 0, "k": 0, "ix": 4}, + "sa": {"a": 0, "k": 0, "ix": 5}, + "hd": False + }) + cleaned.append(sh) + return cleaned + + def fix_layer(layer): + ip = int(round(layer.get("ip", anim_ip))) + op = int(round(layer.get("op", anim_op))) + layer["ip"] = max(anim_ip, ip) + layer["op"] = min(anim_op, max(layer["ip"] + 1, op)) + layer["st"] = int(round(layer.get("st", anim_ip))) + if "ind" in layer: + layer["ind"] = int(layer["ind"]) + if "parent" in layer: + p = int(layer["parent"]) + if p in valid_inds: + layer["parent"] = p + else: + del layer["parent"] + layer.pop("ct", None) + if "shapes" in layer: + layer["shapes"] = clean_shapes(layer["shapes"]) + for sub in layer.get("layers", []): + fix_layer(sub) + return layer + + fixed_layers = [] + for l in anim.get("layers", []): + fix_layer(l) + shapes = l.get("shapes", []) + if l.get("ty") == 4 and not shapes: + continue + fixed_layers.append(l) + anim["layers"] = fixed_layers + + for asset in anim.get("assets", []): + if "layers" in asset: + fixed = [] + for l in asset["layers"]: + fix_layer(l) + if l.get("ty") == 4 and not l.get("shapes"): + continue + fixed.append(l) + asset["layers"] = fixed + + return anim + +def tokens_to_lottie_json(generated_ids): + reconstructed_tensor = LottieTensor.from_list(generated_ids) + reconstructed_sequence = reconstructed_tensor.to_sequence() + reconstructed = from_sequence(reconstructed_sequence) + + json_animation = { + "v": reconstructed.get("v", "5.5.2"), + "fr": reconstructed.get("fr", 8), + "ip": reconstructed.get("ip", 0), + "op": reconstructed.get("op", 16), + "w": reconstructed.get("w", 512), + "h": reconstructed.get("h", 512), + "nm": reconstructed.get("nm", "Animation"), + "ddd": reconstructed.get("ddd", 0), + "assets": [], + "layers": [], + } + + if "fonts" in reconstructed and reconstructed["fonts"]: + fonts_data = reconstructed["fonts"] + if isinstance(fonts_data, dict) and "list" in fonts_data: + fonts_json = {"list": []} + for font in fonts_data["list"]: + if isinstance(font, Font): + fonts_json["list"].append(font_to_json(font)) + else: + fonts_json["list"].append(font) + json_animation["fonts"] = fonts_json + + if "chars" in reconstructed and reconstructed["chars"]: + chars_json = [] + for char in reconstructed["chars"]: + if isinstance(char, Chars): + chars_json.append(char_to_json(char)) + else: + chars_json.append(char) + json_animation["chars"] = chars_json + + for asset in reconstructed.get("assets", []): + asset_json = dict(asset) + if "layers" in asset: + asset_json["layers"] = [] + for layer in asset["layers"]: + if isinstance(layer, ShapeLayer): + asset_json["layers"].append(shape_layer_to_json(layer)) + elif isinstance(layer, NullLayer): + asset_json["layers"].append(null_layer_to_json(layer)) + elif isinstance(layer, PreCompLayer): + asset_json["layers"].append(precomp_layer_to_json(layer)) + elif isinstance(layer, TextLayer): + asset_json["layers"].append(text_layer_to_json(layer)) + elif isinstance(layer, SolidColorLayer): + asset_json["layers"].append(solid_layer_to_json(layer)) + else: + asset_json["layers"].append(layer) + json_animation["assets"].append(asset_json) + + for layer in reconstructed.get("layers", []): + if isinstance(layer, ShapeLayer): + json_animation["layers"].append(shape_layer_to_json(layer)) + elif isinstance(layer, NullLayer): + json_animation["layers"].append(null_layer_to_json(layer)) + elif isinstance(layer, PreCompLayer): + json_animation["layers"].append(precomp_layer_to_json(layer)) + elif isinstance(layer, TextLayer): + json_animation["layers"].append(text_layer_to_json(layer)) + elif isinstance(layer, SolidColorLayer): + json_animation["layers"].append(solid_layer_to_json(layer)) + else: + json_animation["layers"].append(layer) + + json_animation = fix_lottie_json(json_animation) + + return json_animation + +_lottie_js_cache = None + +def get_lottie_js(): + global _lottie_js_cache + if _lottie_js_cache is not None: + return _lottie_js_cache + + local_path = "lottie.min.js" + if os.path.exists(local_path): + with open(local_path, 'r', encoding='utf-8') as f: + _lottie_js_cache = f.read() + print(f"āœ… Loaded local lottie.min.js ({len(_lottie_js_cache)} bytes)") + else: + _lottie_js_cache = '' + print("āš ļø Using CDN lottie.min.js (local file not found)") + + return _lottie_js_cache + +def create_lottie_html(animation_data, height=600): + bg_style = """ + background-image: + linear-gradient(45deg, #666666 25%, transparent 25%), + linear-gradient(-45deg, #666666 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #666666 75%), + linear-gradient(-45deg, transparent 75%, #666666 75%); + background-size: 16px 16px; + background-position: 0 0, 0 8px, 8px -8px, -8px 0px; + background-color: #444444; + """ + + lottie_js = get_lottie_js() + + if lottie_js.startswith('{lottie_js}" + + animation_json_escaped = json.dumps(animation_data).replace('\\', '\\\\').replace("'", "\\'") + + anim_width = animation_data.get('w', 512) + anim_height = animation_data.get('h', 512) + + inner_html = f""" + + + + {lottie_script} + + + +
+
+
+ + +""" + + inner_html_b64 = base64.b64encode(inner_html.encode('utf-8')).decode('utf-8') + iframe_html = f'' + + return iframe_html + +def save_json_to_temp(lottie_json): + if lottie_json is None: + return None + + temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, prefix='lottie_') + json.dump(lottie_json, temp_file, indent=2) + temp_file.close() + return temp_file.name + +def process_text_to_lottie(text_prompt, max_tokens, use_sampling, temperature, top_p, top_k): + with generation_lock: + try: + start_time = time.time() + + if not text_prompt or not text_prompt.strip(): + return None, "āŒ Please enter a text description", None + model, processor, device = load_model_once() + + messages = build_messages("text", text_prompt=text_prompt) + inputs = prepare_inference_input(processor, messages, device) + + generated_ids = generate_lottie( + model, inputs, max_tokens, device, + use_sampling, temperature, top_p, top_k + ) + + del inputs + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + lottie_json = tokens_to_lottie_json(generated_ids) + + html = create_lottie_html(lottie_json, height=600) + + elapsed_time = time.time() - start_time + + status = f"āœ… Generated {len(generated_ids)} tokens | Layers: {len(lottie_json.get('layers', []))} | {lottie_json.get('fr', 8)} fps | Time: {elapsed_time:.1f}s" + + temp_path = save_json_to_temp(lottie_json) + + return html, status, temp_path + + except Exception as e: + if torch.cuda.is_available(): + torch.cuda.empty_cache() + return None, f"āŒ Error: {str(e)}", None + +def load_image_from_file(file_path): + + ext = os.path.splitext(file_path)[1].lower() + + if ext == '.svg': + import cairosvg + import io + png_bytes = cairosvg.svg2png(url=file_path, output_width=448, output_height=448) + image = PILImage.open(io.BytesIO(png_bytes)) + else: + image = PILImage.open(file_path) + + if image.mode == 'RGBA': + image = add_random_background(image) + else: + image = image.convert('RGB') + + return image + + +def process_image_to_lottie(image_file, text_description, max_tokens, use_sampling, temperature, top_p, top_k): + with generation_lock: + try: + start_time = time.time() + + if image_file is None: + return None, "āŒ Please upload an image", None + + model, processor, device = load_model_once() + + image = load_image_from_file(image_file) + + image = image.resize((448, 448), PILImage.LANCZOS) + + desc = text_description if text_description else "A simple animation" + messages = build_messages("image", text_prompt=desc, image=image) + inputs = prepare_inference_input(processor, messages, device) + + generated_ids = generate_lottie( + model, inputs, max_tokens, device, + use_sampling, temperature, top_p, top_k + ) + + del inputs + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + lottie_json = tokens_to_lottie_json(generated_ids) + + html = create_lottie_html(lottie_json, height=600) + + elapsed_time = time.time() - start_time + + status = f"āœ… Generated {len(generated_ids)} tokens | Layers: {len(lottie_json.get('layers', []))} | {lottie_json.get('fr', 8)} fps | Time: {elapsed_time:.1f}s" + + temp_path = save_json_to_temp(lottie_json) + + return html, status, temp_path + + except Exception as e: + if torch.cuda.is_available(): + torch.cuda.empty_cache() + return None, f"āŒ Error: {str(e)}", None + +def process_video_to_lottie(video, max_tokens, use_sampling, temperature, top_p, top_k): + with generation_lock: + try: + start_time = time.time() + + if video is None: + return None, "āŒ Please upload a video/GIF/WebP file", None + + import os + ext = os.path.splitext(video)[1].lower() if isinstance(video, str) else '' + if ext not in ('.mp4', '.avi', '.mov', '.gif', '.webp'): + return None, f"āŒ Unsupported format: {ext}. Please upload MP4/AVI/MOV/GIF/WebP", None + + model, processor, device = load_model_once() + + frames = load_frames_from_video(video, num_frames=8) + + messages = build_messages("video", video_frames=frames) + inputs = prepare_inference_input(processor, messages, device) + + generated_ids = generate_lottie( + model, inputs, max_tokens, device, + use_sampling, temperature, top_p, top_k + ) + + del inputs + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + lottie_json = tokens_to_lottie_json(generated_ids) + + html = create_lottie_html(lottie_json, height=600) + + elapsed_time = time.time() - start_time + + status = f"āœ… Generated {len(generated_ids)} tokens (from {len(frames)} frames) | Layers: {len(lottie_json.get('layers', []))} | {lottie_json.get('fr', 8)} fps | Time: {elapsed_time:.1f}s" + + temp_path = save_json_to_temp(lottie_json) + + return html, status, temp_path + + except Exception as e: + if torch.cuda.is_available(): + torch.cuda.empty_cache() + return None, f"āŒ Error: {str(e)}", None + +def create_gradio_interface(): + + with gr.Blocks(title="OmniLottie Demo Page", theme=gr.themes.Soft()) as demo: + gr.Markdown("# šŸŽØ OmniLottie Demo Page") + gr.Markdown("Offical Demo Page of OmniLottie") + gr.Markdown("Generate Lottie animations from text, images, or videos") + + with gr.Tabs() as tabs: + with gr.Tab("šŸ“ Text-to-Lottie"): + gr.Markdown(""" + ### šŸ’” Prompt Tips for Better Results + + **Good prompts should describe:** + 1. **Main Object**: What is being animated (e.g., "a blue bird", "a yellow folder icon", "an orange piggy bank") + 2. **Motion Pattern**: How it moves (e.g., "appearing, pulsing while sliding", "fading in, floating toward", "bouncing up and down") + 3. **Direction**: Where it moves (e.g., "downward", "toward the top-left", "back to its start") + 4. **Loop Behavior**: How it repeats (e.g., "repeating seamlessly", "looping smoothly", "repeating continuously") + + **Example Patterns:** + - šŸ”„ **Simple Loop**: "a red ball appearing, bouncing up and down, then fading out, repeating seamlessly" + - šŸŽÆ **Movement**: "a blue arrow sliding from left to right, then quickly returning to start, looping continuously" + - šŸ’« **Transformation**: "a yellow star fading in while rotating 360 degrees, holds briefly, then fading out, repeating smoothly" + - šŸŽØ **Static Icon**: "static illustration of a cartoon cat's face with a cute expression, light orange body, red inner ears" + - šŸ‘¤ **Character**: "animated cartoon figure dressed in a beige suit with a white shirt, holding a gray tablet" + + **Pro Tips:** + - Be specific about colors, shapes, and movements + - Describe motion phases clearly (appear → move → hold → return) + - Use descriptive motion verbs: sliding, pulsing, drifting, bouncing, rotating, fading + - For icons: include style details (outline, colors, decorations) + """) + + with gr.Row(): + with gr.Column(scale=1): + text_input = gr.Textbox( + label="Text Description", + placeholder="Example: a blue bird appearing, pulsing while sliding downward, lingers briefly, then growing back while sliding upward to reset, repeating seamlessly", + lines=5 + ) + + with gr.Accordion("āš™ļø Generation Settings", open=False): + gr.Markdown(""" + **Parameter Guide:** + - **Max Tokens**: Higher = more complex animations (slower), Lower = simpler animations (faster) + - **Top-p & Top-k**: Higher = more random/creative, Lower = more stable/consistent + - **Temperature**: Higher = more diverse, Lower = more deterministic + + šŸ’” **Quick Tips:** + - For complex animations: increase max tokens to 5856 + - For faster generation: reduce max tokens to 2048-3072 + - For more creative results: increase top-p (0.5-0.8) and top-k (20-50) + - For consistent results: decrease top-p (0.1-0.25) and top-k (5-10) + """) + text_max_tokens = gr.Slider(512, 5856, value=5556, step=256, label="Max Tokens") + text_use_sampling = gr.Checkbox(label="Use Sampling", value=True) + text_temperature = gr.Slider(0.1, 2.0, value=0.9, step=0.1, label="Temperature") + text_top_p = gr.Slider(0.1, 1.0, value=0.25, step=0.1, label="Top-p") + text_top_k = gr.Slider(1, 100, value=5, step=1, label="Top-k") + + text_generate_btn = gr.Button("šŸš€ Generate", variant="primary", size="lg") + + # Generation time tips + gr.Markdown(""" + ā±ļø **Generation Time:** + - Simple icons/shapes: ~30-60 seconds (1000-2000 tokens) + - Medium animations: ~1-2 minutes (2000-3500 tokens) + - Complex characters: ~4-5 minutes (4500-6000 tokens) + + Please be patient! Complex animations take time to generate. ā˜• + """) + + text_status = gr.Markdown() + + with gr.Column(scale=1): + text_output = gr.HTML(label="Animation Preview") + text_json_file = gr.File(label="šŸ“„ Download JSON", visible=True) + + def get_text_examples(): + examples = [] + demo_txt_path = "./example/demo.txt" + if os.path.exists(demo_txt_path): + with open(demo_txt_path, 'r', encoding='utf-8') as f: + lines = [line.strip() for line in f.readlines() if line.strip()] + examples = [[line] for line in lines[:50]] + return examples + + gr.Examples( + examples=get_text_examples(), + inputs=text_input, + label="šŸ“‚ Example Prompts (Click to Load)", + examples_per_page=10, + cache_examples=False + ) + + text_generate_btn.click( + fn=process_text_to_lottie, + inputs=[text_input, text_max_tokens, text_use_sampling, text_temperature, text_top_p, text_top_k], + outputs=[text_output, text_status, text_json_file] + ) + + with gr.Tab("šŸ–¼ļø Text+Image-to-Lottie"): + with gr.Row(): + with gr.Column(scale=1): + image_input = gr.Image( + label="Upload Image", + type="filepath", + sources=["upload"] + ) + image_text_input = gr.Textbox( + label="Animation Description", + placeholder="Example: The object rotates 360 degrees", + lines=3 + ) + + with gr.Accordion("āš™ļø Generation Settings", open=False): + gr.Markdown(""" + **Parameter Guide:** + - **Max Tokens**: Higher = more complex animations (slower), Lower = simpler (faster) + - **Top-p & Top-k**: Higher = more creative/random, Lower = more stable + - **Temperature**: Controls output diversity + """) + image_max_tokens = gr.Slider(512, 5556, value=5556, step=256, label="Max Tokens") + image_use_sampling = gr.Checkbox(label="Use Sampling", value=True) + image_temperature = gr.Slider(0.1, 2.0, value=0.9, step=0.1, label="Temperature") + image_top_p = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Top-p") + image_top_k = gr.Slider(1, 100, value=5, step=1, label="Top-k") + + image_generate_btn = gr.Button("šŸš€ Generate", variant="primary", size="lg") + + # Generation time tips + gr.Markdown(""" + ā±ļø **Generation Time:** ~1-5 minutes depending on complexity + """) + + image_status = gr.Markdown() + + with gr.Column(scale=1): + image_output = gr.HTML(label="Animation Preview") + image_json_file = gr.File(label="šŸ“„ Download JSON", visible=True) + + def get_image_text_examples(): + examples = [] + demo_images_dir = "./example/demo_images" + if os.path.exists(demo_images_dir): + png_files = sorted([f for f in os.listdir(demo_images_dir) if f.endswith('.png')]) + for png_file in png_files[:50]: + base_name = os.path.splitext(png_file)[0] + txt_file = os.path.join(demo_images_dir, f"{base_name}.txt") + png_path = os.path.join(demo_images_dir, png_file) + if os.path.exists(txt_file): + with open(txt_file, 'r', encoding='utf-8') as f: + text_desc = f.read().strip() + examples.append([png_path, text_desc]) + return examples + + gr.Examples( + examples=get_image_text_examples(), + inputs=[image_input, image_text_input], + label="šŸ“‚ Example Images (Click to Load)", + examples_per_page=5, + cache_examples=False + ) + + image_generate_btn.click( + fn=process_image_to_lottie, + inputs=[image_input, image_text_input, image_max_tokens, image_use_sampling, + image_temperature, image_top_p, image_top_k], + outputs=[image_output, image_status, image_json_file] + ) + + # Tab 3: Video-to-Lottie + with gr.Tab("šŸŽ„ Video-to-Lottie"): + with gr.Row(): + with gr.Column(scale=1): + video_input = gr.Video( + label="Upload Video / GIF / WebP", + sources=["upload"] + ) + + with gr.Accordion("āš™ļø Generation Settings", open=False): + gr.Markdown(""" + **Parameter Guide:** + - **Max Tokens**: Higher = more complex animations (slower), Lower = simpler (faster) + - **Top-p & Top-k**: Higher = more creative/random, Lower = more stable + - **Temperature**: Controls output diversity + """) + video_max_tokens = gr.Slider(512, 5556, value=5556, step=256, label="Max Tokens") + video_use_sampling = gr.Checkbox(label="Use Sampling", value=True) + video_temperature = gr.Slider(0.1, 2.0, value=0.9, step=0.1, label="Temperature") + video_top_p = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Top-p") + video_top_k = gr.Slider(1, 100, value=5, step=1, label="Top-k") + + video_generate_btn = gr.Button("šŸš€ Generate", variant="primary", size="lg") + + # Generation time tips + gr.Markdown(""" + ā±ļø **Generation Time:** ~2-5 minutes depending on video complexity + """) + + video_status = gr.Markdown() + + with gr.Column(scale=1): + video_output = gr.HTML(label="Animation Preview") + video_json_file = gr.File(label="šŸ“„ Download JSON", visible=True) + + def get_video_examples(): + examples = [] + demo_video_dir = "./example/demo_video" + if os.path.exists(demo_video_dir): + video_files = sorted([os.path.join(demo_video_dir, f) + for f in os.listdir(demo_video_dir) + if f.endswith('.mp4')]) + examples = [[vf] for vf in video_files[:50]] + return examples + + gr.Examples( + examples=get_video_examples(), + inputs=video_input, + label="šŸ“‚ Example Videos (Click to Load)", + examples_per_page=5, + cache_examples=False + ) + + video_generate_btn.click( + fn=process_video_to_lottie, + inputs=[video_input, video_max_tokens, video_use_sampling, + video_temperature, video_top_p, video_top_k], + outputs=[video_output, video_status, video_json_file] + ) + + gr.Markdown("---") + gr.Markdown(""" + āš ļø **Important Note:** This demo processes one request at a time. + If another user is generating, your request will wait in queue until the current one completes. + """) + + return demo + +# ========== åÆåŠØåŗ”ē”Ø ========== +if __name__ == "__main__": + demo = create_gradio_interface() + demo.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + show_error=True + ) diff --git a/assets/OmniLottie-demo-1.gif b/assets/OmniLottie-demo-1.gif new file mode 100644 index 0000000..c9493f7 Binary files /dev/null and b/assets/OmniLottie-demo-1.gif differ diff --git a/assets/OmniLottie-demo-2.gif b/assets/OmniLottie-demo-2.gif new file mode 100644 index 0000000..421aacf Binary files /dev/null and b/assets/OmniLottie-demo-2.gif differ diff --git a/assets/OmniLottie-demo-3.gif b/assets/OmniLottie-demo-3.gif new file mode 100644 index 0000000..2b7d2ad Binary files /dev/null and b/assets/OmniLottie-demo-3.gif differ diff --git a/assets/OmniLottie-main-demo.gif b/assets/OmniLottie-main-demo.gif new file mode 100644 index 0000000..fa95c3d Binary files /dev/null and b/assets/OmniLottie-main-demo.gif differ diff --git a/decoder.py b/decoder.py new file mode 100644 index 0000000..74543b6 --- /dev/null +++ b/decoder.py @@ -0,0 +1,67 @@ +import torch +import torch.nn as nn +from transformers import Qwen2_5_VLForConditionalGeneration, AutoConfig +from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import Qwen2_5_VLCausalLMOutputWithPast +from typing import Any, Dict, List, Optional, Tuple, Union + + +import transformers.models.qwen2_5_vl.modeling_qwen2_5_vl as qwen_modeling + + + +class LottieDecoder(nn.Module): + """ + Autoregressive generative model for OmniLottie + """ + + def __init__(self, + pix_len, + text_len, + model_path="Qwen/Qwen2.5-VL-3B-Instruct", + **kwargs): + super().__init__() + + self.pix_len = pix_len + self.text_len = text_len + + self.vocab_size = 192400 + self.bos_token_id = 192398 + self.eos_token_id = 192399 + self.pad_token_id = 151643 + + print(f"Loading model from {model_path}...") + + config = AutoConfig.from_pretrained( + model_path, + vocab_size=self.vocab_size, + bos_token_id=self.bos_token_id, + eos_token_id=self.eos_token_id, + pad_token_id=self.pad_token_id, + trust_remote_code=True + ) + + self.transformer = Qwen2_5_VLForConditionalGeneration.from_pretrained( + model_path, + config=config, + torch_dtype=torch.bfloat16, + attn_implementation="eager", + ignore_mismatched_sizes=True + ) + + self.transformer.resize_token_embeddings(self.vocab_size) + + self.train() + + def forward(self, + input_ids=None, + attention_mask=None, + pixel_values=None, + image_grid_thw=None, + pixel_values_videos = None, + video_grid_thw = None, + labels=None, + past_key_values=None, + use_cache=False, + **kwargs): + + return NotImplementedError \ No newline at end of file diff --git a/download_mmlottie_bench.py b/download_mmlottie_bench.py new file mode 100755 index 0000000..9488b0d --- /dev/null +++ b/download_mmlottie_bench.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +MMLottieBench ę•°ę®é›†äø‹č½½č„šęœ¬ + +用途: + 从 HuggingFace äø‹č½½ OmniLottie/MMLottieBench ę•°ę®é›†å¹¶äæå­˜åˆ°ęœ¬åœ° + +使用方法: + python download_mmlottie_bench.py + python download_mmlottie_bench.py --output_dir /custom/path + +ę•°ę®é›†ē»“ęž„: + čÆ„ę•°ę®é›†ä½æē”Ø HuggingFace Datasets ę ¼å¼ + - Splits: real, synthetic + - Task types: Text-to-Lottie, Text-Image-to-Lottie, Video-to-Lottie + - Fields: id, text, image, video, task_type, subset, etc. + +ę³Øę„: + ę•°ę®é›†ä½æē”Ø Apache Arrow ę ¼å¼å­˜å‚Øļ¼Œå›¾åƒå’Œč§†é¢‘å·²åµŒå…„å…¶äø­ + äøéœ€č¦ę‰‹åŠØč§£åŽ‹ļ¼ē›“ęŽ„é€ščæ‡ datasets API č®æé—®å³åÆ +""" + +import os +import argparse +import sys +from pathlib import Path + +try: + from datasets import load_dataset +except ImportError: + print("āŒ Error: datasets library not installed") + print("Please install it with: pip install datasets") + sys.exit(1) + + +def download_and_save_dataset(output_dir): + """ + 从 HuggingFace äø‹č½½ MMLottieBench ę•°ę®é›†å¹¶äæå­˜åˆ°ęœ¬åœ° + + Args: + output_dir: äæå­˜č·Æå¾„ + + Returns: + bool: äø‹č½½ę˜Æå¦ęˆåŠŸ + """ + # č½¬ę¢äøŗē»åÆ¹č·Æå¾„ + output_dir = os.path.abspath(output_dir) + + print("=" * 70) + print("šŸŽØ MMLottieBench Dataset Downloader") + print("=" * 70) + print(f"šŸ“¦ Repository: OmniLottie/MMLottieBench") + print(f"šŸ“ Output directory: {output_dir}") + print("=" * 70) + print() + + # ę£€ęŸ„ē›®å½•ę˜Æå¦å·²å­˜åœØ + if os.path.exists(output_dir) and os.listdir(output_dir): + print(f"āš ļø Directory already exists and is not empty: {output_dir}") + response = input("Continue and overwrite? [y/N]: ") + if response.lower() != 'y': + print("āŒ Download cancelled by user") + return False + print() + + try: + print("šŸ“„ Step 1/2: Downloading dataset from HuggingFace...") + print("ā³ This may take a while depending on your network speed...") + print() + + # äø‹č½½ę•°ę®é›†ļ¼ˆä¼šč‡ŖåŠØä½æē”Ø HF ē¼“å­˜ļ¼‰ + dataset = load_dataset("OmniLottie/MMLottieBench") + + print() + print("šŸ“Š Dataset loaded successfully!") + print(f" Available splits: {list(dataset.keys())}") + + for split_name, split_data in dataset.items(): + print(f" - {split_name}: {len(split_data)} samples") + + # ē»Ÿč®”ä»»åŠ”ē±»åž‹ + task_types = {} + for sample in split_data: + task_type = sample.get('task_type', 'Unknown') + task_types[task_type] = task_types.get(task_type, 0) + 1 + + for task_type, count in task_types.items(): + print(f" • {task_type}: {count} samples") + + print() + print("šŸ’¾ Step 2/2: Saving dataset to disk...") + + # äæå­˜åˆ°ē£ē›˜ + dataset.save_to_disk(output_dir) + + print() + print("=" * 70) + print("āœ… Download and save completed successfully!") + print("=" * 70) + print(f"šŸ“ Dataset saved to: {output_dir}") + print() + print("šŸ“– Usage in Python:") + print(" from datasets import load_from_disk") + print(f" dataset = load_from_disk('{output_dir}')") + print(" real_data = dataset['real']") + print(" synthetic_data = dataset['synthetic']") + print() + print("šŸ“– Usage in inference:") + print(f" python inference.py --split real --sketch_weight ") + print() + print("šŸ’” Note: Data is stored in Apache Arrow format") + print(" Images and videos are embedded - no need to extract!") + print(" Access them directly through the datasets API.") + print() + + return True + + except KeyboardInterrupt: + print("\n\nāš ļø Download interrupted by user") + return False + + except Exception as e: + print() + print("=" * 70) + print("āŒ Download failed!") + print("=" * 70) + print(f"Error: {str(e)}") + print() + print("šŸ’” Troubleshooting:") + print(" 1. Check your network connection") + print(" 2. Make sure you have write permission to the target directory") + print(" 3. Install required packages: pip install datasets") + print(" 4. Try setting HF mirror: export HF_ENDPOINT=https://hf-mirror.com") + print() + print("šŸ“– Manual access:") + print(" Visit: https://huggingface.co/datasets/OmniLottie/MMLottieBench") + print(" Or use in Python:") + print(" from datasets import load_dataset") + print(" dataset = load_dataset('OmniLottie/MMLottieBench')") + print("=" * 70) + return False + + +def main(): + parser = argparse.ArgumentParser( + description='Download MMLottieBench dataset from HuggingFace', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Download to default location + python download_mmlottie_bench.py + + # Download to specific directory + python download_mmlottie_bench.py --output_dir /data/datasets/mmlottie_bench + +Dataset structure (Arrow format): + mmlottie_bench/ + ā”œā”€ā”€ dataset_dict.json + ā”œā”€ā”€ real/ + │ ā”œā”€ā”€ data-00000-of-00001.arrow (contains images, videos, text) + │ ā”œā”€ā”€ dataset_info.json + │ └── state.json + └── synthetic/ + └── (same structure) + +About the dataset: + MMLottieBench is a benchmark dataset for Lottie animation generation + + Splits: + - real: Real-world Lottie animations (450 samples) + - synthetic: Synthetically generated samples (450 samples) + + Task types (150 samples each per split): + - Text-to-Lottie: Generate from text prompt + - Text-Image-to-Lottie: Generate from text + image + - Video-to-Lottie: Generate from video + + Data format: + - Stored in Apache Arrow format (efficient, compressed) + - Images: embedded as PIL.Image objects + - Videos: embedded as VideoReader objects + - Text: direct string storage + - NO manual extraction needed! + +Usage after download: + 1. In Python code: + from datasets import load_from_disk + dataset = load_from_disk('/data/cref/Lottie-kaiyuan/mmlottie_bench') + real_data = dataset['real'] + + 2. In inference: + python inference.py --split real --sketch_weight + +For more info, see: + - MMLOTTIE_BENCH_USAGE.md + - MMLOTTIE_BENCH_DATA_ACCESS.md + """ + ) + + parser.add_argument( + '--output_dir', + type=str, + default='/data/cref/Lottie-kaiyuan/mmlottie_bench', + help='Output directory to save the dataset (default: /data/cref/Lottie-kaiyuan/mmlottie_bench)' + ) + + args = parser.parse_args() + + # ę‰§č”Œäø‹č½½ + success = download_and_save_dataset(args.output_dir) + + if not success: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/example/demo.txt b/example/demo.txt new file mode 100644 index 0000000..3ca2216 --- /dev/null +++ b/example/demo.txt @@ -0,0 +1,38 @@ +a blue bird appearing, pulsing while sliding downward, lingers briefly, then growing back while sliding upward to reset with clear phase changes, repeating seamlessly +a light blue piggy bank with a darker blue outline, with a single light blue coin with a dark blue yen symbol (£) appears above the piggy bank, then starts descending towards the piggy bank's opening +a purple butterfly appearing, turning while drifting toward the bottom-right, holds position briefly, then drifting toward the top-left back to its start while bouncing, looping smoothly +static illustration of a cartoon cat's face with a cute expression, the cat has a light orange body, red inner ears +animated cartoon-style figure dressed in a beige suit with a white shirt and dark hair, the figure holds a gray tablet in his left hand +animated character dressed in a beige suit jacket over a white shirt, paired with beige trousers and white shoes, the character has light brown hair tied back in a ponytail +a yellow folder icon fading in, sliding to the left while compressing, stays still briefly, then sliding to the right back to its start while fading out with clear phase changes, repeating seamlessly +animated character, a woman dressed in a black blazer over a white shirt and a black skirt, standing +a yellow progress bar fading in, sliding to the left while wobbling, stays still briefly, then sliding to the right back to its start while fading away, looping smoothly +animated scene featuring a chef holding a magnifying glass, the chef is dressed in a white uniform with black buttons and a traditional tall white chef's hat, the chef is standing +a blue QR code icon fading in, floating toward the top-left while pulsing, then briefly dimming, and then floating toward the bottom-right back to its start while turning, repeating continuously +animated sequence featuring a man in a beige suit holding a pink brain-like object labeled 'A.I.' with black text and pink circuit-like patterns, the man has dark hair and a beard, +bright orange piggy bank with a single black eye and a small pink ear, on top of the piggy bank, there are three green paper bills with a stylized Bitcoin symbol (₿) +cartoon-style character with black hair, wearing a gray suit with a white shirt and red tie, sits behind a wooden desk facing forward +cartoonish, yellow, smiley face emoji with pink lips +central Microsoft logo consisting of four colored squares—red, green, blue +consistent animated sequence of a stylized human figure with short brown hair and a light skin tone, the figure is plain with no additional markings, as the frames progress +dynamic emoji animation set, the emoji has a yellow face with an orange border, two eyes with red irises and black pupils +dynamic sequence of a stylized musical note symbol composed of layered black, cyan, and pink tones +five yellow stars appearing in succession, starting with one star and incrementally adding one more in each subsequent frame until all five stars are full +flat design animation of a person standing, the individual is wearing a black shirt and brown pants, and has short dark hair +green money bag icon with an Arabic word written in white, the bag has a brown strap tied around the top, to the right of the bag, there is a red circular badge containing a white lock +multiple pink cartoon crabs scattered, each crab has a simple, cute design with black eyes and antennae +orange-and-white error sign hanging from a brown rope attached to a small orange ball, the sign displaying the text 'ERROR' in dark orange and '404' in bright orange +sequence of three frames showing a static yellow diamond-shaped road sign with a black border, the sign features two stick-figure children playing on a seesaw, depicted as a simple +sequence of transformations involving a yellow logo with a white ghost-like figure outlined, four separate yellow sections containing parts of the ghost +sequence starting with an empty white screen, a light purple computer monitor fading in, followed by two silhouettes of people sitting in front of it +simple animation featuring a black silhouette icon representing two people standing side by side against, the left figure remaining stationary while the right fi +starting with a single group of three question marks in light purple, then expand into a vibrant displaying of multiple colorful question marks arranged in a scattered pattern acro +static cartoon-style illustration of a man wearing a white helmet, a white shirt, and gray pants +static cartoon-style illustration of a person sitting at a desk, the person has brown hair and is wearing a black jacket over a beige shirt, paired with beige pants and dark blue s +static illustration of a person in a beige suit holding a large, oversized blue credit card above their head, the person wears dark sunglasses and black shoes +stylized, animated letter 'U' that is blue with a black outline and simple white circles as eyes, over time +two orange lines forming an angular shape, these lines evolve into a more complex geometric design, incorporating curved arcs and layered elements +vibrant orange-pink balloon appearing, inflating and rising upwards as it filling with air, the balloon features bold white numbers '86' centered within it +woman with light brown hair styled in loose waves is standing, she is wearing a pink short-sleeved shirt, black pants +yellow, cartoon-style emoji face with brown hands raised at the sides, brown eyes +gradual transformation of a gray circular logo featuring a white cat silhouette into a black circular logo with the same white cat silhouette, the background remaining consistently \ No newline at end of file diff --git a/example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.png b/example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.png new file mode 100644 index 0000000..0654325 Binary files /dev/null and b/example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.png differ diff --git a/example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.txt b/example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.txt new file mode 100644 index 0000000..88df796 --- /dev/null +++ b/example/demo_images/00de75e2c031cb3fc3f472e356aba5b6.txt @@ -0,0 +1 @@ +A cyan-colored, elongated shape resembling a smiley face, consisting of two pink oval eyes with navy blue circular pupils positioned symmetrically. The shape rotates gradually around its horizontal axis, revealing small animated sparkles—pink and teal—that appear sporadically near the eyes, enhancing the dynamic visual effect. \ No newline at end of file diff --git a/example/demo_images/04825e6bb18965fc8b7dbdeb3562fb46.png b/example/demo_images/04825e6bb18965fc8b7dbdeb3562fb46.png new file mode 100644 index 0000000..3006f7c Binary files /dev/null and b/example/demo_images/04825e6bb18965fc8b7dbdeb3562fb46.png differ diff --git a/example/demo_images/04825e6bb18965fc8b7dbdeb3562fb46.txt b/example/demo_images/04825e6bb18965fc8b7dbdeb3562fb46.txt new file mode 100644 index 0000000..bca1773 --- /dev/null +++ b/example/demo_images/04825e6bb18965fc8b7dbdeb3562fb46.txt @@ -0,0 +1 @@ +Two simplistic, cartoon-style figures standing behind podiums, engaging in a dialogue. Initially, the figures are partially obscured by the podiums, but as the video progresses, they gradually rise fully into view. Each figure has a speech bubble above them, indicating ongoing dialogue. The figures are primarily purple with black outlines, while the podiums are yellow with orange outlines. The background remains consistently white throughout the sequence, emphasizing the figures and their interaction. \ No newline at end of file diff --git a/example/demo_images/0a8696814a84579432e8dd4125b3bafd.png b/example/demo_images/0a8696814a84579432e8dd4125b3bafd.png new file mode 100644 index 0000000..0008fb1 Binary files /dev/null and b/example/demo_images/0a8696814a84579432e8dd4125b3bafd.png differ diff --git a/example/demo_images/0a8696814a84579432e8dd4125b3bafd.txt b/example/demo_images/0a8696814a84579432e8dd4125b3bafd.txt new file mode 100644 index 0000000..9b974a1 --- /dev/null +++ b/example/demo_images/0a8696814a84579432e8dd4125b3bafd.txt @@ -0,0 +1 @@ +A static illustration features a cartoon-style chef character wearing a white chef's hat and white jacket. The chef holds a large, bright red chat bubble icon with three white dots inside it, positioned prominently in the foreground. The chef's right hand grips the chat bubble, while the left hand is extended outward as if presenting it. There are no visible animations, movements, or transitions within the frames; the image maintains a consistent composition throughout the sequence. \ No newline at end of file diff --git a/example/demo_images/0d185a0ef262e0423fd3072a338894c2.png b/example/demo_images/0d185a0ef262e0423fd3072a338894c2.png new file mode 100644 index 0000000..a77b886 Binary files /dev/null and b/example/demo_images/0d185a0ef262e0423fd3072a338894c2.png differ diff --git a/example/demo_images/0d185a0ef262e0423fd3072a338894c2.txt b/example/demo_images/0d185a0ef262e0423fd3072a338894c2.txt new file mode 100644 index 0000000..72873c2 --- /dev/null +++ b/example/demo_images/0d185a0ef262e0423fd3072a338894c2.txt @@ -0,0 +1 @@ +A dynamic animation of a purple rectangular button with the white text 'Sale' centered on it. The button undergoes multiple transformations, including shifts in position, changes in perspective, and subtle color variations, giving it a three-dimensional appearance. Throughout the sequence, the button rotates slightly, creating a sense of depth and movement while maintaining its central focus in the frame. \ No newline at end of file diff --git a/example/demo_images/0df7918f8e1109a5c2055f06d2958108.png b/example/demo_images/0df7918f8e1109a5c2055f06d2958108.png new file mode 100644 index 0000000..7f8709e Binary files /dev/null and b/example/demo_images/0df7918f8e1109a5c2055f06d2958108.png differ diff --git a/example/demo_images/0df7918f8e1109a5c2055f06d2958108.txt b/example/demo_images/0df7918f8e1109a5c2055f06d2958108.txt new file mode 100644 index 0000000..f8bb3d8 --- /dev/null +++ b/example/demo_images/0df7918f8e1109a5c2055f06d2958108.txt @@ -0,0 +1 @@ +A yellow folder icon gradually appears, followed by a curved gray line that extends outward, forming a circular shape around the folder. A green circle then slides into view from the right side and transforms into a green checkmark within the circle. The overall visual sequence depicts the creation and completion of a successful digital file organization process, represented through a folder icon with a green checkmark indicating confirmation or verification. \ No newline at end of file diff --git a/example/demo_images/1687629f8efa88e75bdf75d9871e5e87.png b/example/demo_images/1687629f8efa88e75bdf75d9871e5e87.png new file mode 100644 index 0000000..15de666 Binary files /dev/null and b/example/demo_images/1687629f8efa88e75bdf75d9871e5e87.png differ diff --git a/example/demo_images/1687629f8efa88e75bdf75d9871e5e87.txt b/example/demo_images/1687629f8efa88e75bdf75d9871e5e87.txt new file mode 100644 index 0000000..918ee96 --- /dev/null +++ b/example/demo_images/1687629f8efa88e75bdf75d9871e5e87.txt @@ -0,0 +1 @@ +A central green square with rounded corners containing white and black text 'Uber Eats'. Attached to the top right corner of the green square is a red discount tag with a pink percentage symbol (%). The discount tag vibrates slightly, causing small pink sparkles to appear intermittently near it, creating a dynamic yet subtle animation effect. \ No newline at end of file diff --git a/example/demo_images/18d9290b82888f57df3456bb88aecee2.png b/example/demo_images/18d9290b82888f57df3456bb88aecee2.png new file mode 100644 index 0000000..00881c3 Binary files /dev/null and b/example/demo_images/18d9290b82888f57df3456bb88aecee2.png differ diff --git a/example/demo_images/18d9290b82888f57df3456bb88aecee2.txt b/example/demo_images/18d9290b82888f57df3456bb88aecee2.txt new file mode 100644 index 0000000..13b0feb --- /dev/null +++ b/example/demo_images/18d9290b82888f57df3456bb88aecee2.txt @@ -0,0 +1 @@ +A light gray rectangular box with rounded edges appears, representing the body of an ATM machine. On top of this box is a slightly darker gray horizontal bar, serving as the card insertion slot.', 'As the scene progresses, a light teal-colored vertical rectangle with a dark blue stripe in the center slides out from the bottom of the ATM machine, symbolizing a credit card being inserted. The card continues to move downward until it stops below the ATM slot.', 'Finally, a red location marker with a white circle at its tip appears directly beneath the credit card, indicating a geolocation feature. This completes the visual narrative, emphasizing the integration of payment and geographic services. \ No newline at end of file diff --git a/example/demo_images/1a3f5add36af9102a0b82a70dfa3f9f2.png b/example/demo_images/1a3f5add36af9102a0b82a70dfa3f9f2.png new file mode 100644 index 0000000..ea113e5 Binary files /dev/null and b/example/demo_images/1a3f5add36af9102a0b82a70dfa3f9f2.png differ diff --git a/example/demo_images/1a3f5add36af9102a0b82a70dfa3f9f2.txt b/example/demo_images/1a3f5add36af9102a0b82a70dfa3f9f2.txt new file mode 100644 index 0000000..7953174 --- /dev/null +++ b/example/demo_images/1a3f5add36af9102a0b82a70dfa3f9f2.txt @@ -0,0 +1 @@ +A small horizontal blue line that gradually extends into a larger rectangular frame. The frame then reveals a projected image, which includes a green and yellow pie chart in the upper left, a bar graph with varying heights in blue, green, and yellow on the right, and some placeholder text boxes in gray and the projection screen is bordered by a dark blue header and footer, each with rounded edges. \ No newline at end of file diff --git a/example/demo_images/2be9b8482af53529ef11c7ca0ed45918.png b/example/demo_images/2be9b8482af53529ef11c7ca0ed45918.png new file mode 100644 index 0000000..f3b5ca1 Binary files /dev/null and b/example/demo_images/2be9b8482af53529ef11c7ca0ed45918.png differ diff --git a/example/demo_images/2be9b8482af53529ef11c7ca0ed45918.txt b/example/demo_images/2be9b8482af53529ef11c7ca0ed45918.txt new file mode 100644 index 0000000..d06d0ca --- /dev/null +++ b/example/demo_images/2be9b8482af53529ef11c7ca0ed45918.txt @@ -0,0 +1 @@ +A sequence of four frames featuring an animated character with short brown hair, pink cheeks, black eyes, and a small black nose. The character's facial expressions change across the frames: starting with closed eyes and a frown, then transitioning to open eyes with a slight smile, followed by a squinted expression with a more pronounced smile, and finally returning to a neutral expression with slightly furrowed eyebrows. The background remains consistently white throughout all frames, emphasizing the character's facial changes. There are no additional elements or visual effects apart from the character's face, which morphs smoothly between expressions. \ No newline at end of file diff --git a/example/demo_images/35153d4358da699d688d77dbcd349c72.png b/example/demo_images/35153d4358da699d688d77dbcd349c72.png new file mode 100644 index 0000000..8c3af72 Binary files /dev/null and b/example/demo_images/35153d4358da699d688d77dbcd349c72.png differ diff --git a/example/demo_images/35153d4358da699d688d77dbcd349c72.txt b/example/demo_images/35153d4358da699d688d77dbcd349c72.txt new file mode 100644 index 0000000..8a7d693 --- /dev/null +++ b/example/demo_images/35153d4358da699d688d77dbcd349c72.txt @@ -0,0 +1 @@ +A man in a purple shirt and black pants stands next to a large black tablet with a white screen. He holds a silver laptop in his hands, interacting with it while a green globe animation appears and expands on the tablet's screen. Surrounding the scene are several colorful circular icons: a blue globe icon on the left, a white Wi-Fi signal icon above, a yellow envelope icon to the right, an orange lightbulb icon in the top-right corner, and a blue human profile icon below the lightbulb. The globe animation on the tablet screen gradually expands and shifts position during the sequence. \ No newline at end of file diff --git a/example/demo_images/3c2a55fe6c19040f011416f2492d2ffa.png b/example/demo_images/3c2a55fe6c19040f011416f2492d2ffa.png new file mode 100644 index 0000000..60c77a6 Binary files /dev/null and b/example/demo_images/3c2a55fe6c19040f011416f2492d2ffa.png differ diff --git a/example/demo_images/3c2a55fe6c19040f011416f2492d2ffa.txt b/example/demo_images/3c2a55fe6c19040f011416f2492d2ffa.txt new file mode 100644 index 0000000..e365a74 --- /dev/null +++ b/example/demo_images/3c2a55fe6c19040f011416f2492d2ffa.txt @@ -0,0 +1 @@ +A black location pin icon with a blue circular area at its center, containing a white ship silhouette. The pin initially appears stationary, then it rises slightly with a subtle drop shadow effect under it, creating an illusion of elevation. After rising, the pin gently descends back to its original position, returning to a stable state without the shadow effect. \ No newline at end of file diff --git a/example/demo_images/447eb0ca72d2b5b64d2569d9a1c1c528.png b/example/demo_images/447eb0ca72d2b5b64d2569d9a1c1c528.png new file mode 100644 index 0000000..ac07d4b Binary files /dev/null and b/example/demo_images/447eb0ca72d2b5b64d2569d9a1c1c528.png differ diff --git a/example/demo_images/447eb0ca72d2b5b64d2569d9a1c1c528.txt b/example/demo_images/447eb0ca72d2b5b64d2569d9a1c1c528.txt new file mode 100644 index 0000000..d69d3e8 --- /dev/null +++ b/example/demo_images/447eb0ca72d2b5b64d2569d9a1c1c528.txt @@ -0,0 +1 @@ +A simple, minimalist animation consisting of a central white figure represented by a circle and a green triangle filled with horizontal lines. Surrounding the figure are four purple L-shaped brackets positioned symmetrically at the top, bottom, left, and right. The white figure performs a slight pulsating motion, growing and shrinking in size while remaining centered. The green triangle remains static throughout the sequence, adding a textured contrast to the composition. The animation employs a clean, geometric style with smooth transitions and a steady rhythm. No other elements or background details are present, emphasizing simplicity and focus on the central figure's movement. \ No newline at end of file diff --git a/example/demo_images/483a0868c4b648a83ef43da55cd57362.png b/example/demo_images/483a0868c4b648a83ef43da55cd57362.png new file mode 100644 index 0000000..6b3e87b Binary files /dev/null and b/example/demo_images/483a0868c4b648a83ef43da55cd57362.png differ diff --git a/example/demo_images/483a0868c4b648a83ef43da55cd57362.txt b/example/demo_images/483a0868c4b648a83ef43da55cd57362.txt new file mode 100644 index 0000000..c97ba5c --- /dev/null +++ b/example/demo_images/483a0868c4b648a83ef43da55cd57362.txt @@ -0,0 +1 @@ +Two flat-style animated characters interacting with monetary elements. The left character, wearing a blue shirt and brown pants, holds a large golden coin with a dollar sign. The right character, dressed in a blue shirt and dark blue pants, holds a brown folder and gestures toward a stack of golden coins with dollar signs, positioned centrally. An orange arrow-shaped cloud icon moves rhythmically above the central coin stack, pointing upward, while additional white clouds appear and disappear dynamically around it. The scene remains static except for the subtle motion of the arrow cloud and the white clouds, emphasizing the financial theme through the characters' positioning and the coin imagery. \ No newline at end of file diff --git a/example/demo_images/525a1e24714cced5b969e32420a1d157.png b/example/demo_images/525a1e24714cced5b969e32420a1d157.png new file mode 100644 index 0000000..db15829 Binary files /dev/null and b/example/demo_images/525a1e24714cced5b969e32420a1d157.png differ diff --git a/example/demo_images/525a1e24714cced5b969e32420a1d157.txt b/example/demo_images/525a1e24714cced5b969e32420a1d157.txt new file mode 100644 index 0000000..0812281 --- /dev/null +++ b/example/demo_images/525a1e24714cced5b969e32420a1d157.txt @@ -0,0 +1 @@ +A single blue speech bubble icon centered. Initially, the speech bubble appears partially filled with light blue, then gradually fills completely with solid blue. Inside the speech bubble, three small white dots appear, animate by moving slightly back and forth horizontally, and remain visible throughout the sequence. \ No newline at end of file diff --git a/example/demo_images/5b4d76eafdb250eda8257c12133ed77d.png b/example/demo_images/5b4d76eafdb250eda8257c12133ed77d.png new file mode 100644 index 0000000..fb1ef88 Binary files /dev/null and b/example/demo_images/5b4d76eafdb250eda8257c12133ed77d.png differ diff --git a/example/demo_images/5b4d76eafdb250eda8257c12133ed77d.txt b/example/demo_images/5b4d76eafdb250eda8257c12133ed77d.txt new file mode 100644 index 0000000..d2d1230 --- /dev/null +++ b/example/demo_images/5b4d76eafdb250eda8257c12133ed77d.txt @@ -0,0 +1 @@ +A blue hanging tag icon with white text '92% OFF' displayed on it. The tag initially appears static. A subtle blur effect then moves across the tag, creating a ghosting effect where multiple instances of the tag overlap slightly. This blurring creates a sense of motion and depth before gradually resolving back to the original static image of the tag. The overall style is minimalist with clean lines and a focus on the motion effect. \ No newline at end of file diff --git a/example/demo_images/5b967e3419dd9f0cdb444d4aae368327.png b/example/demo_images/5b967e3419dd9f0cdb444d4aae368327.png new file mode 100644 index 0000000..f23ca63 Binary files /dev/null and b/example/demo_images/5b967e3419dd9f0cdb444d4aae368327.png differ diff --git a/example/demo_images/5b967e3419dd9f0cdb444d4aae368327.txt b/example/demo_images/5b967e3419dd9f0cdb444d4aae368327.txt new file mode 100644 index 0000000..fcd59fa --- /dev/null +++ b/example/demo_images/5b967e3419dd9f0cdb444d4aae368327.txt @@ -0,0 +1 @@ +A stylized, two-dimensional hourglass icon depicted in light yellow with black outlines. The upper bulb contains sand represented by a dark brown fill, which is positioned slightly off-center to indicate movement. Shadows in light gray surround the hourglass. As the sequence progresses, the hourglass rotates smoothly to achieve a vertical alignment. During this rotation, the sand appears to shift fluidly within it, emphasizing the passage of time. The graphic undergoes a transformation where the entire design fills with a solid yellow hue, enhancing its visibility and creating a bold look. The black outlines remain consistent throughout, providing structure.', 'By the end of the sequence, the hourglass is fully vertical, maintaining its solid yellow fill and black outlines. The sand within has settled into the lower bulb, forming a triangular shape that represents the completion of the time interval. \ No newline at end of file diff --git a/example/demo_images/664543ea2e9e9a1059ea233aabe4d8ff.png b/example/demo_images/664543ea2e9e9a1059ea233aabe4d8ff.png new file mode 100644 index 0000000..3c88ea0 Binary files /dev/null and b/example/demo_images/664543ea2e9e9a1059ea233aabe4d8ff.png differ diff --git a/example/demo_images/664543ea2e9e9a1059ea233aabe4d8ff.txt b/example/demo_images/664543ea2e9e9a1059ea233aabe4d8ff.txt new file mode 100644 index 0000000..f070b08 --- /dev/null +++ b/example/demo_images/664543ea2e9e9a1059ea233aabe4d8ff.txt @@ -0,0 +1 @@ +A simple animation of a hand holding a large yellow coin with a black dollar sign ($). The hand, depicted in light skin tone with a blue sleeve and a black cuff, moves slightly to spin the coin in place, simulating a flipping motion. The coin's rotation is smooth and continuous, emphasizing the central dollar sign, which remains clearly visible throughout the sequence. The background is consistently white, ensuring that the focus remains on the hand and coin interaction. \ No newline at end of file diff --git a/example/demo_images/8abf9b2fc4caa70a07550086f3eced88.png b/example/demo_images/8abf9b2fc4caa70a07550086f3eced88.png new file mode 100644 index 0000000..f9f3292 Binary files /dev/null and b/example/demo_images/8abf9b2fc4caa70a07550086f3eced88.png differ diff --git a/example/demo_images/8abf9b2fc4caa70a07550086f3eced88.txt b/example/demo_images/8abf9b2fc4caa70a07550086f3eced88.txt new file mode 100644 index 0000000..52850de --- /dev/null +++ b/example/demo_images/8abf9b2fc4caa70a07550086f3eced88.txt @@ -0,0 +1 @@ +A circular graphic with a silver outer ring and a bright green inner circle. At the center is an orange hand making a peace sign gesture with the index and middle fingers raised. The hand appears to be slightly animated, as it undergoes minor transformations over the frames, such as slight changes in position and shape, giving the impression of motion or breathing life into the graphic. The black outlines around the hand and the rings emphasize the shapes clearly. \ No newline at end of file diff --git a/example/demo_images/a5ccc7e35a75f830718756ef813211d5.png b/example/demo_images/a5ccc7e35a75f830718756ef813211d5.png new file mode 100644 index 0000000..709db89 Binary files /dev/null and b/example/demo_images/a5ccc7e35a75f830718756ef813211d5.png differ diff --git a/example/demo_images/a5ccc7e35a75f830718756ef813211d5.txt b/example/demo_images/a5ccc7e35a75f830718756ef813211d5.txt new file mode 100644 index 0000000..a2b0596 --- /dev/null +++ b/example/demo_images/a5ccc7e35a75f830718756ef813211d5.txt @@ -0,0 +1 @@ +A single gray gear icon centered. The gear has a circular inner section and evenly spaced teeth around its perimeter.', 'The gear undergoes a transformation where its lines become bolder and turn black, emphasizing the mechanical detail. Simultaneously, two human-like figures, each represented by an outline with simplistic features such as hair and shoulders, appear below the gear. These figures are also outlined in black.', "In the final frame, the gear remains prominently displayed above the two human figures, both of which are positioned symmetrically on either side of the gear's vertical axis, creating a balanced composition that symbolizes human interaction with technology. \ No newline at end of file diff --git a/example/demo_images/b31684d8f7d459a273373c6acc63b0e7.png b/example/demo_images/b31684d8f7d459a273373c6acc63b0e7.png new file mode 100644 index 0000000..51973c8 Binary files /dev/null and b/example/demo_images/b31684d8f7d459a273373c6acc63b0e7.png differ diff --git a/example/demo_images/b31684d8f7d459a273373c6acc63b0e7.txt b/example/demo_images/b31684d8f7d459a273373c6acc63b0e7.txt new file mode 100644 index 0000000..701ed20 --- /dev/null +++ b/example/demo_images/b31684d8f7d459a273373c6acc63b0e7.txt @@ -0,0 +1 @@ +A dynamic animation of a blue rectangular sign with white text reading '41%' centered on it. Initially, the sign appears slightly blurred with two small rounded tabs at the top corners. As the video progresses, the sign becomes sharper, the tabs disappear, and the overall design stabilizes into a cleaner, solid blue rectangle with crisp edges and clear white text. The background remains consistently white throughout the sequence, emphasizing the transformation of the sign's appearance. \ No newline at end of file diff --git a/example/demo_images/b6f855fc8e53aa402410222bd3fc4cd3.png b/example/demo_images/b6f855fc8e53aa402410222bd3fc4cd3.png new file mode 100644 index 0000000..6aac9b1 Binary files /dev/null and b/example/demo_images/b6f855fc8e53aa402410222bd3fc4cd3.png differ diff --git a/example/demo_images/b6f855fc8e53aa402410222bd3fc4cd3.txt b/example/demo_images/b6f855fc8e53aa402410222bd3fc4cd3.txt new file mode 100644 index 0000000..9e91576 --- /dev/null +++ b/example/demo_images/b6f855fc8e53aa402410222bd3fc4cd3.txt @@ -0,0 +1 @@ +A small black dot at the center of the frame. The dot gradually expands into a thick, light gray ring with a darker gray section, forming an incomplete circular shape. This ring continues to expand and transform into a fully filled dark gray circle with a thin black border around it. Finally, the dark gray circle fills with black and reveals a white 'X' shape in the center. \ No newline at end of file diff --git a/example/demo_images/c8c58d6389305d78dba647da3778185b.png b/example/demo_images/c8c58d6389305d78dba647da3778185b.png new file mode 100644 index 0000000..b10771d Binary files /dev/null and b/example/demo_images/c8c58d6389305d78dba647da3778185b.png differ diff --git a/example/demo_images/c8c58d6389305d78dba647da3778185b.txt b/example/demo_images/c8c58d6389305d78dba647da3778185b.txt new file mode 100644 index 0000000..4ab9a1a --- /dev/null +++ b/example/demo_images/c8c58d6389305d78dba647da3778185b.txt @@ -0,0 +1 @@ +A sequence of three frames featuring an animated emoji face with a surprised expression. The face has a bright yellow circular shape, an orange hair outline at the top, black eyebrows raised upwards, and black eyes depicted as vertical lines. The mouth is an open black oval, emphasizing surprise. Throughout the sequence, the face remains centered, with subtle changes in the eye and eyebrow positions suggesting movement and animation. The overall design is minimalist and cartoonish, maintaining consistent proportions and colors across frames. \ No newline at end of file diff --git a/example/demo_images/d8af71f1f117b7a02c7b0b1848eaab77.png b/example/demo_images/d8af71f1f117b7a02c7b0b1848eaab77.png new file mode 100644 index 0000000..0cc05a6 Binary files /dev/null and b/example/demo_images/d8af71f1f117b7a02c7b0b1848eaab77.png differ diff --git a/example/demo_images/d8af71f1f117b7a02c7b0b1848eaab77.txt b/example/demo_images/d8af71f1f117b7a02c7b0b1848eaab77.txt new file mode 100644 index 0000000..81a029a --- /dev/null +++ b/example/demo_images/d8af71f1f117b7a02c7b0b1848eaab77.txt @@ -0,0 +1 @@ +A black rectangular button with white text reading 'PAY.' The button initially appears prominently in the center, casting a subtle shadow. Below the button, there is a symbol resembling radio waves or signal lines, depicted in black. The button undergoes a transformation sequence where it rotates slightly and shrinks while maintaining its position until it completely disappears, leaving only the radio wave symbol briefly visible before also vanishing. The background remains consistently white throughout the video, emphasizing the simplicity and focus on the button and symbol. \ No newline at end of file diff --git a/example/demo_images/db211717efdbc1a59b9833f44115f138.png b/example/demo_images/db211717efdbc1a59b9833f44115f138.png new file mode 100644 index 0000000..ff2b084 Binary files /dev/null and b/example/demo_images/db211717efdbc1a59b9833f44115f138.png differ diff --git a/example/demo_images/db211717efdbc1a59b9833f44115f138.txt b/example/demo_images/db211717efdbc1a59b9833f44115f138.txt new file mode 100644 index 0000000..8130c4a --- /dev/null +++ b/example/demo_images/db211717efdbc1a59b9833f44115f138.txt @@ -0,0 +1 @@ +A black-bordered calendar icon initially appears as an empty white square, then transforms into a calendar view labeled 'August' with multiple horizontal black lines inside the frame. The calendar lines disappear, leaving only the word 'August' and a large central number '08', suggesting the date August 8th. The elements transition smoothly with gradual fades and appearances, maintaining a minimalist black-and-white design throughout the sequence. \ No newline at end of file diff --git a/example/demo_images/e7468481409258592827a92ad5d1f18f.png b/example/demo_images/e7468481409258592827a92ad5d1f18f.png new file mode 100644 index 0000000..97f14c9 Binary files /dev/null and b/example/demo_images/e7468481409258592827a92ad5d1f18f.png differ diff --git a/example/demo_images/e7468481409258592827a92ad5d1f18f.txt b/example/demo_images/e7468481409258592827a92ad5d1f18f.txt new file mode 100644 index 0000000..1ce1d70 --- /dev/null +++ b/example/demo_images/e7468481409258592827a92ad5d1f18f.txt @@ -0,0 +1 @@ +A central blue folder icon with a white paper symbol inside it. The folder has a gradient effect, transitioning from darker blue at the top to lighter blue at the bottom. Initially, the folder appears static, but as the video progresses, two small sparkling stars appear sequentially—one on the left side and another on the right side—before disappearing. These stars add a subtle animation effect, enhancing the visual dynamics of the scene. \ No newline at end of file diff --git a/example/demo_images/edfe9166023fcc3594443a3f75626886.png b/example/demo_images/edfe9166023fcc3594443a3f75626886.png new file mode 100644 index 0000000..675cb3d Binary files /dev/null and b/example/demo_images/edfe9166023fcc3594443a3f75626886.png differ diff --git a/example/demo_images/edfe9166023fcc3594443a3f75626886.txt b/example/demo_images/edfe9166023fcc3594443a3f75626886.txt new file mode 100644 index 0000000..2185b60 --- /dev/null +++ b/example/demo_images/edfe9166023fcc3594443a3f75626886.txt @@ -0,0 +1 @@ +A circular progress indicator with a central blue area and an outer gray ring. The red segment representing progress gradually increases in size clockwise, accompanied by percentage labels (5%, 16%, 26%, 35%, 65%) that appear and update dynamically within the blue area. The animation involves a smooth fill effect for the red segment and text updates, highlighting the increasing progress over time. \ No newline at end of file diff --git a/example/demo_video/02b8ce2014690a9e30dc25da846e8afb.mp4 b/example/demo_video/02b8ce2014690a9e30dc25da846e8afb.mp4 new file mode 100644 index 0000000..4729fab Binary files /dev/null and b/example/demo_video/02b8ce2014690a9e30dc25da846e8afb.mp4 differ diff --git a/example/demo_video/0dd62535b46d6f8ecf42f22dc0e148cd.mp4 b/example/demo_video/0dd62535b46d6f8ecf42f22dc0e148cd.mp4 new file mode 100644 index 0000000..648e28d Binary files /dev/null and b/example/demo_video/0dd62535b46d6f8ecf42f22dc0e148cd.mp4 differ diff --git a/example/demo_video/1891eeab41e148d29e0b7fe3d62c4f3c.mp4 b/example/demo_video/1891eeab41e148d29e0b7fe3d62c4f3c.mp4 new file mode 100644 index 0000000..08813cd Binary files /dev/null and b/example/demo_video/1891eeab41e148d29e0b7fe3d62c4f3c.mp4 differ diff --git a/example/demo_video/25bbca9c361c563942d50c13b69a18d8.mp4 b/example/demo_video/25bbca9c361c563942d50c13b69a18d8.mp4 new file mode 100644 index 0000000..81e0287 Binary files /dev/null and b/example/demo_video/25bbca9c361c563942d50c13b69a18d8.mp4 differ diff --git a/example/demo_video/2df42378085ec6b493b87355d1e03bdd.mp4 b/example/demo_video/2df42378085ec6b493b87355d1e03bdd.mp4 new file mode 100644 index 0000000..8f4ffdc Binary files /dev/null and b/example/demo_video/2df42378085ec6b493b87355d1e03bdd.mp4 differ diff --git a/example/demo_video/2f16aaebc111ee480931b638c06fc1e4.mp4 b/example/demo_video/2f16aaebc111ee480931b638c06fc1e4.mp4 new file mode 100644 index 0000000..f6b5fa0 Binary files /dev/null and b/example/demo_video/2f16aaebc111ee480931b638c06fc1e4.mp4 differ diff --git a/example/demo_video/3f356993f524f1ba9c05369c8ac95d85.mp4 b/example/demo_video/3f356993f524f1ba9c05369c8ac95d85.mp4 new file mode 100644 index 0000000..8c97dea Binary files /dev/null and b/example/demo_video/3f356993f524f1ba9c05369c8ac95d85.mp4 differ diff --git a/example/demo_video/40d849d9ff242d2b61dbdb25b96e2a79.mp4 b/example/demo_video/40d849d9ff242d2b61dbdb25b96e2a79.mp4 new file mode 100644 index 0000000..d25d108 Binary files /dev/null and b/example/demo_video/40d849d9ff242d2b61dbdb25b96e2a79.mp4 differ diff --git a/example/demo_video/44d6dc9d1d5b4a2987f3c47b9002f2b7.mp4 b/example/demo_video/44d6dc9d1d5b4a2987f3c47b9002f2b7.mp4 new file mode 100644 index 0000000..ac8f562 Binary files /dev/null and b/example/demo_video/44d6dc9d1d5b4a2987f3c47b9002f2b7.mp4 differ diff --git a/example/demo_video/46f6839bd192c155c41ac13b86a29bb4.mp4 b/example/demo_video/46f6839bd192c155c41ac13b86a29bb4.mp4 new file mode 100644 index 0000000..4615a8e Binary files /dev/null and b/example/demo_video/46f6839bd192c155c41ac13b86a29bb4.mp4 differ diff --git a/example/demo_video/510e8965336b5ade1cab71b9e42d4dfa.mp4 b/example/demo_video/510e8965336b5ade1cab71b9e42d4dfa.mp4 new file mode 100644 index 0000000..f6b33cd Binary files /dev/null and b/example/demo_video/510e8965336b5ade1cab71b9e42d4dfa.mp4 differ diff --git a/example/demo_video/54671593a4dff76d9fb3d90e080b95c1.mp4 b/example/demo_video/54671593a4dff76d9fb3d90e080b95c1.mp4 new file mode 100644 index 0000000..4c34981 Binary files /dev/null and b/example/demo_video/54671593a4dff76d9fb3d90e080b95c1.mp4 differ diff --git a/example/demo_video/56d6e128398e891a8fb647d327efac3b.mp4 b/example/demo_video/56d6e128398e891a8fb647d327efac3b.mp4 new file mode 100644 index 0000000..87a381e Binary files /dev/null and b/example/demo_video/56d6e128398e891a8fb647d327efac3b.mp4 differ diff --git a/example/demo_video/5b417b21193a483b22b0dffcc4446e2d.mp4 b/example/demo_video/5b417b21193a483b22b0dffcc4446e2d.mp4 new file mode 100644 index 0000000..ecc5279 Binary files /dev/null and b/example/demo_video/5b417b21193a483b22b0dffcc4446e2d.mp4 differ diff --git a/example/demo_video/68213d3deee667b07fad9b3b3eef8c58.mp4 b/example/demo_video/68213d3deee667b07fad9b3b3eef8c58.mp4 new file mode 100644 index 0000000..fc5958e Binary files /dev/null and b/example/demo_video/68213d3deee667b07fad9b3b3eef8c58.mp4 differ diff --git a/example/demo_video/6c52fb38cdd3329d908a4699d0120992.mp4 b/example/demo_video/6c52fb38cdd3329d908a4699d0120992.mp4 new file mode 100644 index 0000000..a74cda8 Binary files /dev/null and b/example/demo_video/6c52fb38cdd3329d908a4699d0120992.mp4 differ diff --git a/example/demo_video/6dfce19cdfe46b0c8f4b693f434b2548.mp4 b/example/demo_video/6dfce19cdfe46b0c8f4b693f434b2548.mp4 new file mode 100644 index 0000000..e791a35 Binary files /dev/null and b/example/demo_video/6dfce19cdfe46b0c8f4b693f434b2548.mp4 differ diff --git a/example/demo_video/7a9feef98672d92e56c51baf5e6f13c6.mp4 b/example/demo_video/7a9feef98672d92e56c51baf5e6f13c6.mp4 new file mode 100644 index 0000000..e9e3823 Binary files /dev/null and b/example/demo_video/7a9feef98672d92e56c51baf5e6f13c6.mp4 differ diff --git a/example/demo_video/7cab5d083e8b51de8672f9b6a0301e3a.mp4 b/example/demo_video/7cab5d083e8b51de8672f9b6a0301e3a.mp4 new file mode 100644 index 0000000..ad6ae0b Binary files /dev/null and b/example/demo_video/7cab5d083e8b51de8672f9b6a0301e3a.mp4 differ diff --git a/example/demo_video/7e676a2f45d68610483c05c9213fddbc.mp4 b/example/demo_video/7e676a2f45d68610483c05c9213fddbc.mp4 new file mode 100644 index 0000000..94a71a4 Binary files /dev/null and b/example/demo_video/7e676a2f45d68610483c05c9213fddbc.mp4 differ diff --git a/example/demo_video/831cc8166268830336f42a40f3f35c03.mp4 b/example/demo_video/831cc8166268830336f42a40f3f35c03.mp4 new file mode 100644 index 0000000..0909fbd Binary files /dev/null and b/example/demo_video/831cc8166268830336f42a40f3f35c03.mp4 differ diff --git a/example/demo_video/9de72d1d50c657e9e8fa60eba5031185.mp4 b/example/demo_video/9de72d1d50c657e9e8fa60eba5031185.mp4 new file mode 100644 index 0000000..4f689f0 Binary files /dev/null and b/example/demo_video/9de72d1d50c657e9e8fa60eba5031185.mp4 differ diff --git a/example/demo_video/a8803c80e53d5f624acb207dd5ec400a.mp4 b/example/demo_video/a8803c80e53d5f624acb207dd5ec400a.mp4 new file mode 100644 index 0000000..1aaaa41 Binary files /dev/null and b/example/demo_video/a8803c80e53d5f624acb207dd5ec400a.mp4 differ diff --git a/example/demo_video/ac69f4ab3a5fa5309815b475df325198.mp4 b/example/demo_video/ac69f4ab3a5fa5309815b475df325198.mp4 new file mode 100644 index 0000000..0afe1da Binary files /dev/null and b/example/demo_video/ac69f4ab3a5fa5309815b475df325198.mp4 differ diff --git a/example/demo_video/ac872589b3e89b8b976a1cd857dfc03d.mp4 b/example/demo_video/ac872589b3e89b8b976a1cd857dfc03d.mp4 new file mode 100644 index 0000000..b904429 Binary files /dev/null and b/example/demo_video/ac872589b3e89b8b976a1cd857dfc03d.mp4 differ diff --git a/example/demo_video/ccfa2a4ce8dc9dcc37e0381333e5e50b.mp4 b/example/demo_video/ccfa2a4ce8dc9dcc37e0381333e5e50b.mp4 new file mode 100644 index 0000000..47d0ae7 Binary files /dev/null and b/example/demo_video/ccfa2a4ce8dc9dcc37e0381333e5e50b.mp4 differ diff --git a/example/demo_video/d7090be17fcc4ba0b6357f986c2b57af.mp4 b/example/demo_video/d7090be17fcc4ba0b6357f986c2b57af.mp4 new file mode 100644 index 0000000..fa3e42f Binary files /dev/null and b/example/demo_video/d7090be17fcc4ba0b6357f986c2b57af.mp4 differ diff --git a/example/demo_video/de5ddcf87c18b4fe27aa520e165e03b7.mp4 b/example/demo_video/de5ddcf87c18b4fe27aa520e165e03b7.mp4 new file mode 100644 index 0000000..d09c5fd Binary files /dev/null and b/example/demo_video/de5ddcf87c18b4fe27aa520e165e03b7.mp4 differ diff --git a/example/demo_video/e4cb37197f63a77c64c4cf8ce7ccd8a3.mp4 b/example/demo_video/e4cb37197f63a77c64c4cf8ce7ccd8a3.mp4 new file mode 100644 index 0000000..ea08454 Binary files /dev/null and b/example/demo_video/e4cb37197f63a77c64c4cf8ce7ccd8a3.mp4 differ diff --git a/example/demo_video/ee2382cd49776fed6cbc704ae457c428.mp4 b/example/demo_video/ee2382cd49776fed6cbc704ae457c428.mp4 new file mode 100644 index 0000000..48ee68f Binary files /dev/null and b/example/demo_video/ee2382cd49776fed6cbc704ae457c428.mp4 differ diff --git a/inference.py b/inference.py new file mode 100644 index 0000000..aae340e --- /dev/null +++ b/inference.py @@ -0,0 +1,1375 @@ +import os +import time +import torch +import argparse +import numpy as np +import re +import json +import datetime +import traceback +import shutil +import random +import pandas as pd +import tempfile +import copy +from PIL import Image +from pathlib import Path +from typing import List, Dict, Set, Optional, Tuple + +from safetensors.torch import load_file +from huggingface_hub import snapshot_download +from datasets import load_dataset, load_from_disk +from decoder import LottieDecoder +from transformers import AutoTokenizer, AutoProcessor +from qwen_vl_utils import process_vision_info +from decord import VideoReader, cpu + +from lottie.objects.lottie_tokenize import LottieTensor +from lottie.objects.lottie_param import ( + from_sequence, ShapeLayer, NullLayer, PreCompLayer, TextLayer, + SolidColorLayer, Font, Chars, + shape_layer_to_json, null_layer_to_json, precomp_layer_to_json, + text_layer_to_json, solid_layer_to_json, font_to_json, char_to_json +) + +from PIL import Image as PILImage + +os.environ["TOKENIZERS_PARALLELISM"] = "false" +torch.backends.cudnn.benchmark = False +torch.backends.cudnn.deterministic = True + +# ========== Constants ========== +TASK_VIDEO = "video" +TASK_IMAGE = "image" +TASK_TEXT = "text" + +SYSTEM_PROMPT = "You are a Lottie animation expert." +VIDEO_PROMPT = "Turn this video into Lottie code." + +# Lottie token IDs +LOTTIE_BOS = 192398 +LOTTIE_EOS = 192399 +PAD_TOKEN = 151643 +COMMAND_OFFSET = 151936 +NUM_COMMANDS = 282 + +def sanitize_filename(text, max_length=180): + text = re.sub(r'[<>:"/\\|?*\n\r\t]', '_', text) + text = re.sub(r'[\s_]+', '_', text) + text = text.strip('_ ') + if len(text) > max_length: + text = text[:max_length] + return text if text else "unnamed" + +def simplify_to_animation_description(text): + if pd.isna(text) or text == '': + return "" + text = str(text) + patterns = [ + r"The video features?", r"The video shows?", r"The image features?", + r"The image shows?", r"This image", r"In this image,?", + ] + for p in patterns: + text = re.sub(p, "", text, flags=re.IGNORECASE) + text = re.sub(r'\s+', ' ', text).strip() + if text: + text = text[0].upper() + text[1:] + return text + +def add_random_background(img): + if img.mode != 'RGBA': + return img.convert('RGB') + light_colors = [ + (255, 255, 255), (245, 245, 245), (250, 250, 250), + (255, 250, 240), (240, 248, 255), + ] + bg_color = random.choice(light_colors) + background = PILImage.new('RGB', img.size, bg_color) + background.paste(img, (0, 0), img) + return background + +def load_frames_from_video(video_path, num_frames=8, target_size=(336, 336)): + + vr = VideoReader(video_path, ctx=cpu(0)) + total_frames = len(vr) + if total_frames < 1: + raise ValueError(f"Video has no frames: {video_path}") + indices = np.linspace(0, total_frames - 1, num_frames).astype(int) + frames_np = vr.get_batch(indices).asnumpy() + + frames = [] + for f in frames_np: + img = PILImage.fromarray(f) + if target_size: + img = img.resize(target_size, PILImage.LANCZOS) + frames.append(img) + + return frames + +def build_video_messages(frames: List[PILImage.Image], fps: float = 8.0): + return [{ + "role": "system", + "content": SYSTEM_PROMPT + }, { + "role": "user", + "content": [ + {"type": "video", "video": frames, "fps": fps}, + {"type": "text", "text": VIDEO_PROMPT} + ] + }] + +def build_image_messages(image, text_description): + return [{ + "role": "system", + "content": SYSTEM_PROMPT + }, { + "role": "user", + "content": [ + {"type": "image", "image": image}, + {"type": "text", "text": f"Animate this image: {text_description}"} + ] + }] + +def build_text_messages(text_description): + + messages = [{ + "role": "system", + "content": SYSTEM_PROMPT + }, { + "role": "user", + "content": [ + {"type": "text", "text": f"Generate Lottie code: {text_description}"} + ] + }] + + return messages + +def prepare_inference_input( + processor, + messages, + device, + text_len: int = 1500, + apply_left_padding: bool = True, + target_context_len: int = 1500): + + text_input = processor.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True + ) + + image_inputs, video_inputs = process_vision_info(messages) + + if video_inputs: + inputs = processor( + text=[text_input], + images=None, + videos=video_inputs, + padding=False, + truncation=False, + max_length=text_len, + return_tensors="pt" + ) + task_type = TASK_VIDEO + elif image_inputs: + inputs = processor( + text=[text_input], + images=image_inputs, + videos=None, + padding=False, + truncation=False, + max_length=text_len, + return_tensors="pt" + ) + task_type = TASK_IMAGE + else: + inputs = processor( + text=[text_input], + images=None, + videos=None, + padding=False, + truncation=False, + max_length=text_len, + return_tensors="pt" + ) + task_type = TASK_TEXT + + input_ids = inputs['input_ids'] + attention_mask = inputs['attention_mask'] + + if apply_left_padding and target_context_len is not None: + current_len = input_ids.shape[1] + if current_len < target_context_len: + pad_len = target_context_len - current_len + pad_ids = torch.full((1, pad_len), PAD_TOKEN, dtype=torch.long) + pad_mask = torch.zeros((1, pad_len), dtype=torch.long) + + input_ids = torch.cat([pad_ids, input_ids], dim=1) + attention_mask = torch.cat([pad_mask, attention_mask], dim=1) + + result = { + 'input_ids': input_ids.to(device), + 'attention_mask': attention_mask.to(device), + 'pixel_values': None, + 'image_grid_thw': None, + 'pixel_values_videos': None, + 'video_grid_thw': None, + 'task_type': task_type, + 'context_len': input_ids.shape[1], + } + + if video_inputs and inputs.get('pixel_values_videos') is not None: + result['pixel_values_videos'] = inputs['pixel_values_videos'].to(device) + if inputs.get('video_grid_thw') is not None: + result['video_grid_thw'] = inputs['video_grid_thw'].to(device) + elif image_inputs and inputs.get('pixel_values') is not None: + result['pixel_values'] = inputs['pixel_values'].to(device) + if inputs.get('image_grid_thw') is not None: + result['image_grid_thw'] = inputs['image_grid_thw'].to(device) + + return result + +def generate_lottie( + model, + inputs: dict, + max_new_tokens: int, + device, + use_sampling: bool = False, + temperature: float = 0.9, + top_p: float = 0.25, + top_k: int = 5, + repetition_penalty: float = 1.01, + num_candidates: int = 1, + verbose: bool = True) -> List[Tuple[List[int], dict]]: + + info = { + 'input_len': inputs['input_ids'].shape[1], + 'task_type': inputs.get('task_type', 'unknown'), + 'generated_len': 0, + 'has_bos': False, + 'has_eos': False, + 'valid_lottie_tokens': 0, + } + + model.transformer.rope_deltas = None + position_ids, _ = model.transformer.get_rope_index( + input_ids=inputs['input_ids'], + attention_mask=inputs['attention_mask'], + image_grid_thw=inputs.get('image_grid_thw'), + video_grid_thw=inputs.get('video_grid_thw')) + position_ids = position_ids * inputs['attention_mask'][None, ] + + generate_kwargs = { + 'input_ids': inputs['input_ids'], + 'attention_mask': inputs['attention_mask'], + 'position_ids': position_ids, + 'max_new_tokens': max_new_tokens, + 'min_new_tokens': 20, + 'num_return_sequences': num_candidates, + 'eos_token_id': LOTTIE_EOS, + 'pad_token_id': PAD_TOKEN, + 'use_cache': True, + 'return_dict_in_generate': True, + } + + if inputs.get('pixel_values') is not None: + generate_kwargs['pixel_values'] = inputs['pixel_values'] + if inputs.get('image_grid_thw') is not None: + generate_kwargs['image_grid_thw'] = inputs['image_grid_thw'] + if inputs.get('pixel_values_videos') is not None: + generate_kwargs['pixel_values_videos'] = inputs['pixel_values_videos'] + if inputs.get('video_grid_thw') is not None: + generate_kwargs['video_grid_thw'] = inputs['video_grid_thw'] + + if repetition_penalty > 1.0: + generate_kwargs['repetition_penalty'] = repetition_penalty + + if use_sampling: + generate_kwargs.update({ + 'do_sample': True, + 'temperature': temperature, + 'top_p': top_p, + 'top_k': top_k, + }) + if verbose: + print(f" Using sampling: temp={temperature}, top_p={top_p}, top_k={top_k}") + else: + generate_kwargs.update({ + 'do_sample': True, + 'num_beams': 1, + }) + if verbose: + print(" Using greedy decoding") + + if verbose: + print(f" Input length: {info['input_len']}") + print(f" Max new tokens: {max_new_tokens}") + print(f" Repetition penalty: {repetition_penalty}") + + with torch.no_grad(): + outputs = model.transformer.generate(**generate_kwargs) + + candidates_results = [] + + if hasattr(outputs, 'sequences'): + sequences = outputs.sequences + else: + sequences = outputs + + input_len = inputs['input_ids'].shape[1] + + for candidate_idx in range(num_candidates): + generated_sequence = sequences[candidate_idx] + generated_ids = generated_sequence[input_len:].tolist() + + cand_info = { + 'candidate_idx': candidate_idx, + 'input_len': input_len, + 'task_type': inputs.get('task_type', 'unknown'), + 'generated_len': len(generated_ids), + 'has_bos': LOTTIE_BOS in generated_ids, + 'has_eos': LOTTIE_EOS in generated_ids, + 'valid_lottie_tokens': sum(1 for t in generated_ids if t >= COMMAND_OFFSET), + 'raw_tokens': generated_ids.copy(), + } + + clean_ids = clean_generated_tokens(generated_ids) + cand_info['clean_len'] = len(clean_ids) + + candidates_results.append((clean_ids, cand_info)) + + if verbose and num_candidates > 1: + print(f" Candidate {candidate_idx}: {cand_info['generated_len']} tokens, " + f"BOS={cand_info['has_bos']}, EOS={cand_info['has_eos']}") + + if verbose and num_candidates == 1: + info = candidates_results[0][1] + print(f" Generated {info['generated_len']} tokens") + print(f" Has BOS: {info['has_bos']}, Has EOS: {info['has_eos']}") + print(f" Valid Lottie tokens: {info['valid_lottie_tokens']}") + if info['raw_tokens']: + print(f" First 30 tokens: {info['raw_tokens'][:30]}") + + + return candidates_results + +def clean_generated_tokens(generated_ids: List[int]) -> List[int]: + if not generated_ids: + return [] + + if generated_ids[0] == LOTTIE_BOS: + generated_ids = generated_ids[1:] + + if LOTTIE_EOS in generated_ids: + eos_idx = generated_ids.index(LOTTIE_EOS) + generated_ids = generated_ids[:eos_idx] + + generated_ids = [t for t in generated_ids if t != PAD_TOKEN] + + return generated_ids + + +def fix_lottie_json(anim): + + anim_ip = int(round(anim.get("ip", 0))) + anim_op = int(round(anim.get("op", 16))) + anim["ip"] = anim_ip + anim["op"] = anim_op + anim["fr"] = int(round(anim.get("fr", 8))) + anim["ddd"] = int(anim.get("ddd", 0)) + + def fix_t_recursive(obj): + if isinstance(obj, dict): + if obj.get("a") == 1 and isinstance(obj.get("k"), list): + for kf in obj["k"]: + if isinstance(kf, dict) and "t" in kf: + kf["t"] = int(round(kf["t"])) + for v in obj.values(): + fix_t_recursive(v) + elif isinstance(obj, list): + for item in obj: + fix_t_recursive(item) + + fix_t_recursive(anim) + + max_x = float(anim.get("w", 512)) + max_y = float(anim.get("h", 512)) + + def collect_pos(layer): + nonlocal max_x, max_y + p = layer.get("ks", {}).get("p", {}) + if isinstance(p, dict): + if p.get("a", 0) == 0: + pv = p.get("k", [0, 0]) + if isinstance(pv, list) and len(pv) >= 2: + max_x = max(max_x, float(pv[0])) + max_y = max(max_y, float(pv[1])) + else: + for kf in p.get("k", []): + if isinstance(kf, dict): + for sv in (kf.get("s", []), kf.get("e", [])): + if isinstance(sv, list) and len(sv) >= 2: + max_x = max(max_x, float(sv[0])) + max_y = max(max_y, float(sv[1])) + for sub in layer.get("layers", []): + collect_pos(sub) + + for layer in anim.get("layers", []): + collect_pos(layer) + + anim["w"] = max(512, int((max_x * 1.1 + 15) // 16 * 16)) + anim["h"] = max(512, int((max_y * 1.1 + 15) // 16 * 16)) + + valid_inds = set() + for layer in anim.get("layers", []): + if "ind" in layer: + valid_inds.add(int(layer["ind"])) + + def clean_shapes(shapes): + + if not isinstance(shapes, list): + return shapes + cleaned = [] + for sh in shapes: + if not isinstance(sh, dict): + continue + if sh.get("ty") == "gr": + sh["it"] = clean_shapes(sh.get("it", [])) + if not sh["it"]: + continue + has_tr = any(item.get("ty") == "tr" for item in sh["it"] if isinstance(item, dict)) + if not has_tr: + sh["it"].append({ + "ty": "tr", "nm": "", + "a": {"a": 0, "k": [0, 0], "ix": 1}, + "p": {"a": 0, "k": [0, 0], "ix": 2}, + "s": {"a": 0, "k": [100, 100], "ix": 3}, + "r": {"a": 0, "k": 0, "ix": 6}, + "o": {"a": 0, "k": 100, "ix": 7}, + "sk": {"a": 0, "k": 0, "ix": 4}, + "sa": {"a": 0, "k": 0, "ix": 5}, + "hd": False + }) + cleaned.append(sh) + return cleaned + + def fix_layer(layer): + ip = int(round(layer.get("ip", anim_ip))) + op = int(round(layer.get("op", anim_op))) + layer["ip"] = max(anim_ip, ip) + layer["op"] = min(anim_op, max(layer["ip"] + 1, op)) + layer["st"] = int(round(layer.get("st", anim_ip))) + if "ind" in layer: + layer["ind"] = int(layer["ind"]) + if "parent" in layer: + p = int(layer["parent"]) + if p in valid_inds: + layer["parent"] = p + else: + del layer["parent"] + layer.pop("ct", None) + if "shapes" in layer: + layer["shapes"] = clean_shapes(layer["shapes"]) + for sub in layer.get("layers", []): + fix_layer(sub) + return layer + + fixed_layers = [] + for l in anim.get("layers", []): + fix_layer(l) + shapes = l.get("shapes", []) + if l.get("ty") == 4 and not shapes: + continue + fixed_layers.append(l) + anim["layers"] = fixed_layers + + for asset in anim.get("assets", []): + if "layers" in asset: + fixed = [] + for l in asset["layers"]: + fix_layer(l) + if l.get("ty") == 4 and not l.get("shapes"): + continue + fixed.append(l) + asset["layers"] = fixed + + return anim + +def check_lottie_validity(json_animation): + issues = [] + + def check_layers(layers, prefix=""): + nonlocal issues + if not layers: + return False + has_visible = False + + for layer in layers: + ty = layer.get("ty") + if ty == 4 and layer.get("shapes"): + has_visible = True + elif ty == 1: + has_visible = True + elif ty == 0: + has_visible = True + + return has_visible + + has_main = check_layers(json_animation.get("layers", []), "Main: ") + + for asset in json_animation.get("assets", []): + if "layers" in asset: + check_layers(asset["layers"], f"Asset {asset.get('id', '?')}: ") + + return has_main and len(issues) == 0, issues + +def tokens_to_lottie_json(generated_ids: List[int], default_json: dict = None, verbose: bool = True): + if default_json is None: + default_json = { + "v": "5.5.2", "fr": 8, "ip": 0, "op": 16, + "w": 512, "h": 512, "nm": "Animation", "ddd": 0 + } + + if verbose: + print(f" Converting {len(generated_ids)} tokens to Lottie JSON...") + + reconstructed_tensor = LottieTensor.from_list(generated_ids) + reconstructed_sequence = reconstructed_tensor.to_sequence() + reconstructed = from_sequence(reconstructed_sequence) + + json_animation = { + "v": reconstructed.get("v", default_json.get("v", "5.5.2")), + "fr": reconstructed.get("fr", default_json.get("fr", 8)), + "ip": reconstructed.get("ip", default_json.get("ip", 0)), + "op": reconstructed.get("op", default_json.get("op", 16)), + "w": reconstructed.get("w", default_json.get("w", 512)), + "h": reconstructed.get("h", default_json.get("h", 512)), + "nm": reconstructed.get("nm", default_json.get("nm", "Animation")), + "ddd": reconstructed.get("ddd", default_json.get("ddd", 0)), + "assets": [], + "layers": [], + } + + if "markers" in reconstructed: + json_animation["markers"] = reconstructed.get("markers", []) + if "props" in reconstructed: + json_animation["props"] = reconstructed.get("props", {}) + + if "fonts" in reconstructed and reconstructed["fonts"]: + fonts_data = reconstructed["fonts"] + if isinstance(fonts_data, dict) and "list" in fonts_data: + fonts_json = {"list": []} + for font in fonts_data["list"]: + if isinstance(font, Font): + fonts_json["list"].append(font_to_json(font)) + else: + fonts_json["list"].append(font) + json_animation["fonts"] = fonts_json + + if "chars" in reconstructed and reconstructed["chars"]: + chars_json = [] + for char in reconstructed["chars"]: + if isinstance(char, Chars): + chars_json.append(char_to_json(char)) + else: + chars_json.append(char) + json_animation["chars"] = chars_json + + for asset in reconstructed.get("assets", []): + asset_json = dict(asset) + if "layers" in asset: + asset_json["layers"] = [] + for layer in asset["layers"]: + if isinstance(layer, ShapeLayer): + asset_json["layers"].append(shape_layer_to_json(layer)) + elif isinstance(layer, NullLayer): + asset_json["layers"].append(null_layer_to_json(layer)) + elif isinstance(layer, PreCompLayer): + asset_json["layers"].append(precomp_layer_to_json(layer)) + elif isinstance(layer, TextLayer): + asset_json["layers"].append(text_layer_to_json(layer)) + elif isinstance(layer, SolidColorLayer): + asset_json["layers"].append(solid_layer_to_json(layer)) + else: + asset_json["layers"].append(layer) + json_animation["assets"].append(asset_json) + + # 处理layers + for layer in reconstructed.get("layers", []): + if isinstance(layer, ShapeLayer): + json_animation["layers"].append(shape_layer_to_json(layer)) + elif isinstance(layer, NullLayer): + json_animation["layers"].append(null_layer_to_json(layer)) + elif isinstance(layer, PreCompLayer): + json_animation["layers"].append(precomp_layer_to_json(layer)) + elif isinstance(layer, TextLayer): + json_animation["layers"].append(text_layer_to_json(layer)) + elif isinstance(layer, SolidColorLayer): + json_animation["layers"].append(solid_layer_to_json(layer)) + else: + json_animation["layers"].append(layer) + + json_animation = fix_lottie_json(json_animation) + + return json_animation + +def run_inference( + model, + processor, + task_type: str, + device, + cfg: dict, + uid: str = None, + video_path: str = None, + image_path: str = None, + text_description: str = None, + use_sampling: bool = False, + temperature: float = 0.9, + top_p: float = 0.25, + top_k: int = 5, + repetition_penalty: float = 1.01, + output_path: str = None, + verbose: bool = True) -> Tuple[dict, dict]: + + + prompt_info = None # + if task_type == TASK_VIDEO: + if not video_path: + raise ValueError("video_path required for video task") + if not os.path.exists(video_path): + raise FileNotFoundError(f"Video path does not exist: {video_path}") + video_path = os.path.abspath(video_path) + frames = load_frames_from_video(video_path, num_frames=8) + messages = build_video_messages(frames, fps=8.0) + + elif task_type == TASK_IMAGE: + if not image_path: + raise ValueError("image_path required for image task") + img = PILImage.open(image_path) + img = add_random_background(img) if img.mode == 'RGBA' else img.convert('RGB') + img = img.resize((448, 448), PILImage.LANCZOS) + desc = text_description or "A simple animation" + messages = build_image_messages(img, desc) + elif task_type == TASK_TEXT: + desc = text_description or "A simple animation" + messages = build_text_messages(desc) + if len(messages) > 2 and "prompt_info" in messages[-1]: + prompt_info = messages[-1]["prompt_info"] + messages = messages[:-1] + else: + raise ValueError(f"Unknown task type: {task_type}") + + inputs = prepare_inference_input( + processor=processor, + messages=messages, + device=device, + text_len=cfg.get('text_len', 1500), + apply_left_padding=True, + target_context_len=1500) + + if verbose: + print(f"\nTask: {task_type}") + print(f"Context length: {inputs['context_len']}") + + num_candidates = cfg.get('num_candidates', 1) + candidates_list = generate_lottie( + model=model, + inputs=inputs, + max_new_tokens=cfg.get('pix_len', 4096), + device=device, + use_sampling=use_sampling, + temperature=temperature, + top_p=top_p, + top_k=top_k, + repetition_penalty=repetition_penalty, + num_candidates=num_candidates, + verbose=verbose) + + processed_candidates = [] + + for token_ids, gen_info in candidates_list: + + if len(token_ids) < 10: + if verbose and num_candidates > 1: + print(f" Candidate {gen_info['candidate_idx']}: Too short ({len(token_ids)} tokens), skipping") + continue + + try: + lottie_json = tokens_to_lottie_json( + token_ids, + verbose=False) + + is_valid, issues = check_lottie_validity(lottie_json) + gen_info['is_valid'] = is_valid + gen_info['issues'] = issues + + processed_candidates.append(( + lottie_json, + token_ids, + gen_info['has_eos'], + gen_info['candidate_idx'], + gen_info + )) + + except Exception as e: + if verbose and num_candidates > 1: + print(f" Candidate {gen_info['candidate_idx']}: Conversion failed: {e}") + continue + + if len(processed_candidates) == 0: + if verbose: + print(f" ERROR: All {num_candidates} candidates failed") + return None, candidates_list[0][1] if candidates_list else {} + + if len(processed_candidates) == 1: + best_idx = 0 + best_score = None + best_details = None + if verbose: + print(f" Only 1 valid candidate, using it") + else: + candidates_for_scoring = [ + (lottie_json, token_ids, has_eos, cand_idx) + for lottie_json, token_ids, has_eos, cand_idx, _ in processed_candidates + ] + + lottie_json, generated_ids, has_eos, selected_cand_idx, gen_info = processed_candidates[best_idx] + + if verbose and num_candidates > 1: + print(f" āœ… Selected candidate {selected_cand_idx} (score: {best_score})") + + if num_candidates > 1: + gen_info['num_candidates'] = num_candidates + gen_info['selected_candidate'] = selected_cand_idx + gen_info['best_score'] = best_score + gen_info['best_details'] = best_details + + if not gen_info.get('is_valid') and verbose: + print(f" WARNING: Lottie may be invalid: {gen_info.get('issues', [])}") + + if output_path: + with open(output_path, 'w') as f: + json.dump(lottie_json, f, indent=2) + if verbose: + print(f" Saved to: {output_path}") + + # Save all candidates when num_candidates > 1 + if num_candidates > 1 and len(processed_candidates) > 1: + base_path = output_path.replace('.json', '') + for idx, (cand_lottie, cand_tokens, cand_has_eos, cand_idx, cand_info) in enumerate(processed_candidates): + cand_path = f"{base_path}_candidate_{cand_idx}.json" + with open(cand_path, 'w') as f: + json.dump(cand_lottie, f, indent=2) + if verbose: + print(f" Saved candidate {cand_idx} to: {cand_path}") + + info_path = output_path.replace('.json', '_info.txt') + with open(info_path, 'w') as f: + f.write(f"=== Generation Info ===\n") + f.write(f"UID: {uid}\n") + f.write(f"Task: {task_type}\n") + f.write(f"Timestamp: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") + + if task_type == TASK_TEXT and prompt_info: + f.write(f"Original prompt ({prompt_info['original_word_count']} words):\n") + f.write(f" {prompt_info['original']}\n\n") + f.write(f"Prompt was NOT enhanced (already detailed enough)\n\n") + + f.write(f"=== Sampling Parameters ===\n") + f.write(f"Use sampling: {use_sampling}\n") + if use_sampling: + f.write(f"Temperature: {temperature}\n") + f.write(f"Top-p: {top_p}\n") + f.write(f"Top-k: {top_k}\n") + f.write(f"Repetition penalty: {repetition_penalty}\n") + f.write(f"Max new tokens: {cfg.get('pix_len', 4096)}\n\n") + + f.write(f"=== Generation Results ===\n") + f.write(f"Generated tokens: {len(generated_ids)}\n") + f.write(f"Valid Lottie: {gen_info.get('is_valid', 'unknown')}\n") + f.write(f"Has BOS: {gen_info.get('has_bos', False)}\n") + f.write(f"Has EOS: {gen_info.get('has_eos', False)}\n") + f.write(f"Valid Lottie tokens: {gen_info.get('valid_lottie_tokens', 0)}\n\n") + + if gen_info.get('num_candidates', 1) > 1: + f.write(f"=== Candidate Selection ===\n") + f.write(f"Total candidates generated: {gen_info['num_candidates']}\n") + f.write(f"Valid candidates: {len(processed_candidates)}\n") + f.write(f"Selected candidate: {gen_info['selected_candidate']}\n") + if gen_info.get('best_score') is not None: + f.write(f"Quality score: {gen_info['best_score']}\n") + if gen_info.get('best_details'): + f.write(f"Quality details:\n") + for key, value in gen_info['best_details'].items(): + f.write(f" {key}: {value}\n") + f.write(f"\n") + + return lottie_json, gen_info + + +def run_batch_text_file_inference(args, cfg): + """ + Generate Lottie from text file. + """ + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") + + if not os.path.exists(args.batch_text_file): + raise FileNotFoundError(f"Batch text file not found: {args.batch_text_file}") + + print("Loading model...") + processor = AutoProcessor.from_pretrained(cfg['tokenizer_name'], padding_side="left") + processor.tokenizer.padding_side = "left" + + model = LottieDecoder(pix_len=cfg['pix_len'], text_len=cfg['text_len']) + + + if os.path.isfile(args.sketch_weight) and args.sketch_weight.endswith('.bin'): + model_path = args.sketch_weight + safetensors_path = args.sketch_weight.replace('.bin', '.safetensors') + else: + model_path = os.path.join(args.sketch_weight, 'pytorch_model.bin') + safetensors_path = os.path.join(args.sketch_weight, 'model.safetensors') + + if os.path.exists(model_path): + model.load_state_dict(torch.load(model_path, map_location='cpu')) + print(f"Loaded from {model_path}") + elif os.path.exists(safetensors_path): + model.load_state_dict(load_file(safetensors_path)) + print(f"Loaded from {safetensors_path}") + else: + raise FileNotFoundError(f"Model not found in {args.sketch_weight}") + + model = model.to(device).eval() + + + print(f"\nReading prompts from: {args.batch_text_file}") + with open(args.batch_text_file, 'r', encoding='utf-8') as f: + prompts = [line.strip() for line in f if line.strip()] + + print(f"Total prompts: {len(prompts)}") + + output_dir = os.path.join(args.output_dir, 'batch_text2lottie') + os.makedirs(output_dir, exist_ok=True) + print(f"Output directory: {output_dir}") + + stats = {'success': 0, 'fail': 0, 'total': len(prompts)} + + print(f"\n{'='*60}") + print(f"Starting batch text2lottie generation...") + print(f"{'='*60}\n") + + for idx, prompt in enumerate(prompts, 1): + print(f"\n[{idx}/{len(prompts)}] Processing:") + print(f" Prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}") + + try: + base_filename = sanitize_filename(prompt) + output_path = os.path.join(output_dir, f"{base_filename}.json") + + lottie_json, gen_info = run_inference( + model=model, + processor=processor, + task_type=TASK_TEXT, + device=device, + cfg=cfg, + uid=f"batch_{idx:04d}", + text_description=prompt, + use_sampling=args.use_sampling, + temperature=args.temperature, + top_p=args.top_p, + top_k=args.top_k, + repetition_penalty=args.repetition_penalty, + output_path=output_path, + verbose=False, + ) + + if lottie_json: + print(f" āœ… Success: {output_path}") + print(f" Layers: {len(lottie_json.get('layers', []))}, Tokens: {gen_info.get('generated_len', 0)}") + stats['success'] += 1 + else: + print(f" āŒ Generation failed") + stats['fail'] += 1 + + except Exception as e: + print(f" āŒ Error: {e}") + if args.debug: + traceback.print_exc() + stats['fail'] += 1 + + print(f"\n{'='*60}") + print(f"Batch Processing Complete!") + print(f"{'='*60}") + print(f"Total prompts: {stats['total']}") + print(f" āœ… Success: {stats['success']}") + print(f" āŒ Failed: {stats['fail']}") + print(f" Success rate: {stats['success']/stats['total']*100:.1f}%") + print(f"\nOutput directory: {output_dir}") + print(f"{'='*60}") + +# ========== MMLottie Benchmark ęŽØē† ========== +def run_mmlottie_bench_inference(args, cfg): + """ + Inference on MMLottieBench dataset from HuggingFace + + Dataset structure: + - Splits: real, synthetic + - Task types: Text-to-Lottie, Text-Image-to-Lottie, Video-to-Lottie + - Fields: id, text, image, video, task_type, subset, etc. + """ + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") + + # 1. Load dataset + print("\nLoading MMLottieBench dataset...") + try: + # Try to load from local directory first + if os.path.exists(args.mmlottie_bench_dir) and os.path.isdir(args.mmlottie_bench_dir): + try: + print(f" Attempting to load from local: {args.mmlottie_bench_dir}") + dataset = load_from_disk(args.mmlottie_bench_dir) + print(f" āœ… Loaded from local directory") + except Exception as e: + print(f" āš ļø Local load failed: {e}") + print(f" Downloading from HuggingFace...") + dataset = load_dataset("OmniLottie/MMLottieBench") + else: + print(f" Local directory not found, downloading from HuggingFace...") + dataset = load_dataset("OmniLottie/MMLottieBench") + + print(f" Available splits: {list(dataset.keys())}") + + except Exception as e: + print(f"\nāŒ Failed to load dataset: {e}") + print("Please check your network or download manually using:") + print(" python download_mmlottie_bench.py") + raise + + # 2. Select split + if args.split not in dataset: + raise ValueError(f"Split '{args.split}' not found in dataset. Available: {list(dataset.keys())}") + + subset = dataset[args.split] + print(f"\nProcessing split: {args.split}") + print(f" Total samples: {len(subset)}") + + # 3. Load model + print("\nLoading model...") + processor = AutoProcessor.from_pretrained(cfg['tokenizer_name'], padding_side="left") + processor.tokenizer.padding_side = "left" + + model = LottieDecoder(pix_len=cfg['pix_len'], text_len=cfg['text_len']) + + if os.path.isfile(args.sketch_weight) and args.sketch_weight.endswith('.bin'): + model_path = args.sketch_weight + safetensors_path = args.sketch_weight.replace('.bin', '.safetensors') + else: + model_path = os.path.join(args.sketch_weight, 'pytorch_model.bin') + safetensors_path = os.path.join(args.sketch_weight, 'model.safetensors') + + if os.path.exists(model_path): + model.load_state_dict(torch.load(model_path, map_location='cpu')) + print(f"Loaded from {model_path}") + elif os.path.exists(safetensors_path): + model.load_state_dict(load_file(safetensors_path)) + print(f"Loaded from {safetensors_path}") + else: + raise FileNotFoundError(f"Model not found in {args.sketch_weight}") + + model = model.to(device).eval() + + # 4. Filter by task type if specified + task_map = { + 'text2lottie': 'Text-to-Lottie', + 'text_image2lottie': 'Text-Image-to-Lottie', + 'video2lottie': 'Video-to-Lottie' + } + + if args.mmlottie_task: + task_type_filter = task_map.get(args.mmlottie_task) + if task_type_filter: + subset = subset.filter(lambda x: x.get('task_type') == task_type_filter) + print(f" Task filter: {args.mmlottie_task} ({task_type_filter})") + print(f" Filtered samples: {len(subset)}") + else: + print(f" āš ļø Unknown task: {args.mmlottie_task}, processing all tasks") + else: + print(f" Processing all task types") + + # 5. Prepare output directories + output_base = os.path.join(args.output_dir, f'mmlottie_bench_{args.split}') + os.makedirs(output_base, exist_ok=True) + + stats = { + 'Text-to-Lottie': {'success': 0, 'fail': 0, 'total': 0}, + 'Text-Image-to-Lottie': {'success': 0, 'fail': 0, 'total': 0}, + 'Video-to-Lottie': {'success': 0, 'fail': 0, 'total': 0} + } + + # 6. Process each sample + print(f"\n{'='*60}") + print("Starting inference...") + print(f"{'='*60}\n") + + for idx, sample in enumerate(subset): + task_type = sample.get('task_type', 'Unknown') + sample_id = sample.get('id', f'sample_{idx}') + + stats[task_type]['total'] += 1 + + print(f"[{idx+1}/{len(subset)}] Processing {sample_id} ({task_type})...") + + try: + if task_type == 'Text-to-Lottie': + # Text-to-Lottie generation + text_prompt = sample['text'] + print(f" Text: {text_prompt[:80]}...") + + # Generate using run_inference + output_path = os.path.join(output_base, f'{sample_id}.json') + lottie_json, info = run_inference( + model=model, + processor=processor, + task_type=TASK_TEXT, + device=device, + cfg=cfg, + text_description=text_prompt, + use_sampling=args.use_sampling, + temperature=args.temperature, + top_p=args.top_p, + top_k=args.top_k, + repetition_penalty=args.repetition_penalty, + output_path=output_path, + verbose=False + ) + + if lottie_json is not None: + print(f" āœ… Saved to {output_path}") + stats[task_type]['success'] += 1 + else: + print(f" āŒ Generation failed") + stats[task_type]['fail'] += 1 + + elif task_type == 'Text-Image-to-Lottie': + # Image + Text to Lottie generation + image = sample['image'] # PIL Image from datasets + text_prompt = sample.get('text', 'A simple animation') + + print(f" Text: {text_prompt[:80]}...") + print(f" Image size: {image.size}") + + # Resize image if needed + if image.size != (448, 448): + image = image.resize((448, 448), PILImage.LANCZOS) + + # Save image to temp file + import tempfile + with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_img: + image.save(tmp_img.name) + tmp_img_path = tmp_img.name + + # Generate using run_inference + output_path = os.path.join(output_base, f'{sample_id}.json') + lottie_json, info = run_inference( + model=model, + processor=processor, + task_type=TASK_IMAGE, + device=device, + cfg=cfg, + image_path=tmp_img_path, + text_description=text_prompt, + use_sampling=args.use_sampling, + temperature=args.temperature, + top_p=args.top_p, + top_k=args.top_k, + repetition_penalty=args.repetition_penalty, + output_path=output_path, + verbose=False + ) + + # Cleanup temp file + os.unlink(tmp_img_path) + + if lottie_json is not None: + print(f" āœ… Saved to {output_path}") + stats[task_type]['success'] += 1 + else: + print(f" āŒ Generation failed") + stats[task_type]['fail'] += 1 + + elif task_type == 'Video-to-Lottie': + # Video to Lottie generation + video_data = sample['video'] + + # For VideoReader objects, skip (can't extract easily) + if str(type(video_data).__name__) == 'VideoReader': + print(f" āš ļø VideoReader format not supported, skipping") + stats[task_type]['fail'] += 1 + continue + + # Save video to temp file for processing + import tempfile + tmp_video_path = None + try: + with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as tmp_video: + if isinstance(video_data, bytes): + tmp_video.write(video_data) + tmp_video_path = tmp_video.name + elif isinstance(video_data, dict) and 'path' in video_data: + tmp_video_path = video_data['path'] + else: + print(f" āš ļø Unknown video format: {type(video_data)}") + stats[task_type]['fail'] += 1 + continue + + print(f" Video: {tmp_video_path}") + + # Generate using run_inference + output_path = os.path.join(output_base, f'{sample_id}.json') + lottie_json, info = run_inference( + model=model, + processor=processor, + task_type=TASK_VIDEO, + device=device, + cfg=cfg, + video_path=tmp_video_path, + use_sampling=args.use_sampling, + temperature=args.temperature, + top_p=args.top_p, + top_k=args.top_k, + repetition_penalty=args.repetition_penalty, + output_path=output_path, + verbose=False + ) + + if lottie_json is not None: + print(f" āœ… Saved to {output_path}") + stats[task_type]['success'] += 1 + else: + print(f" āŒ Generation failed") + stats[task_type]['fail'] += 1 + + finally: + # Cleanup temp file if it was bytes + if tmp_video_path and isinstance(video_data, bytes): + try: + os.unlink(tmp_video_path) + except: + pass + + else: + print(f" āš ļø Unknown task type: {task_type}") + stats[task_type]['fail'] += 1 + + # Clear cache + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + except Exception as e: + print(f" āŒ Error: {e}") + if args.debug: + traceback.print_exc() + + # 7. Print summary + print(f"\n{'='*60}") + print("Benchmark Inference Complete!") + print(f"{'='*60}") + for task_type, task_stats in stats.items(): + if task_stats['total'] > 0: + success_rate = task_stats['success'] / task_stats['total'] * 100 + print(f"{task_type}:") + print(f" Success: {task_stats['success']}/{task_stats['total']} ({success_rate:.1f}%)") + print(f" Failed: {task_stats['fail']}/{task_stats['total']}") + print(f"\nOutput directory: {output_base}") + print(f"{'='*60}") + + +def run_single_inference(args, cfg): + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + print("Loading model...") + processor = AutoProcessor.from_pretrained(cfg['tokenizer_name'], padding_side="left") + processor.tokenizer.padding_side = "left" + + model = LottieDecoder(pix_len=cfg['pix_len'], text_len=cfg['text_len']) + + model_path = os.path.join(args.sketch_weight, 'pytorch_model.bin') + safetensors_path = os.path.join(args.sketch_weight, 'model.safetensors') + + if os.path.exists(model_path): + model.load_state_dict(torch.load(model_path, map_location='cpu')) + elif os.path.exists(safetensors_path): + model.load_state_dict(load_file(safetensors_path)) + + model = model.to(device).eval() + + os.makedirs(args.output_dir, exist_ok=True) + + if args.single_video: + task = TASK_VIDEO + out_path = os.path.join(args.output_dir, 'single_video_result.json') + lottie_json, info = run_inference( + model=model, processor=processor, task_type=task, device=device, cfg=cfg, + uid=None, + video_path=args.single_video, + use_sampling=args.use_sampling, + temperature=args.temperature, + top_p=args.top_p, + top_k=args.top_k, + repetition_penalty=args.repetition_penalty, + output_path=out_path, + verbose=True) + elif args.single_image: + task = TASK_IMAGE + out_path = os.path.join(args.output_dir, 'single_image_result.json') + lottie_json, info = run_inference( + model=model, processor=processor, task_type=task, device=device, cfg=cfg, + uid=None, + image_path=args.single_image, + text_description=args.single_text or "Animate this image", + use_sampling=args.use_sampling, + temperature=args.temperature, + top_p=args.top_p, + top_k=args.top_k, + repetition_penalty=args.repetition_penalty, + output_path=out_path, + verbose=True) + elif args.single_text: + task = TASK_TEXT + out_path = os.path.join(args.output_dir, 'single_text_result.json') + lottie_json, info = run_inference( + model=model, processor=processor, task_type=task, device=device, cfg=cfg, + uid=None, + text_description=args.single_text, + use_sampling=args.use_sampling, + temperature=args.temperature, + top_p=args.top_p, + top_k=args.top_k, + repetition_penalty=args.repetition_penalty, + output_path=out_path, + verbose=True) + else: + print("ERROR: Must specify --single_video, --single_image, or --single_text") + return + + if lottie_json: + print("\nāœ“ Generation successful!") + print(f" Output: {out_path}") + print(f" Layers: {len(lottie_json.get('layers', []))}") + print(f" Tokens generated: {info.get('generated_len', 0)}") + else: + print("\nāœ— Generation failed") + print(f" Info: {info}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Lottie Generation Inference") + + parser.add_argument("--sketch_weight", type=str, required=True, + help="Path to model checkpoint directory") + parser.add_argument("--tokenizer_name", type=str, + default="Qwen/Qwen2.5-VL-3B-Instruct") + + parser.add_argument("--output_dir", type=str, default="./output") + + parser.add_argument("--maxlen", type=int, default=4096, + help="Maximum token length for generation") + parser.add_argument("--text_len", type=int, default=1500, + help="Maximum instruction context length") + + parser.add_argument("--use_sampling", action="store_true", + help="Use sampling instead of greedy decoding") + parser.add_argument("--temperature", type=float, default=0.9, + help="Sampling temperature") + parser.add_argument("--top_p", type=float, default=0.25, + help="Top-p (nucleus) sampling") + parser.add_argument("--top_k", type=int, default=5, + help="Top-k sampling") + parser.add_argument("--repetition_penalty", type=float, default=1.01, + help="Repetition penalty (1.0 = disabled)") + + + parser.add_argument("--max_samples", type=int, default=-1, + help="Maximum samples to process (-1 = all)") + parser.add_argument("--task_filter", type=str, choices=['video', 'image', 'text', None], + default=None, help="Only process specific task type") + parser.add_argument("--shuffle", action="store_true", default=True, + help="Shuffle samples before processing") + + parser.add_argument("--single_video", type=str, default=None, + help="Path to single video for inference") + parser.add_argument("--single_image", type=str, default=None, + help="Path to single image for inference") + parser.add_argument("--single_text", type=str, default=None, + help="Text prompt for single inference") + + # MMLottie BenchmarkęØ”å¼ + parser.add_argument("--mmlottie_bench_dir", type=str, default="./mmlottie_bench", + help="Path to mmlottie_bench directory (default: ./mmlottie_bench)") + parser.add_argument("--split", type=str, choices=['real', 'synthetic'], default=None, + help="Split to use from mmlottie_bench (real or synthetic)") + parser.add_argument("--mmlottie_task", type=str, + choices=['text2lottie', 'text_image2lottie', 'video2lottie'], + default=None, + help="Specific task to run in mmlottie_bench (default: run all tasks)") + + parser.add_argument("--batch_text_file", type=str, default=None, + help="Path to text file with prompts (one per line) for batch text2lottie generation") + + parser.add_argument("--debug", action="store_true", + help="Enable debug mode with full tracebacks") + parser.add_argument("--verbose", action="store_true", default=True, + help="Verbose output") + + parser.add_argument("--num_candidates", type=int, default=1, + help="Number of candidates to generate (for Best-of-N selection, default: 1)") + + args = parser.parse_args() + + cfg = { + 'tokenizer_name': args.tokenizer_name, + 'text_len': args.text_len, + 'pix_len': args.maxlen, + 'num_candidates': args.num_candidates, + } + + print("=" * 60) + print("Lottie Generation Inference (Improved v2 + Multi-Candidate)") + print("=" * 60) + print(f"Model: {args.sketch_weight}") + print(f"Max tokens: {args.maxlen}") + print(f"Sampling: {args.use_sampling}") + if args.use_sampling: + print(f" Temperature: {args.temperature}") + print(f" Top-p: {args.top_p}") + print(f"Repetition penalty: {args.repetition_penalty}") + if args.num_candidates > 1: + print(f"šŸ†• Num candidates: {args.num_candidates} (Best-of-{args.num_candidates})") + print("=" * 60) + + if args.single_video or args.single_image or args.single_text: + print("\nRunning single sample inference...") + run_single_inference(args, cfg) + elif args.batch_text_file: + if not os.path.exists(args.batch_text_file): + raise FileNotFoundError(f"Batch text file not found: {args.batch_text_file}") + print(f"\nRunning batch text file inference") + print(f" Input file: {args.batch_text_file}") + run_batch_text_file_inference(args, cfg) + elif args.split: + # MMLottie Benchmark mode + print(f"\nRunning MMLottie Benchmark inference") + print(f" Split: {args.split}") + if args.mmlottie_bench_dir and os.path.exists(args.mmlottie_bench_dir): + print(f" Local dataset: {args.mmlottie_bench_dir}") + else: + print(f" Will download from HuggingFace if needed") + run_mmlottie_bench_inference(args, cfg) + + else: + print("\nError: No input specified!") + print("Please provide one of:") + print(" - --single_video, --single_image, or --single_text for single sample inference") + print(" - --batch_text_file for batch text2lottie generation") + print(" - --split [real|synthetic] for MMLottie benchmark inference") + exit(1) diff --git a/lottie/__init__.py b/lottie/__init__.py new file mode 100644 index 0000000..ef2e8db --- /dev/null +++ b/lottie/__init__.py @@ -0,0 +1,35 @@ +import os +import subprocess +from . import objects, parsers, utils, exporters, nvector, importers +from .nvector import * +from .utils.color import Color + +try: + from .version import __version__ +except ImportError: + here = os.path.dirname(os.path.abspath(__file__)) + pipe = subprocess.Popen( + ['git', 'describe', '--abbrev=0', '--tags'], + cwd=here, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE + ) + out, err = pipe.communicate() + if pipe.returncode == 0: + __version__ = out.strip()[1:].decode("ascii") + "+git" + else: + vfn = os.path.join(os.path.dirname(os.path.dirname(here)), "version") + if os.path.exists(vfn): + with open(vfn) as vf: + __version__ = vf.read().strip() + "+src" + else: + __version__ = "unknown" + +try: + version_tuple = tuple(map(int, __version__.split("+")[0].split("."))) if __version__ != "unknown" else (0, 0, 0) +except ValueError: + version_tuple = (0, 0, 0) + __version__ = "unknown" + + +__all__ = ["objects", "parsers", "utils", "exporters", "nvector", "NVector", "Point", "Color", "importers"] diff --git a/lottie/exporters/__init__.py b/lottie/exporters/__init__.py new file mode 100644 index 0000000..52ffd04 --- /dev/null +++ b/lottie/exporters/__init__.py @@ -0,0 +1,19 @@ +from . import base, core, sif, svg, pretty_print + +from .base import exporters +from .core import export_lottie, export_tgs, export_embedded_html +from .pretty_print import prettyprint, prettyprint_summary +from .sif import export_sif +from .svg import export_svg + +__all__ = [ + "base", "core", "sif", "svg", "pretty_print", + "exporters", "export_lottie", "export_tgs", "export_embedded_html", + "prettyprint", "prettyprint_summary", "export_sif", "export_svg", +] + +try: + from . import cairo, gif + __all__ += ["cairo", "gif"] +except ImportError: + pass diff --git a/lottie/exporters/base.py b/lottie/exporters/base.py new file mode 100644 index 0000000..6484a0a --- /dev/null +++ b/lottie/exporters/base.py @@ -0,0 +1,32 @@ +from ..parsers.baseporter import Baseporter, Loader, ExtraOption, io_progress + + +class ExporterLoader(Loader): + def __init__(self): + super().__init__(__file__, __name__, "export") + + @property + def exporters(self): + return self.items + + def set_options(self, parser): + group = parser.add_argument_group("Generic output options") + group.add_argument( + "--pretty", "-p", + action="store_true", + help="Pretty print (for formats that support it)", + ) + group.add_argument( + "--frame", + type=int, + default=0, + help="Frame to extract (for single-image formats)", + ) + + super().set_options(parser) + + return group + + +exporters = ExporterLoader() +exporter = exporters.decorator diff --git a/lottie/exporters/cairo.py b/lottie/exporters/cairo.py new file mode 100644 index 0000000..f11bc1a --- /dev/null +++ b/lottie/exporters/cairo.py @@ -0,0 +1,27 @@ +import cairosvg +import io + +from .base import exporter +from .svg import export_svg + + +def _export_cairo(func, animation, fp, frame, dpi): + intermediate = io.StringIO() + export_svg(animation, intermediate, frame) + intermediate.seek(0) + func(file_obj=intermediate, write_to=fp, dpi=dpi) + + +@exporter("PNG", ["png"], [], {"frame"}) +def export_png(animation, fp, frame=0, dpi=96): + _export_cairo(cairosvg.svg2png, animation, fp, frame, dpi) + + +@exporter("PDF", ["pdf"], [], {"frame"}) +def export_pdf(animation, fp, frame=0, dpi=96): + _export_cairo(cairosvg.svg2pdf, animation, fp, frame, dpi) + + +@exporter("PostScript", ["ps"], [], {"frame"}) +def export_ps(animation, fp, frame=0, dpi=96): + _export_cairo(cairosvg.svg2ps, animation, fp, frame, dpi) diff --git a/lottie/exporters/core.py b/lottie/exporters/core.py new file mode 100644 index 0000000..7278879 --- /dev/null +++ b/lottie/exporters/core.py @@ -0,0 +1,119 @@ +import sys +import json +import gzip +import codecs + +from .base import exporter +from ..utils.file import open_file +from ..parsers.baseporter import ExtraOption +from .tgs_validator import TgsValidator + + +@exporter("Lottie JSON", ["json"], [], {"pretty"}, "lottie") +def export_lottie(animation, file, pretty=False): + with open_file(file) as fp: + kw = {} + if pretty: + kw = dict(indent=4) + json.dump(animation.to_dict(), fp, **kw) + + +@exporter("Telegram Animated Sticker", ["tgs"], [ + ExtraOption("no_sanitize", help="Disable Sticker fit", action="store_false", dest="sanitize"), + ExtraOption("no_validate", help="Disable feature validation", action="store_false", dest="validate"), +]) +def export_tgs(animation, file, sanitize=False, validate=False): + if sanitize: + animation.tgs_sanitize() + + with gzip.open(file, "wb") as gzfile: + lottie_dict = animation.to_dict() + lottie_dict["tgs"] = 1 + json.dump(lottie_dict, codecs.getwriter('utf-8')(gzfile)) + + if validate: + validator = TgsValidator() + validator(animation) + validator.check_file_size(file) + if validator.errors: + sys.stdout.write("\n".join(map(str, validator.errors))+"\n") + + +class HtmlOutput: + def __init__(self, animation, file): + self.animation = animation + self.file = file + + def style(self): + self.file.write(""" + + + """ % (self.animation.width, self.animation.height)) + + def body_pre(self): + self.file.write(""" +
+ +""") + + def html_begin(self): + self.file.write(""" + + + + """) + self.style() + self.file.write("") + + def html_end(self): + self.file.write("") + + +@exporter("Lottie HTML", ["html", "htm"]) +def export_embedded_html(animation, file): + with open_file(file) as fp: + out = HtmlOutput(animation, fp) + out.html_begin() + out.body_pre() + out.body_embedded() + out.body_post() + out.html_end() + + +def export_linked_html(animation, file, path): + with open_file(file) as fp: + out = HtmlOutput(animation, fp) + out.html_begin() + out.body_pre() + file.write("path: %r" % path) + out.body_post() + out.html_end() diff --git a/lottie/exporters/dot_lottie.py b/lottie/exporters/dot_lottie.py new file mode 100644 index 0000000..8037707 --- /dev/null +++ b/lottie/exporters/dot_lottie.py @@ -0,0 +1,89 @@ +import json +import string +import zipfile + +from .base import exporter +from ..parsers.baseporter import ExtraOption +from ..parsers.tgs import parse_tgs +from lottie import __version__ +from ..objects import assets + + +@exporter("dotLottie Archive", ["lottie"], [ + ExtraOption("id", help="ID of the animation", default=None), + ExtraOption("append", help="Append animation to existing archive", action="store_true"), + ExtraOption("revision", help="File revision", type=int, default=None), + ExtraOption("author", help="File author", default=None), + ExtraOption("speed", help="Playback speed", type=float, default=1), + ExtraOption("theme_color", help="Theme color", type=str, default="#ffffff"), + ExtraOption("no_loop", help="Disable Looping", action="store_false", dest="loop"), + ExtraOption("no_pack", help="Don't auto-pack images", action="store_false", dest="pack_images"), +], slug="dotlottie") +def export_dotlottie(animation, file, id=None, append=False, revision=None, author=None, + speed=1.0, theme_color="#ffffff", loop=True, pack_images=True): + + files = {} + + if append: + with zipfile.ZipFile(file, "r") as zf: + with zf.open("manifest.json") as manifest: + meta = json.load(manifest) + + for name in zf.namelist(): + if name != "manifest.json": + files[name] = zf.read(name) + else: + meta = { + "generator": "Python Lottie " + __version__, + "version": 1.0, + "revision": 1, + "author": "", + "animations": [], + "custom": {} + } + + if revision is not None: + meta["revision"] = revision + + if author is not None: + meta["author"] = author + + if id is None: + if animation.name: + idok = string.ascii_letters + string.digits + "_-" + id = "".join(filter(lambda x: x in idok, animation.name.replace(" ", "_"))) + if not id: + id = "animation_%s" % len(meta["animations"]) + + meta["animations"].append({ + "id": id, + "speed": speed, + "themeColor": theme_color, + "loop": loop, + }) + + if pack_images and animation.assets: + animation = animation.clone() + image_no = 0 + for asset in animation.assets: + if isinstance(asset, assets.Image): + ext, data = asset.image_data() + if not ext: + continue + pathname = "images/" + while True: + basename = "image_%s.%s" % (image_no, ext) + image_no += 1 + if pathname+basename not in files: + break + files[pathname+basename] = data + asset.image_path = pathname + asset.image = basename + asset.is_embedded = False + + files["manifest.json"] = json.dumps(meta) + files["animations/%s.json" % id] = json.dumps(animation.to_dict()) + + with zipfile.ZipFile(file, "w") as zf: + for name, data in files.items(): + zf.writestr(name, data) diff --git a/lottie/exporters/gif.py b/lottie/exporters/gif.py new file mode 100644 index 0000000..b5e16e8 --- /dev/null +++ b/lottie/exporters/gif.py @@ -0,0 +1,133 @@ +import io +from PIL import Image +from PIL import features + +from .cairo import export_png +from .base import exporter, io_progress +from ..parsers.baseporter import ExtraOption + + +def _png_gif_prepare(image): + if image.mode not in ["RGBA", "RGBa"]: + image = image.convert("RGBA") + alpha = image.getchannel("A") + image = image.convert("RGB").convert('P', palette=Image.ADAPTIVE, colors=255) + mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) + image.paste(255, mask=mask) + return image + + +def _log_frame(fmt, frame_no=None, end=None): + if frame_no is None: + io_progress().report_message("%s frame rendering completed" % (fmt)) + else: + io_progress().report_progress("%s rendering frame" % fmt, frame_no, end) + + +@exporter("GIF", ["gif"], [ + ExtraOption("skip_frames", type=int, default=1, help="Only renderer 1 out of these many frames"), +]) +def export_gif(animation, fp, dpi=96, skip_frames=1): + """ + Gif export + + Note that it's a bit slow. + """ + start = int(animation.in_point) + end = int(animation.out_point) + frames = [] + for i in range(start, end+1, skip_frames): + _log_frame("GIF", i, end) + file = io.BytesIO() + export_png(animation, file, i, dpi) + file.seek(0) + frames.append(_png_gif_prepare(Image.open(file))) + _log_frame("GIF") + + io_progress().report_message("GIF Writing to file...") + duration = int(round(1000 / animation.frame_rate * skip_frames / 10)) * 10 + frames[0].save( + fp, + format='GIF', + append_images=frames[1:], + save_all=True, + duration=duration, + loop=0, + transparency=255, + disposal=2, + ) + + +@exporter("WebP", ["webp"], [ + ExtraOption("lossless", action="store_true", help="If present, use lossless compression"), + ExtraOption("quality", type=int, default=80, + help="Compression effort between 0 and 100\n" + + "for lossy 0 gives the smallest size\n" + + "for lossless 0 gives the largest file"), + ExtraOption("method", type=int, default=0, help="Quality/speed trade-off (0=fast, 6=slower-better)"), + ExtraOption("skip_frames", type=int, default=1, help="Only renderer 1 out of these many frames"), +]) +def export_webp(animation, fp, dpi=96, lossless=False, quality=80, method=0, skip_frames=1): + """ + Export WebP + + See https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#webp + """ + if not features.check("webp_anim"): + raise Exception("WebP animations not supported in this system") + + start = int(animation.in_point) + end = int(animation.out_point) + frames = [] + for i in range(start, end+1, skip_frames): + _log_frame("WebP", i, end) + file = io.BytesIO() + export_png(animation, file, i, dpi) + file.seek(0) + frames.append(Image.open(file)) + + _log_frame("WebP") + + io_progress().report_message("WebP Writing to file...") + duration = int(round(1000 / animation.frame_rate * skip_frames)) + frames[0].save( + fp, + format='WebP', + append_images=frames[1:], + save_all=True, + duration=duration, + loop=0, + background=(0, 0, 0, 0), + lossless=lossless, + quality=quality, + method=method + ) + + +@exporter("TIFF", ["tiff"]) +def export_tiff(animation, fp, dpi=96): + """ + Export TIFF + """ + start = int(animation.in_point) + end = int(animation.out_point) + frames = [] + for i in range(start, end+1): + _log_frame("TIFF", i, end) + file = io.BytesIO() + export_png(animation, file, i, dpi) + file.seek(0) + frames.append(Image.open(file)) + _log_frame("TIFF") + + io_progress().report_message("TIFF Writing to file...") + duration = int(round(1000 / animation.frame_rate)) + frames[0].save( + fp, + format='TIFF', + append_images=frames[1:], + save_all=True, + duration=duration, + loop=0, + dpi=(dpi, dpi), + ) diff --git a/lottie/exporters/pretty_print.py b/lottie/exporters/pretty_print.py new file mode 100644 index 0000000..a2ae48f --- /dev/null +++ b/lottie/exporters/pretty_print.py @@ -0,0 +1,71 @@ +import sys + +from ..objects.base import LottieObject, LottieBase +from ..objects.properties import MultiDimensional, Value, ShapeProperty +from ..objects.layers import Layer + + +def _prettyprint_scalar(lottie_object, out=sys.stdout): + if isinstance(lottie_object, float) and lottie_object == round(lottie_object): + lottie_object = int(lottie_object) + return str(lottie_object) + + +def prettyprint(lottie_object, out=sys.stdout, indent=" ", _i=""): + if isinstance(lottie_object, LottieObject): + out.write(lottie_object.__class__.__name__) + out.write('\n') + _i += indent + maxk = max(map(lambda x: len(x.name), lottie_object._props)) + for k in lottie_object._props: + out.write(_i) + out.write(k.name.ljust(maxk)) + out.write(' : ') + prettyprint(k.get(lottie_object), out, indent, _i) + elif isinstance(lottie_object, (list, tuple)): + if not lottie_object or (not isinstance(lottie_object[0], LottieBase) and len(lottie_object) < 16): + out.write("[") + out.write(", ".join(map(_prettyprint_scalar, lottie_object))) + out.write("]\n") + else: + out.write("[\n") + for k in lottie_object: + out.write(_i + indent) + prettyprint(k, out, indent, _i + indent) + out.write(_i) + out.write(']\n') + else: + out.write(_prettyprint_scalar(lottie_object, out)) + out.write('\n') + + +def _prettyprint_summary_printable(obj): + if isinstance(obj, LottieObject): + return not isinstance(obj, (MultiDimensional, Value, ShapeProperty)) + return obj and isinstance(obj, (list, tuple)) and isinstance(obj[0], LottieObject) + + +def prettyprint_summary(lottie_object, out=sys.stdout, indent=" ", _i=""): + if isinstance(lottie_object, LottieObject): + out.write(lottie_object.__class__.__name__) + name = getattr(lottie_object, "name", None) + if name: + out.write(" %r" % name) + if isinstance(lottie_object, Layer): + out.write(" %s -> %s" % (lottie_object.index, lottie_object.parent_index)) + out.write('\n') + _i += indent + for k in lottie_object._props: + val = k.get(lottie_object) + if _prettyprint_summary_printable(val): + out.write(_i) + out.write(k.name) + out.write(' : ') + prettyprint_summary(val, out, indent, _i) + elif _prettyprint_summary_printable(lottie_object): + out.write("[\n") + for k in lottie_object: + out.write(_i + indent) + prettyprint_summary(k, out, indent, _i + indent) + out.write(_i) + out.write(']\n') diff --git a/lottie/exporters/sif.py b/lottie/exporters/sif.py new file mode 100644 index 0000000..b60a3d9 --- /dev/null +++ b/lottie/exporters/sif.py @@ -0,0 +1,11 @@ + +from .base import exporter +from ..parsers.sif.builder import to_sif +from ..utils.file import open_file + + +@exporter("Synfig", ["sif"], [], {"pretty"}) +def export_sif(animation, file, pretty=True): + with open_file(file) as fp: + dom = to_sif(animation).to_xml() + dom.writexml(fp, "", " " if pretty else "", "\n" if pretty else "") diff --git a/lottie/exporters/svg.py b/lottie/exporters/svg.py new file mode 100644 index 0000000..9e81cfa --- /dev/null +++ b/lottie/exporters/svg.py @@ -0,0 +1,22 @@ +from xml.dom import minidom +from xml.etree import ElementTree + +from .base import exporter +from ..parsers.svg.builder import to_svg +from ..utils.file import open_file + + +def _print_ugly_xml(dom, file): + return dom.write(file, "utf-8", True) + + +def _print_pretty_xml(dom, file): + with open_file(file) as fp: + xmlstr = minidom.parseString(ElementTree.tostring(dom.getroot())).toprettyxml(indent=" ") + fp.write(xmlstr) + + +@exporter("SVG", ["svg"], [], {"pretty", "frame"}) +def export_svg(animation, file, frame=0, pretty=True): + _print_xml = _print_pretty_xml if pretty else _print_ugly_xml + _print_xml(to_svg(animation, frame), file) diff --git a/lottie/exporters/tgs_validator.py b/lottie/exporters/tgs_validator.py new file mode 100644 index 0000000..20d6c71 --- /dev/null +++ b/lottie/exporters/tgs_validator.py @@ -0,0 +1,198 @@ +import os +import enum +import json +import inspect + +from ..parsers.tgs import parse_tgs +from ..objects.base import ObjectVisitor +from ..objects.animation import Animation +from ..objects import layers +from ..objects import shapes +from ..objects import helpers + + +class Severity(enum.Enum): + Note = enum.auto() + Warning = enum.auto() + Error = enum.auto() + + +class TgsError: + def __init__(self, message, target, severity=Severity.Warning): + self.message = message + self.target = target + self.severity = severity + + def target_id(self): + if isinstance(self.target, str): + return self.target + if getattr(self.target, "name", ""): + return self.target.name + return self.target.__class__.__name__ + + def __str__(self): + return "%s: on %s: %s" % ( + self.severity.name, + self.target_id(), + self.message + ) + + +class TgsValidator(ObjectVisitor): + def __init__(self, severity=Severity.Note): + self.errors = [] + self.severity = severity + + def _check(self, expr, message, target, severity=Severity.Warning): + if severity.value >= self.severity.value and not expr: + self.errors.append(TgsError(message, target, severity)) + + def check_file_size(self, filename): + return self.check_size(os.path.getsize(filename)) + + def check_size(self, bytes, filename="file"): + size_k = bytes / 1024 + self._check( + size_k <= 64, + "Invalid size (%.1fk), should be less than 64k" % size_k, + filename, + Severity.Error + ) + + def check_file(self, filename): + self.check_file_size(filename) + try: + self(parse_tgs(filename)) + except json.decoder.JSONDecodeError as e: + self._check( + False, + "Invalid JSON: %s" % e, + filename, + Severity.Error + ) + + def visit(self, object): + for cls in inspect.getmro(object.__class__): + callback = "_visit_%s" % cls.__name__.lower() + if hasattr(self, callback): + getattr(self, callback)(object) + + def _visit_animation(self, o: Animation): + self._check( + o.frame_rate in {30, 60}, + "Invalid framerate %s, should be 30 or 60" % o.frame_rate, + o, + Severity.Error + ) + self._check( + o.width == 512, + "Invalid width %s, should be 512" % o.width, + o, + Severity.Error + ) + self._check( + o.height == 512, + "Invalid height %s, should be 512" % o.height, + o, + Severity.Error + ) + self._check( + (o.out_point-o.in_point) <= 180, + "Too many frames (%s), should be less than 180" % (o.out_point-o.in_point), + o, + Severity.Error + ) + + def _visit_layer(self, o: layers.Layer): + self._check( + not o.has_masks and not o.masks, + "Masks are not officially supported", + o, + Severity.Note + ) + self._check( + not o.effects, + "Effects are not supported", + o, + Severity.Warning + ) + self._check( + not o.threedimensional, + "3D layers are not supported", + o, + Severity.Warning + ) + self._check( + not isinstance(o, layers.TextLayer), + "Text layers are not supported", + o, + Severity.Warning + ) + self._check( + not isinstance(o, layers.ImageLayer), + "Image layers are not supported", + o, + Severity.Warning + ) + self._check( + not o.auto_orient, + "Auto-orient layers are not supported", + o, + Severity.Warning + ) + self._check( + o.matte_mode in {None, layers.MatteMode.Normal}, + "Mattes are not supported", + o, + Severity.Warning + ) + + def _visit_precomplayer(self, o: layers.PreCompLayer): + self._check( + o.time_remapping is None, + "Time remapping is not supported", + o, + Severity.Warning + ) + + def _visit_merge(self, o: shapes.Merge): + self._check( + False, + "Merge paths are not supported", + o, + Severity.Warning + ) + + def _visit_transform(self, o: helpers.Transform): + self._check( + o.skew is None or ( + not o.skew.animated and o.skew.value == 0 + ), + "Skew transforms are not supported", + o, + Severity.Warning + ) + + def _visit_gradientstroke(self, o: shapes.GradientStroke): + self._check( + False, + "Gradient strokes are not officially supported", + o, + Severity.Note + ) + + def _visit_star(self, o: shapes.Star): + self._check( + False, + "Star Shapes are not officially supported", + o, + Severity.Note + ) + + def _visit_repeater(self, o: shapes.Repeater): + self._check( + False, + "Repeaters are not officially supported", + o, + Severity.Note + ) diff --git a/lottie/exporters/video.py b/lottie/exporters/video.py new file mode 100644 index 0000000..acf68d1 --- /dev/null +++ b/lottie/exporters/video.py @@ -0,0 +1,43 @@ +import io +import os + +import cv2 +import numpy +from PIL import Image + +from .cairo import export_png +from .gif import _log_frame +from .base import exporter +from ..parsers.baseporter import ExtraOption + + +## @see http://www.fourcc.org/codecs.php +formats4cc = { + "avi": cv2.VideoWriter_fourcc(*"XVID"), + "mp4": cv2.VideoWriter_fourcc(*'MP4V'), + #"mp4": cv2.VideoWriter_fourcc(*'X264'), + "webm": cv2.VideoWriter_fourcc(*'VP80'), +} + + +@exporter("Video", list(formats4cc.keys()), [ + ExtraOption("format", default=None, help="Specific video format", choices=list(formats4cc.keys())), +], [], "video") +def export_video(animation, fp, format=None): + start = int(animation.in_point) + end = int(animation.out_point) + if format is None: + fn = fp if isinstance(fp, str) else fp.name + format = os.path.splitext(fn)[1][1:] + fmt = formats4cc[format] + video = cv2.VideoWriter(fp, fmt, animation.frame_rate, (animation.width, animation.height)) + + for i in range(start, end+1): + _log_frame(format, i, end) + file = io.BytesIO() + export_png(animation, file, i) + file.seek(0) + video.write(cv2.cvtColor(numpy.array(Image.open(file)), cv2.COLOR_RGB2BGR)) + + _log_frame(format) + video.release() diff --git a/lottie/importers/__init__.py b/lottie/importers/__init__.py new file mode 100644 index 0000000..0ed194a --- /dev/null +++ b/lottie/importers/__init__.py @@ -0,0 +1,13 @@ +from . import base, core, sif, svg +from .base import importers + +__all__ = [ + "base", "core", "sif", "svg", + "importers", +] + +try: + from . import raster + __all__ += ["raster"] +except ImportError: + pass diff --git a/lottie/importers/base.py b/lottie/importers/base.py new file mode 100644 index 0000000..26fd8be --- /dev/null +++ b/lottie/importers/base.py @@ -0,0 +1,21 @@ +from ..parsers.baseporter import Baseporter, Loader + + +class ImporterLoader(Loader): + def __init__(self): + super().__init__(__file__, __name__, "import") + + @property + def importers(self): + return self.items + + def set_options(self, parser): + group = parser.add_argument_group("Generic input options") + + super().set_options(parser) + + return group + + +importers = ImporterLoader() +importer = importers.decorator diff --git a/lottie/importers/core.py b/lottie/importers/core.py new file mode 100644 index 0000000..f897bef --- /dev/null +++ b/lottie/importers/core.py @@ -0,0 +1,7 @@ +from .base import importer +from ..parsers.tgs import parse_tgs + + +@importer("Lottie JSON / Telegram Sticker", ["json", "tgs"], slug="lottie") +def import_tgs(file, *a, **kw): + return parse_tgs(file, *a, **kw) diff --git a/lottie/importers/dot_lottie.py b/lottie/importers/dot_lottie.py new file mode 100644 index 0000000..cce9957 --- /dev/null +++ b/lottie/importers/dot_lottie.py @@ -0,0 +1,32 @@ +import json +import zipfile + +from .base import importer +from ..parsers.baseporter import ExtraOption +from ..parsers.tgs import parse_tgs +from ..objects import Animation, assets + + +@importer("dotLottie Archive", ["lottie"], [ + ExtraOption("id", help="ID of the animation to extract", default=None) +], slug="dotlottie") +def import_dotlottie(file, id=None): + with zipfile.ZipFile(file) as zf: + with zf.open("manifest.json") as manifest: + meta = json.load(manifest) + + if id is None: + id = meta["animations"][0]["id"] + + info = zf.getinfo("animations/%s.json" % id) + + with zf.open(info) as animfile: + an = Animation.load(json.load(animfile)) + if an.assets: + for asset in an.assets: + if isinstance(asset, assets.Image) and not asset.is_embedded: + fname = asset.image_path + asset.image + if fname in zf.namelist(): + with zf.open(fname) as imgfile: + asset.load(imgfile) + return an diff --git a/lottie/importers/krita.py b/lottie/importers/krita.py new file mode 100644 index 0000000..d00a037 --- /dev/null +++ b/lottie/importers/krita.py @@ -0,0 +1,58 @@ +import zipfile +import warnings +from xml.etree import ElementTree + +from .base import importer +from ..parsers.svg.importer import SvgParser +from .. import objects + +ns = "{%s}" % "http://www.calligra.org/DTD/krita" + + +def _ns(string): + return string.format(ns=ns) + + +def _import_layers(zf, animation, xml_parent, svg_parser, parent): + for xml_layer in xml_parent.findall(_ns("./{ns}layers/{ns}layer")): + nodetype = xml_layer.attrib["nodetype"] + + if nodetype == "grouplayer": + layer = animation.add_layer(objects.NullLayer()) + _import_layers(zf, animation, xml_layer, svg_parser, layer) + elif nodetype == "shapelayer": + filename = "%s/layers/%s.shapelayer/content.svg" % (animation.name, xml_layer.attrib["filename"]) + with zf.open(filename) as svg_tree: + layer = svg_parser.etree_to_layer(animation, ElementTree.parse(svg_tree)) + else: + warnings.warn("Unsupported krita layer %s" % nodetype) + continue + + layer.name = xml_layer.attrib["name"] + if xml_layer.attrib["visible"] == 0: + layer.transform.opacity.value = 0 + layer.parent = parent + + +@importer("Krita", ["kra"]) +def import_krita(file): + with zipfile.ZipFile(file) as zf: + with zf.open("maindoc.xml") as main: + main_xml = ElementTree.parse(main) + + image = main_xml.find(_ns("./{ns}IMAGE")) + fps = float(main_xml.find(_ns("./{ns}IMAGE/{ns}animation/{ns}framerate")).attrib["value"]) + framerange = main_xml.find(_ns("./{ns}IMAGE/{ns}animation/{ns}range")).attrib + + animation = objects.Animation(int(framerange["to"]), fps) + animation.in_point = int(framerange["from"]) + animation.width = int(image.attrib["width"]) + animation.height = int(image.attrib["height"]) + animation.name = image.attrib["name"] + + parser = SvgParser() + parser.dpi = int(image.attrib["x-res"]) + + _import_layers(zf, animation, image, parser, None) + + return animation diff --git a/lottie/importers/raster.py b/lottie/importers/raster.py new file mode 100644 index 0000000..6212363 --- /dev/null +++ b/lottie/importers/raster.py @@ -0,0 +1,72 @@ +from .base import importer +from ..parsers.baseporter import ExtraOption +from ..parsers.pixel import ( + pixel_to_animation_paths, pixel_to_animation, + raster_to_embedded_assets, raster_to_linked_assets +) +from ..parsers.svg.importer import parse_color + +try: + from ..parsers.raster import raster_to_animation + raster = True +except ImportError: + raster = False + + +@importer("Raster image", ["bmp", "png", "gif", "webp", "tiff"], [ + ExtraOption("n_colors", type=int, default=1, help="Number of colors to quantize"), + ExtraOption("palette", type=parse_color, default=[], nargs="+", help="Custom palette"), + ExtraOption( + "mode", + default="embed", + choices=["external", "embed", "pixel", "polygon"] + (["trace"] if raster else []), + help="Vectorization mode:\n" + + " * external : load images as linked assets\n" + + " * embed : load images as embedded assets\n" + + " * pixel : Vectorize the image into rectangles\n" + + " * polygon : Vectorize the image into polygonal shapes\n" + + " Looks the same as pixel, but a single shape per color\n" + + " * trace : (if available) Use potrace to vectorize\n" + ), + ExtraOption("frame_delay", type=int, default=4, help="Number of frames to skip between images"), + ExtraOption("framerate", type=int, default=60, help="Frames per second"), + ExtraOption("frame_files", nargs="+", default=[], help="Additional frames to import"), + ExtraOption( + "color_mode", + default="nearest", + choices=["nearest", "exact"], + help="How to quantize colors.\n" + + " * nearest will map each color to the most similar in the palette\n" + + " * exact will only match exact colors" + ), + ExtraOption( + "embed_format", + default=None, + help="Format to store images internally when using `embed` mode" + ), +]) +def import_raster(filenames, n_colors, palette, mode, frame_delay=1, + framerate=60, frame_files=[], color_mode="nearest", embed_format=None): + if not isinstance(filenames, list): + filenames = [filenames] + filenames = filenames + frame_files + + if mode == "embed": + return raster_to_embedded_assets(filenames, frame_delay, framerate, embed_format) + elif mode == "external": + return raster_to_linked_assets(filenames, frame_delay, framerate) + elif mode == "trace": + from ..parsers.raster import QuanzationMode + # TODO QuanzationMode for raster + cm = QuanzationMode.Nearest if color_mode == "nearest" else QuanzationMode.Exact + + return raster_to_animation( + filenames, n_colors, frame_delay, + framerate=framerate, + palette=palette, + mode=cm + ) + elif mode == "polygon": + return pixel_to_animation_paths(filenames, frame_delay, framerate) + else: + return pixel_to_animation(filenames, frame_delay, framerate) diff --git a/lottie/importers/script.py b/lottie/importers/script.py new file mode 100644 index 0000000..cbf885c --- /dev/null +++ b/lottie/importers/script.py @@ -0,0 +1,16 @@ +import json +import tempfile +import subprocess +from .base import importer +from ..objects import Animation + + +@importer("Python script", ["py"]) +def import_python_script(file, *a, **kw): + + out = subprocess.check_output(["python", file, "--version"]) + if b"python-lottie script" not in out: + raise Exception("Not a valid script") + + data = subprocess.check_output(["python", file, "--path", "", "--name", "-", "--format", "json"]) + return Animation.load(json.loads(data)) diff --git a/lottie/importers/sif.py b/lottie/importers/sif.py new file mode 100644 index 0000000..b02fc22 --- /dev/null +++ b/lottie/importers/sif.py @@ -0,0 +1,7 @@ +from .base import importer +from ..parsers.sif import parse_sif_file + + +@importer("Synfig", ["sif", "sifz"]) +def import_sif(file, *a, **kw): + return parse_sif_file(file, *a, **kw) diff --git a/lottie/importers/svg.py b/lottie/importers/svg.py new file mode 100644 index 0000000..6148e77 --- /dev/null +++ b/lottie/importers/svg.py @@ -0,0 +1,16 @@ +from .base import importer +from ..parsers.baseporter import ExtraOption +from ..parsers.svg import parse_svg_file +from ..parsers.tgs import open_maybe_gzipped + + +@importer("SVG", ["svg", "svgz"], [ + ExtraOption( + "layer_frames", type=int, default=0, + help="If greater than 0, treats every layer in the SVG as a different animation frame,\n" + "greater values increase the time each frames lasts for."), + ExtraOption("n_frames", type=int, default=60), + ExtraOption("framerate", type=int, default=60), +]) +def import_svg(file, *a, **kw): + return open_maybe_gzipped(file, lambda svgfile: parse_svg_file(svgfile, *a, **kw)) diff --git a/lottie/nvector.py b/lottie/nvector.py new file mode 100644 index 0000000..0ec470e --- /dev/null +++ b/lottie/nvector.py @@ -0,0 +1,148 @@ +import operator +import math + + +def vop(op, a, b): + return list(map(op, a, b)) + + +class NVector(): + def __init__(self, *components): + self.components = list(components) + + def __str__(self): + return str(self.components) + + def __repr__(self): + return "" % self + + def __len__(self): + return len(self.components) + + def to_list(self): + return list(self.components) + + def __add__(self, other): + return type(self)(*vop(operator.add, self.components, other.components)) + + def __sub__(self, other): + return type(self)(*vop(operator.sub, self.components, other.components)) + + def __mul__(self, scalar): + if isinstance(scalar, NVector): + return type(self)(*vop(operator.mul, self.components, scalar.components)) + return type(self)(*(c * scalar for c in self.components)) + + def __truediv__(self, scalar): + return type(self)(*(c / scalar for c in self.components)) + + def __iadd__(self, other): + self.components = vop(operator.add, self.components, other.components) + return self + + def __isub__(self, other): + self.components = vop(operator.sub, self.components, other.components) + return self + + def __imul__(self, scalar): + if isinstance(scalar, NVector): + self.components = vop(operator.mul, self.components, scalar.components) + else: + self.components = [c * scalar for c in self.components] + return self + + def __itruediv__(self, scalar): + self.components = [c / scalar for c in self.components] + return self + + def __neg__(self): + return type(self)(*(-c for c in self.components)) + + def __getitem__(self, key): + if isinstance(key, slice): + return NVector(*self.components[key]) + return self.components[key] + + def __setitem__(self, key, value): + self.components[key] = value + + def __eq__(self, other): + return self.components == other.components + + def __abs__(self): + return type(self)(*(abs(c) for c in self.components)) + + @property + def length(self): + return math.sqrt(sum(map(lambda x: x**2, self.components))) + + def dot(self, other): + return sum(map(operator.mul, self.components, other.components)) + + def clone(self): + return NVector(*self.components) + + def lerp(self, other, t): + return self * (1-t) + other * t + + @property + def x(self): + return self.components[0] + + @x.setter + def x(self, v): + self.components[0] = v + + @property + def y(self): + return self.components[1] + + @y.setter + def y(self, v): + self.components[1] = v + + @property + def z(self): + return self.components[2] + + @z.setter + def z(self, v): + self.components[2] = v + + def element_scaled(self, other): + return type(self)(*vop(operator.mul, self.components, other.components)) + + def cross(self, other): + """ + @pre len(self) == len(other) == 3 + """ + a = self + b = other + return type(self)( + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ) + + @property + def polar_angle(self): + """ + @pre len(self) == 2 + """ + return math.atan2(self.y, self.x) + + +def Point(x, y): + return NVector(x, y) + + +def Size(x, y): + return NVector(x, y) + + +def Point3D(x, y, z): + return NVector(x, y, z) + + +def PolarVector(length, theta): + return NVector(length * math.cos(theta), length * math.sin(theta)) diff --git a/lottie/objects/__init__.py b/lottie/objects/__init__.py new file mode 100644 index 0000000..d34aeb8 --- /dev/null +++ b/lottie/objects/__init__.py @@ -0,0 +1,23 @@ +"""! +Package with all the Lottie Python bindings +""" +from . import ( + animation, base, effects, enums, helpers, layers, shapes, assets, easing, + text, bezier, composition +) +from .animation import Animation +from .layers import * +from .shapes import * +from .assets import Precomp +from .bezier import Bezier +from .composition import Composition + +__all__ = [ + "animation", "base", "effects", "enums", "helpers", "layers", "shapes", "assets", + "easing", "text", "bezier", + "Animation", + "NullLayer", "TextLayer", "ShapeLayer", "ImageLayer", "PreCompLayer", "SolidColorLayer", + "Rect", "Fill", "Trim", "Repeater", "GradientFill", "Stroke", "RoundedCorners", "Path", + "TransformShape", "Group", "Star", "Ellipse", "Merge", "GradientStroke", + "Bezier", "Precomp", "Composition", +] diff --git a/lottie/objects/animation.py b/lottie/objects/animation.py new file mode 100644 index 0000000..d3bdc33 --- /dev/null +++ b/lottie/objects/animation.py @@ -0,0 +1,123 @@ +from .base import LottieObject, LottieProp, PseudoBool, Index +from .layers import Layer +from .assets import Asset, Chars, Precomp +from .text import FontList +from .composition import Composition + +##\defgroup Lottie Lottie +# +# Objects of the lottie file structure. + +## \defgroup LottieCheck Lottie (to check) +# +# Lottie objects that have not been tested + + +## @ingroup Lottie +class Animation(Composition): + """! + Top level object, describing the animation + + @see http://docs.aenhancers.com/items/compitem/ + """ + _props = [ + LottieProp("version", "v", str, False), + LottieProp("frame_rate", "fr", float, False), + LottieProp("in_point", "ip", float, False), + LottieProp("out_point", "op", float, False), + LottieProp("width", "w", int, False), + LottieProp("height", "h", int, False), + LottieProp("name", "nm", str, False), + LottieProp("threedimensional", "ddd", PseudoBool, False), + LottieProp("assets", "assets", Asset, True), + #LottieProp("comps", "comps", Animation, True), + LottieProp("fonts", "fonts", FontList), + LottieProp("chars", "chars", Chars, True), + #LottieProp("markers", "markers", Marker, True), + #LottieProp("motion_blur", "mb", MotionBlur, False), + ] + _version = "5.5.2" + + def __init__(self, n_frames=60, framerate=60): + super().__init__() + ## The time when the composition work area begins, in frames. + self.in_point = 0 + ## The time when the composition work area ends. + ## Sets the final Frame of the animation + self.out_point = n_frames + ## Frames per second + self.frame_rate = framerate + ## Composition Width + self.width = 512 + ## Composition has 3-D layers + self.threedimensional = False + ## Composition Height + self.height = 512 + ## Bodymovin Version + self.version = self._version + ## Composition name + self.name = None + ## source items that can be used in multiple places. Comps and Images for now. + self.assets = [] # Image, Precomp + ## source chars for text layers + self.chars = None + ## Available fonts + self.fonts = None + + def precomp(self, name): + for ass in self.assets: + if isinstance(ass, Precomp) and ass.id == name: + return ass + return None + + def _on_prepare_layer(self, layer): + if layer.in_point is None: + layer.in_point = self.in_point + if layer.out_point is None: + layer.out_point = self.out_point + + def tgs_sanitize(self): + """! + Cleans up some things to ensure it works as a telegram sticker + """ + if self.width != 512 or self.height != 512: + scale = min(512/self.width, 512/self.height) + self.width = self.height = 512 + + for layer in self.layers: + if layer.parent_index: + continue + + if layer.transform.scale.animated: + for kf in layer.transform.scale.keyframes: + if kf.start is not None: + kf.start *= scale + if kf.end is not None: + kf.end *= scale + else: + layer.transform.scale.value *= scale + + if layer.transform.position.animated: + for kf in layer.transform.position.keyframes: + if kf.start is not None: + kf.start *= scale + if kf.end is not None: + kf.end *= scale + else: + layer.transform.position.value *= scale + + if self.frame_rate < 45: + self.frame_rate = 30 + else: + self.frame_rate = 60 + + def _fixup(self): + super()._fixup() + if self.assets: + for ass in self.assets: + if isinstance(ass, Precomp): + ass.animation = self + ass._fixup() + + def __str__(self): + return self.name or super().__str__() diff --git a/lottie/objects/assets.py b/lottie/objects/assets.py new file mode 100644 index 0000000..bf4698b --- /dev/null +++ b/lottie/objects/assets.py @@ -0,0 +1,209 @@ +import os +import re +import base64 +import mimetypes +from io import BytesIO +from .base import LottieObject, LottieProp, PseudoBool, Index +from .layers import Layer +from .shapes import ShapeElement +from .composition import Composition + + +## @ingroup Lottie +class Asset(LottieObject): + @classmethod + def _load_get_class(cls, lottiedict): + if "p" in lottiedict or "u" in lottiedict: + return Image + if "layers" in lottiedict: + return Precomp + + +## @ingroup Lottie +class Image(Asset): + """! + External image + + @see http://docs.aenhancers.com/sources/filesource/ + """ + _props = [ + LottieProp("height", "h", float, False), + LottieProp("width", "w", float, False), + LottieProp("id", "id", str, False), + LottieProp("image", "p", str, False), + LottieProp("image_path", "u", str, False), + LottieProp("is_embedded", "e", PseudoBool, False), + ] + + @staticmethod + def guess_mime(file): + if isinstance(file, str): + filename = file + elif hasattr(file, "name"): + filename = file.name + else: + return "application/octet-stream" + return mimetypes.guess_type(filename) + + def __init__(self, id=""): + ## Image Height + self.height = 0 + ## Image Width + self.width = 0 + ## Image ID + self.id = id + ## Image name + self.image = "" + ## Image path + self.image_path = "" + ## Image data is stored as a data: url + self.is_embedded = False + + def load(self, file, format=None): + """! + @param file Filename, file object, or PIL.Image.Image to load + @param format Format to store the image data as + """ + from PIL import Image + + if not isinstance(file, Image.Image): + image = Image.open(file) + else: + image = file + + self._id_from_file(file) + + self.image_path = "" + if format is None: + format = (image.format or "png").lower() + self.width, self.height = image.size + output = BytesIO() + image.save(output, format=format) + self.image = "data:image/%s;base64,%s" % ( + format, + base64.b64encode(output.getvalue()).decode("ascii") + ) + self.is_embedded = True + return self + + def _id_from_file(self, file): + if not self.id: + if isinstance(file, str): + self.id = os.path.basename(file) + elif hasattr(file, "name"): + self.id = os.path.basename(file.name) + elif hasattr(file, "filename"): + self.id = os.path.basename(file.filename) + else: + self.id = "image_%s" % id(self) + + @classmethod + def embedded(cls, image, format=None): + """! + Create an object from an image file + """ + lottie_image = cls() + return lottie_image.load(image, format) + + @classmethod + def linked(cls, filename): + from PIL import Image + image = Image.open(filename) + lottie_image = cls() + lottie_image._id_from_file(filename) + lottie_image.image_path, lottie_image.image = os.path.split(filename) + lottie_image.image_path += "/" + lottie_image.width = image.width + lottie_image.height = image.height + return lottie_image + + def image_data(self): + """ + Returns a tuple (format, data) with the contents of the image + + `format` is a string like "png", and `data` is just raw binary data. + + If it's impossible to fetch this info, returns (None, None) + """ + if self.is_embedded: + m = re.match("data:[^/]+/([^;,]+);base64,(.*)", self.image) + if m: + return m.group(1), base64.b64decode(m.group(2)) + return None, None + path = self.image_path + self.image + if os.path.isfile(path): + with open(path, "rb") as imgfile: + return os.path.splitext(path)[1][1:], imgfile.read() + return None, None + + +## @ingroup Lottie +class CharacterData(LottieObject): + """! + Character shapes + """ + _props = [ + LottieProp("shapes", "shapes", ShapeElement, True), + ] + + def __init__(self): + self.shapes = [] + + +## @ingroup Lottie +class Chars(LottieObject): + """! + Defines character shapes to avoid loading system fonts + """ + _props = [ + LottieProp("character", "ch", str, False), + LottieProp("font_family", "fFamily", str, False), + LottieProp("font_size", "size", float, False), + LottieProp("font_style", "style", str, False), + LottieProp("width", "w", float, False), + LottieProp("data", "data", CharacterData, False), + ] + + def __init__(self): + ## Character Value + self.character = "" + ## Character Font Family + self.font_family = "" + ## Character Font Size + self.font_size = 0 + ## Character Font Style + self.font_style = "" # Regular + ## Character Width + self.width = 0 + ## Character Data + self.data = CharacterData() + + @property + def shapes(self): + return self.data.shapes + + +## @ingroup Lottie +class Precomp(Asset, Composition): + _props = [ + LottieProp("id", "id", str, False), + ] + + def __init__(self, id="", animation=None): + super().__init__() + ## Precomp ID + self.id = id + self.animation = animation + if animation: + self.animation.assets.append(self) + + def _on_prepare_layer(self, layer): + if self.animation: + self.animation.prepare_layer(layer) + + def set_timing(self, outpoint, inpoint=0, override=True): + for layer in self.layers: + if override or layer.in_point is None: + layer.in_point = inpoint + if override or layer.out_point is None: + layer.out_point = outpoint diff --git a/lottie/objects/base.py b/lottie/objects/base.py new file mode 100644 index 0000000..14628f9 --- /dev/null +++ b/lottie/objects/base.py @@ -0,0 +1,378 @@ +import enum +import inspect +import importlib +from .nvector import NVector +from .color import Color + + +class LottieBase: + """! + Base class for Lottie JSON objects bindings + """ + def to_dict(self): + """! + Serializes into a JSON object fit for the Lottie format + """ + raise NotImplementedError + + @classmethod + def load(cls, lottiedict): + """! + Loads from a JSON object + @returns An instance of the class + """ + raise NotImplementedError + + def clone(self): + """! + Returns a copy of the object + """ + raise NotImplementedError + + +class EnumMeta(enum.EnumMeta): + """! + Hack to counter-hack the hack in enum meta + """ + def __new__(cls, name, bases, classdict): + classdict["__reduce_ex__"] = lambda *a, **kw: None # pragma: no cover + return super().__new__(cls, name, bases, classdict) + + +class LottieEnum(LottieBase, enum.Enum, metaclass=EnumMeta): + """! + Base class for enum-like types in the Lottie JSON structure + """ + def to_dict(self): + return self.value + + @classmethod + def load(cls, lottieint): + return cls(lottieint) + + def clone(self): + return self + + +class PseudoList: + """! + List tag for some weird values in the Lottie JSON + """ + pass + + +class LottieValueConverter: + """! + Factory for property types that require special conversions + """ + def __init__(self, py, lottie, name=None): + self.py = py + self.lottie = lottie + self.name = name or "%s but displayed as %s" % (self.py.__name__, self.lottie.__name__) + + def py_to_lottie(self, val): + return self.lottie(val) + + def lottie_to_py(self, val): + return self.py(val) + + @property + def __name__(self): + return self.name + + +## For values in Lottie that are bools but ints in the JSON +PseudoBool = LottieValueConverter(bool, int, "0-1 int") + + +class LottieProp: + """! + Lottie <-> Python property mapper + """ + def __init__(self, name, lottie, type=float, list=False, cond=None): + ## Name of the Python property + self.name = name + ## Name of the Lottie JSON property + self.lottie = lottie + ## Type of the property + ## @see LottieValueConverter, PseudoBool + self.type = type + ## Whether the property is a list of self.type + ## @see PseudoList + self.list = list + ## Condition on when the property is loaded from the Lottie JSON + self.cond = cond + + def get(self, obj): + """! + Returns the value of the property from a Python object + """ + return getattr(obj, self.name) + + def set(self, obj, value): + """! + Sets the value of the property from a Python object + """ + if isinstance(getattr(obj.__class__, self.name, None), property): + return + return setattr(obj, self.name, value) + + def load_from_parent(self, lottiedict): + """! + Returns the value for this property from a JSON dict representing the parent object + @returns The loaded value or @c None if the property is not in @p lottiedict + """ + if self.lottie in lottiedict: + return self.load(lottiedict[self.lottie]) + return None + + def load_into(self, lottiedict, obj): + """! + Loads from a Lottie dict into an object + """ + if self.cond and not self.cond(lottiedict): + return + self.set(obj, self.load_from_parent(lottiedict)) + + def load(self, lottieval): + """! + Loads the property from a JSON value + @returns the Python equivalent of the JSON value + """ + if self.list is PseudoList and isinstance(lottieval, list): + return self._load_scalar(lottieval[0]) + #return [ + #self._load_scalar(it) + #for it in lottieval + #] + elif self.list is True: + return list(filter(lambda x: x is not None, ( + self._load_scalar(it) + for it in lottieval + ))) + return self._load_scalar(lottieval) + + def _load_scalar(self, lottieval): + if lottieval is None: + return None + if inspect.isclass(self.type) and issubclass(self.type, LottieBase): + return self.type.load(lottieval) + elif isinstance(self.type, type) and isinstance(lottieval, self.type): + return lottieval + elif isinstance(self.type, LottieValueConverter): + return self.type.lottie_to_py(lottieval) + elif self.type is NVector: + return NVector(*lottieval) + elif self.type is Color: + return Color(*lottieval) + if isinstance(lottieval, list) and lottieval: + lottieval = lottieval[0] + return self.type(lottieval) + + def to_dict(self, obj): + """! + Converts the value of the property as from @p obj into a JSON value + @param obj LottieObject with this property + """ + val = self._basic_to_dict(self.get(obj)) + if self.list is PseudoList: + if not isinstance(obj, list): + return [val] + elif isinstance(self.type, LottieValueConverter): + val = self._basic_to_dict(self.type.py_to_lottie(val)) + return val + + def _basic_to_dict(self, v): + if isinstance(v, LottieBase): + return v.to_dict() + elif isinstance(v, NVector): + return list(map(self._basic_to_dict, v.components)) + elif isinstance(v, list): + return list(map(self._basic_to_dict, v)) + elif isinstance(v, (int, str, bool)): + return v + elif isinstance(v, float): + if v % 1 == 0: + return int(v) + return v #round(v, 3) + else: + raise Exception("Unknown value %r" % v) + + def __repr__(self): + return "" % (self.name, self.lottie) + + def clone_value(self, value): + if isinstance(value, list): + return [self.clone_value(v) for v in value] + if isinstance(value, (LottieBase, NVector)): + return value.clone() + if isinstance(value, (int, float, bool, str)) or value is None: + return value + raise Exception("Could not convert %r" % value) + + +class LottieObjectMeta(type): + def __new__(cls, name, bases, attr): + props = [] + for base in bases: + if type(base) == cls: + props += base._props + attr["_props"] = props + attr.get("_props", []) + return super().__new__(cls, name, bases, attr) + + +class LottieObject(LottieBase, metaclass=LottieObjectMeta): + """! + @brief Base class for mapping Python classes into Lottie JSON objects + """ + def to_dict(self): + return { + prop.lottie: prop.to_dict(self) + for prop in self._props + if prop.get(self) is not None + } + + @classmethod + def load(cls, lottiedict): + if "__pyclass" in lottiedict: + return CustomObject.load(lottiedict) + if not lottiedict: + return None + cls = cls._load_get_class(lottiedict) + obj = cls() + for prop in cls._props: + prop.load_into(lottiedict, obj) + return obj + + @classmethod + def _load_get_class(cls, lottiedict): + return cls + + def find(self, search, propname="name"): + """! + @param search The value of the property to search + @param propname The name of the property used to search + @brief Recursively searches for child objects with a matching property + """ + if getattr(self, propname, None) == search: + return self + for prop in self._props: + v = prop.get(self) + if isinstance(v, LottieObject): + found = v.find(search, propname) + if found: + return found + elif isinstance(v, list) and v and isinstance(v[0], LottieObject): + for obj in v: + found = obj.find(search, propname) + if found: + return found + return None + + def find_all(self, type, predicate=None, include_self=True): + """! + Find all child objects that match a predicate + @param type Type (or tuple of types) of the objects to match + @param predicate Function that returns true on the objects to find + @param include_self Whether should counsider `self` for a potential match + """ + + if isinstance(self, type) and include_self: + if not predicate or predicate(self): + yield self + + for prop in self._props: + v = prop.get(self) + + if isinstance(v, LottieObject): + for found in v.find_all(type, predicate, True): + yield found + elif isinstance(v, list) and v and isinstance(v[0], LottieObject): + for child in v: + for found in child.find_all(type, predicate, True): + yield found + + def clone(self): + obj = self.__class__() + for prop in self._props: + v = prop.get(self) + prop.set(obj, prop.clone_value(v)) + return obj + + def __str__(self): + return type(self).__name__ + + +class Index: + """! + @brief Simple iterator to generate increasing integers + """ + def __init__(self): + self._i = -1 + + def __next__(self): + self._i += 1 + return self._i + + +class CustomObject(LottieObject): + """! + Allows extending the Lottie shapes with custom Python classes + """ + wrapped_lottie = LottieObject + + def __init__(self): + self.wrapped = self.wrapped_lottie() + + @classmethod + def load(cls, lottiedict): + ld = lottiedict.copy() + classname = ld.pop("__pyclass") + modn, clsn = classname.rsplit(".", 1) + subcls = getattr(importlib.import_module(modn), clsn) + obj = subcls() + for prop in subcls._props: + prop.load_into(lottiedict, obj) + obj.wrapped = subcls.wrapped_lottie.load(ld) + return obj + + def clone(self): + obj = self.__class__(**self.to_pyctor()) + obj.wrapped = self.wrapped.clone() + return obj + + def to_dict(self): + dict = self.wrapped.to_dict() + dict["__pyclass"] = "{0.__module__}.{0.__name__}".format(self.__class__) + dict.update(LottieObject.to_dict(self)) + return dict + + def _build_wrapped(self): + return self.wrapped_lottie() + + def refresh(self): + self.wrapped = self._build_wrapped() + + +class ObjectVisitor: + DONT_RECURSE = object() + + def __call__(self, lottie_object): + self._process(lottie_object) + + def _process(self, lottie_object): + self.visit(lottie_object) + for p in lottie_object._props: + pval = p.get(lottie_object) + if self.visit_property(lottie_object, p, pval) is not self.DONT_RECURSE: + if isinstance(pval, LottieObject): + self._process(pval) + elif isinstance(pval, list) and pval and isinstance(pval[0], LottieObject): + for c in pval: + self._process(c) + + def visit(self, object): + pass + + def visit_property(self, object, property, value): + pass diff --git a/lottie/objects/bezier.py b/lottie/objects/bezier.py new file mode 100644 index 0000000..57e8f9b --- /dev/null +++ b/lottie/objects/bezier.py @@ -0,0 +1,485 @@ +import math +from .base import LottieObject, LottieProp +from .nvector import NVector + + +class BezierPoint: + def __init__(self, vertex, in_tangent=None, out_tangent=None): + self.vertex = vertex + self.in_tangent = in_tangent or NVector(0, 0) + self.out_tangent = out_tangent or NVector(0, 0) + + def relative(self): + return self + + @classmethod + def smooth(cls, point, in_tangent): + return cls(point, in_tangent, -in_tangent) + + @classmethod + def from_absolute(cls, point, in_tangent=None, out_tangent=None): + if not in_tangent: + in_tangent = point.clone() + if not out_tangent: + out_tangent = point.clone() + return BezierPoint(point, in_tangent, out_tangent) + + +class BezierPointView: + """ + View for bezier point + """ + def __init__(self, bezier, index): + self.bezier = bezier + self.index = index + + @property + def vertex(self): + return self.bezier.vertices[self.index] + + @vertex.setter + def vertex(self, point): + self.bezier.vertices[self.index] = point + + @property + def in_tangent(self): + return self.bezier.in_tangents[self.index] + + @in_tangent.setter + def in_tangent(self, point): + self.bezier.in_tangents[self.index] = point + + @property + def out_tangent(self): + return self.bezier.out_tangents[self.index] + + @out_tangent.setter + def out_tangent(self, point): + self.bezier.out_tangents[self.index] = point + + def relative(self): + return self + + +class AbsoluteBezierPointView(BezierPointView): + @property + def in_tangent(self): + return self.bezier.in_tangents[self.index] + self.vertex + + @in_tangent.setter + def in_tangent(self, point): + self.bezier.in_tangents[self.index] = point - self.vertex + + @property + def out_tangent(self): + return self.bezier.out_tangents[self.index] + self.vertex + + @out_tangent.setter + def out_tangent(self, point): + self.bezier.out_tangents[self.index] = point - self.vertex + + def relative(self): + return BezierPointView(self.bezier, self.index) + + +class BezierView: + def __init__(self, bezier, absolute=False): + self.bezier = bezier + self.is_absolute = absolute + + def point(self, index): + if self.is_absolute: + return AbsoluteBezierPointView(self.bezier, index) + return BezierPointView(self.bezier, index) + + def __len__(self): + return len(self.bezier.vertices) + + def __getitem__(self, key): + if isinstance(key, slice): + return [ + self.point(i) + for i in key + ] + return self.point(key) + + def __iter__(self): + for i in range(len(self)): + yield self.point(i) + + def append(self, point): + if isinstance(point, NVector): + self.bezier.add_point(point.clone()) + else: + bpt = point.relative() + self.bezier.add_point(bpt.vertex.clone(), bpt.in_tangent.clone(), bpt.out_tangent.clone()) + + @property + def absolute(self): + return BezierView(self.bezier, True) + + +## @ingroup Lottie +class Bezier(LottieObject): + """! + Single bezier curve + """ + _props = [ + LottieProp("closed", "c", bool, False), + LottieProp("in_tangents", "i", NVector, True), + LottieProp("out_tangents", "o", NVector, True), + LottieProp("vertices", "v", NVector, True), + ] + + def __init__(self): + ## Closed property of shape + self.closed = False + ## Cubic bezier handles for the segments before each vertex + self.in_tangents = [] + ## Cubic bezier handles for the segments after each vertex + self.out_tangents = [] + ## Bezier curve vertices. + self.vertices = [] + #self.rel_tangents = rel_tangents + ## More convent way to access points + self.points = BezierView(self) + + def clone(self): + clone = Bezier() + clone.closed = self.closed + clone.in_tangents = [p.clone() for p in self.in_tangents] + clone.out_tangents = [p.clone() for p in self.out_tangents] + clone.vertices = [p.clone() for p in self.vertices] + #clone.rel_tangents = self.rel_tangents + return clone + + def insert_point(self, index, pos, inp=NVector(0, 0), outp=NVector(0, 0)): + """! + Inserts a point at the given index + @param index Index to insert the point at + @param pos Point to add + @param inp Tangent entering the point, as a vector relative to @p pos + @param outp Tangent exiting the point, as a vector relative to @p pos + @returns @c self, for easy chaining + """ + self.vertices.insert(index, pos) + self.in_tangents.insert(index, inp.clone()) + self.out_tangents.insert(index, outp.clone()) + #if not self.rel_tangents: + #self.in_tangents[-1] += pos + #self.out_tangents[-1] += pos + return self + + def add_point(self, pos, inp=NVector(0, 0), outp=NVector(0, 0)): + """! + Appends a point to the curve + @see insert_point + """ + self.insert_point(len(self.vertices), pos, inp, outp) + return self + + def add_smooth_point(self, pos, inp): + """! + Appends a point with symmetrical tangents + @see insert_point + """ + self.add_point(pos, inp, -inp) + return self + + def close(self, closed=True): + """! + Updates self.closed + @returns @c self, for easy chaining + """ + self.closed = closed + return self + + def point_at(self, t): + """! + @param t A value between 0 and 1, percentage along the length of the curve + @returns The point at @p t in the curve + """ + i, t = self._index_t(t) + points = self._bezier_points(i, True) + return self._solve_bezier(t, points) + + def tangent_angle_at(self, t): + i, t = self._index_t(t) + points = self._bezier_points(i, True) + + n = len(points) - 1 + if n > 0: + delta = sum(( + (points[i+1] - points[i]) * n * self._solve_bezier_coeff(i, n - 1, t) + for i in range(n) + ), NVector(0, 0)) + return math.atan2(delta.y, delta.x) + + return 0 + + def _split(self, t): + i, t = self._index_t(t) + cub = self._bezier_points(i, True) + split1, split2 = self._split_segment(t, cub) + return i, split1, split2 + + def _split_segment(self, t, cub): + if len(cub) == 2: + k = self._solve_bezier_step(t, cub)[0] + split1 = [cub[0], NVector(0, 0), NVector(0, 0), k] + split2 = [k, NVector(0, 0), NVector(0, 0), cub[-1]] + return split1, split2 + + if len(cub) == 3: + quad = cub + else: + quad = self._solve_bezier_step(t, cub) + lin = self._solve_bezier_step(t, quad) + k = self._solve_bezier_step(t, lin)[0] + split1 = [cub[0], quad[0]-cub[0], lin[0]-k, k] + split2 = [k, lin[-1]-k, quad[-1]-cub[-1], cub[-1]] + return split1, split2 + + def split_at(self, t): + """! + Get two pieces out of a Bezier curve + @param t A value between 0 and 1, percentage along the length of the curve + @returns Two Bezier objects that correspond to self, but split at @p t + """ + i, split1, split2 = self._split(t) + + seg1 = Bezier() + seg2 = Bezier() + for j in range(i): + seg1.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone()) + for j in range(i+2, len(self.vertices)): + seg2.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone()) + + seg1.add_point(split1[0], self.in_tangents[i].clone(), split1[1]) + seg1.add_point(split1[3], split1[2], split2[1]) + + seg2.insert_point(0, split2[0], split1[2], split2[1]) + seg2.insert_point(1, split2[3], split2[2], self.out_tangents[i+1].clone()) + + return seg1, seg2 + + def segment(self, t1, t2): + """! + Splits a Bezier in two points and returns the segment between the + @param t1 A value between 0 and 1, percentage along the length of the curve + @param t2 A value between 0 and 1, percentage along the length of the curve + @returns Bezier object that correspond to the segment between @p t1 and @p t2 + """ + if self.closed and self.vertices and self.vertices[-1] != self.vertices[0]: + copy = self.clone() + copy.add_point(self.vertices[0]) + copy.closed = False + return copy.segment(t1, t2) + + if t1 > 1: + t1 = 1 + if t2 > 1: + t2 = 1 + + if t1 > t2: + t1, t2 = t2, t1 + elif t1 == t2: + seg = Bezier() + p = self.point_at(t1) + seg.add_point(p) + seg.add_point(p) + return seg + + seg1, seg2 = self.split_at(t1) + t2p = (t2-t1) / (1-t1) + seg3, seg4 = seg2.split_at(t2p) + return seg3 + + def split_self_multi(self, positions): + """! + Adds more points to the Bezier + @param positions list of percentages along the curve + """ + if not len(positions): + return + t1 = positions[0] + seg1, seg2 = self.split_at(t1) + self.vertices = [] + self.in_tangents = [] + self.out_tangents = [] + + self.vertices = seg1.vertices[:-1] + self.in_tangents = seg1.in_tangents[:-1] + self.out_tangents = seg1.out_tangents[:-1] + + for t2 in positions[1:]: + t = (t2-t1) / (1-t1) + seg1, seg2 = seg2.split_at(t) + t1 = t + self.vertices += seg1.vertices[:-1] + self.in_tangents += seg1.in_tangents[:-1] + self.out_tangents += seg1.out_tangents[:-1] + + self.vertices += seg2.vertices + self.in_tangents += seg2.in_tangents + self.out_tangents += seg2.out_tangents + + def split_each_segment(self): + """! + Adds a point in the middle of the segment between every pair of points in the Bezier + """ + vertices = self.vertices + in_tangents = self.in_tangents + out_tangents = self.out_tangents + + self.vertices = [] + self.in_tangents = [] + self.out_tangents = [] + + for i in range(len(vertices)-1): + tocut = [vertices[i], out_tangents[i]+vertices[i], in_tangents[i+1]+vertices[i+1], vertices[i+1]] + split1, split2 = self._split_segment(0.5, tocut) + if i: + self.out_tangents[-1] = split1[1] + else: + self.add_point(vertices[0], in_tangents[0], split1[1]) + self.add_point(split1[3], split1[2], split2[1]) + self.add_point(vertices[i+1], split2[2], NVector(0, 0)) + + def split_self_chunks(self, n_chunks): + """! + Adds points the Bezier, splitting it into @p n_chunks additional chunks. + """ + splits = [i/n_chunks for i in range(1, n_chunks)] + return self.split_self_multi(splits) + + def _bezier_points(self, i, optimize): + v1 = self.vertices[i].clone() + v2 = self.vertices[i+1].clone() + points = [v1] + t1 = self.out_tangents[i].clone() + if not optimize or t1.length != 0: + points.append(t1+v1) + t2 = self.in_tangents[i+1].clone() + if not optimize or t1.length != 0: + points.append(t2+v2) + points.append(v2) + return points + + def _solve_bezier_step(self, t, points): + next = [] + p1 = points[0] + for p2 in points[1:]: + next.append(p1 * (1-t) + p2 * t) + p1 = p2 + return next + + def _solve_bezier_coeff(self, i, n, t): + return ( + math.factorial(n) / (math.factorial(i) * math.factorial(n - i)) # (n choose i) + * (t ** i) * ((1 - t) ** (n-i)) + ) + + def _solve_bezier(self, t, points): + n = len(points) - 1 + if n > 0: + return sum(( + points[i] * self._solve_bezier_coeff(i, n, t) + for i in range(n+1) + ), NVector(0, 0)) + + #while len(points) > 1: + #points = self._solve_bezier_step(t, points) + return points[0] + + def _index_t(self, t): + if t <= 0: + return 0, 0 + + if t >= 1: + return len(self.vertices)-2, 1 + + n = len(self.vertices)-1 + for i in range(n): + if (i+1) / n > t: + break + + return i, (t - (i/n)) * n + + def reverse(self): + """! + Reverses the Bezier curve + """ + self.vertices = list(reversed(self.vertices)) + out_tangents = list(reversed(self.in_tangents)) + in_tangents = list(reversed(self.out_tangents)) + self.in_tangents = in_tangents + self.out_tangents = out_tangents + + """def to_absolute(self): + if self.rel_tangents: + self.rel_tangents = False + for i in range(len(self.vertices)): + p = self.vertices[i] + self.in_tangents[i] += p + self.out_tangents[i] += p + return self""" + + def rounded(self, round_distance): + cloned = Bezier() + cloned.closed = self.closed + round_corner = 0.5519 + + def _get_vt(closest_index): + closer_v = self.vertices[closest_index] + distance = (current - closer_v).length + new_pos_perc = min(distance/2, round_distance) / distance if distance else 0 + vert = current + (closer_v - current) * new_pos_perc + tan = - (vert - current) * round_corner + return vert, tan + + for i, current in enumerate(self.vertices): + if not self.closed and (i == 0 or i == len(self.points) - 1): + cloned.points.append(self.points[i]) + else: + vert1, out_t = _get_vt(i - 1) + cloned.add_point(vert1, NVector(0, 0), out_t) + vert2, in_t = _get_vt((i+1) % len(self.points)) + cloned.add_point(vert2, in_t, NVector(0, 0)) + + return cloned + + def scale(self, amount): + for vl in (self.vertices, self.in_tangents, self.out_tangents): + for v in vl: + v *= amount + + def lerp(self, other, t): + if len(other.vertices) != len(self.vertices): + if t < 1: + return self.clone() + return other.clone() + + bez = Bezier() + bez.closed = self.closed + + for vlist_name in ["vertices", "in_tangents", "out_tangents"]: + vlist = getattr(self, vlist_name) + olist = getattr(other, vlist_name) + out = getattr(bez, vlist_name) + for v, o in zip(vlist, olist): + out.append(v.lerp(o, t)) + + return bez + + def rough_length(self): + if len(self.vertices) < 2: + return 0 + last = self.vertices[0] + length = 0 + for v in self.vertices[1:]: + length += (v-last).length + last = v + if self.closed: + length += (last-self.vertices[0]).length + return length diff --git a/lottie/objects/color.py b/lottie/objects/color.py new file mode 100644 index 0000000..f88361a --- /dev/null +++ b/lottie/objects/color.py @@ -0,0 +1,447 @@ +import enum +import math +import colorsys +from .nvector import NVector + + +def from_uint8(r, g, b, a=255): + return Color(r, g, b, a) / 255 + + +class ColorMode(enum.Enum): + ## sRGB, Components in [0, 1] + RGB = enum.auto() + ## HSV, components in [0, 1] + HSV = enum.auto() + ## HSL, components in [0, 1] + HSL = enum.auto() + ## CIE XYZ with Illuminant D65. Components in [0, 1] + XYZ = enum.auto() + ## CIE L*u*v* + LUV = enum.auto() + ## CIE Lch(uv), polar version of LUV where C is the radius and H an angle in radians + LCH_uv = enum.auto() + ## CIE L*a*b* + LAB = enum.auto() + ## CIE LCh(ab), polar version of LAB where C is the radius and H an angle in radians + #LCH_ab = enum.auto() + + +def _clamp(x): + return max(0, min(1, x)) + + +class Conversion: + _conv_paths = { + (ColorMode.RGB, ColorMode.RGB): [], + (ColorMode.RGB, ColorMode.HSV): [], + (ColorMode.RGB, ColorMode.HSL): [], + (ColorMode.RGB, ColorMode.XYZ): [], + (ColorMode.RGB, ColorMode.LUV): [ColorMode.XYZ], + (ColorMode.RGB, ColorMode.LAB): [ColorMode.XYZ], + (ColorMode.RGB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.RGB, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.HSV, ColorMode.RGB): [], + (ColorMode.HSV, ColorMode.HSV): [], + (ColorMode.HSV, ColorMode.HSL): [], + (ColorMode.HSV, ColorMode.XYZ): [ColorMode.RGB], + (ColorMode.HSV, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSV, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSV, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.HSV, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.HSL, ColorMode.RGB): [], + (ColorMode.HSL, ColorMode.HSV): [], + (ColorMode.HSL, ColorMode.HSL): [], + (ColorMode.HSL, ColorMode.XYZ): [ColorMode.RGB], + (ColorMode.HSL, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSL, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSL, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.HSL, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.XYZ, ColorMode.RGB): [], + (ColorMode.XYZ, ColorMode.HSV): [ColorMode.RGB], + (ColorMode.XYZ, ColorMode.HSL): [ColorMode.RGB], + (ColorMode.XYZ, ColorMode.XYZ): [], + (ColorMode.XYZ, ColorMode.LUV): [], + (ColorMode.XYZ, ColorMode.LAB): [], + (ColorMode.XYZ, ColorMode.LCH_uv): [ColorMode.LUV], + #(ColorMode.XYZ, ColorMode.LCH_ab): [ColorMode.LAB], + + (ColorMode.LCH_uv, ColorMode.RGB): [ColorMode.LUV, ColorMode.XYZ], + (ColorMode.LCH_uv, ColorMode.HSV): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LCH_uv, ColorMode.HSL): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LCH_uv, ColorMode.XYZ): [ColorMode.LUV], + (ColorMode.LCH_uv, ColorMode.LUV): [], + (ColorMode.LCH_uv, ColorMode.LAB): [ColorMode.LUV, ColorMode.XYZ], + (ColorMode.LCH_uv, ColorMode.LCH_uv): [], + #(ColorMode.LCH_uv, ColorMode.LCH_ab): [ColorMode.LUV, ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.LUV, ColorMode.RGB): [ColorMode.XYZ], + (ColorMode.LUV, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LUV, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LUV, ColorMode.XYZ): [], + (ColorMode.LUV, ColorMode.LUV): [], + (ColorMode.LUV, ColorMode.LAB): [ColorMode.XYZ], + (ColorMode.LUV, ColorMode.LCH_uv): [], + #(ColorMode.LUV, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.LAB, ColorMode.RGB): [ColorMode.XYZ], + (ColorMode.LAB, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LAB, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LAB, ColorMode.XYZ): [], + (ColorMode.LAB, ColorMode.LUV): [ColorMode.XYZ], + (ColorMode.LAB, ColorMode.LAB): [], + (ColorMode.LAB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.LAB, ColorMode.LCH_ab): [], + + #(ColorMode.LCH_ab, ColorMode.RGB): [ColorMode.LAB, ColorMode.XYZ], + #(ColorMode.LCH_ab, ColorMode.HSV): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB], + #(ColorMode.LCH_ab, ColorMode.HSL): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB], + #(ColorMode.LCH_ab, ColorMode.XYZ): [ColorMode.LAB], + #(ColorMode.LCH_ab, ColorMode.LUV): [ColorMode.LAB, ColorMode.XYZ], + #(ColorMode.LCH_ab, ColorMode.LAB): [], + #(ColorMode.LCH_ab, ColorMode.LCH_uv): [ColorMode.LAB, ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.LCH_ab, ColorMode.LCH_ab): [], + } + + @staticmethod + def rgb_to_hsv(r, g, b): + return colorsys.rgb_to_hsv(r, g, b) + + @staticmethod + def hsv_to_rgb(r, g, b): + return colorsys.hsv_to_rgb(r, g, b) + + @staticmethod + def hsl_to_hsv(h, s_hsl, l): + v = l + s_hsl * min(l, 1 - l) + s_hsv = 0 if v == 0 else 2 - 2 * l / v + return (h, s_hsv, v) + + @staticmethod + def hsv_to_hsl(h, s_hsv, v): + l = v - v * s_hsv / 2 + s_hsl = 0 if l in (0, 1) else (v - l) / min(l, 1 - l) + return (h, s_hsl, l) + + @staticmethod + def rgb_to_hsl(r, g, b): + h, l, s = colorsys.rgb_to_hls(r, g, b) + return (h, s, l) + + @staticmethod + def hsl_to_rgb(h, s, l): + return colorsys.hls_to_rgb(h, l, s) + + # http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip + #@staticmethod + #def rgb_to_hcl(r, g, b, gamma=3, y0=100): + #maxc = max(r, g, b) + #minc = min(r, g, b) + #if maxc > 0: + #alpha = 1/y0 * minc / maxc + #else: + #alpha = 0 + #q = math.e ** (alpha * gamma) + #h = math.atan2(g - b, r - g) + #if h < 0: + #h += 2*math.pi + #h /= 2*math.pi + #c = q / 3 * (abs(r-g) + abs(g-b) + abs(b-r)) + #l = (q * maxc + (q-1) * minc) / 2 + #return (h, c, l) + + #@staticmethod + #def hcl_to_rgb(h, c, l, gamma=3, y0=100): + #h *= 2*math.pi + + #q = math.e ** ((1 - 2*c / 4*l) * gamma / y0) + #minc = (4*l - 3*c) / (4*q - 2) + #maxc = minc + 3*c / 2*q + + #if h <= math.pi * 1 / 3: + #tan = math.tan(3/2*h) + #r = maxc + #b = minc + #g = (r * tan + b) / (1 + tan) + #elif h <= math.pi * 2 / 3: + #tan = math.tan(3/4*(h-math.pi)) + #g = maxc + #b = minc + #r = (g * (1+tan) - b) / tan + #elif h <= math.pi * 3 / 3: + #tan = math.tan(3/4*(h-math.pi)) + #g = maxc + #r = minc + #b = g * (1+tan) - r * tan + #elif h <= math.pi * 4 / 3: + #tan = math.tan(3/2*(h+math.pi)) + #b = maxc + #r = minc + #g = (r * tan + b) / (1 + tan) + #elif h <= math.pi * 5 / 3: + #tan = math.tan(3/4*h) + #b = maxc + #g = minc + #r = (g * (1+tan) - b) / tan + #else: + #tan = math.tan(3/4*h) + #r = maxc + #g = minc + #b = g * (1+tan) - r * tan + + #return _clamp(r), _clamp(g), _clamp(b) + + @staticmethod + def rgb_to_xyz(r, g, b): + def _gamma(v): + return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 + rgb = (_gamma(r), _gamma(g), _gamma(b)) + matrix = [ + [0.4124564, 0.3575761, 0.1804375], + [0.2126729, 0.7151522, 0.0721750], + [0.0193339, 0.1191920, 0.9503041], + ] + return tuple( + sum(rgb[i] * c for i, c in enumerate(row)) + for row in matrix + ) + + @staticmethod + def xyz_to_rgb(x, y, z): + def _gamma1(v): + return _clamp(v * 12.92 if v <= 0.0031308 else v ** (1/2.4) * 1.055 - 0.055) + matrix = [ + [+3.2404542, -1.5371385, -0.4985314], + [-0.9692660, +1.8760108, +0.0415560], + [+0.0556434, -0.2040259, +1.0572252], + ] + xyz = (x, y, z) + return tuple(map(_gamma1, ( + sum(xyz[i] * c for i, c in enumerate(row)) + for row in matrix + ))) + + @staticmethod + def xyz_to_luv(x, y, z): + u1r = 0.2009 + v1r = 0.4610 + yr = 100 + + kap = (29/3)**3 + eps = (6/29)**3 + + try: + u1 = 4*x / (x + 15*y + 3*z) + v1 = 9*y / (x + 15*y + 3*z) + except ZeroDivisionError: + return 0, 0, 0 + + y_r = y/yr + l = 166 * y_r ** (1/3) - 16 if y_r > eps else kap * y_r + u = 13 * l * (u1 - u1r) + v = 13 * l * (v1 - v1r) + return l, u, v + + @staticmethod + def luv_to_xyz(l, u, v): + u1r = 0.2009 + v1r = 0.4610 + yr = 100 + + kap = (29/3)**3 + + if l == 0: + u1 = u1r + v1 = v1r + else: + u1 = u / (13 * l) + u1r + v1 = v / (13 * l) + v1r + + y = yr * l / kap if l <= 8 else yr * ((l + 16) / 116) ** 3 + x = y * 9*u1 / (4*v1) + z = y * (12 - 3*u1 - 20*v1) / (4*v1) + return x, y, z + + @staticmethod + def luv_to_lch_uv(l, u, v): + c = math.hypot(u, v) + h = math.atan2(v, u) + if h < 0: + h += math.tau + return l, c, h + + @staticmethod + def lch_uv_to_luv(l, c, h): + u = math.cos(h) * c + v = math.sin(h) * c + return l, u, v + + @staticmethod + def xyz_to_lab(x, y, z): + # D65 Illuminant aka sRGB(1,1,1) + xn = 0.950489 + yn = 1 + zn = 108.8840 + + delta = 6 / 29 + + def f(t): + return t ** (1/3) if t > delta ** 3 else t / (3*delta**2) + 4/29 + + fy = f(y/yn) + l = 116 * fy - 16 + a = 500 * (f(x/xn) - fy) + b = 200 * (fy - f(z/zn)) + + return l, a, b + + @staticmethod + def lab_to_xyz(l, a, b): + # D65 Illuminant aka sRGB(1,1,1) + xn = 0.950489 + yn = 1 + zn = 108.8840 + + delta = 6 / 29 + + def f1(t): + return t**3 if t > delta else 3*delta**2*(t-4/29) + + l1 = (l+16) / 116 + x = xn * f1(l1+a/500) + y = yn * f1(l1) + z = zn * f1(l1-b/200) + + return x, y, z + + #@staticmethod + #def lab_to_lch_ab(l, a, b): + #c = math.hypot(a, b) + #h = math.atan2(b, a) + #if h < 0: + #h += math.tau + #return l, c, h + + #@staticmethod + #def lch_ab_to_lab(l, c, h): + #a = math.cos(h) * c + #b = math.sin(h) * c + #return l, a, b + + @staticmethod + def conv_func(mode_from, mode_to): + return getattr(Conversion, "%s_to_%s" % (mode_from.name.lower(), mode_to.name.lower()), None) + + @staticmethod + def convert(tuple, mode_from, mode_to): + if mode_from == mode_to: + return tuple + + if len(tuple) == 4: + alpha = tuple[3] + tuple = tuple[:3] + else: + alpha = None + + func = Conversion.conv_func(mode_from, mode_to) + if func: + return func(*tuple) + + if (mode_from, mode_to) in Conversion._conv_paths: + steps = Conversion._conv_paths[(mode_from, mode_to)] + [mode_to] + for step in steps: + func = Conversion.conv_func(mode_from, step) + if not func: + raise ValueError("Missing definition for conversion from %s to %s" % (mode_from, step)) + tuple = func(*tuple) + mode_from = step + if alpha is not None: + tuple += (alpha,) + return tuple + + raise ValueError("No conversion path from %s to %s" % (mode_from, mode_to)) + + +class Color(NVector): + Mode = ColorMode + + def __init__(self, c1=0, c2=0, c3=0, a=1, *, mode=ColorMode.RGB): + if isinstance(a, ColorMode): + raise TypeError("Please update the Color constructor") + super().__init__(c1, c2, c3, a) + self._mode = mode + + @property + def mode(self): + return self._mode + + def convert(self, v): + if v == self._mode: + return self + + self.components = list(Conversion.convert(self.components, self._mode, v)) + + self._mode = v + return self + + def clone(self): + return Color(*self.components, mode=self._mode) + + def converted(self, mode): + return self.clone().convert(mode) + + def to_rgb(self): + return self.converted(ColorMode.RGB) + + def __repr__(self): + return "<%s %s [%.3f, %.3f, %.3f, %.3f]>" % ( + (self.__class__.__name__, self.mode.name) + tuple(self.components) + ) + + def component_names(self): + comps = None + + if self._mode == ColorMode.RGB: + comps = ({"r", "red"}, {"g", "green"}, {"b", "blue"}) + elif self._mode == ColorMode.HSV: + comps = ({"h", "hue"}, {"s", "saturation"}, {"v", "value"}) + elif self._mode == ColorMode.HSL: + comps = ({"h", "hue"}, {"s", "saturation"}, {"l", "lightness"}) + elif self._mode == ColorMode.LCH_uv: # in (ColorMode.LCH_uv, ColorMode.LCH_ab): + comps = ({"l", "luma", "luminance"}, {"c", "choma"}, {"h", "hue"}) + elif self._mode == ColorMode.XYZ: + comps = "xyz" + elif self._mode == ColorMode.LUV: + comps = "luv" + elif self._mode == ColorMode.LAB: + comps = "lab" + + return comps + + def _attrindex(self, name): + comps = self.component_names() + + if comps: + for i, vals in enumerate(comps): + if name in vals: + return i + + return None + + def __getattr__(self, name): + if name not in vars(self) and name not in {"_mode", "components"}: + i = self._attrindex(name) + if i is not None: + return self.components[i] + raise AttributeError(name) + + def __setattr__(self, name, value): + if name not in vars(self) and name not in {"_mode", "components"}: + i = self._attrindex(name) + if i is not None: + self.components[i] = value + return + return super().__setattr__(name, value) diff --git a/lottie/objects/composition.py b/lottie/objects/composition.py new file mode 100644 index 0000000..c6903ed --- /dev/null +++ b/lottie/objects/composition.py @@ -0,0 +1,80 @@ +from .base import LottieObject, Index, LottieProp +from .layers import Layer + + +## @ingroup Lottie +class Composition(LottieObject): + """! + Base class for layer holders + """ + _props = [ + LottieProp("layers", "layers", Layer, True), + ] + + def __init__(self): + ## List of Composition Layers + self.layers = [] # ShapeLayer, SolidLayer, CompLayer, ImageLayer, NullLayer, TextLayer + + self._index_gen = Index() + + def layer(self, index): + for layer in self.layers: + if layer.index == index: + return layer + raise IndexError("No layer %s" % index) + + def add_layer(self, layer: Layer): + """! + @brief Appends a layer to the composition + @see insert_layer + """ + return self.insert_layer(len(self.layers), layer) + + @classmethod + def load(cls, lottiedict): + obj = super().load(lottiedict) + obj._fixup() + return obj + + def _fixup(self): + for layer in self.layers: + layer.composition = self + + def insert_layer(self, index, layer: Layer): + """! + @brief Inserts a layer to the composition + @note Layers added first will be rendered on top of later layers + """ + self.layers.insert(index, layer) + self.prepare_layer(layer) + return layer + + def prepare_layer(self, layer: Layer): + layer.composition = self + if layer.index is None: + layer.index = next(self._index_gen) + self._on_prepare_layer(layer) + + def _on_prepare_layer(self, layer): + raise NotImplementedError + + def clone(self): + c = super().clone() + c._index_gen._i = self._index_gen._i + return c + + def remove_layer(self, layer: Layer): + """! + @brief Removes a layer (and all of its children) from this composition + @param layer Layer to be removed + """ + if layer.composition is not self: + return + + children = list(layer.children) + + layer.composition = None + self.layers.remove(layer) + + for c in children: + self.remove_layer(c) diff --git a/lottie/objects/convert_lottie_512.py b/lottie/objects/convert_lottie_512.py new file mode 100644 index 0000000..670947d --- /dev/null +++ b/lottie/objects/convert_lottie_512.py @@ -0,0 +1,9943 @@ +import torch +import re +import json +from typing import Union, List, Dict, Tuple, Optional, Any +import difflib +from .test2_0518 import * +import numpy as np +#from test_0819 import * + + + +class LottieTensor: + # Command type constants (ę·»åŠ ę–°ēš„å‘½ä»¤åøøé‡) + tokenizer = None + CMD_ANIMATION = 0 + CMD_LAYER = 1 + CMD_TRANSFORM = 2 + CMD_POSITION = 3 + CMD_KEYFRAME = 4 + CMD_POSITION_END = 5 + CMD_SCALE = 6 + CMD_SCALE_END = 7 + CMD_ROTATION = 8 + CMD_OPACITY = 9 + CMD_OPACITY_END = 10 + CMD_ANCHOR = 11 + CMD_GROUP = 12 + CMD_GROUP_END = 13 + CMD_TRANSFORM_SHAPE = 14 + CMD_PATH = 15 + CMD_PATH_END = 16 + CMD_POINT = 17 + CMD_FILL = 18 + CMD_GRADIENT_FILL = 19 + CMD_GRADIENT_FILL_END = 20 + CMD_START_POINT = 21 + CMD_END_POINT = 22 + CMD_GRADIENT_TYPE = 23 + CMD_HIGHLIGHT_LENGTH = 24 + CMD_HIGHLIGHT_ANGLE = 25 + CMD_TRANSFORM_END = 26 + CMD_LAYER_END = 27 + CMD_PAD = 28 + CMD_EOS = 29 + CMD_SOS = 30 + CMD_RECT = 31 + CMD_RECT_END = 32 + CMD_SIZE = 33 + CMD_ROUNDED = 34 + CMD_ELLIPSE = 35 + CMD_ELLIPSE_END = 36 + CMD_STROKE = 37 + CMD_SKEW = 38 + CMD_SKEW_AXIS = 39 + CMD_ASSET = 40 + CMD_ASSET_END = 41 + CMD_PARENT = 42 + CMD_NULL_LAYER = 43 + CMD_NULL_LAYER_END = 44 + CMD_PRECOMP_LAYER = 45 + CMD_PRECOMP_LAYER_END = 46 + CMD_REFERENCE_ID = 47 + CMD_DIMENSIONS = 48 + CMD_ROTATION_END = 49 + CMD_STAR = 50 + CMD_STAR_END = 51 + CMD_INNER_RADIUS = 52 + CMD_OUTER_RADIUS = 53 + CMD_INNER_ROUNDNESS = 54 + CMD_OUTER_ROUNDNESS = 55 + CMD_POINTS = 56 + CMD_STAR_ROTATION = 57 + CMD_TRIM = 58 + CMD_TRIM_END = 59 + CMD_START = 60 + CMD_END = 61 + CMD_OFFSET = 62 + CMD_MULTIPLE = 63 + CMD_REPEATER = 64 + CMD_REPEATER_END = 65 + CMD_COPIES = 66 + CMD_REPEATER_OFFSET = 67 + CMD_COMPOSITE = 68 + CMD_REPEATER_TRANSFORM = 69 + CMD_REPEATER_TRANSFORM_END = 70 + CMD_GRADIENT_STROKE = 71 + CMD_GRADIENT_STROKE_END = 72 + CMD_WIDTH = 73 + CMD_LINE_CAP = 74 + CMD_LINE_JOIN = 75 + CMD_MITER_LIMIT = 76 + CMD_MERGE = 77 + CMD_MERGE_END = 78 + CMD_MERGE_MODE = 79 + CMD_ROUNDED_CORNERS = 80 + CMD_ROUNDED_CORNERS_END = 81 + CMD_RADIUS = 82 + CMD_TWIST = 83 + CMD_TWIST_END = 84 + CMD_ANGLE = 85 + CMD_CENTER = 86 + CMD_BEZIER = 87 + CMD_BEZIER_END = 88 + CMD_TEXT_LAYER = 89 + CMD_TEXT_LAYER_END = 90 + CMD_TEXT_DATA = 91 + CMD_TEXT_DATA_END = 92 + CMD_DOCUMENT = 93 + CMD_SOLID_LAYER = 94 + CMD_SOLID_LAYER_END = 95 + CMD_POSITION_X = 96 + CMD_POSITION_Y = 97 + CMD_POSITION_Z = 98 + CMD_POSITION_X_END = 99 + CMD_POSITION_Y_END = 100 + CMD_POSITION_Z_END = 101 + CMD_SCALE_X = 102 + CMD_SCALE_Y = 103 + CMD_SCALE_Z = 104 + CMD_SCALE_X_END = 105 + CMD_SCALE_Y_END = 106 + CMD_SCALE_Z_END = 107 + CMD_ROTATION_X = 108 + CMD_ROTATION_Y = 109 + CMD_ROTATION_Z = 110 + CMD_ROTATION_X_END = 111 + CMD_ROTATION_Y_END = 112 + CMD_ROTATION_Z_END = 113 + CMD_EFFECTS = 114 + CMD_EFFECTS_END = 115 + CMD_EFFECT = 116 + CMD_EFFECT_END = 117 + CMD_HAS_MASK = 118 + CMD_MASKS_PROPERTIES = 119 + CMD_CT = 120 + CMD_EF = 121 + CMD_TT = 122 + CMD_TP = 123 + CMD_TD = 124 + CMD_HD = 125 + CMD_CL = 126 + CMD_LN = 127 + CMD_AO = 128 + CMD_ANCHOR_END = 129 + CMD_OPACITY_FILL = 130 + CMD_FILL_RULE = 131 + CMD_COLOR_DIM = 132 + CMD_DDD = 133 + CMD_MARKERS = 134 + CMD_PROPS = 135 + CMD_ORIGINAL_COLORS = 136 + CMD_COLOR_POINTS = 137 + CMD_COLORS = 138 + CMD_ML2 = 139 + CMD_ML2_IX = 140 + CMD_OFFSET_IX = 141 + CMD_TR_P_IX = 142 + CMD_TR_A_IX = 143 + CMD_TR_SCALE = 144 + CMD_TR_S_IX = 145 + CMD_TR_R_IX = 146 + CMD_TR_SO_IX = 147 + CMD_TR_EO_IX = 148 + CMD_KEYFRAME_END = 149 + CMD_POSITION_EXPR = 150 + CMD_SCALE_EXPR = 151 + CMD_ROTATION_EXPR = 152 + CMD_WIDTH_KEYFRAME = 153 # ę–°å¢ž + CMD_WIDTH_ANIMATED_END = 154 # ę–°å¢ž + CMD_FONTS = 155 + CMD_FONTS_END = 156 + CMD_FONT = 157 + CMD_CHARS = 158 + CMD_CHARS_END = 159 + CMD_CHAR = 160 + CMD_CHAR_END = 161 + CMD_CHAR_SHAPES = 162 + CMD_CHAR_SHAPES_END = 163 + CMD_TEXT_KEYFRAMES = 164 + CMD_TEXT_KEYFRAMES_END = 165 + CMD_TEXT_KEYFRAME = 166 + CMD_TEXT_DOC = 167 + CMD_TEXT_DOC_END = 168 + CMD_FONT_SIZE = 169 + CMD_FONT_FAMILY = 170 + CMD_TEXT = 171 + CMD_CA = 172 + CMD_JUSTIFY = 173 + CMD_TRACKING = 174 + CMD_LINE_HEIGHT = 175 + CMD_LETTER_SPACING = 176 + CMD_FILL_COLOR = 177 + CMD_MORE_OPTIONS = 178 + CMD_MORE_OPTIONS_END = 179 + CMD_G = 180 + CMD_ALIGNMENT = 181 + CMD_ALIGNMENT_K = 182 + CMD_ALIGNMENT_IX = 183 + CMD_DROPDOWN = 184 + CMD_IGNORED = 185 + CMD_SLIDER = 186 + CMD_COLOR = 187 + CMD_OPACITY_ANIMATED = 188 + CMD_OPACITY_KEYFRAME = 189 + CMD_MASKS_PROPERTIES_END = 190 + CMD_MASK = 191 + CMD_MASK_END = 192 + CMD_MASK_PT = 193 + CMD_MASK_PT_END = 194 + CMD_MASK_PT_K = 195 + CMD_MASK_PT_K_END = 196 + CMD_MASK_PT_K_I = 197 + CMD_MASK_PT_K_O = 198 + CMD_MASK_PT_K_V = 199 + CMD_MASK_O = 200 + CMD_MASK_X = 201 + CMD_TM = 202 + CMD_TM_END = 203 + CMD_MASK_PT_K_ARRAY = 204 + CMD_MASK_PT_K_ARRAY_END = 205 + CMD_MASK_PT_KEYFRAME = 206 + CMD_MASK_PT_KEYFRAME_END = 207 + CMD_MASK_PT_KF_I = 208 + CMD_MASK_PT_KF_O = 209 + CMD_MASK_PT_KF_S = 210 + CMD_MASK_PT_KF_S_END = 211 + CMD_MASK_PT_KF_SHAPE = 212 + CMD_MASK_PT_KF_SHAPE_END = 213 + CMD_MASK_PT_KF_SHAPE_I = 214 + CMD_MASK_PT_KF_SHAPE_O = 215 + CMD_MASK_PT_KF_SHAPE_V = 216 + CMD_VALUE = 217 + CMD_VALUE_END = 218 + CMD_TR_POSITION = 219 + CMD_TR_ANCHOR = 220 + CMD_TR_ROTATION = 221 + CMD_TR_START_OPACITY = 222 + CMD_TR_END_OPACITY = 223 + CMD_ZIG_ZAG = 224 + CMD_ZIG_ZAG_END = 225 + CMD_FREQUENCY = 226 + CMD_AMPLITUDE = 227 + CMD_POINT_TYPE = 228 + CMD_ANIMATORS = 229 + CMD_ANIMATORS_END = 230 + CMD_ANIMATOR = 231 + CMD_ANIMATOR_END = 232 + CMD_RANGE_SELECTOR = 233 + CMD_RANGE_SELECTOR_END = 234 + CMD_RANGE_START = 235 + CMD_RANGE_START_END = 236 + CMD_RANGE_START_KEYFRAME = 237 + CMD_AMOUNT = 238 + CMD_MAX_EASE = 239 + CMD_MIN_EASE = 240 + CMD_ANIMATOR_PROPERTIES = 241 + CMD_ANIMATOR_PROPERTIES_END = 242 + CMD_OPACITY_ANIMATED_END = 243 + CMD_MASK_PT_K_C = 244 + CMD_RANGE_END = 245 + CMD_RANGE_END_END = 246 + CMD_RANGE_END_KEYFRAME = 247 + CMD_END_END = 248 + CMD_START_END = 249 + CMD_OFFSET_END = 250 + CMD_POINTS_STAR = 251 + CMD_RANGE_OFFSET = 252 + CMD_RANGE_OFFSET_END = 253 + CMD_RANGE_OFFSET_KEYFRAME = 254 + CMD_S_M = 255 + CMD_OPACITY_ANIMATORS = 256 + CMD_SCALE_ANIMATORS = 257 + CMD_SCALE_ANIMATORS_END = 258 + CMD_ROTATION_ANIMATORS = 259 + CMD_ROTATION_ANIMATORS_END = 260 + CMD_POSITION_ANIMATORS = 261 + CMD_POSITION_ANIMATORS_END = 262 + CMD_TRACKING_ANIMATORS = 263 + CMD_OPACITY_ANIMATORS_END = 264 + CMD_COLOR_KEYFRAME = 265 # Add this constant + CMD_COLOR_ANIMATED_END = 266 + CMD_DASHES = 267 + CMD_DASHES_END = 268 + CMD_DASH = 269 + CMD_DASH_OFFSET = 270 + CMD_LAYER_EFFECT = 271 + CMD_NO_VALUE = 272 + CMD_WIDTH_ANIMATED = 273 # Add this if it doesn't exist + CMD_SIZE_END = 274 + CMD_RECT_SIZE = 275 # Add this new constant + CMD_ELLIPSE_SIZE = 276 + CMD_RECT_ROUNDED = 277 # Add this new constant for animated rect_rounded + CMD_RECT_ROUNDED_END = 278 + CMD_DASH_ANIMATED = 279 # New constant + CMD_DASH_KEYFRAME = 280 # New constant + CMD_DASH_ANIMATED_END = 281 # New constant + + # Command names mapped to their numeric constants + COMMANDS = [ + "animation", # 0 + "layer", # 1 + "transform", # 2 + "position", # 3 + "keyframe", # 4 + "/position", # 5 + "scale", # 6 + "/scale", # 7 + "rotation", # 8 + "opacity", # 9 + "/opacity", # 10 + "anchor", # 11 + "group", # 12 + "/group", # 13 + '"TransformShape"', # 14 + "path", # 15 + "/path", # 16 + "point", # 17 + "fill", # 18 + "gradient_fill", # 19 + "/gradient_fill", # 20 + "start_point", # 21 + "end_point", # 22 + "gradient_type", # 23 + "highlight_length", # 24 + "highlight_angle", # 25 + "/transform", # 26 + "/layer", # 27 + "PAD", # 28 + "EOS", # 29 + "SOS", # 30 + "rect", # 31 + "/rect", # 32 + "size", # 33 + "rounded", # 34 + "ellipse", # 35 + "/ellipse", # 36 + "stroke", # 37 + "skew", # 38 + "skew_axis", # 39 + "asset", # 40 + "/asset", # 41 + "parent", # 42 + "null_layer", # 43 + "/null_layer", # 44 + "precomp_layer", # 45 + "/precomp_layer", # 46 + "reference_id", # 47 + "dimensions", # 48 + "/rotation", # 49 + "star", # 50 + "/star", # 51 + "inner_radius", # 52 + "outer_radius", # 53 + "inner_roundness", # 54 + "outer_roundness", # 55 + "points", # 56 + "star_rotation", # 57 + "trim", # 58 + "/trim", # 59 + "start", # 60 + "end", # 61 + "offset", # 62 + "multiple", # 63 + "repeater", # 64 + "/repeater", # 65 + "copies", # 66 + "repeater_offset", # 67 + "composite", # 68 + "repeater_transform", # 69 + "/repeater_transform", # 70 + "gradient_stroke", # 71 + "/gradient_stroke", # 72 + "width", # 73 + "line_cap", # 74 + "line_join", # 75 + "miter_limit", # 76 + "merge", # 77 + "/merge", # 78 + "merge_mode", # 79 + "rounded_corners", # 80 + "/rounded_corners", # 81 + "radius", # 82 + "twist", # 83 + "/twist", # 84 + "angle", # 85 + "center", # 86 + "bezier", # 87 + "/bezier", # 88 + "text_layer", # 89 + "/text_layer", # 90 + "text_data", # 91 + "/text_data", # 92 + "document", # 93 + "solid_layer", # 94 + "/solid_layer", # 95 + "position_x", # 96 + "position_y", # 97 + "position_z", # 98 + "/position_x", # 99 + "/position_y", # 100 + "/position_z", # 101 + "scale_x", # 102 + "scale_y", # 103 + "scale_z", # 104 + "/scale_x", # 105 + "/scale_y", # 106 + "/scale_z", # 107 + "rotation_x", # 108 + "rotation_y", # 109 + "rotation_z", # 110 + "/rotation_x", # 111 + "/rotation_y", # 112 + "/rotation_z", # 113 + "effects", # 114 + "/effects", # 115 + "effect", # 116 + "/effect", # 117 + "hasMask", # 118 + "masksProperties", # 119 + "ct", # 120 + "ef", # 121 + "tt", # 122 + "tp", # 123 + "td", # 124 + "hd", # 125 + "cl", # 126 + "ln", # 127 + "ao", # 128 + "/anchor", # 129 + "opacity_fill", # 130 + "fill_rule", # 131 + "color_dim", # 132 + "ddd", # 133 + "markers", # 134 + "props", # 135 + "original_colors", # 136 + "color_points", # 137 + "colors", # 138 + "ml2", # 139 + "ml2_ix", # 140 + "offset_ix", # 141 + "tr_p_ix", # 142 + "tr_a_ix", # 143 + "tr_scale", # 144 + "tr_s_ix", # 145 + "tr_r_ix", # 146 + "tr_so_ix", # 147 + "tr_eo_ix", # 148 + "/keyframe", # 149 + "position_expr", # 150 + "scale_expr", # 151 + "rotation_expr", # 152 + "width_keyframe", # 153 # ę–°å¢ž + "/width_animated", # 154 # ę–°å¢ž + "fonts", # 155 + "/fonts", # 156 + "font", # 157 + "chars", # 158 + "/chars", # 159 + "char", # 160 + "/char", #161 + "char_shapes", # 162 + "/char_shapes", # 163 + "text_keyframes", # 164 + "/text_keyframes", # 165 + "text_keyframe", # 166 + "text_doc", # 167 + "/text_doc", # 168 + "font_size", # 169 + "font_family", # 170 + "text", # 171 + "ca", # 172 + "justify", # 173 + "tracking_animators", # 174 + "line_height", # 175 + "letter_spacing", # 176 + "fill_color", # 177 + "more_options", # 178 + "/more_options", # 179 + "g", # 180 + "alignment", # 181 + "alignment_k", # 182 + "alignment_ix", # 183 + "dropdown", # 184 + "ignored", # 185 + "slider", # 186 + "color", # 187 + "opacity_animated", # 188 + "opacity_keyframe", # 189 + "/masksProperties", # 190 + "mask", # 191 + "/mask", # 192 + "mask_pt", # 193 + "/mask_pt", # 194 + "mask_pt_k", # 195 + "/mask_pt_k", # 196 + "mask_pt_k_i", # 197 + "mask_pt_k_o", # 198 + "mask_pt_k_v", # 199 + "mask_o", # 200 + "mask_x", # 201 + "tm", # 202 + "/tm", # 203 + "mask_pt_k_array", # 204 + "/mask_pt_k_array", # 205 + "mask_pt_keyframe", # 206 + "/mask_pt_keyframe", # 207 + "mask_pt_kf_i", # 208 + "mask_pt_kf_o", # 209 + "mask_pt_kf_s", # 210 + "/mask_pt_kf_s", # 211 + "mask_pt_kf_shape", # 212 + "/mask_pt_kf_shape", # 213 + "mask_pt_kf_shape_i", # 214 + "mask_pt_kf_shape_o", # 215 + "mask_pt_kf_shape_v", # 216 + "value", # 217 + "/value", # 218 + "tr_position", # 219 + "tr_anchor", # 220 + "tr_rotation", # 221 + "tr_start_opacity", # 222 + "tr_end_opacity", # 223 + "zig_zag", # 224 + "/zig_zag", # 225 + "frequency", # 226 + "amplitude", # 227 + "point_type", # 228 + "animators", # 229 + "/animators", # 230 + "animator", # 231 + "/animator", # 232 + "range_selector", # 233 + "/range_selector", # 234 + "range_start", # 235 + "/range_start", # 236 + "range_start_keyframe", # 237 + "amount", # 238 + "max_ease", # 239 + "min_ease", # 240 + "animator_properties", # 241 + "/animator_properties", # 242 + "/opacity_animated", # 243 + "mask_pt_k_c", #244 + "range_end", # 245 + "/range_end", # 246 + "range_end_keyframe", # 247 + "/end", #248 + "/start" , # 249 + "/offset" , # 250 + "points_star", #251 + "range_offset", # 252 + "/range_offset", # 253 + "range_offset_keyframe", # 254 + "s_m", # 255 + "opacity_animators", # 256 + "scale_animators", # 257 + "/scale_animators", # 258 + "rotation_animators", # 259 + "/rotation_animators", # 260 + "position_animators", # 261 + "/position_animators", # 262 + "tracking_animators", # 263 + "/opacity_animators", #264 + "color_keyframe", # 265 + "/color_animated", #266 + "dashes", # 267 + "/dashes", # 268 + "dash", # 269 + "dash_offset", # 270 + "layer_effect", # 271 + "no_value", #272 + "width_animated" , #273 + "/size", #274 + "rect_size", # 275 # Add this new command + "ellipse_size", # 276 + "rect_rounded", # 277 + "/rounded", #278 + "dash_animated", # 279 # Add this + "dash_keyframe", # 280 # Add this + "/dash_animated", # 281 # Add this + ] + + # Command to index mapping + COMMAND_TO_IDX = {cmd: idx for idx, cmd in enumerate(COMMANDS)} + _OFFSET_CACHE = {} + + # Parameter indices for each command type (ę·»åŠ ę–°ēš„Index定义) + class Index: + # Animation parameters + class Animation: + FR = 0 + IP = 1 + OP = 2 + W = 3 + H = 4 + DDD = 5 + + class Layer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + DDD = 4 + HD = 5 + HAS_MASK = 6 + AO = 7 + TT = 8 + TP = 9 + TD = 10 + CT = 11 + CP = 12 + + + class Value: + VALUE = 0 + + class Transform: + ANIMATED = 0 + X = 1 + Y = 2 + Z = 3 + + class Keyframe: + T = 0 + S1 = 1 + S2 = 2 + S3 = 3 + I_X = 4 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + I_Y = 5 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + O_X = 6 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + O_Y = 7 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + TO1 = 8 + TO2 = 9 + TO3 = 10 + TI1 = 11 + TI2 = 12 + TI3 = 13 + # Multi-dimensional easing (for scale, position, anchor) + I_X2 = 14 + I_X3 = 15 + I_Y2 = 16 + I_Y3 = 17 + O_X2 = 18 + O_X3 = 19 + O_Y2 = 20 + O_Y3 = 21 + H_FLAG = 22 + E1 = 23 + E2 = 24 + E3 = 25 + + + class Tm: + A = 0 + #IX = 1 + + + class WidthKeyframe: # ę–°å¢ž + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + class Path: + IX = 0 + IND = 1 + KS_IX = 2 + CLOSED = 3 + HD = 4 + ANIMATED = 5 + + class Point: + X = 0 + Y = 1 + IN_X = 2 + IN_Y = 3 + OUT_X = 4 + OUT_Y = 5 + + class Fill: + R = 0 + G = 1 + B = 2 + COLOR_DIM = 3 + HAS_C_A = 4 + HAS_C_IX = 5 + C_IX = 6 + BM = 7 + FILL_RULE = 8 + OPACITY = 9 + COLOR_ANIMATED = 10 # New + OPACITY_ANIMATED = 11 # New + HAS_O_A = 12 # New + HAS_O_IX = 13 # New + O_IX = 14 # New + + class TransformShape: + POSITION_X = 0 + POSITION_Y = 1 + SCALE_X = 2 + SCALE_Y = 3 + ROTATION = 4 + OPACITY = 5 + ANCHOR_X = 6 + ANCHOR_Y = 7 + SKEW = 8 + SKEW_AXIS = 9 + HD = 10 + + class Stroke: + R = 0 + G = 1 + B = 2 + COLOR_DIM = 3 + HAS_C_A = 4 + HAS_C_IX = 5 + C_IX = 6 + BM = 7 + LC = 8 + LJ = 9 + ML = 10 + #WIDTH = 11 + #OPACITY = 12 + WIDTH_ANIMATED = 11 # ę–°å¢ž + COLOR_ANIMATED = 12 # Add this + A = 13 # Add alpha channel support + + class Bezier: + CLOSED = 0 + + class Group: + IX = 0 + CIX = 1 + BM = 2 + HD = 3 + NP = 4 + + class Star: + D = 0 + SY = 1 + + class StarValue: # ę–°å¢žē”ØäŗŽ star ēš„å­å‘½ä»¤ + VALUE = 0 + + class Trim: + IX = 0 + START = 1 + END = 2 + OFFSET = 3 + MULTIPLE = 4 + + class TrimValue: + VALUE = 0 + ANIMATED = 1 + IX = 2 + + class Repeater: + IX = 0 + COPIES = 1 + REPEATER_OFFSET = 2 + COMPOSITE = 3 + TR_P_IX = 4 + TR_A_IX = 5 + TR_SCALE = 6 + TR_S_IX = 7 + TR_R_IX = 8 + TR_SO_IX = 9 + TR_EO_IX = 10 + + + class Asset: + #ID = 0 + FR = 0 + ID_TOKEN_0 = 1 + ID_TOKEN_1 = 2 + ID_TOKEN_2 = 3 + ID_TOKEN_3 = 4 + ID_TOKEN_4 = 5 + ID_TOKEN_5 = 6 + ID_TOKEN_6 = 7 + ID_TOKEN_7 = 8 + ID_TOKEN_8 = 9 + ID_TOKEN_9 = 10 + ID_TOKEN_COUNT = 11 # Store count of tokens + + class Rect: + HD = 0 + D = 1 + POSITION_X = 2 + POSITION_Y = 3 + SIZE_X = 4 + SIZE_Y = 5 + ROUNDED = 6 + IX = 7 + + class Ellipse: + POSITION_X = 0 + POSITION_Y = 1 + SIZE_X = 2 + SIZE_Y = 3 + + class SingleValue: + VALUE = 0 + IX = 1 + ANIMATED = 2 + + class TwoValues: + VALUE1 = 0 + VALUE2 = 1 + IX = 2 + + class ThreeValues: + VALUE1 = 0 + VALUE2 = 1 + VALUE3 = 2 + + class NullLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + CT = 4 + #DDD = 5 + HD = 5 + HAS_MASK = 6 + AO = 7 + TT = 8 + TP = 9 + TD = 10 + CP = 11 + + class PrecompLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + W = 4 + H = 5 + CT = 6 # 添加CTå‚ę•° + HAS_MASK = 7 + AO = 8 + TT = 9 + TP = 10 + TD = 11 + DDD =12 + HD = 13 + CP = 14 + + class SolidLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + WIDTH = 4 + HEIGHT = 5 + HAS_MASK = 6 + COLOR_R = 7 + COLOR_G = 8 + COLOR_B = 9 + COLOR_A = 10 + + class Parent: + PARENT_INDEX= 0 + + class ReferenceId: # ę–°å¢ž + ID_TOKEN_0 = 0 + ID_TOKEN_1 = 1 + ID_TOKEN_2 = 2 + ID_TOKEN_3 = 3 + ID_TOKEN_4 = 4 + ID_TOKEN_5 = 5 + ID_TOKEN_6 = 6 + ID_TOKEN_7 = 7 + ID_TOKEN_8 = 8 + ID_TOKEN_9 = 9 + ID_TOKEN_COUNT = 10 # Store count of tokens + + class Dimensions: # ę–°å¢ž + WIDTH = 0 + HEIGHT = 1 + + class Font: + ASCENT = 0 + FAMILY_TOKEN_0 = 1 + FAMILY_TOKEN_1 = 2 + FAMILY_TOKEN_2 = 3 + FAMILY_TOKEN_3 = 4 + FAMILY_TOKEN_4 = 5 + FAMILY_TOKEN_5 = 6 + FAMILY_TOKEN_6 = 7 + FAMILY_TOKEN_7 = 8 + FAMILY_TOKEN_8 = 9 + FAMILY_TOKEN_9 = 10 + FAMILY_TOKEN_COUNT = 11 + # Reserve slots for style tokens + STYLE_TOKEN_0 = 12 + STYLE_TOKEN_1 = 13 + STYLE_TOKEN_2 = 14 + STYLE_TOKEN_3 = 15 + STYLE_TOKEN_4 = 16 + STYLE_TOKEN_5 = 17 + STYLE_TOKEN_6 = 18 + STYLE_TOKEN_7 = 19 + STYLE_TOKEN_8 = 20 + STYLE_TOKEN_9 = 21 + STYLE_TOKEN_COUNT = 22 + + class Char: + SIZE = 0 + W = 1 + CH_TOKEN_0 = 2 + CH_TOKEN_1 = 3 + CH_TOKEN_2 = 4 + CH_TOKEN_3 = 5 + CH_TOKEN_4 = 6 + CH_TOKEN_5 = 7 + CH_TOKEN_6 = 8 + CH_TOKEN_7 = 9 + CH_TOKEN_8 = 10 + CH_TOKEN_9 = 11 + CH_TOKEN_COUNT = 12 + # Reserve slots for style tokens + STYLE_TOKEN_0 = 13 + STYLE_TOKEN_1 = 14 + STYLE_TOKEN_2 = 15 + STYLE_TOKEN_3 = 16 + STYLE_TOKEN_4 = 17 + STYLE_TOKEN_5 = 18 + STYLE_TOKEN_6 = 19 + STYLE_TOKEN_7 = 20 + STYLE_TOKEN_8 = 21 + STYLE_TOKEN_9 = 22 + STYLE_TOKEN_COUNT = 23 + # Reserve slots for family tokens + FAMILY_TOKEN_0 = 24 + FAMILY_TOKEN_1 = 25 + FAMILY_TOKEN_2 = 26 + FAMILY_TOKEN_3 = 27 + FAMILY_TOKEN_4 = 28 + FAMILY_TOKEN_5 = 29 + FAMILY_TOKEN_6 = 30 + FAMILY_TOKEN_7 = 31 + FAMILY_TOKEN_8 = 32 + FAMILY_TOKEN_9 = 33 + FAMILY_TOKEN_COUNT = 34 + + class TextLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + HAS_MASK = 4 # ę–°å¢ž + + class TextKeyframe: + T = 0 + STROKE_WIDTH = 1 + OFFSET = 2 + WRAP_POSITION_X = 3 + WRAP_POSITION_Y = 4 + WRAP_SIZE_X = 5 + WRAP_SIZE_Y = 6 + # Add numeric fields instead of string storage + FONT_SIZE = 7 + CA = 8 + JUSTIFY = 9 + TRACKING = 10 + LINE_HEIGHT = 11 + LETTER_SPACING = 12 + FILL_COLOR_R = 13 + FILL_COLOR_G = 14 + FILL_COLOR_B = 15 + STROKE_COLOR_R = 16 + STROKE_COLOR_G = 17 + STROKE_COLOR_B = 18 + HAS_STROKE_COLOR = 19 # Flag to indicate if stroke_color exists + FONT_FAMILY_TOKENS_START = 20 # Store up to 10 tokens for font_family + TEXT_TOKENS_START = 30 # Store up to 15 tokens for text + FONT_FAMILY_TOKEN_COUNT = 45 # Store the count of font_family tokens + TEXT_TOKEN_COUNT = 46 # Store the count of text tokens + + class MoreOptions: + G = 0 + ALIGNMENT_A = 1 + ALIGNMENT_K1 = 2 + ALIGNMENT_K2 = 3 + ALIGNMENT_IX = 4 + + class OriginalColors: + # Support up to 18 color values + COLOR_0 = 0 + COLOR_1 = 1 + COLOR_2 = 2 + COLOR_3 = 3 + COLOR_4 = 4 + COLOR_5 = 5 + COLOR_6 = 6 + COLOR_7 = 7 + COLOR_8 = 8 + COLOR_9 = 9 + COLOR_10 = 10 + COLOR_11 = 11 + COLOR_12 = 12 + COLOR_13 = 13 + COLOR_14 = 14 + COLOR_15 = 15 + COLOR_16 = 16 + COLOR_17 = 17 + COLOR_18 = 18 # Added + COLOR_19 = 19 # Added + COLOR_20 = 20 # Added + COLOR_21 = 21 # Added + COLOR_22 = 22 # Added + COLOR_23 = 23 # Added + COLOR_24 = 24 # Added + COLOR_25 = 25 # Added + COLOR_26 = 26 # Added + COLOR_27 = 27 # Added + COLOR_28 = 28 # Added + COLOR_29 = 29 # Added + COLOR_30 = 30 # Added + COLOR_31 = 31 # Added + COLOR_32 = 32 # Added + COLOR_33 = 33 # Added + COLOR_34 = 34 # Added + COLOR_35 = 35 # Added + COLOR_36 = 36 # Added + COLOR_37 = 37 # Added + COLOR_38 = 38 # Added + COLOR_39 = 39 # Added + COLOR_40 = 40 # Added + COLOR_41 = 41 # Added + COLOR_42 = 42 # Added + COLOR_43 = 43 # Added + COLOR_44 = 44 # Added + COLOR_45 = 45 # Added + COLOR_46 = 46 # Added + COUNT = 47 # Store the count of colors + + + + class FontSize: + SIZE = 0 + + class Text: + TEXT_TOKEN_0 = 0 + TEXT_TOKEN_1 = 1 + TEXT_TOKEN_2 = 2 + TEXT_TOKEN_3 = 3 + TEXT_TOKEN_4 = 4 + TEXT_TOKEN_5 = 5 + TEXT_TOKEN_6 = 6 + TEXT_TOKEN_7 = 7 + TEXT_TOKEN_8 = 8 + TEXT_TOKEN_9 = 9 + TEXT_TOKEN_COUNT = 10 + + class Ca: + VALUE = 0 + + class Justify: + VALUE = 0 + + class Tracking: + VALUE = 0 + + class LineHeight: + VALUE = 0 + + class LetterSpacing: + VALUE = 0 + + class FillColor: + R = 0 + G = 1 + B = 2 + + class G: + VALUE = 0 + + class Alignment: + A = 0 + + class AlignmentK: + VALUE1 = 0 + VALUE2 = 1 + + class AlignmentIx: + VALUE = 0 + + class GradientFill: + OPACITY = 0 + FILL_RULE = 1 + START_POINT_X = 2 + START_POINT_Y = 3 + END_POINT_X = 4 + END_POINT_Y = 5 + GRADIENT_TYPE = 6 + HIGHLIGHT_LENGTH = 7 + HIGHLIGHT_ANGLE = 8 + COLOR_POINTS = 9 + # Original colors (up to 12 values for RGBA * 3 color stops) + ORIGINAL_COLOR_0 = 10 + ORIGINAL_COLOR_1 = 11 + ORIGINAL_COLOR_2 = 12 + ORIGINAL_COLOR_3 = 13 + ORIGINAL_COLOR_4 = 14 + ORIGINAL_COLOR_5 = 15 + ORIGINAL_COLOR_6 = 16 + ORIGINAL_COLOR_7 = 17 + ORIGINAL_COLOR_8 = 18 + ORIGINAL_COLOR_9 = 19 + ORIGINAL_COLOR_10 = 20 + ORIGINAL_COLOR_11 = 21 + ORIGINAL_COLOR_12 = 22 # Added + ORIGINAL_COLOR_13 = 23 # Added + ORIGINAL_COLOR_14 = 24 # Added + ORIGINAL_COLOR_15 = 25 # Added + ORIGINAL_COLOR_16 = 26 # Added + ORIGINAL_COLOR_17 = 27 # Added + ORIGINAL_COLOR_18 = 28 # Added + ORIGINAL_COLOR_19 = 29 # Added + ORIGINAL_COLOR_20 = 30 # Added + ORIGINAL_COLOR_21 = 31 # Added + ORIGINAL_COLOR_22 = 32 # Added + ORIGINAL_COLOR_23 = 33 # Added + + class GradientStroke: + OPACITY = 0 + WIDTH = 1 + LINE_CAP = 2 + LINE_JOIN = 3 + MITER_LIMIT = 4 + ML2 = 5 + ML2_IX = 6 + START_POINT_X = 7 + START_POINT_Y = 8 + END_POINT_X = 9 + END_POINT_Y = 10 + GRADIENT_TYPE = 11 + HIGHLIGHT_LENGTH = 12 + HIGHLIGHT_ANGLE = 13 + COLOR_POINTS = 14 + # Original colors (up to 18 values for RGBA * 4.5 color stops) + ORIGINAL_COLOR_0 = 15 + ORIGINAL_COLOR_1 = 16 + ORIGINAL_COLOR_2 = 17 + ORIGINAL_COLOR_3 = 18 + ORIGINAL_COLOR_4 = 19 + ORIGINAL_COLOR_5 = 20 + ORIGINAL_COLOR_6 = 21 + ORIGINAL_COLOR_7 = 22 + ORIGINAL_COLOR_8 = 23 + ORIGINAL_COLOR_9 = 24 + ORIGINAL_COLOR_10 = 25 + ORIGINAL_COLOR_11 = 26 + ORIGINAL_COLOR_12 = 27 + ORIGINAL_COLOR_13 = 28 + ORIGINAL_COLOR_14 = 29 + ORIGINAL_COLOR_15 = 30 + ORIGINAL_COLOR_16 = 31 + ORIGINAL_COLOR_17 = 32 + ORIGINAL_COLOR_18 = 33 # Added + ORIGINAL_COLOR_19 = 34 # Added + ORIGINAL_COLOR_20 = 35 # Added + ORIGINAL_COLOR_21 = 36 # Added + ORIGINAL_COLOR_22 = 37 # Added + ORIGINAL_COLOR_23 = 38 # Added + + class StartPointCmd: + X = 0 + Y = 1 + + class EndPointCmd: + X = 0 + Y = 1 + + class OriginalColorsCmd: + COLOR_1 = 0 + COLOR_2 = 1 + COLOR_3 = 2 + COLOR_4 = 3 + COLOR_5 = 4 + COLOR_6 = 5 + COLOR_7 = 6 + COLOR_8 = 7 + COLOR_9 = 8 + COLOR_10 = 9 + COLOR_11 = 10 + COLOR_12 = 11 + + class ColorPoints: + VALUE = 0 + + class Effect: + TYPE = 0 + INDEX = 1 + NP = 2 + ENABLED = 3 + + class LayerEffect: # Add new Index class + INDEX = 0 + VALUE = 1 + + class Dropdown: + INDEX = 0 + VALUE = 1 + + class NO_VALUE: + INDEX = 0 + VALUE = 1 + + class Ignored: + INDEX = 0 + VALUE = 1 + + class Slider: + INDEX = 0 + VALUE = 1 + + class Color: + NAME_INDEX = 0 # Using NAME_INDEX to avoid confusion with INDEX + INDEX = 1 + R = 2 + G = 3 + B = 4 + + class Merge: + # mergeå‘½ä»¤ēš„nameä¼šå­˜å‚ØåœØstring_paramsäø­ + pass + + class MergeMode: + MODE = 0 + + class Mask: + INDEX = 0 + INV = 1 + MODE = 2 # mode will be stored as string + # nm will be stored in string_params + + class MaskPt: + A = 0 + IX = 1 + + class MaskPtK: + C = 0 # closed + + class MaskPtKValues: # For i, o, v + V1 = 0 + V2 = 1 + V3 = 2 + V4 = 3 + V5 = 4 + V6 = 5 + V7 = 6 + V8 = 7 + V9 = 8 + V10 = 9 + V11 = 10 + V12 = 11 + V13 = 12 + V14 = 13 + V15 = 14 + V16 = 15 + V17 = 16 + V18 = 17 + V19 = 18 + V20 = 19 + COUNT = 20 + + class MaskO: # For mask_o + A = 0 + K = 1 + IX = 2 + + class MaskX: # For mask_x + A = 0 + K = 1 + IX = 2 + + + class MaskPtKeyframe: + INDEX = 0 + T = 1 + + class MaskPtKfI: + X = 0 + Y = 1 + + class MaskPtKfO: + X = 0 + Y = 1 + + class MaskPtKfShape: + INDEX = 0 + C = 1 # closed + + + class MaskPtKfShapeValues: # For shape_i, shape_o, shape_v + V1 = 0 + V2 = 1 + V3 = 2 + V4 = 3 + V5 = 4 + V6 = 5 + V7 = 6 + V8 = 7 + V9 = 8 + V10 = 9 + V11 = 10 + V12 = 11 + V13 = 12 + V14 = 13 + V15 = 14 + V16 = 15 + V17 = 16 + V18 = 17 + V19 = 18 + V20 = 19 + COUNT = 20 # Add this to store the count + + class TrPosition: + X = 0 + Y = 1 + + class TrAnchor: + X = 0 + Y = 1 + + class TrRotation: + VALUE = 0 + + class TrStartOpacity: + VALUE = 0 + + class TrEndOpacity: + VALUE = 0 + class ZigZag: + NAME_INDEX = 0 # Will store in string_params + IX = 1 + + class Frequency: + VALUE = 0 + + class Amplitude: + VALUE = 0 + + class PointType: + VALUE = 0 + + class Animator: + # nm will be stored in string_params + pass + + class RangeSelector: + T = 0 + R = 1 + B = 2 + SH = 3 + RN = 4 + + class RangeStart: + A = 0 + + class RangeStartKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + class Amount: + A = 0 + K = 1 + IX = 2 + + class MaxEase: + A = 0 + K = 1 + IX = 2 + + class MinEase: + A = 0 + K = 1 + IX = 2 + + class Radius: + VALUE = 0 + + class RangeEnd: + A = 0 + + class RangeEndKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + #class RangeOffset: + # A = 0 + + class RangeOffsetKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + class SM: + A = 0 + K = 1 + IX = 2 + + class OpacityAnimators: + A = 0 + K = 1 + IX = 2 + class ScaleAnimators: + A = 0 + K_X = 1 + K_Y = 2 + K_Z = 3 + IX = 4 + + class RotationAnimators: + A = 0 + K = 1 + IX = 2 + + class PositionAnimators: + A = 0 + K_X = 1 + K_Y = 2 + K_Z = 3 + IX = 4 + + class TrackingAnimators: + A = 0 + K = 1 + IX = 2 + class Dashes: + # Container command, no parameters + pass + + class Dash: + TYPE = 0 # Store type as numeric (0 for "d", 1 for "g", 2 for "o") + LENGTH = 1 # dash length + V_IX = 2 # v_ix parameter + + class DashAnimated: + TYPE = 0 # Store type as numeric + V_IX = 1 # v_ix parameter + + class DashKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + + class DashOffset: + O = 0 # offset value + + # Parameter dimension (fixed length for all commands) + PARAM_DIM = 50 + PAD_VAL = -2001 + + def __init__(self, commands, params, seq_len=None, PAD_VAL=-2001, flattened_data=None): + """Initialize LottieTensor""" + self.PAD_VAL = PAD_VAL + + self.commands = commands.reshape(-1, 1).long() + self.params = params.float() + self.seq_len = torch.tensor(len(commands)) if seq_len is None else seq_len + + self.sos_token = torch.tensor([LottieTensor.CMD_SOS]).unsqueeze(-1).long() + self.eos_token = self.pad_token = torch.tensor([LottieTensor.CMD_EOS]).unsqueeze(-1).long() + + # Store original string values + self.string_params = {} + + @staticmethod + def _parse_easing_value(value_str: str) -> int: + """Helper function to parse easing values and return as int""" + if not value_str: + return 0 + + # Handle quoted format "0.833 0.833 0.833" + if value_str.startswith('"') and value_str.endswith('"'): + value_str = value_str[1:-1] + + # Handle space-separated (take first value) + parts = value_str.split() + if parts: + try: + return round(float(parts[0])) + except ValueError: + return 0 + + # Try to parse as plain number + try: + return round(float(value_str)) + except ValueError: + return 0 + + + @staticmethod + def _parse_multi_easing_values(value_str: str) -> List[int]: + """Parse multi-dimensional easing values like '0.3 0.3 0.3' and return as int list""" + values = [0, 0, 0] + + if not value_str: + return values + + # Handle quoted format "0.3 0.3 0.3" + if value_str.startswith('"') and value_str.endswith('"'): + value_str = value_str[1:-1] + + # Parse space-separated values + parts = value_str.split() + for i, part in enumerate(parts[:3]): + try: + values[i] = round(float(part)) + except ValueError: + values[i] = 0 + + # If only one value provided, use it for all dimensions + if len(parts) == 1 and parts[0]: + try: + val = round(float(parts[0])) + values = [val, val, val] + except ValueError: + pass + + return values + + + @staticmethod + def from_sequence(sequence: str) -> 'LottieTensor': + """Convert a string sequence to LottieTensor""" + raw_lines = [line.strip() for line in sequence.strip().split('\n') if line.strip()] + + # Process each line and extract all commands + lines = [] + for raw_line in raw_lines: + # Find all commands in the line (commands are enclosed in parentheses) + import re + commands_in_line = re.findall(r'\([^)]+\)', raw_line) + lines.extend(commands_in_line) + + commands = [] + params_list = [] + string_params = {} + current_context = None + + for idx, line in enumerate(lines): + if not (line.startswith('(') and line.endswith(')')): + continue + + # Extract command name and attributes + content = line[1:-1].strip() + + # Handle end tags + if content.startswith('/'): + cmd = content + if cmd in LottieTensor.COMMAND_TO_IDX: + commands.append(LottieTensor.COMMAND_TO_IDX[cmd]) + params_list.append([LottieTensor.PAD_VAL] * LottieTensor.PARAM_DIM) + + # Reset context for certain end tags + if cmd in ["/position", "/scale", "/opacity", "/rotation", "/keyframe", "/anchor", "/path", "/width_animated", + "/position_x", "/position_y", "/position_z", "/tm", "/range_start", "/range_end", "/animator_properties", + "/start", "/end", "/offset", "/color_animated", "/rounded"]: + current_context = None + continue + + # Parse command and attributes + parts = content.split(' ', 1) + cmd = parts[0] + + # Handle quoted commands + if cmd.startswith('"') and cmd.endswith('"'): + cmd = cmd + + attrs_str = parts[1] if len(parts) > 1 else "" + + # Update context - modified to track path animation, width animation, and individual position components + if cmd in ["position", "scale", "opacity", "rotation", "anchor"]: + current_context = cmd + elif cmd in ["position_x", "position_y", "position_z"]: + # Check if animated + if "animated" in attrs_str and "true" in attrs_str.lower(): + current_context = cmd + elif cmd == "path" and "animated" in attrs_str: + current_context = "path" + elif cmd == "width_keyframe": + current_context = "width" + elif cmd == "start" and "animated" in attrs_str and "true" in attrs_str.lower(): + current_context = "trim_start" + elif cmd == "end" and "animated" in attrs_str and "true" in attrs_str.lower(): + current_context = "trim_end" + elif cmd == "offset" and "animated" in attrs_str and "true" in attrs_str.lower(): + current_context = "trim_offset" + elif cmd == "mask_x": + # Check if animated (a=1) + mask_x_attrs = LottieTensor._parse_attributes(attrs_str) + if float(mask_x_attrs.get("a", 0)) > 0.5: + current_context = "mask_x" + + elif cmd == "scale_animators": + # Check if animated + scale_animators_attrs = LottieTensor._parse_attributes(attrs_str) + if float(scale_animators_attrs.get("a", 0)) > 0.5: + current_context = "scale_animators" + elif cmd == "rotation_animators": + # Check if animated + rotation_animators_attrs = LottieTensor._parse_attributes(attrs_str) + if float(rotation_animators_attrs.get("a", 0)) > 0.5: + current_context = "rotation_animators" + + elif cmd == "opacity_animators": + # Check if animated + opacity_animators_attrs = LottieTensor._parse_attributes(attrs_str) + if float(opacity_animators_attrs.get("a", 0)) > 0.5: + current_context = "opacity_animators" + + elif cmd == "position_animators": + # Check if animated + position_animators_attrs = LottieTensor._parse_attributes(attrs_str) + if float(position_animators_attrs.get("a", 0)) > 0.5: + current_context = "position_animators" + + elif cmd == "tracking_animators": + # Check if animated + tracking_animators_attrs = LottieTensor._parse_attributes(attrs_str) + if float(tracking_animators_attrs.get("a", 0)) > 0.5: + current_context = "tracking_animators" + + elif cmd == "rect_rounded" and "animated" in attrs_str and "true" in attrs_str.lower(): + current_context = "rect_rounded" + if cmd not in LottieTensor.COMMAND_TO_IDX: + continue + + cmd_idx = LottieTensor.COMMAND_TO_IDX[cmd] + cmd_key = f"{len(commands)}" # Use command index as key + commands.append(cmd_idx) + + # Initialize parameters + params = [LottieTensor.PAD_VAL] * LottieTensor.PARAM_DIM + attrs = LottieTensor._parse_attributes(attrs_str) + + # Parse parameters based on command type + if cmd_idx == LottieTensor.CMD_ANIMATION: + # Store original string values + #string_params[f"{cmd_key}_v"] = attrs.get("v", "5.12.1") + #string_params[f"{cmd_key}_nm"] = attrs.get("nm", "Comp 1") + #string_params[f"{cmd_key}_markers"] = attrs.get("markers", "[]") + #string_params[f"{cmd_key}_props"] = attrs.get("props", "{}") + + params[LottieTensor.Index.Animation.FR] = round(float(attrs.get("fr", 60))) + params[LottieTensor.Index.Animation.IP] = round(float(attrs.get("ip", 0))) + params[LottieTensor.Index.Animation.OP] = round(float(attrs.get("op", 150))) + params[LottieTensor.Index.Animation.W] = round(float(attrs.get("w", 512))) + params[LottieTensor.Index.Animation.H] = round(float(attrs.get("h", 512))) + params[LottieTensor.Index.Animation.DDD] = int(attrs.get("ddd", 0)) + #eos token + #print("params", params) + + elif cmd_idx == LottieTensor.CMD_LAYER: + # Store layer name and string attributes + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Layer") + + # åæ…éœ€ēš„å±žę€§ + #params[LottieTensor.Index.Layer.INDEX] = float(attrs.get("index", 0)) + params[LottieTensor.Index.Layer.INDEX] = LottieTensor._index_clamp_value(round(float(attrs.get("index", "0")))) # index 0-100 + #params[LottieTensor.Index.Layer.IN_POINT] = float(attrs.get("in_point", 0)) + #params[LottieTensor.Index.Layer.OUT_POINT] = float(attrs.get("out_point", 60)) + params[LottieTensor.Index.Layer.IN_POINT] = LottieTensor._clamp_value(round(float(attrs.get("in_point", 0)))) #ip -2000-2000 + params[LottieTensor.Index.Layer.OUT_POINT] = LottieTensor._clamp_value(round(float(attrs.get("out_point", 60)))) #op -2000-2000 + params[LottieTensor.Index.Layer.START_TIME] = LottieTensor._clamp_value(round(float(attrs.get("start_time", 0)))) #st -2000-2000 + + # åÆé€‰å±žę€§ - åŖåœØå­˜åœØę—¶č§£ęžļ¼Œäøč®¾ē½®é»˜č®¤å€¼ + if "ddd" in attrs: + params[LottieTensor.Index.Layer.DDD] = float(attrs.get("ddd")) #0-1 + + if "hd" in attrs: + params[LottieTensor.Index.Layer.HD] = 1.0 if attrs.get("hd").lower() == "true" else 0.0 # 0-1 + + if "cp" in attrs: + params[LottieTensor.Index.Layer.CP] = 1.0 if attrs.get("cp").lower() == "true" else 0.0 # 0-1 + + if "hasMask" in attrs: + params[LottieTensor.Index.Layer.HAS_MASK] = 1.0 if attrs.get("hasMask").lower() == "true" else 0.0 # 0-1 + + if "ao" in attrs: + params[LottieTensor.Index.Layer.AO] = int(attrs.get("ao")) # 0-1 + + if "tt" in attrs: + params[LottieTensor.Index.Layer.TT] = int(attrs.get("tt")) # 0-4 + + if "tp" in attrs: + params[LottieTensor.Index.Layer.TP] = int(attrs.get("tp")) #0-100仄内 + + if "td" in attrs: + params[LottieTensor.Index.Layer.TD] = int(attrs.get("td")) #0-1 + + if "ct" in attrs: + params[LottieTensor.Index.Layer.CT] = int(attrs.get("ct")) #0-1 + + elif cmd_idx == LottieTensor.CMD_NULL_LAYER: + # Store layer name and string attributes + #string_params[f"{cmd_key}_name"] = attrs.get("name", "null_layer") + + #params[LottieTensor.Index.NullLayer.INDEX] = int(attrs.get("index", 0)) + params[LottieTensor.Index.NullLayer.INDEX] = LottieTensor._index_clamp_value(round(float(attrs.get("index", 0)))) + #params[LottieTensor.Index.NullLayer.IN_POINT] = float(attrs.get("in_point", 0)) + #params[LottieTensor.Index.NullLayer.OUT_POINT] = float(attrs.get("out_point", 60)) + params[LottieTensor.Index.NullLayer.IN_POINT] = LottieTensor._clamp_value(round(float(attrs.get("in_point", 0)))) + params[LottieTensor.Index.NullLayer.OUT_POINT] = LottieTensor._clamp_value(round(float(attrs.get("out_point", 60)))) + params[LottieTensor.Index.NullLayer.START_TIME] = LottieTensor._clamp_value(round(float(attrs.get("start_time", 0)))) + + if "hd" in attrs: + params[LottieTensor.Index.PrecompLayer.HD] = 1.0 if attrs.get("hd").lower() == "true" else 0.0 + + if "cp" in attrs: + params[LottieTensor.Index.PrecompLayer.CP] = 1.0 if attrs.get("cp").lower() == "true" else 0.0 + + if "hasMask" in attrs: + params[LottieTensor.Index.PrecompLayer.HAS_MASK] = 1.0 if attrs.get("hasMask").lower() == "true" else 0.0 + + if "ao" in attrs: + params[LottieTensor.Index.PrecompLayer.AO] = int(attrs.get("ao")) + + if "tt" in attrs: + params[LottieTensor.Index.PrecompLayer.TT] = int(attrs.get("tt")) + + if "tp" in attrs: + params[LottieTensor.Index.PrecompLayer.TP] = int(attrs.get("tp")) + + if "td" in attrs: + params[LottieTensor.Index.PrecompLayer.TD] = int(attrs.get("td")) + + elif cmd_idx == LottieTensor.CMD_PRECOMP_LAYER: + # Parse name more carefully to handle names with spaces + name = attrs.get("name", "precomp_layer") + # If name wasn't properly captured (e.g., due to spaces), try regex + if name == "precomp_layer" or not name: + # Look for name="..." pattern in the original attrs_str + import re + name_match = re.search(r'name="([^"]*)"', attrs_str) + if name_match: + name = name_match.group(1) + else: + # Try without quotes + name_match = re.search(r'name=([^\s]+)', attrs_str) + if name_match: + name = name_match.group(1) + else: + name = "precomp_layer" + + #string_params[f"{cmd_key}_name"] = name + + # Parse numeric parameters + #params[LottieTensor.Index.PrecompLayer.INDEX] = float(attrs.get("index", 0)) + params[LottieTensor.Index.PrecompLayer.INDEX] = LottieTensor._index_clamp_value(round(float(attrs.get("index", 0)))) + #params[LottieTensor.Index.PrecompLayer.IN_POINT] = float(attrs.get("in_point", 0)) + #params[LottieTensor.Index.PrecompLayer.OUT_POINT] = float(attrs.get("out_point", 120)) + params[LottieTensor.Index.PrecompLayer.IN_POINT] = LottieTensor._clamp_value(round(float(attrs.get("in_point", 0)))) + params[LottieTensor.Index.PrecompLayer.OUT_POINT] = LottieTensor._clamp_value(round(float(attrs.get("out_point", 120)))) + params[LottieTensor.Index.PrecompLayer.START_TIME] = LottieTensor._clamp_value(round(float(attrs.get("start_time", 0)))) + + # åÆé€‰å±žę€§ - åŖåœØå­˜åœØę—¶č§£ęžļ¼Œäøč®¾ē½®é»˜č®¤å€¼ + if "h" in attrs: + params[LottieTensor.Index.PrecompLayer.H] = round(float(attrs.get("h"))) #0-2000 + + if "w" in attrs: + params[LottieTensor.Index.PrecompLayer.W] = round(float(attrs.get("w"))) #0-2000 + + + if "ddd" in attrs: + params[LottieTensor.Index.PrecompLayer.DDD] = int(attrs.get("ddd")) #0-1 + + if "hd" in attrs: + params[LottieTensor.Index.PrecompLayer.HD] = 1.0 if attrs.get("hd").lower() == "true" else 0.0 #0-1 + + if "cp" in attrs: + params[LottieTensor.Index.PrecompLayer.CP] = 1.0 if attrs.get("cp").lower() == "true" else 0.0 #0-1 + + if "hasMask" in attrs: + params[LottieTensor.Index.PrecompLayer.HAS_MASK] = 1.0 if attrs.get("hasMask").lower() == "true" else 0.0 #0-1 + + if "ao" in attrs: + params[LottieTensor.Index.PrecompLayer.AO] = int(attrs.get("ao")) #0-1 + + if "tt" in attrs: + params[LottieTensor.Index.PrecompLayer.TT] = int(attrs.get("tt")) + + if "tp" in attrs: + params[LottieTensor.Index.PrecompLayer.TP] = int(attrs.get("tp")) + + if "td" in attrs: + params[LottieTensor.Index.PrecompLayer.TD] = int(attrs.get("td")) + + if "ct" in attrs: + params[LottieTensor.Index.PrecompLayer.CT] = int(attrs.get("ct")) + + + elif cmd_idx == LottieTensor.CMD_TEXT_LAYER: + # å­˜å‚Øname + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Text Layer") + + #params[LottieTensor.Index.TextLayer.INDEX] = float(attrs.get("index", 0)) + params[LottieTensor.Index.TextLayer.INDEX] = LottieTensor._index_clamp_value(round(float(attrs.get("index", 0)))) + #params[LottieTensor.Index.TextLayer.IN_POINT] = float(attrs.get("in_point", 0)) + #params[LottieTensor.Index.TextLayer.OUT_POINT] = float(attrs.get("out_point", 60)) + params[LottieTensor.Index.TextLayer.IN_POINT] = LottieTensor._clamp_value(round(float(attrs.get("in_point", 0)))) + params[LottieTensor.Index.TextLayer.OUT_POINT] = LottieTensor._clamp_value(round(float(attrs.get("out_point", 60)))) + params[LottieTensor.Index.TextLayer.START_TIME] = LottieTensor._clamp_value(round(float(attrs.get("start_time", 0)))) + params[LottieTensor.Index.TextLayer.HAS_MASK] = 1.0 if attrs.get("hasMask", "false").lower() == "true" else 0.0 # ę–°å¢ž + + elif cmd_idx == LottieTensor.CMD_SOLID_LAYER: + # Store string attributes + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Solid Layer") + #string_params[f"{cmd_key}_color"] = attrs.get("color", "#000000") + hex_color = attrs.get("color", "#00000000") + # Remove '#' if present + hex_color = hex_color.lstrip('#') + # Convert to RGB + r = int(hex_color[0:2], 16) if len(hex_color) >= 2 else 0 + g = int(hex_color[2:4], 16) if len(hex_color) >= 4 else 0 + b = int(hex_color[4:6], 16) if len(hex_color) >= 6 else 0 + a = int(hex_color[6:8], 16) if len(hex_color) >= 8 else 0 + + # Store RGB values as separate parameters + params[LottieTensor.Index.SolidLayer.COLOR_R] = round(float(r)) + params[LottieTensor.Index.SolidLayer.COLOR_G] = round(float(g)) + params[LottieTensor.Index.SolidLayer.COLOR_B] = round(float(b)) + params[LottieTensor.Index.SolidLayer.COLOR_A] = round(float(a)) #čæ™é‡Œēš„color都是0-255吧 + + + #params[LottieTensor.Index.SolidLayer.INDEX] = float(attrs.get("index", 0)) + params[LottieTensor.Index.SolidLayer.INDEX] = LottieTensor._index_clamp_value(round(float(attrs.get("index", 0)))) + #params[LottieTensor.Index.SolidLayer.IN_POINT] = float(attrs.get("in_point", 0)) + #params[LottieTensor.Index.SolidLayer.OUT_POINT] = float(attrs.get("out_point", 60)) + params[LottieTensor.Index.SolidLayer.IN_POINT] = LottieTensor._clamp_value(round(float(attrs.get("in_point", 0)))) + params[LottieTensor.Index.SolidLayer.OUT_POINT] = LottieTensor._clamp_value(round(float(attrs.get("out_point", 60)))) + params[LottieTensor.Index.SolidLayer.START_TIME] = LottieTensor._clamp_value(round(float(attrs.get("start_time", 0)))) + params[LottieTensor.Index.SolidLayer.WIDTH] = LottieTensor._clamp_value(round(float(attrs.get("width", 512)))) + params[LottieTensor.Index.SolidLayer.HEIGHT] = LottieTensor._clamp_value(round(float(attrs.get("height", 512)))) + params[LottieTensor.Index.SolidLayer.HAS_MASK] = 1.0 if attrs.get("hasMask", "false").lower() == "true" else 0.0 + + elif cmd_idx in [LottieTensor.CMD_FONTS, LottieTensor.CMD_FONTS_END, LottieTensor.CMD_CHARS, LottieTensor.CMD_CHARS_END, LottieTensor.CMD_CHAR_SHAPES, LottieTensor.CMD_CHAR_SHAPES_END, LottieTensor.CMD_TEXT_KEYFRAMES, LottieTensor.CMD_MORE_OPTIONS, LottieTensor.CMD_OPACITY_ANIMATED_END, LottieTensor.CMD_END_END, LottieTensor.CMD_START_END, LottieTensor.CMD_OFFSET_END, LottieTensor.CMD_OPACITY_ANIMATORS_END]: + pass + + + elif cmd_idx == LottieTensor.CMD_TEXT_KEYFRAME: + if LottieTensor.tokenizer is None: + LottieTensor.init_tokenizer() + + # Parse all attributes from text_keyframe + params[LottieTensor.Index.TextKeyframe.T] = LottieTensor._clamp_value(round(float(attrs.get("t", 0)))) + + # Parse stroke_width as a numeric parameter + stroke_width_str = attrs.get("stroke_width", "0") + try: + params[LottieTensor.Index.TextKeyframe.STROKE_WIDTH] = round(float(stroke_width_str)) + except ValueError: + params[LottieTensor.Index.TextKeyframe.STROKE_WIDTH] = 0 + + # Parse offset as a boolean (1.0 for true, 0.0 for false) + offset_str = attrs.get("offset", "false") + params[LottieTensor.Index.TextKeyframe.OFFSET] = 1 if offset_str.lower() == "true" else 0.0 + + # Parse wrap_position array (ę–°å¢ž) + wrap_position_str = attrs.get("wrap_position", "") + if wrap_position_str: + if wrap_position_str.startswith("[") and wrap_position_str.endswith("]"): + wrap_position_str = wrap_position_str[1:-1] + pos_parts = wrap_position_str.split(",") + if len(pos_parts) >= 2: + try: + params[LottieTensor.Index.TextKeyframe.WRAP_POSITION_X] = round(float(pos_parts[0].strip())) + params[LottieTensor.Index.TextKeyframe.WRAP_POSITION_Y] = round(float(pos_parts[1].strip())) + except ValueError: + pass + + # Parse wrap_size array (ę–°å¢ž) + wrap_size_str = attrs.get("wrap_size", "") + if wrap_size_str: + if wrap_size_str.startswith("[") and wrap_size_str.endswith("]"): + wrap_size_str = wrap_size_str[1:-1] + size_parts = wrap_size_str.split(",") + if len(size_parts) >= 2: + try: + params[LottieTensor.Index.TextKeyframe.WRAP_SIZE_X] = round(float(size_parts[0].strip())) + params[LottieTensor.Index.TextKeyframe.WRAP_SIZE_Y] = round(float(size_parts[1].strip())) + except ValueError: + pass + + # Store all text_keyframe attributes in string_params + #string_params[f"{cmd_key}_font_size"] = attrs.get("font_size", "12") + #string_params[f"{cmd_key}_font_family"] = attrs.get("font_family", "") + #string_params[f"{cmd_key}_text"] = attrs.get("text", "") + #string_params[f"{cmd_key}_ca"] = attrs.get("ca", "1") + #string_params[f"{cmd_key}_justify"] = attrs.get("justify", "0") + #string_params[f"{cmd_key}_tracking"] = attrs.get("tracking", "0") + #string_params[f"{cmd_key}_line_height"] = attrs.get("line_height", "0") + #string_params[f"{cmd_key}_letter_spacing"] = attrs.get("letter_spacing", "0") + params[LottieTensor.Index.TextKeyframe.FONT_SIZE] = round(float(attrs.get("font_size", 12))) + params[LottieTensor.Index.TextKeyframe.CA] = int(attrs.get("ca", 1)) + params[LottieTensor.Index.TextKeyframe.JUSTIFY] = int(attrs.get("justify", 0)) + params[LottieTensor.Index.TextKeyframe.TRACKING] = int(float(attrs.get("tracking", 0))) + params[LottieTensor.Index.TextKeyframe.LINE_HEIGHT] = round(float(attrs.get("line_height", 0))) + params[LottieTensor.Index.TextKeyframe.LETTER_SPACING] = int(float(attrs.get("letter_spacing", 0))) + + font_family = attrs.get("font_family", "") + if font_family: + font_family_tokens = LottieTensor.tokenizer.encode(font_family, add_special_tokens=False) + # Store up to 10 tokens + for i, token in enumerate(font_family_tokens[:10]): + params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START + i] = int(token) + params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT] = int(len(font_family_tokens[:10])) + else: + params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT] = 0 + + # Tokenize text + text = attrs.get("text", "") + if text: + text_tokens = LottieTensor.tokenizer.encode(text, add_special_tokens=False) + # Store up to 15 tokens + for i, token in enumerate(text_tokens[:15]): + params[LottieTensor.Index.TextKeyframe.TEXT_TOKENS_START + i] = int(token) + params[LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT] = int(len(text_tokens[:15])) + else: + params[LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT] = 0 + + + # Parse fill_color array + fill_color_str = attrs.get("fill_color", "[0,0,0]") + if fill_color_str.startswith("[") and fill_color_str.endswith("]"): + fill_color_str = fill_color_str[1:-1] + color_parts = fill_color_str.split(",") + #string_params[f"{cmd_key}_fill_color"] = ",".join([p.strip() for p in color_parts]) + if len(color_parts) >= 3: + params[LottieTensor.Index.TextKeyframe.FILL_COLOR_R] = round(float(color_parts[0].strip()) * 255) + params[LottieTensor.Index.TextKeyframe.FILL_COLOR_G] = round(float(color_parts[1].strip()) * 255) + params[LottieTensor.Index.TextKeyframe.FILL_COLOR_B] = round(float(color_parts[2].strip()) * 255) + + # Parse stroke_color array (if present) + stroke_color_str = attrs.get("stroke_color", "") + if stroke_color_str: + if stroke_color_str.startswith("[") and stroke_color_str.endswith("]"): + stroke_color_str = stroke_color_str[1:-1] + color_parts = stroke_color_str.split(",") + if len(color_parts) >= 3: + params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_R] = round(float(color_parts[0].strip()) * 255) + params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_G] = round(float(color_parts[1].strip()) * 255) + params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_B] = round(float(color_parts[2].strip()) * 255) + else: + params[LottieTensor.Index.TextKeyframe.HAS_STROKE_COLOR] = 0 + + + elif cmd_idx == LottieTensor.CMD_MORE_OPTIONS: + # Parse the entire more_options line + parts_list = attrs_str.split() + i = 0 + while i < len(parts_list): + if parts_list[i] == "g" and i + 1 < len(parts_list): + params[LottieTensor.Index.MoreOptions.G] = round(float(parts_list[i + 1])) #1-4 + i += 2 + elif parts_list[i] == "alignment" and i + 1 < len(parts_list): + if parts_list[i + 1].startswith("a="): + params[LottieTensor.Index.MoreOptions.ALIGNMENT_A] = round(float(parts_list[i + 1].split("=")[1])) + i += 2 + else: + i += 1 + elif parts_list[i] == "alignment_k" and i + 2 < len(parts_list): + params[LottieTensor.Index.MoreOptions.ALIGNMENT_K1] = round(float(parts_list[i + 1])) + params[LottieTensor.Index.MoreOptions.ALIGNMENT_K2] = round(float(parts_list[i + 2])) + i += 3 + elif parts_list[i] == "alignment_ix" and i + 1 < len(parts_list): + params[LottieTensor.Index.MoreOptions.ALIGNMENT_IX] = round(float(parts_list[i + 1])) + i += 2 + else: + i += 1 + + + elif cmd_idx == LottieTensor.CMD_REFERENCE_ID: + if LottieTensor.tokenizer is None: + LottieTensor.init_tokenizer() + + # Extract reference_id from attrs_str + ref_id_match = re.search(r'"([^"]*)"', attrs_str) + if ref_id_match: + reference_id = ref_id_match.group(1) + else: + # Try without quotes + parts = attrs_str.strip().split() + if parts: + reference_id = parts[0] + else: + reference_id = "comp_0" + + # Tokenize reference_id + id_tokens = LottieTensor.tokenizer.encode(reference_id, add_special_tokens=False)[:10] # Limit to 10 tokens + + for i, token_id in enumerate(id_tokens): + if i < 10: + params[LottieTensor.Index.ReferenceId.ID_TOKEN_0 + i] = int(token_id) + params[LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT] = int(len(id_tokens)) + + + + elif cmd_idx == LottieTensor.CMD_DIMENSIONS: + # 处理dimensions命令 + params[LottieTensor.Index.Dimensions.WIDTH] = round(float(attrs.get("width", 512))) #0-2000 + params[LottieTensor.Index.Dimensions.HEIGHT] = round(float(attrs.get("height", 512))) #0-2000 + + + + # 3. Modify the stroke parsing in from_sequence method: + + + elif cmd_idx == LottieTensor.CMD_STROKE: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Stroke") + + # Check if color is animated + color_animated = attrs.get("color_animated", "false").lower() == "true" + params[LottieTensor.Index.Stroke.COLOR_ANIMATED] = 1.0 if color_animated else 0.0 # 0-1 + + if not color_animated: + # Parse static color and convert from 0-1 to 0-255 range + params[LottieTensor.Index.Stroke.R] = round(float(attrs.get("r", 0)) * 255) # 0-1 → 0-255 + params[LottieTensor.Index.Stroke.G] = round(float(attrs.get("g", 0)) * 255) # 0-1 → 0-255 + params[LottieTensor.Index.Stroke.B] = round(float(attrs.get("b", 0)) * 255) # 0-1 → 0-255 + params[LottieTensor.Index.Stroke.A] = round(float(attrs.get("a", 1)) * 255) # 0-1 → 0-255 + + params[LottieTensor.Index.Stroke.COLOR_DIM] = int(attrs.get("color_dim", 4)) #3-4 + params[LottieTensor.Index.Stroke.HAS_C_A] = 1.0 if attrs.get("has_c_a", "").lower() == "true" else 0.0 #0-1 + params[LottieTensor.Index.Stroke.HAS_C_IX] = 1.0 if attrs.get("has_c_ix", "").lower() == "true" else 0.0 #0-1 + params[LottieTensor.Index.Stroke.C_IX] = int(attrs.get("c_ix", 3)) #2-4 + params[LottieTensor.Index.Stroke.BM] = int(attrs.get("bm", 0)) #0-1 + params[LottieTensor.Index.Stroke.LC] = int(attrs.get("lc", 1)) #1-3 + params[LottieTensor.Index.Stroke.LJ] = int(attrs.get("lj", 1)) #1-3 + params[LottieTensor.Index.Stroke.ML] = int(float((attrs.get("ml", "4")))) #0-50 + + # Handle width or width_animated + if "width_animated" in attrs and attrs.get("width_animated", "").lower() == "true": + params[LottieTensor.Index.Stroke.WIDTH_ANIMATED] = 1.0 + current_context = "width" + + # ADD THIS: Append width_animated command after stroke + params_list.append(params) + commands.append(LottieTensor.CMD_WIDTH_ANIMATED) # Note: need to define this constant + params_list.append([LottieTensor.PAD_VAL] * LottieTensor.PARAM_DIM) + params = [LottieTensor.PAD_VAL] * LottieTensor.PARAM_DIM # Reset for next command + else: + params[LottieTensor.Index.Stroke.WIDTH_ANIMATED] = 0.0 + + # Set context for color keyframes if animated + if color_animated: + current_context = "stroke_color" + + # 4. Add parsing for color_keyframe command: keframeēš„s和i_x, i_y, o_x, o_yę˜Æäøčƒ½č¶Šē•Œēš„ļ¼Œč¶Šē•Œéœ€č¦åŽ»é™¤ + elif cmd_idx == LottieTensor.CMD_COLOR_KEYFRAME: + # Parse color keyframe parameters + params[LottieTensor.Index.Keyframe.T] = round(float(attrs.get("t", 0))) #-2000-2000 + params[LottieTensor.Index.Keyframe.S1] = round(float(attrs.get("r", 0)) * 255) # Use S1 for R + params[LottieTensor.Index.Keyframe.S2] = round(float(attrs.get("g", 0)) * 255) # Use S2 for G + params[LottieTensor.Index.Keyframe.S3] = round(float(attrs.get("b", 0)) * 255) # Use S3 for B + params[LottieTensor.Index.Keyframe.E1] = round(float(attrs.get("a", 1)) * 255) # Use E1 for A + + # Parse easing parameters + params[LottieTensor.Index.Keyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0"))*100)) + params[LottieTensor.Index.Keyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0"))*100)) + params[LottieTensor.Index.Keyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0"))*100)) + params[LottieTensor.Index.Keyframe.O_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0"))*100)) + + + elif cmd_idx == LottieTensor.CMD_OPACITY_ANIMATED: + # Check if animated + if attrs.get("true", "false").lower() == "true" or "true" in attrs_str.lower(): + current_context = "opacity_animated" + + elif cmd_idx == LottieTensor.CMD_OPACITY_KEYFRAME: + # Parse opacity keyframe parameters + params[LottieTensor.Index.Keyframe.T] = round(float(attrs.get("t", 0))) + + # Parse s parameter if present + if "s" in attrs: + s_str = attrs.get("s", "0").strip('"') + params[LottieTensor.Index.Keyframe.S1] = round(float(s_str)) + + # Parse easing parameters if present + if "i_x" in attrs or "i_y" in attrs or "o_x" in attrs or "o_y" in attrs: + params[LottieTensor.Index.Keyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0"))*100)) + params[LottieTensor.Index.Keyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0"))*100)) + params[LottieTensor.Index.Keyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0"))*100)) + params[LottieTensor.Index.Keyframe.O_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0"))*100)) + + + elif cmd_idx == LottieTensor.CMD_WIDTH_KEYFRAME: + params[LottieTensor.Index.WidthKeyframe.T] = round(float(attrs.get("t", 0))) + + # 处理så‚ę•° - ę”¹ęˆä¹˜ä»„10 + if "s" in attrs: + s_str = attrs.get("s", "0").strip('"') + try: + s_val = round(float(s_str) * 10) + s_val = max(0, min(10000, s_val)) # 裁剪 + params[LottieTensor.Index.WidthKeyframe.S] = s_val + except ValueError: + params[LottieTensor.Index.WidthKeyframe.S] = 0.0 + + # easingå‚ę•°äæęŒäøå˜ + params[LottieTensor.Index.WidthKeyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0"))*100)) + params[LottieTensor.Index.WidthKeyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0"))*100)) + params[LottieTensor.Index.WidthKeyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0"))*100)) + params[LottieTensor.Index.WidthKeyframe.O_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0"))*100)) + + + + elif cmd_idx == LottieTensor.CMD_POSITION: + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + elif attrs.get("separated", "").lower() == "true": + # Handle separated position (for 3D layers) + params[LottieTensor.Index.Transform.ANIMATED] = 2.0 # Use 2.0 to indicate separated + + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + # Parse position values + pos_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + pos_parts.append(part) + except ValueError: + pass + + if len(pos_parts) >= 1: + params[LottieTensor.Index.Transform.X] = round(float(pos_parts[0])) + if len(pos_parts) >= 2: + params[LottieTensor.Index.Transform.Y] = round(float(pos_parts[1])) + if len(pos_parts) >= 3: + params[LottieTensor.Index.Transform.Z] = round(float(pos_parts[2])) + + + elif cmd_idx in [LottieTensor.CMD_POSITION_X, LottieTensor.CMD_POSITION_Y, LottieTensor.CMD_POSITION_Z]: + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1 + current_context = cmd # Set context to the specific component + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0 + # Parse the value + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Transform.X] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_SCALE: + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + # Parse scale values + scale_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + scale_parts.append(part) + except ValueError: + pass + + if len(scale_parts) >= 1: + params[LottieTensor.Index.Transform.X] = round(float(scale_parts[0])) + if len(scale_parts) >= 2: + params[LottieTensor.Index.Transform.Y] = round(float(scale_parts[1])) + if len(scale_parts) >= 3: + params[LottieTensor.Index.Transform.Z] = round(float(scale_parts[2])) + + elif cmd_idx == LottieTensor.CMD_ROTATION: + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + # Parse rotation value + rot_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + rot_parts.append(part) + except ValueError: + pass + if rot_parts: + val = round(float(rot_parts[0])) + # ę·»åŠ č£å‰Ŗļ¼šå°† rotation é™åˆ¶åœØ -720 到 720 čŒƒå›“å†… + val = max(-720, min(720, val % 360 if abs(val) > 720 else val)) + params[LottieTensor.Index.Transform.X] = val + + elif cmd_idx == LottieTensor.CMD_OPACITY: + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + # Parse opacity value + op_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + op_parts.append(part) + except ValueError: + pass + if op_parts: + params[LottieTensor.Index.Transform.X] = round(float(op_parts[0])) + + elif cmd_idx == LottieTensor.CMD_ANCHOR: + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + # Parse anchor values + anchor_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + anchor_parts.append(part) + except ValueError: + pass + + if len(anchor_parts) >= 1: + params[LottieTensor.Index.Transform.X] = round(float(anchor_parts[0])) + if len(anchor_parts) >= 2: + params[LottieTensor.Index.Transform.Y] = round(float(anchor_parts[1])) + if len(anchor_parts) >= 3: + params[LottieTensor.Index.Transform.Z] = round(float(anchor_parts[2])) + + elif cmd_idx == LottieTensor.CMD_TM: + params[LottieTensor.Index.Tm.A] = int(attrs.get("a", 1)) + #params[LottieTensor.Index.Tm.IX] = float(attrs.get("ix", 2)) + + # Check if animated + a_value = int(attrs.get("a", 1)) + if a_value > 0.5: + current_context = "tm" # Set context for keyframes + else: + current_context = "tm_static" # Different context for static value + + # Don't set special context for a=0, let value command be parsed normally + + # Add value command parsing + elif cmd_idx == LottieTensor.CMD_VALUE: + # Parse the numeric value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Value.VALUE] = round(float(value_parts[0])) + + + + elif cmd_idx == LottieTensor.CMD_SKEW: + # Handle standalone skew command + # Parse the numeric value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + else: + params[LottieTensor.Index.SingleValue.VALUE] = 0 + + elif cmd_idx == LottieTensor.CMD_SKEW_AXIS: + # Handle standalone skew_axis command + # Parse the numeric value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + else: + params[LottieTensor.Index.SingleValue.VALUE] = 0 + + elif cmd_idx == LottieTensor.CMD_KEYFRAME: + params[LottieTensor.Index.Keyframe.T] = round(float(attrs.get("t", 0))) + + # Check for h parameter (hold keyframe) - for ALL contexts, not just tm + h_value = attrs.get("h", "0") + is_hold = h_value == "1" or h_value.lower() == "true" + + # Store h parameter flag using dedicated H_FLAG slot + if is_hold: + params[LottieTensor.Index.Keyframe.H_FLAG] = 1.0 # Using H_FLAG to store h flag + + # Parse s parameter based on context - check if s exists + if "s" in attrs: + s_str = attrs.get("s", "0").strip('"') + s_parts = s_str.split() + + if current_context in ["rotation", "opacity", "position_x", "position_y", "position_z", "tm", "width", + "trim_start", "trim_end", "trim_offset", "mask_x", "rotation_animators", "opacity_animators", "tracking_animators", "rect_rounded"]: # Added trim contexts + # For single-value properties, only use S1 + if s_parts: + params[LottieTensor.Index.Keyframe.S1] = round(float(s_parts[0])) + elif current_context in ["position", "scale", "anchor", "scale_animators", "position_animators", "size"]: + # For position/scale/anchor, use x,y,z values + for i, part in enumerate(s_parts[:3]): + params[LottieTensor.Index.Keyframe.S1 + i] = round(float(part)) + elif current_context == "path": + # Path keyframes don't have s parameter + pass + else: + # Default case + for i, part in enumerate(s_parts[:3]): + params[LottieTensor.Index.Keyframe.S1 + i] = round(float(part)) + + + # Parse e parameter based on context - ADD THIS FOR TRIM CONTEXTS + if "e" in attrs: + e_str = attrs.get("e", "0").strip('"') + # Remove brackets if present + if e_str.startswith("[") and e_str.endswith("]"): + e_str = e_str[1:-1] + + e_parts = e_str.split(',') + + if current_context in ["trim_start", "trim_end", "trim_offset"]: + # For trim contexts, e is a single value + if e_parts: + params[LottieTensor.Index.Keyframe.E1] = round(float(e_parts[0].strip())) + elif current_context == "rotation": + # For rotation, e is a single value + if e_parts: + params[LottieTensor.Index.Keyframe.E1] = round(float(e_parts[0].strip())) + elif current_context == "scale": + # For scale, e has three values + for i, part in enumerate(e_parts[:3]): + params[LottieTensor.Index.Keyframe.E1 + i] = round(float(part.strip())) + # Add other contexts as needed + + # Only parse easing parameters if not a hold keyframe + if not is_hold: + # Parse easing parameters based on context + if current_context in ["anchor", "size"]: + # For multi-dimensional properties, parse multiple easing values + i_x_values = LottieTensor._parse_multi_easing_values(attrs.get("i_x", "0")) + i_y_values = LottieTensor._parse_multi_easing_values(attrs.get("i_y", "0")) + o_x_values = LottieTensor._parse_multi_easing_values(attrs.get("o_x", "0")) + o_y_values = LottieTensor._parse_multi_easing_values(attrs.get("o_y", "0")) + + # Store all three values - MULTIPLY BY 100 AND ROUND + params[LottieTensor.Index.Keyframe.I_X] = round(i_x_values[0] * 100) + params[LottieTensor.Index.Keyframe.I_X2] = round(i_x_values[1] * 100) + params[LottieTensor.Index.Keyframe.I_X3] = round(i_x_values[2] * 100) + + params[LottieTensor.Index.Keyframe.I_Y] = round(i_y_values[0] * 100) + params[LottieTensor.Index.Keyframe.I_Y2] = round(i_y_values[1] * 100) + params[LottieTensor.Index.Keyframe.I_Y3] = round(i_y_values[2] * 100) + + params[LottieTensor.Index.Keyframe.O_X] = round(o_x_values[0] * 100) + params[LottieTensor.Index.Keyframe.O_X2] = round(o_x_values[1] * 100) + params[LottieTensor.Index.Keyframe.O_X3] = round(o_x_values[2] * 100) + + params[LottieTensor.Index.Keyframe.O_Y] = round(o_y_values[0] * 100) + params[LottieTensor.Index.Keyframe.O_Y2] = round(o_y_values[1] * 100) + params[LottieTensor.Index.Keyframe.O_Y3] = round(o_y_values[2] * 100) + else: + # For single-dimensional properties, parse single easing values - MULTIPLY BY 100 AND ROUND + params[LottieTensor.Index.Keyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0")) * 100)) + params[LottieTensor.Index.Keyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0")) * 100)) + params[LottieTensor.Index.Keyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0")) * 100)) + params[LottieTensor.Index.Keyframe.O_Y] =round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0")) * 100)) + + # Parse to/ti parameters (always use TO1-TO3 and TI1-TI3 for their actual values) + to_str = attrs.get("to", "") + ti_str = attrs.get("ti", "") + + if to_str: + to_values = LottieTensor._extract_array_values(to_str, 3) + for i in range(3): + params[LottieTensor.Index.Keyframe.TO1 + i] = to_values[i] + + if ti_str: + ti_values = LottieTensor._extract_array_values(ti_str, 3) + for i in range(3): + params[LottieTensor.Index.Keyframe.TI1 + i] = ti_values[i] + # Parse e parameter based on context + + if "e" in attrs: + e_str = attrs.get("e", "0").strip('"') + # Remove brackets if present + if e_str.startswith("[") and e_str.endswith("]"): + e_str = e_str[1:-1] + + e_parts = e_str.split(',') + + if current_context == "rotation": + # For rotation, e is a single value + if e_parts: + params[LottieTensor.Index.Keyframe.E1] = round(float(e_parts[0].strip())) + elif current_context == "scale": + # For scale, e has three values + for i, part in enumerate(e_parts[:3]): + params[LottieTensor.Index.Keyframe.E1 + i] = round(float(part.strip())) + # Add other contexts as needed + + + elif cmd_idx == LottieTensor.CMD_GROUP: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Group") + #string_params[f"{cmd_key}_mn"] = attrs.get("mn", "ADBE Vector Group") + + params[LottieTensor.Index.Group.IX] = int(attrs.get("ix", 1)) #0-1000 + params[LottieTensor.Index.Group.CIX] = int(attrs.get("cix", 2)) #1-10 + params[LottieTensor.Index.Group.BM] = int(attrs.get("bm", 0)) #0-1 + params[LottieTensor.Index.Group.HD] = 1.0 if attrs.get("hd", "false").lower() == "true" else 0.0 #0-1 + params[LottieTensor.Index.Group.NP] = int(attrs.get("np", 0)) # 0-1000 + + + elif cmd_idx == LottieTensor.CMD_PATH: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Path") + #string_params[f"{cmd_key}_mn"] = attrs.get("mn", "ADBE Vector Path") # Add mn + + params[LottieTensor.Index.Path.IX] = int(attrs.get("ix", 1)) # 0-1000 + params[LottieTensor.Index.Path.IND] = int(attrs.get("ind", 0)) + params[LottieTensor.Index.Path.KS_IX] = int(attrs.get("ks_ix", 2))# 2-2 + params[LottieTensor.Index.Path.CLOSED] = 1.0 if attrs.get("closed", "true").lower() == "true" else 0.0 + params[LottieTensor.Index.Path.HD] = 1.0 if attrs.get("hd", "false").lower() == "true" else 0.0 # Add HD + + # Track if path is animated + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Path.ANIMATED] = 1.0 + current_context = "path" + else: + params[LottieTensor.Index.Path.ANIMATED] = 0.0 + + + elif cmd_idx == LottieTensor.CMD_POINT: + params[LottieTensor.Index.Point.X] = round(float(attrs.get("x", 0))) + params[LottieTensor.Index.Point.Y] = round(float(attrs.get("y", 0))) + params[LottieTensor.Index.Point.IN_X] = round(float(attrs.get("in_x", 0))) + params[LottieTensor.Index.Point.IN_Y] = round(float(attrs.get("in_y", 0))) + params[LottieTensor.Index.Point.OUT_X] = round(float(attrs.get("out_x", 0))) + params[LottieTensor.Index.Point.OUT_Y] = round(float(attrs.get("out_y", 0))) + + + elif cmd_idx == LottieTensor.CMD_FILL: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Fill") + + # Check if color is animated + color_animated = attrs.get("color_animated", "false").lower() == "true" + params[LottieTensor.Index.Fill.COLOR_ANIMATED] = 1.0 if color_animated else 0.0 + + # Check if opacity is animated + opacity_animated = attrs.get("opacity_animated", "false").lower() == "true" + params[LottieTensor.Index.Fill.OPACITY_ANIMATED] = 1.0 if opacity_animated else 0.0 + + # Parse color keyframes if animated + if color_animated: + c_kf_count = int(attrs.get("c_kf_count", 0)) + color_keyframes = [] + for i in range(c_kf_count): + kf = { + 't': round(float(attrs.get(f"c_kf_{i}_t", 0))), + 'r': round(float(attrs.get(f"c_kf_{i}_r", 0))*255), + 'g': round(float(attrs.get(f"c_kf_{i}_g", 0))*255), + 'b': round(float(attrs.get(f"c_kf_{i}_b", 0))*255), + # Add easing parameters for color keyframes (multiply by 100 and round) + 'i_x': round(float(attrs.get(f"c_kf_{i}_i_x", 0)) * 100), + 'i_y': round(float(attrs.get(f"c_kf_{i}_i_y", 0)) * 100), + 'o_x': round(float(attrs.get(f"c_kf_{i}_o_x", 0)) * 100), + 'o_y': round(float(attrs.get(f"c_kf_{i}_o_y", 0)) * 100) + } + color_keyframes.append(kf) + # Store in string_params as JSON + #string_params[f"{cmd_key}_color_keyframes"] = json.dumps(color_keyframes) + else: + # Parse static color + params[LottieTensor.Index.Fill.R] = round(float(attrs.get("r", 0))*255) + params[LottieTensor.Index.Fill.G] = round(float(attrs.get("g", 0))*255) + params[LottieTensor.Index.Fill.B] = round(float(attrs.get("b", 0))*255) + + # Parse opacity keyframes if animated + if opacity_animated: + o_kf_count = int(attrs.get("o_kf_count", 0)) + opacity_keyframes = [] + for i in range(o_kf_count): + kf = { + 't': round(float(attrs.get(f"o_kf_{i}_t", 0))), + 's': round(float(attrs.get(f"o_kf_{i}_s", 100))), + 'i_x': round(float(attrs.get(f"o_kf_{i}_i_x", 0)) * 100), + 'i_y': round(float(attrs.get(f"o_kf_{i}_i_y", 0)) * 100), + 'o_x': round(float(attrs.get(f"o_kf_{i}_o_x", 0)) * 100), + 'o_y': round(float(attrs.get(f"o_kf_{i}_o_y", 0)) * 100) + } + opacity_keyframes.append(kf) + # Store in string_params as JSON + #string_params[f"{cmd_key}_opacity_keyframes"] = json.dumps(opacity_keyframes) + else: + # Parse static opacity + params[LottieTensor.Index.Fill.OPACITY] = round(float(attrs.get("opacity", 100))) + + # Parse other parameters + params[LottieTensor.Index.Fill.COLOR_DIM] = int(attrs.get("color_dim", 3)) + params[LottieTensor.Index.Fill.HAS_C_A] = 1.0 if attrs.get("has_c_a", "").lower() == "true" else 0.0 + params[LottieTensor.Index.Fill.HAS_C_IX] = 1.0 if attrs.get("has_c_ix", "").lower() == "true" else 0.0 + params[LottieTensor.Index.Fill.C_IX] = int(attrs.get("c_ix", 4)) + params[LottieTensor.Index.Fill.BM] = int(attrs.get("bm", 0)) + params[LottieTensor.Index.Fill.FILL_RULE] = int(attrs.get("fill_rule", 1)) + params[LottieTensor.Index.Fill.HAS_O_A] = 1.0 if attrs.get("has_o_a", "").lower() == "true" else 0.0 + params[LottieTensor.Index.Fill.HAS_O_IX] = 1.0 if attrs.get("has_o_ix", "").lower() == "true" else 0.0 + params[LottieTensor.Index.Fill.O_IX] = int(attrs.get("o_ix", 5)) + + + elif cmd_idx == LottieTensor.CMD_BEZIER: + # Parse the closed attribute from input + closed_str = attrs.get("closed", "true") + params[LottieTensor.Index.Bezier.CLOSED] = 1.0 if closed_str.lower() == "true" else 0.0 + + #elif cmd_idx == LottieTensor.CMD_ELLIPSE: + #name = string_params.get(f"{cmd_key}_name", "Ellipse Path 1") + #lines.append(f'({cmd} name="{name}")') + + + elif cmd_idx in [LottieTensor.CMD_POSITION, LottieTensor.CMD_SIZE]: + if cmd == "size": + # Check if size is animated + if attrs.get("animated", "").lower() == "true": + # Don't parse values when animated + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + current_context = "size" # Set context for size keyframes + else: + # Explicitly set ANIMATED to 0.0 for static size + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + # Parse two values for static size + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + + if len(value_parts) >= 1: + params[LottieTensor.Index.Transform.X] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.Transform.Y] = round(float(value_parts[1])) + + else: + # Handle position as before + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + + if len(value_parts) >= 1: + params[LottieTensor.Index.TwoValues.VALUE1] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.TwoValues.VALUE2] = round(float(value_parts[1])) + + + + elif cmd_idx == LottieTensor.CMD_RECT: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Rectangle Path 1") + + params[LottieTensor.Index.Rect.HD] = 1.0 if attrs.get("hd", "false").lower() == "true" else 0.0 + params[LottieTensor.Index.Rect.D] = int(attrs.get("d", 1)) + + elif cmd_idx == LottieTensor.CMD_ROUNDED: + # Parse rounded value and ix + rounded_val = round(float(attrs.get("rounded", attrs_str.split()[0] if attrs_str.split() else "0"))) + params[LottieTensor.Index.SingleValue.VALUE] = rounded_val + params[LottieTensor.Index.SingleValue.IX] = int(attrs.get("ix", 4)) + + + # ADD THIS NEW SECTION: + elif cmd_idx == LottieTensor.CMD_RECT_ROUNDED: + # Check if animated + if "animated" in attrs and attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.SingleValue.ANIMATED] = 1.0 + current_context = "rect_rounded" # Set context for keyframes + else: + params[LottieTensor.Index.SingleValue.ANIMATED] = 0.0 + # Parse the value if not animated + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + + elif cmd_idx == LottieTensor.CMD_TRIM: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Trim Paths 1") + params[LottieTensor.Index.Trim.IX] = int(attrs.get("ix", 1)) + + elif cmd_idx == LottieTensor.CMD_END: + # Handle trim sub-commands + # Check if animated + if "animated" in attrs and attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.SingleValue.ANIMATED] = 1.0 + current_context = "trim_end" # Set context for keyframes + # Don't parse value when animated=true + else: + params[LottieTensor.Index.SingleValue.ANIMATED] = 0.0 + # Parse the value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_START: + # Handle trim sub-commands + # Check if animated + if "animated" in attrs and attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.SingleValue.ANIMATED] = 1.0 + current_context = "trim_start" # Set context for keyframes + # Don't parse value when animated=true + else: + params[LottieTensor.Index.SingleValue.ANIMATED] = 0.0 + # Parse the value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + elif cmd_idx == LottieTensor.CMD_OFFSET: + # Handle trim sub-commands + # Check if animated + if "animated" in attrs and attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.SingleValue.ANIMATED] = 1.0 + current_context = "trim_offset" # Set context for keyframes + # Don't parse value when animated=true + else: + params[LottieTensor.Index.SingleValue.ANIMATED] = 0.0 + # Parse the value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + + elif cmd_idx == LottieTensor.CMD_MULTIPLE: + # Parse multiple value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_REPEATER: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Repeater 1") + params[LottieTensor.Index.Repeater.IX] = int(attrs.get("ix", 1)) + + elif cmd_idx == LottieTensor.CMD_COPIES: + # Parse copies value and ix + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + params[LottieTensor.Index.SingleValue.IX] = int(attrs.get("ix", 1)) + + elif cmd_idx == LottieTensor.CMD_REPEATER_OFFSET: + # Parse repeater_offset value and ix + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + params[LottieTensor.Index.SingleValue.IX] = int(attrs.get("ix", 2)) + + elif cmd_idx == LottieTensor.CMD_COMPOSITE: + # Parse composite value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_TR_SCALE: + # Parse tr_scale values + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if len(value_parts) >= 1: + params[LottieTensor.Index.TwoValues.VALUE1] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.TwoValues.VALUE2] = round(float(value_parts[1])) + + elif cmd_idx in [LottieTensor.CMD_TR_P_IX, LottieTensor.CMD_TR_A_IX, LottieTensor.CMD_TR_S_IX, + LottieTensor.CMD_TR_R_IX, LottieTensor.CMD_TR_SO_IX, LottieTensor.CMD_TR_EO_IX]: + # Parse single index value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_TRANSFORM_SHAPE: + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Transform") + + # Parse hd attribute + params[LottieTensor.Index.TransformShape.HD] = 1.0 if attrs.get("hd", "false").lower() == "true" else 0.0 + + # Parse position + position_str = attrs.get("position", "0 0").strip('"') + pos_parts = position_str.split() + if len(pos_parts) >= 1: + params[LottieTensor.Index.TransformShape.POSITION_X] = round(float(pos_parts[0])) + if len(pos_parts) >= 2: + params[LottieTensor.Index.TransformShape.POSITION_Y] = round(float(pos_parts[1])) + + # Parse scale + scale_str = attrs.get("scale", "100 100").strip('"') + scale_parts = scale_str.split() + if len(scale_parts) >= 1: + params[LottieTensor.Index.TransformShape.SCALE_X] = round(float(scale_parts[0])) + if len(scale_parts) >= 2: + params[LottieTensor.Index.TransformShape.SCALE_Y] = round(float(scale_parts[1])) + + # Parse rotation + rotation_str = attrs.get("rotation", "0").strip('"') + params[LottieTensor.Index.TransformShape.ROTATION] = round(float(rotation_str)) + + # Parse opacity + opacity_str = attrs.get("opacity", "100").strip('"') + params[LottieTensor.Index.TransformShape.OPACITY] = round(float(opacity_str)) + + # Parse anchor + anchor_str = attrs.get("anchor", "0 0").strip('"') + anchor_parts = anchor_str.split() + if len(anchor_parts) >= 1: + params[LottieTensor.Index.TransformShape.ANCHOR_X] = round(float(anchor_parts[0])) + if len(anchor_parts) >= 2: + params[LottieTensor.Index.TransformShape.ANCHOR_Y] = round(float(anchor_parts[1])) + + # Parse skew (only if present) + if "skew" in attrs: + skew_str = attrs.get("skew", "0").strip('"') + params[LottieTensor.Index.TransformShape.SKEW] = round(float(skew_str)) + + # Parse skew_axis (only if present) + if "skew_axis" in attrs: + skew_axis_str = attrs.get("skew_axis", "0").strip('"') + params[LottieTensor.Index.TransformShape.SKEW_AXIS] = round(float(skew_axis_str)) + + + elif cmd_idx == LottieTensor.CMD_PARENT: + # Parse parent index + parent_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + parent_parts.append(part) + except ValueError: + pass + + if parent_parts: + params[LottieTensor.Index.Parent.PARENT_INDEX] = round(float(parent_parts[0])) + + elif cmd_idx == LottieTensor.CMD_ASSET: + if LottieTensor.tokenizer is None: + LottieTensor.init_tokenizer() + + asset_id = attrs.get("id", "comp_0") + id_tokens = LottieTensor.tokenizer.encode(asset_id, add_special_tokens=False)[:10] # Limit to 10 tokens + + for i, token_id in enumerate(id_tokens): + if i < 10: + params[LottieTensor.Index.Asset.ID_TOKEN_0 + i] = int(token_id) + params[LottieTensor.Index.Asset.ID_TOKEN_COUNT] = int(len(id_tokens)) + + + #string_params[f"{cmd_key}_id"] = attrs.get("id", "comp_0") + #string_params[f"{cmd_key}_nm"] = attrs.get("nm", "asset") + + params[LottieTensor.Index.Asset.FR] = round(float(attrs.get("fr", 30))) + elif cmd_idx == LottieTensor.CMD_FONT: + # å­˜å‚Øå­—ē¬¦äø²å‚ę•° + #string_params[f"{cmd_key}_family"] = attrs.get("family", "") + #string_params[f"{cmd_key}_style"] = attrs.get("style", "") + if LottieTensor.tokenizer is None: + LottieTensor.init_tokenizer() + + # 编码family和style + family = attrs.get("family", "") + style = attrs.get("style", "") + + family_tokens = LottieTensor.tokenizer.encode(family, add_special_tokens=False)[:10] + style_tokens = LottieTensor.tokenizer.encode(style, add_special_tokens=False)[:10] + + params[LottieTensor.Index.Font.ASCENT] = round(float(attrs.get("ascent", 75))) + + for i, token_id in enumerate(family_tokens): + if i < 10: + params[LottieTensor.Index.Font.FAMILY_TOKEN_0 + i] = int(token_id) + params[LottieTensor.Index.Font.FAMILY_TOKEN_COUNT] = int(len(family_tokens)) + + # Store style tokens + for i, token_id in enumerate(style_tokens): + if i < 10: + params[LottieTensor.Index.Font.STYLE_TOKEN_0 + i] = int(token_id) + params[LottieTensor.Index.Font.STYLE_TOKEN_COUNT] = int(len(style_tokens)) + + + elif cmd_idx == LottieTensor.CMD_CHAR: + if LottieTensor.tokenizer is None: + LottieTensor.init_tokenizer() + + # ē‰¹ę®Šå¤„ē† ch å±žę€§ļ¼Œå› äøŗåÆčƒ½åŒ…å«ē‰¹ę®Šå­—ē¬¦å¦‚ ")" + ch_match = re.search(r'ch="([^"]*)"', attrs_str) + ch = ch_match.group(1) if ch_match else attrs.get("ch", "") + + # č§£ęžå…¶ä»–å±žę€§ę—¶ļ¼Œéœ€č¦å…ˆē§»é™¤ ch å±žę€§ä»„éæå…å¹²ę‰° + temp_attrs_str = attrs_str + if ch_match: + temp_attrs_str = attrs_str[:ch_match.start()] + attrs_str[ch_match.end():] + + # é‡ę–°č§£ęžå…¶ä»–å±žę€§ + temp_attrs = LottieTensor._parse_attributes(temp_attrs_str) + + style = temp_attrs.get("style", "") + family = temp_attrs.get("family", "") + + # Encode strings + ch_tokens = LottieTensor.tokenizer.encode(ch, add_special_tokens=False)[:10] + style_tokens = LottieTensor.tokenizer.encode(style, add_special_tokens=False)[:10] + family_tokens = LottieTensor.tokenizer.encode(family, add_special_tokens=False)[:10] + + params[LottieTensor.Index.Char.SIZE] = round(float(temp_attrs.get("size", 100))) + params[LottieTensor.Index.Char.W] = round(float(temp_attrs.get("w", 0))) + + # Store ch tokens + for i, token_id in enumerate(ch_tokens): + if i < 10: + params[LottieTensor.Index.Char.CH_TOKEN_0 + i] = int(token_id) + params[LottieTensor.Index.Char.CH_TOKEN_COUNT] = int(len(ch_tokens)) + + # Store style tokens + for i, token_id in enumerate(style_tokens): + if i < 10: + params[LottieTensor.Index.Char.STYLE_TOKEN_0 + i] = int(token_id) + params[LottieTensor.Index.Char.STYLE_TOKEN_COUNT] = int(len(style_tokens)) + + # Store family tokens + for i, token_id in enumerate(family_tokens): + if i < 10: + params[LottieTensor.Index.Char.FAMILY_TOKEN_0 + i] = int(token_id) + params[LottieTensor.Index.Char.FAMILY_TOKEN_COUNT] = int(len(family_tokens)) + + elif cmd_idx == LottieTensor.CMD_FONT_SIZE: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.FontSize.SIZE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_FONT_FAMILY: + # ęå–å¼•å·å†…ēš„å­—ä½“ę—åē§° + family_match = re.search(r'"([^"]*)"', attrs_str) + if family_match: + string_params[f"{cmd_key}_family"] = family_match.group(1) + + elif cmd_idx == LottieTensor.CMD_TEXT: + # ęå–å¼•å·å†…ēš„ę–‡ęœ¬ + text_match = re.search(r'"([^"]*)"', attrs_str) + if text_match: + string_params[f"{cmd_key}_text"] = text_match.group(1) + + elif cmd_idx == LottieTensor.CMD_CA: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Ca.VALUE] = int(value_parts[0]) + + elif cmd_idx == LottieTensor.CMD_JUSTIFY: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Justify.VALUE] = int(value_parts[0]) + + elif cmd_idx == LottieTensor.CMD_TRACKING: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Tracking.VALUE] = int(value_parts[0]) + + elif cmd_idx == LottieTensor.CMD_LINE_HEIGHT: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.LineHeight.VALUE] = int(value_parts[0]) + + elif cmd_idx == LottieTensor.CMD_LETTER_SPACING: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.LetterSpacing.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_FILL_COLOR: + # ęå–RGB值 + value_parts = [] + for part in attrs_str.split(): + try: + round((part)) + value_parts.append(part) + except ValueError: + pass + if len(value_parts) >= 1: + params[LottieTensor.Index.FillColor.R] = round(float(value_parts[0]) * 255) + if len(value_parts) >= 2: + params[LottieTensor.Index.FillColor.G] = round(float(value_parts[1]) * 255) + if len(value_parts) >= 3: + params[LottieTensor.Index.FillColor.B] = round(float(value_parts[2]) * 255) + + elif cmd_idx == LottieTensor.CMD_G: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.G.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT: + params[LottieTensor.Index.Alignment.A] = round(float(attrs.get("a", 0))) + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT_K: + # ęå–äø¤äøŖę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if len(value_parts) >= 1: + params[LottieTensor.Index.AlignmentK.VALUE1] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.AlignmentK.VALUE2] = round(float(value_parts[1])) + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT_IX: + # ęå–ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.AlignmentIx.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_EFFECTS: + # effectså®¹å™Øå‘½ä»¤ļ¼Œäøéœ€č¦å‚ę•° + pass + + elif cmd_idx == LottieTensor.CMD_EFFECT: + # Store string parameters + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + #string_params[f"{cmd_key}_match_name"] = attrs.get("match_name", "") + + # Store numeric parameters + params[LottieTensor.Index.Effect.TYPE] = int(attrs.get("type", 0)) + params[LottieTensor.Index.Effect.INDEX] = round(float(attrs.get("index", 1))) + params[LottieTensor.Index.Effect.NP] = int(attrs.get("np", 0)) # Add NP + params[LottieTensor.Index.Effect.ENABLED] = round(float(attrs.get("enabled", 1))) # Add ENABLED + + # Add CMD_LAYER_EFFECT parsing: + elif cmd_idx == LottieTensor.CMD_LAYER_EFFECT: + # Store string parameters + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + #string_params[f"{cmd_key}_match_name"] = attrs.get("match_name", "") + + # Store numeric parameters + params[LottieTensor.Index.LayerEffect.INDEX] = round(float(attrs.get("index", 1))) + params[LottieTensor.Index.LayerEffect.VALUE] = round(float(attrs.get("value", 0))) + + + elif cmd_idx == LottieTensor.CMD_DROPDOWN: + # å­˜å‚Øå­—ē¬¦äø²å‚ę•° + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + + # å­˜å‚Øę•°å€¼å‚ę•° + params[LottieTensor.Index.Dropdown.INDEX] = round(float(attrs.get("index", 1))) + params[LottieTensor.Index.Dropdown.VALUE] = round(float(attrs.get("value", 0))) + + elif cmd_idx == LottieTensor.CMD_NO_VALUE: + # å­˜å‚Øå­—ē¬¦äø²å‚ę•° + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + + # å­˜å‚Øę•°å€¼å‚ę•° + params[LottieTensor.Index.NO_VALUE.INDEX] = round(float(attrs.get("index", 1))) + params[LottieTensor.Index.NO_VALUE.VALUE] = round(float(attrs.get("value", 0))) + + + elif cmd_idx == LottieTensor.CMD_IGNORED: + # å­˜å‚Øå­—ē¬¦äø²å‚ę•° + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + + # å­˜å‚Øę•°å€¼å‚ę•° + params[LottieTensor.Index.Ignored.INDEX] = round(float(attrs.get("index", 1))) + params[LottieTensor.Index.Ignored.VALUE] = round(float(attrs.get("value", 0))) + + elif cmd_idx == LottieTensor.CMD_SLIDER: + # å­˜å‚Øå­—ē¬¦äø²å‚ę•° + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + + # å­˜å‚Øę•°å€¼å‚ę•° + params[LottieTensor.Index.Slider.INDEX] = round(float(attrs.get("index", 1))) + params[LottieTensor.Index.Slider.VALUE] = round(float(attrs.get("value", 0))) + + elif cmd_idx == LottieTensor.CMD_GRADIENT_FILL: + # Store name only for gradient_fill command + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Gradient Fill 1") + # Set context for subsequent commands + current_context = "gradient_fill" + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "gradient_fill": + # Parse opacity value for gradient_fill context + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_FILL_RULE: + # Parse fill_rule value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + + elif cmd_idx == LottieTensor.CMD_START_POINT: + # Parse start_point values + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if len(value_parts) >= 1: + params[LottieTensor.Index.TwoValues.VALUE1] = LottieTensor._clamp_value(round(float(value_parts[0]))) + if len(value_parts) >= 2: + params[LottieTensor.Index.TwoValues.VALUE2] = LottieTensor._clamp_value(round(float(value_parts[1]))) + + + elif cmd_idx == LottieTensor.CMD_END_POINT: + # Parse end_point values + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + + if len(value_parts) >= 1: + params[LottieTensor.Index.TwoValues.VALUE1] = LottieTensor._clamp_value(round(float(value_parts[0]))) + if len(value_parts) >= 2: + params[LottieTensor.Index.TwoValues.VALUE2] = LottieTensor._clamp_value(round(float(value_parts[1]))) + + elif cmd_idx == LottieTensor.CMD_GRADIENT_TYPE: + # Parse gradient_type value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_STAR: + # č§£ęž star å‘½ä»¤ēš„å±žę€§ + #string_params[f"{cmd_key}_name"] = attrs.get("name", "None") + + # č§£ęž d 和 sy å‚ę•° + params[LottieTensor.Index.Star.D] = round(float(attrs.get("d", 1))) + params[LottieTensor.Index.Star.SY] = round(float(attrs.get("sy", 1))) + # IX å‚ę•°å¦‚ęžœå­˜åœØēš„čÆ + #params[LottieTensor.Index.Star.IX] = float(attrs.get("ix", 1)) + + # 3. 添加 inner_radius ē­‰å­å‘½ä»¤ēš„č§£ęž + elif cmd_idx == LottieTensor.CMD_INNER_RADIUS: + # č§£ęžę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_OUTER_RADIUS: + # č§£ęžę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_INNER_ROUNDNESS: + # č§£ęžę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_OUTER_ROUNDNESS: + # č§£ęžę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_POINTS_STAR: # čæ™åŗ”čÆ„ę˜Æ points_star + # č§£ęžę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_STAR_ROTATION: + # č§£ęžę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + + elif cmd_idx == LottieTensor.CMD_HIGHLIGHT_LENGTH: + # Parse highlight_length value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_HIGHLIGHT_ANGLE: + # Parse highlight_angle value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + + elif cmd_idx == LottieTensor.CMD_ORIGINAL_COLORS: + colors_match = re.search(r'\[([^\]]*)\]', attrs_str) + if colors_match: + colors_str = colors_match.group(1) + color_values = [] + for color_part in colors_str.split(','): + try: + color_values.append(round(float(color_part.strip())*255)) + except ValueError: + color_values.append(0) + + # Store up to 24 color values (increased from 18) + for i, val in enumerate(color_values[:48]): + if i < 48: # Make sure we don't exceed our storage capacity + params[LottieTensor.Index.OriginalColors.COLOR_0 + i] = val + + # Store count of colors + params[LottieTensor.Index.OriginalColors.COUNT] = int(len(color_values)) + + + elif cmd_idx == LottieTensor.CMD_COLOR_POINTS: + # Parse color_points value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_GRADIENT_FILL_END: + # Reset context when gradient_fill ends + current_context = None + + + elif cmd_idx == LottieTensor.CMD_GRADIENT_STROKE: + # Store name only - similar to gradient_fill + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Gradient Stroke 1") + # Set context for subsequent commands + current_context = "gradient_stroke" + + # Add handling for individual gradient_stroke sub-commands: + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "gradient_stroke": + # Parse opacity value for gradient_stroke context + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_WIDTH: + value_parts = [] + for part in attrs_str.split(): + try: + float(part) # åŖę£€ęŸ„ę˜Æå¦ę˜Æę•°å­—ļ¼Œäøē”Øround + value_parts.append(part) + except ValueError: + pass + if value_parts: + # 乘仄100äæē•™å°ę•°ē²¾åŗ¦ + value = round(float(value_parts[0]) * 10) + value = max(0, min(10000, value)) # 裁剪 + params[LottieTensor.Index.SingleValue.VALUE] = value + + + elif cmd_idx == LottieTensor.CMD_LINE_CAP: + # Parse line_cap value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_LINE_JOIN: + # Parse line_join value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_MITER_LIMIT: + # Parse miter_limit value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_GRADIENT_STROKE_END: + # Reset context when gradient_stroke ends + current_context = None + + elif cmd_idx == LottieTensor.CMD_COLOR: + # Store string parameters + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Color") + + # Store numeric parameters + params[LottieTensor.Index.Color.INDEX] = round(float(attrs.get("index", 1))) + params[LottieTensor.Index.Color.R] = round(float(attrs.get("r", 0))*255) + params[LottieTensor.Index.Color.G] = round(float(attrs.get("g", 0))*255) + params[LottieTensor.Index.Color.B] = round(float(attrs.get("b", 0))*255) + + + #elif cmd_idx == LottieTensor.CMD_MERGE: + # å­˜å‚Ømergeēš„nameå±žę€§ + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Merge Paths 1") + + elif cmd_idx == LottieTensor.CMD_MERGE_MODE: + # č§£ęžmerge_modeēš„ę•°å€¼ + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.MergeMode.MODE] = round(float(value_parts[0])) + + + + elif cmd_idx == LottieTensor.CMD_MASK: + # Store string attributes + #string_params[f"{cmd_key}_nm"] = attrs.get("nm", "Mask") + + params[LottieTensor.Index.Mask.INDEX] = round(float(attrs.get("index", 0))) + params[LottieTensor.Index.Mask.INV] = 1.0 if attrs.get("inv", "false").lower() == "true" else 0.0 + + # Parse mode attribute + mode = attrs.get("mode", "a") + # Convert mode to numeric for storage + mode_map = {"a": 0, "s": 1, "i": 2, "n": 3} + mode_val = mode_map.get(mode, 0) + params[LottieTensor.Index.Mask.MODE] = round(float(mode_val)) + + elif cmd_idx == LottieTensor.CMD_MASK_PT: + params[LottieTensor.Index.MaskPt.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.MaskPt.IX] = int(attrs.get("ix", 1)) + + + elif cmd_idx == LottieTensor.CMD_MASK_PT_K_C: + # Parse c (closed) attribute - handle both formats + if "true" in attrs_str.lower(): + params[LottieTensor.Index.MaskPtK.C] = 1.0 + elif "false" in attrs_str.lower(): + params[LottieTensor.Index.MaskPtK.C] = 0.0 + else: + # Try to parse from c= format if present + c_str = attrs.get("c", "true") + params[LottieTensor.Index.MaskPtK.C] = 1.0 if c_str.lower() == "true" else 0.0 + + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_K_I, LottieTensor.CMD_MASK_PT_K_O, LottieTensor.CMD_MASK_PT_K_V]: + # Parse all numeric values from the line + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + + # Store up to 20 values (increased from 8) + for i in range(min(20, len(value_parts))): + if i < LottieTensor.PARAM_DIM: # Make sure we don't exceed param dimension + params[LottieTensor.Index.MaskPtKValues.V1 + i] = round(float(value_parts[i])) + + # Store the count of values in string_params for reconstruction + params[LottieTensor.Index.MaskPtKValues.COUNT] = round(float(len(value_parts))) + + + elif cmd_idx == LottieTensor.CMD_MASK_O: + params[LottieTensor.Index.MaskO.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.MaskO.K] = round(float(attrs.get("k", 100))) + params[LottieTensor.Index.MaskO.IX] = int(attrs.get("ix", 3)) + + elif cmd_idx == LottieTensor.CMD_MASK_X: + params[LottieTensor.Index.MaskX.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.MaskX.K] = round(float(attrs.get("k", 0))) + params[LottieTensor.Index.MaskX.IX] = int(attrs.get("ix", 4)) + # ę·»åŠ čæ™éƒØåˆ†ļ¼šę£€ęŸ„ę˜Æå¦ę˜ÆåŠØē”» + if float(attrs.get("a", 0)) > 0.5: + current_context = "mask_x" + + elif cmd_idx == LottieTensor.CMD_MASKS_PROPERTIES: + # Container command, no parameters needed + pass + elif cmd_idx in [LottieTensor.CMD_MASK_PT_K_ARRAY, LottieTensor.CMD_MASK_PT_K_ARRAY_END, + LottieTensor.CMD_MASK_PT_KF_S, LottieTensor.CMD_MASK_PT_KF_S_END, + LottieTensor.CMD_MASK_PT_KF_SHAPE_END, LottieTensor.CMD_MASK_PT_KEYFRAME_END]: + # Container commands, no parameters + pass + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KEYFRAME: + params[LottieTensor.Index.MaskPtKeyframe.INDEX] = int(attrs.get("index", 0)) + params[LottieTensor.Index.MaskPtKeyframe.T] = round(float(attrs.get("t", 0))) + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_I: + params[LottieTensor.Index.MaskPtKfI.X] = round(float(attrs.get("x", 0))) + params[LottieTensor.Index.MaskPtKfI.Y] = round(float(attrs.get("y", 0))) + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_O: + params[LottieTensor.Index.MaskPtKfO.X] = round(float(attrs.get("x", 0))) + params[LottieTensor.Index.MaskPtKfO.Y] = round(float(attrs.get("y", 0))) + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_SHAPE: + params[LottieTensor.Index.MaskPtKfShape.INDEX] = int(attrs.get("index", 0)) + c_str = attrs.get("c", "true") + params[LottieTensor.Index.MaskPtKfShape.C] = 1.0 if c_str.lower() == "true" else 0.0 + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_KF_SHAPE_I, LottieTensor.CMD_MASK_PT_KF_SHAPE_O, + LottieTensor.CMD_MASK_PT_KF_SHAPE_V]: + # Parse all numeric values from the line + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + + # Store up to 20 values (increased from 8) + for i in range(min(20, len(value_parts))): + if i < LottieTensor.PARAM_DIM: # Make sure we don't exceed param dimension + params[LottieTensor.Index.MaskPtKfShapeValues.V1 + i] = round(float(value_parts[i])) + + # Store the count in params, NOT in string_params + params[LottieTensor.Index.MaskPtKfShapeValues.COUNT] = round(float(len(value_parts))) + + + elif cmd_idx == LottieTensor.CMD_TR_POSITION: + # Parse tr_position values + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if len(value_parts) >= 1: + params[LottieTensor.Index.TrPosition.X] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.TrPosition.Y] = round(float(value_parts[1])) + + elif cmd_idx == LottieTensor.CMD_TR_ANCHOR: + # Parse tr_anchor values + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if len(value_parts) >= 1: + params[LottieTensor.Index.TrAnchor.X] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.TrAnchor.Y] = round(float(value_parts[1])) + + elif cmd_idx == LottieTensor.CMD_TR_ROTATION: + # Parse tr_rotation value + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.TrRotation.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_TR_START_OPACITY: + # Parse tr_start_opacity value + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.TrStartOpacity.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_TR_END_OPACITY: + # Parse tr_end_opacity value + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.TrEndOpacity.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_ZIG_ZAG: + # Store string parameters + #string_params[f"{cmd_key}_name"] = attrs.get("name", "Zig Zag 1") + + # Store numeric parameters + params[LottieTensor.Index.ZigZag.IX] = int(attrs.get("ix", 2)) + + elif cmd_idx == LottieTensor.CMD_FREQUENCY: + # Parse frequency value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Frequency.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_AMPLITUDE: + # Parse amplitude value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Amplitude.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_POINT_TYPE: + # Parse point_type value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.PointType.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_ANIMATORS: + # Container command, no parameters + pass + + #elif cmd_idx == LottieTensor.CMD_ANIMATOR: + # Store animator name + #string_params[f"{cmd_key}_nm"] = attrs.get("nm", "Animator 1") + + + elif cmd_idx == LottieTensor.CMD_RANGE_SELECTOR: + params[LottieTensor.Index.RangeSelector.T] = round(float(attrs.get("t", 0))) + params[LottieTensor.Index.RangeSelector.R] = round(float(attrs.get("r", 1))) + params[LottieTensor.Index.RangeSelector.B] = round(float(attrs.get("b", 1))) + params[LottieTensor.Index.RangeSelector.SH] = round(float(attrs.get("sh", 1))) #čæ™é‡Œčæ˜éœ€č¦checkäø‹ + params[LottieTensor.Index.RangeSelector.RN] = round(float(attrs.get("rn", 0))) + + elif cmd_idx == LottieTensor.CMD_RANGE_START: + params[LottieTensor.Index.RangeStart.A] = round(float(attrs.get("a", 0))) + # Check if animated + if float(attrs.get("a", 0)) > 0.5: + current_context = "range_start" + + elif cmd_idx == LottieTensor.CMD_RANGE_START_KEYFRAME: + params[LottieTensor.Index.RangeStartKeyframe.T] = round(float(attrs.get("t", 0))) + params[LottieTensor.Index.RangeStartKeyframe.S] = round(float(attrs.get("s", 0))) + params[LottieTensor.Index.RangeStartKeyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0"))*100)) + params[LottieTensor.Index.RangeStartKeyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0"))*100)) + params[LottieTensor.Index.RangeStartKeyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0"))*100)) + params[LottieTensor.Index.RangeStartKeyframe.O_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0"))*100)) + + + elif cmd_idx == LottieTensor.CMD_AMOUNT: + params[LottieTensor.Index.Amount.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.Amount.K] = round(float(attrs.get("k", 100))) + params[LottieTensor.Index.Amount.IX] = int(attrs.get("ix", 4)) + + elif cmd_idx == LottieTensor.CMD_MAX_EASE: + params[LottieTensor.Index.MaxEase.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.MaxEase.K] = round(float(attrs.get("k", 0))) + params[LottieTensor.Index.MaxEase.IX] = int(attrs.get("ix", 7)) + + elif cmd_idx == LottieTensor.CMD_MIN_EASE: + params[LottieTensor.Index.MinEase.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.MinEase.K] = round(float(attrs.get("k", 0))) + params[LottieTensor.Index.MinEase.IX] = int(attrs.get("ix", 8)) + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_PROPERTIES: + # Container command, no parameters + current_context = "animator_properties" # Set context for opacity handling + pass + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_PROPERTIES_END: + current_context = None # Reset context + pass + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "animator_properties": + # Special handling for opacity within animator_properties + params[LottieTensor.Index.Amount.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.Amount.K] = round(float(attrs.get("k", 0))) + params[LottieTensor.Index.Amount.IX] = int(attrs.get("ix", 9)) + + elif cmd_idx == LottieTensor.CMD_RADIUS: + # Parse radius value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.Radius.VALUE] = round(float(value_parts[0])) + + elif cmd_idx == LottieTensor.CMD_RANGE_END: + params[LottieTensor.Index.RangeEnd.A] = round(float(attrs.get("a", 0))) + # Check if animated + if float(attrs.get("a", 0)) > 0.5: + current_context = "range_end" + + elif cmd_idx == LottieTensor.CMD_RANGE_END_KEYFRAME: + params[LottieTensor.Index.RangeEndKeyframe.T] = round(float(attrs.get("t", 0))) + params[LottieTensor.Index.RangeEndKeyframe.S] = round(float(attrs.get("s", 0))) + params[LottieTensor.Index.RangeEndKeyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0"))*100)) + params[LottieTensor.Index.RangeEndKeyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0"))*100)) + params[LottieTensor.Index.RangeEndKeyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0"))*100)) + params[LottieTensor.Index.RangeEndKeyframe.O_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0"))*100)) + + + elif cmd_idx == LottieTensor.CMD_POSITION and current_context == "animator_properties": + # Special handling for position within animator_properties + params[LottieTensor.Index.Amount.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.Amount.IX] = int(attrs.get("ix", 2)) + + # Parse k parameter which can be an array + k_str = attrs.get("k", "0") + if k_str.startswith("[") and k_str.endswith("]"): + # Parse array values + k_str = k_str[1:-1] # Remove brackets + k_parts = k_str.split(",") + if len(k_parts) >= 1: + params[LottieTensor.Index.Transform.X] = round(float(k_parts[0].strip())) + if len(k_parts) >= 2: + params[LottieTensor.Index.Transform.Y] = round(float(k_parts[1].strip())) + if len(k_parts) >= 3: + params[LottieTensor.Index.Transform.Z] = round(float(k_parts[2].strip())) + else: + params[LottieTensor.Index.Amount.K] = round(float(k_str)) + + elif cmd_idx == LottieTensor.CMD_ML2: + # Parse ml2 value + value_parts = [] + for part in attrs_str.split(): + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + if value_parts: + params[LottieTensor.Index.SingleValue.VALUE] = round(float(value_parts[0])) + + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: + params[LottieTensor.Index.RangeOffsetKeyframe.T] = round(float(attrs.get("t", 0))) + params[LottieTensor.Index.RangeOffsetKeyframe.S] = round(float(attrs.get("s", 0))) + params[LottieTensor.Index.RangeOffsetKeyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0"))*100)) + params[LottieTensor.Index.RangeOffsetKeyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0"))*100)) + params[LottieTensor.Index.RangeOffsetKeyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0"))*100)) + params[LottieTensor.Index.RangeOffsetKeyframe.O_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0"))*100)) + + elif cmd_idx == LottieTensor.CMD_S_M: + params[LottieTensor.Index.SM.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.SM.K] = round(float(attrs.get("k", 100))) + params[LottieTensor.Index.SM.IX] = int(attrs.get("ix", 6)) + + + elif cmd_idx == LottieTensor.CMD_OPACITY_ANIMATORS: + params[LottieTensor.Index.OpacityAnimators.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.OpacityAnimators.IX] = int(attrs.get("ix", 9)) + + # If not animated (a=0), parse k value + if float(attrs.get("a", 0)) < 0.5: + params[LottieTensor.Index.OpacityAnimators.K] = round(float(attrs.get("k", 0))) + else: + # Set context for animated keyframes + current_context = "opacity_animators" + + elif cmd_idx == LottieTensor.CMD_POSITION_ANIMATORS_END: + current_context = None + + # 添加 CMD_POSITION_ANIMATORS å¤„ē†ļ¼š + elif cmd_idx == LottieTensor.CMD_POSITION_ANIMATORS: + params[LottieTensor.Index.PositionAnimators.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.PositionAnimators.IX] = int(attrs.get("ix", 2)) + + # If not animated (a=0), parse k value + if float(attrs.get("a", 0)) < 0.5: + # Parse k value which could be a single value or array + k_str = attrs.get("k", "0") + if k_str.startswith("[") and k_str.endswith("]"): + # Parse array values + k_str = k_str[1:-1] # Remove brackets + k_parts = k_str.split(",") + if len(k_parts) >= 1: + params[LottieTensor.Index.PositionAnimators.K_X] = round(float(k_parts[0].strip())) + if len(k_parts) >= 2: + params[LottieTensor.Index.PositionAnimators.K_Y] = round(float(k_parts[1].strip())) + if len(k_parts) >= 3: + params[LottieTensor.Index.PositionAnimators.K_Z] = round(float(k_parts[2].strip())) + else: + # Single value - apply to all dimensions + try: + k_val = round(float(k_str)) + params[LottieTensor.Index.PositionAnimators.K_X] = k_val + params[LottieTensor.Index.PositionAnimators.K_Y] = k_val + params[LottieTensor.Index.PositionAnimators.K_Z] = k_val + except ValueError: + # Default to 0 if parsing fails + params[LottieTensor.Index.PositionAnimators.K_X] = 0 + params[LottieTensor.Index.PositionAnimators.K_Y] = 0 + params[LottieTensor.Index.PositionAnimators.K_Z] = 0 + else: + # Set context for animated keyframes + current_context = "position_animators" + + + # 添加 CMD_TRACKING_ANIMATORS å¤„ē†ļ¼š + elif cmd_idx == LottieTensor.CMD_TRACKING_ANIMATORS: + params[LottieTensor.Index.TrackingAnimators.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.TrackingAnimators.K] = round(float(attrs.get("k", 0))) + params[LottieTensor.Index.TrackingAnimators.IX] = int(attrs.get("ix", 89)) + + # If animated, set context + if float(attrs.get("a", 0)) > 0.5: + current_context = "tracking_animators" + + + elif cmd_idx == LottieTensor.CMD_SCALE_ANIMATORS: + params[LottieTensor.Index.ScaleAnimators.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.ScaleAnimators.IX] = int(attrs.get("ix", 3)) + + # If not animated (a=0), parse k value + if float(attrs.get("a", 0)) < 0.5: + # Parse k value which could be a single value or array + k_str = attrs.get("k", "100") + if k_str.startswith("[") and k_str.endswith("]"): + # Parse array values + k_str = k_str[1:-1] # Remove brackets + k_parts = k_str.split(",") + if len(k_parts) >= 1: + params[LottieTensor.Index.ScaleAnimators.K_X] = round(float(k_parts[0].strip())) + if len(k_parts) >= 2: + params[LottieTensor.Index.ScaleAnimators.K_Y] = round(float(k_parts[1].strip())) + if len(k_parts) >= 3: + params[LottieTensor.Index.ScaleAnimators.K_Z] = round(float(k_parts[2].strip())) + else: + # Single value - apply to all dimensions + k_val = round(float(k_str)) + params[LottieTensor.Index.ScaleAnimators.K_X] = k_val + params[LottieTensor.Index.ScaleAnimators.K_Y] = k_val + params[LottieTensor.Index.ScaleAnimators.K_Z] = k_val + else: + # Set context for animated keyframes + current_context = "scale_animators" + + elif cmd_idx == LottieTensor.CMD_ROTATION_ANIMATORS: + params[LottieTensor.Index.RotationAnimators.A] = int(attrs.get("a", 0)) + params[LottieTensor.Index.RotationAnimators.IX] = int(attrs.get("ix", 4)) + + # If not animated (a=0), parse k value + if float(attrs.get("a", 0)) < 0.5: + params[LottieTensor.Index.RotationAnimators.K] = int(attrs.get("k", 0)) + else: + # Set context for animated keyframes + current_context = "rotation_animators" + + elif cmd_idx == LottieTensor.CMD_SCALE_ANIMATORS_END: + current_context = None + + elif cmd_idx == LottieTensor.CMD_ROTATION_ANIMATORS_END: + current_context = None + + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET: + # Parse the 'a' attribute correctly + params[LottieTensor.Index.Amount.A] = int(attrs.get("a", 0)) + # Check if animated + if float(attrs.get("a", 0)) > 0.5: + # Animated case - set context for keyframes + current_context = "range_offset" + else: + # Static case - also parse k and ix values + params[LottieTensor.Index.Amount.K] = int(attrs.get("k", 0)) + params[LottieTensor.Index.Amount.IX] = int(attrs.get("ix", 3)) + + elif cmd_idx == LottieTensor.CMD_DASHES: + # Container command for dashes + # Parse the entire dashes string if present + dashes_str = attrs_str.strip() + if dashes_str: + string_params[f"{cmd_key}_dashes"] = dashes_str + + + elif cmd_idx == LottieTensor.CMD_DASH: + dash_type = attrs.get("type", "d") + type_map = {"d": 0, "g": 1, "o": 2} + params[LottieTensor.Index.Dash.TYPE] = int(type_map.get(dash_type, 0)) + + # 乘仄100äæē•™å°ę•°ē²¾åŗ¦ + length_val = round(float(attrs.get("length", 0)) * 10) + length_val = max(0, min(10000, length_val)) + params[LottieTensor.Index.Dash.LENGTH] = length_val + + params[LottieTensor.Index.Dash.V_IX] = int(attrs.get("v_ix", 1)) + + + # Store name in string_params + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED: + # Parse dash_animated attributes + dash_type = attrs.get("type", "o") + # Convert type to numeric + type_map = {"d": 0, "g": 1, "o": 2} + params[LottieTensor.Index.DashAnimated.TYPE] = int(type_map.get(dash_type, 2)) + + # Parse v_ix + params[LottieTensor.Index.DashAnimated.V_IX] = int(attrs.get("v_ix", 7)) + + # Store name in string_params + #string_params[f"{cmd_key}_name"] = attrs.get("name", "") + + # Set context for keyframes + current_context = "dash_animated" + + elif cmd_idx == LottieTensor.CMD_DASH_KEYFRAME: + params[LottieTensor.Index.DashKeyframe.T] = round(float(attrs.get("t", 0))) + # ę”¹ęˆä¹˜ä»„10ļ¼Œå¹¶č£å‰Ŗ + s_val = round(float(attrs.get("s", 0)) * 10) + s_val = max(0, min(10000, s_val)) + params[LottieTensor.Index.DashKeyframe.S] = s_val + + # easingå‚ę•°äæęŒäøå˜ + params[LottieTensor.Index.DashKeyframe.I_X] = round(float(LottieTensor._parse_easing_value(attrs.get("i_x", "0"))*100)) + params[LottieTensor.Index.DashKeyframe.I_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("i_y", "0"))*100)) + params[LottieTensor.Index.DashKeyframe.O_X] = round(float(LottieTensor._parse_easing_value(attrs.get("o_x", "0"))*100)) + params[LottieTensor.Index.DashKeyframe.O_Y] = round(float(LottieTensor._parse_easing_value(attrs.get("o_y", "0"))*100)) + + + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED_END: + # Reset context + current_context = None + + elif cmd_idx == LottieTensor.CMD_DASH_OFFSET: + value_parts = [] + for part in attrs_str.split(): + try: + float(part) + value_parts.append(part) + except ValueError: + pass + if value_parts: + # ę”¹ęˆä¹˜ä»„10ļ¼Œå¹¶č£å‰Ŗ + value = round(float(value_parts[0]) * 10) + value = max(0, min(10000, value)) + params[LottieTensor.Index.DashOffset.O] = value + + + + elif cmd_idx == LottieTensor.CMD_DASHES_END: + # End of dashes container + pass + elif cmd_idx == LottieTensor.CMD_SIZE_END: + # End of dashes container + pass + + + elif cmd_idx == LottieTensor.CMD_RECT_SIZE: + # ę£€ęŸ„ę˜Æå¦ę˜ÆåŠØē”» + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + current_context = "size" + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + + if len(value_parts) >= 1: + params[LottieTensor.Index.Transform.X] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.Transform.Y] = round(float(value_parts[1])) + + + + elif cmd_idx == LottieTensor.CMD_ELLIPSE_SIZE: + # ę£€ęŸ„ę˜Æå¦ę˜ÆåŠØē”» + if attrs.get("animated", "").lower() == "true": + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + current_context = "size" # č®¾ē½®äøŠäø‹ę–‡ē”ØäŗŽå…³é”®åø§ + else: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + # č§£ęžé™ę€å€¼ + value_parts = [] + for part in attrs_str.split(): + if '=' not in part: + try: + round(float(part)) + value_parts.append(part) + except ValueError: + pass + + if len(value_parts) >= 1: + params[LottieTensor.Index.Transform.X] = round(float(value_parts[0])) + if len(value_parts) >= 2: + params[LottieTensor.Index.Transform.Y] = round(float(value_parts[1])) + + params_list.append(params) + + + # Convert to tensors + commands_tensor = torch.tensor(commands).long() + params_tensor = torch.tensor(params_list).float() + lottie_tensor = LottieTensor(commands_tensor, params_tensor) + #lottie_tensor.string_params = string_params + + return lottie_tensor + + + def from_sequence_v2(sequence_str)-> 'LottieTensor': + """ + Parse a sequence string back into a Lottie animation dictionary. + Properly handles nested group structures using a stack. + """ + lines = sequence_str.strip().split('\n') + + animation = { + "v": "5.5.2", + "fr": 30, + "ip": 0, + "op": 60, + "w": 512, + "h": 512, + "nm": "Animation", + "ddd": 0, + "assets": [], + "layers": [], + "markers": [], + "props": {}, + "fonts": None, + "chars": None + } + + # Stack for tracking nested structures + # Each entry is (type, object, items_list) + stack = [] + + current_layer = None + current_transform = None + current_path = None + current_path_points = [] + current_keyframes = [] + current_context = None # 'position', 'scale', 'rotation', 'opacity', 'anchor' + + def parse_attrs(attr_str): + """Parse attribute string into dict""" + attrs = {} + if not attr_str: + return attrs + + import re + # Handle quoted values + pattern = r'(\w+)=(?:"([^"]*)"|(\S+))' + for match in re.finditer(pattern, attr_str): + key = match.group(1) + value = match.group(2) if match.group(2) is not None else match.group(3) + attrs[key] = value + + # Handle space-separated values without keys + parts = attr_str.split() + positional = [] + for part in parts: + if '=' not in part: + try: + positional.append(float(part)) + except ValueError: + pass + if positional: + attrs['_positional'] = positional + + return attrs + + def parse_command(line): + """Parse a command line into (command, attrs_str)""" + line = line.strip() + if not line.startswith('(') or not line.endswith(')'): + return None, "" + + content = line[1:-1].strip() + + # Handle end tags + if content.startswith('/'): + return content, "" + + # Handle quoted commands like "TransformShape" + if content.startswith('"'): + end_quote = content.find('"', 1) + if end_quote > 0: + cmd = content[:end_quote+1] + attrs = content[end_quote+1:].strip() + return cmd, attrs + + # Regular command + parts = content.split(' ', 1) + cmd = parts[0] + attrs = parts[1] if len(parts) > 1 else "" + return cmd, attrs + + def get_current_items_container(): + """Get the current container for adding items""" + if stack: + return stack[-1][2] # items list + elif current_layer: + return current_layer.get('shapes', []) + return None + + def add_to_current_container(item): + """Add item to current container""" + container = get_current_items_container() + if container is not None: + container.append(item) + + for line in lines: + cmd, attrs_str = parse_command(line) + if cmd is None: + continue + + attrs = parse_attrs(attrs_str) + + # ========== Animation ========== + if cmd == 'animation': + animation['v'] = attrs.get('v', '5.5.2').strip('"') + animation['fr'] = float(attrs.get('fr', 30)) + animation['ip'] = float(attrs.get('ip', 0)) + animation['op'] = float(attrs.get('op', 60)) + animation['w'] = float(attrs.get('w', 512)) + animation['h'] = float(attrs.get('h', 512)) + animation['ddd'] = int(attrs.get('ddd', 0)) + + # ========== Layer ========== + elif cmd == 'layer': + current_layer = { + 'ddd': int(attrs.get('ddd', 0)), + 'ind': int(float(attrs.get('index', 0))), + 'ty': 4, # Shape layer + 'nm': 'Layer', + 'sr': 1, + 'ks': {}, + 'ao': int(attrs.get('ao', 0)), + 'shapes': [], + 'ip': float(attrs.get('in_point', 0)), + 'op': float(attrs.get('out_point', 60)), + 'st': float(attrs.get('start_time', 0)), + 'bm': 0 + } + animation['layers'].append(current_layer) + stack.clear() # New layer, clear stack + + elif cmd == '/layer': + current_layer = None + stack.clear() + + # ========== Transform ========== + elif cmd == 'transform': + current_transform = { + 'o': {'a': 0, 'k': 100, 'ix': 11}, + 'r': {'a': 0, 'k': 0, 'ix': 10}, + 'p': {'a': 0, 'k': [0, 0, 0], 'ix': 2}, + 'a': {'a': 0, 'k': [0, 0, 0], 'ix': 1}, + 's': {'a': 0, 'k': [100, 100, 100], 'ix': 6} + } + + elif cmd == '/transform': + if current_layer and current_transform: + current_layer['ks'] = current_transform + current_transform = None + current_context = None + + # ========== Transform properties ========== + elif cmd == 'position': + current_context = 'position' + current_keyframes = [] + if 'animated' in attrs and attrs.get('animated', '').lower() == 'true': + if current_transform: + current_transform['p'] = {'a': 1, 'k': [], 'ix': 2} + else: + pos = attrs.get('_positional', [0, 0]) + if current_transform: + current_transform['p'] = {'a': 0, 'k': pos + [0] if len(pos) == 2 else pos, 'ix': 2} + + elif cmd == '/position': + if current_transform and current_keyframes: + current_transform['p'] = {'a': 1, 'k': current_keyframes, 'ix': 2} + current_context = None + current_keyframes = [] + + elif cmd == 'scale': + current_context = 'scale' + current_keyframes = [] + if 'animated' in attrs and attrs.get('animated', '').lower() == 'true': + if current_transform: + current_transform['s'] = {'a': 1, 'k': [], 'ix': 6} + else: + scale = attrs.get('_positional', [100, 100, 100]) + if current_transform: + current_transform['s'] = {'a': 0, 'k': scale, 'ix': 6} + + elif cmd == '/scale': + if current_transform and current_keyframes: + current_transform['s'] = {'a': 1, 'k': current_keyframes, 'ix': 6} + current_context = None + current_keyframes = [] + + elif cmd == 'rotation': + current_context = 'rotation' + current_keyframes = [] + if 'animated' in attrs and attrs.get('animated', '').lower() == 'true': + if current_transform: + current_transform['r'] = {'a': 1, 'k': [], 'ix': 10} + else: + rot = attrs.get('_positional', [0]) + if current_transform: + current_transform['r'] = {'a': 0, 'k': rot[0] if rot else 0, 'ix': 10} + + elif cmd == 'opacity': + current_context = 'opacity' + current_keyframes = [] + if 'animated' in attrs and attrs.get('animated', '').lower() == 'true': + if current_transform: + current_transform['o'] = {'a': 1, 'k': [], 'ix': 11} + else: + op = attrs.get('_positional', [100]) + if current_transform: + current_transform['o'] = {'a': 0, 'k': op[0] if op else 100, 'ix': 11} + + elif cmd == '/opacity': + if current_transform and current_keyframes: + current_transform['o'] = {'a': 1, 'k': current_keyframes, 'ix': 11} + current_context = None + current_keyframes = [] + + elif cmd == 'anchor': + pos = attrs.get('_positional', [0, 0]) + if current_transform: + current_transform['a'] = {'a': 0, 'k': pos + [0] if len(pos) == 2 else pos, 'ix': 1} + + # ========== Keyframes ========== + elif cmd == 'keyframe': + t = float(attrs.get('t', 0)) + + # Parse s value + s_str = attrs.get('s', '0') + if s_str.startswith('"') and s_str.endswith('"'): + s_str = s_str[1:-1] + s_parts = s_str.split() + s_val = [float(x) for x in s_parts] if s_parts else [0] + + keyframe = {'t': t, 's': s_val} + + # Parse easing + i_x = float(attrs.get('i_x', 0)) + i_y = float(attrs.get('i_y', 0)) + o_x = float(attrs.get('o_x', 0)) + o_y = float(attrs.get('o_y', 0)) + + if i_x != 0 or i_y != 0 or o_x != 0 or o_y != 0: + keyframe['i'] = {'x': [i_x], 'y': [i_y]} + keyframe['o'] = {'x': [o_x], 'y': [o_y]} + + # Parse to/ti + if 'to' in attrs: + to_str = attrs['to'].strip('"[]') + to_parts = [float(x.strip()) for x in to_str.split(',') if x.strip()] + if to_parts: + keyframe['to'] = to_parts + + if 'ti' in attrs: + ti_str = attrs['ti'].strip('"[]') + ti_parts = [float(x.strip()) for x in ti_str.split(',') if x.strip()] + if ti_parts: + keyframe['ti'] = ti_parts + + current_keyframes.append(keyframe) + + # ========== Group ========== + elif cmd == 'group': + group = { + 'ty': 'gr', + 'nm': 'Group', + 'np': int(attrs.get('np', 0)), + 'cix': int(attrs.get('cix', 2)), + 'bm': int(attrs.get('bm', 0)), + 'ix': int(attrs.get('ix', 1)), + 'mn': 'ADBE Vector Group', + 'hd': attrs.get('hd', 'false').lower() == 'true', + 'it': [] # Items go here + } + + # Add to current container + add_to_current_container(group) + + # Push onto stack + stack.append(('group', group, group['it'])) + + elif cmd == '/group': + if stack and stack[-1][0] == 'group': + stack.pop() + + # ========== Path ========== + elif cmd == 'path': + current_path = { + 'ty': 'sh', + 'nm': 'Path', + 'mn': 'ADBE Vector Shape - Group', + 'hd': attrs.get('hd', 'false').lower() == 'true', + 'ix': int(attrs.get('ix', 1)), + 'ind': int(attrs.get('ind', 0)), + 'ks': { + 'a': 0, + 'k': { + 'c': attrs.get('closed', 'true').lower() == 'true', + 'v': [], + 'i': [], + 'o': [] + }, + 'ix': int(attrs.get('ks_ix', 2)) + } + } + current_path_points = [] + + elif cmd == '/path': + if current_path: + # Build bezier from points + bezier = current_path['ks']['k'] + for pt in current_path_points: + bezier['v'].append([pt['x'], pt['y']]) + bezier['i'].append([pt['in_x'], pt['in_y']]) + bezier['o'].append([pt['out_x'], pt['out_y']]) + + add_to_current_container(current_path) + current_path = None + current_path_points = [] + + elif cmd == 'point': + pt = { + 'x': float(attrs.get('x', 0)), + 'y': float(attrs.get('y', 0)), + 'in_x': float(attrs.get('in_x', 0)), + 'in_y': float(attrs.get('in_y', 0)), + 'out_x': float(attrs.get('out_x', 0)), + 'out_y': float(attrs.get('out_y', 0)) + } + current_path_points.append(pt) + + # ========== Fill ========== + elif cmd == 'fill': + fill = { + 'ty': 'fl', + 'nm': 'Fill', + 'mn': 'ADBE Vector Graphic - Fill', + 'hd': False, + 'c': { + 'a': 0, + 'k': [ + float(attrs.get('r', 0.5)), + float(attrs.get('g', 0.5)), + float(attrs.get('b', 0.5)), + 1 + ], + 'ix': int(attrs.get('c_ix', 4)) + }, + 'o': { + 'a': 0, + 'k': float(attrs.get('opacity', 100)), + 'ix': int(attrs.get('o_ix', 5)) + }, + 'r': int(attrs.get('fill_rule', 1)), + 'bm': int(attrs.get('bm', 0)) + } + add_to_current_container(fill) + + # ========== Stroke ========== + elif cmd == 'stroke': + stroke = { + 'ty': 'st', + 'nm': 'Stroke', + 'mn': 'ADBE Vector Graphic - Stroke', + 'hd': False, + 'c': { + 'a': 0, + 'k': [ + float(attrs.get('r', 0)), + float(attrs.get('g', 0)), + float(attrs.get('b', 0)), + 1 + ], + 'ix': int(attrs.get('c_ix', 3)) + }, + 'o': { + 'a': 0, + 'k': 100, + 'ix': 4 + }, + 'w': { + 'a': 0, + 'k': float(attrs.get('width', 2)), + 'ix': 5 + }, + 'lc': int(attrs.get('lc', 2)), + 'lj': int(attrs.get('lj', 2)), + 'ml': float(attrs.get('ml', 4)), + 'bm': int(attrs.get('bm', 0)) + } + add_to_current_container(stroke) + + # ========== TransformShape (group transform) ========== + elif cmd == '"TransformShape"': + # Parse position + pos_str = attrs.get('position', '0 0').strip('"') + pos_parts = pos_str.split() + pos = [float(x) for x in pos_parts] if pos_parts else [0, 0] + + # Parse scale + scale_str = attrs.get('scale', '100 100').strip('"') + scale_parts = scale_str.split() + scale = [float(x) for x in scale_parts] if scale_parts else [100, 100] + + # Parse rotation + rot_str = attrs.get('rotation', '0').strip('"') + rot = float(rot_str) + + # Parse opacity + op_str = attrs.get('opacity', '100').strip('"') + opacity = float(op_str) + + # Parse anchor + anchor_str = attrs.get('anchor', '0 0').strip('"') + anchor_parts = anchor_str.split() + anchor = [float(x) for x in anchor_parts] if anchor_parts else [0, 0] + + transform = { + 'ty': 'tr', + 'p': {'a': 0, 'k': pos, 'ix': 2}, + 'a': {'a': 0, 'k': anchor, 'ix': 1}, + 's': {'a': 0, 'k': scale, 'ix': 3}, + 'r': {'a': 0, 'k': rot, 'ix': 6}, + 'o': {'a': 0, 'k': opacity, 'ix': 7}, + 'sk': {'a': 0, 'k': 0, 'ix': 4}, + 'sa': {'a': 0, 'k': 0, 'ix': 5}, + 'nm': 'Transform' + } + + # Parse skew if present + if 'skew' in attrs: + skew_str = attrs.get('skew', '0').strip('"') + transform['sk']['k'] = float(skew_str) + + if 'skew_axis' in attrs: + sa_str = attrs.get('skew_axis', '0').strip('"') + transform['sa']['k'] = float(sa_str) + + add_to_current_container(transform) + + # ========== Rectangle ========== + elif cmd == 'rect': + rect = { + 'ty': 'rc', + 'nm': 'Rectangle', + 'mn': 'ADBE Vector Shape - Rect', + 'hd': attrs.get('hd', 'false').lower() == 'true', + 'd': int(attrs.get('d', 1)), + 'p': {'a': 0, 'k': [0, 0], 'ix': 3}, + 's': {'a': 0, 'k': [100, 100], 'ix': 2}, + 'r': {'a': 0, 'k': 0, 'ix': 4} + } + add_to_current_container(rect) + + elif cmd == '/rect': + pass + + # ========== Ellipse ========== + elif cmd == 'ellipse': + ellipse = { + 'ty': 'el', + 'nm': 'Ellipse', + 'mn': 'ADBE Vector Shape - Ellipse', + 'hd': False, + 'd': 1, + 'p': {'a': 0, 'k': [0, 0], 'ix': 3}, + 's': {'a': 0, 'k': [100, 100], 'ix': 2} + } + add_to_current_container(ellipse) + + elif cmd == '/ellipse': + pass + + return animation + + + + @staticmethod + def _parse_attributes(attrs_str: str) -> Dict[str, str]: + """Parse attribute string to dictionary - no change needed as it returns strings""" + attrs = {} + + # First, handle special attributes with quotes that might contain special characters + # Handle ch attribute specially (for char command) + ch_match = re.search(r'ch="([^"]*)"', attrs_str) + if ch_match: + attrs['ch'] = ch_match.group(1) + # Remove the ch attribute from the string to avoid re-parsing + attrs_str = attrs_str[:ch_match.start()] + attrs_str[ch_match.end():] + + # Handle name attribute specially if it contains quotes + name_match = re.search(r'name="([^"]*)"', attrs_str) + if name_match: + attrs['name'] = name_match.group(1) + # Remove the name attribute from the string to avoid re-parsing + attrs_str = attrs_str[:name_match.start()] + attrs_str[name_match.end():] + else: + # Try without quotes + name_match = re.search(r'name=([^\s]+)', attrs_str) + if name_match: + attrs['name'] = name_match.group(1) + attrs_str = attrs_str[:name_match.start()] + attrs_str[name_match.end():] + + # Parse remaining attributes + # Pattern for key=value or key="value" + pattern = r'([^\s=]+)=(?:"([^"]*)"|([^\s]*))' + for match in re.finditer(pattern, attrs_str): + key, quoted_val, unquoted_val = match.groups() + if key not in ['name', 'ch']: # Skip if we already handled these + attrs[key] = quoted_val if quoted_val is not None else unquoted_val + + return attrs + + + @staticmethod + def _extract_array_values(array_str: str, max_values: int) -> List[int]: + """Extract values from array string and return as int list""" + values = [0] * max_values + + if array_str: + # Handle quoted array + if array_str.startswith('"') and array_str.endswith('"'): + array_str = array_str[1:-1] + + # Handle bracketed array + if array_str.startswith("[") and array_str.endswith("]"): + try: + array_str = array_str.strip('[]') + parts = array_str.split(',') + for i, part in enumerate(parts): + if i >= max_values: + break + values[i] = round(float(part.strip())) + except ValueError: + pass + # Handle space-separated values + else: + parts = array_str.split() + for i, part in enumerate(parts): + if i >= max_values: + break + try: + values[i] = round(float(part)) + except ValueError: + pass + + return values + + @staticmethod + def _format_value(value, preserve_int=True): + """Format value as integer with proper rounding""" + val = float(value) + + # Handle special case for very small values + if abs(val) < 1e-10: + return 0 + + # Always round to integer + return val + + + + def to_sequence(self) -> str: + """Convert LottieTensor to sequence string""" + lines = [] + current_context = None + string_params = getattr(self, 'string_params', {}) + + for i in range(self.seq_len.item()): + cmd_idx = int(self.commands[i].item()) + + # Skip padding and special tokens + if cmd_idx in [LottieTensor.CMD_PAD, LottieTensor.CMD_EOS, LottieTensor.CMD_SOS]: + continue + + cmd = LottieTensor.COMMANDS[cmd_idx] + if not cmd: # Skip empty command entries + continue + + cmd_key = f"{i}" + + # Handle end tags + if cmd.startswith('/'): + lines.append(f"({cmd})") + # Reset context + if cmd in ["/position", "/scale", "/opacity", "/rotation", "/keyframe", "/anchor", "/path", "/width_animated", + "/range_start", "/range_end", "/range_offset", "/scale_animators", "/rotation_animators", + "/position_x", "/position_y", "/position_z", "/tm", "/start", "/end", "/offset", "/color_animated", "/size", "/rounded"]: + current_context = None + continue + + # Update context + if cmd in ["position", "scale", "opacity", "rotation", "anchor"]: + current_context = cmd + elif cmd == "size": + # Check if size is animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = "size" + elif cmd in ["ellipse_size", "rect_size"]: + # Check if size is animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = "size" + + elif cmd in ["position_x", "position_y", "position_z"]: + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = cmd + elif cmd == "path": + # Check if path is animated (would be stored in string_params) + if f"{cmd_key}_animated" in string_params: + current_context = "path" + elif cmd == "width_keyframe": + current_context = "width" + elif cmd == "start": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "trim_start" + elif cmd == "end": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "trim_end" + elif cmd == "offset": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "trim_offset" + elif cmd == "mask_x": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.MaskX.A] > 0.5: + current_context = "mask_x" + + + + elif cmd == "scale_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.ScaleAnimators.A] > 0.5: + current_context = "scale_animators" + elif cmd == "rotation_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.RotationAnimators.A] > 0.5: + current_context = "rotation_animators" + + elif cmd == "opacity_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.OpacityAnimators.A] > 0.5: + current_context = "opacity_animators" + + elif cmd == "position_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.PositionAnimators.A] > 0.5: + current_context = "position_animators" + + elif cmd == "tracking_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.TrackingAnimators.A] > 0.5: + current_context = "tracking_animators" + + elif cmd == "rect_rounded": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "rect_rounded" + + elif cmd in ["ellipse_size", "rect_size"]: + # Check if ellipse/rect size is animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = "size" + + + # Extract parameters + params = self.params[i].tolist() + + # Format line based on command type + if cmd_idx == LottieTensor.CMD_ANIMATION: + # Use stored string values if available + #v = string_params.get(f"{cmd_key}_v", "5.12.1") + v = "5.12.1" + #nm = string_params.get(f"{cmd_key}_nm", "Comp 1") + #markers = string_params.get(f"{cmd_key}_markers", "[]") + #props = string_params.get(f"{cmd_key}_props", "{}") + + fr = LottieTensor._format_value(params[LottieTensor.Index.Animation.FR]) + ip = LottieTensor._format_value(params[LottieTensor.Index.Animation.IP]) + op = LottieTensor._format_value(params[LottieTensor.Index.Animation.OP]) + w = LottieTensor._format_value(params[LottieTensor.Index.Animation.W]) + h = LottieTensor._format_value(params[LottieTensor.Index.Animation.H]) + ddd = int(params[LottieTensor.Index.Animation.DDD]) + lines.append(f'({cmd} v="{v}" fr={fr} ip={ip} op={op} w={w} h={h} ddd={ddd})') + + elif cmd_idx in [LottieTensor.CMD_FONTS, LottieTensor.CMD_FONTS_END, LottieTensor.CMD_CHARS, + LottieTensor.CMD_CHARS_END, LottieTensor.CMD_CHAR_SHAPES, + LottieTensor.CMD_CHAR_SHAPES_END, LottieTensor.CMD_TEXT_KEYFRAMES, + LottieTensor.CMD_TEXT_KEYFRAMES_END, LottieTensor.CMD_TEXT_DATA, + LottieTensor.CMD_TEXT_DATA_END, LottieTensor.CMD_OPACITY_ANIMATED_END, + LottieTensor.CMD_END_END, LottieTensor.CMD_START_END, + LottieTensor.CMD_OFFSET_END, LottieTensor.CMD_OPACITY_ANIMATORS_END]: + lines.append(f"({cmd})") + continue + + elif cmd_idx in [LottieTensor.CMD_POSITION_X, LottieTensor.CMD_POSITION_Y, LottieTensor.CMD_POSITION_Z]: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + current_context = cmd # Set context + else: + val = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + lines.append(f"({cmd} {val})") + # line based on command type + # Keep all existing formatting logic but update text_keyframe + elif cmd_idx == LottieTensor.CMD_TEXT_KEYFRAME: + # Initialize tokenizer if not already done + if LottieTensor.tokenizer is None: + LottieTensor.init_tokenizer() + t = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.T]) + + # Retrieve all stored attributes + # Retrieve numeric values + font_size = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FONT_SIZE]) + ca = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.CA]) + justify = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.JUSTIFY]) + tracking = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.TRACKING]) + line_height = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.LINE_HEIGHT]) + letter_spacing = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.LETTER_SPACING]) + + # Retrieve fill_color from numeric params + fill_r = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FILL_COLOR_R]/255) + fill_g = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FILL_COLOR_G]/255) + fill_b = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FILL_COLOR_B]/255) + fill_color = f"[{fill_r},{fill_g},{fill_b}]" + + # Retrieve string values + #font_family = string_params.get(f"{cmd_key}_font_family", "") + #text = string_params.get(f"{cmd_key}_text", "") + # Decode font_family from tokens + font_family = "" + font_family_count = int(params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT]) if params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT] > -2000 else 0 + if font_family_count > 0: + font_family_tokens = [] + for i in range(min(font_family_count, 10)): + token_val = params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START + i] + if token_val > -2000: + font_family_tokens.append(int(token_val)) + if font_family_tokens: + try: + font_family = LottieTensor.tokenizer.decode(font_family_tokens) + except: + font_family = "" + + # Decode text from tokens + text = "" + text_count = int(params[LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT]) if params[LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT] > -2000 else 0 + if text_count > 0: + text_tokens = [] + for i in range(min(text_count, 15)): + token_val = params[LottieTensor.Index.TextKeyframe.TEXT_TOKENS_START + i] + if token_val > -2000: + text_tokens.append(int(token_val)) + if text_tokens: + try: + text = LottieTensor.tokenizer.decode(text_tokens) + except: + text = "" + # Build the output line + line = f'({cmd} t={t} font_size={font_size} font_family="{font_family}" text="{text}" ca={ca} justify={justify} tracking={tracking} line_height={line_height} letter_spacing={letter_spacing} fill_color={fill_color}' + + # Add stroke_color if present + if params[LottieTensor.Index.TextKeyframe.HAS_STROKE_COLOR] > 0.5: + stroke_r = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_R]/255) + stroke_g = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_G]/255) + stroke_b = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_B]/255) + stroke_color = f"[{stroke_r},{stroke_g},{stroke_b}]" + line += f' stroke_color={stroke_color}' + + # Add stroke_width if present and not zero + stroke_width = params[LottieTensor.Index.TextKeyframe.STROKE_WIDTH] if params[LottieTensor.Index.TextKeyframe.STROKE_WIDTH] > -2000 else 0 + if abs(stroke_width) > 1e-6: + line += f' stroke_width={LottieTensor._format_value(stroke_width)}' + + # Add offset if true + if params[LottieTensor.Index.TextKeyframe.OFFSET] > 0.5: + line += ' offset=true' + + # Add wrap_position if present (ę–°å¢ž) + wrap_pos_x = params[LottieTensor.Index.TextKeyframe.WRAP_POSITION_X] + wrap_pos_y = params[LottieTensor.Index.TextKeyframe.WRAP_POSITION_Y] + if wrap_pos_x > -2000 and wrap_pos_y > -2000: + line += f' wrap_position=[{LottieTensor._format_value(wrap_pos_x)},{LottieTensor._format_value(wrap_pos_y)}]' + + # Add wrap_size if present (ę–°å¢ž) + wrap_size_x = params[LottieTensor.Index.TextKeyframe.WRAP_SIZE_X] + wrap_size_y = params[LottieTensor.Index.TextKeyframe.WRAP_SIZE_Y] + if wrap_size_x > -2000 and wrap_size_y > -2000: + line += f' wrap_size=[{LottieTensor._format_value(wrap_size_x)},{LottieTensor._format_value(wrap_size_y)}]' + + line += ')' + lines.append(line) + + + elif cmd_idx == LottieTensor.CMD_STAR: + #name = string_params.get(f"{cmd_key}_name", "None") + d = int(params[LottieTensor.Index.Star.D]) if params[LottieTensor.Index.Star.D] > -2000 else 1 + sy = int(params[LottieTensor.Index.Star.SY]) if params[LottieTensor.Index.Star.SY] > -2000 else 1 + + lines.append(f'({cmd} d={d} sy={sy})') + + elif cmd_idx == LottieTensor.CMD_INNER_RADIUS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_OUTER_RADIUS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_INNER_ROUNDNESS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_OUTER_ROUNDNESS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_POINTS_STAR: # čæ™ä¼šč¾“å‡ŗ points_star + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'(points_star {val})') # å¼ŗåˆ¶č¾“å‡ŗäøŗ points_star + + elif cmd_idx == LottieTensor.CMD_STAR_ROTATION: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + + + elif cmd_idx == LottieTensor.CMD_MORE_OPTIONS: + # Reconstruct more_options line + g = int(params[LottieTensor.Index.MoreOptions.G]) if params[LottieTensor.Index.MoreOptions.G] > -2000 else 1 + alignment_a = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_A]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_A] > -2000 else 0 + alignment_k1 = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_K1]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_K1] > -2000 else 0 + alignment_k2 = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_K2]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_K2] > -2000 else 0 + alignment_ix = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_IX]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_IX] > -2000 else 2 + + lines.append(f'({cmd} g {g} alignment a={alignment_a} alignment_k {alignment_k1} {alignment_k2} alignment_ix {alignment_ix})') + + + elif cmd_idx == LottieTensor.CMD_LAYER: + # Use stored layer name if available + #name = string_params.get(f"{cmd_key}_name", "Layer") + + index = LottieTensor._format_value(params[LottieTensor.Index.Layer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.Layer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.Layer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.Layer.START_TIME]) + + # å¼€å§‹ęž„å»ŗč¾“å‡ŗč”Œ + line = f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time}' + + # åŖč¾“å‡ŗéžé»˜č®¤/éžå”«å……å€¼ēš„åÆé€‰å‚ę•° + if params[LottieTensor.Index.Layer.DDD] > -2000: + ddd = int(params[LottieTensor.Index.Layer.DDD]) + line += f' ddd={ddd}' + + #if params[LottieTensor.Index.Layer.HD] > -2000: + # hd = "true" if params[LottieTensor.Index.Layer.HD] > 0.5 else "false" + # line += f' hd={hd}' + if params[LottieTensor.Index.Layer.HD] > -2000 and params[LottieTensor.Index.Layer.HD] > 0.5: + line += f' hd=true' + + if params[LottieTensor.Index.Layer.CP] > -2000: + cp = "true" if params[LottieTensor.Index.Layer.CP] > 0.5 else "false" + line += f' cp={cp}' + + if params[LottieTensor.Index.Layer.CT] > -2000: + ct = int(params[LottieTensor.Index.Layer.CT]) + line += f' ct={ct}' + + if params[LottieTensor.Index.Layer.HAS_MASK] > -2000: + hasMask = "true" if params[LottieTensor.Index.Layer.HAS_MASK] > 0.5 else "false" + line += f' hasMask={hasMask}' + + # masksProperties ę€»ę˜Æä½œäøŗå­—ē¬¦äø²å­˜å‚Ø + masksProperties = string_params.get(f"{cmd_key}_masksProperties", "") + if masksProperties: + line += f' masksProperties={masksProperties}' + + if params[LottieTensor.Index.Layer.AO] > -2000: + ao = int(params[LottieTensor.Index.Layer.AO]) + line += f' ao={ao}' + + if params[LottieTensor.Index.Layer.TT] > -2000: + tt = int(params[LottieTensor.Index.Layer.TT]) + line += f' tt={tt}' + + if params[LottieTensor.Index.Layer.TP] > -2000: + tp = int(params[LottieTensor.Index.Layer.TP]) + line += f' tp={tp}' + + if params[LottieTensor.Index.Layer.TD] > -2000: + td = int(params[LottieTensor.Index.Layer.TD]) + line += f' td={td}' + + line += ')' + lines.append(line) + + + + elif cmd_idx == LottieTensor.CMD_NULL_LAYER: + # Use stored layer name if available + #name = string_params.get(f"{cmd_key}_name", "null_layer") + + index = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.START_TIME]) + + line = f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time}' + #lines.append(f'({cmd} index={index} name="{name}" in_point={in_point} out_point={out_point} start_time={start_time} ct={ct})') + + if params[LottieTensor.Index.PrecompLayer.HD] > -2000 and params[LottieTensor.Index.PrecompLayer.HD] > 0.5: + line += f' hd=true' + + if params[LottieTensor.Index.PrecompLayer.CP] > -2000: + cp = "true" if params[LottieTensor.Index.PrecompLayer.CP] > 0.5 else "false" + line += f' cp={cp}' + + + if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > -2000: + hasMask = "true" if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > 0.5 else "false" + line += f' hasMask={hasMask}' + + + if params[LottieTensor.Index.PrecompLayer.AO] > -2000: + ao = int(params[LottieTensor.Index.PrecompLayer.AO]) + line += f' ao={ao}' + + if params[LottieTensor.Index.PrecompLayer.TT] > -2000: + tt = int(params[LottieTensor.Index.PrecompLayer.TT]) + line += f' tt={tt}' + + if params[LottieTensor.Index.PrecompLayer.TP] > -2000: + tp = int(params[LottieTensor.Index.PrecompLayer.TP]) + line += f' tp={tp}' + + if params[LottieTensor.Index.PrecompLayer.TD] > -2000: + td = int(params[LottieTensor.Index.PrecompLayer.TD]) + line += f' td={td}' + + + line += ')' + lines.append(line) + + elif cmd_idx == LottieTensor.CMD_PRECOMP_LAYER: + # Use stored layer name if available + #name = string_params.get(f"{cmd_key}_name", "precomp_layer") + + index = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.START_TIME]) + + # Output w and h with full precision + #w = params[LottieTensor.Index.PrecompLayer.W] + #h = params[LottieTensor.Index.PrecompLayer.H] + + # Format w and h preserving their full precision + # Check if the value is very close to an integer + #if abs(w - round(w)) < 1e-10: + # w_str = str(int(round(w))) + #else: + # Keep full precision for non-integer values + # w_str = str(w) + + #if abs(h - round(h)) < 1e-10: + # h_str = str(int(round(h))) + #else: + # Keep full precision for non-integer values + # h_str = str(h) + + + line = f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time}' + + + + if params[LottieTensor.Index.PrecompLayer.H] > -2000: + h = int(params[LottieTensor.Index.PrecompLayer.H]) + line += f' h={h}' + + if params[LottieTensor.Index.PrecompLayer.W] > -2000: + w = int(params[LottieTensor.Index.PrecompLayer.W]) + line += f' w={w}' + + if params[LottieTensor.Index.PrecompLayer.DDD] > -2000: + ddd = int(params[LottieTensor.Index.PrecompLayer.DDD]) + line += f' ddd={ddd}' + + if params[LottieTensor.Index.PrecompLayer.HD] > -2000 and params[LottieTensor.Index.PrecompLayer.HD] > 0.5: + line += f' hd=true' + + if params[LottieTensor.Index.PrecompLayer.CP] > -2000: + cp = "true" if params[LottieTensor.Index.PrecompLayer.CP] > 0.5 else "false" + line += f' cp={cp}' + + if params[LottieTensor.Index.PrecompLayer.CT] > -2000: + ct = int(params[LottieTensor.Index.PrecompLayer.CT]) + line += f' ct={ct}' + + if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > -2000: + hasMask = "true" if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > 0.5 else "false" + line += f' hasMask={hasMask}' + + # masksProperties ę€»ę˜Æä½œäøŗå­—ē¬¦äø²å­˜å‚Ø + masksProperties = string_params.get(f"{cmd_key}_masksProperties", "") + if masksProperties: + line += f' masksProperties={masksProperties}' + + if params[LottieTensor.Index.PrecompLayer.AO] > -2000: + ao = int(params[LottieTensor.Index.PrecompLayer.AO]) + line += f' ao={ao}' + + if params[LottieTensor.Index.PrecompLayer.TT] > -2000: + tt = int(params[LottieTensor.Index.PrecompLayer.TT]) + line += f' tt={tt}' + + if params[LottieTensor.Index.PrecompLayer.TP] > -2000: + tp = int(params[LottieTensor.Index.PrecompLayer.TP]) + line += f' tp={tp}' + + if params[LottieTensor.Index.PrecompLayer.TD] > -2000: + td = int(params[LottieTensor.Index.PrecompLayer.TD]) + line += f' td={td}' + + + line += ')' + lines.append(line) + + + elif cmd_idx == LottieTensor.CMD_REFERENCE_ID: + # Get tokenizer + tokenizer = LottieTensor.get_tokenizer() + + # Decode reference_id from tokens + id_count = int(params[LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT]) if params[LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT] > -2000 else 0 + id_tokens = [] + for i in range(id_count): + if params[LottieTensor.Index.ReferenceId.ID_TOKEN_0 + i] > -2000: + id_tokens.append(int(params[LottieTensor.Index.ReferenceId.ID_TOKEN_0 + i])) + + reference_id = tokenizer.decode(id_tokens, skip_special_tokens=True) if id_tokens else "comp_0" + lines.append(f'({cmd} "{reference_id}")') + + + + elif cmd_idx == LottieTensor.CMD_DIMENSIONS: + # 输出dimensions命令 + width = LottieTensor._format_value(params[LottieTensor.Index.Dimensions.WIDTH]) + height = LottieTensor._format_value(params[LottieTensor.Index.Dimensions.HEIGHT]) + lines.append(f'({cmd} width={width} height={height})') + + + elif cmd_idx == LottieTensor.CMD_STROKE: + #name = string_params.get(f"{cmd_key}_name", "Stroke") + color_animated = params[LottieTensor.Index.Stroke.COLOR_ANIMATED] > 0.5 + + line = f'({cmd}' + + if color_animated: + line += ' color_animated=true' + else: + # Convert RGB values from 0-255 back to 0-1 range + r = LottieTensor._format_value(params[LottieTensor.Index.Stroke.R] / 255) + g = LottieTensor._format_value(params[LottieTensor.Index.Stroke.G] / 255) + b = LottieTensor._format_value(params[LottieTensor.Index.Stroke.B] / 255) + a = LottieTensor._format_value(params[LottieTensor.Index.Stroke.A] / 255) + line += f' r={r} g={g} b={b} a={a}' + + color_dim = int(params[LottieTensor.Index.Stroke.COLOR_DIM]) if params[LottieTensor.Index.Stroke.COLOR_DIM] > -2000 else 4 + has_c_a = "True" if params[LottieTensor.Index.Stroke.HAS_C_A] > 0.5 else "False" + has_c_ix = "True" if params[LottieTensor.Index.Stroke.HAS_C_IX] > 0.5 else "False" + c_ix = int(params[LottieTensor.Index.Stroke.C_IX]) if params[LottieTensor.Index.Stroke.C_IX] > -2000 else 3 + bm = int(params[LottieTensor.Index.Stroke.BM]) if params[LottieTensor.Index.Stroke.BM] > -2000 else 0 + lc = int(params[LottieTensor.Index.Stroke.LC]) if params[LottieTensor.Index.Stroke.LC] > -2000 else 1 + lj = int(params[LottieTensor.Index.Stroke.LJ]) if params[LottieTensor.Index.Stroke.LJ] > -2000 else 1 + ml = int(params[LottieTensor.Index.Stroke.ML]) if params[LottieTensor.Index.Stroke.ML] > -2000 else 4 + + line += f' color_dim={color_dim} has_c_a={has_c_a} has_c_ix={has_c_ix}' + + if not color_animated: + line += f' c_ix={c_ix}' + + line += f' bm={bm} lc={lc} lj={lj} ml={ml}' + + # Check if width is animated + width_animated = params[LottieTensor.Index.Stroke.WIDTH_ANIMATED] > 0.5 + if width_animated: + line += ' width_animated=true' + current_context = "width" + + # IMPORTANT: Close the parenthesis here + line += ')' + + lines.append(line) + + # Add the separate (width_animated true) command after the stroke + if width_animated: + lines.append('(width_animated true)') + + if color_animated: + current_context = "stroke_color" + + + # Add dashes output in to_sequence method (after line 5800): + elif cmd_idx == LottieTensor.CMD_DASHES: + # Check if we have the complete dashes string stored + dashes_str = string_params.get(f"{cmd_key}_dashes", "") + if dashes_str: + # The stored string already includes quotes if needed, don't add extra quotes + lines.append(f'({cmd} {dashes_str})') + else: + lines.append(f'({cmd})') + + + elif cmd_idx == LottieTensor.CMD_DASH: + type_map = {0: "d", 1: "g", 2: "o"} + type_val = int(params[LottieTensor.Index.Dash.TYPE]) if params[LottieTensor.Index.Dash.TYPE] > -2000 else 0 + dash_type = type_map.get(type_val, "d") + + # 除仄100ę¢å¤åŽŸå€¼ + length = LottieTensor._format_value(params[LottieTensor.Index.Dash.LENGTH] / 10, preserve_int=False) + v_ix = int(params[LottieTensor.Index.Dash.V_IX]) if params[LottieTensor.Index.Dash.V_IX] > -2000 else 1 + + lines.append(f'({cmd} type="{dash_type}" length={length} v_ix={v_ix})') + + + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED: + # Convert numeric type back to string + type_map = {0: "d", 1: "g", 2: "o"} + type_val = int(params[LottieTensor.Index.DashAnimated.TYPE]) if params[LottieTensor.Index.DashAnimated.TYPE] > -2000 else 2 + dash_type = type_map.get(type_val, "o") + + v_ix = int(params[LottieTensor.Index.DashAnimated.V_IX]) if params[LottieTensor.Index.DashAnimated.V_IX] > -2000 else 7 + + # Get name from string_params + name = string_params.get(f"{cmd_key}_name", "") + + lines.append(f'({cmd} type="{dash_type}" name="{name}" v_ix={v_ix})') + current_context = "dash_animated" + + elif cmd_idx == LottieTensor.CMD_DASH_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.DashKeyframe.T]) + # 除仄100ę¢å¤åŽŸå€¼ + s = LottieTensor._format_value(params[LottieTensor.Index.DashKeyframe.S] / 10, preserve_int=False) + + i_x = params[LottieTensor.Index.DashKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.DashKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.DashKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.DashKeyframe.O_Y]/100 + + has_easing = (i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000) + + if has_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_DASH_OFFSET: + # 除仄100ę¢å¤åŽŸå€¼ + o = LottieTensor._format_value(params[LottieTensor.Index.DashOffset.O] / 10, preserve_int=False) + lines.append(f'({cmd} {o})') + + + elif cmd_idx == LottieTensor.CMD_DASHES_END: + lines.append(f'({cmd})') + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_SIZE_END: + lines.append(f'({cmd})') + # 7. Add output for color_keyframe: + elif cmd_idx == LottieTensor.CMD_COLOR_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.T]) + r = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S1]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S2]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S3]/255) + a = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1]/255) + + i_x = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X]/100, preserve_int=False) + i_y = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y]/100, preserve_int=False) + o_x = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X]/100, preserve_int=False) + o_y = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y]/100, preserve_int=False) + + lines.append(f'({cmd} t={t} r={r} g={g} b={b} a={a} i_x={i_x} i_y={i_y} o_x={o_x} o_y={o_y})') + + elif cmd_idx == LottieTensor.CMD_OPACITY_ANIMATED: + lines.append(f'({cmd} true)') + current_context = "opacity_animated" + + elif cmd_idx == LottieTensor.CMD_OPACITY_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.T]) + + keyframe_line = f'({cmd} t={t}' + + # Check if s parameter is valid + if params[LottieTensor.Index.Keyframe.S1] > -2000: + s = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S1]) + keyframe_line += f' s={s}' + + # Check if easing parameters exist + i_x = params[LottieTensor.Index.Keyframe.I_X]/100 + i_y = params[LottieTensor.Index.Keyframe.I_Y]/100 + o_x = params[LottieTensor.Index.Keyframe.O_X]/100 + o_y = params[LottieTensor.Index.Keyframe.O_Y]/100 + + if i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + + keyframe_line += ')' + lines.append(keyframe_line) + + elif cmd_idx == LottieTensor.CMD_WIDTH_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.WidthKeyframe.T]) + + keyframe_line = f'({cmd} t={t}' + + # Check if s parameter is valid - 除仄100ę¢å¤åŽŸå€¼ + if params[LottieTensor.Index.WidthKeyframe.S] > -2000: + s = LottieTensor._format_value(params[LottieTensor.Index.WidthKeyframe.S] / 10, preserve_int=False) + keyframe_line += f' s={s}' + + # easingå‚ę•°å¤„ē†ļ¼ˆäæęŒäøå˜ļ¼Œé™¤ä»„100) + i_x = params[LottieTensor.Index.WidthKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.WidthKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.WidthKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.WidthKeyframe.O_Y]/100 + + has_easing = (i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000) + + if has_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + + keyframe_line += ")" + lines.append(keyframe_line) + + + elif cmd_idx == LottieTensor.CMD_TRANSFORM: + lines.append(f"({cmd})") + + elif cmd_idx == LottieTensor.CMD_POSITION: + if cmd == "position" and current_context not in ["position", "scale", "opacity", "rotation", "anchor"]: + # This is a position command for shapes (ellipse, rect, etc.) + x = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + y = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f"({cmd} {x} {y})") + else: + # This is a transform position + if params[LottieTensor.Index.Transform.ANIMATED] == 2.0: + # Separated position (for 3D layers) + lines.append(f"({cmd} separated=true)") + elif params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + z = LottieTensor._format_value(params[LottieTensor.Index.Transform.Z]) + # Check if Z is meaningful + if params[LottieTensor.Index.Transform.Z] > -2000 and abs(params[LottieTensor.Index.Transform.Z]) > 1e-6: + lines.append(f"({cmd} {x} {y} {z})") + else: + lines.append(f"({cmd} {x} {y})") + + + elif cmd_idx == LottieTensor.CMD_POSITION_X: + # Output position_x with its value + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + value = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f"({cmd} {value})") + else: + lines.append(f"({cmd})") + + elif cmd_idx == LottieTensor.CMD_POSITION_Y: + # Output position_y with its value + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + value = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f"({cmd} {value})") + else: + lines.append(f"({cmd})") + + elif cmd_idx == LottieTensor.CMD_POSITION_Z: + # Output position_z with its value + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + value = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f"({cmd} {value})") + else: + lines.append(f"({cmd})") + + + elif cmd_idx == LottieTensor.CMD_SCALE: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + z = LottieTensor._format_value(params[LottieTensor.Index.Transform.Z]) + # Check if Z is meaningful - output Z if it's not padding value + if params[LottieTensor.Index.Transform.Z] > -2000: + lines.append(f"({cmd} {x} {y} {z})") + else: + lines.append(f"({cmd} {x} {y})") + + elif cmd_idx == LottieTensor.CMD_ROTATION: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + angle = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + lines.append(f"({cmd} {angle})") + + elif cmd_idx == LottieTensor.CMD_OPACITY: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + val = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + lines.append(f"({cmd} {val})") + + elif cmd_idx == LottieTensor.CMD_ANCHOR: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + z = LottieTensor._format_value(params[LottieTensor.Index.Transform.Z]) + # Check if Z is meaningful + if params[LottieTensor.Index.Transform.Z] > -2000 and abs(params[LottieTensor.Index.Transform.Z]) > 1e-6: + lines.append(f"({cmd} {x} {y} {z})") + else: + lines.append(f"({cmd} {x} {y})") + + elif cmd_idx == LottieTensor.CMD_TM: + a = int(params[LottieTensor.Index.Tm.A]) if params[LottieTensor.Index.Tm.A] > -2000 else 1 + + # Always output a parameter + lines.append(f'({cmd} a={a})') + + # Set context based on a value + if a > 0.5: + current_context = "tm" # Set context for keyframes + else: + current_context = "tm_static" # Reset context for static value + + # Add value command output + elif cmd_idx == LottieTensor.CMD_VALUE: + val = LottieTensor._format_value(params[LottieTensor.Index.Value.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.T]) + + keyframe_line = f'({cmd} t={t}' + + # Check if this is a hold keyframe (using H_FLAG slot) + is_hold = params[LottieTensor.Index.Keyframe.H_FLAG] > 0.5 + + # Check if s parameter is valid (not padding) + has_s = params[LottieTensor.Index.Keyframe.S1] > -2000 + + # Only add s parameter if valid and not in path context + if has_s and current_context != "path": + # Format s parameter based on context + if current_context in ["opacity", "rotation", "position_x", "position_y", "position_z", "tm", "width", + "trim_start", "trim_end", "trim_offset", "mask_x", "rotation_animators", "opacity_animators", "tracking_animators"]: + # For single-value properties + s = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S1], preserve_int=False) + s_str = f'"{s}"' + else: + # For position, scale, anchor - output only non-padding values + s1 = params[LottieTensor.Index.Keyframe.S1] + s2 = params[LottieTensor.Index.Keyframe.S2] + s3 = params[LottieTensor.Index.Keyframe.S3] + + s_parts = [] + s_parts.append(str(LottieTensor._format_value(s1, preserve_int=False))) + s_parts.append(str(LottieTensor._format_value(s2, preserve_int=False))) + + # Only add s3 if it's not padding value and not zero (for 2D animations) + if s3 > -2000 and abs(s3) > 1e-6: + s_parts.append(str(LottieTensor._format_value(s3, preserve_int=False))) + + s_str = f'"{" ".join(s_parts)}"' + + keyframe_line += f' s={s_str}' + + + # Check and output e parameter based on context - MODIFIED TO INCLUDE TRIM CONTEXTS + has_e = params[LottieTensor.Index.Keyframe.E1] > -2000 + + if has_e: # Output e regardless of hold flag + if current_context in ["trim_start", "trim_end", "trim_offset"]: + # For trim contexts, output single e value as quoted string + e_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) + keyframe_line += f' e="{e_val}"' + elif current_context == "rotation": + # For rotation, output single e value + e_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) + keyframe_line += f' e="[{e_val}]"' + elif current_context == "scale": + # For scale, output three e values + e1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) if params[LottieTensor.Index.Keyframe.E1] > -2000 else 0 + e2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E2], preserve_int=False) if params[LottieTensor.Index.Keyframe.E2] > -2000 else 0 + e3 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E3], preserve_int=False) if params[LottieTensor.Index.Keyframe.E3] > -2000 else 0 + keyframe_line += f' e="[{e1}, {e2}, {e3}]"' + # Add other contexts as needed + + + # Handle hold keyframe + if is_hold: + keyframe_line += ' h=1' + + # ALWAYS check and add easing parameters, regardless of hold flag + # The hold flag just means the value is held, but easing can still be defined + if current_context in ["position", "anchor", "scale_animators", "position_animators", "size"]: + # For multi-dimensional properties, output multi-dimensional easing + # Check if we have multi-dimensional easing values + has_multi_easing = ( + params[LottieTensor.Index.Keyframe.I_X2] > -2000 or + params[LottieTensor.Index.Keyframe.I_Y2] > -2000 or + params[LottieTensor.Index.Keyframe.O_X2] > -2000 or + params[LottieTensor.Index.Keyframe.O_Y2] > -2000 + ) + + if has_multi_easing: + # Format multi-dimensional easing - DIVIDE BY 100 + i_x_vals = [] + i_y_vals = [] + o_x_vals = [] + o_y_vals = [] + + # Always include first two dimensions - DIVIDE BY 100 + i_x1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_X] > -2000 else 0 + i_x2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_X2] > -2000 else i_x1 + i_x_vals = [i_x1, i_x2] + + i_y1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_Y] > -2000 else 0 + i_y2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_Y2] > -2000 else i_y1 + i_y_vals = [i_y1, i_y2] + + o_x1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_X] > -2000 else 0 + o_x2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_X2] > -2000 else o_x1 + o_x_vals = [o_x1, o_x2] + + o_y1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_Y] > -2000 else 0 + o_y2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_Y2] > -2000 else o_y1 + o_y_vals = [o_y1, o_y2] + + # Only add third dimension if it exists and is non-zero - DIVIDE BY 100 + i_x3 = params[LottieTensor.Index.Keyframe.I_X3] + i_y3 = params[LottieTensor.Index.Keyframe.I_Y3] + o_x3 = params[LottieTensor.Index.Keyframe.O_X3] + o_y3 = params[LottieTensor.Index.Keyframe.O_Y3] + + # Check if ANY third dimension value is meaningful (not padding and not zero) + has_third_dim = ( + (i_x3 > -2000 and abs(i_x3) > 1e-6) or + (i_y3 > -2000 and abs(i_y3) > 1e-6) or + (o_x3 > -2000 and abs(o_x3) > 1e-6) or + (o_y3 > -2000 and abs(o_y3) > 1e-6) + ) + + if has_third_dim: + i_x_vals.append(LottieTensor._format_value(i_x3 / 100, preserve_int=False) if i_x3 > -2000 else i_x1) + i_y_vals.append(LottieTensor._format_value(i_y3 / 100, preserve_int=False) if i_y3 > -2000 else i_y1) + o_x_vals.append(LottieTensor._format_value(o_x3 / 100, preserve_int=False) if o_x3 > -2000 else o_x1) + o_y_vals.append(LottieTensor._format_value(o_y3 / 100, preserve_int=False) if o_y3 > -2000 else o_y1) + + keyframe_line += f' i_x="{" ".join(str(v) for v in i_x_vals)}" i_y="{" ".join(str(v) for v in i_y_vals)}" o_x="{" ".join(str(v) for v in o_x_vals)}" o_y="{" ".join(str(v) for v in o_y_vals)}"' + + elif params[LottieTensor.Index.Keyframe.I_X] > -2000 or params[LottieTensor.Index.Keyframe.I_Y] > -2000 or params[LottieTensor.Index.Keyframe.O_X] > -2000 or params[LottieTensor.Index.Keyframe.O_Y] > -2000: + # Fallback to single values if no multi-dimensional values found - DIVIDE BY 100 + i_x_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_X] > -2000 else 0 + i_y_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_Y] > -2000 else 0 + o_x_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_X] > -2000 else 0 + o_y_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_Y] > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + else: + # For single-dimensional properties, parse single easing values - DIVIDE BY 100 + i_x = params[LottieTensor.Index.Keyframe.I_X] + i_y = params[LottieTensor.Index.Keyframe.I_Y] + o_x = params[LottieTensor.Index.Keyframe.O_X] + o_y = params[LottieTensor.Index.Keyframe.O_Y] + + if i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000: + i_x_val = LottieTensor._format_value(i_x / 100, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y / 100, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x / 100, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y / 100, preserve_int=False) if o_y > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + + # Add to/ti parameters (they are separate from hold flag) + has_to = any(params[LottieTensor.Index.Keyframe.TO1 + j] > -2000 for j in range(3)) + has_ti = any(params[LottieTensor.Index.Keyframe.TI1 + j] > -2000 for j in range(3)) + + if has_to: + to_values = [] + for i in range(3): + val = params[LottieTensor.Index.Keyframe.TO1 + i] + if val > -2000: + to_values.append(LottieTensor._format_value(val, preserve_int=False)) + else: + break # Stop at first padding value + + # Only output non-zero values, but always include at least 2 dimensions if any exist + while len(to_values) > 2 and abs(to_values[-1]) < 1e-10: + to_values.pop() # Remove trailing zeros + + # Ensure we have at least 2 values if we have any + while len(to_values) < 2: + to_values.append(0) + + keyframe_line += f' to="[{", ".join(str(v) for v in to_values)}]"' + + if has_ti: + ti_values = [] + for i in range(3): + val = params[LottieTensor.Index.Keyframe.TI1 + i] + if val > -2000: + ti_values.append(LottieTensor._format_value(val, preserve_int=False)) + else: + break # Stop at first padding value + + # Only output non-zero values, but always include at least 2 dimensions if any exist + while len(ti_values) > 2 and abs(ti_values[-1]) < 1e-10: + ti_values.pop() # Remove trailing zeros + + # Ensure we have at least 2 values if we have any + while len(ti_values) < 2: + ti_values.append(0) + + keyframe_line += f' ti="[{", ".join(str(v) for v in ti_values)}]"' + + # Check and output e parameter based on context + has_e = params[LottieTensor.Index.Keyframe.E1] > -2000 + + if has_e: # Output e regardless of hold flag + if current_context == "rotation": + # For rotation, output single e value + e_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) + keyframe_line += f' e="[{e_val}]"' + elif current_context == "scale": + # For scale, output three e values + e1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) if params[LottieTensor.Index.Keyframe.E1] > -2000 else 0 + e2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E2], preserve_int=False) if params[LottieTensor.Index.Keyframe.E2] > -2000 else 0 + e3 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E3], preserve_int=False) if params[LottieTensor.Index.Keyframe.E3] > -2000 else 0 + keyframe_line += f' e="[{e1}, {e2}, {e3}]"' + # Add other contexts as needed + + keyframe_line += ")" + lines.append(keyframe_line) + + + + elif cmd_idx == LottieTensor.CMD_GROUP: + #name = string_params.get(f"{cmd_key}_name", "Group") + #mn = string_params.get(f"{cmd_key}_mn", "ADBE Vector Group") + + ix = int(params[LottieTensor.Index.Group.IX]) if params[LottieTensor.Index.Group.IX] > -2000 else 1 + cix = int(params[LottieTensor.Index.Group.CIX]) if params[LottieTensor.Index.Group.CIX] > -2000 else 2 + bm = int(params[LottieTensor.Index.Group.BM]) if params[LottieTensor.Index.Group.BM] > -2000 else 0 + hd = "true" if params[LottieTensor.Index.Group.HD] > 0.5 else "false" + np = int(params[LottieTensor.Index.Group.NP]) if params[LottieTensor.Index.Group.NP] > -2000 else 0 + + lines.append(f'({cmd} ix={ix} cix={cix} bm={bm} hd={hd} np={np})') + + + elif cmd_idx == LottieTensor.CMD_PATH: + #name = string_params.get(f"{cmd_key}_name", "Path") + #mn = string_params.get(f"{cmd_key}_mn", "ADBE Vector Path") # Add mn + + ix = int(params[LottieTensor.Index.Path.IX]) if params[LottieTensor.Index.Path.IX] > -2000 else 1 + ind = int(params[LottieTensor.Index.Path.IND]) if params[LottieTensor.Index.Path.IND] > -2000 else 0 + ks_ix = int(params[LottieTensor.Index.Path.KS_IX]) if params[LottieTensor.Index.Path.KS_IX] > -2000 else 2 + closed = "true" if params[LottieTensor.Index.Path.CLOSED] > 0.5 else "false" + hd = "true" if params[LottieTensor.Index.Path.HD] > 0.5 else "false" # Add HD + + # Check if path is animated + if params[LottieTensor.Index.Path.ANIMATED] > 0.5: + lines.append(f'({cmd} ix={ix} ind={ind} ks_ix={ks_ix} animated="true" hd={hd})') + current_context = "path" + else: + lines.append(f'({cmd} ix={ix} ind={ind} ks_ix={ks_ix} closed={closed} hd={hd})') + + + elif cmd_idx == LottieTensor.CMD_POINT: + # Check if this is a valid point (not padding) + if params[LottieTensor.Index.Point.X] > -2000 and params[LottieTensor.Index.Point.Y] > -2000: + x = LottieTensor._format_value(params[LottieTensor.Index.Point.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Point.Y]) + in_x = LottieTensor._format_value(params[LottieTensor.Index.Point.IN_X]) + in_y = LottieTensor._format_value(params[LottieTensor.Index.Point.IN_Y]) + out_x = LottieTensor._format_value(params[LottieTensor.Index.Point.OUT_X]) + out_y = LottieTensor._format_value(params[LottieTensor.Index.Point.OUT_Y]) + lines.append(f"({cmd} x={x} y={y} in_x={in_x} in_y={in_y} out_x={out_x} out_y={out_y})") + # else: skip padding points + + + elif cmd_idx == LottieTensor.CMD_FILL: + #name = string_params.get(f"{cmd_key}_name", "Fill") + + color_dim = int(params[LottieTensor.Index.Fill.COLOR_DIM]) if params[LottieTensor.Index.Fill.COLOR_DIM] > -2000 else 3 + has_c_a = "True" if params[LottieTensor.Index.Fill.HAS_C_A] > 0.5 else "False" + has_c_ix = "True" if params[LottieTensor.Index.Fill.HAS_C_IX] > 0.5 else "False" + c_ix = int(params[LottieTensor.Index.Fill.C_IX]) if params[LottieTensor.Index.Fill.C_IX] > -2000 else 4 + bm = int(params[LottieTensor.Index.Fill.BM]) if params[LottieTensor.Index.Fill.BM] > -2000 else 0 + fill_rule = int(params[LottieTensor.Index.Fill.FILL_RULE]) if params[LottieTensor.Index.Fill.FILL_RULE] > -2000 else 1 + has_o_a = "True" if params[LottieTensor.Index.Fill.HAS_O_A] > 0.5 else "False" + has_o_ix = "True" if params[LottieTensor.Index.Fill.HAS_O_IX] > 0.5 else "False" + o_ix = int(params[LottieTensor.Index.Fill.O_IX]) if params[LottieTensor.Index.Fill.O_IX] > -2000 else 5 + + color_animated = params[LottieTensor.Index.Fill.COLOR_ANIMATED] > 0.5 + opacity_animated = params[LottieTensor.Index.Fill.OPACITY_ANIMATED] > 0.5 + + line_parts = [f'({cmd}'] + + # Handle color output + if color_animated: + # Output color keyframes with easing + color_keyframes_json = string_params.get(f"{cmd_key}_color_keyframes", "[]") + color_keyframes = json.loads(color_keyframes_json) + for i, kf in enumerate(color_keyframes): + line_parts.append(f' c_kf_{i}_t={LottieTensor._format_value(kf["t"])}') + line_parts.append(f' c_kf_{i}_r={LottieTensor._format_value(kf["r"]/255)}') + line_parts.append(f' c_kf_{i}_g={LottieTensor._format_value(kf["g"]/255)}') + line_parts.append(f' c_kf_{i}_b={LottieTensor._format_value(kf["b"]/255)}') + # Add easing parameters if they exist and are non-zero (divide by 100 for float output) + if "i_x" in kf and (abs(kf["i_x"]) > 1e-6 or abs(kf.get("i_y", 0)) > 1e-6 or + abs(kf.get("o_x", 0)) > 1e-6 or abs(kf.get("o_y", 0)) > 1e-6): + line_parts.append(f' c_kf_{i}_i_x={LottieTensor._format_value(kf["i_x"] / 100, preserve_int=False)}') + line_parts.append(f' c_kf_{i}_i_y={LottieTensor._format_value(kf.get("i_y", 0) / 100, preserve_int=False)}') + line_parts.append(f' c_kf_{i}_o_x={LottieTensor._format_value(kf.get("o_x", 0) / 100, preserve_int=False)}') + line_parts.append(f' c_kf_{i}_o_y={LottieTensor._format_value(kf.get("o_y", 0) / 100, preserve_int=False)}') + line_parts.append(f' c_kf_count={len(color_keyframes)}') + line_parts.append(' color_animated=true') + else: + # Output static color + r = LottieTensor._format_value(params[LottieTensor.Index.Fill.R]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.Fill.G]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.Fill.B]/255) + line_parts.append(f' r={r} g={g} b={b} color_animated=false') + + # Add common color parameters + line_parts.append(f' color_dim={color_dim} has_c_a={has_c_a} has_c_ix={has_c_ix} c_ix={c_ix} bm={bm} fill_rule={fill_rule}') + + # Handle opacity output + if opacity_animated: + # Output opacity keyframes (divide by 100 for float output) + opacity_keyframes_json = string_params.get(f"{cmd_key}_opacity_keyframes", "[]") + opacity_keyframes = json.loads(opacity_keyframes_json) + for i, kf in enumerate(opacity_keyframes): + line_parts.append(f' o_kf_{i}_t={LottieTensor._format_value(kf["t"])}') + line_parts.append(f' o_kf_{i}_s={LottieTensor._format_value(kf["s"])}') + line_parts.append(f' o_kf_{i}_i_x={LottieTensor._format_value(kf["i_x"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_{i}_i_y={LottieTensor._format_value(kf["i_y"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_{i}_o_x={LottieTensor._format_value(kf["o_x"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_{i}_o_y={LottieTensor._format_value(kf["o_y"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_count={len(opacity_keyframes)}') + line_parts.append(' opacity_animated=true') + else: + # Output static opacity + opacity = LottieTensor._format_value(params[LottieTensor.Index.Fill.OPACITY]) + line_parts.append(f' opacity={opacity} opacity_animated=false') + + # Add opacity-related parameters + line_parts.append(f' has_o_a={has_o_a} has_o_ix={has_o_ix} o_ix={o_ix})') + + lines.append(''.join(line_parts)) + + + + elif cmd_idx == LottieTensor.CMD_BEZIER: + closed = "true" if params[LottieTensor.Index.Bezier.CLOSED] > 0.5 else "false" + lines.append(f'({cmd} closed={closed})') + + elif cmd_idx == LottieTensor.CMD_ELLIPSE: + #name = string_params.get(f"{cmd_key}_name", "Ellipse Path 1") + lines.append(f'({cmd})') + + + elif cmd_idx == LottieTensor.CMD_SIZE: + # Check if size is animated - also check for PAD_VAL + if params[LottieTensor.Index.Transform.ANIMATED] > -2000 and params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "size" + else: + # äæ®ę”¹ļ¼šä½æē”Ø Transform.X 和 Transform.Y + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + lines.append(f'({cmd} {x} {y})') + + + elif cmd_idx == LottieTensor.CMD_RECT: + #name = string_params.get(f"{cmd_key}_name", "Rectangle Path 1") + hd = "true" if params[LottieTensor.Index.Rect.HD] > 0.5 else "false" + d = int(params[LottieTensor.Index.Rect.D]) if params[LottieTensor.Index.Rect.D] > -2000 else 1 + lines.append(f'({cmd} hd={hd} d={d})') + + elif cmd_idx == LottieTensor.CMD_ROUNDED: + rounded = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + ix = int(params[LottieTensor.Index.SingleValue.IX]) if params[LottieTensor.Index.SingleValue.IX] > -2000 else 4 + lines.append(f'({cmd} {rounded} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_TRIM: + #name = string_params.get(f"{cmd_key}_name", "Trim Paths 1") + ix = int(params[LottieTensor.Index.Trim.IX]) if params[LottieTensor.Index.Trim.IX] > -2000 else 1 + lines.append(f'({cmd} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_END: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "trim_end" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_START: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "trim_start" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_OFFSET: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "trim_offset" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + + + elif cmd_idx == LottieTensor.CMD_MULTIPLE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_REPEATER: + #name = string_params.get(f"{cmd_key}_name", "Repeater 1") + ix = int(params[LottieTensor.Index.Repeater.IX]) if params[LottieTensor.Index.Repeater.IX] > -2000 else 1 + lines.append(f'({cmd} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_COPIES: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + ix = int(params[LottieTensor.Index.SingleValue.IX]) if params[LottieTensor.Index.SingleValue.IX] > -2000 else 1 + lines.append(f'({cmd} {val} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_REPEATER_OFFSET: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + ix = int(params[LottieTensor.Index.SingleValue.IX]) if params[LottieTensor.Index.SingleValue.IX] > -2000 else 2 + lines.append(f'({cmd} {val} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_COMPOSITE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_REPEATER_TRANSFORM: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_TR_P_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_A_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_SCALE: + val1 = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + val2 = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f'({cmd} {val1} {val2})') + + elif cmd_idx == LottieTensor.CMD_TR_S_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 3 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_R_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 4 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_SO_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 5 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_EO_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 6 + lines.append(f'({cmd} {val})') + + + elif cmd_idx == LottieTensor.CMD_TRANSFORM_SHAPE: + #name = string_params.get(f"{cmd_key}_name", "Transform") + + hd = "true" if params[LottieTensor.Index.TransformShape.HD] > 0.5 else "false" + position_x = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.POSITION_X]) + position_y = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.POSITION_Y]) + scale_x = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SCALE_X]) + scale_y = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SCALE_Y]) + rotation = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.ROTATION]) + opacity = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.OPACITY]) + anchor_x = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.ANCHOR_X]) + anchor_y = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.ANCHOR_Y]) + + # Build the output line + line = f'({cmd} hd={hd} position="{position_x} {position_y}" scale="{scale_x} {scale_y}" rotation="{rotation}" opacity="{opacity}" anchor="{anchor_x} {anchor_y}"' + + # Only add skew if it's not 0 or PAD_VAL + if params[LottieTensor.Index.TransformShape.SKEW] > -2000 and abs(params[LottieTensor.Index.TransformShape.SKEW]) > 1e-6: + skew = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SKEW]) + line += f' skew="{skew}"' + + # Only add skew_axis if it's not 0 or PAD_VAL + if params[LottieTensor.Index.TransformShape.SKEW_AXIS] > -2000 and abs(params[LottieTensor.Index.TransformShape.SKEW_AXIS]) > 1e-6: + skew_axis = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SKEW_AXIS]) + line += f' skew_axis="{skew_axis}"' + + line += ')' + lines.append(line) + + elif cmd_idx == LottieTensor.CMD_PARENT: + parent_index = int(params[LottieTensor.Index.Parent.PARENT_INDEX]) if params[LottieTensor.Index.Parent.PARENT_INDEX] > -2000 else 0 + lines.append(f'({cmd} {parent_index})') + + elif cmd_idx == LottieTensor.CMD_ASSET: + # Get tokenizer + tokenizer = LottieTensor.get_tokenizer() + + # Decode ID from tokens + id_count = int(params[LottieTensor.Index.Asset.ID_TOKEN_COUNT]) if params[LottieTensor.Index.Asset.ID_TOKEN_COUNT] > -2000 else 0 + id_tokens = [] + for i in range(id_count): + if params[LottieTensor.Index.Asset.ID_TOKEN_0 + i] > -2000: + id_tokens.append(int(params[LottieTensor.Index.Asset.ID_TOKEN_0 + i])) + + asset_id = tokenizer.decode(id_tokens, skip_special_tokens=True) if id_tokens else "comp_0" + fr = LottieTensor._format_value(params[LottieTensor.Index.Asset.FR]) + + lines.append(f'({cmd} id="{asset_id}" fr={fr})') + + elif cmd_idx == LottieTensor.CMD_FONT: + # Get tokenizer + tokenizer = LottieTensor.get_tokenizer() + + # Decode family from tokens + family_count = int(params[LottieTensor.Index.Font.FAMILY_TOKEN_COUNT]) if params[LottieTensor.Index.Font.FAMILY_TOKEN_COUNT] > -2000 else 0 + family_tokens = [] + for i in range(family_count): + if params[LottieTensor.Index.Font.FAMILY_TOKEN_0 + i] > -2000: + family_tokens.append(int(params[LottieTensor.Index.Font.FAMILY_TOKEN_0 + i])) + + # Decode style from tokens + style_count = int(params[LottieTensor.Index.Font.STYLE_TOKEN_COUNT]) if params[LottieTensor.Index.Font.STYLE_TOKEN_COUNT] > -2000 else 0 + style_tokens = [] + for i in range(style_count): + if params[LottieTensor.Index.Font.STYLE_TOKEN_0 + i] > -2000: + style_tokens.append(int(params[LottieTensor.Index.Font.STYLE_TOKEN_0 + i])) + + family = tokenizer.decode(family_tokens, skip_special_tokens=True) if family_tokens else "" + style = tokenizer.decode(style_tokens, skip_special_tokens=True) if style_tokens else "" + ascent = LottieTensor._format_value(params[LottieTensor.Index.Font.ASCENT]) + + lines.append(f'({cmd} family="{family}" style="{style}" ascent={ascent})') + + elif cmd_idx == LottieTensor.CMD_CHAR: + # Get tokenizer + tokenizer = LottieTensor.get_tokenizer() + + # Decode ch from tokens + ch_count = int(params[LottieTensor.Index.Char.CH_TOKEN_COUNT]) if params[LottieTensor.Index.Char.CH_TOKEN_COUNT] > -2000 else 0 + ch_tokens = [] + for i in range(ch_count): + if params[LottieTensor.Index.Char.CH_TOKEN_0 + i] > -2000: + ch_tokens.append(int(params[LottieTensor.Index.Char.CH_TOKEN_0 + i])) + + # Decode style from tokens + style_count = int(params[LottieTensor.Index.Char.STYLE_TOKEN_COUNT]) if params[LottieTensor.Index.Char.STYLE_TOKEN_COUNT] > -2000 else 0 + style_tokens = [] + for i in range(style_count): + if params[LottieTensor.Index.Char.STYLE_TOKEN_0 + i] > -2000: + style_tokens.append(int(params[LottieTensor.Index.Char.STYLE_TOKEN_0 + i])) + + # Decode family from tokens + family_count = int(params[LottieTensor.Index.Char.FAMILY_TOKEN_COUNT]) if params[LottieTensor.Index.Char.FAMILY_TOKEN_COUNT] > -2000 else 0 + family_tokens = [] + for i in range(family_count): + if params[LottieTensor.Index.Char.FAMILY_TOKEN_0 + i] > -2000: + family_tokens.append(int(params[LottieTensor.Index.Char.FAMILY_TOKEN_0 + i])) + + ch = tokenizer.decode(ch_tokens, skip_special_tokens=True) if ch_tokens else "" + style = tokenizer.decode(style_tokens, skip_special_tokens=True) if style_tokens else "" + family = tokenizer.decode(family_tokens, skip_special_tokens=True) if family_tokens else "" + size = LottieTensor._format_value(params[LottieTensor.Index.Char.SIZE]) + w = LottieTensor._format_value(params[LottieTensor.Index.Char.W]) + + lines.append(f'({cmd} ch="{ch}" size={size} style="{style}" w={w} family="{family}")') + + + elif cmd_idx == LottieTensor.CMD_TEXT_LAYER: + #name = string_params.get(f"{cmd_key}_name", "Text Layer") + index = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.START_TIME]) + hasMask = "True" if params[LottieTensor.Index.TextLayer.HAS_MASK] > 0.5 else "False" # ę–°å¢ž + lines.append(f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time} hasMask={hasMask})') # 修改 + + + #elif cmd_idx == LottieTensor.CMD_TEXT_KEYFRAME: + # t = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.T]) + # lines.append(f'({cmd} t={t})') + + elif cmd_idx == LottieTensor.CMD_FONT_SIZE: + size = LottieTensor._format_value(params[LottieTensor.Index.FontSize.SIZE]) + lines.append(f'({cmd} {size})') + + elif cmd_idx == LottieTensor.CMD_FONT_FAMILY: + family = string_params.get(f"{cmd_key}_family", "") + lines.append(f'({cmd} "{family}")') + + elif cmd_idx == LottieTensor.CMD_TEXT: + text = string_params.get(f"{cmd_key}_text", "") + lines.append(f'({cmd} "{text}")') + + elif cmd_idx == LottieTensor.CMD_CA: + value = LottieTensor._format_value(params[LottieTensor.Index.Ca.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_JUSTIFY: + value = LottieTensor._format_value(params[LottieTensor.Index.Justify.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_TRACKING: + value = LottieTensor._format_value(params[LottieTensor.Index.Tracking.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_LINE_HEIGHT: + value = LottieTensor._format_value(params[LottieTensor.Index.LineHeight.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_LETTER_SPACING: + value = LottieTensor._format_value(params[LottieTensor.Index.LetterSpacing.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_FILL_COLOR: + r = LottieTensor._format_value(params[LottieTensor.Index.FillColor.R]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.FillColor.G]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.FillColor.B]/255) + lines.append(f'({cmd} {r} {g} {b})') + + elif cmd_idx == LottieTensor.CMD_G: + value = LottieTensor._format_value(params[LottieTensor.Index.G.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT: + a = LottieTensor._format_value(params[LottieTensor.Index.Alignment.A]) + lines.append(f'({cmd} a={a})') + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT_K: + val1 = LottieTensor._format_value(params[LottieTensor.Index.AlignmentK.VALUE1]) + val2 = LottieTensor._format_value(params[LottieTensor.Index.AlignmentK.VALUE2]) + lines.append(f'({cmd} {val1} {val2})') + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT_IX: + value = LottieTensor._format_value(params[LottieTensor.Index.AlignmentIx.VALUE]) + lines.append(f'({cmd} {value})') + elif cmd_idx == LottieTensor.CMD_EFFECTS: + lines.append(f'({cmd})') + + + elif cmd_idx == LottieTensor.CMD_EFFECT: + #name = string_params.get(f"{cmd_key}_name", "") + match_name = string_params.get(f"{cmd_key}_match_name", "") + type_val = int(params[LottieTensor.Index.Effect.TYPE]) if params[LottieTensor.Index.Effect.TYPE] > -2000 else 0 + index = int(params[LottieTensor.Index.Effect.INDEX]) if params[LottieTensor.Index.Effect.INDEX] > -2000 else 1 + np = int(params[LottieTensor.Index.Effect.NP]) if params[LottieTensor.Index.Effect.NP] > -2000 else 0 + enabled = int(params[LottieTensor.Index.Effect.ENABLED]) if params[LottieTensor.Index.Effect.ENABLED] > -2000 else 1 + + line = f'({cmd} type={type_val} index={index}' + if np > 0: # Only output np if it's non-zero + line += f' np={np}' + line += f' match_name="{match_name}"' + if enabled != 1: # Only output enabled if it's not the default value + line += f' enabled={enabled}' + line += ')' + lines.append(line) + + # Add CMD_LAYER_EFFECT output: + elif cmd_idx == LottieTensor.CMD_LAYER_EFFECT: + #name = string_params.get(f"{cmd_key}_name", "") + match_name = string_params.get(f"{cmd_key}_match_name", "") + index = int(params[LottieTensor.Index.LayerEffect.INDEX]) if params[LottieTensor.Index.LayerEffect.INDEX] > -2000 else 1 + value = LottieTensor._format_value(params[LottieTensor.Index.LayerEffect.VALUE]) + lines.append(f'({cmd} index={index} value={value} match_name="{match_name}")') + + + elif cmd_idx == LottieTensor.CMD_DROPDOWN: + #name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.Dropdown.INDEX]) if params[LottieTensor.Index.Dropdown.INDEX] > -2000 else 1 + value = int(params[LottieTensor.Index.Dropdown.VALUE]) if params[LottieTensor.Index.Dropdown.VALUE] > -2000 else 0 + lines.append(f'({cmd} index={index} value={value})') + + elif cmd_idx == LottieTensor.CMD_NO_VALUE: + #@name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.NO_VALUE.INDEX]) if params[LottieTensor.Index.NO_VALUE.INDEX] > -2000 else 1 + value = int(params[LottieTensor.Index.NO_VALUE.VALUE]) if params[LottieTensor.Index.NO_VALUE.VALUE] > -2000 else 0 + lines.append(f'({cmd} index={index} value={value})') + + + elif cmd_idx == LottieTensor.CMD_IGNORED: + #name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.Ignored.INDEX]) if params[LottieTensor.Index.Ignored.INDEX] > -2000 else 1 + value = LottieTensor._format_value(params[LottieTensor.Index.Ignored.VALUE]) + lines.append(f'({cmd} index={index} value={value})') + + elif cmd_idx == LottieTensor.CMD_SLIDER: + #name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.Slider.INDEX]) if params[LottieTensor.Index.Slider.INDEX] > -2000 else 1 + value = LottieTensor._format_value(params[LottieTensor.Index.Slider.VALUE]) + lines.append(f'({cmd} index={index} value={value})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_FILL: + #name = string_params.get(f"{cmd_key}_name", "Gradient Fill 1") + lines.append(f'({cmd})') + current_context = "gradient_fill" # Set context for subsequent commands + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "gradient_fill": + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_FILL_RULE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_START_POINT: + x = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + y = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_END_POINT: + x = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + y = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_TYPE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_HIGHLIGHT_LENGTH: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_HIGHLIGHT_ANGLE: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_ORIGINAL_COLORS: + count = int(params[LottieTensor.Index.OriginalColors.COUNT]) if params[LottieTensor.Index.OriginalColors.COUNT] > -2000 else 0 + + color_values = [] + for i in range(count): + if params[LottieTensor.Index.OriginalColors.COLOR_0 + i] > -2000: + color_values.append(LottieTensor._format_value(params[LottieTensor.Index.OriginalColors.COLOR_0 + i])/255) + + colors_str = ", ".join(str(v) for v in color_values) + lines.append(f'({cmd} [{colors_str}])') + + + elif cmd_idx == LottieTensor.CMD_COLOR_POINTS: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_FILL_END: + lines.append(f'({cmd})') + current_context = None # Reset context + + elif cmd_idx == LottieTensor.CMD_GRADIENT_STROKE: + #name = string_params.get(f"{cmd_key}_name", "Gradient Stroke 1") + lines.append(f'({cmd})') + current_context = "gradient_stroke" # Set context for subsequent commands + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "gradient_stroke": + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + current_context = "gradient_stroke" # Set context for subsequent commands + + + elif cmd_idx == LottieTensor.CMD_WIDTH: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + # 除仄100ę¢å¤åŽŸå€¼ + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE] / 10, preserve_int=False) + lines.append(f'({cmd} {val})') + + #if current_context == "gradient_stroke": + # Check if value exists (not PAD_VAL) + # if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + # val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + # lines.append(f'({cmd} {val})') + # else: + # No value, output just the command + # lines.append(f'({cmd})') + #else: + # Handle width in other contexts if needed + # lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_LINE_CAP: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_LINE_JOIN: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_MITER_LIMIT: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 0 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_STROKE_END: + lines.append(f'({cmd})') + current_context = None # Reset context + + elif cmd_idx == LottieTensor.CMD_COLOR: + #name = string_params.get(f"{cmd_key}_name", "Color") + index = int(params[LottieTensor.Index.Color.INDEX]) if params[LottieTensor.Index.Color.INDEX] > -2000 else 1 + r = LottieTensor._format_value(params[LottieTensor.Index.Color.R]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.Color.G]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.Color.B]/255) + lines.append(f'({cmd} index={index} r={r} g={g} b={b})') + + + elif cmd_idx == LottieTensor.CMD_MERGE: + #name = string_params.get(f"{cmd_key}_name", "Merge Paths 1") + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MERGE_MODE: + mode = int(params[LottieTensor.Index.MergeMode.MODE]) if params[LottieTensor.Index.MergeMode.MODE] > -2000 else 1 + lines.append(f'({cmd} {mode})') + + + elif cmd_idx == LottieTensor.CMD_SOLID_LAYER: + #name = string_params.get(f"{cmd_key}_name", "Solid Layer") + #color = string_params.get(f"{cmd_key}_color", "#000000") + r = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_R]))) + g = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_G]))) + b = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_B]))) + a = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_A]))) + + # You can use RGB values directly or convert back to hex if needed + color_rgb = (r, g, b, a) + # Or convert back to hex format if required: + color = f"#{r:02x}{g:02x}{b:02x}{a:02x}" + + index = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.START_TIME]) + width = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.WIDTH]) + height = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.HEIGHT]) + hasMask = "True" if params[LottieTensor.Index.SolidLayer.HAS_MASK] > 0.5 else "False" + + lines.append(f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time} color="{color}" width={width} height={height} hasMask={hasMask})') + + elif cmd_idx == LottieTensor.CMD_MASKS_PROPERTIES: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK: + #nm = string_params.get(f"{cmd_key}_nm", "Mask 1") + + index = int(params[LottieTensor.Index.Mask.INDEX]) if params[LottieTensor.Index.Mask.INDEX] > -2000 else 0 + inv = "true" if params[LottieTensor.Index.Mask.INV] > 0.5 else "false" + + # Convert mode value back to string + mode_val = int(params[LottieTensor.Index.Mask.MODE]) if params[LottieTensor.Index.Mask.MODE] > -2000 else 0 + mode_map = {0: "a", 1: "s", 2: "i", 3: "n"} + mode = mode_map.get(mode_val, "a") + + lines.append(f'({cmd} index={index} inv={inv} mode={mode})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT: + a = int(params[LottieTensor.Index.MaskPt.A]) if params[LottieTensor.Index.MaskPt.A] > -2000 else 1 + ix = int(params[LottieTensor.Index.MaskPt.IX]) if params[LottieTensor.Index.MaskPt.IX] > -2000 else 1 + lines.append(f'({cmd} a={a} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_MASK_PT_K_C: + c = "true" if params[LottieTensor.Index.MaskPtK.C] > 0.5 else "false" + lines.append(f'({cmd} {c})') # Changed from c={c} to just {c} + + + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_K_I, LottieTensor.CMD_MASK_PT_K_O, LottieTensor.CMD_MASK_PT_K_V]: + count = int(params[LottieTensor.Index.MaskPtKValues.COUNT]) if params[LottieTensor.Index.MaskPtKValues.COUNT] > -2000 else 0 + + if count == 0: + # Fallback: find last non-padding value + for i in range(19, -1, -1): + val = params[LottieTensor.Index.MaskPtKValues.V1 + i] + if val > -2000: + count = i + 1 + break + + values = [] + for i in range(count): + val = params[LottieTensor.Index.MaskPtKValues.V1 + i] + if val > -2000: + values.append(LottieTensor._format_value(val)) + else: + values.append(0.0) + + lines.append(f'({cmd} {" ".join(str(v) for v in values)})') + + elif cmd_idx == LottieTensor.CMD_MASK_O: + a = int(params[LottieTensor.Index.MaskO.A]) if params[LottieTensor.Index.MaskO.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MaskO.K]) if params[LottieTensor.Index.MaskO.K] > -2000 else 100 + ix = int(params[LottieTensor.Index.MaskO.IX]) if params[LottieTensor.Index.MaskO.IX] > -2000 else 3 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_MASK_X: + a = int(params[LottieTensor.Index.MaskX.A]) if params[LottieTensor.Index.MaskX.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MaskX.K]) if params[LottieTensor.Index.MaskX.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.MaskX.IX]) if params[LottieTensor.Index.MaskX.IX] > -2000 else 4 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_MASK_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_K: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_K_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASKS_PROPERTIES_END: + lines.append(f'({cmd})') + + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_K_ARRAY, LottieTensor.CMD_MASK_PT_K_ARRAY_END, + LottieTensor.CMD_MASK_PT_KF_S, LottieTensor.CMD_MASK_PT_KF_S_END, + LottieTensor.CMD_MASK_PT_KF_SHAPE_END, LottieTensor.CMD_MASK_PT_KEYFRAME_END]: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KEYFRAME: + index = int(params[LottieTensor.Index.MaskPtKeyframe.INDEX]) if params[LottieTensor.Index.MaskPtKeyframe.INDEX] > -2000 else 0 + t = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKeyframe.T]) + lines.append(f'({cmd} index={index} t={t})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_I: + x = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfI.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfI.Y]) + lines.append(f'({cmd} x={x} y={y})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_O: + x = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfO.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfO.Y]) + lines.append(f'({cmd} x={x} y={y})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_SHAPE: + index = int(params[LottieTensor.Index.MaskPtKfShape.INDEX]) if params[LottieTensor.Index.MaskPtKfShape.INDEX] > -2000 else 0 + c = "true" if params[LottieTensor.Index.MaskPtKfShape.C] > 0.5 else "false" + lines.append(f'({cmd} index={index} c={c})') + + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_KF_SHAPE_I, LottieTensor.CMD_MASK_PT_KF_SHAPE_O, + LottieTensor.CMD_MASK_PT_KF_SHAPE_V]: + # Get the count from params, not from string_params + count = int(params[LottieTensor.Index.MaskPtKfShapeValues.COUNT]) if params[LottieTensor.Index.MaskPtKfShapeValues.COUNT] > -2000 else 0 + + if count == 0: + # If no count stored, find the last non-padding value + for i in range(19, -1, -1): # Check V1 through V20 + if i < LottieTensor.PARAM_DIM: + val = params[LottieTensor.Index.MaskPtKfShapeValues.V1 + i] + if val > -2000: + count = i + 1 + break + if count == 0: + count = 8 # Default to 8 if no valid values found + + # Output the exact number of values + values = [] + for i in range(count): + if i < 20: + val = params[LottieTensor.Index.MaskPtKfShapeValues.V1 + i] + if val > -2000: + values.append(LottieTensor._format_value(val)) + else: + values.append(0.0) + else: + values.append(0.0) + + lines.append(f'({cmd} {" ".join(str(v) for v in values)})') + + elif cmd_idx == LottieTensor.CMD_TR_POSITION: + x = LottieTensor._format_value(params[LottieTensor.Index.TrPosition.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.TrPosition.Y]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_TR_ANCHOR: + x = LottieTensor._format_value(params[LottieTensor.Index.TrAnchor.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.TrAnchor.Y]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_TR_ROTATION: + val = LottieTensor._format_value(params[LottieTensor.Index.TrRotation.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_START_OPACITY: + val = LottieTensor._format_value(params[LottieTensor.Index.TrStartOpacity.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_END_OPACITY: + if params[LottieTensor.Index.TrEndOpacity.VALUE] > -2000: + val = LottieTensor._format_value(params[LottieTensor.Index.TrEndOpacity.VALUE]) + lines.append(f'({cmd} {val})') + else: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ZIG_ZAG: + #name = string_params.get(f"{cmd_key}_name", "Zig Zag 1") + ix = int(params[LottieTensor.Index.ZigZag.IX]) if params[LottieTensor.Index.ZigZag.IX] > -2000 else 2 + lines.append(f'({cmd} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_FREQUENCY: + value = LottieTensor._format_value(params[LottieTensor.Index.Frequency.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_AMPLITUDE: + value = LottieTensor._format_value(params[LottieTensor.Index.Amplitude.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_POINT_TYPE: + value = int(params[LottieTensor.Index.PointType.VALUE]) if params[LottieTensor.Index.PointType.VALUE] > -2000 else 2 + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_ZIG_ZAG_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATORS: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATOR: + #nm = string_params.get(f"{cmd_key}_nm", "Animator 1") + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_RANGE_SELECTOR: + t = int(params[LottieTensor.Index.RangeSelector.T]) if params[LottieTensor.Index.RangeSelector.T] > -2000 else 0 + r = int(params[LottieTensor.Index.RangeSelector.R]) if params[LottieTensor.Index.RangeSelector.R] > -2000 else 1 + b = int(params[LottieTensor.Index.RangeSelector.B]) if params[LottieTensor.Index.RangeSelector.B] > -2000 else 1 + sh = int(params[LottieTensor.Index.RangeSelector.SH]) if params[LottieTensor.Index.RangeSelector.SH] > -2000 else 1 + rn = int(params[LottieTensor.Index.RangeSelector.RN]) if params[LottieTensor.Index.RangeSelector.RN] > -2000 else 0 + lines.append(f'({cmd} t={t} r={r} b={b} sh={sh} rn={rn})') + + elif cmd_idx == LottieTensor.CMD_RANGE_START: + a = int(params[LottieTensor.Index.RangeStart.A]) if params[LottieTensor.Index.RangeStart.A] > -2000 else 0 + lines.append(f'({cmd} a={a})') + if a > 0.5: + current_context = "range_start" + + + elif cmd_idx == LottieTensor.CMD_RANGE_START_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.RangeStartKeyframe.T]) + s = LottieTensor._format_value(params[LottieTensor.Index.RangeStartKeyframe.S]) + + # Check if this is the last keyframe (no easing values or all zeros) + i_x = params[LottieTensor.Index.RangeStartKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.RangeStartKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.RangeStartKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.RangeStartKeyframe.O_Y]/100 + + has_meaningful_easing = ( + (i_x > -2000 and abs(i_x) > 1e-6) or + (i_y > -2000 and abs(i_y) > 1e-6) or + (o_x > -2000 and abs(o_x) > 1e-6) or + (o_y > -2000 and abs(o_y) > 1e-6) + ) + + if has_meaningful_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + elif cmd_idx == LottieTensor.CMD_RANGE_START_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_AMOUNT: + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 100 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 4 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_MAX_EASE: + a = int(params[LottieTensor.Index.MaxEase.A]) if params[LottieTensor.Index.MaxEase.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MaxEase.K]) if params[LottieTensor.Index.MaxEase.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.MaxEase.IX]) if params[LottieTensor.Index.MaxEase.IX] > -2000 else 7 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_MIN_EASE: + a = int(params[LottieTensor.Index.MinEase.A]) if params[LottieTensor.Index.MinEase.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MinEase.K]) if params[LottieTensor.Index.MinEase.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.MinEase.IX]) if params[LottieTensor.Index.MinEase.IX] > -2000 else 8 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_PROPERTIES: + lines.append(f'({cmd})') + current_context = "animator_properties" + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_PROPERTIES_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "animator_properties": + # Special handling for opacity within animator_properties + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 9 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_RANGE_SELECTOR_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATORS_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_RADIUS: + val = LottieTensor._format_value(params[LottieTensor.Index.Radius.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_RANGE_END: + a = int(params[LottieTensor.Index.RangeEnd.A]) if params[LottieTensor.Index.RangeEnd.A] > -2000 else 0 + lines.append(f'({cmd} a={a})') + if a > 0.5: + current_context = "range_end" + + + elif cmd_idx == LottieTensor.CMD_RANGE_END_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.RangeEndKeyframe.T]) + s = LottieTensor._format_value(params[LottieTensor.Index.RangeEndKeyframe.S]) + + # Check if this is the last keyframe (no easing values or all zeros) + i_x = params[LottieTensor.Index.RangeEndKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.RangeEndKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.RangeEndKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.RangeEndKeyframe.O_Y]/100 + + has_meaningful_easing = ( + (i_x > -2000 and abs(i_x) > 1e-6) or + (i_y > -2000 and abs(i_y) > 1e-6) or + (o_x > -2000 and abs(o_x) > 1e-6) or + (o_y > -2000 and abs(o_y) > 1e-6) + ) + + if has_meaningful_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + elif cmd_idx == LottieTensor.CMD_RANGE_END_END: + lines.append(f'({cmd})') + current_context = None + + # Fix position output in animator_properties context + + elif cmd_idx == LottieTensor.CMD_POSITION and current_context == "animator_properties": + # Special handling for position within animator_properties + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 2 + + # Check if we have array values stored + x = params[LottieTensor.Index.Transform.X] + y = params[LottieTensor.Index.Transform.Y] + z = params[LottieTensor.Index.Transform.Z] + + if x > -2000 or y > -2000: # Changed condition - check x or y + # Format as array + x_val = LottieTensor._format_value(x) if x > -2000 else 0 + y_val = LottieTensor._format_value(y) if y > -2000 else 0 + + # Only include z if it's meaningful + if z > -2000 and abs(z) > 1e-6: + lines.append(f'({cmd} a={a} k=[{x_val}, {y_val}, {LottieTensor._format_value(z)}] ix={ix})') + else: + lines.append(f'({cmd} a={a} k=[{x_val}, {y_val}, 0] ix={ix})') + else: + # Format as single value + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 0 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_ML2: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 4 + lines.append(f'({cmd} {val})') + + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.RangeOffsetKeyframe.T]) + s = LottieTensor._format_value(params[LottieTensor.Index.RangeOffsetKeyframe.S]) + + # Check if this is the last keyframe (no easing values or all zeros) + i_x = params[LottieTensor.Index.RangeOffsetKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.RangeOffsetKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.RangeOffsetKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.RangeOffsetKeyframe.O_Y]/100 + + has_meaningful_easing = ( + (i_x > -2000 and abs(i_x) > 1e-6) or + (i_y > -2000 and abs(i_y) > 1e-6) or + (o_x > -2000 and abs(o_x) > 1e-6) or + (o_y > -2000 and abs(o_y) > 1e-6) + ) + + if has_meaningful_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_S_M: + a = int(params[LottieTensor.Index.SM.A]) if params[LottieTensor.Index.SM.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.SM.K]) if params[LottieTensor.Index.SM.K] > -2000 else 100 + ix = int(params[LottieTensor.Index.SM.IX]) if params[LottieTensor.Index.SM.IX] > -2000 else 6 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + # 修改 CMD_OPACITY_ANIMATORS ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_OPACITY_ANIMATORS: + a = int(params[LottieTensor.Index.OpacityAnimators.A]) if params[LottieTensor.Index.OpacityAnimators.A] > -2000 else 0 + + if a > 0.5: + # Animated case - only output a + lines.append(f'({cmd} a={a})') + current_context = "opacity_animators" + else: + # Static case with k value + k = LottieTensor._format_value(params[LottieTensor.Index.OpacityAnimators.K]) if params[LottieTensor.Index.OpacityAnimators.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.OpacityAnimators.IX]) if params[LottieTensor.Index.OpacityAnimators.IX] > -2000 else 9 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + # 添加 CMD_POSITION_ANIMATORS ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_POSITION_ANIMATORS: + a = int(params[LottieTensor.Index.PositionAnimators.A]) if params[LottieTensor.Index.PositionAnimators.A] > -2000 else 0 + ix = int(params[LottieTensor.Index.PositionAnimators.IX]) if params[LottieTensor.Index.PositionAnimators.IX] > -2000 else 2 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "position_animators" + else: + # Static case with k value + k_x = params[LottieTensor.Index.PositionAnimators.K_X] + k_y = params[LottieTensor.Index.PositionAnimators.K_Y] + k_z = params[LottieTensor.Index.PositionAnimators.K_Z] + + if k_x > -2000 and k_y > -2000: + # Format as array + x_val = LottieTensor._format_value(k_x) if k_x > -2000 else 0 + y_val = LottieTensor._format_value(k_y) if k_y > -2000 else 0 + z_val = LottieTensor._format_value(k_z) if k_z > -2000 else 0 + lines.append(f'({cmd} a={a} k=[{x_val}, {y_val}, {z_val}] ix={ix})') + else: + lines.append(f'({cmd} a={a} k=[0.0, 0.0, 0.0] ix={ix})') + + # 添加 CMD_TRACKING_ANIMATORS ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_TRACKING_ANIMATORS: + a = int(params[LottieTensor.Index.TrackingAnimators.A]) if params[LottieTensor.Index.TrackingAnimators.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.TrackingAnimators.K]) if params[LottieTensor.Index.TrackingAnimators.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.TrackingAnimators.IX]) if params[LottieTensor.Index.TrackingAnimators.IX] > -2000 else 89 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "tracking_animators" + else: + # Static case + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + # ę·»åŠ ē»“ęŸå‘½ä»¤ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_POSITION_ANIMATORS_END: + lines.append(f'({cmd})') + current_context = None + + + # Add output formatting (after CMD_OPACITY_ANIMATORS, around line 5590) + elif cmd_idx == LottieTensor.CMD_SCALE_ANIMATORS: + a = int(params[LottieTensor.Index.ScaleAnimators.A]) if params[LottieTensor.Index.ScaleAnimators.A] > -2000 else 0 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "scale_animators" + else: + # Static case with k value + k_x = params[LottieTensor.Index.ScaleAnimators.K_X] + k_y = params[LottieTensor.Index.ScaleAnimators.K_Y] + k_z = params[LottieTensor.Index.ScaleAnimators.K_Z] + + if k_x > -2000 and k_y > -2000 and k_z > -2000: + # Check if all values are the same + if abs(k_x - k_y) < 1e-6 and abs(k_y - k_z) < 1e-6: + # Output single value + lines.append(f'({cmd} a={a} k={LottieTensor._format_value(k_x)})') + else: + # Output array + lines.append(f'({cmd} a={a} k=[{LottieTensor._format_value(k_x)}, {LottieTensor._format_value(k_y)}, {LottieTensor._format_value(k_z)}])') + else: + lines.append(f'({cmd} a={a} k=100)') + + elif cmd_idx == LottieTensor.CMD_SCALE_ANIMATORS_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_ROTATION_ANIMATORS: + a = int(params[LottieTensor.Index.RotationAnimators.A]) if params[LottieTensor.Index.RotationAnimators.A] > -2000 else 0 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "rotation_animators" + else: + # Static case with k value + k = LottieTensor._format_value(params[LottieTensor.Index.RotationAnimators.K]) if params[LottieTensor.Index.RotationAnimators.K] > -2000 else 0 + lines.append(f'({cmd} a={a} k={k})') + + elif cmd_idx == LottieTensor.CMD_WIDTH_ANIMATED: + # This is a standalone width_animated command + # The context should already be set from the stroke command + # No parameters needed for this command + pass + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET: + # Use Amount indices for range_offset + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + + if a > 0.5: # This should be checking a, not params[LottieTensor.Index.Amount.A] again + # Animated case - only output a + lines.append(f'({cmd} a={a})') + current_context = "range_offset" + else: + # Static case - output a, k, and ix + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 3 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + + elif cmd_idx == LottieTensor.CMD_ROTATION_ANIMATORS_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_RECT_SIZE: + # Output rect_size with two values + if params[LottieTensor.Index.Transform.ANIMATED] > -2000 and params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "size" + else: + # äæ®ę”¹ļ¼šä½æē”Ø Transform.X 和 Transform.Y + val1 = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + val2 = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + lines.append(f'({cmd} {val1} {val2})') + + + + elif cmd_idx == LottieTensor.CMD_ELLIPSE_SIZE: + # Output rect_size with two values + # ę£€ęŸ„ę˜Æå¦ę˜ÆåŠØē”» + animated_val = params[LottieTensor.Index.Transform.ANIMATED] + if animated_val > -2000 and animated_val > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "size" # ē”®äæč®¾ē½®äøŠäø‹ę–‡ + else: + # é™ę€å€¼ - ę£€ęŸ„ X 和 Y ę˜Æå¦äøŗęœ‰ę•ˆå€¼ + x_val = params[LottieTensor.Index.Transform.X] + y_val = params[LottieTensor.Index.Transform.Y] + # å¦‚ęžœ X 和 Y éƒ½ę˜Æé»˜č®¤å€¼ 0 äø” ANIMATED ęœŖč®¾ē½®ļ¼ŒåÆčƒ½ę˜Æę•°ę®äø¢å¤± + val1 = LottieTensor._format_value(x_val if x_val > -2000 else 0) + val2 = LottieTensor._format_value(y_val if y_val > -2000 else 0) + lines.append(f'({cmd} {val1} {val2})') + + + elif cmd_idx == LottieTensor.CMD_RECT_ROUNDED: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "rect_rounded" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_RECT_ROUNDED_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_SKEW: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_SKEW_AXIS: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + else: + # Default case for any unhandled commands + lines.append(f"({cmd})") + + return '\n'.join(lines) + + # Keep other methods unchanged + def to_tensor(self) -> torch.Tensor: + """Convert LottieTensor to a single tensor""" + return torch.cat([self.commands.float(), self.params], dim=1) + + @staticmethod + def from_tensor(tensor: torch.Tensor) -> 'LottieTensor': + """Create LottieTensor from tensor""" + commands = tensor[:, 0:1].long() + params = tensor[:, 1:1+LottieTensor.PARAM_DIM].float() + + return LottieTensor(commands, params, PAD_VAL=-2001) + + def add_sos(self): + """Add start-of-sequence token""" + self.commands = torch.cat([self.sos_token, self.commands]) + pad_params = torch.ones((1, self.PARAM_DIM)) * self.PAD_VAL + self.params = torch.cat([pad_params, self.params]) + self.seq_len += 1 + return self + + def add_eos(self): + """Add end-of-sequence token""" + self.commands = torch.cat([self.commands, self.eos_token]) + pad_params = torch.ones((1, self.PARAM_DIM)) * self.PAD_VAL + self.params = torch.cat([self.params, pad_params]) + self.seq_len += 1 + return self + + def pad(self, seq_len: int): + """Pad sequence to specified length""" + pad_len = max(seq_len - len(self.commands), 0) + if pad_len > 0: + pad_commands = torch.ones((pad_len, 1)) * LottieTensor.CMD_PAD + pad_params = torch.ones((pad_len, self.PARAM_DIM)) * self.PAD_VAL + + self.commands = torch.cat([self.commands, pad_commands.long()]) + self.params = torch.cat([self.params, pad_params]) + return self + + @staticmethod + def _clamp_value(value: float, min_val: float = -2000, max_val: float = 2000) -> float: + """Clamp a value between min and max bounds""" + return max(min_val, min(max_val, value)) + + @staticmethod + def _index_clamp_value(value: float, min_val: float = -100, max_val: float = 100) -> float: + """Clamp a value between min and max bounds""" + return max(min_val, min(max_val, value)) + + + @classmethod + def init_tokenizer(cls, model_path=None): + """Initialize tokenizer once for the class - ę”ÆęŒå¤šč·Æå¾„fallback""" + if cls.tokenizer is None: + from transformers import AutoTokenizer + if model_path is None: + # å°čÆ•å¤šäøŖåÆčƒ½ēš„č·Æå¾„ + possible_paths = [ + '/mnt/jfs-test/Qwen2.5-VL-3B-Instruct', + '/data/models/Qwen2.5-VL-3B-Instruct', + 'Qwen/Qwen2.5-VL-3B-Instruct', # HuggingFace Hub + ] + for path in possible_paths: + try: + cls.tokenizer = AutoTokenizer.from_pretrained(path) + # åŖåœØäø»čæ›ēØ‹ę‰“å°äø€ę¬” + import os + if os.environ.get('RANK', '0') == '0': + print(f"Tokenizer loaded successfully from: {path}") + return + except Exception as e: + continue + raise ValueError(f"Failed to load tokenizer from any known path: {possible_paths}") + else: + cls.tokenizer = AutoTokenizer.from_pretrained(model_path) + + @classmethod + def get_tokenizer(cls): + if cls.tokenizer is None: + from transformers import AutoTokenizer + cls.tokenizer = AutoTokenizer.from_pretrained('/mnt/jfs-test/Qwen2.5-VL-3B-Instruct') + return cls.tokenizer + + + @staticmethod + def get_param_offset(cmd_idx: int, param_idx: int) -> int: + """ + Get the offset for a parameter based on its command and parameter index. + Returns the offset to add to the parameter value. + """ + # 1. ęŸ„ē¼“å­˜ + cache_key = (cmd_idx, param_idx) + if cache_key in LottieTensor._OFFSET_CACHE: + return LottieTensor._OFFSET_CACHE[cache_key] + + + # ę›“ę–°åŽēš„offsetčŒƒå›“ļ¼Œē”®äæę²”ęœ‰overlap + TIME_OFFSET = 155000 # -2000 to 2000: range [153000, 157000] (4001 values) + SPACE_OFFSET = 159100 # -2000 to 2000: range [157100, 161100] (4001 values) + AMPLITUDE_OFFSET = 161200 # 0 to 20: range [161200, 161220] (21 values) + ANCHOR_OFFSET = 161300 # -2000 to 2000: range [161300, 165300] (4001 values) + ANIMATED_OFFSET = 165400 # 0 to 1: range [165400, 165401] (2 values) + H_FLAG_OFFSET = 165402 # 0 to 1: range [165402, 165403] (2 values) + OFFSET_VAL_OFFSET = 165404 # 0 to 1: range [165404, 165405] (2 values) + CA_OFFSET = 165406 # 0 to 2: range [165406, 165408] (3 values) + JUSTIFY_OFFSET = 165409 # 0 to 6: range [165409, 165415] (7 values) + TEXT_TRACKING_OFFSET = 165416 # -100 to 500: range [165416, 166016] (601 values) + HAS_STROKE_COLOR_OFFSET = 166017 # 0 to 1: range [166017, 166018] (2 values) + IX_OFFSET = 166019 # 0 to 1000: range [166019, 167019] (1001 values) + BM_OFFSET = 167020 # 0 to 20: range [167020, 167040] (21 values) + CLOSED_OFFSET = 167041 # 0 to 1: range [167041, 167042] (2 values) + DIRECTION_OFFSET = 167043 # 0 to 5: range [167043, 167048] (6 values) + STAR_TYPE_OFFSET = 167049 # 0 to 5: range [167049, 167054] (6 values) + MULTIPLE_OFFSET = 167055 # 0 to 5: range [167055, 167060] (6 values) + COMPOSITE_OFFSET = 167061 # 0 to 5: range [167061, 167066] (6 values) + SKEW_OFFSET = 167067 # -25 to 25: range [167067, 167117] (51 values) + SKEW_AXIS_OFFSET = 167118 # -25 to 25: range [167118, 167168] (51 values) + SCALE_OFFSET = 167169 # -1000 to 2000: range [167169, 170169] (3001 values) + ROTATION_OFFSET = 170170 # -720 to 720: range [170170, 171610] (1441 values) + EASE_OFFSET = 171611 # -100 to 100: range [171611, 171811] (201 values) + SMOOTH_OFFSET = 171812 # 0 to 100: range [171812, 171912] (101 values) + TRACKING_OFFSET = 171913 # -50 to 50: range [171913, 172013] (101 values) + INDEX_OFFSET = 172014 # 0 to 1000: range [172014, 173014] (1001 values) + DDD_OFFSET = 173015 # 0 to 1: range [173015, 173016] (2 values) + HD_OFFSET = 173017 # 0 to 1: range [173017, 173018] (2 values) + CP_OFFSET = 173019 # 0 to 50: range [173019, 173069] (51 values) + HAS_MASK_OFFSET = 173070 # 0 to 1: range [173070, 173071] (2 values) + AO_OFFSET = 173072 # 0 to 1: range [173072, 173073] (2 values) + TT_OFFSET = 173074 # 0 to 5: range [173074, 173079] (6 values) + TP_OFFSET = 173080 # 0 to 100: range [173080, 173180] (101 values) + TD_OFFSET = 173181 # 0 to 2: range [173181, 173183] (3 values) + CT_OFFSET = 173184 # 0 to 1: range [173184, 173185] (2 values) + NUMBER_OFFSET = 173186 # 0 to 500: range [173186, 173686] (501 values) + DIM_OFFSET = 173687 # 0 to 10: range [173687, 173697] (11 values) + HAS_C_A_OFFSET = 173698 # 0 to 1: range [173698, 173699] (2 values) + HAS_C_IX_OFFSET = 173700 # 0 to 1: range [173700, 173701] (2 values) + HAS_O_A_OFFSET = 173702 # 0 to 1: range [173702, 173703] (2 values) + HAS_O_IX_OFFSET = 173704 # 0 to 1: range [173704, 173705] (2 values) + FILL_RULE_OFFSET = 173706 # 0 to 4: range [173706, 173710] (5 values) + TYPE_OFFSET = 173711 # 0 to 40: range [173711, 173751] (41 values) + TEXT_RANGE_UNITS_OFFSET = 173752 # 0 to 10: range [173752, 173762] (11 values) + INV_OFFSET = 173763 # 0 to 1: range [173763, 173764] (2 values) + MODE_OFFSET = 173765 # 0 to 10: range [173765, 173775] (11 values) + TEXT_SHAPE_TYPE_OFFSET = 173776 # 0 to 10: range [173776, 173786] (11 values) + TEXT_RANDOM_OFFSET = 173787 # 0 to 1: range [173787, 173788] (2 values) + COLOR_POINTS_OFFSET = 173789 # 0 to 50: range [173789, 173839] (51 values) + ROUND_OFFSET = 173840 # -100 to 1000: range [173840, 174940] (1101 values) + RADIUS_OFFSET = 174941 # 0 to 300: range [174941, 175241] (301 values) + FREQUENCY_OFFSET = 175242 # 0 to 150: range [175242, 175392] (151 values) + SPEED_OFFSET = 175393 # -1000 to 1000: range [175393, 177393] (2001 values) + FONT_OFFSET = 177394 # -100 to 2000: range [177394, 179494] (2101 values) + COLOR_OFFSET = 179495 # 0 to 255: range [179495, 179750] (256 values) + LINE_CAP_OFFSET = 179752 # 1 to 3: range [179752, 179754] (3 values) + LINE_JOIN_OFFSET = 179757 # 1 to 3: range [179757, 179759] (3 values) + MITER_LIMIT_OFFSET = 179760 # 0 to 100: range [179760, 179860] (101 values) + EFFECT_OFFSET = 179861 # -250 to 1000: range [179861, 181111] (1251 values) + OPACITY_OFFSET = 181112 # 0 to 100: range [181112, 181212] (101 values) + WIDTH_VALUE_OFFSET = 181300 # 0 to 10000: range [181300, 191300] (10001 values for 0-100.00) + + NO_OFFSET = 0 + + # Time dictionary parameters - all time-related values + time_params = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.IP, + LottieTensor.Index.Animation.OP + ], + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.IN_POINT, + LottieTensor.Index.Layer.OUT_POINT, + LottieTensor.Index.Layer.START_TIME + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.IN_POINT, + LottieTensor.Index.NullLayer.OUT_POINT, + LottieTensor.Index.NullLayer.START_TIME + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.IN_POINT, + LottieTensor.Index.PrecompLayer.OUT_POINT, + LottieTensor.Index.PrecompLayer.START_TIME + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.IN_POINT, + LottieTensor.Index.TextLayer.OUT_POINT, + LottieTensor.Index.TextLayer.START_TIME + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.IN_POINT, + LottieTensor.Index.SolidLayer.OUT_POINT, + LottieTensor.Index.SolidLayer.START_TIME + ], + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.T + ], + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.T + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.T + ], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.T + ], + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.T + ], + LottieTensor.CMD_MASK_PT_KEYFRAME: [ + LottieTensor.Index.MaskPtKeyframe.T + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.T + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.T + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.T + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.T + ], + } + + # Space dictionary parameters - all spatial/positional values + space_params = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.W, + LottieTensor.Index.Animation.H + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.W, + LottieTensor.Index.PrecompLayer.H + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.WIDTH, + LottieTensor.Index.SolidLayer.HEIGHT + ], + LottieTensor.CMD_DIMENSIONS: [ + LottieTensor.Index.Dimensions.WIDTH, + LottieTensor.Index.Dimensions.HEIGHT + ], + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.STROKE_WIDTH, + LottieTensor.Index.TextKeyframe.WRAP_POSITION_X, + LottieTensor.Index.TextKeyframe.WRAP_POSITION_Y, + LottieTensor.Index.TextKeyframe.WRAP_SIZE_X, + LottieTensor.Index.TextKeyframe.WRAP_SIZE_Y + ], + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.S1, LottieTensor.Index.Keyframe.S2, LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.E1, LottieTensor.Index.Keyframe.E2, LottieTensor.Index.Keyframe.E3, + LottieTensor.Index.Keyframe.TO1, LottieTensor.Index.Keyframe.TO2, LottieTensor.Index.Keyframe.TO3, + LottieTensor.Index.Keyframe.TI1, LottieTensor.Index.Keyframe.TI2, LottieTensor.Index.Keyframe.TI3 + ], + #LottieTensor.CMD_WIDTH_KEYFRAME: [ + # LottieTensor.Index.WidthKeyframe.S, + #], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.S1, + ], + LottieTensor.CMD_POSITION: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y, LottieTensor.Index.Transform.Z, + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_POSITION_X: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Y: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Z: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POINT: [ + LottieTensor.Index.Point.X, LottieTensor.Index.Point.Y, + LottieTensor.Index.Point.IN_X, LottieTensor.Index.Point.IN_Y, + LottieTensor.Index.Point.OUT_X, LottieTensor.Index.Point.OUT_Y + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.POSITION_X, LottieTensor.Index.TransformShape.POSITION_Y, + ], + LottieTensor.CMD_SIZE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_RECT_SIZE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_ELLIPSE_SIZE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_START_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_END_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_POINTS_STAR: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_START: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_END: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OFFSET: [ + LottieTensor.Index.SingleValue.VALUE + ], + #LottieTensor.CMD_WIDTH: [ + # LottieTensor.Index.SingleValue.VALUE + #], + #LottieTensor.CMD_DASH: [ + # LottieTensor.Index.Dash.LENGTH + #], + #LottieTensor.CMD_DASH_OFFSET: [ + # LottieTensor.Index.DashOffset.O + #], + #LottieTensor.CMD_DASH_KEYFRAME: [ + # LottieTensor.Index.DashKeyframe.S, + # ], + LottieTensor.CMD_TR_POSITION: [ + LottieTensor.Index.TrPosition.X, LottieTensor.Index.TrPosition.Y + ], + LottieTensor.CMD_REPEATER_OFFSET: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.S, + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.S, + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.S, + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.K_X, + LottieTensor.Index.PositionAnimators.K_Y, + LottieTensor.Index.PositionAnimators.K_Z + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.K + ], + LottieTensor.CMD_MASK_PT_K_I: list(range(20)), + LottieTensor.CMD_MASK_PT_K_O: list(range(20)), + LottieTensor.CMD_MASK_PT_K_V: list(range(20)), + LottieTensor.CMD_MASK_PT_KF_I: [ + LottieTensor.Index.MaskPtKfI.X, LottieTensor.Index.MaskPtKfI.Y + ], + LottieTensor.CMD_MASK_PT_KF_O: [ + LottieTensor.Index.MaskPtKfO.X, LottieTensor.Index.MaskPtKfO.Y + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE_I: list(range(20)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_O: list(range(20)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_V: list(range(20)), + LottieTensor.CMD_VALUE: [ + LottieTensor.Index.Value.VALUE + ], + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.ALIGNMENT_K1, + LottieTensor.Index.MoreOptions.ALIGNMENT_K2 + ], + LottieTensor.CMD_ALIGNMENT_K: [ + LottieTensor.Index.AlignmentK.VALUE1, + LottieTensor.Index.AlignmentK.VALUE2 + ], + LottieTensor.CMD_CHAR: [ + LottieTensor.Index.Char.W + ], + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.START_POINT_X, + LottieTensor.Index.GradientFill.START_POINT_Y, + LottieTensor.Index.GradientFill.END_POINT_X, + LottieTensor.Index.GradientFill.END_POINT_Y, + LottieTensor.Index.GradientFill.HIGHLIGHT_LENGTH, + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.WIDTH, + LottieTensor.Index.GradientStroke.START_POINT_X, + LottieTensor.Index.GradientStroke.START_POINT_Y, + LottieTensor.Index.GradientStroke.END_POINT_X, + LottieTensor.Index.GradientStroke.END_POINT_Y, + LottieTensor.Index.GradientStroke.HIGHLIGHT_LENGTH, + ], + LottieTensor.CMD_HIGHLIGHT_LENGTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + width_value_params = { + LottieTensor.CMD_WIDTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.LENGTH + ], + LottieTensor.CMD_DASH_OFFSET: [ + LottieTensor.Index.DashOffset.O + ], + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.S, + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.S, + ], + } + + amplitude_params = { + LottieTensor.CMD_AMPLITUDE: [ + LottieTensor.Index.Amplitude.VALUE + ], + } + + anchor_params = { + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.ANCHOR_X, LottieTensor.Index.TransformShape.ANCHOR_Y, + ], + LottieTensor.CMD_TR_ANCHOR: [ + LottieTensor.Index.TrAnchor.X, LottieTensor.Index.TrAnchor.Y + ], + LottieTensor.CMD_ANCHOR: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y, LottieTensor.Index.Transform.Z + ], + } + + animated_params = { + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.ALIGNMENT_A, + ], + LottieTensor.CMD_ELLIPSE_SIZE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_RECT_SIZE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION_X: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION_Y: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION_Z: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_SCALE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_ROTATION: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_OPACITY: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_ANCHOR: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_SIZE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.ANIMATED, + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.COLOR_ANIMATED, + LottieTensor.Index.Fill.OPACITY_ANIMATED, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.WIDTH_ANIMATED, + LottieTensor.Index.Stroke.COLOR_ANIMATED + ], + LottieTensor.CMD_RECT_ROUNDED: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_START: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_END: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_OFFSET: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_MASK_PT: [ + LottieTensor.Index.MaskPt.A, + ], + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.A, + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.A, + ], + LottieTensor.CMD_TM: [ + LottieTensor.Index.Tm.A + ], + LottieTensor.CMD_RANGE_START: [ + LottieTensor.Index.RangeStart.A + ], + LottieTensor.CMD_RANGE_END: [ + LottieTensor.Index.RangeEnd.A + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.A, + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.A, + ], + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.A, + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.A, + ], + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.A, + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.A, + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.A, + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.A, + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.A, + ], + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.A, + ], + LottieTensor.CMD_ALIGNMENT: [ + LottieTensor.Index.Alignment.A + ], + } + + h_flag_params = { + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.H_FLAG + ], + } + + offset_val_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.OFFSET, + ], + } + + ca_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.CA, + ], + LottieTensor.CMD_CA: [ + LottieTensor.Index.Ca.VALUE + ], + } + + justify_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.JUSTIFY, + ], + LottieTensor.CMD_JUSTIFY: [ + LottieTensor.Index.Justify.VALUE + ], + } + + text_tracking_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.TRACKING, + ], + LottieTensor.CMD_TRACKING: [ + LottieTensor.Index.Tracking.VALUE + ], + } + + has_stroke_color_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.HAS_STROKE_COLOR, + ], + } + + ix_params = { + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.IX, + LottieTensor.Index.Path.KS_IX, + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.IX, + LottieTensor.Index.Group.CIX, + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.C_IX, + LottieTensor.Index.Fill.O_IX, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.C_IX, + ], + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.IX, + ], + LottieTensor.CMD_ROUNDED: [ + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_TRIM: [ + LottieTensor.Index.Trim.IX + ], + LottieTensor.CMD_REPEATER: [ + LottieTensor.Index.Repeater.IX + ], + LottieTensor.CMD_COPIES: [ + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_REPEATER_OFFSET: [ + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_TR_P_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_A_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_S_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_R_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_SO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_EO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.ML2_IX, + ], + LottieTensor.CMD_MASK_PT: [ + LottieTensor.Index.MaskPt.IX, + ], + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.IX, + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.IX, + ], + LottieTensor.CMD_ZIG_ZAG: [ + LottieTensor.Index.ZigZag.IX + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.IX + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.IX + ], + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.IX + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.IX + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.IX + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.IX + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.IX + ], + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.IX + ], + LottieTensor.CMD_ALIGNMENT_IX: [ + LottieTensor.Index.AlignmentIx.VALUE + ], + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.ALIGNMENT_IX + ], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.V_IX + ], + LottieTensor.CMD_DASH_ANIMATED: [ + LottieTensor.Index.DashAnimated.V_IX, + ], + } + + bm_params = { + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.BM, + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.BM, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.BM, + ], + } + + closed_params = { + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.CLOSED, + ], + LottieTensor.CMD_BEZIER: [ + LottieTensor.Index.Bezier.CLOSED + ], + LottieTensor.CMD_MASK_PT_K_C: [ + LottieTensor.Index.MaskPtK.C + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE: [ + LottieTensor.Index.MaskPtKfShape.C, + ], + } + + direction_params = { + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.D, + ], + LottieTensor.CMD_STAR: [ + LottieTensor.Index.Star.D, + ], + } + + star_type_params = { + LottieTensor.CMD_STAR: [ + LottieTensor.Index.Star.SY + ], + } + + multiple_params = { + LottieTensor.CMD_MULTIPLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + composite_params = { + LottieTensor.CMD_COMPOSITE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + skew_params = { + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.SKEW, + ], + LottieTensor.CMD_SKEW: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + skew_axis_params = { + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.SKEW_AXIS, + ], + LottieTensor.CMD_SKEW_AXIS: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + scale_params = { + LottieTensor.CMD_SCALE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y, LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.SCALE_X, LottieTensor.Index.TransformShape.SCALE_Y, + ], + LottieTensor.CMD_TR_SCALE: [ + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.K_X, + LottieTensor.Index.ScaleAnimators.K_Y, + LottieTensor.Index.ScaleAnimators.K_Z + ], + } + + rotation_params = { + LottieTensor.CMD_ROTATION: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.ROTATION + ], + LottieTensor.CMD_STAR_ROTATION: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_ROTATION: [ + LottieTensor.Index.TrRotation.VALUE + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.K + ], + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.HIGHLIGHT_ANGLE, + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.HIGHLIGHT_ANGLE + ], + LottieTensor.CMD_HIGHLIGHT_ANGLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + ease_params = { + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.K + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.K + ], + } + + smooth_params = { + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.K + ], + } + + tracking_params = { + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.K + ], + } + + index_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.INDEX, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.INDEX, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.INDEX, + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.INDEX, + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.INDEX, + ], + LottieTensor.CMD_PARENT: [ + LottieTensor.Index.Parent.PARENT_INDEX + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.IND, + ], + LottieTensor.CMD_COLOR: [ + LottieTensor.Index.Color.INDEX + ], + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.INDEX, + ], + LottieTensor.CMD_MASK_PT_KEYFRAME: [ + LottieTensor.Index.MaskPtKeyframe.INDEX + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE: [ + LottieTensor.Index.MaskPtKfShape.INDEX, + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.INDEX, + ], + LottieTensor.CMD_LAYER_EFFECT: [ + LottieTensor.Index.LayerEffect.INDEX, + ], + LottieTensor.CMD_DROPDOWN: [ + LottieTensor.Index.Dropdown.INDEX, + ], + LottieTensor.CMD_NO_VALUE: [ + LottieTensor.Index.NO_VALUE.INDEX, + ], + LottieTensor.CMD_IGNORED: [ + LottieTensor.Index.Ignored.INDEX, + ], + LottieTensor.CMD_SLIDER: [ + LottieTensor.Index.Slider.INDEX, + ], + } + + ddd_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.DDD, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.DDD, + ], + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.DDD + ], + } + + hd_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.HD, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.HD, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.HD, + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.HD, + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.HD, + ], + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.HD, + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.HD + ], + } + + cp_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.CP, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.CP, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.CP, + ], + } + + has_mask_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.HAS_MASK, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.HAS_MASK, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.HAS_MASK, + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.HAS_MASK, + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.HAS_MASK + ], + } + + ao_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.AO, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.AO, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.AO, + ], + } + + tt_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.TT, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.TT, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.TT, + ], + } + + tp_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.TP, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.TP, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.TP, + ], + } + + td_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.TD, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.TD, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.TD, + ], + } + + ct_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.CT, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.CT, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.CT, + ], + } + + number_params = { + LottieTensor.CMD_COPIES: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.K + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.K + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.NP + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.NP, + ], + } + + dim_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.COLOR_DIM, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.COLOR_DIM, + ], + } + + has_c_a_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_C_A, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.HAS_C_A, + ], + } + + has_c_ix_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_C_IX, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.HAS_C_IX, + ], + } + + has_o_a_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_O_A, + ], + } + + has_o_ix_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_O_IX, + ], + } + + fill_rule_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.FILL_RULE, + ], + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.FILL_RULE, + ], + LottieTensor.CMD_FILL_RULE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + type_params = { + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.GRADIENT_TYPE, + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.GRADIENT_TYPE, + ], + LottieTensor.CMD_GRADIENT_TYPE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_POINT_TYPE: [ + LottieTensor.Index.PointType.VALUE + ], + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.T, + ], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.TYPE, + ], + LottieTensor.CMD_DASH_ANIMATED: [ + LottieTensor.Index.DashAnimated.TYPE, + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.TYPE, + ], + } + + text_range_units = { + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.R, + ], + } + + inv_params = { + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.INV, + ], + } + + mode_params = { + LottieTensor.CMD_MERGE_MODE: [ + LottieTensor.Index.MergeMode.MODE + ], + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.MODE, + ], + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.B, + ], + } + + text_shape_type = { + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.SH, + ], + } + + text_random = { + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.RN + ], + } + + color_points_params = { + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.COLOR_POINTS + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.COLOR_POINTS + ], + LottieTensor.CMD_COLOR_POINTS: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + round_params = { + LottieTensor.CMD_ROUNDED: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_RECT_ROUNDED: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_INNER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + radius_params = { + LottieTensor.CMD_INNER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_RADIUS: [ + LottieTensor.Index.Radius.VALUE + ], + } + + frequency_params = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.FR, + ], + LottieTensor.CMD_FREQUENCY: [ + LottieTensor.Index.Frequency.VALUE + ], + LottieTensor.CMD_ASSET: [ + LottieTensor.Index.Asset.FR + ], + } + + speed_params = { + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.I_X, LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, LottieTensor.Index.Keyframe.O_Y, + LottieTensor.Index.Keyframe.I_X2, LottieTensor.Index.Keyframe.I_Y2, + LottieTensor.Index.Keyframe.O_X2, LottieTensor.Index.Keyframe.O_Y2, + LottieTensor.Index.Keyframe.I_X3, LottieTensor.Index.Keyframe.I_Y3, + LottieTensor.Index.Keyframe.O_X3, LottieTensor.Index.Keyframe.O_Y3, + ], + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.I_X, LottieTensor.Index.WidthKeyframe.I_Y, + LottieTensor.Index.WidthKeyframe.O_X, LottieTensor.Index.WidthKeyframe.O_Y + ], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.I_X, LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.I_X, LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.I_X, LottieTensor.Index.DashKeyframe.I_Y, + LottieTensor.Index.DashKeyframe.O_X, LottieTensor.Index.DashKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.I_X, LottieTensor.Index.RangeStartKeyframe.I_Y, + LottieTensor.Index.RangeStartKeyframe.O_X, LottieTensor.Index.RangeStartKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.I_X, LottieTensor.Index.RangeEndKeyframe.I_Y, + LottieTensor.Index.RangeEndKeyframe.O_X, LottieTensor.Index.RangeEndKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.I_X, LottieTensor.Index.RangeOffsetKeyframe.I_Y, + LottieTensor.Index.RangeOffsetKeyframe.O_X, LottieTensor.Index.RangeOffsetKeyframe.O_Y + ], + } + + font_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.FONT_SIZE, + LottieTensor.Index.TextKeyframe.LINE_HEIGHT, + LottieTensor.Index.TextKeyframe.LETTER_SPACING + ], + LottieTensor.CMD_FONT_SIZE: [ + LottieTensor.Index.FontSize.SIZE + ], + LottieTensor.CMD_LINE_HEIGHT: [ + LottieTensor.Index.LineHeight.VALUE + ], + LottieTensor.CMD_LETTER_SPACING: [ + LottieTensor.Index.LetterSpacing.VALUE + ], + LottieTensor.CMD_FONT: [ + LottieTensor.Index.Font.ASCENT + ], + LottieTensor.CMD_CHAR: [ + LottieTensor.Index.Char.SIZE + ], + } + + color_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.FILL_COLOR_R, + LottieTensor.Index.TextKeyframe.FILL_COLOR_G, + LottieTensor.Index.TextKeyframe.FILL_COLOR_B, + LottieTensor.Index.TextKeyframe.STROKE_COLOR_R, + LottieTensor.Index.TextKeyframe.STROKE_COLOR_G, + LottieTensor.Index.TextKeyframe.STROKE_COLOR_B + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.R, + LottieTensor.Index.Stroke.G, + LottieTensor.Index.Stroke.B, + LottieTensor.Index.Stroke.A + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.R, + LottieTensor.Index.Fill.G, + LottieTensor.Index.Fill.B, + ], + LottieTensor.CMD_FILL_COLOR: [ + LottieTensor.Index.FillColor.R, + LottieTensor.Index.FillColor.G, + LottieTensor.Index.FillColor.B + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.COLOR_R, + LottieTensor.Index.SolidLayer.COLOR_G, + LottieTensor.Index.SolidLayer.COLOR_B, + LottieTensor.Index.SolidLayer.COLOR_A + ], + LottieTensor.CMD_COLOR: [ + LottieTensor.Index.Color.R, + LottieTensor.Index.Color.G, + LottieTensor.Index.Color.B + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.S2, + LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.E1 + ], + LottieTensor.CMD_ORIGINAL_COLORS: list(range(LottieTensor.Index.OriginalColors.COUNT)), + LottieTensor.CMD_GRADIENT_FILL: list(range(LottieTensor.Index.GradientFill.ORIGINAL_COLOR_0, + LottieTensor.Index.GradientFill.ORIGINAL_COLOR_23 + 1)), + LottieTensor.CMD_GRADIENT_STROKE: list(range(LottieTensor.Index.GradientStroke.ORIGINAL_COLOR_0, + LottieTensor.Index.GradientStroke.ORIGINAL_COLOR_23 + 1)), + } + + line_cap_params = { + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.LC, + ], + LottieTensor.CMD_LINE_CAP: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.LINE_CAP, + ], + } + line_join_params = { + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.LJ, + ], + LottieTensor.CMD_LINE_JOIN: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.LINE_JOIN, + ], + } + + miter_limit_params = { + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.ML + ], + LottieTensor.CMD_MITER_LIMIT: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_ML2: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.MITER_LIMIT, + LottieTensor.Index.GradientStroke.ML2 + ], + } + + enabled_params = { + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.ENABLED + ], + } + + effect_params = { + LottieTensor.CMD_LAYER_EFFECT: [ + LottieTensor.Index.LayerEffect.VALUE + ], + LottieTensor.CMD_DROPDOWN: [ + LottieTensor.Index.Dropdown.VALUE + ], + LottieTensor.CMD_NO_VALUE: [ + LottieTensor.Index.NO_VALUE.VALUE + ], + LottieTensor.CMD_IGNORED: [ + LottieTensor.Index.Ignored.VALUE + ], + LottieTensor.CMD_SLIDER: [ + LottieTensor.Index.Slider.VALUE + ], + } + + opacity_params = { + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.OPACITY + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.OPACITY + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.OPACITY + ], + LottieTensor.CMD_OPACITY: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.OPACITY, + ], + LottieTensor.CMD_TR_START_OPACITY: [ + LottieTensor.Index.TrStartOpacity.VALUE + ], + LottieTensor.CMD_TR_END_OPACITY: [ + LottieTensor.Index.TrEndOpacity.VALUE + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.K + ], + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.K + ], + } + + # Tokenizer parameters (no offset) + tokenizer_params = { + LottieTensor.CMD_TEXT_KEYFRAME: list(range(LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START, + LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT + 1)), + LottieTensor.CMD_ASSET: list(range(LottieTensor.Index.Asset.ID_TOKEN_0, + LottieTensor.Index.Asset.ID_TOKEN_COUNT + 1)), + LottieTensor.CMD_REFERENCE_ID: list(range(LottieTensor.Index.ReferenceId.ID_TOKEN_0, + LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT + 1)), + LottieTensor.CMD_FONT: list(range(LottieTensor.Index.Font.FAMILY_TOKEN_0, + LottieTensor.Index.Font.STYLE_TOKEN_COUNT + 1)), + LottieTensor.CMD_CHAR: list(range(LottieTensor.Index.Char.CH_TOKEN_0, + LottieTensor.Index.Char.FAMILY_TOKEN_COUNT + 1)), + } + # 添加 width_value_params ēš„åˆ¤ę–­ļ¼ˆåœØå…¶ä»–åˆ¤ę–­ä¹‹å‰ļ¼‰ + if cmd_idx in width_value_params and param_idx in width_value_params[cmd_idx]: + return WIDTH_VALUE_OFFSET + + elif cmd_idx in tokenizer_params and param_idx in tokenizer_params[cmd_idx]: + return NO_OFFSET # No offset for tokenizer tokens + elif cmd_idx in time_params and param_idx in time_params[cmd_idx]: + return TIME_OFFSET + elif cmd_idx in space_params and param_idx in space_params[cmd_idx]: + return SPACE_OFFSET + elif cmd_idx in amplitude_params and param_idx in amplitude_params[cmd_idx]: + return AMPLITUDE_OFFSET + elif cmd_idx in anchor_params and param_idx in anchor_params[cmd_idx]: + return ANCHOR_OFFSET + elif cmd_idx in animated_params and param_idx in animated_params[cmd_idx]: + return ANIMATED_OFFSET + elif cmd_idx in h_flag_params and param_idx in h_flag_params[cmd_idx]: + return H_FLAG_OFFSET + elif cmd_idx in offset_val_params and param_idx in offset_val_params[cmd_idx]: + return OFFSET_VAL_OFFSET + elif cmd_idx in ca_params and param_idx in ca_params[cmd_idx]: + return CA_OFFSET + elif cmd_idx in justify_params and param_idx in justify_params[cmd_idx]: + return JUSTIFY_OFFSET + elif cmd_idx in text_tracking_params and param_idx in text_tracking_params[cmd_idx]: + return TEXT_TRACKING_OFFSET + elif cmd_idx in has_stroke_color_params and param_idx in has_stroke_color_params[cmd_idx]: + return HAS_STROKE_COLOR_OFFSET + elif cmd_idx in ix_params and param_idx in ix_params[cmd_idx]: + return IX_OFFSET + elif cmd_idx in bm_params and param_idx in bm_params[cmd_idx]: + return BM_OFFSET + elif cmd_idx in closed_params and param_idx in closed_params[cmd_idx]: + return CLOSED_OFFSET + elif cmd_idx in direction_params and param_idx in direction_params[cmd_idx]: + return DIRECTION_OFFSET + elif cmd_idx in star_type_params and param_idx in star_type_params[cmd_idx]: + return STAR_TYPE_OFFSET + elif cmd_idx in multiple_params and param_idx in multiple_params[cmd_idx]: + return MULTIPLE_OFFSET + elif cmd_idx in composite_params and param_idx in composite_params[cmd_idx]: + return COMPOSITE_OFFSET + elif cmd_idx in skew_params and param_idx in skew_params[cmd_idx]: + return SKEW_OFFSET + elif cmd_idx in skew_axis_params and param_idx in skew_axis_params[cmd_idx]: + return SKEW_AXIS_OFFSET + elif cmd_idx in scale_params and param_idx in scale_params[cmd_idx]: + return SCALE_OFFSET + elif cmd_idx in rotation_params and param_idx in rotation_params[cmd_idx]: + return ROTATION_OFFSET + elif cmd_idx in ease_params and param_idx in ease_params[cmd_idx]: + return EASE_OFFSET + elif cmd_idx in smooth_params and param_idx in smooth_params[cmd_idx]: + return SMOOTH_OFFSET + elif cmd_idx in tracking_params and param_idx in tracking_params[cmd_idx]: + return TRACKING_OFFSET + elif cmd_idx in index_params and param_idx in index_params[cmd_idx]: + return INDEX_OFFSET + elif cmd_idx in ddd_params and param_idx in ddd_params[cmd_idx]: + return DDD_OFFSET + elif cmd_idx in hd_params and param_idx in hd_params[cmd_idx]: + return HD_OFFSET + elif cmd_idx in cp_params and param_idx in cp_params[cmd_idx]: + return CP_OFFSET + elif cmd_idx in has_mask_params and param_idx in has_mask_params[cmd_idx]: + return HAS_MASK_OFFSET + elif cmd_idx in ao_params and param_idx in ao_params[cmd_idx]: + return AO_OFFSET + elif cmd_idx in tt_params and param_idx in tt_params[cmd_idx]: + return TT_OFFSET + elif cmd_idx in tp_params and param_idx in tp_params[cmd_idx]: + return TP_OFFSET + elif cmd_idx in td_params and param_idx in td_params[cmd_idx]: + return TD_OFFSET + elif cmd_idx in ct_params and param_idx in ct_params[cmd_idx]: + return CT_OFFSET + elif cmd_idx in number_params and param_idx in number_params[cmd_idx]: + return NUMBER_OFFSET + elif cmd_idx in dim_params and param_idx in dim_params[cmd_idx]: + return DIM_OFFSET + elif cmd_idx in has_c_a_params and param_idx in has_c_a_params[cmd_idx]: + return HAS_C_A_OFFSET + elif cmd_idx in has_c_ix_params and param_idx in has_c_ix_params[cmd_idx]: + return HAS_C_IX_OFFSET + elif cmd_idx in has_o_a_params and param_idx in has_o_a_params[cmd_idx]: + return HAS_O_A_OFFSET + elif cmd_idx in has_o_ix_params and param_idx in has_o_ix_params[cmd_idx]: + return HAS_O_IX_OFFSET + elif cmd_idx in fill_rule_params and param_idx in fill_rule_params[cmd_idx]: + return FILL_RULE_OFFSET + elif cmd_idx in type_params and param_idx in type_params[cmd_idx]: + return TYPE_OFFSET + elif cmd_idx in text_range_units and param_idx in text_range_units[cmd_idx]: + return TEXT_RANGE_UNITS_OFFSET + elif cmd_idx in inv_params and param_idx in inv_params[cmd_idx]: + return INV_OFFSET + elif cmd_idx in mode_params and param_idx in mode_params[cmd_idx]: + return MODE_OFFSET + elif cmd_idx in text_shape_type and param_idx in text_shape_type[cmd_idx]: + return TEXT_SHAPE_TYPE_OFFSET + elif cmd_idx in text_random and param_idx in text_random[cmd_idx]: + return TEXT_RANDOM_OFFSET + elif cmd_idx in color_points_params and param_idx in color_points_params[cmd_idx]: + return COLOR_POINTS_OFFSET + elif cmd_idx in round_params and param_idx in round_params[cmd_idx]: + return ROUND_OFFSET + elif cmd_idx in radius_params and param_idx in radius_params[cmd_idx]: + return RADIUS_OFFSET + elif cmd_idx in frequency_params and param_idx in frequency_params[cmd_idx]: + return FREQUENCY_OFFSET + elif cmd_idx in speed_params and param_idx in speed_params[cmd_idx]: + return SPEED_OFFSET + elif cmd_idx in font_params and param_idx in font_params[cmd_idx]: + return FONT_OFFSET + elif cmd_idx in color_params and param_idx in color_params[cmd_idx]: + return COLOR_OFFSET + elif cmd_idx in line_cap_params and param_idx in line_cap_params[cmd_idx]: + return LINE_CAP_OFFSET + elif cmd_idx in line_join_params and param_idx in line_join_params[cmd_idx]: + return LINE_JOIN_OFFSET + elif cmd_idx in miter_limit_params and param_idx in miter_limit_params[cmd_idx]: + return MITER_LIMIT_OFFSET + elif cmd_idx in effect_params and param_idx in effect_params[cmd_idx]: + return EFFECT_OFFSET + elif cmd_idx in opacity_params and param_idx in opacity_params[cmd_idx]: + return OPACITY_OFFSET + + else: + return 0 # Default to no offset if not found + + LottieTensor._OFFSET_CACHE[cache_key] = offset + return offset + + + @staticmethod + def get_command_param_indices(cmd_idx: int) -> List[int]: + """ + Get the list of parameter indices for a command in their fixed order. + Returns empty list for commands without parameters. + """ + param_orders = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.FR, + LottieTensor.Index.Animation.IP, + LottieTensor.Index.Animation.OP, + LottieTensor.Index.Animation.W, + LottieTensor.Index.Animation.H, + LottieTensor.Index.Animation.DDD + ], + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.INDEX, + LottieTensor.Index.Layer.IN_POINT, + LottieTensor.Index.Layer.OUT_POINT, + LottieTensor.Index.Layer.START_TIME, + LottieTensor.Index.Layer.DDD, + LottieTensor.Index.Layer.HD, + LottieTensor.Index.Layer.CP, + LottieTensor.Index.Layer.HAS_MASK, + LottieTensor.Index.Layer.AO, + LottieTensor.Index.Layer.TT, + LottieTensor.Index.Layer.TP, + LottieTensor.Index.Layer.TD, + LottieTensor.Index.Layer.CT + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.INDEX, + LottieTensor.Index.NullLayer.IN_POINT, + LottieTensor.Index.NullLayer.OUT_POINT, + LottieTensor.Index.NullLayer.START_TIME, + LottieTensor.Index.NullLayer.CT, + LottieTensor.Index.NullLayer.HD, + LottieTensor.Index.NullLayer.HAS_MASK, + LottieTensor.Index.NullLayer.AO, + LottieTensor.Index.NullLayer.TT, + LottieTensor.Index.NullLayer.TP, + LottieTensor.Index.NullLayer.TD, + LottieTensor.Index.NullLayer.CP + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.INDEX, + LottieTensor.Index.PrecompLayer.IN_POINT, + LottieTensor.Index.PrecompLayer.OUT_POINT, + LottieTensor.Index.PrecompLayer.START_TIME, + LottieTensor.Index.PrecompLayer.W, + LottieTensor.Index.PrecompLayer.H, + LottieTensor.Index.PrecompLayer.CT, + LottieTensor.Index.PrecompLayer.HAS_MASK, + LottieTensor.Index.PrecompLayer.AO, + LottieTensor.Index.PrecompLayer.TT, + LottieTensor.Index.PrecompLayer.TP, + LottieTensor.Index.PrecompLayer.TD, + LottieTensor.Index.PrecompLayer.DDD, + LottieTensor.Index.PrecompLayer.HD, + LottieTensor.Index.PrecompLayer.CP + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.INDEX, + LottieTensor.Index.TextLayer.IN_POINT, + LottieTensor.Index.TextLayer.OUT_POINT, + LottieTensor.Index.TextLayer.START_TIME, + LottieTensor.Index.TextLayer.HAS_MASK + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.INDEX, + LottieTensor.Index.SolidLayer.IN_POINT, + LottieTensor.Index.SolidLayer.OUT_POINT, + LottieTensor.Index.SolidLayer.START_TIME, + LottieTensor.Index.SolidLayer.WIDTH, + LottieTensor.Index.SolidLayer.HEIGHT, + LottieTensor.Index.SolidLayer.HAS_MASK, + LottieTensor.Index.SolidLayer.COLOR_R, + LottieTensor.Index.SolidLayer.COLOR_G, + LottieTensor.Index.SolidLayer.COLOR_B, + LottieTensor.Index.SolidLayer.COLOR_A + ], + LottieTensor.CMD_TRANSFORM: [], + LottieTensor.CMD_POSITION: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, + LottieTensor.Index.Transform.Y, + LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_POSITION_X: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Y: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Z: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_SCALE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, + LottieTensor.Index.Transform.Y, + LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_ROTATION: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_OPACITY: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_ANCHOR: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, + LottieTensor.Index.Transform.Y, + LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.T, + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.S2, + LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.I_X, + LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, + LottieTensor.Index.Keyframe.O_Y, + LottieTensor.Index.Keyframe.TO1, + LottieTensor.Index.Keyframe.TO2, + LottieTensor.Index.Keyframe.TO3, + LottieTensor.Index.Keyframe.TI1, + LottieTensor.Index.Keyframe.TI2, + LottieTensor.Index.Keyframe.TI3, + LottieTensor.Index.Keyframe.I_X2, + LottieTensor.Index.Keyframe.I_X3, + LottieTensor.Index.Keyframe.I_Y2, + LottieTensor.Index.Keyframe.I_Y3, + LottieTensor.Index.Keyframe.O_X2, + LottieTensor.Index.Keyframe.O_X3, + LottieTensor.Index.Keyframe.O_Y2, + LottieTensor.Index.Keyframe.O_Y3, + LottieTensor.Index.Keyframe.H_FLAG, + LottieTensor.Index.Keyframe.E1, + LottieTensor.Index.Keyframe.E2, + LottieTensor.Index.Keyframe.E3 + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.IX, + LottieTensor.Index.Group.CIX, + LottieTensor.Index.Group.BM, + LottieTensor.Index.Group.HD, + LottieTensor.Index.Group.NP + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.IX, + LottieTensor.Index.Path.IND, + LottieTensor.Index.Path.KS_IX, + LottieTensor.Index.Path.CLOSED, + LottieTensor.Index.Path.HD, + LottieTensor.Index.Path.ANIMATED + ], + LottieTensor.CMD_POINT: [ + LottieTensor.Index.Point.X, + LottieTensor.Index.Point.Y, + LottieTensor.Index.Point.IN_X, + LottieTensor.Index.Point.IN_Y, + LottieTensor.Index.Point.OUT_X, + LottieTensor.Index.Point.OUT_Y + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.R, + LottieTensor.Index.Fill.G, + LottieTensor.Index.Fill.B, + LottieTensor.Index.Fill.COLOR_DIM, + LottieTensor.Index.Fill.HAS_C_A, + LottieTensor.Index.Fill.HAS_C_IX, + LottieTensor.Index.Fill.C_IX, + LottieTensor.Index.Fill.BM, + LottieTensor.Index.Fill.FILL_RULE, + LottieTensor.Index.Fill.OPACITY, + LottieTensor.Index.Fill.COLOR_ANIMATED, + LottieTensor.Index.Fill.OPACITY_ANIMATED, + LottieTensor.Index.Fill.HAS_O_A, + LottieTensor.Index.Fill.HAS_O_IX, + LottieTensor.Index.Fill.O_IX + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.R, + LottieTensor.Index.Stroke.G, + LottieTensor.Index.Stroke.B, + LottieTensor.Index.Stroke.COLOR_DIM, + LottieTensor.Index.Stroke.HAS_C_A, + LottieTensor.Index.Stroke.HAS_C_IX, + LottieTensor.Index.Stroke.C_IX, + LottieTensor.Index.Stroke.BM, + LottieTensor.Index.Stroke.LC, + LottieTensor.Index.Stroke.LJ, + LottieTensor.Index.Stroke.ML, + LottieTensor.Index.Stroke.WIDTH_ANIMATED, + LottieTensor.Index.Stroke.COLOR_ANIMATED, + LottieTensor.Index.Stroke.A + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.POSITION_X, + LottieTensor.Index.TransformShape.POSITION_Y, + LottieTensor.Index.TransformShape.SCALE_X, + LottieTensor.Index.TransformShape.SCALE_Y, + LottieTensor.Index.TransformShape.ROTATION, + LottieTensor.Index.TransformShape.OPACITY, + LottieTensor.Index.TransformShape.ANCHOR_X, + LottieTensor.Index.TransformShape.ANCHOR_Y, + LottieTensor.Index.TransformShape.SKEW, + LottieTensor.Index.TransformShape.SKEW_AXIS, + LottieTensor.Index.TransformShape.HD + ], + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.HD, + LottieTensor.Index.Rect.D + ], + LottieTensor.CMD_ELLIPSE: [], + LottieTensor.CMD_BEZIER: [ + LottieTensor.Index.Bezier.CLOSED + ], + LottieTensor.CMD_SIZE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, # äæ®ę”¹ļ¼šä»Ž TwoValues.VALUE1 改为 Transform.X + LottieTensor.Index.Transform.Y # äæ®ę”¹ļ¼šä»Ž TwoValues.VALUE2 改为 Transform.Y + ], + LottieTensor.CMD_RECT_SIZE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, # 修改 + LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_ELLIPSE_SIZE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, # 修改 + LottieTensor.Index.Transform.Y # 修改 + ], + + LottieTensor.CMD_ROUNDED: [ + LottieTensor.Index.SingleValue.VALUE, + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_RECT_ROUNDED: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TRIM: [ + LottieTensor.Index.Trim.IX + ], + LottieTensor.CMD_START: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_END: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OFFSET: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_PARENT: [ + LottieTensor.Index.Parent.PARENT_INDEX + ], + LottieTensor.CMD_REFERENCE_ID: list(range(11)), # 11 tokens + LottieTensor.CMD_DIMENSIONS: [ + LottieTensor.Index.Dimensions.WIDTH, + LottieTensor.Index.Dimensions.HEIGHT + ], + LottieTensor.CMD_ASSET: list(range(12)), # FR + 10 tokens + count + LottieTensor.CMD_TEXT_KEYFRAME: list(range(47)), # All text keyframe params + LottieTensor.CMD_FONT: list(range(23)), # All font params + LottieTensor.CMD_CHAR: list(range(35)), # All char params + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.T, + LottieTensor.Index.WidthKeyframe.S, + LottieTensor.Index.WidthKeyframe.I_X, + LottieTensor.Index.WidthKeyframe.I_Y, + LottieTensor.Index.WidthKeyframe.O_X, + LottieTensor.Index.WidthKeyframe.O_Y + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.T, + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.S2, + LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.E1, + LottieTensor.Index.Keyframe.I_X, + LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, + LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.T, + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.I_X, + LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, + LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_OPACITY_ANIMATED: [], + LottieTensor.CMD_TM: [ + LottieTensor.Index.Tm.A + ], + LottieTensor.CMD_VALUE: [ + LottieTensor.Index.Value.VALUE + ], + LottieTensor.CMD_SKEW: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_SKEW_AXIS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_STAR: [ + LottieTensor.Index.Star.D, + LottieTensor.Index.Star.SY + ], + LottieTensor.CMD_INNER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_INNER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_POINTS_STAR: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_STAR_ROTATION: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MULTIPLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_REPEATER: [ + LottieTensor.Index.Repeater.IX + ], + LottieTensor.CMD_COPIES: [ + LottieTensor.Index.SingleValue.VALUE, + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_REPEATER_OFFSET: [ + LottieTensor.Index.SingleValue.VALUE, + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_COMPOSITE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_REPEATER_TRANSFORM: [], + LottieTensor.CMD_TR_P_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_A_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_SCALE: [ + LottieTensor.Index.TwoValues.VALUE1, + LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_TR_S_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_R_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_SO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_EO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.G, + LottieTensor.Index.MoreOptions.ALIGNMENT_A, + LottieTensor.Index.MoreOptions.ALIGNMENT_K1, + LottieTensor.Index.MoreOptions.ALIGNMENT_K2, + LottieTensor.Index.MoreOptions.ALIGNMENT_IX + ], + LottieTensor.CMD_GRADIENT_FILL: [], + LottieTensor.CMD_START_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, + LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_END_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, + LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_GRADIENT_TYPE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_HIGHLIGHT_LENGTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_HIGHLIGHT_ANGLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_ORIGINAL_COLORS: list(range(48)), # All color values + count + LottieTensor.CMD_COLOR_POINTS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [], + LottieTensor.CMD_WIDTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_LINE_CAP: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_LINE_JOIN: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MITER_LIMIT: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_ML2: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_COLOR: [ + LottieTensor.Index.Color.INDEX, + LottieTensor.Index.Color.R, + LottieTensor.Index.Color.G, + LottieTensor.Index.Color.B + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.TYPE, + LottieTensor.Index.Effect.INDEX, + LottieTensor.Index.Effect.NP, + LottieTensor.Index.Effect.ENABLED + ], + LottieTensor.CMD_LAYER_EFFECT: [ + LottieTensor.Index.LayerEffect.INDEX, + LottieTensor.Index.LayerEffect.VALUE + ], + LottieTensor.CMD_DROPDOWN: [ + LottieTensor.Index.Dropdown.INDEX, + LottieTensor.Index.Dropdown.VALUE + ], + LottieTensor.CMD_NO_VALUE: [ + LottieTensor.Index.NO_VALUE.INDEX, + LottieTensor.Index.NO_VALUE.VALUE + ], + LottieTensor.CMD_IGNORED: [ + LottieTensor.Index.Ignored.INDEX, + LottieTensor.Index.Ignored.VALUE + ], + LottieTensor.CMD_SLIDER: [ + LottieTensor.Index.Slider.INDEX, + LottieTensor.Index.Slider.VALUE + ], + LottieTensor.CMD_FILL_RULE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MERGE: [], + LottieTensor.CMD_MERGE_MODE: [ + LottieTensor.Index.MergeMode.MODE + ], + LottieTensor.CMD_MASKS_PROPERTIES: [], + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.INDEX, + LottieTensor.Index.Mask.INV, + LottieTensor.Index.Mask.MODE + ], + LottieTensor.CMD_MASK_PT: [ + LottieTensor.Index.MaskPt.A, + LottieTensor.Index.MaskPt.IX + ], + LottieTensor.CMD_MASK_PT_K: [], + LottieTensor.CMD_MASK_PT_K_C: [ + LottieTensor.Index.MaskPtK.C + ], + LottieTensor.CMD_MASK_PT_K_I: list(range(21)), # V1-V20 + COUNT + LottieTensor.CMD_MASK_PT_K_O: list(range(21)), + LottieTensor.CMD_MASK_PT_K_V: list(range(21)), + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.A, + LottieTensor.Index.MaskO.K, + LottieTensor.Index.MaskO.IX + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.A, + LottieTensor.Index.MaskX.K, + LottieTensor.Index.MaskX.IX + ], + LottieTensor.CMD_MASK_PT_K_ARRAY: [], + LottieTensor.CMD_MASK_PT_KEYFRAME: [ + LottieTensor.Index.MaskPtKeyframe.INDEX, + LottieTensor.Index.MaskPtKeyframe.T + ], + LottieTensor.CMD_MASK_PT_KF_I: [ + LottieTensor.Index.MaskPtKfI.X, + LottieTensor.Index.MaskPtKfI.Y + ], + LottieTensor.CMD_MASK_PT_KF_O: [ + LottieTensor.Index.MaskPtKfO.X, + LottieTensor.Index.MaskPtKfO.Y + ], + LottieTensor.CMD_MASK_PT_KF_S: [], + LottieTensor.CMD_MASK_PT_KF_SHAPE: [ + LottieTensor.Index.MaskPtKfShape.INDEX, + LottieTensor.Index.MaskPtKfShape.C + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE_I: list(range(21)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_O: list(range(21)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_V: list(range(21)), + LottieTensor.CMD_TR_POSITION: [ + LottieTensor.Index.TrPosition.X, + LottieTensor.Index.TrPosition.Y + ], + LottieTensor.CMD_TR_ANCHOR: [ + LottieTensor.Index.TrAnchor.X, + LottieTensor.Index.TrAnchor.Y + ], + LottieTensor.CMD_TR_ROTATION: [ + LottieTensor.Index.TrRotation.VALUE + ], + LottieTensor.CMD_TR_START_OPACITY: [ + LottieTensor.Index.TrStartOpacity.VALUE + ], + LottieTensor.CMD_TR_END_OPACITY: [ + LottieTensor.Index.TrEndOpacity.VALUE + ], + LottieTensor.CMD_ZIG_ZAG: [ + LottieTensor.Index.ZigZag.IX + ], + LottieTensor.CMD_FREQUENCY: [ + LottieTensor.Index.Frequency.VALUE + ], + LottieTensor.CMD_AMPLITUDE: [ + LottieTensor.Index.Amplitude.VALUE + ], + LottieTensor.CMD_POINT_TYPE: [ + LottieTensor.Index.PointType.VALUE + ], + LottieTensor.CMD_ANIMATORS: [], + LottieTensor.CMD_ANIMATOR: [], + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.T, + LottieTensor.Index.RangeSelector.R, + LottieTensor.Index.RangeSelector.B, + LottieTensor.Index.RangeSelector.SH, + LottieTensor.Index.RangeSelector.RN + ], + LottieTensor.CMD_RANGE_START: [ + LottieTensor.Index.RangeStart.A + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.T, + LottieTensor.Index.RangeStartKeyframe.S, + LottieTensor.Index.RangeStartKeyframe.I_X, + LottieTensor.Index.RangeStartKeyframe.I_Y, + LottieTensor.Index.RangeStartKeyframe.O_X, + LottieTensor.Index.RangeStartKeyframe.O_Y + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.A, + LottieTensor.Index.Amount.K, + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.A, + LottieTensor.Index.MaxEase.K, + LottieTensor.Index.MaxEase.IX + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.A, + LottieTensor.Index.MinEase.K, + LottieTensor.Index.MinEase.IX + ], + LottieTensor.CMD_ANIMATOR_PROPERTIES: [], + LottieTensor.CMD_RADIUS: [ + LottieTensor.Index.Radius.VALUE + ], + LottieTensor.CMD_RANGE_END: [ + LottieTensor.Index.RangeEnd.A + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.T, + LottieTensor.Index.RangeEndKeyframe.S, + LottieTensor.Index.RangeEndKeyframe.I_X, + LottieTensor.Index.RangeEndKeyframe.I_Y, + LottieTensor.Index.RangeEndKeyframe.O_X, + LottieTensor.Index.RangeEndKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.A, + LottieTensor.Index.Amount.K, + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.T, + LottieTensor.Index.RangeOffsetKeyframe.S, + LottieTensor.Index.RangeOffsetKeyframe.I_X, + LottieTensor.Index.RangeOffsetKeyframe.I_Y, + LottieTensor.Index.RangeOffsetKeyframe.O_X, + LottieTensor.Index.RangeOffsetKeyframe.O_Y + ], + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.A, + LottieTensor.Index.SM.K, + LottieTensor.Index.SM.IX + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.A, + LottieTensor.Index.OpacityAnimators.K, + LottieTensor.Index.OpacityAnimators.IX + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.A, + LottieTensor.Index.ScaleAnimators.K_X, + LottieTensor.Index.ScaleAnimators.K_Y, + LottieTensor.Index.ScaleAnimators.K_Z, + LottieTensor.Index.ScaleAnimators.IX + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.A, + LottieTensor.Index.RotationAnimators.K, + LottieTensor.Index.RotationAnimators.IX + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.A, + LottieTensor.Index.PositionAnimators.K_X, + LottieTensor.Index.PositionAnimators.K_Y, + LottieTensor.Index.PositionAnimators.K_Z, + LottieTensor.Index.PositionAnimators.IX + ], + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.A, + LottieTensor.Index.TrackingAnimators.K, + LottieTensor.Index.TrackingAnimators.IX + ], + LottieTensor.CMD_DASHES: [], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.TYPE, + LottieTensor.Index.Dash.LENGTH, + LottieTensor.Index.Dash.V_IX + ], + LottieTensor.CMD_DASH_ANIMATED: [ + LottieTensor.Index.DashAnimated.TYPE, + LottieTensor.Index.DashAnimated.V_IX + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.T, + LottieTensor.Index.DashKeyframe.S, + LottieTensor.Index.DashKeyframe.I_X, + LottieTensor.Index.DashKeyframe.I_Y, + LottieTensor.Index.DashKeyframe.O_X, + LottieTensor.Index.DashKeyframe.O_Y + ], + LottieTensor.CMD_DASH_OFFSET: [ + LottieTensor.Index.DashOffset.O + ], + LottieTensor.CMD_WIDTH_ANIMATED: [], + # All end commands have empty param lists + LottieTensor.CMD_POSITION_END: [], + LottieTensor.CMD_SCALE_END: [], + LottieTensor.CMD_ROTATION_END: [], + LottieTensor.CMD_OPACITY_END: [], + LottieTensor.CMD_ANCHOR_END: [], + LottieTensor.CMD_GROUP_END: [], + LottieTensor.CMD_TRANSFORM_END: [], + LottieTensor.CMD_LAYER_END: [], + LottieTensor.CMD_PATH_END: [], + LottieTensor.CMD_RECT_END: [], + LottieTensor.CMD_ELLIPSE_END: [], + LottieTensor.CMD_STAR_END: [], + LottieTensor.CMD_TRIM_END: [], + LottieTensor.CMD_REPEATER_END: [], + LottieTensor.CMD_REPEATER_TRANSFORM_END: [], + LottieTensor.CMD_GRADIENT_FILL_END: [], + LottieTensor.CMD_GRADIENT_STROKE_END: [], + LottieTensor.CMD_MERGE_END: [], + LottieTensor.CMD_ROUNDED_CORNERS_END: [], + LottieTensor.CMD_TWIST_END: [], + LottieTensor.CMD_BEZIER_END: [], + LottieTensor.CMD_TEXT_LAYER_END: [], + LottieTensor.CMD_TEXT_DATA_END: [], + LottieTensor.CMD_SOLID_LAYER_END: [], + LottieTensor.CMD_NULL_LAYER_END: [], + LottieTensor.CMD_PRECOMP_LAYER_END: [], + LottieTensor.CMD_POSITION_X_END: [], + LottieTensor.CMD_POSITION_Y_END: [], + LottieTensor.CMD_POSITION_Z_END: [], + LottieTensor.CMD_SCALE_X_END: [], + LottieTensor.CMD_SCALE_Y_END: [], + LottieTensor.CMD_SCALE_Z_END: [], + LottieTensor.CMD_ROTATION_X_END: [], + LottieTensor.CMD_ROTATION_Y_END: [], + LottieTensor.CMD_ROTATION_Z_END: [], + LottieTensor.CMD_EFFECTS_END: [], + LottieTensor.CMD_EFFECT_END: [], + LottieTensor.CMD_KEYFRAME_END: [], + LottieTensor.CMD_WIDTH_ANIMATED_END: [], + LottieTensor.CMD_FONTS_END: [], + LottieTensor.CMD_CHARS_END: [], + LottieTensor.CMD_CHAR_END: [], + LottieTensor.CMD_CHAR_SHAPES_END: [], + LottieTensor.CMD_TEXT_KEYFRAMES_END: [], + LottieTensor.CMD_TEXT_DOC_END: [], + LottieTensor.CMD_MORE_OPTIONS_END: [], + LottieTensor.CMD_OPACITY_ANIMATED_END: [], + LottieTensor.CMD_MASKS_PROPERTIES_END: [], + LottieTensor.CMD_MASK_END: [], + LottieTensor.CMD_MASK_PT_END: [], + LottieTensor.CMD_MASK_PT_K_END: [], + LottieTensor.CMD_TM_END: [], + LottieTensor.CMD_MASK_PT_K_ARRAY_END: [], + LottieTensor.CMD_MASK_PT_KEYFRAME_END: [], + LottieTensor.CMD_MASK_PT_KF_S_END: [], + LottieTensor.CMD_MASK_PT_KF_SHAPE_END: [], + LottieTensor.CMD_VALUE_END: [], + LottieTensor.CMD_ZIG_ZAG_END: [], + LottieTensor.CMD_ANIMATORS_END: [], + LottieTensor.CMD_ANIMATOR_END: [], + LottieTensor.CMD_RANGE_SELECTOR_END: [], + LottieTensor.CMD_RANGE_START_END: [], + LottieTensor.CMD_RANGE_END_END: [], + LottieTensor.CMD_END_END: [], + LottieTensor.CMD_START_END: [], + LottieTensor.CMD_OFFSET_END: [], + LottieTensor.CMD_RANGE_OFFSET_END: [], + LottieTensor.CMD_SCALE_ANIMATORS_END: [], + LottieTensor.CMD_ROTATION_ANIMATORS_END: [], + LottieTensor.CMD_POSITION_ANIMATORS_END: [], + LottieTensor.CMD_OPACITY_ANIMATORS_END: [], + LottieTensor.CMD_COLOR_ANIMATED_END: [], + LottieTensor.CMD_DASHES_END: [], + LottieTensor.CMD_DASH_ANIMATED_END: [], + LottieTensor.CMD_SIZE_END: [], + LottieTensor.CMD_RECT_ROUNDED_END: [], + LottieTensor.CMD_ANIMATOR_PROPERTIES_END: [], + LottieTensor.CMD_ASSET_END: [], + LottieTensor.CMD_EFFECTS: [], + LottieTensor.CMD_FONTS: [], + LottieTensor.CMD_CHARS: [], + LottieTensor.CMD_CHAR_SHAPES: [], + LottieTensor.CMD_TEXT_KEYFRAMES: [], + LottieTensor.CMD_TEXT_DATA: [], + LottieTensor.CMD_DOCUMENT: [], + LottieTensor.CMD_TEXT_DOC: [], + } + + # Commands without parameters + empty_param_cmds = {k for k, v in param_orders.items() if not v} + + return param_orders.get(cmd_idx, []) + + + @staticmethod + def get_vocab_range_for_offset(offset: int) -> tuple: + """Get the vocabulary range (start, end) for a given offset.""" + vocab_ranges = { + 0: (1, 151643), # NO_OFFSET (tokenizer tokens) + 155000: (153000, 157000), # TIME_OFFSET: -2000 to 2000 + 159100: (157100, 161100), # SPACE_OFFSET: -2000 to 2000 + 161200: (161200, 161220), # AMPLITUDE_OFFSET: 0 to 20 + 161300: (161300, 165300), # ANCHOR_OFFSET: -2000 to 2000 + 165400: (165400, 165401), # ANIMATED_OFFSET: 0 to 1 + 165402: (165402, 165403), # H_FLAG_OFFSET: 0 to 1 + 165404: (165404, 165405), # OFFSET_VAL_OFFSET: 0 to 1 + 165406: (165406, 165408), # CA_OFFSET: 0 to 2 + 165409: (165409, 165415), # JUSTIFY_OFFSET: 0 to 6 + 165416: (165416, 166016), # TEXT_TRACKING_OFFSET: -100 to 500 + 166017: (166017, 166018), # HAS_STROKE_COLOR_OFFSET: 0 to 1 + 166019: (166019, 167019), # IX_OFFSET: 0 to 1000 + 167020: (167020, 167040), # BM_OFFSET: 0 to 20 + 167041: (167041, 167042), # CLOSED_OFFSET: 0 to 1 + 167043: (167043, 167048), # DIRECTION_OFFSET: 0 to 5 + 167049: (167049, 167054), # STAR_TYPE_OFFSET: 0 to 5 + 167055: (167055, 167060), # MULTIPLE_OFFSET: 0 to 5 + 167061: (167061, 167066), # COMPOSITE_OFFSET: 0 to 5 + 167067: (167067, 167117), # SKEW_OFFSET: -25 to 25 + 167118: (167118, 167168), # SKEW_AXIS_OFFSET: -25 to 25 + 167169: (167169, 170169), # SCALE_OFFSET: -1000 to 2000 + 170170: (170170, 171610), # ROTATION_OFFSET: -720 to 720 + 171611: (171611, 171811), # EASE_OFFSET: -100 to 100 + 171812: (171812, 171912), # SMOOTH_OFFSET: 0 to 100 + 171913: (171913, 172013), # TRACKING_OFFSET: -50 to 50 + 172014: (172014, 173014), # INDEX_OFFSET: 0 to 1000 + 173015: (173015, 173016), # DDD_OFFSET: 0 to 1 + 173017: (173017, 173018), # HD_OFFSET: 0 to 1 + 173019: (173019, 173069), # CP_OFFSET: 0 to 50 + 173070: (173070, 173071), # HAS_MASK_OFFSET: 0 to 1 + 173072: (173072, 173073), # AO_OFFSET: 0 to 1 + 173074: (173074, 173079), # TT_OFFSET: 0 to 5 + 173080: (173080, 173180), # TP_OFFSET: 0 to 100 + 173181: (173181, 173183), # TD_OFFSET: 0 to 2 + 173184: (173184, 173185), # CT_OFFSET: 0 to 1 + 173186: (173186, 173686), # NUMBER_OFFSET: 0 to 500 + 173687: (173687, 173697), # DIM_OFFSET: 0 to 10 + 173698: (173698, 173699), # HAS_C_A_OFFSET: 0 to 1 + 173700: (173700, 173701), # HAS_C_IX_OFFSET: 0 to 1 + 173702: (173702, 173703), # HAS_O_A_OFFSET: 0 to 1 + 173704: (173704, 173705), # HAS_O_IX_OFFSET: 0 to 1 + 173706: (173706, 173710), # FILL_RULE_OFFSET: 0 to 4 + 173711: (173711, 173751), # TYPE_OFFSET: 0 to 40 + 173752: (173752, 173762), # TEXT_RANGE_UNITS_OFFSET: 0 to 10 + 173763: (173763, 173764), # INV_OFFSET: 0 to 1 + 173765: (173765, 173775), # MODE_OFFSET: 0 to 10 + 173776: (173776, 173786), # TEXT_SHAPE_TYPE_OFFSET: 0 to 10 + 173787: (173787, 173788), # TEXT_RANDOM_OFFSET: 0 to 1 + 173789: (173789, 173839), # COLOR_POINTS_OFFSET: 0 to 50 + 173840: (173840, 174940), # ROUND_OFFSET: -100 to 1000 + 174941: (174941, 175241), # RADIUS_OFFSET: 0 to 300 + 175242: (175242, 175392), # FREQUENCY_OFFSET: 0 to 150 + 175393: (175393, 177393), # SPEED_OFFSET: -1000 to 1000 + 177394: (177394, 179494), # FONT_OFFSET: -100 to 2000 + 179495: (179495, 179750), # COLOR_OFFSET: 0 to 255 + 179752: (179752, 179754), # LINE_CAP_OFFSET: 1 to 3 + 179757: (179757, 179759), # LINE_JOIN_OFFSET: 1 to 3 + 179760: (179760, 179860), # MITER_LIMIT_OFFSET: 0 to 100 + 179861: (179861, 181111), # EFFECT_OFFSET: -250 to 1000 + 181112: (181112, 181212), # OPACITY_OFFSET: 0 to 100 + 181300: (181300, 191300), # WIDTH_VALUE_OFFSET: 0 to 10000 (ę–°å¢ž) + } + return vocab_ranges.get(offset, (0, 0)) + + + + @staticmethod + def _find_nearest_layer_end(flattened: List[int], max_length: int, command_offset: int) -> int: + """ + 在flattened listäø­ęŸ„ę‰¾ęœ€ęŽ„čæ‘max_lengthēš„layer endä½ē½® + + ę™ŗčƒ½ęˆŖę–­č§„åˆ™ļ¼š + 1. 从max_lengthä½ē½®å‘å‰ęœē“¢ļ¼Œę‰¾ęœ€čæ‘ēš„layer end + 2. åæ…é”»ē”®äæč‡³å°‘ęœ‰ANIMATIONå‘½ä»¤å’Œäø€äøŖå®Œę•“ēš„layer + 3. layer endåŒ…ę‹¬: LAYER_END, PRECOMP_LAYER_END, TEXT_LAYER_END, NULL_LAYER_END, SOLID_LAYER_END + 4. å¦‚ęžœę‰¾äøåˆ°åˆé€‚ēš„ä½ē½®ļ¼Œčæ”å›ž-1ļ¼ˆč”Øē¤ŗę”¾å¼ƒę ·ęœ¬ļ¼‰ + + Args: + flattened: ę‰å¹³åŒ–ēš„tokenåˆ—č”Ø + max_length: ē›®ę ‡ęœ€å¤§é•æåŗ¦ + command_offset: 命令tokenēš„offset (151936) + + Returns: + ęœ€čæ‘ēš„layer endä½ē½®ļ¼ˆęˆŖę–­åˆ°čæ™é‡Œļ¼‰ļ¼Œå¦‚ęžœę‰¾äøåˆ°čæ”å›ž-1 + """ + # Layer end命令集合 + LAYER_END_CMDS = { + LottieTensor.CMD_LAYER_END, # 27 + LottieTensor.CMD_PRECOMP_LAYER_END, # 46 + LottieTensor.CMD_TEXT_LAYER_END, # 90 + LottieTensor.CMD_NULL_LAYER_END, # 44 + LottieTensor.CMD_SOLID_LAYER_END, # 95 + } + + # å‘å‰ęœē“¢čŒƒå›“ļ¼šä»Žmax_lengthå‘å‰ęœ€å¤šęœē“¢2000äøŖtoken + # 2000äøŖtokenå¤§ēŗ¦čƒ½åŒ…å«1-2äøŖå®Œę•“ēš„layer + search_start = max(0, max_length - 2000) + + best_pos = -1 + + # 从max_lengthä½ē½®å‘å‰ęœē“¢ + for i in range(min(max_length - 1, len(flattened) - 1), search_start - 1, -1): + token = flattened[i] + + # ę£€ęŸ„ę˜Æå¦ę˜Æå‘½ä»¤token + if token >= command_offset and token < command_offset + len(LottieTensor.COMMANDS): + cmd_idx = token - command_offset + + if cmd_idx in LAYER_END_CMDS: + # ę‰¾åˆ°layer endļ¼ŒęˆŖę–­ē‚¹ę˜Æčæ™äøŖtoken之后 + candidate_pos = i + 1 + + # éŖŒčÆęˆŖę–­åŽēš„åŗåˆ—ę˜Æå¦å®Œę•“ļ¼ˆåæ…é”»ęœ‰ANIMATIONå’Œč‡³å°‘äø€äøŖlayer) + if LottieTensor._validate_truncated_sequence(flattened[:candidate_pos], command_offset): + best_pos = candidate_pos + break + + return best_pos + + @staticmethod + def _validate_truncated_sequence(flattened: List[int], command_offset: int) -> bool: + """ + éŖŒčÆęˆŖę–­åŽēš„åŗåˆ—ę˜Æå¦å®Œę•“ęœ‰ę•ˆ + + č¦ę±‚ļ¼š + 1. åæ…é”»ęœ‰ANIMATION命令 + 2. åæ…é”»č‡³å°‘ęœ‰äø€äøŖå®Œę•“ēš„layerļ¼ˆęœ‰layer start和layer endé…åÆ¹ļ¼‰ + 3. layersäøčƒ½äøŗē©ŗ + 4. ć€ę–°å¢žć€‘åæ…é”»ęœ‰äø»layersļ¼ˆäøčƒ½åŖęœ‰assetsäø­ēš„layers) + + Args: + flattened: ęˆŖę–­åŽēš„tokenåˆ—č”Ø + command_offset: 命令tokenēš„offset + + Returns: + ę˜Æå¦ę˜Æęœ‰ę•ˆēš„åŗåˆ— + """ + has_animation = False + layer_count = 0 + asset_depth = 0 # 跟踪是否在assetå†…éƒØ + main_layer_count = 0 # äø»layersč®”ę•°ļ¼ˆäøåœØassetå†…ēš„layer) + + # Layer start和end命令 + LAYER_START_CMDS = { + LottieTensor.CMD_LAYER, # 26 - ShapeLayer + LottieTensor.CMD_PRECOMP_LAYER, # 45 + LottieTensor.CMD_TEXT_LAYER, # 89 + LottieTensor.CMD_NULL_LAYER, # 43 + LottieTensor.CMD_SOLID_LAYER, # 94 + } + + LAYER_END_CMDS = { + LottieTensor.CMD_LAYER_END, + LottieTensor.CMD_PRECOMP_LAYER_END, + LottieTensor.CMD_TEXT_LAYER_END, + LottieTensor.CMD_NULL_LAYER_END, + LottieTensor.CMD_SOLID_LAYER_END, + } + + layer_stack = 0 # 跟踪layerēš„åµŒå„—ę·±åŗ¦ + + for token in flattened: + if token >= command_offset and token < command_offset + len(LottieTensor.COMMANDS): + cmd_idx = token - command_offset + + if cmd_idx == LottieTensor.CMD_ANIMATION: + has_animation = True + elif cmd_idx == LottieTensor.CMD_ASSET: # 进兄asset + asset_depth += 1 + elif cmd_idx == LottieTensor.CMD_ASSET_END: # 离开asset + if asset_depth > 0: + asset_depth -= 1 + elif cmd_idx in LAYER_START_CMDS: + layer_stack += 1 + elif cmd_idx in LAYER_END_CMDS: + if layer_stack > 0: + layer_stack -= 1 + layer_count += 1 # å®Œęˆäø€äøŖå®Œę•“ēš„layer + # å¦‚ęžœäøåœØassetå†…éƒØļ¼Œčæ™ę˜Æäø»layer + if asset_depth == 0: + main_layer_count += 1 + + # éŖŒčÆę”ä»¶ļ¼š + # 1. ꜉ANIMATION命令 + # 2. č‡³å°‘ęœ‰äø€äøŖå®Œę•“ēš„layer(layer_count >= 1) + # 3. ꉀ꜉layeréƒ½å·²ę­£ē”®é—­åˆļ¼ˆlayer_stack == 0) + # 4. ć€ę–°å¢žć€‘č‡³å°‘ęœ‰äø€äøŖäø»layer(main_layer_count >= 1ļ¼‰ļ¼Œé˜²ę­¢åŖęœ‰assetsę²”ęœ‰äø»layers + return has_animation and layer_count >= 1 and layer_stack == 0 and main_layer_count >= 1 + + + + def flatten_to_list(lottie_tensor: 'LottieTensor', max_length: int = None) -> List[int]: + """ + Flatten LottieTensor to a 1D list with proper offsets. + ē“§å‡‘ę ¼å¼ļ¼šäøä½æē”Ø SKIP_TOKENļ¼ŒåŖč¾“å‡ŗęœ‰ę„ä¹‰ēš„å‚ę•°ć€‚ + """ + COMMAND_OFFSET = 151936 + NUMBER_OFFSET = 173186 + NUM_COMMANDS = len(LottieTensor.COMMANDS) + + # Essential parameter counts - params before this index must always be output + ESSENTIAL_PARAM_COUNT = { + LottieTensor.CMD_ANIMATION: 6, + LottieTensor.CMD_LAYER: 4, + LottieTensor.CMD_NULL_LAYER: 4, + LottieTensor.CMD_PRECOMP_LAYER: 4, + LottieTensor.CMD_TEXT_LAYER: 4, + LottieTensor.CMD_SOLID_LAYER: 6, + LottieTensor.CMD_KEYFRAME: 1, + LottieTensor.CMD_WIDTH_KEYFRAME: 2, + LottieTensor.CMD_COLOR_KEYFRAME: 5, + LottieTensor.CMD_OPACITY_KEYFRAME: 2, + LottieTensor.CMD_POINT: 2, + LottieTensor.CMD_FILL: 3, + LottieTensor.CMD_STROKE: 3, + LottieTensor.CMD_TRANSFORM_SHAPE: 8, + LottieTensor.CMD_GROUP: 1, + LottieTensor.CMD_PATH: 4, + LottieTensor.CMD_POSITION: 1, + LottieTensor.CMD_SCALE: 1, + LottieTensor.CMD_ROTATION: 1, + LottieTensor.CMD_OPACITY: 1, + LottieTensor.CMD_ANCHOR: 1, + LottieTensor.CMD_SIZE: 1, + LottieTensor.CMD_RECT: 1, + LottieTensor.CMD_STAR: 2, + LottieTensor.CMD_TRIM: 1, + LottieTensor.CMD_REPEATER: 1, + LottieTensor.CMD_MASK: 3, + LottieTensor.CMD_MASK_PT: 2, + LottieTensor.CMD_MASK_O: 3, + LottieTensor.CMD_MASK_X: 3, + LottieTensor.CMD_RANGE_SELECTOR: 5, + LottieTensor.CMD_RANGE_START: 1, + LottieTensor.CMD_RANGE_END: 1, + LottieTensor.CMD_RANGE_OFFSET: 1, + LottieTensor.CMD_AMOUNT: 3, + LottieTensor.CMD_EFFECT: 2, + LottieTensor.CMD_COLOR: 4, + LottieTensor.CMD_GRADIENT_TYPE: 1, + LottieTensor.CMD_TR_SCALE: 2, + LottieTensor.CMD_TR_POSITION: 2, + LottieTensor.CMD_TR_ANCHOR: 2, + LottieTensor.CMD_ORIGINAL_COLORS: 0, + LottieTensor.CMD_DASH_KEYFRAME: 2, + LottieTensor.CMD_RANGE_START_KEYFRAME: 2, + LottieTensor.CMD_RANGE_END_KEYFRAME: 2, + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: 2, + LottieTensor.CMD_ELLIPSE_SIZE: 1, + LottieTensor.CMD_RECT_SIZE: 1, + } + + # Parameters with default value 0 (not PAD_VAL) + ZERO_DEFAULT_PARAMS = { + LottieTensor.CMD_KEYFRAME: {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 22, 23, 24, 25}, + LottieTensor.CMD_WIDTH_KEYFRAME: {2, 3, 4, 5}, + LottieTensor.CMD_COLOR_KEYFRAME: {5, 6, 7, 8}, + LottieTensor.CMD_OPACITY_KEYFRAME: {2, 3, 4, 5}, + LottieTensor.CMD_DASH_KEYFRAME: {2, 3, 4, 5}, + LottieTensor.CMD_RANGE_START_KEYFRAME: {2, 3, 4, 5}, + LottieTensor.CMD_RANGE_END_KEYFRAME: {2, 3, 4, 5}, + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: {2, 3, 4, 5}, + LottieTensor.CMD_POINT: {2, 3, 4, 5}, + LottieTensor.CMD_ANIMATION: {5}, + LottieTensor.CMD_LAYER: {4, 5, 6, 7, 8, 9, 10, 11, 12}, + LottieTensor.CMD_NULL_LAYER: {4, 5, 6, 7, 8, 9, 10, 11}, + LottieTensor.CMD_PRECOMP_LAYER: {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, + LottieTensor.CMD_FILL: {3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14}, # Removed 8 (FILL_RULE) - should not default to 0 + LottieTensor.CMD_STROKE: {3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, + LottieTensor.CMD_GROUP: {1, 2, 3, 4}, + LottieTensor.CMD_PATH: {4, 5}, + LottieTensor.CMD_TRANSFORM_SHAPE: {8, 9, 10}, + LottieTensor.CMD_RECT: {1}, + LottieTensor.CMD_POSITION: {1, 2, 3}, + LottieTensor.CMD_SCALE: {1, 2, 3}, + LottieTensor.CMD_ROTATION: {1}, + LottieTensor.CMD_OPACITY: {1}, + LottieTensor.CMD_ANCHOR: {1, 2, 3}, + LottieTensor.CMD_MASK_PT_K_I: set(range(21)), + LottieTensor.CMD_MASK_PT_K_O: set(range(21)), + LottieTensor.CMD_MASK_PT_K_V: set(range(21)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_I: set(range(21)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_O: set(range(21)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_V: set(range(21)), + } + + # SIZEē±»å‘½ä»¤é›†åˆ + SIZE_COMMANDS = { + LottieTensor.CMD_SIZE, + LottieTensor.CMD_ELLIPSE_SIZE, + LottieTensor.CMD_RECT_SIZE + } + + TOKENIZER_COMMANDS = { + LottieTensor.CMD_FONT: { + 'regular': [LottieTensor.Index.Font.ASCENT], + 'token_groups': [ + (LottieTensor.Index.Font.FAMILY_TOKEN_COUNT, + LottieTensor.Index.Font.FAMILY_TOKEN_0, 10), + (LottieTensor.Index.Font.STYLE_TOKEN_COUNT, + LottieTensor.Index.Font.STYLE_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_CHAR: { + 'regular': [ + LottieTensor.Index.Char.SIZE, + LottieTensor.Index.Char.W + ], + 'token_groups': [ + (LottieTensor.Index.Char.CH_TOKEN_COUNT, + LottieTensor.Index.Char.CH_TOKEN_0, 10), + (LottieTensor.Index.Char.STYLE_TOKEN_COUNT, + LottieTensor.Index.Char.STYLE_TOKEN_0, 10), + (LottieTensor.Index.Char.FAMILY_TOKEN_COUNT, + LottieTensor.Index.Char.FAMILY_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_ASSET: { + 'regular': [LottieTensor.Index.Asset.FR], + 'token_groups': [ + (LottieTensor.Index.Asset.ID_TOKEN_COUNT, + LottieTensor.Index.Asset.ID_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_REFERENCE_ID: { + 'regular': [], + 'token_groups': [ + (LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT, + LottieTensor.Index.ReferenceId.ID_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_TEXT_KEYFRAME: { + 'regular': list(range(LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START)), + 'token_groups': [ + (LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT, + LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START, 10), + (LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT, + LottieTensor.Index.TextKeyframe.TEXT_TOKENS_START, 15) + ] + } + } + + # ć€äæ®å¤ć€‘ę”¹čæ›ēš„ ANIMATED å€¼åˆ¤ę–­å‡½ę•° - ä½æē”Øę›“äø„ę ¼ēš„é˜ˆå€¼ + def is_animated_value(value): + """åˆ¤ę–­äø€äøŖå€¼ę˜Æå¦č”Øē¤ŗåŠØē”»ēŠ¶ę€""" + if value == LottieTensor.PAD_VAL: + return False + # 使用 0.5 ä½œäøŗé˜ˆå€¼ļ¼Œä»»ä½• >= 0.5 ēš„å€¼éƒ½č§†äøŗ animated + return value >= 0.5 + + def get_param_default(cmd_idx, param_pos, params): + """Get default value for a parameter.""" + + # åÆ¹äŗŽSIZEē±»å‘½ä»¤ēš„ē‰¹ę®Šå¤„ē† + if cmd_idx in SIZE_COMMANDS: + # ę£€ęŸ„ANIMATEDēŠ¶ę€ + animated_val = params[LottieTensor.Index.Transform.ANIMATED] if len(params) > 0 else LottieTensor.PAD_VAL + is_animated = animated_val != LottieTensor.PAD_VAL and is_animated_value(animated_val) + + if param_pos == 0: # ANIMATEDå‚ę•°ęœ¬čŗ« + return LottieTensor.PAD_VAL + elif param_pos in {1, 2}: # X 和 Y å‚ę•° + if is_animated: + # ANIMATED=1ę—¶ļ¼ŒX和Yäøåŗ”čÆ„č¾“å‡ŗļ¼ˆē”±keyframeęä¾›ļ¼‰ + return LottieTensor.PAD_VAL + else: + # ANIMATED=0ę—¶ļ¼ŒX和Y默认为0 + return 0.0 + + zero_set = ZERO_DEFAULT_PARAMS.get(cmd_idx, set()) + if param_pos in zero_set: + return 0.0 + return LottieTensor.PAD_VAL + + def is_meaningful_value(value, default): + """Check if a value is meaningful (not default).""" + if value == LottieTensor.PAD_VAL: + return False + if default == LottieTensor.PAD_VAL: + return True + return abs(value - default) > 1e-6 + + flattened = [] + + SIZE_END_COMMANDS = { + LottieTensor.CMD_SIZE: LottieTensor.CMD_SIZE_END, + LottieTensor.CMD_ELLIPSE_SIZE: LottieTensor.CMD_SIZE_END, + LottieTensor.CMD_RECT_SIZE: LottieTensor.CMD_SIZE_END, + } + # ć€ę–°å¢žć€‘å‰ēž»ę£€ęµ‹ļ¼šč®°å½•ęÆäøŖSIZEå‘½ä»¤åŽé¢ę˜Æå¦ęœ‰keyframe + size_cmd_has_keyframe = {} + for i in range(lottie_tensor.seq_len.item()): + cmd_idx = int(lottie_tensor.commands[i].item()) + if cmd_idx in SIZE_COMMANDS: + # ę£€ęŸ„åŽē»­ę˜Æå¦ęœ‰keyframeļ¼ˆåœØé‡åˆ°ē»“ęŸę ‡č®°ęˆ–å…¶ä»–å½¢ēŠ¶å‘½ä»¤ä¹‹å‰ļ¼‰ + has_kf = False + end_cmd = SIZE_END_COMMANDS.get(cmd_idx, LottieTensor.CMD_SIZE_END) + + # å®šä¹‰ä¼šē»“ęŸ size äøŠäø‹ę–‡ēš„å‘½ä»¤ + context_end_cmds = { + end_cmd, + LottieTensor.CMD_FILL, + LottieTensor.CMD_STROKE, + LottieTensor.CMD_GROUP_END, + LottieTensor.CMD_ELLIPSE_END, + LottieTensor.CMD_RECT_END, + } + + for j in range(i + 1, lottie_tensor.seq_len.item()): + next_cmd = int(lottie_tensor.commands[j].item()) + if next_cmd in context_end_cmds: + break + if next_cmd == LottieTensor.CMD_KEYFRAME: + has_kf = True + break + size_cmd_has_keyframe[i] = has_kf + + + for i in range(lottie_tensor.seq_len.item()): + cmd_idx = int(lottie_tensor.commands[i].item()) + + if cmd_idx in [LottieTensor.CMD_EOS, LottieTensor.CMD_SOS, LottieTensor.CMD_PAD]: + continue + + # Add command token + flattened.append(cmd_idx + COMMAND_OFFSET) + + params = lottie_tensor.params[i].tolist() + + if cmd_idx in TOKENIZER_COMMANDS: + # Handle tokenizer commands specially + cmd_info = TOKENIZER_COMMANDS[cmd_idx] + + # ć€ę–¹ę”ˆC】对TOKENIZER_COMMANDSļ¼Œę€»ę˜Æå†™å…„ę‰€ęœ‰regular paramsļ¼ˆåŒ…ę‹¬PAD_VAL) + # åŽŸå› ļ¼šregular paramsę•°é‡å°‘ļ¼ˆ20äøŖļ¼‰ļ¼Œä½†č·³čæ‡PAD_VALä¼šåÆ¼č‡“unflattenę— ę³•åÆé č§£ē  + # å…¶ä»–å‘½ä»¤ä»ē„¶åŠØę€ęˆŖę–­ä»„čŠ‚ēœtoken + for param_idx in cmd_info['regular']: + if param_idx < len(params): + value = params[param_idx] + offset = LottieTensor.get_param_offset(cmd_idx, param_idx) + flattened.append(int(round(value)) + offset) + + for count_idx, token_start, max_tokens in cmd_info['token_groups']: + actual_count = 0 + for j in range(max_tokens): + if token_start + j < len(params): + token_val = int(params[token_start + j]) + if token_val != LottieTensor.PAD_VAL and token_val > 0: + actual_count = j + 1 + + flattened.append(actual_count + NUMBER_OFFSET) + + for j in range(int(max(0, actual_count))): + if token_start + j < len(params): + token_val = int(params[token_start + j]) + if token_val != LottieTensor.PAD_VAL and token_val > 0: + flattened.append(token_val) + else: + # Handle regular commands + param_indices = LottieTensor.get_command_param_indices(cmd_idx) + + if param_indices: + essential_count = ESSENTIAL_PARAM_COUNT.get(cmd_idx, len(param_indices)) + + # ć€å…³é”®äæ®å¤ć€‘åÆ¹äŗŽSIZEē±»å‘½ä»¤ēš„ē‰¹ę®Šå¤„ē† + if cmd_idx in SIZE_COMMANDS: + animated_val = params[LottieTensor.Index.Transform.ANIMATED] + + # ć€äæ®å¤ć€‘ē»¼åˆåˆ¤ę–­ę˜Æå¦ę˜ÆåŠØē”»ļ¼š + # 1. ANIMATEDå‚ę•°ę˜Žē”®č®¾äøŗ1 + # 2. ęˆ–č€…åŽē»­ęœ‰keyframeå‘½ä»¤ļ¼ˆå‰ēž»ę£€ęµ‹ē»“ęžœļ¼‰ + is_animated_by_param = animated_val != LottieTensor.PAD_VAL and is_animated_value(animated_val) + is_animated_by_keyframe = size_cmd_has_keyframe.get(i, False) + is_animated = is_animated_by_param or is_animated_by_keyframe + + if is_animated: + # åŠØē”»ęØ”å¼ļ¼šåŖč¾“å‡ŗANIMATEDå‚ę•°ļ¼Œå€¼å›ŗå®šäøŗ1 + offset = LottieTensor.get_param_offset(cmd_idx, param_indices[0]) + flattened.append(1 + offset) # å›ŗå®šč¾“å‡ŗę•“ę•°1 + continue # č·³čæ‡åŽē»­å¤„ē† + + # ē»Ÿäø€ä½æē”ØåŠØę€ęˆŖę–­é€»č¾‘ + last_meaningful = -1 + for j, param_idx in enumerate(param_indices): + if param_idx < len(params): + value = params[param_idx] + default = get_param_default(cmd_idx, j, params) + if is_meaningful_value(value, default): + last_meaningful = j + + output_count = max(essential_count, last_meaningful + 1) if last_meaningful >= 0 else essential_count + output_count = min(output_count, len(param_indices)) + + # å†™å…„å‚ę•° + for j in range(output_count): + param_idx = param_indices[j] + if param_idx < len(params): + value = params[param_idx] + + if value == LottieTensor.PAD_VAL: + default = get_param_default(cmd_idx, j, params) + value = default if default != LottieTensor.PAD_VAL else 0.0 + + offset = LottieTensor.get_param_offset(cmd_idx, param_idx) + flattened.append(int(round(value)) + offset) + + # ę™ŗčƒ½ęˆŖę–­é€»č¾‘ + if max_length and len(flattened) > max_length: + # å¦‚ęžœé•æåŗ¦åœØ max_length 和 2*max_length ä¹‹é—“ļ¼Œę™ŗčƒ½ęˆŖę–­åˆ°ęœ€čæ‘ēš„layer end + if len(flattened) <= 3 * max_length: + truncate_point = LottieTensor._find_nearest_layer_end(flattened, max_length, COMMAND_OFFSET) + if truncate_point > 0: + flattened = flattened[:truncate_point] + else: + # āŒ ę‰¾äøåˆ°åˆé€‚ēš„layer endļ¼Œę”¾å¼ƒčæ™äøŖę ·ęœ¬ļ¼ˆčæ”å›žNone标记) + return None + else: + # 超过2å€é•æåŗ¦ļ¼Œä¹Ÿę”¾å¼ƒļ¼ˆå¤Ŗé•æäŗ†ļ¼‰ + return None + + return flattened + + + @staticmethod + def from_list(flattened: List[int]) -> 'LottieTensor': + """ + Reconstruct LottieTensor from a flattened 1D list. + ē“§å‡‘ę ¼å¼č§£ē ļ¼šé€ščæ‡åˆ¤ę–­ę˜Æå¦äøŗå‘½ä»¤Tokenę„åŒŗåˆ†č¾¹ē•Œć€‚ + """ + # ć€ę–°å¢žć€‘ē”®äætokenizeråÆē”ØäŗŽę–‡ęœ¬č§£ē  + if LottieTensor.tokenizer is None: + try: + LottieTensor.init_tokenizer() + except Exception as e: + print(f"Warning: Failed to initialize tokenizer in from_list: {e}") + + COMMAND_OFFSET = 151936 + NUMBER_OFFSET = 173186 + NUM_COMMANDS = len(LottieTensor.COMMANDS) + + # SIZEē±»å‘½ä»¤é›†åˆ + SIZE_COMMANDS = { + LottieTensor.CMD_SIZE, + LottieTensor.CMD_ELLIPSE_SIZE, + LottieTensor.CMD_RECT_SIZE + } + + # ć€äæ®å¤ć€‘ANIMATED é˜ˆå€¼åøøé‡ + ANIMATED_THRESHOLD = 0.5 + + # Default values for parameters when not provided + PARAM_DEFAULTS = {} + + # Keyframe easing defaults + #for i in [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 22, 23, 24, 25]: + for i in [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 22]: + PARAM_DEFAULTS[(LottieTensor.CMD_KEYFRAME, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_WIDTH_KEYFRAME, i)] = 0.0 + + for i in range(5, 9): + PARAM_DEFAULTS[(LottieTensor.CMD_COLOR_KEYFRAME, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_OPACITY_KEYFRAME, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_POINT, i)] = 0.0 + + PARAM_DEFAULTS[(LottieTensor.CMD_ANIMATION, 5)] = 0.0 + + for i in range(4, 13): + PARAM_DEFAULTS[(LottieTensor.CMD_LAYER, i)] = 0.0 + + for i in range(4, 12): + PARAM_DEFAULTS[(LottieTensor.CMD_NULL_LAYER, i)] = 0.0 + + for i in range(4, 15): + PARAM_DEFAULTS[(LottieTensor.CMD_PRECOMP_LAYER, i)] = 0.0 + + for i in range(3, 15): + PARAM_DEFAULTS[(LottieTensor.CMD_FILL, i)] = 0.0 + + for i in range(3, 14): + PARAM_DEFAULTS[(LottieTensor.CMD_STROKE, i)] = 0.0 + + for i in range(1, 5): + PARAM_DEFAULTS[(LottieTensor.CMD_GROUP, i)] = 0.0 + + for i in range(4, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_PATH, i)] = 0.0 + + for i in range(8, 11): + PARAM_DEFAULTS[(LottieTensor.CMD_TRANSFORM_SHAPE, i)] = 0.0 + + for i in range(1, 4): + PARAM_DEFAULTS[(LottieTensor.CMD_POSITION, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_SCALE, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_ANCHOR, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_ROTATION, 1)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_OPACITY, 1)] = 0.0 + + PARAM_DEFAULTS[(LottieTensor.CMD_RECT, 1)] = 0.0 + + for i in range(21): + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_K_I, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_K_O, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_K_V, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_KF_SHAPE_I, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_KF_SHAPE_O, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_KF_SHAPE_V, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_DASH_KEYFRAME, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_RANGE_START_KEYFRAME, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_RANGE_END_KEYFRAME, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_RANGE_OFFSET_KEYFRAME, i)] = 0.0 + + TOKENIZER_COMMANDS = { + LottieTensor.CMD_FONT: { + 'regular': [LottieTensor.Index.Font.ASCENT], + 'token_groups': [ + (LottieTensor.Index.Font.FAMILY_TOKEN_COUNT, + LottieTensor.Index.Font.FAMILY_TOKEN_0, 10), + (LottieTensor.Index.Font.STYLE_TOKEN_COUNT, + LottieTensor.Index.Font.STYLE_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_CHAR: { + 'regular': [ + LottieTensor.Index.Char.SIZE, + LottieTensor.Index.Char.W + ], + 'token_groups': [ + (LottieTensor.Index.Char.CH_TOKEN_COUNT, + LottieTensor.Index.Char.CH_TOKEN_0, 10), + (LottieTensor.Index.Char.STYLE_TOKEN_COUNT, + LottieTensor.Index.Char.STYLE_TOKEN_0, 10), + (LottieTensor.Index.Char.FAMILY_TOKEN_COUNT, + LottieTensor.Index.Char.FAMILY_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_ASSET: { + 'regular': [LottieTensor.Index.Asset.FR], + 'token_groups': [ + (LottieTensor.Index.Asset.ID_TOKEN_COUNT, + LottieTensor.Index.Asset.ID_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_REFERENCE_ID: { + 'regular': [], + 'token_groups': [ + (LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT, + LottieTensor.Index.ReferenceId.ID_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_TEXT_KEYFRAME: { + 'regular': list(range(LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START)), + 'token_groups': [ + (LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT, + LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START, 10), + (LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT, + LottieTensor.Index.TextKeyframe.TEXT_TOKENS_START, 15) + ] + } + } + + def is_command_token(token): + """Check if a token is a command token.""" + return COMMAND_OFFSET <= token < COMMAND_OFFSET + NUM_COMMANDS + + def get_default_value(cmd_idx, param_pos, params): + """Get default value for a parameter.""" + + # SIZEē±»å‘½ä»¤ēš„ē‰¹ę®Šå¤„ē† + if cmd_idx in SIZE_COMMANDS: + animated_val = params[LottieTensor.Index.Transform.ANIMATED] + is_animated = animated_val != LottieTensor.PAD_VAL and animated_val >= ANIMATED_THRESHOLD + + if param_pos in {1, 2}: # X 和 Y å‚ę•° + if is_animated: + # ANIMATED=1ę—¶ļ¼ŒX和Yåŗ”čÆ„äæęŒPAD_VALļ¼ˆē”±keyframeęä¾›ļ¼‰ + return LottieTensor.PAD_VAL + else: + # ANIMATED=0ę—¶ļ¼ŒX和Y默认为0 + return 0.0 + + key = (cmd_idx, param_pos) + return PARAM_DEFAULTS.get(key, LottieTensor.PAD_VAL) + + # ć€ę–°å¢žć€‘č¾…åŠ©å‡½ę•°ļ¼šę£€ęŸ„åŽē»­ę˜Æå¦ęœ‰keyframeå‘½ä»¤ļ¼ˆåœØé‡åˆ°åÆ¹åŗ”ēš„ENDå‘½ä»¤ä¹‹å‰ļ¼‰ + def has_following_keyframe(flattened_list, start_idx, cmd_idx): + """ę£€ęŸ„ä»Žstart_idxå¼€å§‹ļ¼Œę˜Æå¦ęœ‰keyframeå‘½ä»¤å‡ŗēŽ°åœØē»“ęŸę ‡č®°ä¹‹å‰""" + # SIZEå‘½ä»¤ēš„ē»“ęŸę ‡č®° + SIZE_END_COMMANDS = { + LottieTensor.CMD_SIZE: LottieTensor.CMD_SIZE_END, + LottieTensor.CMD_ELLIPSE_SIZE: LottieTensor.CMD_SIZE_END, + LottieTensor.CMD_RECT_SIZE: LottieTensor.CMD_SIZE_END, + } + + end_cmd = SIZE_END_COMMANDS.get(cmd_idx, LottieTensor.CMD_SIZE_END) + + # ä¼šē»“ęŸ size äøŠäø‹ę–‡ēš„å‘½ä»¤é›†åˆ + context_end_cmds = { + end_cmd, + LottieTensor.CMD_FILL, + LottieTensor.CMD_STROKE, + LottieTensor.CMD_GROUP_END, + LottieTensor.CMD_ELLIPSE_END, + LottieTensor.CMD_RECT_END, + } + + for k in range(start_idx, len(flattened_list)): + if is_command_token(flattened_list[k]): + cmd = flattened_list[k] - COMMAND_OFFSET + if cmd in context_end_cmds: + return False + if cmd == LottieTensor.CMD_KEYFRAME: + return True + return False + + commands = [] + params_list = [] + + i = 0 + while i < len(flattened): + if is_command_token(flattened[i]): + cmd_idx = flattened[i] - COMMAND_OFFSET + commands.append(cmd_idx) + cmd_start_i = i # č®°å½•å‘½ä»¤ēš„čµ·å§‹ä½ē½® + i += 1 + + params = [LottieTensor.PAD_VAL] * LottieTensor.PARAM_DIM + + if cmd_idx in TOKENIZER_COMMANDS: + cmd_info = TOKENIZER_COMMANDS[cmd_idx] + regular_params = cmd_info['regular'] + + # ć€ę–¹ę”ˆCć€‘čÆ»å–ę‰€ęœ‰regular params - å›ŗå®šé•æåŗ¦ļ¼ŒęÆäøŖéƒ½čÆ»å– + for param_idx in regular_params: + if i < len(flattened) and not is_command_token(flattened[i]): + offset = LottieTensor.get_param_offset(cmd_idx, param_idx) + params[param_idx] = float(flattened[i] - offset) + i += 1 + + for count_idx, token_start, max_tokens in cmd_info['token_groups']: + if i < len(flattened) and not is_command_token(flattened[i]): + count = flattened[i] - NUMBER_OFFSET + params[count_idx] = float(count) + i += 1 + + for j in range(int(max(0, count))): + if i < len(flattened) and not is_command_token(flattened[i]): + if token_start + j < LottieTensor.PARAM_DIM: + params[token_start + j] = float(flattened[i]) + i += 1 + else: + break + else: + param_indices = LottieTensor.get_command_param_indices(cmd_idx) + + # Read parameters until next command + param_pos = 0 + while (param_pos < len(param_indices) and + i < len(flattened) and + not is_command_token(flattened[i])): + + param_idx = param_indices[param_pos] + offset = LottieTensor.get_param_offset(cmd_idx, param_idx) + params[param_idx] = float(flattened[i] - offset) + param_pos += 1 + i += 1 + + # ć€å…³é”®äæ®å¤ć€‘åÆ¹äŗŽSIZEē±»å‘½ä»¤ēš„ē‰¹ę®ŠåŽå¤„ē† + if cmd_idx in SIZE_COMMANDS: + animated_val = params[LottieTensor.Index.Transform.ANIMATED] + + # ęƒ…å†µ1ļ¼šåŖčÆ»åˆ°äŗ†ANIMATEDå‚ę•°äø”å€¼äøŗ1 + if param_pos == 1 and animated_val != LottieTensor.PAD_VAL and animated_val >= ANIMATED_THRESHOLD: + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + + # ęƒ…å†µ2ļ¼šę²”ęœ‰čÆ»åˆ°ęœ‰ę•ˆēš„ANIMATEDå‚ę•°ļ¼Œé€ščæ‡å‰ēž»ę£€ęµ‹åˆ¤ę–­ + elif animated_val == LottieTensor.PAD_VAL or animated_val < ANIMATED_THRESHOLD: + # ä½æē”Øäæ®ę”¹åŽēš„å‡½ę•°ļ¼Œä¼ å…„å‘½ä»¤ē±»åž‹ + if has_following_keyframe(flattened, i, cmd_idx): + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + elif param_pos >= 2: + # čÆ»åˆ°äŗ†å¤šäøŖå‚ę•°ļ¼Œę˜Æé™ę€ size + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + + # ęƒ…å†µ3ļ¼ščÆ»åˆ°äŗ†å¤šäøŖå‚ę•°ļ¼ˆé™ę€sizeļ¼‰ļ¼Œē”®äæANIMATED=0 + elif param_pos >= 2 and (animated_val == LottieTensor.PAD_VAL or animated_val < ANIMATED_THRESHOLD): + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + + # Fill in defaults for remaining parameters + for j in range(param_pos, len(param_indices)): + param_idx = param_indices[j] + + # čŽ·å–é»˜č®¤å€¼ļ¼Œéœ€č¦ä¼ å…„å½“å‰paramsę„åˆ¤ę–­ANIMATEDēŠ¶ę€ + default_val = get_default_value(cmd_idx, j, params) + + if default_val != LottieTensor.PAD_VAL: + params[param_idx] = default_val + + params_list.append(params) + else: + i += 1 + + if commands: + commands_tensor = torch.tensor(commands).reshape(-1, 1).long() + params_tensor = torch.tensor(params_list).float() + else: + commands_tensor = torch.zeros((0, 1)).long() + params_tensor = torch.zeros((0, LottieTensor.PARAM_DIM)).float() + + return LottieTensor(commands_tensor, params_tensor) + + diff --git a/lottie/objects/easing.py b/lottie/objects/easing.py new file mode 100644 index 0000000..6b579aa --- /dev/null +++ b/lottie/objects/easing.py @@ -0,0 +1,114 @@ +import math +from .base import LottieObject, LottieProp, PseudoList, PseudoBool + + +## @ingroup Lottie +class KeyframeBezierHandle(LottieObject): + """! + Bezier handle for keyframe interpolation + """ + _props = [ + LottieProp("x", "x", list=PseudoList), + LottieProp("y", "y", list=PseudoList), + ] + + def __init__(self, x=0, y=0): + ## x position of the handle. + ## This represents the change in time of the keyframe + self.x = x + ## y position of the handle. + ## This represents the change in value of the keyframe + self.y = y + + +class Linear: + """! + Linear easing, the value will change from start to end in a straight line + """ + def __call__(self, keyframe): + keyframe.out_value = KeyframeBezierHandle( + 0, + 0 + ) + keyframe.in_value = KeyframeBezierHandle( + 1, + 1 + ) + + +class EaseIn: + """! + The value lingers near the start before accelerating towards the end + """ + def __init__(self, delay=1/3): + self.delay = delay + + def __call__(self, keyframe): + keyframe.out_value = KeyframeBezierHandle( + self.delay, + 0 + ) + keyframe.in_value = KeyframeBezierHandle( + 1, + 1 + ) + + +class EaseOut: + """! + The value starts fast before decelerating towards the end + """ + def __init__(self, delay=1/3): + self.delay = delay + + def __call__(self, keyframe): + keyframe.out_value = KeyframeBezierHandle( + 0, + 0 + ) + keyframe.in_value = KeyframeBezierHandle( + 1-self.delay, + 1 + ) + + +class Jump: + """! + Jumps to the end value at the end of the keyframe + """ + def __call__(self, keyframe): + keyframe.jump = True + + +class Sigmoid: + """! + Combines the effects of EaseIn and EaseOut + """ + def __init__(self, delay=1/3): + self.delay = delay + + def __call__(self, keyframe): + keyframe.out_value = KeyframeBezierHandle( + self.delay, + 0 + ) + keyframe.in_value = KeyframeBezierHandle( + 1 - self.delay, + 1 + ) + + +class Split: + """ + Uses different easing methods for in/out + """ + + def __init__(self, out_ease, in_ease): + self.out_ease = out_ease + self.in_ease = in_ease + + def __call__(self, keyframe): + self.out_ease(keyframe) + t = keyframe.out_value + self.in_ease(keyframe) + keyframe.out_value = t diff --git a/lottie/objects/effects.py b/lottie/objects/effects.py new file mode 100644 index 0000000..d617549 --- /dev/null +++ b/lottie/objects/effects.py @@ -0,0 +1,441 @@ +from .base import LottieObject, LottieProp, PseudoBool +from .properties import Value, MultiDimensional, ColorValue +from .nvector import NVector +from .color import Color + + +#5: EffectsManager, +#11: MaskEffect, +class EffectValue(LottieObject): + """! + Value for an effect + """ + ## %Effect value type. + type = None + _classses = {} + + _props = [ + LottieProp("effect_index", "ix", int, False), + #LottieProp("match_name", "mn", str, False), + LottieProp("name", "nm", str, False), + LottieProp("type", "ty", int, False), + ] + + def __init__(self): + ## Effect Index. Used for expressions. + self.effect_index = None + ## After Effect's Name. Used for expressions. + self.name = None + + """ + ## After Effect's Match Name. Used for expressions. + self.match_name = "" + """ + + @classmethod + def _load_get_class(cls, lottiedict): + if not EffectValue._classses: + EffectValue._classses = { + sc.type: sc + for sc in EffectValue.__subclasses__() + } + return EffectValue._classses[lottiedict["ty"]] + + def __str__(self): + return self.name or super().__str__() + + +## @ingroup Lottie +class Effect(LottieObject): + """! + Layer effect + """ + ## %Effect type. + type = None + _classses = {} + + _props = [ + LottieProp("effect_index", "ix", int, False), + #LottieProp("match_name", "mn", str, False), + LottieProp("name", "nm", str, False), + LottieProp("type", "ty", int, False), + LottieProp("effects", "ef", EffectValue, True), + ] + _effects = [] + + def __init__(self, *args, **kwargs): + ## Effect Index. Used for expressions. + self.effect_index = None + ## After Effect's Name. Used for expressions. + self.name = None + ## Effect parameters + self.effects = self._load_values(*args, **kwargs) + + """ + ## After Effect's Match Name. Used for expressions. + self.match_name = "" + """ + + @classmethod + def _load_get_class(cls, lottiedict): + if not Effect._classses: + Effect._classses = { + sc.type: sc + for sc in Effect.__subclasses__() + } + type = lottiedict["ty"] + + if type in Effect._classses: + return Effect._classses[type] + else: + return Effect + + def _load_values(self, *args, **kwargs): + values = [] + for i, (name, type) in enumerate(self._effects): + val = [] + if len(args) > i: + val = [args[i]] + if name in kwargs: + val = [kwargs[name]] + values.append(type(*val)) + return values + + def __getattr__(self, key): + for i, (name, type) in enumerate(self._effects): + if name == key: + return self.effects[i].value + return super().__getattr__(key) + + def __str__(self): + return self.name or super().__str__() + + +## @ingroup Lottie +## @ingroup LottieCheck +class EffectNoValue(EffectValue): + _props = [] + + +## @ingroup Lottie +class EffectValueSlider(EffectValue): + _props = [ + LottieProp("value", "v", Value, False), + ] + ## %Effect type. + type = 0 + + def __init__(self, value=0): + EffectValue.__init__(self) + ## Effect value. + self.value = Value(value) + + +## @ingroup Lottie +class EffectValueAngle(EffectValue): + _props = [ + LottieProp("value", "v", Value, False), + ] + ## %Effect type. + type = 1 + + def __init__(self, angle=0): + EffectValue.__init__(self) + ## Effect value. + self.value = Value(angle) + + +## @ingroup Lottie +class EffectValueColor(EffectValue): + _props = [ + LottieProp("value", "v", ColorValue, False), + ] + ## %Effect type. + type = 2 + + def __init__(self, value=Color(0, 0, 0)): + EffectValue.__init__(self) + ## Effect value. + self.value = ColorValue(value) + + +## @ingroup Lottie +class EffectValuePoint(EffectValue): + _props = [ + LottieProp("value", "v", MultiDimensional, False), + ] + ## %Effect type. + type = 3 + + def __init__(self, value=NVector(0, 0)): + EffectValue.__init__(self) + ## Effect value. + self.value = MultiDimensional(value) + + +## @ingroup Lottie +class EffectValueCheckbox(EffectValue): + _props = [ + LottieProp("value", "v", Value, False), + ] + ## %Effect type. + type = 4 + + def __init__(self, value=0): + EffectValue.__init__(self) + ## Effect value. + self.value = Value(value) + + +## @ingroup Lottie +## @ingroup LottieCheck +## Lottie-web ignores these +class IgnoredValue(EffectValue): + _props = [ + LottieProp("value", "v", float, False), + ] + ## %Effect type. + type = 6 + + def __init__(self, value=0): + EffectValue.__init__(self) + ## Effect value. + self.value = value + + +## @ingroup Lottie +## @ingroup LottieCheck +class EffectValueDropDown(EffectValue): + _props = [ + LottieProp("value", "v", Value, False), + ] + ## %Effect type. + type = 7 + + def __init__(self, value=0): + EffectValue.__init__(self) + ## Effect value. + self.value = Value(value) + + +## @ingroup Lottie +## @ingroup LottieCheck +class EffectValueLayer(EffectValue): + _props = [ + LottieProp("value", "v", Value, False), + ] + ## %Effect type. + type = 10 + + def __init__(self): + EffectValue.__init__(self) + ## Effect value. + self.value = Value() + + +## @ingroup Lottie +class FillEffect(Effect): + """! + Replaces the whole layer with the given color + @note Opacity is in [0, 1] + """ + _effects = [ + ("00", EffectValuePoint), + ("01", EffectValueDropDown), + ("color", EffectValueColor), + ("03", EffectValueDropDown), + ("04", EffectValueSlider), + ("05", EffectValueSlider), + ("opacity", EffectValueSlider), + ] + ## %Effect type. + type = 21 + + +## @ingroup Lottie +class StrokeEffect(Effect): + _effects = [ + ("00", EffectValueColor), + ("01", EffectValueCheckbox), + ("02", EffectValueCheckbox), + ("color", EffectValueColor), + ("04", EffectValueSlider), + ("05", EffectValueSlider), + ("06", EffectValueSlider), + ("07", EffectValueSlider), + ("08", EffectValueSlider), + ("09", EffectValueDropDown), + ("type", EffectValueDropDown), + ] + ## %Effect type. + type = 22 + + +## @ingroup Lottie +class TritoneEffect(Effect): + """! + Maps layers colors based on bright/mid/dark colors + """ + _effects = [ + ("bright", EffectValueColor), + ("mid", EffectValueColor), + ("dark", EffectValueColor), + ] + ## %Effect type. + type = 23 + + +""" +## @ingroup Lottie +## @ingroup LottieCheck +class GroupEffect(Effect): + _props = [ + LottieProp("enabled", "en", PseudoBool, False), + ] + + def __init__(self): + Effect.__init__(self) + ## Enabled AE property value + self.enabled = True +""" + + +## @ingroup Lottie +## @ingroup LottieCheck +class ProLevelsEffect(Effect): + _effects = [ + ("00", EffectValueDropDown), + ("01", EffectNoValue), + ("02", EffectNoValue), + ("comp_inblack", EffectValueSlider), + ("comp_inwhite", EffectValueSlider), + ("comp_gamma", EffectValueSlider), + ("comp_outblack", EffectValueSlider), + ("comp_outwhite", EffectNoValue), + ("08", EffectNoValue), + ("09", EffectValueSlider), + ("r_inblack", EffectValueSlider), + ("r_inwhite", EffectValueSlider), + ("r_gamma", EffectValueSlider), + ("r_outblack", EffectValueSlider), + ("r_outwhite", EffectNoValue), + ("15", EffectValueSlider), + ("16", EffectValueSlider), + ("g_inblack", EffectValueSlider), + ("g_inwhite", EffectValueSlider), + ("g_gamma", EffectValueSlider), + ("g_outblack", EffectValueSlider), + ("g_outwhite", EffectNoValue), + ("22", EffectValueSlider), + ("b3", EffectValueSlider), + ("b_inblack", EffectValueSlider), + ("b_inwhite", EffectValueSlider), + ("b_gamma", EffectValueSlider), + ("b_outblack", EffectValueSlider), + ("b_outwhite", EffectNoValue), + ("29", EffectValueSlider), + ("a_inblack", EffectValueSlider), + ("a_inwhite", EffectValueSlider), + ("a_gamma", EffectValueSlider), + ("a_outblack", EffectValueSlider), + ("a_outwhite", EffectNoValue), + ] + ## %Effect type. + type = 24 + + +## @ingroup Lottie +class TintEffect(Effect): + """! + Colorizes the layer + @note Opacity is in [0, 100] + """ + _effects = [ + ("color_black", EffectValueColor), + ("color_white", EffectValueColor), + ("opacity", EffectValueSlider), + ] + ## %Effect type. + type = 20 + + +## @ingroup Lottie +class DropShadowEffect(Effect): + """! + Adds a shadow to the layer + @note Opacity is in [0, 255] + """ + _effects = [ + ("color", EffectValueColor), + ("opacity", EffectValueSlider), + ("angle", EffectValueAngle), + ("distance", EffectValueSlider), + ("blur", EffectValueSlider), + ] + ## %Effect type. + type = 25 + + +## @ingroup Lottie +## @ingroup LottieCheck +class Matte3Effect(Effect): + _effects = [ + ("index", EffectValueSlider), + ] + ## %Effect type. + type = 28 + + +## @ingroup Lottie +class GaussianBlurEffect(Effect): + """! + Gaussian blur + """ + _effects = [ + ("sigma", EffectValueSlider), + ("dimensions", EffectValueSlider), + ("wrap", EffectValueCheckbox), + ] + ## %Effect type. + type = 29 + + +#class ChangeColorEffect(Effect): + #"""! + #Gaussian blur + #""" + #_effects = [ + #("view", EffectValueDropDown), + #("hue", EffectValueSlider), + #("lightness", EffectValueSlider), + #("saturation", EffectValueSlider), + #("color_to_change", EffectValueColor), + #("tolerance", EffectValueSlider), + #("softness", EffectValueSlider), + #("match", EffectValueDropDown), + #("invert_mask", EffectValueDropDown), + #] + ### %Effect type. + #type = 29 + + +## @ingroup Lottie +class ChangeToColorEffect(Effect): + """! + Change to color + """ + _effects = [ + ("from_color", EffectValueColor), + ("to_color", EffectValueColor), + ("change", EffectValueDropDown), + ("change_by", EffectValueDropDown), + ("tolerance", IgnoredValue), + ("hue", EffectValueSlider), + ("lightness", EffectValueSlider), + ("saturation", EffectValueSlider), + ("saturation_", IgnoredValue), + ("softness", EffectValueSlider), + ("view_correction", EffectValueDropDown), + ] + ## %Effect type. + type = 5 diff --git a/lottie/objects/enums.py b/lottie/objects/enums.py new file mode 100644 index 0000000..c083d8e --- /dev/null +++ b/lottie/objects/enums.py @@ -0,0 +1,39 @@ +from .base import LottieEnum + + +## @ingroup Lottie +class TestBased(LottieEnum): + Characters = 1 + CharacterExcludingSpaces = 2 + Words = 3 + Lines = 4 + + @classmethod + def default(cls): + return cls.Characters + + +## @ingroup Lottie +class TextShape(LottieEnum): + Square = 1 + RampUp = 2 + RampDown = 3 + Triangle = 4 + Round = 5 + Smooth = 6 + + @classmethod + def default(cls): + return cls.Square + + +## @ingroup Lottie +class TextGrouping(LottieEnum): + Characters = 1 + Word = 2 + Line = 3 + All = 4 + + @classmethod + def default(cls): + return cls.Characters diff --git a/lottie/objects/helpers.py b/lottie/objects/helpers.py new file mode 100644 index 0000000..f9915dc --- /dev/null +++ b/lottie/objects/helpers.py @@ -0,0 +1,125 @@ +import math +from .base import LottieObject, LottieProp, LottieEnum +from .properties import MultiDimensional, Value, NVector, ShapeProperty, PositionValue + + +## @ingroup Lottie +class Transform(LottieObject): + """! + Layer transform + """ + _props = [ + LottieProp("anchor_point", "a", MultiDimensional, False), + LottieProp("position", "p", PositionValue, False), + LottieProp("scale", "s", MultiDimensional, False), + LottieProp("rotation", "r", Value, False), + LottieProp("opacity", "o", Value, False), + #LottieProp("position_x", "px", Value, False), + #LottieProp("position_y", "py", Value, False), + #LottieProp("position_z", "pz", Value, False), + LottieProp("skew", "sk", Value, False), + LottieProp("skew_axis", "sa", Value, False), + ] + + def __init__(self): + ## Transform Anchor Point + self.anchor_point = MultiDimensional(NVector(0, 0)) + ## Transform Position + self.position = PositionValue(NVector(0, 0)) + ## Transform Scale + self.scale = MultiDimensional(NVector(100, 100)) + ## Transform Rotation + self.rotation = Value(0) + ## Transform Opacity + self.opacity = Value(100) + + """ + # Transform Position X + #self.position_x = Value() + ## Transform Position Y + #self.position_y = Value() + ## Transform Position Z + #self.position_z = Value() + """ + + ## Transform Skew + self.skew = Value(0) + ## Transform Skew Axis. + ## An angle, if 0 skews on the X axis, if 90 skews on the Y axis + self.skew_axis = Value(0) + + def to_matrix(self, time, auto_orient=False): + from ..utils.transform import TransformMatrix + mat = TransformMatrix() + + anchor = self.anchor_point.get_value(time) if self.anchor_point else NVector(0, 0) + mat.translate(-anchor.x, -anchor.y) + + scale = self.scale.get_value(time) if self.scale else NVector(100, 100) + mat.scale(scale.x / 100, scale.y / 100) + + skew = (self.skew.get_value(time) * math.pi / 180) if self.skew else 0 + if skew != 0: + axis = (self.skew_axis.get_value(time) * math.pi / 180) if self.skew_axis else 0 + mat.skew_from_axis(-skew, axis) + + rot = (self.rotation.get_value(time) * math.pi / 180) if self.rotation else 0 + if rot: + mat.rotate(-rot) + + if auto_orient: + if self.position and self.position.animated: + ao_angle = self.position.get_tangent_angle(time) + mat.rotate(-ao_angle) + + pos = self.position.get_value(time) if self.position else NVector(0, 0) + mat.translate(pos.x, pos.y) + + return mat + + +## @ingroup Lottie +class MaskMode(LottieEnum): + """! + How masks interact with each other + @see https://helpx.adobe.com/after-effects/using/alpha-channels-masks-mattes.html + """ + No = "n" + Add = "a" + Subtract = "s" + Intersect = "i" + ## @note Not in lottie web + Lightent = "l" + ## @note Not in lottie web + Darken = "d" + ## @note Not in lottie web + Difference = "f" + + +## @ingroup Lottie +## @todo Implement SVG/SIF I/O +class Mask(LottieObject): + _props = [ + LottieProp("inverted", "inv", bool, False), + LottieProp("name", "nm", str, False), + LottieProp("shape", "pt", ShapeProperty, False), + LottieProp("opacity", "o", Value, False), + LottieProp("mode", "mode", MaskMode, False), + LottieProp("dilate", "x", Value, False), + ] + + def __init__(self, bezier=None): + ## Inverted Mask flag + self.inverted = False + ## Mask name. Used for expressions and effects. + self.name = None + ## Mask vertices + self.shape = ShapeProperty(bezier) + ## Mask opacity. + self.opacity = Value(100) + ## Mask mode. Not all mask types are supported. + self.mode = MaskMode.Intersect + self.dilate = Value(0) + + def __str__(self): + return self.name or super().__str__() diff --git a/lottie/objects/layers.py b/lottie/objects/layers.py new file mode 100644 index 0000000..b6db9d1 --- /dev/null +++ b/lottie/objects/layers.py @@ -0,0 +1,294 @@ +import warnings +from .base import LottieObject, LottieProp, PseudoBool, LottieEnum +from .effects import Effect +from .helpers import Transform, Mask +from .shapes import ShapeElement +from .text import TextAnimatorData +from .properties import Value + + +## @ingroup Lottie +class BlendMode(LottieEnum): + Normal = 0 + Multiply = 1 + Screen = 2 + Overlay = 3 + Darken = 4 + Lighten = 5 + ColorDodge = 6 + ColorBurn = 7 + HardLight = 8 + SoftLight = 9 + Difference = 10 + Exclusion = 11 + Hue = 12 + Saturation = 13 + Color = 14 + Luminosity = 15 + + +## @ingroup Lottie +## @todo SVG masks +class MatteMode(LottieEnum): + Normal = 0 + Alpha = 1 + InvertedAlpha = 2 + Luma = 3 + InvertedLuma = 4 + + +## @ingroup Lottie +class Layer(LottieObject): + _props = [ + LottieProp("threedimensional", "ddd", PseudoBool, False), + LottieProp("hidden", "hd", bool, False), + LottieProp("type", "ty", int, False), + LottieProp("name", "nm", str, False), + LottieProp("parent_index", "parent", int, False), + + LottieProp("stretch", "sr", float, False), + LottieProp("transform", "ks", Transform, False), + LottieProp("auto_orient", "ao", PseudoBool, False), + + LottieProp("in_point", "ip", float, False), + LottieProp("out_point", "op", float, False), + LottieProp("start_time", "st", float, False), + LottieProp("blend_mode", "bm", BlendMode, False), + + LottieProp("matte_mode", "tt", MatteMode, False), + LottieProp("index", "ind", int, False), + #LottieProp("css_class", "cl", str, False), + LottieProp("layer_html_id", "ln", str, False), + LottieProp("has_masks", "hasMask", bool, False), + LottieProp("masks", "masksProperties", Mask, True), + LottieProp("effects", "ef", Effect, True), + LottieProp("matte_target", "td", int, False), + ] + ## %Layer type. + ## @see https://github.com/bodymovin/bodymovin-extension/blob/master/bundle/jsx/enums/layerTypes.jsx + type = None + _classses = {} + + @property + def has_masks(self): + """! + Whether the layer has some masks applied + """ + return bool(self.masks) if getattr(self, "masks") is not None else None + + def __init__(self): + ## Transform properties + self.transform = Transform() + ## Auto-Orient along path AE property. + self.auto_orient = False + ## 3d layer flag + self.threedimensional = False + ## Hidden layer + self.hidden = None + ## Layer index in AE. Used for parenting and expressions. + self.index = None + + """ + # Parsed layer name used as html class on SVG/HTML renderer + #self.css_class = "" + # Parsed layer name used as html id on SVG/HTML renderer + #self.layer_html_id = "" + """ + ## In Point of layer. Sets the initial frame of the layer. + self.in_point = None + ## Out Point of layer. Sets the final frame of the layer. + self.out_point = None + ## Start Time of layer. Sets the start time of the layer. + self.start_time = 0 + ## After Effects Layer Name. Used for expressions. + self.name = None + ## List of Effects + self.effects = None + ## Layer Time Stretching + self.stretch = 1 + ## Layer Parent. Uses ind of parent. + self.parent_index = None + ## List of Masks + self.masks = None + ## Blend Mode + self.blend_mode = BlendMode.Normal + ## Matte mode, the layer will inherit the transparency from the layer above + self.matte_mode = None + self.matte_target = None + ## Composition owning the layer, set by add_layer + self.composition = None + + def add_child(self, layer): + if not self.composition or self.index is None: + raise Exception("Must set composition / index first") + self._child_inout_auto(layer) + self.composition.add_layer(layer) + layer.parent_index = self.index + return layer + + def _child_inout_auto(self, layer): + if layer.in_point is None: + layer.in_point = self.in_point + if layer.out_point is None: + layer.out_point = self.out_point + + @property + def parent(self): + if self.parent_index is None: + return None + return self.composition.layer(self.parent_index) + + @parent.setter + def parent(self, layer): + if layer is None: + self.parent_index = None + else: + self.parent_index = layer.index + layer._child_inout_auto(self) + + @property + def children(self): + for layer in self.composition.layers: + if layer.parent_index == self.index: + yield layer + + @classmethod + def _load_get_class(cls, lottiedict): + if not Layer._classses: + Layer._classses = { + sc.type: sc + for sc in Layer.__subclasses__() + } + type_id = lottiedict["ty"] + if type_id not in Layer._classses: + warnings.warn("Unknown layer type: %s" % type_id) + return Layer + return Layer._classses[type_id] + + def __repr__(self): + return "<%s %s %s>" % (type(self).__name__, self.index, self.name) + + def __str__(self): + return "%s %s" % ( + self.name or super().__str__(), + self.index if self.index is not None else "" + ) + + def remove(self): + """! + @brief Removes this layer from the componsitin + """ + self.composition.remove_layer(self) + + +## @ingroup Lottie +class NullLayer(Layer): + """! + Layer with no data, useful to group layers together + """ + ## %Layer type. + type = 3 + + def __init__(self): + Layer.__init__(self) + + +## @ingroup Lottie +class TextLayer(Layer): + _props = [ + LottieProp("data", "t", TextAnimatorData, False), + ] + ## %Layer type. + type = 5 + + def __init__(self): + Layer.__init__(self) + ## Text Data + self.data = TextAnimatorData() + + +## @ingroup Lottie +class ShapeLayer(Layer): + """! + Layer containing ShapeElement objects + """ + _props = [ + LottieProp("shapes", "shapes", ShapeElement, True), + ] + ## %Layer type. + type = 4 + + def __init__(self): + Layer.__init__(self) + ## Shape list of items + self.shapes = [] # ShapeElement + + def add_shape(self, shape): + self.shapes.append(shape) + return shape + + def insert_shape(self, index, shape): + self.shapes.insert(index, shape) + return shape + + +## @ingroup Lottie +## @todo SIF I/O +class ImageLayer(Layer): + _props = [ + LottieProp("image_id", "refId", str, False), + ] + ## %Layer type. + type = 2 + + def __init__(self, image_id=""): + Layer.__init__(self) + ## id pointing to the source image defined on 'assets' object + self.image_id = image_id + + +## @ingroup Lottie +class PreCompLayer(Layer): + _props = [ + LottieProp("reference_id", "refId", str, False), + LottieProp("time_remapping", "tm", Value, False), + LottieProp("width", "w", int, False), + LottieProp("height", "h", int, False), + ] + ## %Layer type. + type = 0 + + def __init__(self, reference_id=""): + Layer.__init__(self) + ## id pointing to the source composition defined on 'assets' object + self.reference_id = reference_id + ## Comp's Time remapping + self.time_remapping = None + ## Width + self.width = 512 + ## Height + self.height = 512 + + +## @ingroup Lottie +class SolidColorLayer(Layer): + """! + Layer with a solid color rectangle + """ + _props = [ + LottieProp("color", "sc", str, False), + LottieProp("height", "sh", float, False), + LottieProp("width", "sw", float, False), + ] + ## %Layer type. + type = 1 + + def __init__(self, color="", width=512, height=512): + Layer.__init__(self) + ## Color of the layer as a @c \#rrggbb hex + # @todo Convert NVector to string + self.color = color + ## Height of the layer. + self.height = height + ## Width of the layer. + self.width = width diff --git a/lottie/objects/lottie_param.py b/lottie/objects/lottie_param.py new file mode 100644 index 0000000..c24fd90 --- /dev/null +++ b/lottie/objects/lottie_param.py @@ -0,0 +1,8073 @@ +from .layers import ShapeLayer, NullLayer, PreCompLayer, TextLayer, SolidColorLayer +from .properties import MultiDimensional, ColorValue +from .nvector import NVector +from .color import Color +from .shapes import * +import json +import re +import ast +import sys +import os +import threading +import concurrent.futures +import multiprocessing +import torch +from .effects import Effect, EffectValueSlider, EffectValueColor, EffectValueAngle, EffectValuePoint, EffectValueCheckbox, IgnoredValue, EffectValueDropDown, EffectValueLayer, EffectNoValue +from .assets import * +from .text import * + + +class ElementType: + PRECOMP_LAYER = 0 + SOLID_LAYER = 1 + NULL_LAYER = 3 + SHAPE_LAYER = 4 + TEXT_LAYER = 5 + + GROUP = "gr" + PATH = "sh" + STROKE = "st" + FILL = "fl" + TRANSFORM = "tr" + TRIM = "tm" + RECT = "rc" + ELLIPSE = "el" + STAR = "sr" + REPEATER = "rp" + GRADIENT_FILL = "gf" + GRADIENT_STROKE = "gs" # + MERGE = "mm" # + NO_STYLE = "no" # + OFFSET_PATH = "op" # + PUCKER_BLOAT = "pb" # + ROUNDED_CORNERS = "rd" # + TWIST = "tw" # + ZIG_ZAG = "zz" # + +class Keyframe: + """Represents a single keyframe in an animation""" + def __init__(self, time, value): + self.time = time + self.value = value + self.in_tan = None # Incoming Bezier handle + self.out_tan = None # Outgoing Bezier handle + self.h = None # Hold keyframe flag + self.to = None + self.ti = None + self.n = None # + self.e = None # + +class Value: + """Class for single numeric values that can be animated""" + def __init__(self, value): + self.value = value + self.keyframes = [] + + def add_keyframe(self, time, value): + """Add a keyframe to this property""" + kf = Keyframe(time, value) + self.keyframes.append(kf) + return kf + +# NullLayer + +def solid_layer_to_json(layer): + """SolidColorLayerJSON""" + json_data = { + "ddd": 0, + "ind": layer.index, + "ty": ElementType.SOLID_LAYER, + "nm": layer.name, + "sr": 1, + "ks": { + "o": property_to_json(layer.transform.opacity, 100, "ix", 11), + "r": property_to_json(layer.transform.rotation, 0, "ix", 10), + "p": property_to_json(layer.transform.position, [0, 0, 0], "ix", 2, "l", 2), + "a": property_to_json(layer.transform.anchor, [0, 0], "ix", 1, "l", 2), + "s": property_to_json(layer.transform.scale, [100, 100, 100], "ix", 6, "l", 2) + }, + "ao": 0, + "sc": layer.color, + "sw": layer.width, + "sh": layer.height, + "ip": layer.in_point, + "op": layer.out_point, + "st": layer.start_time, + "bm": 0 + } + # FIX: Add hasMask and masksProperties + if hasattr(layer, 'hasMask'): + json_data["hasMask"] = layer.hasMask + else: + # Set default value if not present + json_data["hasMask"] = False + + if hasattr(layer, 'masksProperties') and layer.masksProperties: + json_data["masksProperties"] = layer.masksProperties + + # track matte + if hasattr(layer, 'tt') and layer.tt is not None: + json_data["tt"] = layer.tt + if hasattr(layer, 'tp') and layer.tp is not None: + json_data["tp"] = layer.tp + if hasattr(layer, 'td') and layer.td is not None: + json_data["td"] = layer.td + + if hasattr(layer, 'ef'): + json_data["ef"] = effects_to_json(layer.ef) + + if hasattr(layer, 'tm'): + json_data["tm"] = layer.tm + + # parent + if hasattr(layer, 'parent_index') and layer.parent_index is not None: + json_data["parent"] = layer.parent_index + elif hasattr(layer, 'parent') and layer.parent is not None: + if isinstance(layer.parent, (int, float)): + json_data["parent"] = layer.parent + elif hasattr(layer.parent, 'index'): + json_data["parent"] = layer.parent.index + + return json_data + + +def text_layer_to_json(layer): + """TextLayerJSON""" + json_data = { + "ddd": 0, + "ind": layer.index, + "ty": ElementType.TEXT_LAYER, + "nm": layer.name, + "sr": 1, + "ks": { + "o": property_to_json(layer.transform.opacity, 100, "ix", 11), + "r": property_to_json(layer.transform.rotation, 0, "ix", 10), + "p": property_to_json(layer.transform.position, [0, 0, 0], "ix", 2, "l", 2), + "a": property_to_json(layer.transform.anchor, [0, 0], "ix", 1, "l", 2), + "s": property_to_json(layer.transform.scale, [100, 100, 100], "ix", 6, "l", 2) + }, + "ao": 0, + "t": { + "d": {"k": []}, + "p": {}, + "m": {"g": 1, "a": {"a": 0, "k": [0, 0], "ix": 2}}, + "a": [] + }, + "ip": layer.in_point, + "op": layer.out_point, + "st": layer.start_time, + "bm": 0 + } + + # hasMaskTrue + if hasattr(layer, 'hasMask') and layer.hasMask: + json_data["hasMask"] = layer.hasMask + + if hasattr(layer, 'masksProperties') and layer.masksProperties: + json_data["masksProperties"] = layer.masksProperties + + # Text LayerctctNone + if hasattr(layer, 'ct'): + json_data["ct"] = layer.ct + + #if hasattr(layer, 'ln') and layer.ln is not None: + # json_data["ln"] = layer.ln + + if hasattr(layer, 'ef'): + json_data["ef"] = layer.ef + if hasattr(layer.data, "document") and hasattr(layer.data.document, "value"): + json_data["t"]["d"]["k"] = layer.data.document.value + + if hasattr(layer.data, "path_option") and layer.data.path_option: + json_data["t"]["p"] = layer.data.path_option + + if hasattr(layer.data, "more_options") and layer.data.more_options: + json_data["t"]["m"] = layer.data.more_options + + if hasattr(layer.data, "animators") and layer.data.animators: + json_data["t"]["a"] = layer.data.animators + + # track matte + if hasattr(layer, 'tt') and layer.tt is not None: + json_data["tt"] = layer.tt + if hasattr(layer, 'tp') and layer.tp is not None: + json_data["tp"] = layer.tp + if hasattr(layer, 'td') and layer.td is not None: + json_data["td"] = layer.td + + # parent + if hasattr(layer, 'parent_index') and layer.parent_index is not None: + json_data["parent"] = layer.parent_index + elif hasattr(layer, 'parent') and layer.parent is not None: + if isinstance(layer.parent, (int, float)): + json_data["parent"] = layer.parent + elif hasattr(layer.parent, 'index'): + json_data["parent"] = layer.parent.index + + return json_data + + +# - +# - +# - +# - +# - +# - +# - +def shape_layer_to_json(layer): + """Fixed version: All 2D layers should have 'l' parameter""" + json_data = { + "ddd": getattr(layer, 'ddd', 0), + "ind": layer.index, + "ty": ElementType.SHAPE_LAYER, + "nm": layer.name, + "sr": 1, + "ks": {}, + "ao": getattr(layer, 'ao', 0), + "shapes": [], + "ip": layer.in_point, + "op": layer.out_point, + "st": layer.start_time, + "bm": 0 + } + # hd + if hasattr(layer, 'hd') and layer.hd is not None: + json_data["hd"] = layer.hd + # cpFalse + if hasattr(layer, 'cp') and layer.cp is not None and layer.cp is not False: + json_data["cp"] = layer.cp + # cl + if hasattr(layer, 'cl') and layer.cl is not None: + json_data["cl"] = layer.cl + + if hasattr(layer, 'ct'): + json_data["ct"] = layer.ct + + if hasattr(layer, 'tm'): + json_data["tm"] = layer.tm + + # hasMaskTrue + if hasattr(layer, 'hasMask') and layer.hasMask: + json_data["hasMask"] = layer.hasMask + if hasattr(layer, 'masksProperties'): + json_data["masksProperties"] = layer.masksProperties + + + # Build ks with correct parameters based on 3D flag + + ks = {} + + # Check if this is a 3D layer + is_3d = json_data["ddd"] == 1 + + if is_3d: + # For 3D layers - no ix or l parameters + ks["o"] = property_to_json(layer.transform.opacity, 100) + + # For 3D layers, use separate rotation axes - FIX: Check for rx, ry, rz + if hasattr(layer.transform, 'rx') and layer.transform.rx is not None: + ks["rx"] = property_to_json(layer.transform.rx, 0) + else: + ks["rx"] = {"a": 0, "k": 0} + + if hasattr(layer.transform, 'ry') and layer.transform.ry is not None: + ks["ry"] = property_to_json(layer.transform.ry, 0) + else: + ks["ry"] = {"a": 0, "k": 0} + + if hasattr(layer.transform, 'rz') and layer.transform.rz is not None: + ks["rz"] = property_to_json(layer.transform.rz, 0) + elif hasattr(layer.transform, 'rotation'): + ks["rz"] = property_to_json(layer.transform.rotation, 0) + else: + ks["rz"] = {"a": 0, "k": 0} + + + # Orientation for 3D + if hasattr(layer.transform, 'orientation'): + ks["or"] = property_to_json(layer.transform.orientation, [0, 0, 0]) + else: + ks["or"] = {"a": 0, "k": [0, 0, 0]} + + # No 'l' parameter for 3D + ks["p"] = property_to_json(layer.transform.position, [0, 0, 0], "ix", 2) + ks["a"] = property_to_json(layer.transform.anchor, [0, 0, 0], "ix", 1) + ks["s"] = property_to_json(layer.transform.scale, [100, 100, 100], "ix", 6) + + else: + # For 2D layers - ALWAYS add ix and l parameters (regardless of asset or main layer) + ks["o"] = property_to_json(layer.transform.opacity, 100, "ix", 11) + ks["r"] = property_to_json(layer.transform.rotation, 0, "ix", 10) + + # Position and anchor should have 'l' parameter for 2D but keep 3 dimensions + ks["p"] = property_to_json(layer.transform.position, [0, 0, 0], "ix", 2, "l", 2) + ks["a"] = property_to_json(layer.transform.anchor, [0, 0, 0], "ix", 1, "l", 2) + + # Scale ALSO needs 'l' parameter for 2D layers and keeps 3 dimensions + ks["s"] = property_to_json(layer.transform.scale, [100, 100, 100], "ix", 6, "l", 2) + + # Add skew and skew_axis for 2D layers + if hasattr(layer.transform, 'skew'): + ks["sk"] = property_to_json(layer.transform.skew, 0, "ix", 2) + else: + ks["sk"] = {"a": 0, "k": 0, "ix": 2} + + if hasattr(layer.transform, 'skew_axis'): + ks["sa"] = property_to_json(layer.transform.skew_axis, 0, "ix", 2) + else: + ks["sa"] = {"a": 0, "k": 0, "ix": 2} + + #ks["ty"] = "tr" + json_data["ks"] = ks + + + # FIX: Always add ef if it exists, even if empty array + if hasattr(layer, 'ef'): + json_data["ef"] = layer.ef + + # matte + if hasattr(layer, 'tt') and layer.tt is not None and layer.tt != 0: + json_data["tt"] = layer.tt + if hasattr(layer, 'tp') and layer.tp is not None and layer.tp != 0: + json_data["tp"] = layer.tp + if hasattr(layer, 'td') and layer.td is not None and layer.td != 0: + json_data["td"] = layer.td + + # FIX: Handle parent index 0 properly + if hasattr(layer, 'parent_index') and layer.parent_index is not None: + json_data["parent"] = layer.parent_index + elif hasattr(layer, 'parent') and layer.parent is not None: + if isinstance(layer.parent, (int, float)): + json_data["parent"] = layer.parent + elif hasattr(layer.parent, 'index'): + json_data["parent"] = layer.parent.index + + for shape in layer.shapes: + json_shape = shape_to_json(shape) + if json_shape: + json_data["shapes"].append(json_shape) + + return json_data + + +def font_to_json(font): + """FontJSON - Lottie""" + font_json = { + "fName": font.name, + "fFamily": font.font_family, + "fStyle": font.font_style + } + if hasattr(font, 'ascent') and font.ascent is not None: + font_json["ascent"] = font.ascent + return font_json + + +def char_to_json(char): + """CharsJSON""" + json_data = { + "ch": char.character, + "size": char.font_size, + "style": char.font_style, + "w": char.width, + "fFamily": char.font_family + } + + # shapesshapesdata{} + if hasattr(char, 'data') and hasattr(char.data, 'shapes') and char.data.shapes: + # shapesshapes + json_data["data"] = {"shapes": []} + for shape in char.data.shapes: + shape_json = shape_to_json(shape) + if shape_json: + json_data["data"]["shapes"].append(shape_json) + else: + # shapesdata + json_data["data"] = {} + + return json_data + +def extract_string_value(line): + """""" + start = line.find('"') + 1 + end = line.rfind('"') + if start > 0 and end > start: + return line[start:end] + return "" + +def null_layer_to_json(layer): + """NullLayerJSON""" + json_data = { + "nm": layer.name, + "ddd": 1 if getattr(layer, 'threedimensional', False) else getattr(layer, 'ddd', 0), + "ty": layer.type, + "ind": layer.index, + "sr": layer.stretch, + "ip": layer.in_point, + "op": layer.out_point, + "st": layer.start_time, + #"ao": getattr(layer, 'ao', 0), + #"ao": layer.ao, + "bm": 0 + } + # hd + if hasattr(layer, 'hd') and layer.hd is not None: + json_data["hd"] = layer.hd + + # cl + if hasattr(layer, 'cl') and layer.cl is not None: + json_data["cl"] = layer.cl + # Add ct if it exists + if hasattr(layer, 'ct') and layer.ct is not None: + json_data["ct"] = layer.ct + if hasattr(layer, 'auto_orient') and layer.auto_orient is not None: + json_data["ao"] = layer.auto_orient + + #if hasattr(layer, 'ln'): + # json_data["ln"] = layer.ln + + # FIX: Always add hasMask (default to False if not present) + if hasattr(layer, 'hasMask'): + json_data["hasMask"] = layer.hasMask + if hasattr(layer, 'hd'): + json_data["hd"] = layer.hd + if hasattr(layer, 'cp'): + json_data["cp"] = layer.cp + + # parentparent + if hasattr(layer, 'parent_index') and layer.parent_index is not None: + json_data["parent"] = layer.parent_index + elif hasattr(layer, 'parent') and layer.parent is not None: + if isinstance(layer.parent, (int, float)): + json_data["parent"] = layer.parent + elif hasattr(layer.parent, 'index'): + json_data["parent"] = layer.parent.index + + + # track matte + if hasattr(layer, 'td') and layer.td is not None: + json_data["td"] = layer.td + if hasattr(layer, 'tt') and layer.tt is not None: + json_data["tt"] = layer.tt + if hasattr(layer, 'tp') and layer.tp is not None: + json_data["tp"] = layer.tp + + if hasattr(layer, 'ef'): + json_data["ef"] = layer.ef + + # - + ks = {} + + if layer.transform.anchor is not None: + ks["a"] = property_to_json(layer.transform.anchor, [0, 0, 0], "ix", 1, "l", 2) + + if layer.transform.position is not None: + # position + pos_json = property_to_json(layer.transform.position, [0, 0, 0], "ix", 2, "l", 2) + # positiona + if hasattr(layer.transform.position, 'animated') and layer.transform.position.animated: + pos_json["a"] = 1 + elif hasattr(layer.transform.position, 'keyframes') and layer.transform.position.keyframes: + pos_json["a"] = 1 + ks["p"] = pos_json + + # ... rest of transform properties remain the same ... + if hasattr(layer.transform, 'scale') and layer.transform.scale is not None: + if hasattr(layer.transform.scale, 'separated') and layer.transform.scale.separated: + scale_data = {"s": True} + if hasattr(layer.transform.scale, 'x') and layer.transform.scale.x is not None: + scale_data["x"] = property_to_json(layer.transform.scale.x, 100) + if hasattr(layer.transform.scale, 'x_ix'): + scale_data["x"]["ix"] = layer.transform.scale.x_ix + if hasattr(layer.transform.scale, 'y') and layer.transform.scale.y is not None: + scale_data["y"] = property_to_json(layer.transform.scale.y, 100) + if hasattr(layer.transform.scale, 'y_ix'): + scale_data["y"]["ix"] = layer.transform.scale.y_ix + if hasattr(layer.transform.scale, 'z') and layer.transform.scale.z is not None: + scale_data["z"] = property_to_json(layer.transform.scale.z, 100) + if hasattr(layer.transform.scale, 'z_ix'): + scale_data["z"]["ix"] = layer.transform.scale.z_ix + ks["s"] = scale_data + else: + ks["s"] = property_to_json(layer.transform.scale, [100, 100, 100], "ix", 6, "l", 2) + + if layer.transform.rotation is not None: + ks["r"] = property_to_json(layer.transform.rotation, 0, "ix", 10) + + if layer.transform.opacity is not None: + ks["o"] = property_to_json(layer.transform.opacity, 100, "ix", 11) + + if hasattr(layer.transform, 'skew') and layer.transform.skew is not None: + ks["sk"] = property_to_json(layer.transform.skew, 0) + + if hasattr(layer.transform, 'skew_axis') and layer.transform.skew_axis is not None: + ks["sa"] = property_to_json(layer.transform.skew_axis, 0) + + json_data["ks"] = ks + + return json_data + +# PrecompLayer + +def precomp_layer_to_json(layer): + """Convert a PreCompLayer object to JSON format""" + json_data = { + "ddd": 0, + "ind": layer.index, + "ty": ElementType.PRECOMP_LAYER, + "nm": layer.name, + "sr": 1, + "ks":{}, + "ao": 0, + "ip": layer.in_point, + "op": layer.out_point, + "st": layer.start_time, + "bm": 0, + "refId": layer.reference_id + } + + # FIX: Add ln property - + #if hasattr(layer, 'ln'): + # json_data["ln"] = int(layer.ln) + + # FIX: Always add hasMask (default to False if not present) + if hasattr(layer, 'hasMask'): + json_data["hasMask"] = layer.hasMask + if hasattr(layer, 'hd'): + json_data["hd"] = layer.hd + + if hasattr(layer, 'cp'): + json_data["cp"] = layer.cp + + # FIX: Add masksProperties if present + if hasattr(layer, 'masksProperties') and layer.masksProperties: + json_data["masksProperties"] = layer.masksProperties + + # FIX: Add tm (time remapping) property - None + if hasattr(layer, 'tm'): + json_data["tm"] = layer.tm + + # FIX: Add w and h if present + if hasattr(layer, 'w'): + json_data["w"] = layer.w + if hasattr(layer, 'h'): + json_data["h"] = layer.h + + # ct + if hasattr(layer, 'ct') and layer.ct is not None: + json_data["ct"] = layer.ct + # effects + if hasattr(layer, 'ef') and layer.ef is not None: + json_data["ef"] = layer.ef + + # ks + ks = {} + + # opacity + ks["o"] = property_to_json(layer.transform.opacity, 100, "ix", 11) + + # rotation + ks["r"] = property_to_json(layer.transform.rotation, 0, "ix", 10) + + # position - 3 + ks["p"] = property_to_json(layer.transform.position, [0, 0, 0], "ix", 2, "l", 2) + + # anchor - 3 + ks["a"] = property_to_json(layer.transform.anchor, [0, 0, 0], "ix", 1, "l", 2) + + # scale - 3 + ks["s"] = property_to_json(layer.transform.scale, [100, 100, 100], "ix", 6, "l", 2) + + json_data["ks"] = ks + + # Add track matte related attributes + if hasattr(layer, 'tt') and layer.tt is not None: + json_data["tt"] = layer.tt + if hasattr(layer, 'tp') and layer.tp is not None: + json_data["tp"] = layer.tp + if hasattr(layer, 'td') and layer.td is not None: + json_data["td"] = layer.td + + # Add parent info + if hasattr(layer, 'parent_index') and layer.parent_index is not None: + json_data["parent"] = layer.parent_index + elif hasattr(layer, 'parent') and layer.parent is not None: + if isinstance(layer.parent, (int, float)): + json_data["parent"] = layer.parent + elif hasattr(layer.parent, 'index'): + json_data["parent"] = layer.parent.index + + return json_data + + +def zig_zag_to_json(shape): + """JSON""" + json_data = { + "ty": "zz", + "nm": shape.name if hasattr(shape, 'name') else "", + "mn": "ADBE Vector Filter - Zigzag", + "hd": getattr(shape, 'hd', False), + "ix": shape.property_index if hasattr(shape, 'property_index') else 1 + } + + # ZigZag + if hasattr(shape, 'frequency') and shape.frequency is not None: + json_data["r"] = property_to_json(shape.frequency, 5) + + if hasattr(shape, 'amplitude') and shape.amplitude is not None: + json_data["s"] = property_to_json(shape.amplitude, 10) + + if hasattr(shape, 'point_type') and shape.point_type is not None: + json_data["pt"] = property_to_json(shape.point_type, 1) + + # + if hasattr(shape, 'bm') and shape.bm is not None: + json_data["bm"] = shape.bm + + if hasattr(shape, 'cl') and shape.cl is not None: + json_data["cl"] = shape.cl + + #if hasattr(shape, 'ln') and shape.ln is not None: + # json_data["ln"] = shape.ln + + return json_data + + + +def shape_to_json(shape): + """JSON""" + if isinstance(shape, Group): + return group_to_json(shape) + elif isinstance(shape, Path): + return path_to_json(shape) + elif isinstance(shape, Stroke): + return stroke_to_json(shape) + elif isinstance(shape, Fill): + return fill_to_json(shape) + elif isinstance(shape, Trim): + return trim_to_json(shape) + elif isinstance(shape, Rect): + return rect_to_json(shape) + elif isinstance(shape, Ellipse): + return ellipse_to_json(shape) + elif isinstance(shape, Star): + return star_to_json(shape) + elif isinstance(shape, Repeater): + return repeater_to_json(shape) + elif isinstance(shape, TransformShape): + return transform_shape_to_json(shape) # Use the correct function for TransformShape + elif isinstance(shape, GradientFill): + return gradient_fill_to_json(shape) + elif isinstance(shape, GradientStroke): + return gradient_stroke_to_json(shape) + elif isinstance(shape, Merge): + return merge_to_json(shape) + elif isinstance(shape, RoundedCorners): + return rounded_corners_to_json(shape) + elif isinstance(shape, Twist): + return twist_to_json(shape) + elif isinstance(shape, ZigZag): + return zig_zag_to_json(shape) + else: + print(f"Warning: : {type(shape)}") + return None + +def group_to_json(group): + """JSON - np""" + # + all_items = [] + + for item in group.shapes: + json_item = shape_to_json(item) + if json_item: + all_items.append(json_item) + + # group.nameNone + group_name = group.name if group.name is not None else "" + + # npit + if hasattr(group, 'number_of_properties') and group.number_of_properties is not None: + np_value = group.number_of_properties + else: + # npit + np_value = len(all_items) + + # JSON + json_data = { + "ty": ElementType.GROUP, + "it": all_items, + "nm": group_name, + "np": int(np_value), # + "cix": getattr(group, 'cix', 2), + "bm": 0, + "ix": getattr(group, 'property_index', 1), + "mn": getattr(group, 'mn', "ADBE Vector Group"), + "hd": getattr(group, 'hd', False) + } + + return json_data + + + +def rect_to_json(rect): + """JSON""" + json_data = { + "ty": ElementType.RECT, + "nm": rect.name, + "ix": rect.property_index, + "d": getattr(rect, 'direction', 1), # + "mn": "ADBE Vector Shape - Rect", + "hd": getattr(rect, 'hd', False) # hd + } + + # - ix + if hasattr(rect, "position") and rect.position: + json_data["p"] = property_to_json(rect.position, [0, 0], "ix", 3) + + # - ix + if hasattr(rect, "size") and rect.size: + json_data["s"] = property_to_json(rect.size, [100, 100], "ix", 2) + + # - ix + if hasattr(rect, "rounded") and rect.rounded: + rounded_ix = getattr(rect, 'rounded_ix', 4) # 41 + json_data["r"] = property_to_json(rect.rounded, 0, "ix", rounded_ix) + + return json_data + + +def path_to_json(path): + """JSON - ind, mn, hd""" + json_data = { + "ty": ElementType.PATH, + "nm": path.name, + } + + # pathd + if hasattr(path, 'd') and path.d is not None: + json_data["d"] = path.d + + # āœ… ind + if hasattr(path, 'ind') and path.ind is not None: + json_data["ind"] = path.ind + + # ixNone + if hasattr(path, 'property_index') and path.property_index is not None: + json_data["ix"] = path.property_index + + # āœ… mn - + if hasattr(path, 'mn') and path.mn is not None: + json_data["mn"] = path.mn + + # āœ… hd - + if hasattr(path, 'hd') and path.hd is not None: + json_data["hd"] = path.hd + + # ks + ks = { + "a": 0 # + } + + # + if hasattr(path.shape, 'keyframes') and path.shape.keyframes: + # + ks["a"] = 1 + keyframes = [] + + for kf in path.shape.keyframes: + bezier = kf.value + + keyframe = { + "t": kf.time, + "s": [{ + "i": [[t.x, t.y] for t in bezier.in_tangents], + "o": [[t.x, t.y] for t in bezier.out_tangents], + "v": [[v.x, v.y] for v in bezier.vertices], + "c": bezier.closed + }] + } + + # h + if hasattr(kf, 'h') and kf.h is not None: + keyframe["h"] = kf.h + + # + if hasattr(kf, 'in_tan') and kf.in_tan: + # - + x_val = kf.in_tan['x'] + y_val = kf.in_tan['y'] + + # + if isinstance(x_val, list) and len(x_val) == 1: + x_val = x_val[0] + if isinstance(y_val, list) and len(y_val) == 1: + y_val = y_val[0] + + # + if isinstance(x_val, list) or isinstance(y_val, list): + keyframe["i"] = {"x": x_val if isinstance(x_val, list) else [x_val], + "y": y_val if isinstance(y_val, list) else [y_val]} + else: + # + keyframe["i"] = {"x": x_val, "y": y_val} + + if hasattr(kf, 'out_tan') and kf.out_tan: + # out_tan + x_val = kf.out_tan['x'] + y_val = kf.out_tan['y'] + + # + if isinstance(x_val, list) and len(x_val) == 1: + x_val = x_val[0] + if isinstance(y_val, list) and len(y_val) == 1: + y_val = y_val[0] + + # + if isinstance(x_val, list) or isinstance(y_val, list): + keyframe["o"] = {"x": x_val if isinstance(x_val, list) else [x_val], + "y": y_val if isinstance(y_val, list) else [y_val]} + else: + # + keyframe["o"] = {"x": x_val, "y": y_val} + + keyframes.append(keyframe) + + ks["k"] = keyframes + else: + # + bezier = path.shape.value + + ks["k"] = { + "i": [[t.x, t.y] for t in bezier.in_tangents], + "o": [[t.x, t.y] for t in bezier.out_tangents], + "v": [[v.x, v.y] for v in bezier.vertices], + "c": bezier.closed + } + + json_data["ks"] = ks + + return json_data + + + +def ellipse_to_json(ellipse): + """JSON""" + json_data = { + "ty": ElementType.ELLIPSE, + "nm": ellipse.name, + "ix": ellipse.property_index, + "d": 1, + "mn": "ADBE Vector Shape - Ellipse", + "hd": False + } + + # + if hasattr(ellipse, "size") and ellipse.size is not None: + # + is_animated = hasattr(ellipse.size, 'animated') and ellipse.size.animated + + if is_animated and hasattr(ellipse.size, "value") and hasattr(ellipse.size.value, "components"): + # keyframes + components = ellipse.size.value.components + if isinstance(components, list) and len(components) > 0 and isinstance(components[0], dict): + json_data["s"] = { + "a": 1, + "k": components, # componentskeyframe dict + "ix": 2 + } + else: + # + size_value = [components[0], components[1]] if len(components) >= 2 else [723.001, 723.001] + json_data["s"] = { + "a": 0, + "k": size_value, + "ix": 2 + } + elif hasattr(ellipse.size, "value"): + if hasattr(ellipse.size.value, "components"): + components = ellipse.size.value.components + size_value = [components[0], components[1]] if len(components) >= 2 else [723.001, 723.001] + elif hasattr(ellipse.size.value, "x") and hasattr(ellipse.size.value, "y"): + size_value = [ellipse.size.value.x, ellipse.size.value.y] + else: + size_value = [723.001, 723.001] + json_data["s"] = { + "a": 0, + "k": size_value, + "ix": 2 + } + else: + size_value = [723.001, 723.001] # + json_data["s"] = { + "a": 0, + "k": size_value, + "ix": 2 + } + else: + # + json_data["s"] = { + "a": 0, + "k": [723.001, 723.001], + "ix": 2 + } + + # + if hasattr(ellipse, "position") and ellipse.position is not None: + # + is_animated = hasattr(ellipse.position, 'animated') and ellipse.position.animated + + if is_animated and hasattr(ellipse.position, "value") and hasattr(ellipse.position.value, "components"): + # keyframes + components = ellipse.position.value.components + if isinstance(components, list) and len(components) > 0 and isinstance(components[0], dict): + json_data["p"] = { + "a": 1, + "k": components, + "ix": 3 + } + else: + # + pos_value = [components[0], components[1]] if len(components) >= 2 else [0, 0] + json_data["p"] = { + "a": 0, + "k": pos_value, + "ix": 3 + } + elif hasattr(ellipse.position, "value"): + if hasattr(ellipse.position.value, "components"): + components = ellipse.position.value.components + pos_value = [components[0], components[1]] if len(components) >= 2 else [0, 0] + elif hasattr(ellipse.position.value, "x") and hasattr(ellipse.position.value, "y"): + pos_value = [ellipse.position.value.x, ellipse.position.value.y] + else: + pos_value = [0, 0] + json_data["p"] = { + "a": 0, + "k": pos_value, + "ix": 3 + } + else: + pos_value = [0, 0] # + json_data["p"] = { + "a": 0, + "k": pos_value, + "ix": 3 + } + else: + # + json_data["p"] = { + "a": 0, + "k": [0, 0], + "ix": 3 + } + + return json_data + +def star_to_json(star): + """JSON""" + json_data = { + "ty": ElementType.STAR, + "nm": star.name, + "ix": star.property_index, + "mn": "ADBE Vector Shape - Star", + "hd": False + } + + # direction + if hasattr(star, 'direction'): + json_data["d"] = star.direction + else: + json_data["d"] = 1 + + # star_type + if hasattr(star, 'star_type'): + json_data["sy"] = star.star_type.value if hasattr(star.star_type, 'value') else star.star_type + else: + json_data["sy"] = 1 # Star + + # ix + if hasattr(star, "position") and star.position: + p_json = property_to_json(star.position, [0, 0]) + if hasattr(star, "position_ix"): + p_json["ix"] = star.position_ix + else: + p_json["ix"] = 3 + json_data["p"] = p_json + + # ix + if hasattr(star, "inner_radius") and star.inner_radius: + ir_json = property_to_json(star.inner_radius, 0) + if hasattr(star, "inner_radius_ix"): + ir_json["ix"] = star.inner_radius_ix + else: + ir_json["ix"] = 6 + json_data["ir"] = ir_json + + # ix + if hasattr(star, "outer_radius") and star.outer_radius: + or_json = property_to_json(star.outer_radius, 0) + if hasattr(star, "outer_radius_ix"): + or_json["ix"] = star.outer_radius_ix + else: + or_json["ix"] = 7 + json_data["or"] = or_json + + # ix + if hasattr(star, "inner_roundness") and star.inner_roundness: + is_json = property_to_json(star.inner_roundness, 0) + if hasattr(star, "inner_roundness_ix"): + is_json["ix"] = star.inner_roundness_ix + else: + is_json["ix"] = 10 + json_data["is"] = is_json + + # ix + if hasattr(star, "outer_roundness") and star.outer_roundness: + os_json = property_to_json(star.outer_roundness, 0) + if hasattr(star, "outer_roundness_ix"): + os_json["ix"] = star.outer_roundness_ix + else: + os_json["ix"] = 11 + json_data["os"] = os_json + + # ix + if hasattr(star, "points") and star.points: + pt_json = property_to_json(star.points, 5) + if hasattr(star, "points_ix"): + pt_json["ix"] = star.points_ix + else: + pt_json["ix"] = 1 + json_data["pt"] = pt_json + + # ix + if hasattr(star, "rotation") and star.rotation: + r_json = property_to_json(star.rotation, 0) + if hasattr(star, "rotation_ix"): + r_json["ix"] = star.rotation_ix + else: + r_json["ix"] = 4 + json_data["r"] = r_json + + return json_data + +def repeater_to_json(repeater): + """JSON - ix""" + json_data = { + "ty": ElementType.REPEATER, + "nm": repeater.name, + "ix": repeater.property_index, + "mn": "ADBE Vector Filter - Repeater", + "hd": False + } + + # - ix + if hasattr(repeater, "copies") and repeater.copies: + copies_json = property_to_json(repeater.copies, 1) + if hasattr(repeater, "copies_ix"): + copies_json["ix"] = repeater.copies_ix + json_data["c"] = copies_json + + # - ix + if hasattr(repeater, "offset") and repeater.offset: + offset_json = property_to_json(repeater.offset, 0) + if hasattr(repeater, "offset_ix"): + offset_json["ix"] = repeater.offset_ix + json_data["o"] = offset_json + + # composite mode + if hasattr(repeater, "composite"): + json_data["m"] = repeater.composite + + # - ix + if hasattr(repeater, "transform") and repeater.transform: + tr_json = {} + + # ty + if hasattr(repeater.transform, "type"): + tr_json["ty"] = repeater.transform.type + else: + tr_json["ty"] = "tr" + + # positionix + if hasattr(repeater.transform, "position"): + p_json = property_to_json(repeater.transform.position, [0, 0]) + if hasattr(repeater.transform, "position_ix"): + p_json["ix"] = repeater.transform.position_ix + tr_json["p"] = p_json + + # anchorix + if hasattr(repeater.transform, "anchor_point"): + a_json = property_to_json(repeater.transform.anchor, [0, 0]) + if hasattr(repeater.transform, "anchor_ix"): + a_json["ix"] = repeater.transform.anchor_ix + tr_json["a"] = a_json + + # scaleix + if hasattr(repeater.transform, "scale"): + s_json = property_to_json(repeater.transform.scale, [100, 100]) + if hasattr(repeater.transform, "scale_ix"): + s_json["ix"] = repeater.transform.scale_ix + tr_json["s"] = s_json + + # rotationix + if hasattr(repeater.transform, "rotation"): + r_json = property_to_json(repeater.transform.rotation, 0) + if hasattr(repeater.transform, "rotation_ix"): + r_json["ix"] = repeater.transform.rotation_ix + tr_json["r"] = r_json + + # start_opacityix + if hasattr(repeater.transform, "start_opacity"): + so_json = property_to_json(repeater.transform.start_opacity, 100) + if hasattr(repeater.transform, "start_opacity_ix"): + so_json["ix"] = repeater.transform.start_opacity_ix + tr_json["so"] = so_json + + # end_opacityix + if hasattr(repeater.transform, "end_opacity"): + eo_json = property_to_json(repeater.transform.end_opacity, 100) + if hasattr(repeater.transform, "end_opacity_ix"): + eo_json["ix"] = repeater.transform.end_opacity_ix + tr_json["eo"] = eo_json + + # name + if hasattr(repeater.transform, "name"): + tr_json["nm"] = repeater.transform.name + + json_data["tr"] = tr_json + + return json_data + + +def property_to_json(prop, default_value, *args): + """Fixed: Handle separated properties, expressions, spatial interpolation, and e field""" + extra_params = {} + for i in range(0, len(args), 2): + if i + 1 < len(args): + extra_params[args[i]] = args[i+1] + + # Handle separated properties + if hasattr(prop, "separated") and prop.separated: + json_data = {"s": True} + + # Check for expression on the separated property itself + if hasattr(prop, "expression") and prop.expression: + json_data["x"] = prop.expression + + # Calculate correct default values for separated components + if isinstance(default_value, list): + x_default = default_value[0] if len(default_value) > 0 else 0 + y_default = default_value[1] if len(default_value) > 1 else 0 + z_default = default_value[2] if len(default_value) > 2 else 0 + else: + x_default = y_default = z_default = default_value + + # Process separated x component (only if it exists as a dict property) + if hasattr(prop, "x") and prop.x is not None and not isinstance(prop.x, str): + x_json = property_to_json(prop.x, x_default) + if hasattr(prop, 'x_ix'): + x_json["ix"] = prop.x_ix + else: + x_json["ix"] = 3 + json_data["x"] = x_json + + if hasattr(prop, "y") and prop.y is not None: + y_json = property_to_json(prop.y, y_default) + if hasattr(prop, 'y_ix'): + y_json["ix"] = prop.y_ix + else: + y_json["ix"] = 4 + json_data["y"] = y_json + + if hasattr(prop, "z") and prop.z is not None: + z_json = property_to_json(prop.z, z_default) + if hasattr(prop, 'z_ix'): + z_json["ix"] = prop.z_ix + else: + z_json["ix"] = 5 + json_data["z"] = z_json + + # Add animated flag if present + if hasattr(prop, "animated") and prop.animated: + json_data["a"] = 1 + + # Preserve the main ix value + if hasattr(prop, "ix") and prop.ix is not None: + json_data["ix"] = prop.ix + elif "ix" in extra_params: + json_data["ix"] = extra_params["ix"] + + return json_data + + # Handle keyframe animation (non-separated) + if hasattr(prop, "keyframes") and prop.keyframes: + keyframes = [] + for kf in prop.keyframes: + keyframe = {"t": kf.time} + + # Format value + if isinstance(kf.value, list): + keyframe["s"] = kf.value + elif hasattr(kf.value, "components"): + keyframe["s"] = list(kf.value.components) + else: + keyframe["s"] = [kf.value] + + # Add e field (end value) if it exists + if hasattr(kf, "e") and kf.e is not None: + if isinstance(kf.e, list): + keyframe["e"] = kf.e + elif hasattr(kf.e, "components"): + keyframe["e"] = list(kf.e.components) + else: + keyframe["e"] = [kf.e] + + # Add h attribute only if it exists and is not None + if hasattr(kf, "h") and kf.h is not None: + keyframe["h"] = kf.h + + # Add spatial interpolation (to/ti) + if hasattr(kf, "to") and kf.to: + keyframe["to"] = kf.to + if hasattr(kf, "ti") and kf.ti: + keyframe["ti"] = kf.ti + + # Add n attribute + if hasattr(kf, "n") and kf.n is not None: + keyframe["n"] = kf.n + + # Add easing - DO NOT wrap in list unless already a list + if hasattr(kf, "in_tan") and kf.in_tan: + x_val = kf.in_tan["x"] + y_val = kf.in_tan["y"] + # Only wrap in list if the value isn't already a list + if not isinstance(x_val, list): + x_val = x_val + if not isinstance(y_val, list): + y_val = y_val + keyframe["i"] = {"x": x_val, "y": y_val} + + if hasattr(kf, "out_tan") and kf.out_tan: + x_val = kf.out_tan["x"] + y_val = kf.out_tan["y"] + # Only wrap in list if the value isn't already a list + if not isinstance(x_val, list): + x_val = x_val + if not isinstance(y_val, list): + y_val = y_val + keyframe["o"] = {"x": x_val, "y": y_val} + + + keyframes.append(keyframe) + + json_data = {"a": 1, "k": keyframes} + + else: + # Static property + if hasattr(prop, "value"): + value = prop.value + + if hasattr(value, "components"): + k_value = list(value.components) + elif hasattr(value, "r") and hasattr(value, "g") and hasattr(value, "b"): + k_value = [value.r, value.g, value.b] + if hasattr(value, "a"): + k_value.append(value.a) + elif isinstance(value, list): + k_value = value + else: + k_value = value + + # Check if animated flag is set + is_animated = 0 + if hasattr(prop, 'animated') and prop.animated: + is_animated = 1 + elif hasattr(prop, 'expression') and prop.expression: + is_animated = 1 + + json_data = {"a": is_animated, "k": k_value} + else: + json_data = {"a": 0, "k": default_value} + + # Add expression if exists (for non-separated properties) + if hasattr(prop, "expression") and prop.expression and not hasattr(prop, "separated"): + json_data["x"] = prop.expression + + # Add 'l' parameter for 2D layer transforms + if "l" in extra_params: + json_data["l"] = extra_params["l"] + + # Add 'ix' only if explicitly requested + if "ix" in extra_params: + json_data["ix"] = extra_params["ix"] + + # Add 'a' parameter if specified + if "a" in extra_params: + json_data["a"] = extra_params["a"] + + return json_data + +def stroke_to_json(stroke): + """JSON""" + json_data = { + "ty": ElementType.STROKE, + "bm": 0, + "nm": stroke.name, + "mn": "ADBE Vector Graphic - Stroke", + "hd": False, + } + + # Color - + + # FIX: Color - properly handle animated colors with keyframes + if hasattr(stroke.color, 'keyframes') and stroke.color.keyframes: + # Animated color + keyframes = [] + for kf in stroke.color.keyframes: + # Respect original dimensions + color_dim = getattr(stroke, 'color_dimensions', 4) + if color_dim == 2: + s_data = [kf.value.r, kf.value.g] + elif color_dim == 3: + s_data = [kf.value.r, kf.value.g, kf.value.b] + else: + s_data = [kf.value.r, kf.value.g, kf.value.b, 1] + + kf_data = { + "s": s_data, + "t": kf.time + } + + # Add easing if present + if hasattr(kf, 'in_tan') and kf.in_tan: + kf_data["i"] = kf.in_tan + if hasattr(kf, 'out_tan') and kf.out_tan: + kf_data["o"] = kf.out_tan + + keyframes.append(kf_data) + + # Output with "k" containing keyframes array directly (no "a" field needed when animated) + json_data["c"] = {"k": keyframes} + + # Add ix if originally present + if hasattr(stroke, 'has_c_ix') and stroke.has_c_ix: + json_data["c"]["ix"] = getattr(stroke, 'c_ix', 3) + else: + # Static color + color_dim = getattr(stroke, 'color_dimensions', 3) + has_c_a = getattr(stroke, 'has_c_a', False) + has_c_ix = getattr(stroke, 'has_c_ix', False) + + color_array = [] + if color_dim == 1: + color_array = [stroke.color.value.r] + elif color_dim == 2: + color_array = [stroke.color.value.r, stroke.color.value.g] + elif color_dim == 3: + color_array = [stroke.color.value.r, stroke.color.value.g, stroke.color.value.b] + else: # 4 or more + color_array = [stroke.color.value.r, stroke.color.value.g, stroke.color.value.b, 1] + + json_data["c"] = {"k": color_array} + if has_c_a: + json_data["c"]["a"] = 0 + if has_c_ix: + json_data["c"]["ix"] = getattr(stroke, 'c_ix', 3) + + + # Opacity - FIX: Handle animated opacity + if hasattr(stroke.opacity, 'keyframes') and stroke.opacity.keyframes: + # Animated opacity + keyframes = [] + for kf in stroke.opacity.keyframes: + kf_data = { + "t": kf.time, + "s": [kf.value] # Opacity value as single-element array + } + + # Add easing parameters if present + if hasattr(kf, 'in_tan') and kf.in_tan: + if isinstance(kf.in_tan, dict): + kf_data["i"] = { + "x": [kf.in_tan.get('x', 0.833)], + "y": [kf.in_tan.get('y', 0.833)] + } + else: + kf_data["i"] = kf.in_tan + + if hasattr(kf, 'out_tan') and kf.out_tan: + if isinstance(kf.out_tan, dict): + kf_data["o"] = { + "x": [kf.out_tan.get('x', 0.167)], + "y": [kf.out_tan.get('y', 0.167)] + } + else: + kf_data["o"] = kf.out_tan + + keyframes.append(kf_data) + + json_data["o"] = { + "a": 1, + "k": keyframes, + "ix": 4 + } + else: + # Static opacity + json_data["o"] = { + "a": 0, + "k": stroke.opacity.value, + "ix": 4 + } + + # Width - FIX: Handle animated width + if hasattr(stroke.width, 'keyframes') and stroke.width.keyframes: + # Animated width + keyframes = [] + for kf in stroke.width.keyframes: + kf_data = { + "t": kf.time, + "s": [kf.value] # Width value as single-element array + } + + # Add easing parameters if present + if hasattr(kf, 'in_tan') and kf.in_tan: + if isinstance(kf.in_tan, dict): + kf_data["i"] = { + "x": [kf.in_tan.get('x', 0.833)], + "y": [kf.in_tan.get('y', 0.833)] + } + else: + kf_data["i"] = kf.in_tan + + if hasattr(kf, 'out_tan') and kf.out_tan: + if isinstance(kf.out_tan, dict): + kf_data["o"] = { + "x": [kf.out_tan.get('x', 0.167)], + "y": [kf.out_tan.get('y', 0.167)] + } + else: + kf_data["o"] = kf.out_tan + + keyframes.append(kf_data) + + json_data["w"] = { + "a": 1, + "k": keyframes, + "ix": 5 + } + else: + # Static width + json_data["w"] = { + "a": 0, + "k": stroke.width.value, + "ix": 5 + } + + # Line cap and join + json_data["lc"] = stroke.line_cap.value if hasattr(stroke, "line_cap") and stroke.line_cap else 2 + json_data["lj"] = stroke.line_join.value if hasattr(stroke, "line_join") and stroke.line_join else 2 + + # Miter limit - FIX: Output ml whenever it exists, regardless of line_join value + if hasattr(stroke, "miter_limit") and stroke.miter_limit is not None: + json_data["ml"] = stroke.miter_limit + + # ml2 + if hasattr(stroke, "ml2") and stroke.ml2 is not None: + if isinstance(stroke.ml2, Value): + if hasattr(stroke.ml2, 'keyframes') and stroke.ml2.keyframes: + # Animated ml2 + keyframes = [] + for kf in stroke.ml2.keyframes: + kf_data = { + "t": kf.time, + "s": [kf.value] if not isinstance(kf.value, list) else kf.value + } + + if hasattr(kf, 'in_tan') and kf.in_tan: + kf_data["i"] = kf.in_tan + if hasattr(kf, 'out_tan') and kf.out_tan: + kf_data["o"] = kf.out_tan + + keyframes.append(kf_data) + + ml2_json = {"a": 1, "k": keyframes} + else: + ml2_json = {"a": 0, "k": stroke.ml2.value} + + if hasattr(stroke, 'ml2_ix') and stroke.ml2_ix is not None: + ml2_json["ix"] = stroke.ml2_ix + + json_data["ml2"] = ml2_json + else: + json_data["ml2"] = stroke.ml2 + + # Dashes - FIX: Handle animated dash lengths + if hasattr(stroke, "dashes") and stroke.dashes: + dash_array = [] + for i, dash in enumerate(stroke.dashes): + dash_item = {"n": dash.type.value if hasattr(dash.type, 'value') else dash.type} + + # Check if dash length is animated + if hasattr(dash.length, 'keyframes') and dash.length.keyframes: + # Animated dash length + keyframes = [] + for kf in dash.length.keyframes: + kf_data = { + "t": kf.time, + "s": [kf.value] + } + + # ADD: Include hold property if present + if hasattr(kf, 'hold'): + kf_data["h"] = kf.hold + + # Only add easing if not a hold keyframe + if not hasattr(kf, 'hold') or not kf.hold: + if hasattr(kf, 'in_tan') and kf.in_tan: + if isinstance(kf.in_tan, dict): + kf_data["i"] = { + "x": [kf.in_tan.get('x', 0.833)], + "y": [kf.in_tan.get('y', 0.833)] + } + + if hasattr(kf, 'out_tan') and kf.out_tan: + if isinstance(kf.out_tan, dict): + kf_data["o"] = { + "x": [kf.out_tan.get('x', 0.167)], + "y": [kf.out_tan.get('y', 0.167)] + } + + keyframes.append(kf_data) + + dash_item["v"] = { + "a": 1, + "k": keyframes + } + else: + # Static dash length + dash_item["v"] = { + "a": 0, + "k": dash.length.value if hasattr(dash.length, 'value') else dash.length + } + + # Add name + if hasattr(dash, 'name') and dash.name: + dash_item["nm"] = dash.name + else: + if dash.type == StrokeDashType.Dash or dash.type == "d": + dash_item["nm"] = "dash" + elif dash.type == StrokeDashType.Gap or dash.type == "g": + dash_item["nm"] = "gap" + elif dash.type == StrokeDashType.Offset or dash.type == "o": + dash_item["nm"] = "offset" + + if hasattr(dash, 'v_ix'): + dash_item["v"]["ix"] = dash.v_ix + else: + dash_item["v"]["ix"] = i + 1 + + dash_array.append(dash_item) + + json_data["d"] = dash_array + + # ix + if hasattr(stroke, 'property_index') and stroke.property_index is not None: + json_data["ix"] = stroke.property_index + + return json_data + +def fill_to_json(fill): + """Fixed: Support animated colors and variable color dimensions""" + json_data = { + "ty": ElementType.FILL, + "bm": 0, + "nm": fill.name, + "mn": getattr(fill, 'mn', "ADBE Vector Graphic - Fill"), + "hd": getattr(fill, 'hd', False) + } + + # Color - + if hasattr(fill.color, 'keyframes') and fill.color.keyframes: + # + keyframes = [] + for kf in fill.color.keyframes: + # + color_dim = getattr(fill, 'color_dimensions', 4) + if color_dim == 2: + s_data = [kf.value.r, kf.value.g] + elif color_dim == 3: + s_data = [kf.value.r, kf.value.g, kf.value.b] + else: + s_data = [kf.value.r, kf.value.g, kf.value.b, 1] + + kf_data = { + "t": kf.time, + "s": s_data + } + if hasattr(kf, 'in_tan') and kf.in_tan: + kf_data["i"] = kf.in_tan + if hasattr(kf, 'out_tan') and kf.out_tan: + kf_data["o"] = kf.out_tan + keyframes.append(kf_data) + + json_data["c"] = {"a": 1, "k": keyframes} + + # ix + if hasattr(fill, 'has_c_ix') and fill.has_c_ix: + json_data["c"]["ix"] = getattr(fill, 'c_ix', 4) + else: + # FIX: Respect actual color dimensions + color_dim = getattr(fill, 'color_dimensions', 3) + has_c_a = getattr(fill, 'has_c_a', False) + has_c_ix = getattr(fill, 'has_c_ix', False) + + color_array = [] + if color_dim == 1: + color_array = [fill.color.value.r] + elif color_dim == 2: + color_array = [fill.color.value.r, fill.color.value.g] + elif color_dim == 3: + color_array = [fill.color.value.r, fill.color.value.g, fill.color.value.b] + else: # 4 or more + color_array = [fill.color.value.r, fill.color.value.g, fill.color.value.b, 1] + + json_data["c"] = {"k": color_array} + if has_c_a: + json_data["c"]["a"] = 0 + if has_c_ix: + json_data["c"]["ix"] = getattr(fill, 'c_ix', 4) + + # Opacity + json_data["o"] = { + "a": 0, + "k": fill.opacity.value, + "ix": 5 + } + + # Check if opacity is animated + if hasattr(fill.opacity, 'keyframes') and fill.opacity.keyframes: + json_data["o"]["a"] = 1 + keyframes = [] + for kf in fill.opacity.keyframes: + keyframe = {"t": kf.time, "s": [kf.value]} + if hasattr(kf, 'in_tan') and kf.in_tan: + keyframe["i"] = {"x": [kf.in_tan['x']], "y": [kf.in_tan['y']]} + if hasattr(kf, 'out_tan') and kf.out_tan: + keyframe["o"] = {"x": [kf.out_tan['x']], "y": [kf.out_tan['y']]} + keyframes.append(keyframe) + json_data["o"]["k"] = keyframes + + # Fill rule + json_data["r"] = fill.fill_rule.value if hasattr(fill, "fill_rule") and fill.fill_rule is not None else 1 + + # Add property index to fill element itself if it exists + if hasattr(fill, 'property_index') and fill.property_index is not None: + json_data["ix"] = fill.property_index + + return json_data + + +def gradient_fill_to_json(gradient_fill): + """JSON - """ + json_data = { + "ty": ElementType.GRADIENT_FILL, + "nm": gradient_fill.name, + "o": {"a": 0, "k": gradient_fill.opacity.value if hasattr(gradient_fill, "opacity") else 100, "ix": 10}, + "r": gradient_fill.fill_rule.value if hasattr(gradient_fill, "fill_rule") and gradient_fill.fill_rule is not None else 1, + "bm": 0, + "mn": "ADBE Vector Graphic - G-Fill", + "hd": False + } + + # Add property index if exists + if hasattr(gradient_fill, 'property_index') and gradient_fill.property_index is not None: + json_data["ix"] = gradient_fill.property_index + + # Start and end points + if hasattr(gradient_fill, "start_point"): + json_data["s"] = property_to_json(gradient_fill.start_point, [0, 0], "ix", 5) + else: + json_data["s"] = {"a": 0, "k": [0, 0], "ix": 5} + + if hasattr(gradient_fill, "end_point"): + json_data["e"] = property_to_json(gradient_fill.end_point, [100, 100], "ix", 6) + else: + json_data["e"] = {"a": 0, "k": [100, 100], "ix": 6} + + # Gradient type + gradient_type = 1 # Default linear + if hasattr(gradient_fill, "gradient_type"): + gradient_type = gradient_fill.gradient_type.value + json_data["t"] = gradient_type + else: + json_data["t"] = 1 + + # FIX: Always add h and a attributes if they exist + if hasattr(gradient_fill, "highlight_length"): + json_data["h"] = property_to_json(gradient_fill.highlight_length, 0, "ix", 7) + + if hasattr(gradient_fill, "highlight_angle"): + json_data["a"] = property_to_json(gradient_fill.highlight_angle, 0, "ix", 8) + + # - + if hasattr(gradient_fill, "_original_color_array") and gradient_fill._original_color_array: + # + json_data["g"] = { + "p": gradient_fill._color_points, + "k": { + "a": 0, + "k": gradient_fill._original_color_array, + "ix": 9 + } + } + elif hasattr(gradient_fill, "colors") and gradient_fill.colors is not None: + # + if hasattr(gradient_fill.colors, "colors"): + colors_list = gradient_fill.colors.colors + + if isinstance(colors_list, list) and colors_list: + flat_colors = [] + num_colors = len(colors_list) + + # 4rgb + for pos, color in colors_list: + flat_colors.extend([pos, color.r, color.g, color.b]) + + json_data["g"] = { + "p": num_colors, + "k": { + "a": 0, + "k": flat_colors, + "ix": 9 + } + } + else: + # + json_data["g"] = { + "p": 3, + "k": { + "a": 0, + "k": [0.0, 0.85, 0.36, 0.33, 0.5, 0.84, 1.0, 0.0, 1.0, 0.85, 0.36, 0.33], + "ix": 9 + } + } + else: + # + json_data["g"] = { + "p": 3, + "k": { + "a": 0, + "k": [0.0, 0.85, 0.36, 0.33, 0.5, 0.84, 1.0, 0.0, 1.0, 0.85, 0.36, 0.33], + "ix": 9 + } + } + + return json_data + + +def gradient_stroke_to_json(shape): + """JSON - """ + json_data = { + "ty": ElementType.GRADIENT_STROKE, + "nm": shape.name, + "bm": 0, + "mn": "ADBE Vector Graphic - G-Stroke", + "hd": False + } + + # property_index + if hasattr(shape, 'property_index') and shape.property_index is not None: + json_data["ix"] = shape.property_index + + # - ix + json_data["o"] = { + "a": 0, + "k": shape.opacity.value if hasattr(shape, "opacity") else 100, + "ix": 9 # ix + } + + # - ix + json_data["w"] = { + "a": 0, + "k": shape.width.value if hasattr(shape, "width") else 14, # + "ix": 10 # ix + } + + # - + json_data["lc"] = shape.line_cap.value if hasattr(shape, "line_cap") and shape.line_cap is not None else 1 + json_data["lj"] = shape.line_join.value if hasattr(shape, "line_join") and shape.line_join is not None else 1 + + # + if hasattr(shape, "miter_limit") and shape.miter_limit is not None: + json_data["ml"] = shape.miter_limit + + # ml2 + if hasattr(shape, "ml2") and shape.ml2 is not None: + if isinstance(shape.ml2, Value): + ml2_json = {"a": 0, "k": shape.ml2.value} + if hasattr(shape, 'ml2_ix'): + ml2_json["ix"] = shape.ml2_ix + json_data["ml2"] = ml2_json + else: + json_data["ml2"] = shape.ml2 + + # - ix + if hasattr(shape, "start_point"): + json_data["s"] = property_to_json(shape.start_point, [0, 0], "ix", 4) + else: + json_data["s"] = {"a": 0, "k": [0, 0], "ix": 4} + + # - ix + if hasattr(shape, "end_point"): + json_data["e"] = property_to_json(shape.end_point, [100, 0], "ix", 5) # + else: + json_data["e"] = {"a": 0, "k": [100, 0], "ix": 5} # + + # + json_data["t"] = shape.gradient_type.value if hasattr(shape, "gradient_type") else 1 + + # + if hasattr(shape, "highlight_length"): + json_data["h"] = property_to_json(shape.highlight_length, 0, "ix", 7) + else: + json_data["h"] = {"a": 0, "k": 0, "ix": 7} + + if hasattr(shape, "highlight_angle"): + json_data["a"] = property_to_json(shape.highlight_angle, 0, "ix", 8) + else: + json_data["a"] = {"a": 0, "k": 0, "ix": 8} + + # - + if hasattr(shape, "_original_g_data") and shape._original_g_data: + # + json_data["g"] = shape._original_g_data + elif hasattr(shape, "_original_color_array") and shape._original_color_array: + # + json_data["g"] = { + "p": shape._color_points, + "k": { + "a": 0, + "k": shape._original_color_array, + "ix": 8 # 89 + } + } + elif hasattr(shape, "colors") and shape.colors: + # ... ... + json_data["g"] = { + "p": len(colors_list) if 'colors_list' in locals() else 3, + "k": { + "a": 0, + "k": flat_colors if 'flat_colors' in locals() else [0, 0, 0, 0, 0.5, 1, 1, 1, 1, 0, 0, 0], + "ix": 8 # 89 + } + } + else: + # - 312 + json_data["g"] = { + "p": 3, + "k": { + "a": 0, + "k": [0, 0, 0, 0, 0.5, 1, 1, 1, 1, 0, 0, 0], + "ix": 8 # 89 + } + } + + # + if hasattr(shape, "dashes") and shape.dashes: + dash_array = [] + for dash in shape.dashes: + dash_item = { + "n": dash.type.value, + "v": {"a": 0, "k": dash.length.value} + } + dash_array.append(dash_item) + json_data["d"] = dash_array + + return json_data + +# - JSON +def merge_to_json(merge): + """JSON""" + json_data = { + "ty": ElementType.MERGE, + "nm": merge.name, + "mm": merge.merge_mode, + "mn": "ADBE Vector Filter - Merge", + "hd": False + } + + if hasattr(merge, "property_index") and merge.property_index is not None: + json_data["ix"] = merge.property_index + + return json_data + +# - JSON +def rounded_corners_to_json(rounded_corners): + """JSON""" + json_data = { + "ty": ElementType.ROUNDED_CORNERS, + "nm": rounded_corners.name, + "mn": "ADBE Vector Filter - RC", + "hd": False + } + + if hasattr(rounded_corners, "property_index") and rounded_corners.property_index is not None: + json_data["ix"] = rounded_corners.property_index + + # + if hasattr(rounded_corners, "radius"): + json_data["r"] = property_to_json(rounded_corners.radius, 0, "ix", 1) + + return json_data + +# - JSON +def twist_to_json(twist): + """JSON""" + json_data = { + "ty": ElementType.TWIST, + "nm": twist.name, + "mn": "ADBE Vector Filter - Twist", + "hd": False + } + + if hasattr(twist, "property_index") and twist.property_index is not None: + json_data["ix"] = twist.property_index + + # + if hasattr(twist, "angle"): + json_data["a"] = property_to_json(twist.angle, 0, "ix", 1) + + # + if hasattr(twist, "center"): + json_data["c"] = property_to_json(twist.center, [0, 0], "ix", 2) + + return json_data + + +def trim_to_json(trim): + """Convert a Trim object to JSON, properly handling animations""" + json_data = { + "ty": ElementType.TRIM, + "nm": trim.name, + "ix": trim.property_index, + "mn": "ADBE Vector Filter - Trim", + "hd": False + } + + # + if hasattr(trim, "multiple"): + json_data["m"] = trim.multiple.value + else: + json_data["m"] = 1 # + + # Start value + if hasattr(trim, "start") and trim.start: + json_data["s"] = property_to_json(trim.start, 0, "ix", 1) + + # End value + if hasattr(trim, "end") and trim.end: + json_data["e"] = property_to_json(trim.end, 100, "ix", 2) + + # Offset value + if hasattr(trim, "offset") and trim.offset: + json_data["o"] = property_to_json(trim.offset, 0, "ix", 3) + + return json_data + +def effects_to_json(effects): + """JSON""" + if not isinstance(effects, list): + effects = [effects] # + + json_effects = [] + + for effect in effects: + # effect + if isinstance(effect, dict) and "ty" in effect: + json_effect = { + "ty": effect.get("ty", 5), + "nm": effect.get("nm", ""), + "mn": effect.get("mn", ""), + "ix": effect.get("ix", 1), + "en": effect.get("en", 1) + } + + # np0 + if "np" in effect and effect["np"] > 0: + json_effect["np"] = effect["np"] + + # + if "ef" in effect: + json_effect["ef"] = effect["ef"] + + json_effects.append(json_effect) + + return json_effects + + +# JSON +def transform_shape_to_json(transform): + """Convert TransformShape to JSON with full separated properties support""" + json_data = { + "ty": "tr", + "nm": transform.name if hasattr(transform, 'name') else "Transform Shape" + } + + if hasattr(transform, 'property_index') and transform.property_index is not None: + json_data["ix"] = transform.property_index + + if hasattr(transform, 'hd') and transform.hd is not None: + json_data["hd"] = transform.hd + # Process anchor + if hasattr(transform, 'anchor') and transform.anchor is not None: + json_data["a"] = property_to_json(transform.anchor, [0, 0]) + if not hasattr(transform, '_is_shape_transform'): + json_data["a"]["ix"] = 1 + + # Process position + if hasattr(transform, 'position') and transform.position is not None: + json_data["p"] = property_to_json(transform.position, [0, 0]) + if not hasattr(transform, '_is_shape_transform'): + json_data["p"]["ix"] = 2 + + # Process scale - FIXED: Properly output separated properties + if hasattr(transform, 'scale') and transform.scale is not None: + if hasattr(transform.scale, 'separated') and transform.scale.separated: + # Output separated scale + scale_json = {"s": True} + + if hasattr(transform.scale, 'x') and transform.scale.x is not None: + scale_json["x"] = property_to_json(transform.scale.x, 100) + if hasattr(transform.scale, 'x_ix'): + scale_json["x"]["ix"] = transform.scale.x_ix + else: + scale_json["x"]["ix"] = 3 + + if hasattr(transform.scale, 'y') and transform.scale.y is not None: + scale_json["y"] = property_to_json(transform.scale.y, 100) + if hasattr(transform.scale, 'y_ix'): + scale_json["y"]["ix"] = transform.scale.y_ix + else: + scale_json["y"]["ix"] = 4 + + if hasattr(transform.scale, 'z') and transform.scale.z is not None: + scale_json["z"] = property_to_json(transform.scale.z, 100) + if hasattr(transform.scale, 'z_ix'): + scale_json["z"]["ix"] = transform.scale.z_ix + else: + scale_json["z"]["ix"] = 5 + + if hasattr(transform.scale, 'ix'): + scale_json["ix"] = transform.scale.ix + elif not hasattr(transform, '_is_shape_transform'): + scale_json["ix"] = 3 + + json_data["s"] = scale_json + else: + scale_json = property_to_json(transform.scale, [100, 100, 100]) + if not hasattr(transform, '_is_shape_transform'): + scale_json["ix"] = 3 + json_data["s"] = scale_json + + # Process rotation - FIXED: Handle separated rotation + if hasattr(transform, 'rotation') and transform.rotation is not None: + if hasattr(transform.rotation, 'separated') and transform.rotation.separated: + # Output separated rotation + rot_json = {"s": True} + + if hasattr(transform.rotation, 'x') and transform.rotation.x is not None: + rot_json["x"] = property_to_json(transform.rotation.x, 0) + if hasattr(transform.rotation, 'x_ix'): + rot_json["x"]["ix"] = transform.rotation.x_ix + + if hasattr(transform.rotation, 'y') and transform.rotation.y is not None: + rot_json["y"] = property_to_json(transform.rotation.y, 0) + if hasattr(transform.rotation, 'y_ix'): + rot_json["y"]["ix"] = transform.rotation.y_ix + + if hasattr(transform.rotation, 'z') and transform.rotation.z is not None: + rot_json["z"] = property_to_json(transform.rotation.z, 0) + if hasattr(transform.rotation, 'z_ix'): + rot_json["z"]["ix"] = transform.rotation.z_ix + + if hasattr(transform.rotation, 'ix'): + rot_json["ix"] = transform.rotation.ix + elif not hasattr(transform, '_is_shape_transform'): + rot_json["ix"] = 6 + + json_data["r"] = rot_json + else: + json_data["r"] = property_to_json(transform.rotation, 0) + if not hasattr(transform, '_is_shape_transform'): + json_data["r"]["ix"] = 6 + + # Process opacity + if hasattr(transform, 'opacity') and transform.opacity is not None: + json_data["o"] = property_to_json(transform.opacity, 100) + if not hasattr(transform, '_is_shape_transform'): + json_data["o"]["ix"] = 7 + + # Process skew + if hasattr(transform, 'skew') and transform.skew is not None: + json_data["sk"] = property_to_json(transform.skew, 0) + if not hasattr(transform, '_is_shape_transform'): + json_data["sk"]["ix"] = 4 + + # Process skew axis + if hasattr(transform, 'skew_axis') and transform.skew_axis is not None: + json_data["sa"] = property_to_json(transform.skew_axis, 0) + if not hasattr(transform, '_is_shape_transform'): + json_data["sa"]["ix"] = 5 + + return json_data + + +# To use the function: +def from_sequence(sequence_text): + """ + + Args: + sequence_text + Returns: + """ + lines = sequence_text.strip().split('\n') + + idx = 0 + animation, idx = parse_animation_tag(lines, idx) + return animation + + + + + +def parse_asset_tag(lines, idx): + """asset""" + if not lines[idx].startswith('(asset'): + raise ValueError(f"Expected asset tag, got: {lines[idx]}") + + asset_attrs = parse_tag_attrs(lines[idx]) + + asset = { + "id": asset_attrs.get("id", ""), + "layers": [] + } + + # + if "nm" in asset_attrs: + asset["nm"] = asset_attrs["nm"] + if "fr" in asset_attrs: + asset["fr"] = float(asset_attrs["fr"]) + if "w" in asset_attrs: + asset["w"] = float(asset_attrs["w"]) + if "h" in asset_attrs: + asset["h"] = float(asset_attrs["h"]) + if "u" in asset_attrs: + asset["u"] = asset_attrs["u"] + if "p" in asset_attrs: + asset["p"] = asset_attrs["p"] + if "e" in asset_attrs: + asset["e"] = int(asset_attrs["e"]) + + idx += 1 + while idx < len(lines): + if lines[idx].startswith('(layer') or lines[idx].startswith('(shape_layer'): + layer, new_idx = parse_layer_tag(lines, idx) + # assetlayer + if hasattr(layer, '__dict__'): + layer._is_asset_layer = True + asset["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(null_layer'): + layer, new_idx = parse_layer_tag(lines, idx) + if hasattr(layer, '__dict__'): + layer._is_asset_layer = True + asset["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(precomp_layer'): + layer, new_idx = parse_precomp_layer_tag(lines, idx) + if hasattr(layer, '__dict__'): + layer._is_asset_layer = True + asset["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(text_layer'): # ADD THIS CASE FOR TEXT LAYERS + layer, new_idx = parse_text_layer_tag(lines, idx) + if hasattr(layer, '__dict__'): + layer._is_asset_layer = True + asset["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(solid_layer'): + layer, new_idx = parse_solid_layer_tag(lines, idx) + if hasattr(layer, '__dict__'): + layer._is_asset_layer = True + asset["layers"].append(layer) + idx = new_idx + elif lines[idx].strip() == '(/asset)': + idx += 1 + break + else: + idx += 1 + + return asset, idx + + +def parse_animation_tag(lines, idx): + """Parse animation tag and its contents""" + if not lines[idx].startswith('(animation'): + raise ValueError(f"Expected animation tag, got: {lines[idx]}") + + attrs = parse_tag_attrs(lines[idx]) + + animation = { + "v": attrs.get("v", "5.0.0"), + "fr": float(attrs.get("fr", 30)), + "ip": float(attrs.get("ip", 0)), + "op": float(attrs.get("op", 30)), + "w": float(attrs.get("w", 360)), + "h": float(attrs.get("h", 360)), + "nm": attrs.get("nm", "Animation"), + "ddd": int(attrs.get("ddd", 0)), + "assets": [], + "layers": [], + "markers": [], # Initialize as empty list + "props": {}, # Initialize as empty dict + "fonts": None, # fonts + "chars": [] # chars + } + + idx += 1 + while idx < len(lines): + if lines[idx].startswith('(markers'): + # Parse markers properly + markers_line = lines[idx] + start = markers_line.find(' ') + 1 + end = markers_line.rfind(')') + if start > 0 and end > start: + markers_content = markers_line[start:end].strip() + if markers_content and markers_content != "[]": + try: + animation["markers"] = json.loads(markers_content) + except: + animation["markers"] = [] + else: + animation["markers"] = [] + idx += 1 + elif lines[idx].startswith('(props'): + # Parse props properly + props_line = lines[idx] + start = props_line.find(' ') + 1 + end = props_line.rfind(')') + if start > 0 and end > start: + props_content = props_line[start:end].strip() + if props_content and props_content != "{}": + try: + animation["props"] = json.loads(props_content) + except: + animation["props"] = {} + idx += 1 + elif lines[idx].startswith('(fonts'): + # fonts + fonts_list = [] + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/fonts'): + if lines[idx].startswith('(font'): + font_attrs = parse_tag_attrs(lines[idx]) + font = Font( + font_family=font_attrs.get("family", ""), + font_style=font_attrs.get("style", "Regular"), + name=font_attrs.get("name", "") + ) + if "ascent" in font_attrs: + font.ascent = float(font_attrs["ascent"]) + if "path" in font_attrs: + font.path = font_attrs["path"] + if "weight" in font_attrs: + font.weight = font_attrs["weight"] + if "origin" in font_attrs: + font.origin = int(font_attrs["origin"]) + fonts_list.append(font) + idx += 1 + animation["fonts"] = {"list": fonts_list} + if lines[idx].startswith('(/fonts'): + idx += 1 + + elif lines[idx].startswith('(chars'): + # chars + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/chars'): + if lines[idx].startswith('(char '): + char_attrs = parse_tag_attrs(lines[idx]) + char = Chars() + char.character = char_attrs.get("ch", "") + char.font_family = char_attrs.get("family", "") + char.font_size = float(char_attrs.get("size", 0)) + char.font_style = char_attrs.get("style", "") + char.width = float(char_attrs.get("w", 0)) + + idx += 1 + # character shapes + if idx < len(lines) and lines[idx].startswith('(char_shapes'): + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/char_shapes'): + # Parse shape elements for chars + if lines[idx].startswith('(group'): + shape, new_idx = parse_group_tag(lines, idx) + char.data.shapes.append(shape) + idx = new_idx + elif lines[idx].startswith('(path'): + shape, new_idx = parse_path_tag(lines, idx) + char.data.shapes.append(shape) + idx = new_idx + elif lines[idx].startswith('(fill'): + shape, new_idx = parse_fill_tag(lines, idx) + char.data.shapes.append(shape) + idx = new_idx + elif lines[idx].startswith('(stroke'): + shape, new_idx = parse_stroke_tag(lines, idx) + char.data.shapes.append(shape) + idx = new_idx + elif lines[idx].startswith('(rect'): + shape, new_idx = parse_rect_tag(lines, idx) + char.data.shapes.append(shape) + idx = new_idx + elif lines[idx].startswith('(ellipse'): + shape, new_idx = parse_ellipse_tag(lines, idx) + char.data.shapes.append(shape) + idx = new_idx + else: + idx += 1 + if lines[idx].startswith('(/char_shapes'): + idx += 1 + + animation["chars"].append(char) + + if idx < len(lines) and lines[idx].startswith('(/char'): + idx += 1 + else: + idx += 1 + + elif lines[idx].startswith('(/chars'): + idx += 1 + + elif lines[idx].startswith('(asset'): + asset, new_idx = parse_asset_tag(lines, idx) + animation["assets"].append(asset) + idx = new_idx + elif lines[idx].startswith('(layer') or lines[idx].startswith('(shape_layer'): + layer, new_idx = parse_layer_tag(lines, idx) + animation["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(null_layer'): + layer, new_idx = parse_layer_tag(lines, idx) + animation["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(precomp_layer'): + layer, new_idx = parse_precomp_layer_tag(lines, idx) + animation["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(text_layer'): + layer, new_idx = parse_text_layer_tag(lines, idx) + animation["layers"].append(layer) + idx = new_idx + elif lines[idx].startswith('(solid_layer'): # Add this + layer, new_idx = parse_solid_layer_tag(lines, idx) + animation["layers"].append(layer) + idx = new_idx + else: + idx += 1 + + asset_map = {asset.get('id'): asset for asset in animation["assets"] if 'id' in asset} + for layer in animation["layers"]: + if isinstance(layer, PreCompLayer) and hasattr(layer, 'reference_id'): + ref_id = layer.reference_id + if ref_id in asset_map: + layer.referenced_asset = asset_map[ref_id] + + # Build parent-child relationships + layer_map = {layer.index: layer for layer in animation["layers"] if hasattr(layer, 'index')} + for layer in animation["layers"]: + if hasattr(layer, 'parent_index') and layer.parent_index is not None: + parent_index = layer.parent_index + if parent_index in layer_map: + layer.parent = layer_map[parent_index] + + for asset in animation["assets"]: + if "layers" in asset: + layer_map = {layer.index: layer for layer in asset["layers"] if hasattr(layer, 'index')} + for layer in asset["layers"]: + if hasattr(layer, 'parent_index') and layer.parent_index is not None: + parent_index = layer.parent_index + if parent_index in layer_map: + layer.parent = layer_map[parent_index] + + return animation, idx + + +def parse_layer_tag(lines, idx): + """""" + if not (lines[idx].startswith('(layer') or lines[idx].startswith('(null_layer')): + raise ValueError(f"Expected layer tag, got: {lines[idx]}") + + layer_attrs = parse_tag_attrs(lines[idx]) + + # + if 'null_layer' in lines[idx]: + layer = NullLayer() + layer.type = ElementType.NULL_LAYER + else: + layer = ShapeLayer() + + # + layer.index = int(float(layer_attrs.get("index", 0))) + layer.name = layer_attrs.get("name", "Layer") + layer.in_point = float(layer_attrs.get("in_point", 0)) + layer.out_point = float(layer_attrs.get("out_point", 60)) + layer.start_time = float(layer_attrs.get("start_time", 0)) + + # Parse optional ShapeLayer attributes - only set if present + if "ddd" in layer_attrs: + layer.ddd = int(layer_attrs["ddd"]) + + if "hd" in layer_attrs: + layer.hd = layer_attrs["hd"].lower() == 'true' + + if "cp" in layer_attrs: + layer.cp = layer_attrs["cp"].lower() == 'true' + + if "cl" in layer_attrs: + cl_value = layer_attrs["cl"] + if cl_value and cl_value != '""': + layer.cl = cl_value.strip('"') + + if "ao" in layer_attrs: + layer.ao = int(layer_attrs["ao"]) + + # Parse track matte attributes - only set if present + if "tt" in layer_attrs: + layer.tt = int(layer_attrs["tt"]) + + if "tp" in layer_attrs: + tp_value = layer_attrs["tp"] + if tp_value and tp_value.isdigit(): + layer.tp = int(tp_value) + else: + layer.tp = tp_value + + if "td" in layer_attrs: + layer.td = int(layer_attrs["td"]) + + if "ct" in layer_attrs: + layer.ct = int(layer_attrs["ct"]) + + if "hasMask" in layer_attrs: + layer.hasMask = layer_attrs["hasMask"].lower() == 'true' + + + + # is_asset + if layer_attrs.get("is_asset", "").lower() == "true": + layer._is_asset_layer = True + + idx += 1 + while idx < len(lines): + line = lines[idx] + if line.startswith('(parent'): + # parent + parent_index = extract_number(line) + layer.parent_index = int(parent_index) + idx += 1 + elif line.strip() == '(transform)': + transform, new_idx = parse_transform_tag(lines, idx) + layer.transform = transform + idx = new_idx + # + elif line.startswith('(group'): + group, new_idx = parse_group_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(group) + elif hasattr(layer, 'shapes'): + layer.shapes.append(group) + idx = new_idx + elif line.startswith('(path'): + path, new_idx = parse_path_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(path) + elif hasattr(layer, 'shapes'): + layer.shapes.append(path) + idx = new_idx + elif line.startswith('(fill'): + fill, new_idx = parse_fill_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(fill) + elif hasattr(layer, 'shapes'): + layer.shapes.append(fill) + idx = new_idx + elif line.startswith('(stroke'): + stroke, new_idx = parse_stroke_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(stroke) + elif hasattr(layer, 'shapes'): + layer.shapes.append(stroke) + idx = new_idx + elif line.startswith('(rect'): + rect, new_idx = parse_rect_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(rect) + elif hasattr(layer, 'shapes'): + layer.shapes.append(rect) + idx = new_idx + elif line.startswith('(ellipse'): + ellipse, new_idx = parse_ellipse_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(ellipse) + elif hasattr(layer, 'shapes'): + layer.shapes.append(ellipse) + idx = new_idx + elif line.startswith('(star'): + star, new_idx = parse_star_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(star) + elif hasattr(layer, 'shapes'): + layer.shapes.append(star) + idx = new_idx + elif line.startswith('(trim'): + trim, new_idx = parse_trim_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(trim) + elif hasattr(layer, 'shapes'): + layer.shapes.append(trim) + idx = new_idx + elif line.startswith('(repeater'): + repeater, new_idx = parse_repeater_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(repeater) + elif hasattr(layer, 'shapes'): + layer.shapes.append(repeater) + idx = new_idx + elif line.startswith('(gradient_fill'): + gradient_fill, new_idx = parse_gradient_fill_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(gradient_fill) + elif hasattr(layer, 'shapes'): + layer.shapes.append(gradient_fill) + idx = new_idx + elif line.startswith('(gradient_stroke'): + gradient_stroke, new_idx = parse_gradient_stroke_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(gradient_stroke) + elif hasattr(layer, 'shapes'): + layer.shapes.append(gradient_stroke) + idx = new_idx + elif line.startswith('(merge'): + merge, new_idx = parse_merge_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(merge) + elif hasattr(layer, 'shapes'): + layer.shapes.append(merge) + idx = new_idx + elif line.startswith('(rounded_corners'): + rounded_corners, new_idx = parse_rounded_corners_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(rounded_corners) + elif hasattr(layer, 'shapes'): + layer.shapes.append(rounded_corners) + idx = new_idx + elif line.startswith('(twist'): + twist, new_idx = parse_twist_tag(lines, idx) + if hasattr(layer, 'add_shape'): + layer.add_shape(twist) + elif hasattr(layer, 'shapes'): + layer.shapes.append(twist) + idx = new_idx + + elif lines[idx].startswith('(zig_zag'): + zig_zag, new_idx = parse_zig_zag_tag(lines, idx) + layer.shapes.append(zig_zag) + idx = new_idx + elif line.startswith('(tm '): + # tm - + tm_attrs = parse_tag_attrs(line) + #print("tm_attrs", tm_attrs) + tm_data = {} + if 'a' in tm_attrs: + tm_data['a'] = int(tm_attrs['a']) + if 'ix' in tm_attrs: + tm_data['ix'] = int(tm_attrs['ix']) + + # keyframes + idx += 1 + keyframes = [] + while idx < len(lines): + line = lines[idx] + if line.startswith('(keyframe'): + kf_attrs = parse_tag_attrs(line) + kf = {} + if 't' in kf_attrs: + kf['t'] = float(kf_attrs['t']) + if 's' in kf_attrs: + kf['s'] = [float(kf_attrs['s'])] + if 'h' in kf_attrs: + kf['h'] = int(kf_attrs['h']) + + # + if 'i_x' in kf_attrs or 'i_y' in kf_attrs: + kf['i'] = {} + if 'i_x' in kf_attrs: + kf['i']['x'] = [float(kf_attrs['i_x'])] + if 'i_y' in kf_attrs: + kf['i']['y'] = [float(kf_attrs['i_y'])] + + if 'o_x' in kf_attrs or 'o_y' in kf_attrs: + kf['o'] = {} + if 'o_x' in kf_attrs: + kf['o']['x'] = [float(kf_attrs['o_x'])] + if 'o_y' in kf_attrs: + kf['o']['y'] = [float(kf_attrs['o_y'])] + + keyframes.append(kf) + idx += 1 + elif line.startswith('(value'): + # + val = extract_number(line) + tm_data['k'] = val + idx += 1 + elif line.strip() == '(/tm)': + if keyframes: + tm_data['k'] = keyframes + idx += 1 + break + else: + idx += 1 + + layer.tm = tm_data + + elif lines[idx].startswith('(effects'): + # effects + effects, new_idx = parse_effects_tag(lines, idx) + layer.ef = effects + idx = new_idx + + + elif line.startswith('(tt'): + tt_value = extract_number(line) + layer.tt = int(tt_value) + idx += 1 + elif line.startswith('(tp'): + tp_value = extract_number(line) + layer.tp = int(tp_value) + idx += 1 + elif line.startswith('(td'): + td_value = extract_number(line) + layer.td = int(td_value) + idx += 1 + + elif line.startswith('(masksProperties'): + # masksProperties + layer.masksProperties = [] + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/masksProperties)'): + if lines[idx].startswith('(mask '): + # mask + mask_attrs = parse_tag_attrs(lines[idx]) + mask = {} + + # + if "inv" in mask_attrs: + mask["inv"] = mask_attrs["inv"].lower() == "true" + if "mode" in mask_attrs: + mask["mode"] = mask_attrs["mode"] + if "nm" in mask_attrs: + mask["nm"] = mask_attrs["nm"] + + idx += 1 + + # mask + while idx < len(lines) and not lines[idx].startswith('(/mask)'): + if lines[idx].startswith('(mask_pt '): + # pt () + pt_attrs = parse_tag_attrs(lines[idx]) + mask["pt"] = {} + if "a" in pt_attrs: + mask["pt"]["a"] = int(pt_attrs["a"]) + if "ix" in pt_attrs: + mask["pt"]["ix"] = int(pt_attrs["ix"]) + + idx += 1 + + # pt.k () + if idx < len(lines) and lines[idx].startswith('(mask_pt_k'): + if lines[idx].startswith('(mask_pt_k_array'): + # k + mask["pt"]["k"] = [] + idx += 1 + + # + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k_array)'): + if lines[idx].startswith('(mask_pt_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + keyframe = {} + + if "t" in kf_attrs: + keyframe["t"] = float(kf_attrs["t"]) + + idx += 1 + + # + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_keyframe)'): + if lines[idx].startswith('(mask_pt_kf_i'): + i_attrs = parse_tag_attrs(lines[idx]) + keyframe["i"] = { + "x": float(i_attrs.get("x", 0)), + "y": float(i_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_o'): + o_attrs = parse_tag_attrs(lines[idx]) + keyframe["o"] = { + "x": float(o_attrs.get("x", 0)), + "y": float(o_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_s'): + keyframe["s"] = [] + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_s)'): + if lines[idx].startswith('(mask_pt_kf_shape'): + shape_attrs = parse_tag_attrs(lines[idx]) + shape = {} + if "c" in shape_attrs: + shape["c"] = shape_attrs["c"].lower() == "true" + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_shape)'): + if lines[idx].startswith('(mask_pt_kf_shape_i'): + values = extract_numbers(lines[idx]) + shape["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["i"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_o'): + values = extract_numbers(lines[idx]) + shape["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["o"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_v'): + values = extract_numbers(lines[idx]) + shape["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_shape)'): + idx += 1 + keyframe["s"].append(shape) + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_s)'): + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_keyframe)'): + idx += 1 + + mask["pt"]["k"].append(keyframe) + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k_array)'): + idx += 1 + + else: + # kshape + mask["pt"]["k"] = {} + idx += 1 + + # shape + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k)'): + if lines[idx].startswith('(mask_pt_k_c'): + # closed + parts = lines[idx].split() + if len(parts) > 1: + mask["pt"]["k"]["c"] = parts[1].rstrip(')').lower() == "true" + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_i'): + # i + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["i"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_o'): + # o + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["o"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_v'): + # v + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k)'): + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/mask_pt)'): + idx += 1 + + elif lines[idx].startswith('(mask_o '): + # opacity + o_attrs = parse_tag_attrs(lines[idx]) + mask["o"] = {} + if "a" in o_attrs: + mask["o"]["a"] = int(o_attrs["a"]) + if "ix" in o_attrs: + mask["o"]["ix"] = int(o_attrs["ix"]) + + # + if "a" in o_attrs and int(o_attrs["a"]) == 1: + # + mask["o"]["k"] = [] + idx += 1 + while idx < len(lines) and lines[idx].startswith('(keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + keyframe = {} + if "t" in kf_attrs: + keyframe["t"] = float(kf_attrs["t"]) + if "s" in kf_attrs: + keyframe["s"] = [float(kf_attrs["s"])] + if "i_x" in kf_attrs and "i_y" in kf_attrs: + keyframe["i"] = { + "x": [float(kf_attrs["i_x"])], + "y": [float(kf_attrs["i_y"])] + } + if "o_x" in kf_attrs and "o_y" in kf_attrs: + keyframe["o"] = { + "x": [float(kf_attrs["o_x"])], + "y": [float(kf_attrs["o_y"])] + } + mask["o"]["k"].append(keyframe) + idx += 1 + else: + # + if "k" in o_attrs: + mask["o"]["k"] = float(o_attrs["k"]) + idx += 1 + + elif lines[idx].startswith('(mask_x '): + # dilate + x_attrs = parse_tag_attrs(lines[idx]) + mask["x"] = {} + if "a" in x_attrs: + mask["x"]["a"] = int(x_attrs["a"]) + if "ix" in x_attrs: + mask["x"]["ix"] = int(x_attrs["ix"]) + + # + if "a" in x_attrs and int(x_attrs["a"]) == 1: + # + mask["x"]["k"] = [] + idx += 1 + while idx < len(lines) and lines[idx].startswith('(keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + keyframe = {} + if "t" in kf_attrs: + keyframe["t"] = float(kf_attrs["t"]) + if "s" in kf_attrs: + keyframe["s"] = [float(kf_attrs["s"])] + if "i_x" in kf_attrs and "i_y" in kf_attrs: + keyframe["i"] = { + "x": [float(kf_attrs["i_x"])], + "y": [float(kf_attrs["i_y"])] + } + if "o_x" in kf_attrs and "o_y" in kf_attrs: + keyframe["o"] = { + "x": [float(kf_attrs["o_x"])], + "y": [float(kf_attrs["o_y"])] + } + mask["x"]["k"].append(keyframe) + idx += 1 + else: + # + if "k" in x_attrs: + mask["x"]["k"] = float(x_attrs["k"]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask)'): + idx += 1 + + layer.masksProperties.append(mask) + else: + idx += 1 + + if lines[idx].startswith('(/masksProperties)'): + idx += 1 + + + + elif lines[idx].strip() == '(/layer)' or lines[idx].strip() == '(/null_layer)': + idx += 1 + break + else: + idx += 1 + + return layer, idx + +def parse_transform_tag(lines, idx): + """Parse transform tag with support for single-line static properties""" + transform = Transform() + + # Initialize default values + transform.position = MultiDimensional(NVector(0, 0, 0)) + transform.scale = MultiDimensional(NVector(100, 100, 100)) + transform.rotation = Value(0) + transform.opacity = Value(100) + transform.anchor = MultiDimensional(NVector(0, 0, 0)) + + idx += 1 + while idx < len(lines): + line = lines[idx].strip() + + # Check if this line contains multiple static properties + if '(' in line and ')' in line and line.count('(') > 1: + # Parse multiple properties on the same line + import re + props = re.findall(r'\([^)]+\)', line) + for prop in props: + prop = prop.strip() + if prop.startswith('(position'): + components = extract_numbers(prop) + if components: + if len(components) == 1: + transform.position = MultiDimensional(NVector(components[0], 0, 0)) + elif len(components) == 2: + transform.position = MultiDimensional(NVector(components[0], components[1], 0)) + else: + transform.position = MultiDimensional(NVector(*components)) + elif prop.startswith('(scale'): + components = extract_numbers(prop) + if len(components) >= 3: + transform.scale = MultiDimensional(NVector(*components)) + elif len(components) == 2: + transform.scale = MultiDimensional(NVector(components[0], components[1], 100)) + elif len(components) == 1: + transform.scale = MultiDimensional(NVector(components[0], components[0], 100)) + elif prop.startswith('(rotation'): + value = extract_number(prop) + transform.rotation = Value(value) + elif prop.startswith('(opacity'): + value = extract_number(prop) + transform.opacity = Value(value) + elif prop.startswith('(anchor'): + components = extract_numbers(prop) + if components: + if len(components) == 1: + transform.anchor = MultiDimensional(NVector(components[0], 0, 0)) + else: + transform.anchor = MultiDimensional(NVector(*components)) + idx += 1 + continue + + # Add expression parsing + + if line.startswith('(position'): + if 'separated=true' in line: + # Handle separated position with animated components + transform.position = Value(NVector(0, 0, 0)) + transform.position.separated = True + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/position)'): + if lines[idx].startswith('(position_x'): + if 'animated=true' in lines[idx]: + # Parse animated x component + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/position_x)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 0)) + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + # Handle bracketed format for easing parameters + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + # Handle bracketed format for easing parameters + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + if 'h' in attrs: + kf.h = int(attrs['h']) + # Handle n and e attributes + if 'n' in attrs: + kf.n = attrs['n'] + if 'e' in attrs: + try: + kf.e = json.loads(attrs['e']) if attrs['e'].startswith('[') else float(attrs['e']) + except: + kf.e = attrs['e'] + + keyframes.append(kf) + idx += 1 + + if keyframes: + x_value = Value(keyframes[0].value if keyframes else 0) + x_value.keyframes = keyframes + transform.position.x = x_value + + if idx < len(lines) and lines[idx].startswith('(/position_x)'): + idx += 1 + else: + value = extract_number(lines[idx]) + transform.position.x = Value(value) + idx += 1 + elif lines[idx].startswith('(position_y'): + if 'animated=true' in lines[idx]: + # Parse animated y component + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/position_y)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 0)) + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + # Handle bracketed format for easing parameters + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + # Handle bracketed format for easing parameters + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + if 'h' in attrs: + kf.h = int(attrs['h']) + # Handle n and e attributes + if 'n' in attrs: + kf.n = attrs['n'] + if 'e' in attrs: + try: + kf.e = json.loads(attrs['e']) if attrs['e'].startswith('[') else float(attrs['e']) + except: + kf.e = attrs['e'] + + keyframes.append(kf) + idx += 1 + + if keyframes: + y_value = Value(keyframes[0].value if keyframes else 0) + y_value.keyframes = keyframes + transform.position.y = y_value + + if idx < len(lines) and lines[idx].startswith('(/position_y)'): + idx += 1 + else: + value = extract_number(lines[idx]) + transform.position.y = Value(value) + idx += 1 + elif lines[idx].startswith('(position_z'): + if 'animated=true' in lines[idx]: + # Parse animated z component + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/position_z)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 0)) + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + # Handle bracketed format for easing parameters + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + # Handle bracketed format for easing parameters + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + if 'h' in attrs: + kf.h = int(attrs['h']) + # Handle n and e attributes + if 'n' in attrs: + kf.n = attrs['n'] + if 'e' in attrs: + try: + kf.e = json.loads(attrs['e']) if attrs['e'].startswith('[') else float(attrs['e']) + except: + kf.e = attrs['e'] + + keyframes.append(kf) + idx += 1 + + if keyframes: + z_value = Value(keyframes[0].value if keyframes else 0) + z_value.keyframes = keyframes + transform.position.z = z_value + + if idx < len(lines) and lines[idx].startswith('(/position_z)'): + idx += 1 + else: + value = extract_number(lines[idx]) + transform.position.z = Value(value) + idx += 1 + else: + idx += 1 + if lines[idx].startswith('(/position)'): + idx += 1 + elif 'animated=true' in line: + # Regular animated position + idx += 1 + keyframes = [] + + while idx < len(lines) and not lines[idx].startswith('(/position)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + + time = float(attrs.get('t', 0)) + + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + if len(values) >= 3: + value = NVector(values[0], values[1], values[2]) + elif len(values) == 2: + value = NVector(values[0], values[1], 0) + else: + value = NVector(0, 0, 0) + else: + value = NVector(0, 0, 0) + + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + # Handle space-separated list format + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + i_y = [float(v) for v in i_y_str.split()] + # Handle bracketed format + elif i_x_str.startswith('[') and i_x_str.endswith(']'): + # Extract value from bracketed format [0.833] -> 0.833 + i_x = float(i_x_str[1:-1]) + i_y = float(i_y_str[1:-1]) + else: + i_x = float(i_x_str) + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + # Handle space-separated list format + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + o_y = [float(v) for v in o_y_str.split()] + # Handle bracketed format + elif o_x_str.startswith('[') and o_x_str.endswith(']'): + # Extract value from bracketed format [0.833] -> 0.833 + o_x = float(o_x_str[1:-1]) + o_y = float(o_y_str[1:-1]) + else: + o_x = float(o_x_str) + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + if 'to' in attrs: + try: + kf.to = json.loads(attrs['to']) + except: + pass + + if 'ti' in attrs: + try: + kf.ti = json.loads(attrs['ti']) + except: + pass + + if 'h' in attrs: + kf.h = int(attrs['h']) + # Handle n and e attributes + if 'n' in attrs: + kf.n = attrs['n'] + if 'e' in attrs: + try: + kf.e = json.loads(attrs['e']) if attrs['e'].startswith('[') else [float(x) for x in attrs['e'].split()] + except: + try: + kf.e = float(attrs['e']) + except: + kf.e = attrs['e'] + + keyframes.append(kf) + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/position)'): + idx += 1 + + if keyframes: + pos_value = MultiDimensional(keyframes[0].value) + pos_value.keyframes = keyframes + transform.position = pos_value + else: + components = extract_numbers(line) + if components: + if len(components) == 1: + transform.position = MultiDimensional(NVector(components[0], 0, 0)) + elif len(components) == 2: + transform.position = MultiDimensional(NVector(components[0], components[1], 0)) + else: + transform.position = MultiDimensional(NVector(*components)) + else: + transform.position = MultiDimensional(NVector(250, 250, 0)) + idx += 1 + + elif line.startswith('(scale'): + # Scale parsing code with bracketed format handling + if 'separated=true' in line: + # Handle separated scale + transform.scale = Value(NVector(100, 100, 100)) + transform.scale.separated = True + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/scale)'): + if lines[idx].startswith('(scale_x'): + if 'animated=true' in lines[idx]: + # Parse animated x component + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/scale_x)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 100)) + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + x_value = Value(keyframes[0].value if keyframes else 100) + x_value.keyframes = keyframes + transform.scale.x = x_value + + if idx < len(lines) and lines[idx].startswith('(/scale_x)'): + idx += 1 + else: + value = extract_number(lines[idx]) + transform.scale.x = Value(value) + idx += 1 + elif lines[idx].startswith('(scale_y'): + if 'animated=true' in lines[idx]: + # Parse animated y component + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/scale_y)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 100)) + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + y_value = Value(keyframes[0].value if keyframes else 100) + y_value.keyframes = keyframes + transform.scale.y = y_value + + if idx < len(lines) and lines[idx].startswith('(/scale_y)'): + idx += 1 + else: + value = extract_number(lines[idx]) + transform.scale.y = Value(value) + idx += 1 + elif lines[idx].startswith('(scale_z'): + if 'animated=true' in lines[idx]: + # Parse animated z component + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/scale_z)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 100)) + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + z_value = Value(keyframes[0].value if keyframes else 100) + z_value.keyframes = keyframes + transform.scale.z = z_value + + if idx < len(lines) and lines[idx].startswith('(/scale_z)'): + idx += 1 + else: + value = extract_number(lines[idx]) + transform.scale.z = Value(value) + idx += 1 + else: + idx += 1 + if lines[idx].startswith('(/scale)'): + idx += 1 + elif 'animated=true' in line: + # Regular animated scale + idx += 1 + keyframes = [] + + while idx < len(lines) and not lines[idx].startswith('(/scale)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + + time = float(attrs.get('t', 0)) + + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + if len(values) >= 3: + value = NVector(values[0], values[1], values[2]) + elif len(values) == 2: + value = NVector(values[0], values[1], 100) + else: + value = NVector(values[0] if values else 100, values[0] if values else 100, 100) + else: + value = NVector(100, 100, 100) + + kf = Keyframe(time, value) + + # Handle easing parameters + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + # Handle space-separated list format + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + i_y = [float(v) for v in i_y_str.split()] + # Handle bracketed format + elif i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + i_y = float(i_y_str[1:-1]) + else: + i_x = float(i_x_str) + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + # Handle space-separated list format + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + o_y = [float(v) for v in o_y_str.split()] + # Handle bracketed format + elif o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + o_y = float(o_y_str[1:-1]) + else: + o_x = float(o_x_str) + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + # FIX: Add h attribute restoration + if 'h' in attrs: + kf.h = int(attrs['h']) + + # Add n and e attributes + if 'n' in attrs: + kf.n = attrs['n'] + if 'e' in attrs: + try: + kf.e = json.loads(attrs['e']) if attrs['e'].startswith('[') else float(attrs['e']) + except: + kf.e = attrs['e'] + + keyframes.append(kf) + idx += 1 + + if keyframes: + scale_value = MultiDimensional(keyframes[0].value) + scale_value.keyframes = keyframes + transform.scale = scale_value + else: + transform.scale = MultiDimensional(NVector(100, 100, 100)) + + if idx < len(lines) and lines[idx].startswith('(/scale)'): + idx += 1 + + else: + components = extract_numbers(line) + if len(components) >= 3: + transform.scale = MultiDimensional(NVector(*components)) + elif len(components) == 2: + transform.scale = MultiDimensional(NVector(components[0], components[1], 100)) + elif len(components) == 1: + transform.scale = MultiDimensional(NVector(components[0], components[0], 100)) + idx += 1 + + elif line.startswith('(rotation'): + # Handle separated rotation + if 'separated=true' in line: + # Handle separated rotation + transform.rotation = Value(0) + transform.rotation.separated = True + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/rotation)'): + if lines[idx].startswith('(rotation_x'): + if 'animated=true' in lines[idx]: + # Parse animated x component + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/rotation_x)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 0)) + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + x_value = Value(keyframes[0].value if keyframes else 0) + x_value.keyframes = keyframes + transform.rotation.x = x_value + + if idx < len(lines) and lines[idx].startswith('(/rotation_x)'): + idx += 1 + else: + value = extract_number(lines[idx]) + transform.rotation.x = Value(value) + idx += 1 + elif lines[idx].startswith('(rotation_y'): + value = extract_number(lines[idx]) + transform.rotation.y = Value(value) + idx += 1 + elif lines[idx].startswith('(rotation_z'): + value = extract_number(lines[idx]) + transform.rotation.z = Value(value) + idx += 1 + else: + idx += 1 + if lines[idx].startswith('(/rotation)'): + idx += 1 + elif 'animated=true' in line: + idx += 1 + keyframes = [] + + while idx < len(lines) and not lines[idx].startswith('(/rotation)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '') + value = float(s_str) if s_str else 0 + + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + # Handle space-separated list format + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + i_y = [float(v) for v in i_y_str.split()] + # Handle bracketed format + elif i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + i_y = float(i_y_str[1:-1]) + else: + i_x = float(i_x_str) + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + # Handle space-separated list format + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + o_y = [float(v) for v in o_y_str.split()] + # Handle bracketed format + elif o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + o_y = float(o_y_str[1:-1]) + else: + o_x = float(o_x_str) + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + if 'h' in attrs: + kf.h = int(attrs['h']) + # Handle n and e attributes + if 'n' in attrs: + kf.n = attrs['n'] + if 'e' in attrs: + try: + kf.e = json.loads(attrs['e']) if attrs['e'].startswith('[') else float(attrs['e']) + except: + kf.e = attrs['e'] + + keyframes.append(kf) + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/rotation)'): + idx += 1 + + if keyframes: + rot_value = Value(keyframes[0].value) + rot_value.keyframes = keyframes + transform.rotation = rot_value + else: + value = extract_number(line) + transform.rotation = Value(value) + idx += 1 + + elif line.startswith('(opacity'): + if 'animated=true' in line: + idx += 1 + keyframes = [] + + while idx < len(lines) and not lines[idx].startswith('(/opacity)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '') + value = float(s_str) if s_str else 100 + + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + # Handle space-separated list format + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + i_y = [float(v) for v in i_y_str.split()] + # Handle bracketed format + elif i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + i_y = float(i_y_str[1:-1]) + else: + i_x = float(i_x_str) + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + # Handle space-separated list format + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + o_y = [float(v) for v in o_y_str.split()] + # Handle bracketed format + elif o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + o_y = float(o_y_str[1:-1]) + else: + o_x = float(o_x_str) + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + # ADD THIS: Parse h attribute for hold keyframe + if 'h' in attrs: + kf.h = int(attrs['h']) + + # Handle n and e attributes + if 'n' in attrs: + kf.n = attrs['n'] + if 'e' in attrs: + try: + kf.e = json.loads(attrs['e']) if attrs['e'].startswith('[') else float(attrs['e']) + except: + kf.e = attrs['e'] + + keyframes.append(kf) + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/opacity)'): + idx += 1 + + if keyframes: + op_value = Value(keyframes[0].value) + op_value.keyframes = keyframes + transform.opacity = op_value + else: + value = extract_number(line) + transform.opacity = Value(value) + idx += 1 + + + elif line.startswith('(anchor'): + if 'animated=true' in line: + idx += 1 + keyframes = [] + + while idx < len(lines) and not lines[idx].startswith('(/anchor)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + + time = float(attrs.get('t', 0)) + + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + if len(values) >= 2: + value = NVector(values[0], values[1], 0) + else: + value = NVector(0, 0, 0) + else: + value = NVector(0, 0, 0) + + kf = Keyframe(time, value) + + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + # Handle space-separated list format + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + i_y = [float(v) for v in i_y_str.split()] + # Handle bracketed format + elif i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + i_y = float(i_y_str[1:-1]) + else: + i_x = float(i_x_str) + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + # Handle space-separated list format + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + o_y = [float(v) for v in o_y_str.split()] + # Handle bracketed format + elif o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + o_y = float(o_y_str[1:-1]) + else: + o_x = float(o_x_str) + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + # Add to/ti attributes parsing + if 'to' in attrs: + try: + kf.to = json.loads(attrs['to']) + except: + pass + + if 'ti' in attrs: + try: + kf.ti = json.loads(attrs['ti']) + except: + pass + if 'h' in attrs: + kf.h = int(attrs['h']) + + keyframes.append(kf) + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/anchor)'): + idx += 1 + + if keyframes: + anchor_value = MultiDimensional(keyframes[0].value) + anchor_value.keyframes = keyframes + transform.anchor = anchor_value + else: + components = extract_numbers(line) + if components: + if len(components) == 1: + transform.anchor = MultiDimensional(NVector(components[0], 0, 0)) + else: + transform.anchor = MultiDimensional(NVector(*components)) + idx += 1 + + elif line.startswith('(skew'): + value = extract_number(line) + transform.skew = Value(value) + idx += 1 + + elif line.startswith('(skew_axis'): + value = extract_number(line) + transform.skew_axis = Value(value) + idx += 1 + + elif line.strip() == '(/transform)': + idx += 1 + break + else: + idx += 1 + + return transform, idx + + +def parse_solid_layer_tag(lines, idx): + """""" + from .layers import SolidColorLayer + + solid_attrs = parse_tag_attrs(lines[idx]) + + solid_layer = SolidColorLayer() + solid_layer.type = ElementType.SOLID_LAYER + solid_layer.index = int(float(solid_attrs.get("index", 0))) + solid_layer.name = solid_attrs.get("name", "Solid Layer") + solid_layer.in_point = float(solid_attrs.get("in_point", 0)) + solid_layer.out_point = float(solid_attrs.get("out_point", 60)) + solid_layer.start_time = float(solid_attrs.get("start_time", 0)) + solid_layer.color = solid_attrs.get("color", "#000000") + solid_layer.width = float(solid_attrs.get("width", 512)) + solid_layer.height = float(solid_attrs.get("height", 512)) + + # hasMask + if "hasMask" in solid_attrs: + solid_layer.hasMask = solid_attrs["hasMask"].lower() == "true" + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(parent'): + parent_index = int(extract_number(line)) + solid_layer.parent_index = parent_index + idx += 1 + elif line.startswith('(transform'): + transform, new_idx = parse_transform_tag(lines, idx) + solid_layer.transform = transform + idx = new_idx + + elif line.startswith('(masksProperties'): + # masksProperties + solid_layer.masksProperties = [] + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/masksProperties)'): + if lines[idx].startswith('(mask '): + # mask + mask_attrs = parse_tag_attrs(lines[idx]) + mask = {} + + # + if "inv" in mask_attrs: + mask["inv"] = mask_attrs["inv"].lower() == "true" + if "mode" in mask_attrs: + mask["mode"] = mask_attrs["mode"] + if "nm" in mask_attrs: + mask["nm"] = mask_attrs["nm"] + + idx += 1 + + # mask + while idx < len(lines) and not lines[idx].startswith('(/mask)'): + if lines[idx].startswith('(mask_pt '): + # pt + pt_attrs = parse_tag_attrs(lines[idx]) + mask["pt"] = {} + if "a" in pt_attrs: + mask["pt"]["a"] = int(pt_attrs["a"]) + if "ix" in pt_attrs: + mask["pt"]["ix"] = int(pt_attrs["ix"]) + + idx += 1 + + # pt.k + if idx < len(lines) and lines[idx].startswith('(mask_pt_k'): + if lines[idx].startswith('(mask_pt_k_array'): + # k + mask["pt"]["k"] = [] + idx += 1 + + # + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k_array)'): + if lines[idx].startswith('(mask_pt_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + keyframe = {} + + if "t" in kf_attrs: + keyframe["t"] = float(kf_attrs["t"]) + + idx += 1 + + # ... + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_keyframe)'): + if lines[idx].startswith('(mask_pt_kf_i'): + i_attrs = parse_tag_attrs(lines[idx]) + keyframe["i"] = { + "x": float(i_attrs.get("x", 0)), + "y": float(i_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_o'): + o_attrs = parse_tag_attrs(lines[idx]) + keyframe["o"] = { + "x": float(o_attrs.get("x", 0)), + "y": float(o_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_s'): + keyframe["s"] = [] + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_s)'): + if lines[idx].startswith('(mask_pt_kf_shape'): + shape_attrs = parse_tag_attrs(lines[idx]) + shape = {} + if "c" in shape_attrs: + shape["c"] = shape_attrs["c"].lower() == "true" + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_shape)'): + if lines[idx].startswith('(mask_pt_kf_shape_i'): + values = extract_numbers(lines[idx]) + shape["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["i"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_o'): + values = extract_numbers(lines[idx]) + shape["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["o"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_v'): + values = extract_numbers(lines[idx]) + shape["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_shape)'): + idx += 1 + keyframe["s"].append(shape) + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_s)'): + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_keyframe)'): + idx += 1 + + mask["pt"]["k"].append(keyframe) + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k_array)'): + idx += 1 + + else: + # kshape + mask["pt"]["k"] = {} + idx += 1 + + # shape + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k)'): + if lines[idx].startswith('(mask_pt_k_c'): + # closed + parts = lines[idx].split() + if len(parts) > 1: + mask["pt"]["k"]["c"] = parts[1].rstrip(')').lower() == "true" + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_i'): + # i + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["i"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_o'): + # o + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["o"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_v'): + # v + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k)'): + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/mask_pt)'): + idx += 1 + + elif lines[idx].startswith('(mask_o '): + # opacity + o_attrs = parse_tag_attrs(lines[idx]) + mask["o"] = {} + if "a" in o_attrs: + mask["o"]["a"] = int(o_attrs["a"]) + if "k" in o_attrs: + mask["o"]["k"] = float(o_attrs["k"]) + if "ix" in o_attrs: + mask["o"]["ix"] = int(o_attrs["ix"]) + idx += 1 + + elif lines[idx].startswith('(mask_x '): + # dilate + x_attrs = parse_tag_attrs(lines[idx]) + mask["x"] = {} + if "a" in x_attrs: + mask["x"]["a"] = int(x_attrs["a"]) + if "k" in x_attrs: + mask["x"]["k"] = float(x_attrs["k"]) + if "ix" in x_attrs: + mask["x"]["ix"] = int(x_attrs["ix"]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask)'): + idx += 1 + + solid_layer.masksProperties.append(mask) + else: + idx += 1 + + if lines[idx].startswith('(/masksProperties)'): + idx += 1 + + elif lines[idx].startswith('(effects'): + # effects + effects, new_idx = parse_effects_tag(lines, idx) + solid_layer.ef = effects + idx = new_idx + + + + elif line.startswith('(tm '): + # tm - + tm_attrs = parse_tag_attrs(line) + tm_data = {} + if 'a' in tm_attrs: + tm_data['a'] = int(tm_attrs['a']) + if 'ix' in tm_attrs: + tm_data['ix'] = int(tm_attrs['ix']) + + # keyframes + idx += 1 + keyframes = [] + while idx < len(lines): + line = lines[idx] + if line.startswith('(keyframe'): + kf_attrs = parse_tag_attrs(line) + kf = {} + if 't' in kf_attrs: + kf['t'] = float(kf_attrs['t']) + if 's' in kf_attrs: + kf['s'] = [float(kf_attrs['s'])] + if 'h' in kf_attrs: + kf['h'] = int(kf_attrs['h']) + + # + if 'i_x' in kf_attrs or 'i_y' in kf_attrs: + kf['i'] = {} + if 'i_x' in kf_attrs: + kf['i']['x'] = [float(kf_attrs['i_x'])] + if 'i_y' in kf_attrs: + kf['i']['y'] = [float(kf_attrs['i_y'])] + + if 'o_x' in kf_attrs or 'o_y' in kf_attrs: + kf['o'] = {} + if 'o_x' in kf_attrs: + kf['o']['x'] = [float(kf_attrs['o_x'])] + if 'o_y' in kf_attrs: + kf['o']['y'] = [float(kf_attrs['o_y'])] + + keyframes.append(kf) + idx += 1 + elif line.startswith('(value'): + # + val = extract_number(line) + tm_data['k'] = val + idx += 1 + elif line.strip() == '(/tm)': + if keyframes: + tm_data['k'] = keyframes + idx += 1 + break + else: + idx += 1 + + solid_layer.tm = tm_data + + + elif line.startswith('(tt'): + tt_value = extract_number(line) + solid_layer.tt = int(tt_value) + idx += 1 + elif line.startswith('(tp'): + tp_value = extract_number(line) + solid_layer.tp = int(tp_value) + idx += 1 + elif line.startswith('(td'): + td_value = extract_number(line) + solid_layer.td = int(td_value) + idx += 1 + + elif line.strip() == '(/solid_layer)': + idx += 1 + break + else: + idx += 1 + + return solid_layer, idx + + +def parse_text_layer_tag(lines, idx): + """ - """ + text_attrs = parse_tag_attrs(lines[idx]) + + text_layer = TextLayer() + text_layer.type = ElementType.TEXT_LAYER + text_layer.index = int(float(text_attrs.get("index", 0))) + text_layer.name = text_attrs.get("name", "Text Layer") + text_layer.in_point = float(text_attrs.get("in_point", 0)) + text_layer.out_point = float(text_attrs.get("out_point", 60)) + text_layer.start_time = float(text_attrs.get("start_time", 0)) + #text_layer.ct = int(text_attrs.get("ct", 0)) + + # ln + #text_layer.ln =(text_attrs.get("ln", "")) + if "ct" in text_attrs: + text_layer.ct = int(text_attrs.get("ct")) + # hasMask + if "hasMask" in text_attrs: + text_layer.hasMask = text_attrs["hasMask"].lower() == "true" + + idx += 1 + while idx < len(lines): + line = lines[idx] + + + if line.startswith('(tt'): + tt_value = extract_number(line) + text_layer.tt = int(tt_value) + idx += 1 + elif line.startswith('(tp'): + tp_value = extract_number(line) + text_layer.tp = int(tp_value) + idx += 1 + elif line.startswith('(td'): + td_value = extract_number(line) + text_layer.td = int(td_value) + idx += 1 + elif line.startswith('(parent'): + parent_index = int(extract_number(line)) + text_layer.parent_index = parent_index + idx += 1 + + elif lines[idx].startswith('(effects'): + # effects + effects, new_idx = parse_effects_tag(lines, idx) + text_layer.ef = effects + idx = new_idx + + + elif line.startswith('(transform'): + transform, new_idx = parse_transform_tag(lines, idx) + text_layer.transform = transform + idx = new_idx + + + elif line.startswith('(text_data'): + # + idx += 1 + keyframes = [] + path_option = {} + more_options = {} + animators = [] + + while idx < len(lines) and not lines[idx].startswith('(/text_data)'): + if lines[idx].startswith('(text_keyframes'): + # + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/text_keyframes)'): + if lines[idx].startswith('(text_keyframe'): + # text_keyframe + line_content = lines[idx] + + # + doc_data = {} + time = 0 + + # t - + t_match = re.search(r't=([\d\.\-]+)', line_content) + if t_match: + time = float(t_match.group(1)) + + # font_size - + fs_match = re.search(r'font_size=([\d\.\-]+)', line_content) + if fs_match: + doc_data['s'] = float(fs_match.group(1)) + + # font_family + ff_match = re.search(r'font_family="([^"]*)"', line_content) + if ff_match: + doc_data['f'] = ff_match.group(1) + + # text - + text_match = re.search(r'text="([^"]*(?:\\.[^"]*)*)"', line_content) + if text_match: + text_val = text_match.group(1) + doc_data['t'] = text_val.replace('\\"', '"') + + # ca - + ca_match = re.search(r'ca=([\d]+)', line_content) + if ca_match: + doc_data['ca'] = int(ca_match.group(1)) + + # justify - + j_match = re.search(r'justify=([\d]+)', line_content) + if j_match: + doc_data['j'] = int(j_match.group(1)) + + # tracking - + tr_match = re.search(r'tracking=([\d\.\-]+)', line_content) + if tr_match: + doc_data['tr'] = float(tr_match.group(1)) + + # line_height - + lh_match = re.search(r'line_height=([\d\.\-]+)', line_content) + if lh_match: + doc_data['lh'] = float(lh_match.group(1)) + + # letter_spacing - + ls_match = re.search(r'letter_spacing=([\d\.\-]+)', line_content) + if ls_match: + doc_data['ls'] = float(ls_match.group(1)) + + # fill_color + fc_match = re.search(r'fill_color=\[([^\]]+)\]', line_content) + if fc_match: + fc_str = fc_match.group(1) + fc_values = [float(v.strip()) for v in fc_str.split(',')] + doc_data['fc'] = fc_values + + # ADD PARSING FOR MISSING FIELDS + # stroke_color + sc_match = re.search(r'stroke_color=\[([^\]]+)\]', line_content) + if sc_match: + sc_str = sc_match.group(1) + sc_values = [float(v.strip()) for v in sc_str.split(',')] + doc_data['sc'] = sc_values + + # stroke_width - + sw_match = re.search(r'stroke_width=([\d\.\-]+)', line_content) + if sw_match: + doc_data['sw'] = float(sw_match.group(1)) + + # offset + of_match = re.search(r'offset=(true|false)', line_content) + if of_match: + doc_data['of'] = of_match.group(1) == 'true' + + # Extract wrap_size (sz) + sz_match = re.search(r'wrap_size=\[([^\]]+)\]', line_content) + if sz_match: + sz_str = sz_match.group(1) + sz_values = [float(v.strip()) for v in sz_str.split(',')] + doc_data['sz'] = sz_values + + # Extract wrap_position (ps) + ps_match = re.search(r'wrap_position=\[([^\]]+)\]', line_content) + if ps_match: + ps_str = ps_match.group(1) + ps_values = [float(v.strip()) for v in ps_str.split(',')] + doc_data['ps'] = ps_values + + keyframes.append({"s": doc_data, "t": time}) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/text_keyframes'): + idx += 1 + + elif lines[idx].startswith('(document_full'): + # + doc_json = extract_string_value(lines[idx]) + if doc_json: + try: + keyframes = json.loads(doc_json) + except json.JSONDecodeError: + keyframes = [] + idx += 1 + + elif lines[idx].startswith('(path_option'): + path_json = extract_string_value(lines[idx]) + if path_json: + try: + path_option = json.loads(path_json) + except json.JSONDecodeError: + path_option = {} + idx += 1 + + elif lines[idx].startswith('(more_options') and not lines[idx].endswith('")'): + # more_options + line_content = lines[idx] + + # g + g_match = re.search(r'\(more_options g (\d+)', line_content) + g_value = int(g_match.group(1)) if g_match else 1 + + # alignment + alignment_data = {"a": 0, "k": [0, 0], "ix": 2} + + # a + a_match = re.search(r'alignment a=(\d+)', line_content) + if a_match: + alignment_data['a'] = int(a_match.group(1)) + + # alignment_k + k_match = re.search(r'alignment_k ([\d\.\-\s]+)(?:alignment_ix|$|\))', line_content) + if k_match: + k_values = [float(v) for v in k_match.group(1).strip().split()] + alignment_data['k'] = k_values + + # alignment_ix + ix_match = re.search(r'alignment_ix (\d+)', line_content) + if ix_match: + alignment_data['ix'] = int(ix_match.group(1)) + + more_options = {"g": g_value, "a": alignment_data} + idx += 1 + + elif lines[idx].startswith('(more_options "'): + # JSON + more_json = extract_string_value(lines[idx]) + if more_json: + try: + more_options = json.loads(more_json) + except json.JSONDecodeError: + more_options = {} + idx += 1 + + elif lines[idx].startswith('(animators'): + animators = [] + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/animators'): + if lines[idx].startswith('(animator '): + animator = {} + nm_match = re.search(r'nm="([^"]*)"', lines[idx]) + if nm_match: + animator["nm"] = nm_match.group(1) + idx += 1 + + # Parse range selector + if idx < len(lines) and lines[idx].startswith('(range_selector'): + range_selector = {} + params = lines[idx] + + # Parse basic range selector properties + # Fix: Use [^)\s]+ to exclude closing parenthesis and whitespace + for prop in ["t", "r", "b", "sh", "rn"]: + match = re.search(rf'{prop}=([^)\s]+)', params) + if match: + range_selector[prop] = float(match.group(1)) if '.' in match.group(1) else int(match.group(1)) + + idx += 1 + + # Parse range properties + while idx < len(lines) and not lines[idx].startswith('(/range_selector'): + prop_line = lines[idx] + + # Map property names + prop_map = { + "range_start": "s", + "range_end": "e", + "range_offset": "o", + "amount": "a", + "max_ease": "xe", + "min_ease": "ne", + "s_m": "sm" + } + + found_prop = False + for prop_name, prop_key in prop_map.items(): + if prop_line.startswith(f'({prop_name} '): + found_prop = True + # Check if animated + a_match = re.search(r'a=(\d+)', prop_line) + + if a_match and a_match.group(1) == "1": + # Animated property - parse keyframes + prop_data = {"a": 1, "k": []} + idx += 1 + + while idx < len(lines) and lines[idx].startswith(f'({prop_name}_keyframe'): + kf_line = lines[idx] + keyframe = {} + + # Parse time + t_match = re.search(r't=([^)\s]+)', kf_line) + if t_match: + keyframe["t"] = float(t_match.group(1)) + + # Parse value + s_match = re.search(r's=([^)]+?)(?:\s+[io]_|$|\))', kf_line) + if s_match: + s_values = s_match.group(1).strip().split() + if len(s_values) > 1: + keyframe["s"] = [float(v) for v in s_values] + else: + keyframe["s"] = [float(s_values[0])] + + # Parse interpolation + for interp in ["i", "o"]: + x_match = re.search(rf'{interp}_x=([^)]+?)(?:\s+[io]_|$|\))', kf_line) + y_match = re.search(rf'{interp}_y=([^)]+?)(?:\s+[io]_|$|\))', kf_line) + + if x_match or y_match: + keyframe[interp] = {} + if x_match: + x_values = x_match.group(1).strip().split() + keyframe[interp]["x"] = [float(v) for v in x_values] + if y_match: + y_values = y_match.group(1).strip().split() + keyframe[interp]["y"] = [float(v) for v in y_values] + + prop_data["k"].append(keyframe) + idx += 1 + + # Skip closing tag if present + if idx < len(lines) and lines[idx] == f'(/{prop_name})': + idx += 1 + + range_selector[prop_key] = prop_data + else: + # Static property - ALWAYS use the {a, k, ix} structure + k_match = re.search(r'k=([^)\s]+)', prop_line) + ix_match = re.search(r'ix=(\d+)', prop_line) + + k_value = 0 + if k_match: + k_str = k_match.group(1) + # Handle both single values and space-separated values + if ' ' in prop_line[prop_line.find('k='):]: + # Multiple values - extract until we hit ix= or ) + k_full_match = re.search(r'k=([\d.\s-]+?)(?:\s+ix=|\))', prop_line) + if k_full_match: + k_values = k_full_match.group(1).strip().split() + k_value = [float(v) for v in k_values] + else: + k_value = float(k_str) + else: + k_value = float(k_str) + + range_selector[prop_key] = { + "a": int(a_match.group(1)) if a_match else 0, + "k": k_value, + "ix": int(ix_match.group(1)) if ix_match else 0 + } + idx += 1 + break + + if not found_prop: + idx += 1 + + animator["s"] = range_selector + if idx < len(lines) and lines[idx] == '(/range_selector)': + idx += 1 + + # Parse animator properties + if idx < len(lines) and lines[idx].startswith('(animator_properties'): + idx += 1 + a_props = {} + + prop_map = { + "opacity_animators": "o", + "position_animators": "p", + "scale_animators": "s", + "rotation_animators": "r", + "anchor_animators": "a", + "skew_animators": "sk", + "skew_axis_animators": "sa", + "fill_colo_animatorsr": "fc", + "stroke_color_animators": "sc", + "stroke_width_animators": "sw", + "tracking_animators": "t" + } + + while idx < len(lines) and not lines[idx].startswith('(/animator_properties'): + line = lines[idx] + + # Check each property type + for prop_name, prop_key in prop_map.items(): + if line.startswith(f'({prop_name} '): + # Check if animated + a_match = re.search(r'a=(\d+)', line) + + if a_match and a_match.group(1) == "1": + # Animated property - parse keyframes + prop_data = {"a": 1, "k": []} + idx += 1 + + # Parse keyframes + while idx < len(lines) and lines[idx].startswith('(keyframe '): + kf_line = lines[idx] + keyframe = {} + + # Parse time + t_match = re.search(r't=([^)\s]+)', kf_line) + if t_match: + keyframe["t"] = float(t_match.group(1)) + + # Parse value(s) - now properly handle quoted values + s_match = re.search(r's="([^"]*)"', kf_line) + if s_match: + s_values = s_match.group(1).strip().split() + if len(s_values) > 1: + keyframe["s"] = [float(v) for v in s_values] + else: + keyframe["s"] = [float(s_values[0])] + + # Parse 'to' and 'ti' + to_match = re.search(r'to="([^"]*)"', kf_line) + if to_match: + to_values = to_match.group(1).strip().split() + if len(to_values) > 1: + keyframe["to"] = [float(v) for v in to_values] + else: + keyframe["to"] = [float(to_values[0])] + + ti_match = re.search(r'ti="([^"]*)"', kf_line) + if ti_match: + ti_values = ti_match.group(1).strip().split() + if len(ti_values) > 1: + keyframe["ti"] = [float(v) for v in ti_values] + else: + keyframe["ti"] = [float(ti_values[0])] + + + # Parse interpolation - handle quoted values + for interp in ["i", "o"]: + x_match = re.search(rf'{interp}_x="([^"]*)"', kf_line) + y_match = re.search(rf'{interp}_y="([^"]*)"', kf_line) + + if x_match or y_match: + keyframe[interp] = {} + if x_match: + x_values = x_match.group(1).strip().split() + if len(x_values) > 1: + keyframe[interp]["x"] = [float(v) for v in x_values] + else: + keyframe[interp]["x"] = float(x_values[0]) + if y_match: + y_values = y_match.group(1).strip().split() + if len(y_values) > 1: + keyframe[interp]["y"] = [float(v) for v in y_values] + else: + keyframe[interp]["y"] = float(y_values[0]) + + prop_data["k"].append(keyframe) + idx += 1 + + # Check for closing tag + if idx < len(lines) and lines[idx] == f'(/{prop_name})': + idx += 1 + + a_props[prop_key] = prop_data + else: + # Static property + k_match = re.search(r'k=(\[[\d.,\s-]+\]|[^)\s]+)', line) + ix_match = re.search(r'ix=(\d+)', line) + + k_value = 0 + if k_match: + k_str = k_match.group(1) + if k_str.startswith('['): + # Parse array + k_str = k_str.strip('[]') + k_value = [float(v.strip()) for v in k_str.split(',')] + else: + k_value = float(k_str) + + a_props[prop_key] = { + "a": int(a_match.group(1)) if a_match else 0, + "k": k_value, + "ix": int(ix_match.group(1)) if ix_match else 0 + } + idx += 1 + break + else: + # Line doesn't match any property + idx += 1 + + animator["a"] = a_props + idx += 1 # Skip (/animator_properties) + + + idx += 1 # Skip (/animator) + animators.append(animator) + else: + idx += 1 + + text_layer.data.animators = animators + idx += 1 # Skip (/animators) + + else: + idx += 1 + + # + if keyframes: + text_layer.data.document = Value(keyframes) + text_layer.data.path_option = path_option + text_layer.data.more_options = more_options + text_layer.data.animators = animators + + if lines[idx].startswith('(/text_data)'): + idx += 1 + + + + elif line.startswith('(masksProperties'): + # masksProperties + text_layer.masksProperties = [] + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/masksProperties)'): + if lines[idx].startswith('(mask '): + # mask + mask_attrs = parse_tag_attrs(lines[idx]) + mask = {} + + # + if "inv" in mask_attrs: + mask["inv"] = mask_attrs["inv"].lower() == "true" + if "mode" in mask_attrs: + mask["mode"] = mask_attrs["mode"] + if "nm" in mask_attrs: + mask["nm"] = mask_attrs["nm"] + + idx += 1 + + # mask + while idx < len(lines) and not lines[idx].startswith('(/mask)'): + if lines[idx].startswith('(mask_pt '): + # pt + pt_attrs = parse_tag_attrs(lines[idx]) + mask["pt"] = {} + if "a" in pt_attrs: + mask["pt"]["a"] = int(pt_attrs["a"]) + if "ix" in pt_attrs: + mask["pt"]["ix"] = int(pt_attrs["ix"]) + + idx += 1 + + # pt.k + if idx < len(lines) and lines[idx].startswith('(mask_pt_k'): + if lines[idx].startswith('(mask_pt_k_array'): + # k + mask["pt"]["k"] = [] + idx += 1 + + # + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k_array)'): + if lines[idx].startswith('(mask_pt_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + keyframe = {} + + if "t" in kf_attrs: + keyframe["t"] = float(kf_attrs["t"]) + + idx += 1 + + # ... + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_keyframe)'): + if lines[idx].startswith('(mask_pt_kf_i'): + i_attrs = parse_tag_attrs(lines[idx]) + keyframe["i"] = { + "x": float(i_attrs.get("x", 0)), + "y": float(i_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_o'): + o_attrs = parse_tag_attrs(lines[idx]) + keyframe["o"] = { + "x": float(o_attrs.get("x", 0)), + "y": float(o_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_s'): + keyframe["s"] = [] + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_s)'): + if lines[idx].startswith('(mask_pt_kf_shape'): + shape_attrs = parse_tag_attrs(lines[idx]) + shape = {} + if "c" in shape_attrs: + shape["c"] = shape_attrs["c"].lower() == "true" + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_shape)'): + if lines[idx].startswith('(mask_pt_kf_shape_i'): + values = extract_numbers(lines[idx]) + shape["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["i"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_o'): + values = extract_numbers(lines[idx]) + shape["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["o"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_v'): + values = extract_numbers(lines[idx]) + shape["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_shape)'): + idx += 1 + keyframe["s"].append(shape) + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_s)'): + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_keyframe)'): + idx += 1 + + mask["pt"]["k"].append(keyframe) + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k_array)'): + idx += 1 + + else: + # kshape + mask["pt"]["k"] = {} + idx += 1 + + # shape + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k)'): + if lines[idx].startswith('(mask_pt_k_c'): + # closed + parts = lines[idx].split() + if len(parts) > 1: + mask["pt"]["k"]["c"] = parts[1].rstrip(')').lower() == "true" + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_i'): + # i + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["i"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_o'): + # o + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["o"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_v'): + # v + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k)'): + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/mask_pt)'): + idx += 1 + + elif lines[idx].startswith('(mask_o '): + # opacity + o_attrs = parse_tag_attrs(lines[idx]) + mask["o"] = {} + if "a" in o_attrs: + mask["o"]["a"] = int(o_attrs["a"]) + if "k" in o_attrs: + mask["o"]["k"] = float(o_attrs["k"]) + if "ix" in o_attrs: + mask["o"]["ix"] = int(o_attrs["ix"]) + idx += 1 + + elif lines[idx].startswith('(mask_x '): + # dilate + x_attrs = parse_tag_attrs(lines[idx]) + mask["x"] = {} + if "a" in x_attrs: + mask["x"]["a"] = int(x_attrs["a"]) + if "k" in x_attrs: + mask["x"]["k"] = float(x_attrs["k"]) + if "ix" in x_attrs: + mask["x"]["ix"] = int(x_attrs["ix"]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask)'): + idx += 1 + + text_layer.masksProperties.append(mask) + else: + idx += 1 + + if lines[idx].startswith('(/masksProperties)'): + idx += 1 + + + elif line.strip() == '(/text_layer)': + idx += 1 + break + else: + idx += 1 + + return text_layer, idx + +def parse_group_tag(lines, idx): + """Parse a group tag and its contents""" + group_attrs = parse_tag_attrs(lines[idx]) + + group = Group() + group.shapes = [] + + group.name = group_attrs.get("name", "") + + # property_index + if "ix" in group_attrs: + group.property_index = int(group_attrs.get("ix", 1)) + if "cix" in group_attrs: + group.cix = int(group_attrs.get("cix", 2)) + if "bm" in group_attrs: + group.bm = 0 + if "hd" in group_attrs: + group.hd = group_attrs.get("hd", "false").lower() == "true" + if "mn" in group_attrs: + group.mn = group_attrs.get("mn", "") + if "np" in group_attrs: + group.number_of_properties = int(float(group_attrs.get("np"))) + + + idx += 1 + while idx < len(lines): + if lines[idx].startswith('("TransformShape"'): + transform_shape, new_idx = parse_transform_shape_tag(lines, idx) + group.shapes.append(transform_shape) + idx = new_idx + elif lines[idx].startswith('(group'): + nested_group, new_idx = parse_group_tag(lines, idx) + group.shapes.append(nested_group) + idx = new_idx + elif lines[idx].startswith('(path'): + path, new_idx = parse_path_tag(lines, idx) + group.shapes.append(path) + idx = new_idx + elif lines[idx].startswith('(fill'): + fill, new_idx = parse_fill_tag(lines, idx) + group.shapes.append(fill) + idx = new_idx + elif lines[idx].startswith('(stroke'): + stroke, new_idx = parse_stroke_tag(lines, idx) + group.shapes.append(stroke) + idx = new_idx + elif lines[idx].startswith('(rect'): + rect, new_idx = parse_rect_tag(lines, idx) + group.shapes.append(rect) + idx = new_idx + elif lines[idx].startswith('(ellipse'): + ellipse, new_idx = parse_ellipse_tag(lines, idx) + group.shapes.append(ellipse) + idx = new_idx + elif lines[idx].startswith('(star'): + star, new_idx = parse_star_tag(lines, idx) + group.shapes.append(star) + idx = new_idx + elif lines[idx].startswith('(trim'): + trim, new_idx = parse_trim_tag(lines, idx) + group.shapes.append(trim) + idx = new_idx + elif lines[idx].startswith('(repeater'): + repeater, new_idx = parse_repeater_tag(lines, idx) + group.shapes.append(repeater) + idx = new_idx + elif lines[idx].startswith('(gradient_fill'): + gradient_fill, new_idx = parse_gradient_fill_tag(lines, idx) + group.shapes.append(gradient_fill) + idx = new_idx + elif lines[idx].startswith('(gradient_stroke'): + gradient_stroke, new_idx = parse_gradient_stroke_tag(lines, idx) + group.shapes.append(gradient_stroke) + idx = new_idx + elif lines[idx].startswith('(merge'): + merge, new_idx = parse_merge_tag(lines, idx) + group.shapes.append(merge) + idx = new_idx + elif lines[idx].startswith('(rounded_corners'): + rounded_corners, new_idx = parse_rounded_corners_tag(lines, idx) + group.shapes.append(rounded_corners) + idx = new_idx + elif lines[idx].startswith('(twist'): + twist, new_idx = parse_twist_tag(lines, idx) + group.shapes.append(twist) + idx = new_idx + elif lines[idx].startswith('(zig_zag'): + zig_zag, new_idx = parse_zig_zag_tag(lines, idx) + group.shapes.append(zig_zag) + idx = new_idx + elif lines[idx].strip() == '(/group)': + idx += 1 + break + else: + idx += 1 + + return group, idx + +def parse_zig_zag_tag(lines, idx): + """""" + zig_zag = ZigZag() + + # + if lines[idx].startswith('(zig_zag'): + attrs = lines[idx][8:-1].strip() # Remove '(zig_zag' and ')' + + # + if 'name=' in attrs: + import re + name_match = re.search(r'name="([^"]*)"', attrs) + if name_match: + zig_zag.name = name_match.group(1) + + if 'ix=' in attrs: + import re + ix_match = re.search(r'ix=(\d+)', attrs) + if ix_match: + zig_zag.property_index = int(ix_match.group(1)) + + idx += 1 + + # + while idx < len(lines) and not lines[idx].startswith('(/zig_zag)'): + line = lines[idx].strip() + + if line.startswith('(frequency'): + # frequency + parts = line.split() + if len(parts) > 1: + value_str = parts[1].rstrip(')') + zig_zag.frequency = Value(float(value_str)) + + elif line.startswith('(amplitude'): + # amplitude + parts = line.split() + if len(parts) > 1: + value_str = parts[1].rstrip(')') + zig_zag.amplitude = Value(float(value_str)) + + elif line.startswith('(point_type'): + # point_type + parts = line.split() + if len(parts) > 1: + value_str = parts[1].rstrip(')') + zig_zag.point_type = Value(float(value_str)) + + idx += 1 + + # + if idx < len(lines) and lines[idx].startswith('(/zig_zag)'): + idx += 1 + + return zig_zag, idx + + + +def parse_trim_tag(lines, idx): + """Parse trim tag and its contents - handle keyframes properly with e field""" + trim_attrs = parse_tag_attrs(lines[idx]) + + trim = Trim() + trim.name = trim_attrs.get("name", "") + + if "ix" in trim_attrs: + trim.property_index = int(trim_attrs.get("ix", 1)) + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(start'): + if 'animated=true' in line: + # Process animated start with keyframes + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/start)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '0') + value = float(s_str) if s_str else 0 + + kf = Keyframe(time, value) + + # Handle e field (end value) + if 'e' in attrs: + e_str = attrs.get('e', '0') + kf.e = float(e_str) if e_str else 0 + + # Handle easing parameters + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + else: + i_x = float(i_x_str) + + if ' ' in i_y_str: + i_y = [float(v) for v in i_y_str.split()] + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + else: + o_x = float(o_x_str) + + if ' ' in o_y_str: + o_y = [float(v) for v in o_y_str.split()] + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + trim.start = Value(keyframes[0].value) + trim.start.keyframes = keyframes + trim.start.animated = True + else: + trim.start = Value(0) + trim.start.animated = True + + if idx < len(lines) and lines[idx].startswith('(/start)'): + idx += 1 + else: + value = extract_number(line) + trim.start = Value(value) + idx += 1 + + elif line.startswith('(end'): + if 'animated=true' in line: + # Process animated end with keyframes + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/end)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '100') + value = float(s_str) if s_str else 100 + + kf = Keyframe(time, value) + + # Handle e field (end value) + if 'e' in attrs: + e_str = attrs.get('e', '100') + kf.e = float(e_str) if e_str else 100 + + # Handle easing parameters + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if ' ' in i_x_str: + i_y = [float(v) for v in i_x_str.split()] + else: + i_x = float(i_x_str) + + if ' ' in i_y_str: + i_y = [float(v) for v in i_y_str.split()] + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + else: + o_x = float(o_x_str) + + if ' ' in o_y_str: + o_y = [float(v) for v in o_y_str.split()] + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + trim.end = Value(keyframes[0].value) + trim.end.keyframes = keyframes + trim.end.animated = True + else: + trim.end = Value(100) + trim.end.animated = True + + if idx < len(lines) and lines[idx].startswith('(/end)'): + idx += 1 + else: + value = extract_number(line) + trim.end = Value(value) + idx += 1 + + elif line.startswith('(offset'): + if 'animated=true' in line: + # Process animated offset with keyframes + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/offset)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '0') + value = float(s_str) if s_str else 0 + + kf = Keyframe(time, value) + + # Handle e field (end value) + if 'e' in attrs: + e_str = attrs.get('e', '0') + kf.e = float(e_str) if e_str else 0 + + # Handle easing parameters + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + else: + i_x = float(i_x_str) + + if ' ' in i_y_str: + i_y = [float(v) for v in i_y_str.split()] + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + else: + o_x = float(o_x_str) + + if ' ' in o_y_str: + o_y = [float(v) for v in o_y_str.split()] + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + trim.offset = Value(keyframes[0].value) + trim.offset.keyframes = keyframes + trim.offset.animated = True + else: + trim.offset = Value(0) + trim.offset.animated = True + + if idx < len(lines) and lines[idx].startswith('(/offset)'): + idx += 1 + else: + value = extract_number(line) + trim.offset = Value(value) + idx += 1 + + elif line.startswith('(multiple'): + value = int(extract_number(line)) + trim.multiple = TrimMultipleShapes(value) + idx += 1 + elif line.strip() == '(/trim)': + idx += 1 + break + else: + idx += 1 + + return trim, idx + + +def parse_star_tag(lines, idx): + """Parse star tag and its contents""" + star_attrs = parse_tag_attrs(lines[idx]) + + star = Star() + star.name = star_attrs.get("name", "") + + if "ix" in star_attrs: + star.property_index = int(star_attrs.get("ix", 1)) + + # directionstar_type + if "d" in star_attrs: + star.direction = float(star_attrs.get("d", 1)) + else: + star.direction = 1 + + if "sy" in star_attrs: + try: + sy_value = int(star_attrs.get("sy", 1)) + # StarType: 1(Star), 2(Polygon) + if sy_value in [1, 2]: + star.star_type = StarType(sy_value) + else: + # Star(1) + star.star_type = StarType.Star + except (ValueError, KeyError): + star.star_type = StarType.Star + else: + star.star_type = StarType.Star + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(position'): + attrs = parse_tag_attrs(line) + components = extract_numbers(line) + if components: + star.position = MultiDimensional(NVector(*components)) + if "ix" in attrs: + star.position_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(inner_radius'): + attrs = parse_tag_attrs(line) + value = extract_number(line) + star.inner_radius = Value(value) + if "ix" in attrs: + star.inner_radius_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(outer_radius'): + attrs = parse_tag_attrs(line) + value = extract_number(line) + star.outer_radius = Value(value) + if "ix" in attrs: + star.outer_radius_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(inner_roundness'): + attrs = parse_tag_attrs(line) + value = extract_number(line) + star.inner_roundness = Value(value) + if "ix" in attrs: + star.inner_roundness_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(outer_roundness'): + attrs = parse_tag_attrs(line) + value = extract_number(line) + star.outer_roundness = Value(value) + if "ix" in attrs: + star.outer_roundness_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(points_star'): + attrs = parse_tag_attrs(line) + value = extract_number(line) + star.points = Value(value) + if "ix" in attrs: + star.points_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(star_rotation'): + attrs = parse_tag_attrs(line) + value = extract_number(line) + star.rotation = Value(value) + if "ix" in attrs: + star.rotation_ix = int(attrs["ix"]) + idx += 1 + elif line.strip() == '(/star)': + idx += 1 + break + else: + idx += 1 + + return star, idx + +def parse_repeater_tag(lines, idx): + """Parse repeater tag and its contents - Fixed to parse all transform values""" + repeater_attrs = parse_tag_attrs(lines[idx]) + + repeater = Repeater() + repeater.name = repeater_attrs.get("name", "") + + if "ix" in repeater_attrs: + repeater.property_index = int(repeater_attrs.get("ix", 1)) + + # Initialize default values + repeater.copies = Value(1) + repeater.offset = Value(0) + repeater.composite = 1 # Default value + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(copies'): + attrs = parse_tag_attrs(line) + if 'animated=true' in line: + # Handle animated copies + value = extract_number(line) + repeater.copies = Value(value) + repeater.copies.animated = True + else: + value = extract_number(line) + repeater.copies = Value(value) + # Extract ix value + if "ix" in attrs: + repeater.copies_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(repeater_offset'): + attrs = parse_tag_attrs(line) + if 'animated=true' in line: + # Handle animated offset + idx += 1 + keyframes = [] + offset_ix = None + while idx < len(lines) and not lines[idx].startswith('(/repeater_offset)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 0)) + kf = Keyframe(time, value) + + # Handle easing parameters + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if i_x_str.startswith('[') and i_x_str.endswith(']'): + i_x = float(i_x_str[1:-1]) + else: + i_x = float(i_x_str) + + if i_y_str.startswith('[') and i_y_str.endswith(']'): + i_y = float(i_y_str[1:-1]) + else: + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if o_x_str.startswith('[') and o_x_str.endswith(']'): + o_x = float(o_x_str[1:-1]) + else: + o_x = float(o_x_str) + + if o_y_str.startswith('[') and o_y_str.endswith(']'): + o_y = float(o_y_str[1:-1]) + else: + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + elif lines[idx].startswith('(offset_ix'): + offset_ix = int(extract_number(lines[idx])) + idx += 1 + + if keyframes: + repeater.offset = Value(keyframes[0].value if keyframes else 0) + repeater.offset.keyframes = keyframes + repeater.offset.animated = True + + if offset_ix is not None: + repeater.offset_ix = offset_ix + + if lines[idx].startswith('(/repeater_offset)'): + idx += 1 + else: + value = extract_number(line) + repeater.offset = Value(value) + # Extract ix value + if "ix" in attrs: + repeater.offset_ix = int(attrs["ix"]) + idx += 1 + elif line.startswith('(composite'): + value = int(extract_number(line)) + repeater.composite = value + idx += 1 + elif line.startswith('(repeater_transform'): + # Parse transform + transform = TransformShape() + transform.type = "tr" + transform.name = "Transform" + + # Set default values + transform.anchor = MultiDimensional(NVector(0, 0)) + transform.position = MultiDimensional(NVector(0, 0)) + transform.scale = MultiDimensional(NVector(100, 100)) + transform.rotation = Value(0) + transform.start_opacity = Value(100) + transform.end_opacity = Value(100) + + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/repeater_transform)'): + if lines[idx].startswith('(tr_position'): + components = extract_numbers(lines[idx]) + if len(components) >= 2: + transform.position = MultiDimensional(NVector(components[0], components[1])) + elif lines[idx].startswith('(tr_anchor'): + components = extract_numbers(lines[idx]) + if len(components) >= 2: + transform.anchor = MultiDimensional(NVector(components[0], components[1])) + elif lines[idx].startswith('(tr_scale'): + components = extract_numbers(lines[idx]) + if len(components) >= 2: + transform.scale = MultiDimensional(NVector(components[0], components[1])) + elif lines[idx].startswith('(tr_rotation'): + # PARSE ROTATION VALUE - THIS WAS MISSING! + value = extract_number(lines[idx]) + transform.rotation = Value(value) + elif lines[idx].startswith('(tr_start_opacity'): + value = extract_number(lines[idx]) + transform.start_opacity = Value(value) + elif lines[idx].startswith('(tr_end_opacity'): + value = extract_number(lines[idx]) + transform.end_opacity = Value(value) + elif lines[idx].startswith('(tr_p_ix'): + transform.position_ix = int(extract_number(lines[idx])) + elif lines[idx].startswith('(tr_a_ix'): + transform.anchor_ix = int(extract_number(lines[idx])) + elif lines[idx].startswith('(tr_s_ix'): + transform.scale_ix = int(extract_number(lines[idx])) + elif lines[idx].startswith('(tr_r_ix'): + transform.rotation_ix = int(extract_number(lines[idx])) + elif lines[idx].startswith('(tr_so_ix'): + transform.start_opacity_ix = int(extract_number(lines[idx])) + elif lines[idx].startswith('(tr_eo_ix'): + transform.end_opacity_ix = int(extract_number(lines[idx])) + idx += 1 + + if lines[idx].startswith('(/repeater_transform)'): + idx += 1 + + repeater.transform = transform + elif line.strip() == '(/repeater)': + idx += 1 + break + else: + idx += 1 + + return repeater, idx + + +def parse_merge_tag(lines, idx): + """Parse merge tag and its contents""" + merge_attrs = parse_tag_attrs(lines[idx]) + + merge = Merge() + merge.name = merge_attrs.get("name", "") + + if "ix" in merge_attrs: + merge.property_index = int(merge_attrs.get("ix", 1)) + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(merge_mode'): + value = int(extract_number(line)) + merge.merge_mode = value + idx += 1 + elif line.strip() == '(/merge)': + idx += 1 + break + else: + idx += 1 + + return merge, idx + + +def parse_rounded_corners_tag(lines, idx): + """Parse rounded corners tag and its contents""" + rc_attrs = parse_tag_attrs(lines[idx]) + + rounded_corners = RoundedCorners() + rounded_corners.name = rc_attrs.get("name", "") + + if "ix" in rc_attrs: + rounded_corners.property_index = int(rc_attrs.get("ix", 1)) + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(radius'): + value = extract_number(line) + rounded_corners.radius = Value(value) + idx += 1 + elif line.strip() == '(/rounded_corners)': + idx += 1 + break + else: + idx += 1 + + return rounded_corners, idx + + +def parse_twist_tag(lines, idx): + """Parse twist tag and its contents""" + twist_attrs = parse_tag_attrs(lines[idx]) + + twist = Twist() + twist.name = twist_attrs.get("name", "") + + if "ix" in twist_attrs: + twist.property_index = int(twist_attrs.get("ix", 1)) + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(angle'): + value = extract_number(line) + twist.angle = Value(value) + idx += 1 + elif line.startswith('(center'): + components = extract_numbers(line) + if components: + twist.center = MultiDimensional(NVector(*components)) + idx += 1 + elif line.strip() == '(/twist)': + idx += 1 + break + else: + idx += 1 + + return twist, idx + +def parse_stroke_tag(lines, idx): + """""" + stroke_attrs = parse_tag_attrs(lines[idx]) + + stroke = Stroke() + stroke.name = stroke_attrs.get("name", "") + + # property_index + if "ix" in stroke_attrs: + stroke.property_index = int(stroke_attrs["ix"]) + + # bm + if "bm" in stroke_attrs: + stroke.bm = int(stroke_attrs.get("bm", 0)) + else: + stroke.bm = 0 + + # + if stroke_attrs.get("color_animated") == "true": + # - look for color_keyframe tags + stroke.color = ColorValue(Color(0, 0, 0)) + stroke.color.keyframes = [] + + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/color_animated)'): + if lines[idx].startswith('(color_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + time = float(kf_attrs.get('t', 0)) + r = float(kf_attrs.get('r', 0)) + g = float(kf_attrs.get('g', 0)) + b = float(kf_attrs.get('b', 0)) + + color = Color(r, g, b) + kf = Keyframe(time, color) + + if 'i_x' in kf_attrs and 'i_y' in kf_attrs: + kf.in_tan = { + 'x': [float(kf_attrs['i_x'])], + 'y': [float(kf_attrs['i_y'])] + } + + if 'o_x' in kf_attrs and 'o_y' in kf_attrs: + kf.out_tan = { + 'x': [float(kf_attrs['o_x'])], + 'y': [float(kf_attrs['o_y'])] + } + + stroke.color.keyframes.append(kf) + idx += 1 + else: + # + r = float(stroke_attrs.get("r", 0)) + g = float(stroke_attrs.get("g", 0)) + b = float(stroke_attrs.get("b", 0)) + stroke.color = ColorValue(Color(r, g, b)) + + # - + stroke.color_dimensions = int(stroke_attrs.get("color_dim", 3)) + + # aix + if "has_c_a" in stroke_attrs: + stroke.has_c_a = stroke_attrs["has_c_a"] == "True" + if "has_c_ix" in stroke_attrs: + stroke.has_c_ix = stroke_attrs["has_c_ix"] == "True" + if "c_ix" in stroke_attrs: + stroke.c_ix = int(stroke_attrs["c_ix"]) + + # line_capline_join - + if "lc" in stroke_attrs: + try: + lc_value = int(stroke_attrs["lc"]) + # LineCap: 1(Butt), 2(Round), 3(Square) + if lc_value in [1, 2, 3]: + stroke.line_cap = LineCap(lc_value) + else: + # Round(2) + stroke.line_cap = LineCap(2) + except (ValueError, KeyError): + stroke.line_cap = LineCap(2) + else: + stroke.line_cap = LineCap(2) + + if "lj" in stroke_attrs: + try: + lj_value = int(stroke_attrs["lj"]) + # LineJoin: 1(Miter), 2(Round), 3(Bevel) + if lj_value in [1, 2, 3]: + stroke.line_join = LineJoin(lj_value) + else: + # Round(2) + stroke.line_join = LineJoin(2) + except (ValueError, KeyError): + stroke.line_join = LineJoin(2) + else: + stroke.line_join = LineJoin(2) + + # miter_limit + if "ml" in stroke_attrs: + stroke.miter_limit = float(stroke_attrs["ml"]) + + # ml2 + if "ml2_animated" in stroke_attrs: + try: + keyframes_data = json.loads(stroke_attrs["ml2_animated"]) + ml2_value = Value(10) + ml2_value.keyframes = [] + for kf_data in keyframes_data: + time = kf_data.get('t', 0) + value = kf_data.get('s', 10) + kf = Keyframe(time, value) + if 'i' in kf_data: + kf.in_tan = kf_data['i'] + if 'o' in kf_data: + kf.out_tan = kf_data['o'] + ml2_value.keyframes.append(kf) + stroke.ml2 = ml2_value + except: + stroke.ml2 = Value(10) + elif "ml2" in stroke_attrs: + stroke.ml2 = Value(float(stroke_attrs["ml2"])) + + if "ml2_ix" in stroke_attrs: + stroke.ml2_ix = int(stroke_attrs["ml2_ix"]) + + # Parse width - check next line + if idx + 1 < len(lines): + if lines[idx + 1].startswith('(width_animated'): + stroke.width = Value(1) + stroke.width.keyframes = [] + idx += 2 # Skip the width_animated tag + + while idx < len(lines) and not lines[idx].startswith('(/width_animated)'): + if lines[idx].startswith('(width_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + time = float(kf_attrs.get('t', 0)) + value = float(kf_attrs.get('s', 1)) + + kf = Keyframe(time, value) + + if 'i_x' in kf_attrs and 'i_y' in kf_attrs: + kf.in_tan = { + 'x': float(kf_attrs['i_x']), + 'y': float(kf_attrs['i_y']) + } + + if 'o_x' in kf_attrs and 'o_y' in kf_attrs: + kf.out_tan = { + 'x': float(kf_attrs['o_x']), + 'y': float(kf_attrs['o_y']) + } + + stroke.width.keyframes.append(kf) + idx += 1 + + if stroke.width.keyframes: + stroke.width.value = stroke.width.keyframes[0].value + idx += 1 # Skip the /width_animated tag + elif lines[idx + 1].startswith('(width '): + # Static width in separate tag + idx += 1 + width_value = extract_number(lines[idx]) + stroke.width = Value(width_value) + idx += 1 + else: + stroke.width = Value(1) + idx += 1 + else: + stroke.width = Value(1) + idx += 1 + + # Parse opacity - check next line + if idx < len(lines): + if lines[idx].startswith('(opacity_animated'): + stroke.opacity = Value(100) + stroke.opacity.keyframes = [] + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/opacity_animated)'): + if lines[idx].startswith('(opacity_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + time = float(kf_attrs.get('t', 0)) + value = float(kf_attrs.get('s', 100)) + + kf = Keyframe(time, value) + + if 'i_x' in kf_attrs and 'i_y' in kf_attrs: + kf.in_tan = { + 'x': float(kf_attrs['i_x']), + 'y': float(kf_attrs['i_y']) + } + + if 'o_x' in kf_attrs and 'o_y' in kf_attrs: + kf.out_tan = { + 'x': float(kf_attrs['o_x']), + 'y': float(kf_attrs['o_y']) + } + + stroke.opacity.keyframes.append(kf) + idx += 1 + + if stroke.opacity.keyframes: + stroke.opacity.value = stroke.opacity.keyframes[0].value + idx += 1 + elif lines[idx].startswith('(opacity '): + # Static opacity in separate tag + opacity_value = extract_number(lines[idx]) + stroke.opacity = Value(opacity_value) + idx += 1 + else: + stroke.opacity = Value(100) + + # Dashes handling - updated to parse keyframe format + if idx < len(lines): + stroke.dashes = [] + + # Keep parsing dashes until we hit something that's not dash-related + while idx < len(lines): + if lines[idx].startswith('(dash_animated'): + # Animated dash + dash_attrs = parse_tag_attrs(lines[idx]) + dash = StrokeDash() + dash.type = StrokeDashType(dash_attrs.get('type', 'd')) + dash.name = dash_attrs.get('name', '') + if 'v_ix' in dash_attrs: + dash.v_ix = int(dash_attrs['v_ix']) + + dash.length = Value(0) + dash.length.keyframes = [] + + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/dash_animated)'): + if lines[idx].startswith('(dash_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + time = float(kf_attrs.get('t', 0)) + value = float(kf_attrs.get('s', 0)) + + kf = Keyframe(time, value) + + # Restore hold property if present + if 'h' in kf_attrs: + kf.hold = kf_attrs['h'] == 'True' if kf_attrs['h'] in ['True', 'False'] else bool(kf_attrs['h']) + + if 'i_x' in kf_attrs and 'i_y' in kf_attrs: + kf.in_tan = { + 'x': float(kf_attrs['i_x']), + 'y': float(kf_attrs['i_y']) + } + + if 'o_x' in kf_attrs and 'o_y' in kf_attrs: + kf.out_tan = { + 'x': float(kf_attrs['o_x']), + 'y': float(kf_attrs['o_y']) + } + + dash.length.keyframes.append(kf) + idx += 1 + + if dash.length.keyframes: + dash.length.value = dash.length.keyframes[0].value + + stroke.dashes.append(dash) + idx += 1 + + elif lines[idx].startswith('(dash '): + # Static dash + dash_attrs = parse_tag_attrs(lines[idx]) + dash = StrokeDash() + dash.type = StrokeDashType(dash_attrs.get('type', 'd')) + dash.length = Value(float(dash_attrs.get('length', 0))) + dash.name = dash_attrs.get('name', '') + if 'v_ix' in dash_attrs: + dash.v_ix = int(dash_attrs['v_ix']) + + stroke.dashes.append(dash) + idx += 1 + + elif lines[idx].startswith('(dashes'): + # Old format for backwards compatibility + dashes_str = extract_string_value(lines[idx]) + if dashes_str: + dash_pairs = dashes_str.split(';') + for dash_pair in dash_pairs: + parts = dash_pair.split('|') + if len(parts) >= 2: + dash = StrokeDash() + dash.type = StrokeDashType(parts[0]) + + try: + dash.length = Value(float(parts[1])) + except ValueError: + dash.length = Value(0) + + if len(parts) > 2 and parts[2]: + dash.name = parts[2] + if len(parts) > 3 and parts[3]: + try: + dash.v_ix = int(parts[3]) + except: + pass + stroke.dashes.append(dash) + idx += 1 + break + else: + # Not a dash-related tag, stop parsing dashes + break + + return stroke, idx + + +def parse_rect_tag(lines, idx): + """""" + rect_attrs = parse_tag_attrs(lines[idx]) + + rect = Rect() + rect.name = rect_attrs.get("name", "") + + # property_index + if "ix" in rect_attrs: + rect.property_index = int(rect_attrs.get("ix", 1)) + # hd + if "hd" in rect_attrs: + rect.hd = rect_attrs.get("hd", "false").lower() == "true" + + # direction + if "d" in rect_attrs: + rect.direction = float(rect_attrs.get("d", 1)) + else: + rect.direction = 1 + + idx += 1 + while idx < len(lines): + line = lines[idx] + + # Handle animated position + if line.startswith('(position') and 'animated=true' in line: + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/position)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + value = NVector(*values) if values else NVector(0, 0) + else: + value = NVector(0, 0) + kf = Keyframe(time, value) + + # Handle easing parameters - + if 'i_x' in attrs and 'i_y' in attrs: + i_x = attrs['i_x'] + i_y = attrs['i_y'] + # + if ' ' in i_x: + i_x_vals = [float(v) for v in i_x.split()] + i_y_vals = [float(v) for v in i_y.split()] + else: + i_x_vals = [float(i_x)] + i_y_vals = [float(i_y)] + kf.in_tan = { + 'x': i_x_vals, + 'y': i_y_vals + } + + if 'o_x' in attrs and 'o_y' in attrs: + o_x = attrs['o_x'] + o_y = attrs['o_y'] + # + if ' ' in o_x: + o_x_vals = [float(v) for v in o_x.split()] + o_y_vals = [float(v) for v in o_y.split()] + else: + o_x_vals = [float(o_x)] + o_y_vals = [float(o_y)] + kf.out_tan = { + 'x': o_x_vals, + 'y': o_y_vals + } + + if 'to' in attrs: + try: + kf.to = json.loads(attrs['to']) + except: + pass + if 'ti' in attrs: + try: + kf.ti = json.loads(attrs['ti']) + except: + pass + + keyframes.append(kf) + idx += 1 + if keyframes: + rect.position = Value(keyframes[0].value) + rect.position.keyframes = keyframes + rect.position.animated = True + if idx < len(lines) and lines[idx].startswith('(/position)'): + idx += 1 + + # Handle static position + elif line.startswith('(position') and 'animated=true' not in line: + components = extract_numbers(line) + if components: + rect.position = Value(NVector(*components)) + idx += 1 + + # Handle animated size + elif line.startswith('(size') and 'animated=true' in line: + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/size)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + value = NVector(*values) if values else NVector(100, 100) + else: + value = NVector(100, 100) + kf = Keyframe(time, value) + + # Handle easing parameters - + if 'i_x' in attrs and 'i_y' in attrs: + i_x = attrs['i_x'] + i_y = attrs['i_y'] + # + if ' ' in i_x: + i_x_vals = [float(v) for v in i_x.split()] + i_y_vals = [float(v) for v in i_y.split()] + else: + i_x_vals = [float(i_x)] + i_y_vals = [float(i_y)] + kf.in_tan = { + 'x': i_x_vals, + 'y': i_y_vals + } + + if 'o_x' in attrs and 'o_y' in attrs: + o_x = attrs['o_x'] + o_y = attrs['o_y'] + # + if ' ' in o_x: + o_x_vals = [float(v) for v in o_x.split()] + o_y_vals = [float(v) for v in o_y.split()] + else: + o_x_vals = [float(o_x)] + o_y_vals = [float(o_y)] + kf.out_tan = { + 'x': o_x_vals, + 'y': o_y_vals + } + + keyframes.append(kf) + idx += 1 + if keyframes: + rect.size = Value(keyframes[0].value) + rect.size.keyframes = keyframes + rect.size.animated = True + if idx < len(lines) and lines[idx].startswith('(/size)'): + idx += 1 + + # Handle static size + elif line.startswith('(rect_size') and 'animated=true' not in line: + components = extract_numbers(line) + if components: + rect.size = Value(NVector(*components)) + idx += 1 + + # Handle animated rounded + elif line.startswith('(rounded') and 'animated=true' in line: + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/rounded)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + value = values[0] if values else 0 + else: + value = 0 + kf = Keyframe(time, value) + + # Handle easing parameters - + if 'i_x' in attrs and 'i_y' in attrs: + i_x = attrs['i_x'] + i_y = attrs['i_y'] + # + if ' ' in i_x: + i_x_vals = [float(v) for v in i_x.split()] + i_y_vals = [float(v) for v in i_y.split()] + else: + i_x_vals = [float(i_x)] + i_y_vals = [float(i_y)] + kf.in_tan = { + 'x': i_x_vals, + 'y': i_y_vals + } + + if 'o_x' in attrs and 'o_y' in attrs: + o_x = attrs['o_x'] + o_y = attrs['o_y'] + # + if ' ' in o_x: + o_x_vals = [float(v) for v in o_x.split()] + o_y_vals = [float(v) for v in o_y.split()] + else: + o_x_vals = [float(o_x)] + o_y_vals = [float(o_y)] + kf.out_tan = { + 'x': o_x_vals, + 'y': o_y_vals + } + + keyframes.append(kf) + idx += 1 + if keyframes: + rect.rounded = Value(keyframes[0].value) + rect.rounded.keyframes = keyframes + rect.rounded.animated = True + if idx < len(lines) and lines[idx].startswith('(/rounded)'): + idx += 1 + + # Handle static rounded - FIX: Parse numeric values correctly + elif line.startswith('(rounded') and 'animated=true' not in line: + components = extract_numbers(line) + if components: + # For rounded, we typically want just a single value + rect.rounded = Value(components[0] if components else 0) + idx += 1 + + elif line.strip() == '(/rect)': + idx += 1 + break + else: + idx += 1 + + return rect, idx + + +def parse_fill_tag(lines, idx): + """ - """ + fill_attrs = parse_tag_attrs(lines[idx]) + + fill = Fill() + fill.name = fill_attrs.get("name", "") + + # property_index + if "ix" in fill_attrs: + fill.property_index = int(fill_attrs.get("ix", 1)) + + # bm + if "bm" in fill_attrs: + fill.bm = int(fill_attrs.get("bm", 0)) + else: + fill.bm = 0 + + # Check for animated color + if fill_attrs.get("color_animated") == "true": + # Parse animated color keyframes + kf_count = int(fill_attrs.get("c_kf_count", 0)) + fill.color = ColorValue(Color(0, 0, 0)) + + # FIX: Ensure keyframes is a list, not None + if not hasattr(fill.color, 'keyframes'): + fill.color.keyframes = [] + elif fill.color.keyframes is None: + fill.color.keyframes = [] + + for i in range(kf_count): + time = float(fill_attrs.get(f"c_kf_{i}_t", 0)) + r = float(fill_attrs.get(f"c_kf_{i}_r", 0)) + g = float(fill_attrs.get(f"c_kf_{i}_g", 0)) + b = float(fill_attrs.get(f"c_kf_{i}_b", 0)) + + color = Color(r, g, b) + kf = Keyframe(time, color) + + # Parse tangents if present + if f"c_kf_{i}_i_x" in fill_attrs: + kf.in_tan = { + 'x': [float(fill_attrs.get(f"c_kf_{i}_i_x", 0.667))], + 'y': [float(fill_attrs.get(f"c_kf_{i}_i_y", 1))] + } + + if f"c_kf_{i}_o_x" in fill_attrs: + kf.out_tan = { + 'x': [float(fill_attrs.get(f"c_kf_{i}_o_x", 0.333))], + 'y': [float(fill_attrs.get(f"c_kf_{i}_o_y", 0))] + } + + fill.color.keyframes.append(kf) + else: + # Static color + try: + r = float(fill_attrs.get("r", 0)) + except ValueError: + r = 0 + try: + g = float(fill_attrs.get("g", 0)) + except ValueError: + g = 0 + try: + b = float(fill_attrs.get("b", 0)) + except ValueError: + b = 0 + + fill.color = ColorValue(Color(r, g, b)) + + # + fill.color_dimensions = int(fill_attrs.get("color_dim", 3)) + + # aix + if "has_c_a" in fill_attrs: + fill.has_c_a = fill_attrs["has_c_a"] == "True" + if "has_c_ix" in fill_attrs: + fill.has_c_ix = fill_attrs["has_c_ix"] == "True" + if "c_ix" in fill_attrs: + fill.c_ix = int(fill_attrs["c_ix"]) + + # Parse fill_rule if present - + if "fill_rule" in fill_attrs: + try: + fill_rule_value = int(fill_attrs.get("fill_rule", 1)) + # FillRule: 1(NonZero), 2(EvenOdd) + if fill_rule_value in [1, 2]: + fill.fill_rule = FillRule(fill_rule_value) + else: + # NonZero(1) + fill.fill_rule = FillRule(1) + except (ValueError, KeyError): + fill.fill_rule = FillRule(1) + + # Handle opacity - Fixed: properly parse animated opacity keyframes + if fill_attrs.get("opacity_animated") == "true": + # Parse animated opacity keyframes + kf_count = int(fill_attrs.get("o_kf_count", 0)) + opacity_value = Value(100) + opacity_value.animated = True + + # FIX: Ensure keyframes is a list, not None + if not hasattr(opacity_value, 'keyframes'): + opacity_value.keyframes = [] + elif opacity_value.keyframes is None: + opacity_value.keyframes = [] + + for i in range(kf_count): + time = float(fill_attrs.get(f"o_kf_{i}_t", 0)) + value = float(fill_attrs.get(f"o_kf_{i}_s", 100)) + + kf = Keyframe(time, value) + + # Parse tangents if present + if f"o_kf_{i}_i_x" in fill_attrs: + kf.in_tan = { + 'x': float(fill_attrs.get(f"o_kf_{i}_i_x", 0.667)), + 'y': float(fill_attrs.get(f"o_kf_{i}_i_y", 1)) + } + + if f"o_kf_{i}_o_x" in fill_attrs: + kf.out_tan = { + 'x': float(fill_attrs.get(f"o_kf_{i}_o_x", 0.333)), + 'y': float(fill_attrs.get(f"o_kf_{i}_o_y", 0)) + } + + opacity_value.keyframes.append(kf) + + fill.opacity = opacity_value + else: + # Static opacity + try: + fill.opacity = Value(float(fill_attrs.get("opacity", 100))) + except: + fill.opacity = Value(100) + + # aix + if "has_o_a" in fill_attrs: + fill.has_o_a = fill_attrs["has_o_a"] == "True" + if "has_o_ix" in fill_attrs: + fill.has_o_ix = fill_attrs["has_o_ix"] == "True" + if "o_ix" in fill_attrs: + fill.o_ix = int(fill_attrs["o_ix"]) + + return fill, idx + 1 + +def parse_effects_tag(lines, start_idx): + """""" + effects_list = [] + idx = start_idx + + while idx < len(lines): + line = lines[idx].strip() + + if line.startswith('(effect '): + # + effect_dict, new_idx = parse_effect_tag(lines, idx) + effects_list.append(effect_dict) + idx = new_idx + elif line.startswith('(/'): + # + break + else: + idx += 1 + + return effects_list, idx + + +def parse_effect_tag(lines, idx): + """""" + import json + + effect_attrs = parse_tag_attrs(lines[idx]) + + # + effect_dict = { + 'nm': effect_attrs.get("name", ""), + 'ty': int(effect_attrs.get("type", 5)), + 'ix': int(effect_attrs.get("index", 1)), + 'mn': effect_attrs.get("match_name", ""), + 'en': int(effect_attrs.get("enabled", 1)), + 'ef': [] + } + + # np + if "np" in effect_attrs: + effect_dict['np'] = int(effect_attrs.get("np", 0)) + + idx += 1 + + def parse_keyframes(lines, start_idx, end_tag): + """""" + keyframes = [] + idx = start_idx + + while idx < len(lines): + line = lines[idx].strip() + + if line.startswith('(keyframe'): + kf_attrs = parse_tag_attrs(line) + kf = { + 't': float(kf_attrs.get('t', 0)) + } + + # + if 's' in kf_attrs: + # slider, angle, checkbox, dropdown + kf['s'] = [float(kf_attrs['s'])] + elif 'x' in kf_attrs and 'y' in kf_attrs: + # Point + kf['s'] = [float(kf_attrs['x']), float(kf_attrs['y'])] + elif 'r' in kf_attrs and 'g' in kf_attrs and 'b' in kf_attrs: + # Color + color_val = [ + float(kf_attrs['r']), + float(kf_attrs['g']), + float(kf_attrs['b']) + ] + if 'a' in kf_attrs: + color_val.append(float(kf_attrs['a'])) + else: + color_val.append(1) + kf['s'] = color_val + + # + if 'i_x' in kf_attrs and 'i_y' in kf_attrs: + kf['i'] = { + 'x': [float(kf_attrs['i_x'])], + 'y': [float(kf_attrs['i_y'])] + } + + # + if 'o_x' in kf_attrs and 'o_y' in kf_attrs: + kf['o'] = { + 'x': [float(kf_attrs['o_x'])], + 'y': [float(kf_attrs['o_y'])] + } + + # hold + if 'h' in kf_attrs: + kf['h'] = int(kf_attrs['h']) + + keyframes.append(kf) + idx += 1 + + elif line.startswith(end_tag): + break + else: + idx += 1 + + return keyframes, idx + + while idx < len(lines): + line = lines[idx].strip() + + if line.startswith('(slider'): + slider_attrs = parse_tag_attrs(line) + + # + if slider_attrs.get('animated') == 'true': + # + idx += 1 + print("lines", lines) + keyframes, idx = parse_keyframes(lines, idx, '(/slider)') + + sub_effect = { + 'ty': 0, + 'nm': slider_attrs.get("name", ""), + 'mn': slider_attrs.get("match_name", ""), + 'ix': int(slider_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(slider_attrs.get("index", 0)) + } + } + else: + # + try: + slider_value = float(slider_attrs.get("value", 0)) + except (ValueError, TypeError): + slider_value = 0 + + sub_effect = { + 'ty': 0, + 'nm': slider_attrs.get("name", ""), + 'mn': slider_attrs.get("match_name", ""), + 'ix': int(slider_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': slider_value, + 'ix': int(slider_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(color'): + color_attrs = parse_tag_attrs(line) + + if color_attrs.get('animated') == 'true': + idx += 1 + keyframes, idx = parse_keyframes(lines, idx, '(/color)') + + sub_effect = { + 'ty': 2, + 'nm': color_attrs.get("name", ""), + 'mn': color_attrs.get("match_name", ""), + 'ix': int(color_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(color_attrs.get("index", 0)) + } + } + else: + sub_effect = { + 'ty': 2, + 'nm': color_attrs.get("name", ""), + 'mn': color_attrs.get("match_name", ""), + 'ix': int(color_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': [ + float(color_attrs.get("r", 0)), + float(color_attrs.get("g", 0)), + float(color_attrs.get("b", 0)), + 1 + ], + 'ix': int(color_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(angle'): + angle_attrs = parse_tag_attrs(line) + + if angle_attrs.get('animated') == 'true': + idx += 1 + keyframes, idx = parse_keyframes(lines, idx, '(/angle)') + + sub_effect = { + 'ty': 1, + 'nm': angle_attrs.get("name", ""), + 'mn': angle_attrs.get("match_name", ""), + 'ix': int(angle_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(angle_attrs.get("index", 0)) + } + } + else: + sub_effect = { + 'ty': 1, + 'nm': angle_attrs.get("name", ""), + 'mn': angle_attrs.get("match_name", ""), + 'ix': int(angle_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': float(angle_attrs.get("value", 0)), + 'ix': int(angle_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(point'): + point_attrs = parse_tag_attrs(line) + + if point_attrs.get('animated') == 'true': + idx += 1 + keyframes, idx = parse_keyframes(lines, idx, '(/point)') + + sub_effect = { + 'ty': 3, + 'nm': point_attrs.get("name", ""), + 'mn': point_attrs.get("match_name", ""), + 'ix': int(point_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(point_attrs.get("index", 0)) + } + } + else: + sub_effect = { + 'ty': 3, + 'nm': point_attrs.get("name", ""), + 'mn': point_attrs.get("match_name", ""), + 'ix': int(point_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': [ + float(point_attrs.get("x", 0)), + float(point_attrs.get("y", 0)) + ], + 'ix': int(point_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(checkbox'): + checkbox_attrs = parse_tag_attrs(line) + + if checkbox_attrs.get('animated') == 'true': + idx += 1 + keyframes, idx = parse_keyframes(lines, idx, '(/checkbox)') + + # + for kf in keyframes: + if 's' in kf and isinstance(kf['s'], list): + kf['s'] = [int(kf['s'][0])] + + sub_effect = { + 'ty': 4, + 'nm': checkbox_attrs.get("name", ""), + 'mn': checkbox_attrs.get("match_name", ""), + 'ix': int(checkbox_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(checkbox_attrs.get("index", 0)) + } + } + else: + sub_effect = { + 'ty': 4, + 'nm': checkbox_attrs.get("name", ""), + 'mn': checkbox_attrs.get("match_name", ""), + 'ix': int(checkbox_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': int(checkbox_attrs.get("value", 0)), + 'ix': int(checkbox_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(dropdown'): + dropdown_attrs = parse_tag_attrs(line) + + if dropdown_attrs.get('animated') == 'true': + idx += 1 + keyframes, idx = parse_keyframes(lines, idx, '(/dropdown)') + + # + for kf in keyframes: + if 's' in kf and isinstance(kf['s'], list): + kf['s'] = [int(kf['s'][0])] + + sub_effect = { + 'ty': 7, + 'nm': dropdown_attrs.get("name", ""), + 'mn': dropdown_attrs.get("match_name", ""), + 'ix': int(dropdown_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(dropdown_attrs.get("index", 0)) + } + } + else: + sub_effect = { + 'ty': 7, + 'nm': dropdown_attrs.get("name", ""), + 'mn': dropdown_attrs.get("match_name", ""), + 'ix': int(dropdown_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': int(dropdown_attrs.get("value", 1)), + 'ix': int(dropdown_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(layer_effect'): + layer_attrs = parse_tag_attrs(line) + + if layer_attrs.get('animated') == 'true': + idx += 1 + keyframes, idx = parse_keyframes(lines, idx, '(/layer_effect)') + + # + for kf in keyframes: + if 's' in kf and isinstance(kf['s'], list): + kf['s'] = [int(kf['s'][0])] + + sub_effect = { + 'ty': 10, + 'nm': layer_attrs.get("name", ""), + 'mn': layer_attrs.get("match_name", ""), + 'ix': int(layer_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(layer_attrs.get("index", 0)) + } + } + else: + try: + layer_value = int(layer_attrs.get("value", 0)) + except (ValueError, TypeError): + layer_value = 0 + + sub_effect = { + 'ty': 10, + 'nm': layer_attrs.get("name", ""), + 'mn': layer_attrs.get("match_name", ""), + 'ix': int(layer_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': layer_value, + 'ix': int(layer_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(ignored'): + ignored_attrs = parse_tag_attrs(line) + + if ignored_attrs.get('animated') == 'true': + idx += 1 + keyframes, idx = parse_keyframes(lines, idx, '(/ignored)') + + sub_effect = { + 'ty': 0, # slider + 'nm': ignored_attrs.get("name", ""), + 'mn': ignored_attrs.get("match_name", ""), + 'ix': int(ignored_attrs.get("index", 0)), + 'v': { + 'a': 1, + 'k': keyframes, + 'ix': int(ignored_attrs.get("index", 0)) + } + } + else: + try: + ignored_value = float(ignored_attrs.get("value", 0)) + except (ValueError, TypeError): + ignored_value = 0 + + sub_effect = { + 'ty': 0, # slider + 'nm': ignored_attrs.get("name", ""), + 'mn': ignored_attrs.get("match_name", ""), + 'ix': int(ignored_attrs.get("index", 0)), + 'v': { + 'a': 0, + 'k': ignored_value, + 'ix': int(ignored_attrs.get("index", 0)) + } + } + + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line.startswith('(no_value'): + no_value_attrs = parse_tag_attrs(line) + sub_effect = { + 'ty': 6, + 'nm': no_value_attrs.get("name", ""), + 'mn': no_value_attrs.get("match_name", ""), + 'ix': int(no_value_attrs.get("index", 0)), + 'v': 0 + } + effect_dict['ef'].append(sub_effect) + idx += 1 + + elif line == '(/effect)': + idx += 1 + break + else: + idx += 1 + + return effect_dict, idx + + +def parse_gradient_fill_tag(lines, idx): + """ - """ + attrs = parse_tag_attrs(lines[idx]) + + gradient_fill = GradientFill() + gradient_fill.name = attrs.get("name", "") + + if "ix" in attrs: + gradient_fill.property_index = int(attrs.get("ix", 1)) + + idx += 1 + colors_data = [] + original_array = None + color_points = 0 + + while idx < len(lines): + line = lines[idx] + + if line.startswith('(opacity'): + value = extract_number(line) + gradient_fill.opacity = Value(value) + idx += 1 + elif line.startswith('(fill_rule'): + value = int(extract_number(line)) + gradient_fill.fill_rule = FillRule(value) + idx += 1 + elif line.startswith('(start_point'): + components = extract_numbers(line) + if components: + gradient_fill.start_point = MultiDimensional(NVector(*components)) + idx += 1 + elif line.startswith('(end_point'): + components = extract_numbers(line) + if components: + gradient_fill.end_point = MultiDimensional(NVector(*components)) + idx += 1 + elif line.startswith('(gradient_type'): + value = int(extract_number(line)) + gradient_fill.gradient_type = GradientType(value) + idx += 1 + elif line.startswith('(highlight_length'): + value = extract_number(line) + gradient_fill.highlight_length = Value(value) + idx += 1 + elif line.startswith('(highlight_angle'): + value = extract_number(line) + gradient_fill.highlight_angle = Value(value) + idx += 1 + elif line.startswith('(original_colors'): + # + text = line[line.find(' ') + 1:].strip().rstrip(')') + if text: + try: + original_array = json.loads(text) + except: + pass + idx += 1 + elif line.startswith('(color_points'): + color_points = int(extract_number(line)) + idx += 1 + elif line.startswith('(colors'): + # + text = line[line.find(' ') + 1:].strip().rstrip(')') + if text: + for color_data in text.split(): + parts = color_data.split(',') + if len(parts) >= 4: + pos = float(parts[0]) + r = float(parts[1]) + g = float(parts[2]) + b = float(parts[3]) + colors_data.append((pos, Color(r, g, b))) + idx += 1 + elif line.strip() == '(/gradient_fill)': + idx += 1 + break + else: + idx += 1 + + # + if original_array: + # + gradient_fill._original_color_array = original_array + gradient_fill._color_points = color_points + + # + if color_points > 0: + values_per_point = len(original_array) / color_points + colors = [] + + if values_per_point >= 4: + step = int(values_per_point) + for i in range(color_points): + base = i * step + pos = original_array[base] + r = original_array[base + 1] + g = original_array[base + 2] + b = original_array[base + 3] + colors.append((pos, Color(r, g, b))) + gradient_fill.colors = GradientColors(colors) + elif colors_data: + gradient_fill.colors = GradientColors(colors_data) + else: + # + gradient_fill.colors = GradientColors([ + (0.0, Color(0.85, 0.36, 0.33)), + (0.15, Color(0.84, 1.0, 0.0)) + ]) + + return gradient_fill, idx + + +def parse_transform_shape_tag(lines, idx): + """TransformShape - """ + transform_attrs = parse_tag_attrs(lines[idx]) + + transform = TransformShape() + transform.name = transform_attrs.get("name", "") + + # property_index + if "ix" in transform_attrs: + transform.property_index = int(transform_attrs.get("ix")) + + if "hd" in transform_attrs: + transform.hd = transform_attrs.get("hd", "false").lower() == "true" + + # + if "position" in transform_attrs: + values = [float(x) for x in transform_attrs["position"].split()] + transform.position = MultiDimensional(NVector(*values)) + + if "scale" in transform_attrs: + values = [float(x) for x in transform_attrs["scale"].split()] + if len(values) == 2: + transform.scale = MultiDimensional(NVector(values[0], values[1])) + elif len(values) == 3: + transform.scale = MultiDimensional(NVector(values[0], values[1], values[2])) + else: + transform.scale = MultiDimensional(NVector(values[0], values[0])) + + if "rotation" in transform_attrs: + transform.rotation = Value(float(transform_attrs["rotation"])) + + if "opacity" in transform_attrs: + transform.opacity = Value(float(transform_attrs["opacity"])) + + if "anchor" in transform_attrs: + values = [float(x) for x in transform_attrs["anchor"].split()] + transform.anchor = MultiDimensional(NVector(*values)) + + if "skew" in transform_attrs: + transform.skew = Value(float(transform_attrs["skew"])) + + if "skew_axis" in transform_attrs: + transform.skew_axis = Value(float(transform_attrs["skew_axis"])) + + # + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(position') and 'animated=true' in line: + # position + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/position)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + value = NVector(*values) if values else NVector(0, 0, 0) + else: + value = NVector(0, 0, 0) + kf = Keyframe(time, value) + + # - float + if 'i_x' in attrs and 'i_y' in attrs: + kf.in_tan = { + 'x': float(attrs['i_x']), + 'y': float(attrs['i_y']) + } + + if 'o_x' in attrs and 'o_y' in attrs: + kf.out_tan = { + 'x': float(attrs['o_x']), + 'y': float(attrs['o_y']) + } + + if 'to' in attrs: + try: + kf.to = json.loads(attrs['to']) + except: + pass + if 'ti' in attrs: + try: + kf.ti = json.loads(attrs['ti']) + except: + pass + + keyframes.append(kf) + idx += 1 + if keyframes: + transform.position = MultiDimensional(keyframes[0].value) + transform.position.keyframes = keyframes + if idx < len(lines) and lines[idx].startswith('(/position)'): + idx += 1 + + + elif line.startswith('(scale'): + if 'separated=true' in line: + # Handle separated scale + transform.scale = Value(NVector(100, 100)) + transform.scale.separated = True + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/scale)'): + if lines[idx].startswith('(scale_x'): + value = extract_number(lines[idx]) + transform.scale.x = Value(value) + elif lines[idx].startswith('(scale_y'): + value = extract_number(lines[idx]) + transform.scale.y = Value(value) + elif lines[idx].startswith('(scale_z'): + value = extract_number(lines[idx]) + transform.scale.z = Value(value) + idx += 1 + if idx < len(lines) and lines[idx].startswith('(/scale)'): + idx += 1 + elif 'animated=true' in line: + # scale + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/scale)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + s_str = attrs.get('s', '') + if s_str: + values = [float(x) for x in s_str.split()] + if len(values) == 2: + value = NVector(values[0], values[1]) + elif len(values) == 3: + value = NVector(values[0], values[1], values[2]) + else: + value = NVector(100, 100) + else: + value = NVector(100, 100) + + kf = Keyframe(time, value) + + # - + if 'i_x' in attrs and 'i_y' in attrs: + i_x_str = attrs['i_x'] + i_y_str = attrs['i_y'] + + if ' ' in i_x_str: + i_x = [float(v) for v in i_x_str.split()] + i_y = [float(v) for v in i_y_str.split()] + else: + i_x = float(i_x_str) + i_y = float(i_y_str) + + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x_str = attrs['o_x'] + o_y_str = attrs['o_y'] + + if ' ' in o_x_str: + o_x = [float(v) for v in o_x_str.split()] + o_y = [float(v) for v in o_y_str.split()] + else: + o_x = float(o_x_str) + o_y = float(o_y_str) + + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + + if keyframes: + transform.scale = MultiDimensional(keyframes[0].value) + transform.scale.keyframes = keyframes + else: + if not hasattr(transform, 'scale'): + transform.scale = MultiDimensional(NVector(100, 100)) + + if idx < len(lines) and lines[idx].startswith('(/scale)'): + idx += 1 + else: + idx += 1 + + elif line.startswith('(rotation'): + if 'separated=true' in line: + transform.rotation = Value(0) + transform.rotation.separated = True + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/rotation)'): + if lines[idx].startswith('(rotation_x'): + value = extract_number(lines[idx]) + transform.rotation.x = Value(value) + elif lines[idx].startswith('(rotation_y'): + value = extract_number(lines[idx]) + transform.rotation.y = Value(value) + elif lines[idx].startswith('(rotation_z'): + value = extract_number(lines[idx]) + transform.rotation.z = Value(value) + idx += 1 + if idx < len(lines) and lines[idx].startswith('(/rotation)'): + idx += 1 + elif 'animated=true' in line: + # rotation + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/rotation)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 0)) + kf = Keyframe(time, value) + + # + if 'i_x' in attrs and 'i_y' in attrs: + i_x = float(attrs['i_x']) + i_y = float(attrs['i_y']) + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x = float(attrs['o_x']) + o_y = float(attrs['o_y']) + kf.out_tan = {'x': o_x, 'y': o_y} + + keyframes.append(kf) + idx += 1 + if keyframes: + transform.rotation = Value(keyframes[0].value) + transform.rotation.keyframes = keyframes + if idx < len(lines) and lines[idx].startswith('(/rotation)'): + idx += 1 + else: + idx += 1 + + elif line.startswith('(opacity') and 'animated=true' in line: + # opacity + idx += 1 + keyframes = [] + while idx < len(lines) and not lines[idx].startswith('(/opacity)'): + if lines[idx].startswith('(keyframe'): + attrs = parse_tag_attrs(lines[idx]) + time = float(attrs.get('t', 0)) + value = float(attrs.get('s', 100)) + kf = Keyframe(time, value) + + # + if 'i_x' in attrs and 'i_y' in attrs: + i_x = float(attrs['i_x']) + i_y = float(attrs['i_y']) + kf.in_tan = {'x': i_x, 'y': i_y} + + if 'o_x' in attrs and 'o_y' in attrs: + o_x = float(attrs['o_x']) + o_y = float(attrs['o_y']) + kf.out_tan = {'x': o_x, 'y': o_y} + + # h + if 'h' in attrs: + kf.h = int(attrs['h']) + + keyframes.append(kf) + idx += 1 + if keyframes: + transform.opacity = Value(keyframes[0].value) + transform.opacity.keyframes = keyframes + if idx < len(lines) and lines[idx].startswith('(/opacity)'): + idx += 1 + + elif line.startswith('(/'): # Any closing tag + break + else: + idx += 1 + + return transform, idx + + + +def parse_path_tag(lines, idx): + """ - h""" + path_attrs = parse_tag_attrs(lines[idx]) + + path = Path() + path.name = path_attrs.get("name", "") + + # property_index + if "ix" in path_attrs: + path.property_index = int(path_attrs.get("ix", 1)) + if "d" in path_attrs: + path.d = int(path_attrs.get("d", 1)) + + if "ind" in path_attrs: + path.ind = int(path_attrs.get("ind", 1)) + else: + path.ind = 1 # Default value + + if "hd" in path_attrs: + path.hd = path_attrs.get("hd", "false").lower() == "true" + + if "mn" in path_attrs: + path.mn = path_attrs.get("mn", "") + + # Check if animated + is_animated = path_attrs.get("animated", "").lower() == "true" + + if is_animated: + # Handle animated path + keyframes = [] + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/path)'): + if lines[idx].startswith('(keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + time = float(kf_attrs.get("t", 0)) + + kf = Keyframe(time, None) + + if "i_x" in kf_attrs and "i_y" in kf_attrs: + kf.in_tan = { + "x": float(kf_attrs["i_x"]), + "y": float(kf_attrs["i_y"]) + } + + if "o_x" in kf_attrs and "o_y" in kf_attrs: + kf.out_tan = { + "x": float(kf_attrs["o_x"]), + "y": float(kf_attrs["o_y"]) + } + + # h + if "h" in kf_attrs: + kf.h = int(kf_attrs["h"]) + + idx += 1 + + if lines[idx].startswith('(bezier'): + bezier_attrs = parse_tag_attrs(lines[idx]) + + bezier = Bezier() + bezier.closed = bezier_attrs.get("closed", "").lower() == "true" + + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/bezier)'): + if lines[idx].startswith('(point'): + point_attrs = parse_tag_attrs(lines[idx]) + + x = float(point_attrs.get("x", 0)) + y = float(point_attrs.get("y", 0)) + in_x = float(point_attrs.get("in_x", 0)) + in_y = float(point_attrs.get("in_y", 0)) + out_x = float(point_attrs.get("out_x", 0)) + out_y = float(point_attrs.get("out_y", 0)) + + bezier.add_point( + NVector(x, y), + NVector(in_x, in_y), + NVector(out_x, out_y) + ) + + idx += 1 + + kf.value = bezier + + if idx < len(lines) and lines[idx].startswith('(/bezier)'): + idx += 1 + + keyframes.append(kf) + + if idx < len(lines) and lines[idx].startswith('(/keyframe)'): + idx += 1 + else: + idx += 1 + + if keyframes: + path.shape = Value(keyframes[0].value) + path.shape.keyframes = keyframes + + if idx < len(lines) and lines[idx].startswith('(/path)'): + idx += 1 + else: + # Static path + bezier = Bezier() + bezier.closed = path_attrs.get("closed", "true").lower() == "true" + + idx += 1 + while idx < len(lines): + if lines[idx].startswith('(point'): + point_attrs = parse_tag_attrs(lines[idx]) + + x = float(point_attrs.get("x", 0)) + y = float(point_attrs.get("y", 0)) + in_x = float(point_attrs.get("in_x", 0)) + in_y = float(point_attrs.get("in_y", 0)) + out_x = float(point_attrs.get("out_x", 0)) + out_y = float(point_attrs.get("out_y", 0)) + + bezier.add_point(NVector(x, y), NVector(in_x, in_y), NVector(out_x, out_y)) + idx += 1 + elif lines[idx].strip() == '(/path)': + idx += 1 + break + else: + idx += 1 + + path.shape = Value(bezier) + + return path, idx + + + + +def parse_ellipse_tag(lines, idx): + """""" + ellipse_attrs = parse_tag_attrs(lines[idx]) + + ellipse = Ellipse() + ellipse.name = ellipse_attrs.get("name", "") + + # property_index + if "ix" in ellipse_attrs: + ellipse.property_index = int(ellipse_attrs.get("ix", 1)) + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(ellipse_position animated'): + # animated position keyframes + keyframes = [] + idx += 1 + while idx < len(lines): + if lines[idx].startswith('(/ellipse_position)'): + idx += 1 + break + elif lines[idx].startswith('(keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + kf_dict = {'t': float(kf_attrs.get('t', 0))} + if 's' in kf_attrs: + s_vals = [float(v) for v in kf_attrs['s'].split()] + kf_dict['s'] = s_vals + if 'i_x' in kf_attrs and 'i_y' in kf_attrs: + i_x = [float(v) for v in kf_attrs['i_x'].split()] if ' ' in kf_attrs['i_x'] else float(kf_attrs['i_x']) + i_y = [float(v) for v in kf_attrs['i_y'].split()] if ' ' in kf_attrs['i_y'] else float(kf_attrs['i_y']) + kf_dict['i'] = {'x': i_x, 'y': i_y} + if 'o_x' in kf_attrs and 'o_y' in kf_attrs: + o_x = [float(v) for v in kf_attrs['o_x'].split()] if ' ' in kf_attrs['o_x'] else float(kf_attrs['o_x']) + o_y = [float(v) for v in kf_attrs['o_y'].split()] if ' ' in kf_attrs['o_y'] else float(kf_attrs['o_y']) + kf_dict['o'] = {'x': o_x, 'y': o_y} + if 'h' in kf_attrs: + kf_dict['h'] = int(kf_attrs['h']) + if 'to' in kf_attrs: + kf_dict['to'] = [float(v) for v in kf_attrs['to'].split()] + if 'ti' in kf_attrs: + kf_dict['ti'] = [float(v) for v in kf_attrs['ti'].split()] + keyframes.append(kf_dict) + idx += 1 + else: + # keyframes + break + ellipse.position = MultiDimensional() + nvec = NVector() + nvec.components = keyframes + ellipse.position.value = nvec + ellipse.position.animated = True + elif line.startswith('(ellipse_position'): + components = extract_numbers(line) + if components: + ellipse.position = MultiDimensional(NVector(*components)) + idx += 1 + elif line.startswith('(ellipse_size animated'): + # animated size keyframes + keyframes = [] + idx += 1 + while idx < len(lines): + if lines[idx].startswith('(/ellipse_size)'): + idx += 1 + break + elif lines[idx].startswith('(keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + kf_dict = {'t': float(kf_attrs.get('t', 0))} + if 's' in kf_attrs: + s_vals = [float(v) for v in kf_attrs['s'].split()] + kf_dict['s'] = s_vals + if 'i_x' in kf_attrs and 'i_y' in kf_attrs: + i_x = [float(v) for v in kf_attrs['i_x'].split()] if ' ' in kf_attrs['i_x'] else float(kf_attrs['i_x']) + i_y = [float(v) for v in kf_attrs['i_y'].split()] if ' ' in kf_attrs['i_y'] else float(kf_attrs['i_y']) + kf_dict['i'] = {'x': i_x, 'y': i_y} + if 'o_x' in kf_attrs and 'o_y' in kf_attrs: + o_x = [float(v) for v in kf_attrs['o_x'].split()] if ' ' in kf_attrs['o_x'] else float(kf_attrs['o_x']) + o_y = [float(v) for v in kf_attrs['o_y'].split()] if ' ' in kf_attrs['o_y'] else float(kf_attrs['o_y']) + kf_dict['o'] = {'x': o_x, 'y': o_y} + if 'h' in kf_attrs: + kf_dict['h'] = int(kf_attrs['h']) + keyframes.append(kf_dict) + idx += 1 + else: + # keyframes + break + ellipse.size = MultiDimensional() + nvec = NVector() + nvec.components = keyframes + ellipse.size.value = nvec + ellipse.size.animated = True + elif line.startswith('(ellipse_size'): + components = extract_numbers(line) + if components: + ellipse.size = MultiDimensional(NVector(*components)) + idx += 1 + elif line.strip() == '(/ellipse)': + idx += 1 + break + else: + idx += 1 + + return ellipse, idx + + +def parse_gradient_stroke_tag(lines, idx): + """ - ml2""" + gradient_stroke_attrs = parse_tag_attrs(lines[idx]) + + gradient_stroke = GradientStroke() + gradient_stroke.name = gradient_stroke_attrs.get("name", "") + + if "ix" in gradient_stroke_attrs: + gradient_stroke.property_index = int(gradient_stroke_attrs.get("ix", 1)) + + idx += 1 + original_array = None + color_points = 0 + + while idx < len(lines): + line = lines[idx] + + if line.startswith('(opacity'): + value = extract_number(line) + gradient_stroke.opacity = Value(value) + idx += 1 + elif line.startswith('(width'): + value = extract_number(line) + gradient_stroke.width = Value(value) + idx += 1 + elif line.startswith('(line_cap'): + value = int(extract_number(line)) + gradient_stroke.line_cap = LineCap(value) + idx += 1 + elif line.startswith('(line_join'): + value = int(extract_number(line)) + gradient_stroke.line_join = LineJoin(value) + idx += 1 + elif line.startswith('(miter_limit'): + value = extract_number(line) + gradient_stroke.miter_limit = value + idx += 1 + elif line.startswith('(ml2_ix'): + gradient_stroke.ml2_ix = int(extract_number(line)) + idx += 1 + elif line.startswith('(ml2'): + value = extract_number(line) + gradient_stroke.ml2 = Value(value) + idx += 1 + elif line.startswith('(start_point'): + components = extract_numbers(line) + gradient_stroke.start_point = MultiDimensional(NVector(*components)) + # animated + idx += 1 + elif line.startswith('(end_point'): + components = extract_numbers(line) + gradient_stroke.end_point = MultiDimensional(NVector(*components)) + # animated + idx += 1 + elif line.startswith('(gradient_type'): + value = int(extract_number(line)) + gradient_stroke.gradient_type = GradientType(value) + idx += 1 + elif line.startswith('(highlight_length'): + value = extract_number(line) + gradient_stroke.highlight_length = Value(value) + idx += 1 + elif line.startswith('(highlight_angle'): + value = extract_number(line) + gradient_stroke.highlight_angle = Value(value) + idx += 1 + elif line.startswith('(original_colors'): + # + text = line[line.find(' ') + 1:].strip().rstrip(')') + if text: + try: + original_array = json.loads(text) + gradient_stroke._original_color_array = original_array + except: + pass + idx += 1 + elif line.startswith('(color_points'): + color_points = int(extract_number(line)) + gradient_stroke._color_points = color_points + idx += 1 + elif line.startswith('(colors'): + # + text = line[line.find(' ') + 1:].strip().rstrip(')') + colors = [] + + for color_data in text.split(): + parts = color_data.split(',') + if len(parts) >= 4: + pos = float(parts[0]) + r = float(parts[1]) + g = float(parts[2]) + b = float(parts[3]) + colors.append((pos, Color(r, g, b))) + + gradient_stroke.colors = GradientColors(colors) + idx += 1 + elif line.strip() == '(/gradient_stroke)': + idx += 1 + break + else: + idx += 1 + + # + if original_array and color_points > 0: + gradient_stroke._original_color_array = original_array + gradient_stroke._color_points = color_points + + # + colors = [] + values_per_point = len(original_array) / color_points + if values_per_point >= 4: + step = int(values_per_point) + for i in range(color_points): + base = i * step + pos = original_array[base] + r = original_array[base + 1] + g = original_array[base + 2] + b = original_array[base + 3] + colors.append((pos, Color(r, g, b))) + gradient_stroke.colors = GradientColors(colors) + + return gradient_stroke, idx + + +def parse_precomp_layer_tag(lines, idx): + """Parse a precomp layer tag and its contents""" + precomp_attrs = parse_tag_attrs(lines[idx]) + + precomp_layer = PreCompLayer() + precomp_layer.index = int(float(precomp_attrs.get("index", 0))) + precomp_layer.name = precomp_attrs.get("name", "PreComp Layer") + precomp_layer.in_point = float(precomp_attrs.get("in_point", 0)) + precomp_layer.out_point = float(precomp_attrs.get("out_point", 60)) + precomp_layer.start_time = float(precomp_attrs.get("start_time", 0)) + + if "w" in precomp_attrs: + precomp_layer.w = float(precomp_attrs["w"]) + if "h" in precomp_attrs: + precomp_layer.h = float(precomp_attrs["h"]) + + if "tt" in precomp_attrs: + precomp_layer.tt = float(precomp_attrs["tt"]) + if "tp" in precomp_attrs: + precomp_layer.tp = float(precomp_attrs["tp"]) + if "td" in precomp_attrs: + precomp_layer.td = float(precomp_attrs["td"]) + + if "hasMask" in precomp_attrs: + precomp_layer.hasMask = precomp_attrs["hasMask"] == "true" + if "hd" in precomp_attrs: + precomp_layer.hd = precomp_attrs["hd"] == "true" + if "cp" in precomp_attrs: + precomp_layer.cp = precomp_attrs["cp"] == "true" + + + #if "ln" in precomp_attrs: + # precomp_layer.ln = float(precomp_attrs["ln"]) + + idx += 1 + while idx < len(lines): + line = lines[idx] + + if line.startswith('(reference_id'): + reference_id = line[line.find('"')+1:line.rfind('"')] + precomp_layer.reference_id = reference_id + idx += 1 + elif line.startswith('(dimensions'): + dim_attrs = parse_tag_attrs(line) + precomp_layer.width = int(float(dim_attrs.get("width", 512))) + precomp_layer.height = int(float(dim_attrs.get("height", 512))) + idx += 1 + elif line.startswith('(masksProperties'): + # masksProperties + precomp_layer.masksProperties = [] + idx += 1 + + while idx < len(lines) and not lines[idx].startswith('(/masksProperties)'): + if lines[idx].startswith('(mask '): + # mask + mask_attrs = parse_tag_attrs(lines[idx]) + mask = {} + + # + if "inv" in mask_attrs: + mask["inv"] = mask_attrs["inv"].lower() == "true" + if "mode" in mask_attrs: + mask["mode"] = mask_attrs["mode"] + if "nm" in mask_attrs: + mask["nm"] = mask_attrs["nm"] + + idx += 1 + + # mask + while idx < len(lines) and not lines[idx].startswith('(/mask)'): + if lines[idx].startswith('(mask_pt '): + # pt + pt_attrs = parse_tag_attrs(lines[idx]) + mask["pt"] = {} + if "a" in pt_attrs: + mask["pt"]["a"] = int(pt_attrs["a"]) + if "ix" in pt_attrs: + mask["pt"]["ix"] = int(pt_attrs["ix"]) + + idx += 1 + + # pt.k + if idx < len(lines) and lines[idx].startswith('(mask_pt_k'): + if lines[idx].startswith('(mask_pt_k_array'): + # k + mask["pt"]["k"] = [] + idx += 1 + + # + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k_array)'): + if lines[idx].startswith('(mask_pt_keyframe'): + kf_attrs = parse_tag_attrs(lines[idx]) + keyframe = {} + + if "t" in kf_attrs: + keyframe["t"] = float(kf_attrs["t"]) + + idx += 1 + + # ... + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_keyframe)'): + if lines[idx].startswith('(mask_pt_kf_i'): + i_attrs = parse_tag_attrs(lines[idx]) + keyframe["i"] = { + "x": float(i_attrs.get("x", 0)), + "y": float(i_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_o'): + o_attrs = parse_tag_attrs(lines[idx]) + keyframe["o"] = { + "x": float(o_attrs.get("x", 0)), + "y": float(o_attrs.get("y", 0)) + } + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_s'): + keyframe["s"] = [] + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_s)'): + if lines[idx].startswith('(mask_pt_kf_shape'): + shape_attrs = parse_tag_attrs(lines[idx]) + shape = {} + if "c" in shape_attrs: + shape["c"] = shape_attrs["c"].lower() == "true" + idx += 1 + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_kf_shape)'): + if lines[idx].startswith('(mask_pt_kf_shape_i'): + values = extract_numbers(lines[idx]) + shape["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["i"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_o'): + values = extract_numbers(lines[idx]) + shape["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["o"].append([values[i], values[i+1]]) + idx += 1 + elif lines[idx].startswith('(mask_pt_kf_shape_v'): + values = extract_numbers(lines[idx]) + shape["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + shape["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_shape)'): + idx += 1 + keyframe["s"].append(shape) + else: + idx += 1 + if lines[idx].startswith('(/mask_pt_kf_s)'): + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_keyframe)'): + idx += 1 + + mask["pt"]["k"].append(keyframe) + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k_array)'): + idx += 1 + + else: + # kshape + mask["pt"]["k"] = {} + idx += 1 + + # shape + while idx < len(lines) and not lines[idx].startswith('(/mask_pt_k)'): + if lines[idx].startswith('(mask_pt_k_c'): + # closed + parts = lines[idx].split() + if len(parts) > 1: + mask["pt"]["k"]["c"] = parts[1].rstrip(')').lower() == "true" + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_i'): + # i + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["i"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["i"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_o'): + # o + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["o"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["o"].append([values[i], values[i+1]]) + idx += 1 + + elif lines[idx].startswith('(mask_pt_k_v'): + # v + values = extract_numbers(lines[idx]) + mask["pt"]["k"]["v"] = [] + for i in range(0, len(values), 2): + if i + 1 < len(values): + mask["pt"]["k"]["v"].append([values[i], values[i+1]]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask_pt_k)'): + idx += 1 + + if idx < len(lines) and lines[idx].startswith('(/mask_pt)'): + idx += 1 + + elif lines[idx].startswith('(mask_o '): + # opacity + o_attrs = parse_tag_attrs(lines[idx]) + mask["o"] = {} + if "a" in o_attrs: + mask["o"]["a"] = int(o_attrs["a"]) + if "k" in o_attrs: + mask["o"]["k"] = float(o_attrs["k"]) + if "ix" in o_attrs: + mask["o"]["ix"] = int(o_attrs["ix"]) + idx += 1 + + elif lines[idx].startswith('(mask_x '): + # dilate + x_attrs = parse_tag_attrs(lines[idx]) + mask["x"] = {} + if "a" in x_attrs: + mask["x"]["a"] = int(x_attrs["a"]) + if "k" in x_attrs: + mask["x"]["k"] = float(x_attrs["k"]) + if "ix" in x_attrs: + mask["x"]["ix"] = int(x_attrs["ix"]) + idx += 1 + else: + idx += 1 + + if lines[idx].startswith('(/mask)'): + idx += 1 + + precomp_layer.masksProperties.append(mask) + else: + idx += 1 + + if lines[idx].startswith('(/masksProperties)'): + idx += 1 + + + + elif lines[idx].startswith('(effects'): + # effects + effects, new_idx = parse_effects_tag(lines, idx) + precomp_layer.ef = effects + idx = new_idx + + elif line.startswith('(ct'): + # ct + ct_value = extract_number(line) + precomp_layer.ct = ct_value + idx += 1 + + elif line.startswith('(tm '): + # tm + tm_attrs = parse_tag_attrs(line) + tm_data = {} + if 'a' in tm_attrs: + tm_data['a'] = int(tm_attrs['a']) + if 'ix' in tm_attrs: + tm_data['ix'] = int(tm_attrs['ix']) + + # keyframes + idx += 1 + keyframes = [] + while idx < len(lines): + line = lines[idx] + if line.startswith('(keyframe'): + kf_attrs = parse_tag_attrs(line) + kf = {} + if 't' in kf_attrs: + kf['t'] = float(kf_attrs['t']) + if 's' in kf_attrs: + kf['s'] = [float(kf_attrs['s'])] + if 'h' in kf_attrs: + kf['h'] = int(kf_attrs['h']) + + # - + if 'i_x' in kf_attrs or 'i_y' in kf_attrs: + kf['i'] = {} + if 'i_x' in kf_attrs: + kf['i']['x'] = float(kf_attrs['i_x']) # + if 'i_y' in kf_attrs: + kf['i']['y'] = float(kf_attrs['i_y']) # + + if 'o_x' in kf_attrs or 'o_y' in kf_attrs: + kf['o'] = {} + if 'o_x' in kf_attrs: + kf['o']['x'] = float(kf_attrs['o_x']) # + if 'o_y' in kf_attrs: + kf['o']['y'] = float(kf_attrs['o_y']) # + + keyframes.append(kf) + idx += 1 + elif line.startswith('(value'): + # + val = extract_number(line) + tm_data['k'] = val + idx += 1 + elif line.strip() == '(/tm)': + if keyframes: + tm_data['k'] = keyframes + idx += 1 + break + else: + idx += 1 + + precomp_layer.tm = tm_data + + elif line.startswith('(parent'): + parent_index = int(extract_number(line)) + precomp_layer.parent_index = parent_index + idx += 1 + elif line.startswith('(transform'): + transform, new_idx = parse_transform_tag(lines, idx) + precomp_layer.transform = transform + idx = new_idx + elif line.strip() == '(/precomp_layer)': + idx += 1 + break + else: + idx += 1 + + return precomp_layer, idx + + +def parse_tag_attrs(line): + """Parse tag attributes, including complex values like JSON arrays""" + # Extract tag content + content = line[1:-1] # Remove parentheses + + # Find first space to separate tag name and attributes + first_space = content.find(' ') + if first_space == -1: + return {} + + tag_name = content[:first_space] + attrs_part = content[first_space+1:] + + # Handle quoted tag name + if tag_name.startswith('"') and tag_name.endswith('"'): + tag_name = tag_name[1:-1] + + # Parse attributes + attrs = {} + + # Process the attributes string with proper handling for complex values + i = 0 + while i < len(attrs_part): + # Skip whitespace + while i < len(attrs_part) and attrs_part[i].isspace(): + i += 1 + + if i >= len(attrs_part): + break + + # Find attribute name + start = i + while i < len(attrs_part) and attrs_part[i] not in "= \t\n\r": + i += 1 + + if i >= len(attrs_part): + break + + attr_name = attrs_part[start:i] + + # Skip to the value + while i < len(attrs_part) and (attrs_part[i].isspace() or attrs_part[i] == '='): + i += 1 + + if i >= len(attrs_part): + break + + # Parse attribute value based on its format + if attrs_part[i] == '"': + # Quoted string + i += 1 # Skip opening quote + start = i + while i < len(attrs_part) and attrs_part[i] != '"': + i += 1 + + attr_value = attrs_part[start:i] + i += 1 # Skip closing quote + elif attrs_part[i] == '[': + # Handle JSON array or list + bracket_count = 1 + start = i + i += 1 + + while i < len(attrs_part) and bracket_count > 0: + if attrs_part[i] == '[': + bracket_count += 1 + elif attrs_part[i] == ']': + bracket_count -= 1 + + i += 1 + + attr_value = attrs_part[start:i] + else: + # Regular value + start = i + while i < len(attrs_part) and not attrs_part[i].isspace(): + i += 1 + + attr_value = attrs_part[start:i] + + attrs[attr_name] = attr_value + + return attrs + + +def extract_numbers(line): + """""" + # + start_idx = line.find(' ') + if start_idx == -1: + return [] + content = line[start_idx:].strip() + + # + return [float(x) for x in re.findall(r'-?\d+\.?\d*', content)] + +def extract_number(line): + """""" + numbers = extract_numbers(line) + return numbers[0] if numbers else 0 + + +# Multi-threaded folder processing +solid_color_layer_to_json = solid_layer_to_json + diff --git a/lottie/objects/lottie_tokenize.py b/lottie/objects/lottie_tokenize.py new file mode 100644 index 0000000..3f1a35d --- /dev/null +++ b/lottie/objects/lottie_tokenize.py @@ -0,0 +1,6246 @@ +import torch +import re +import json +from typing import Union, List, Dict, Tuple, Optional, Any +import difflib +import numpy as np + + +class LottieTensor: + # Command type constants (ę·»åŠ ę–°ēš„å‘½ä»¤åøøé‡) + tokenizer = None + CMD_ANIMATION = 0 + CMD_LAYER = 1 + CMD_TRANSFORM = 2 + CMD_POSITION = 3 + CMD_KEYFRAME = 4 + CMD_POSITION_END = 5 + CMD_SCALE = 6 + CMD_SCALE_END = 7 + CMD_ROTATION = 8 + CMD_OPACITY = 9 + CMD_OPACITY_END = 10 + CMD_ANCHOR = 11 + CMD_GROUP = 12 + CMD_GROUP_END = 13 + CMD_TRANSFORM_SHAPE = 14 + CMD_PATH = 15 + CMD_PATH_END = 16 + CMD_POINT = 17 + CMD_FILL = 18 + CMD_GRADIENT_FILL = 19 + CMD_GRADIENT_FILL_END = 20 + CMD_START_POINT = 21 + CMD_END_POINT = 22 + CMD_GRADIENT_TYPE = 23 + CMD_HIGHLIGHT_LENGTH = 24 + CMD_HIGHLIGHT_ANGLE = 25 + CMD_TRANSFORM_END = 26 + CMD_LAYER_END = 27 + CMD_PAD = 28 + CMD_EOS = 29 + CMD_SOS = 30 + CMD_RECT = 31 + CMD_RECT_END = 32 + CMD_SIZE = 33 + CMD_ROUNDED = 34 + CMD_ELLIPSE = 35 + CMD_ELLIPSE_END = 36 + CMD_STROKE = 37 + CMD_SKEW = 38 + CMD_SKEW_AXIS = 39 + CMD_ASSET = 40 + CMD_ASSET_END = 41 + CMD_PARENT = 42 + CMD_NULL_LAYER = 43 + CMD_NULL_LAYER_END = 44 + CMD_PRECOMP_LAYER = 45 + CMD_PRECOMP_LAYER_END = 46 + CMD_REFERENCE_ID = 47 + CMD_DIMENSIONS = 48 + CMD_ROTATION_END = 49 + CMD_STAR = 50 + CMD_STAR_END = 51 + CMD_INNER_RADIUS = 52 + CMD_OUTER_RADIUS = 53 + CMD_INNER_ROUNDNESS = 54 + CMD_OUTER_ROUNDNESS = 55 + CMD_POINTS = 56 + CMD_STAR_ROTATION = 57 + CMD_TRIM = 58 + CMD_TRIM_END = 59 + CMD_START = 60 + CMD_END = 61 + CMD_OFFSET = 62 + CMD_MULTIPLE = 63 + CMD_REPEATER = 64 + CMD_REPEATER_END = 65 + CMD_COPIES = 66 + CMD_REPEATER_OFFSET = 67 + CMD_COMPOSITE = 68 + CMD_REPEATER_TRANSFORM = 69 + CMD_REPEATER_TRANSFORM_END = 70 + CMD_GRADIENT_STROKE = 71 + CMD_GRADIENT_STROKE_END = 72 + CMD_WIDTH = 73 + CMD_LINE_CAP = 74 + CMD_LINE_JOIN = 75 + CMD_MITER_LIMIT = 76 + CMD_MERGE = 77 + CMD_MERGE_END = 78 + CMD_MERGE_MODE = 79 + CMD_ROUNDED_CORNERS = 80 + CMD_ROUNDED_CORNERS_END = 81 + CMD_RADIUS = 82 + CMD_TWIST = 83 + CMD_TWIST_END = 84 + CMD_ANGLE = 85 + CMD_CENTER = 86 + CMD_BEZIER = 87 + CMD_BEZIER_END = 88 + CMD_TEXT_LAYER = 89 + CMD_TEXT_LAYER_END = 90 + CMD_TEXT_DATA = 91 + CMD_TEXT_DATA_END = 92 + CMD_DOCUMENT = 93 + CMD_SOLID_LAYER = 94 + CMD_SOLID_LAYER_END = 95 + CMD_POSITION_X = 96 + CMD_POSITION_Y = 97 + CMD_POSITION_Z = 98 + CMD_POSITION_X_END = 99 + CMD_POSITION_Y_END = 100 + CMD_POSITION_Z_END = 101 + CMD_SCALE_X = 102 + CMD_SCALE_Y = 103 + CMD_SCALE_Z = 104 + CMD_SCALE_X_END = 105 + CMD_SCALE_Y_END = 106 + CMD_SCALE_Z_END = 107 + CMD_ROTATION_X = 108 + CMD_ROTATION_Y = 109 + CMD_ROTATION_Z = 110 + CMD_ROTATION_X_END = 111 + CMD_ROTATION_Y_END = 112 + CMD_ROTATION_Z_END = 113 + CMD_EFFECTS = 114 + CMD_EFFECTS_END = 115 + CMD_EFFECT = 116 + CMD_EFFECT_END = 117 + CMD_HAS_MASK = 118 + CMD_MASKS_PROPERTIES = 119 + CMD_CT = 120 + CMD_EF = 121 + CMD_TT = 122 + CMD_TP = 123 + CMD_TD = 124 + CMD_HD = 125 + CMD_CL = 126 + CMD_LN = 127 + CMD_AO = 128 + CMD_ANCHOR_END = 129 + CMD_OPACITY_FILL = 130 + CMD_FILL_RULE = 131 + CMD_COLOR_DIM = 132 + CMD_DDD = 133 + CMD_MARKERS = 134 + CMD_PROPS = 135 + CMD_ORIGINAL_COLORS = 136 + CMD_COLOR_POINTS = 137 + CMD_COLORS = 138 + CMD_ML2 = 139 + CMD_ML2_IX = 140 + CMD_OFFSET_IX = 141 + CMD_TR_P_IX = 142 + CMD_TR_A_IX = 143 + CMD_TR_SCALE = 144 + CMD_TR_S_IX = 145 + CMD_TR_R_IX = 146 + CMD_TR_SO_IX = 147 + CMD_TR_EO_IX = 148 + CMD_KEYFRAME_END = 149 + CMD_POSITION_EXPR = 150 + CMD_SCALE_EXPR = 151 + CMD_ROTATION_EXPR = 152 + CMD_WIDTH_KEYFRAME = 153 # ę–°å¢ž + CMD_WIDTH_ANIMATED_END = 154 # ę–°å¢ž + CMD_FONTS = 155 + CMD_FONTS_END = 156 + CMD_FONT = 157 + CMD_CHARS = 158 + CMD_CHARS_END = 159 + CMD_CHAR = 160 + CMD_CHAR_END = 161 + CMD_CHAR_SHAPES = 162 + CMD_CHAR_SHAPES_END = 163 + CMD_TEXT_KEYFRAMES = 164 + CMD_TEXT_KEYFRAMES_END = 165 + CMD_TEXT_KEYFRAME = 166 + CMD_TEXT_DOC = 167 + CMD_TEXT_DOC_END = 168 + CMD_FONT_SIZE = 169 + CMD_FONT_FAMILY = 170 + CMD_TEXT = 171 + CMD_CA = 172 + CMD_JUSTIFY = 173 + CMD_TRACKING = 174 + CMD_LINE_HEIGHT = 175 + CMD_LETTER_SPACING = 176 + CMD_FILL_COLOR = 177 + CMD_MORE_OPTIONS = 178 + CMD_MORE_OPTIONS_END = 179 + CMD_G = 180 + CMD_ALIGNMENT = 181 + CMD_ALIGNMENT_K = 182 + CMD_ALIGNMENT_IX = 183 + CMD_DROPDOWN = 184 + CMD_IGNORED = 185 + CMD_SLIDER = 186 + CMD_COLOR = 187 + CMD_OPACITY_ANIMATED = 188 + CMD_OPACITY_KEYFRAME = 189 + CMD_MASKS_PROPERTIES_END = 190 + CMD_MASK = 191 + CMD_MASK_END = 192 + CMD_MASK_PT = 193 + CMD_MASK_PT_END = 194 + CMD_MASK_PT_K = 195 + CMD_MASK_PT_K_END = 196 + CMD_MASK_PT_K_I = 197 + CMD_MASK_PT_K_O = 198 + CMD_MASK_PT_K_V = 199 + CMD_MASK_O = 200 + CMD_MASK_X = 201 + CMD_TM = 202 + CMD_TM_END = 203 + CMD_MASK_PT_K_ARRAY = 204 + CMD_MASK_PT_K_ARRAY_END = 205 + CMD_MASK_PT_KEYFRAME = 206 + CMD_MASK_PT_KEYFRAME_END = 207 + CMD_MASK_PT_KF_I = 208 + CMD_MASK_PT_KF_O = 209 + CMD_MASK_PT_KF_S = 210 + CMD_MASK_PT_KF_S_END = 211 + CMD_MASK_PT_KF_SHAPE = 212 + CMD_MASK_PT_KF_SHAPE_END = 213 + CMD_MASK_PT_KF_SHAPE_I = 214 + CMD_MASK_PT_KF_SHAPE_O = 215 + CMD_MASK_PT_KF_SHAPE_V = 216 + CMD_VALUE = 217 + CMD_VALUE_END = 218 + CMD_TR_POSITION = 219 + CMD_TR_ANCHOR = 220 + CMD_TR_ROTATION = 221 + CMD_TR_START_OPACITY = 222 + CMD_TR_END_OPACITY = 223 + CMD_ZIG_ZAG = 224 + CMD_ZIG_ZAG_END = 225 + CMD_FREQUENCY = 226 + CMD_AMPLITUDE = 227 + CMD_POINT_TYPE = 228 + CMD_ANIMATORS = 229 + CMD_ANIMATORS_END = 230 + CMD_ANIMATOR = 231 + CMD_ANIMATOR_END = 232 + CMD_RANGE_SELECTOR = 233 + CMD_RANGE_SELECTOR_END = 234 + CMD_RANGE_START = 235 + CMD_RANGE_START_END = 236 + CMD_RANGE_START_KEYFRAME = 237 + CMD_AMOUNT = 238 + CMD_MAX_EASE = 239 + CMD_MIN_EASE = 240 + CMD_ANIMATOR_PROPERTIES = 241 + CMD_ANIMATOR_PROPERTIES_END = 242 + CMD_OPACITY_ANIMATED_END = 243 + CMD_MASK_PT_K_C = 244 + CMD_RANGE_END = 245 + CMD_RANGE_END_END = 246 + CMD_RANGE_END_KEYFRAME = 247 + CMD_END_END = 248 + CMD_START_END = 249 + CMD_OFFSET_END = 250 + CMD_POINTS_STAR = 251 + CMD_RANGE_OFFSET = 252 + CMD_RANGE_OFFSET_END = 253 + CMD_RANGE_OFFSET_KEYFRAME = 254 + CMD_S_M = 255 + CMD_OPACITY_ANIMATORS = 256 + CMD_SCALE_ANIMATORS = 257 + CMD_SCALE_ANIMATORS_END = 258 + CMD_ROTATION_ANIMATORS = 259 + CMD_ROTATION_ANIMATORS_END = 260 + CMD_POSITION_ANIMATORS = 261 + CMD_POSITION_ANIMATORS_END = 262 + CMD_TRACKING_ANIMATORS = 263 + CMD_OPACITY_ANIMATORS_END = 264 + CMD_COLOR_KEYFRAME = 265 # Add this constant + CMD_COLOR_ANIMATED_END = 266 + CMD_DASHES = 267 + CMD_DASHES_END = 268 + CMD_DASH = 269 + CMD_DASH_OFFSET = 270 + CMD_LAYER_EFFECT = 271 + CMD_NO_VALUE = 272 + CMD_WIDTH_ANIMATED = 273 # Add this if it doesn't exist + CMD_SIZE_END = 274 + CMD_RECT_SIZE = 275 # Add this new constant + CMD_ELLIPSE_SIZE = 276 + CMD_RECT_ROUNDED = 277 # Add this new constant for animated rect_rounded + CMD_RECT_ROUNDED_END = 278 + CMD_DASH_ANIMATED = 279 # New constant + CMD_DASH_KEYFRAME = 280 # New constant + CMD_DASH_ANIMATED_END = 281 # New constant + + # Command names mapped to their numeric constants + COMMANDS = [ + "animation", # 0 + "layer", # 1 + "transform", # 2 + "position", # 3 + "keyframe", # 4 + "/position", # 5 + "scale", # 6 + "/scale", # 7 + "rotation", # 8 + "opacity", # 9 + "/opacity", # 10 + "anchor", # 11 + "group", # 12 + "/group", # 13 + '"TransformShape"', # 14 + "path", # 15 + "/path", # 16 + "point", # 17 + "fill", # 18 + "gradient_fill", # 19 + "/gradient_fill", # 20 + "start_point", # 21 + "end_point", # 22 + "gradient_type", # 23 + "highlight_length", # 24 + "highlight_angle", # 25 + "/transform", # 26 + "/layer", # 27 + "PAD", # 28 + "EOS", # 29 + "SOS", # 30 + "rect", # 31 + "/rect", # 32 + "size", # 33 + "rounded", # 34 + "ellipse", # 35 + "/ellipse", # 36 + "stroke", # 37 + "skew", # 38 + "skew_axis", # 39 + "asset", # 40 + "/asset", # 41 + "parent", # 42 + "null_layer", # 43 + "/null_layer", # 44 + "precomp_layer", # 45 + "/precomp_layer", # 46 + "reference_id", # 47 + "dimensions", # 48 + "/rotation", # 49 + "star", # 50 + "/star", # 51 + "inner_radius", # 52 + "outer_radius", # 53 + "inner_roundness", # 54 + "outer_roundness", # 55 + "points", # 56 + "star_rotation", # 57 + "trim", # 58 + "/trim", # 59 + "start", # 60 + "end", # 61 + "offset", # 62 + "multiple", # 63 + "repeater", # 64 + "/repeater", # 65 + "copies", # 66 + "repeater_offset", # 67 + "composite", # 68 + "repeater_transform", # 69 + "/repeater_transform", # 70 + "gradient_stroke", # 71 + "/gradient_stroke", # 72 + "width", # 73 + "line_cap", # 74 + "line_join", # 75 + "miter_limit", # 76 + "merge", # 77 + "/merge", # 78 + "merge_mode", # 79 + "rounded_corners", # 80 + "/rounded_corners", # 81 + "radius", # 82 + "twist", # 83 + "/twist", # 84 + "angle", # 85 + "center", # 86 + "bezier", # 87 + "/bezier", # 88 + "text_layer", # 89 + "/text_layer", # 90 + "text_data", # 91 + "/text_data", # 92 + "document", # 93 + "solid_layer", # 94 + "/solid_layer", # 95 + "position_x", # 96 + "position_y", # 97 + "position_z", # 98 + "/position_x", # 99 + "/position_y", # 100 + "/position_z", # 101 + "scale_x", # 102 + "scale_y", # 103 + "scale_z", # 104 + "/scale_x", # 105 + "/scale_y", # 106 + "/scale_z", # 107 + "rotation_x", # 108 + "rotation_y", # 109 + "rotation_z", # 110 + "/rotation_x", # 111 + "/rotation_y", # 112 + "/rotation_z", # 113 + "effects", # 114 + "/effects", # 115 + "effect", # 116 + "/effect", # 117 + "hasMask", # 118 + "masksProperties", # 119 + "ct", # 120 + "ef", # 121 + "tt", # 122 + "tp", # 123 + "td", # 124 + "hd", # 125 + "cl", # 126 + "ln", # 127 + "ao", # 128 + "/anchor", # 129 + "opacity_fill", # 130 + "fill_rule", # 131 + "color_dim", # 132 + "ddd", # 133 + "markers", # 134 + "props", # 135 + "original_colors", # 136 + "color_points", # 137 + "colors", # 138 + "ml2", # 139 + "ml2_ix", # 140 + "offset_ix", # 141 + "tr_p_ix", # 142 + "tr_a_ix", # 143 + "tr_scale", # 144 + "tr_s_ix", # 145 + "tr_r_ix", # 146 + "tr_so_ix", # 147 + "tr_eo_ix", # 148 + "/keyframe", # 149 + "position_expr", # 150 + "scale_expr", # 151 + "rotation_expr", # 152 + "width_keyframe", # 153 # ę–°å¢ž + "/width_animated", # 154 # ę–°å¢ž + "fonts", # 155 + "/fonts", # 156 + "font", # 157 + "chars", # 158 + "/chars", # 159 + "char", # 160 + "/char", #161 + "char_shapes", # 162 + "/char_shapes", # 163 + "text_keyframes", # 164 + "/text_keyframes", # 165 + "text_keyframe", # 166 + "text_doc", # 167 + "/text_doc", # 168 + "font_size", # 169 + "font_family", # 170 + "text", # 171 + "ca", # 172 + "justify", # 173 + "tracking_animators", # 174 + "line_height", # 175 + "letter_spacing", # 176 + "fill_color", # 177 + "more_options", # 178 + "/more_options", # 179 + "g", # 180 + "alignment", # 181 + "alignment_k", # 182 + "alignment_ix", # 183 + "dropdown", # 184 + "ignored", # 185 + "slider", # 186 + "color", # 187 + "opacity_animated", # 188 + "opacity_keyframe", # 189 + "/masksProperties", # 190 + "mask", # 191 + "/mask", # 192 + "mask_pt", # 193 + "/mask_pt", # 194 + "mask_pt_k", # 195 + "/mask_pt_k", # 196 + "mask_pt_k_i", # 197 + "mask_pt_k_o", # 198 + "mask_pt_k_v", # 199 + "mask_o", # 200 + "mask_x", # 201 + "tm", # 202 + "/tm", # 203 + "mask_pt_k_array", # 204 + "/mask_pt_k_array", # 205 + "mask_pt_keyframe", # 206 + "/mask_pt_keyframe", # 207 + "mask_pt_kf_i", # 208 + "mask_pt_kf_o", # 209 + "mask_pt_kf_s", # 210 + "/mask_pt_kf_s", # 211 + "mask_pt_kf_shape", # 212 + "/mask_pt_kf_shape", # 213 + "mask_pt_kf_shape_i", # 214 + "mask_pt_kf_shape_o", # 215 + "mask_pt_kf_shape_v", # 216 + "value", # 217 + "/value", # 218 + "tr_position", # 219 + "tr_anchor", # 220 + "tr_rotation", # 221 + "tr_start_opacity", # 222 + "tr_end_opacity", # 223 + "zig_zag", # 224 + "/zig_zag", # 225 + "frequency", # 226 + "amplitude", # 227 + "point_type", # 228 + "animators", # 229 + "/animators", # 230 + "animator", # 231 + "/animator", # 232 + "range_selector", # 233 + "/range_selector", # 234 + "range_start", # 235 + "/range_start", # 236 + "range_start_keyframe", # 237 + "amount", # 238 + "max_ease", # 239 + "min_ease", # 240 + "animator_properties", # 241 + "/animator_properties", # 242 + "/opacity_animated", # 243 + "mask_pt_k_c", #244 + "range_end", # 245 + "/range_end", # 246 + "range_end_keyframe", # 247 + "/end", #248 + "/start" , # 249 + "/offset" , # 250 + "points_star", #251 + "range_offset", # 252 + "/range_offset", # 253 + "range_offset_keyframe", # 254 + "s_m", # 255 + "opacity_animators", # 256 + "scale_animators", # 257 + "/scale_animators", # 258 + "rotation_animators", # 259 + "/rotation_animators", # 260 + "position_animators", # 261 + "/position_animators", # 262 + "tracking_animators", # 263 + "/opacity_animators", #264 + "color_keyframe", # 265 + "/color_animated", #266 + "dashes", # 267 + "/dashes", # 268 + "dash", # 269 + "dash_offset", # 270 + "layer_effect", # 271 + "no_value", #272 + "width_animated" , #273 + "/size", #274 + "rect_size", # 275 # Add this new command + "ellipse_size", # 276 + "rect_rounded", # 277 + "/rounded", #278 + "dash_animated", # 279 # Add this + "dash_keyframe", # 280 # Add this + "/dash_animated", # 281 # Add this + ] + + # Command to index mapping + COMMAND_TO_IDX = {cmd: idx for idx, cmd in enumerate(COMMANDS)} + _OFFSET_CACHE = {} + + # Parameter indices for each command type (ę·»åŠ ę–°ēš„Index定义) + class Index: + # Animation parameters + class Animation: + FR = 0 + IP = 1 + OP = 2 + W = 3 + H = 4 + DDD = 5 + + class Layer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + DDD = 4 + HD = 5 + HAS_MASK = 6 + AO = 7 + TT = 8 + TP = 9 + TD = 10 + CT = 11 + CP = 12 + + + class Value: + VALUE = 0 + + class Transform: + ANIMATED = 0 + X = 1 + Y = 2 + Z = 3 + + class Keyframe: + T = 0 + S1 = 1 + S2 = 2 + S3 = 3 + I_X = 4 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + I_Y = 5 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + O_X = 6 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + O_Y = 7 # ē¬¬äø€äøŖå€¼ļ¼Œęˆ–å•å€¼ęƒ…å†µ + TO1 = 8 + TO2 = 9 + TO3 = 10 + TI1 = 11 + TI2 = 12 + TI3 = 13 + # Multi-dimensional easing (for scale, position, anchor) + I_X2 = 14 + I_X3 = 15 + I_Y2 = 16 + I_Y3 = 17 + O_X2 = 18 + O_X3 = 19 + O_Y2 = 20 + O_Y3 = 21 + H_FLAG = 22 + E1 = 23 + E2 = 24 + E3 = 25 + + + class Tm: + A = 0 + #IX = 1 + + + class WidthKeyframe: # ę–°å¢ž + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + class Path: + IX = 0 + IND = 1 + KS_IX = 2 + CLOSED = 3 + HD = 4 + ANIMATED = 5 + + class Point: + X = 0 + Y = 1 + IN_X = 2 + IN_Y = 3 + OUT_X = 4 + OUT_Y = 5 + + class Fill: + R = 0 + G = 1 + B = 2 + COLOR_DIM = 3 + HAS_C_A = 4 + HAS_C_IX = 5 + C_IX = 6 + BM = 7 + FILL_RULE = 8 + OPACITY = 9 + COLOR_ANIMATED = 10 # New + OPACITY_ANIMATED = 11 # New + HAS_O_A = 12 # New + HAS_O_IX = 13 # New + O_IX = 14 # New + + class TransformShape: + POSITION_X = 0 + POSITION_Y = 1 + SCALE_X = 2 + SCALE_Y = 3 + ROTATION = 4 + OPACITY = 5 + ANCHOR_X = 6 + ANCHOR_Y = 7 + SKEW = 8 + SKEW_AXIS = 9 + HD = 10 + + class Stroke: + R = 0 + G = 1 + B = 2 + COLOR_DIM = 3 + HAS_C_A = 4 + HAS_C_IX = 5 + C_IX = 6 + BM = 7 + LC = 8 + LJ = 9 + ML = 10 + #WIDTH = 11 + #OPACITY = 12 + WIDTH_ANIMATED = 11 # ę–°å¢ž + COLOR_ANIMATED = 12 # Add this + A = 13 # Add alpha channel support + + class Bezier: + CLOSED = 0 + + class Group: + IX = 0 + CIX = 1 + BM = 2 + HD = 3 + NP = 4 + + class Star: + D = 0 + SY = 1 + + class StarValue: # ę–°å¢žē”ØäŗŽ star ēš„å­å‘½ä»¤ + VALUE = 0 + + class Trim: + IX = 0 + START = 1 + END = 2 + OFFSET = 3 + MULTIPLE = 4 + + class TrimValue: + VALUE = 0 + ANIMATED = 1 + IX = 2 + + class Repeater: + IX = 0 + COPIES = 1 + REPEATER_OFFSET = 2 + COMPOSITE = 3 + TR_P_IX = 4 + TR_A_IX = 5 + TR_SCALE = 6 + TR_S_IX = 7 + TR_R_IX = 8 + TR_SO_IX = 9 + TR_EO_IX = 10 + + + class Asset: + #ID = 0 + FR = 0 + ID_TOKEN_0 = 1 + ID_TOKEN_1 = 2 + ID_TOKEN_2 = 3 + ID_TOKEN_3 = 4 + ID_TOKEN_4 = 5 + ID_TOKEN_5 = 6 + ID_TOKEN_6 = 7 + ID_TOKEN_7 = 8 + ID_TOKEN_8 = 9 + ID_TOKEN_9 = 10 + ID_TOKEN_COUNT = 11 # Store count of tokens + + class Rect: + HD = 0 + D = 1 + POSITION_X = 2 + POSITION_Y = 3 + SIZE_X = 4 + SIZE_Y = 5 + ROUNDED = 6 + IX = 7 + + class Ellipse: + POSITION_X = 0 + POSITION_Y = 1 + SIZE_X = 2 + SIZE_Y = 3 + + class SingleValue: + VALUE = 0 + IX = 1 + ANIMATED = 2 + + class TwoValues: + VALUE1 = 0 + VALUE2 = 1 + IX = 2 + + class ThreeValues: + VALUE1 = 0 + VALUE2 = 1 + VALUE3 = 2 + + class NullLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + CT = 4 + #DDD = 5 + HD = 5 + HAS_MASK = 6 + AO = 7 + TT = 8 + TP = 9 + TD = 10 + CP = 11 + + class PrecompLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + W = 4 + H = 5 + CT = 6 # 添加CTå‚ę•° + HAS_MASK = 7 + AO = 8 + TT = 9 + TP = 10 + TD = 11 + DDD =12 + HD = 13 + CP = 14 + + class SolidLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + WIDTH = 4 + HEIGHT = 5 + HAS_MASK = 6 + COLOR_R = 7 + COLOR_G = 8 + COLOR_B = 9 + COLOR_A = 10 + + class Parent: + PARENT_INDEX= 0 + + class ReferenceId: # ę–°å¢ž + ID_TOKEN_0 = 0 + ID_TOKEN_1 = 1 + ID_TOKEN_2 = 2 + ID_TOKEN_3 = 3 + ID_TOKEN_4 = 4 + ID_TOKEN_5 = 5 + ID_TOKEN_6 = 6 + ID_TOKEN_7 = 7 + ID_TOKEN_8 = 8 + ID_TOKEN_9 = 9 + ID_TOKEN_COUNT = 10 # Store count of tokens + + class Dimensions: # ę–°å¢ž + WIDTH = 0 + HEIGHT = 1 + + class Font: + ASCENT = 0 + FAMILY_TOKEN_0 = 1 + FAMILY_TOKEN_1 = 2 + FAMILY_TOKEN_2 = 3 + FAMILY_TOKEN_3 = 4 + FAMILY_TOKEN_4 = 5 + FAMILY_TOKEN_5 = 6 + FAMILY_TOKEN_6 = 7 + FAMILY_TOKEN_7 = 8 + FAMILY_TOKEN_8 = 9 + FAMILY_TOKEN_9 = 10 + FAMILY_TOKEN_COUNT = 11 + # Reserve slots for style tokens + STYLE_TOKEN_0 = 12 + STYLE_TOKEN_1 = 13 + STYLE_TOKEN_2 = 14 + STYLE_TOKEN_3 = 15 + STYLE_TOKEN_4 = 16 + STYLE_TOKEN_5 = 17 + STYLE_TOKEN_6 = 18 + STYLE_TOKEN_7 = 19 + STYLE_TOKEN_8 = 20 + STYLE_TOKEN_9 = 21 + STYLE_TOKEN_COUNT = 22 + + class Char: + SIZE = 0 + W = 1 + CH_TOKEN_0 = 2 + CH_TOKEN_1 = 3 + CH_TOKEN_2 = 4 + CH_TOKEN_3 = 5 + CH_TOKEN_4 = 6 + CH_TOKEN_5 = 7 + CH_TOKEN_6 = 8 + CH_TOKEN_7 = 9 + CH_TOKEN_8 = 10 + CH_TOKEN_9 = 11 + CH_TOKEN_COUNT = 12 + # Reserve slots for style tokens + STYLE_TOKEN_0 = 13 + STYLE_TOKEN_1 = 14 + STYLE_TOKEN_2 = 15 + STYLE_TOKEN_3 = 16 + STYLE_TOKEN_4 = 17 + STYLE_TOKEN_5 = 18 + STYLE_TOKEN_6 = 19 + STYLE_TOKEN_7 = 20 + STYLE_TOKEN_8 = 21 + STYLE_TOKEN_9 = 22 + STYLE_TOKEN_COUNT = 23 + # Reserve slots for family tokens + FAMILY_TOKEN_0 = 24 + FAMILY_TOKEN_1 = 25 + FAMILY_TOKEN_2 = 26 + FAMILY_TOKEN_3 = 27 + FAMILY_TOKEN_4 = 28 + FAMILY_TOKEN_5 = 29 + FAMILY_TOKEN_6 = 30 + FAMILY_TOKEN_7 = 31 + FAMILY_TOKEN_8 = 32 + FAMILY_TOKEN_9 = 33 + FAMILY_TOKEN_COUNT = 34 + + class TextLayer: + INDEX = 0 + IN_POINT = 1 + OUT_POINT = 2 + START_TIME = 3 + HAS_MASK = 4 # ę–°å¢ž + + class TextKeyframe: + T = 0 + STROKE_WIDTH = 1 + OFFSET = 2 + WRAP_POSITION_X = 3 + WRAP_POSITION_Y = 4 + WRAP_SIZE_X = 5 + WRAP_SIZE_Y = 6 + # Add numeric fields instead of string storage + FONT_SIZE = 7 + CA = 8 + JUSTIFY = 9 + TRACKING = 10 + LINE_HEIGHT = 11 + LETTER_SPACING = 12 + FILL_COLOR_R = 13 + FILL_COLOR_G = 14 + FILL_COLOR_B = 15 + STROKE_COLOR_R = 16 + STROKE_COLOR_G = 17 + STROKE_COLOR_B = 18 + HAS_STROKE_COLOR = 19 # Flag to indicate if stroke_color exists + FONT_FAMILY_TOKENS_START = 20 # Store up to 10 tokens for font_family + TEXT_TOKENS_START = 30 # Store up to 15 tokens for text + FONT_FAMILY_TOKEN_COUNT = 45 # Store the count of font_family tokens + TEXT_TOKEN_COUNT = 46 # Store the count of text tokens + + class MoreOptions: + G = 0 + ALIGNMENT_A = 1 + ALIGNMENT_K1 = 2 + ALIGNMENT_K2 = 3 + ALIGNMENT_IX = 4 + + class OriginalColors: + # Support up to 18 color values + COLOR_0 = 0 + COLOR_1 = 1 + COLOR_2 = 2 + COLOR_3 = 3 + COLOR_4 = 4 + COLOR_5 = 5 + COLOR_6 = 6 + COLOR_7 = 7 + COLOR_8 = 8 + COLOR_9 = 9 + COLOR_10 = 10 + COLOR_11 = 11 + COLOR_12 = 12 + COLOR_13 = 13 + COLOR_14 = 14 + COLOR_15 = 15 + COLOR_16 = 16 + COLOR_17 = 17 + COLOR_18 = 18 # Added + COLOR_19 = 19 # Added + COLOR_20 = 20 # Added + COLOR_21 = 21 # Added + COLOR_22 = 22 # Added + COLOR_23 = 23 # Added + COLOR_24 = 24 # Added + COLOR_25 = 25 # Added + COLOR_26 = 26 # Added + COLOR_27 = 27 # Added + COLOR_28 = 28 # Added + COLOR_29 = 29 # Added + COLOR_30 = 30 # Added + COLOR_31 = 31 # Added + COLOR_32 = 32 # Added + COLOR_33 = 33 # Added + COLOR_34 = 34 # Added + COLOR_35 = 35 # Added + COLOR_36 = 36 # Added + COLOR_37 = 37 # Added + COLOR_38 = 38 # Added + COLOR_39 = 39 # Added + COLOR_40 = 40 # Added + COLOR_41 = 41 # Added + COLOR_42 = 42 # Added + COLOR_43 = 43 # Added + COLOR_44 = 44 # Added + COLOR_45 = 45 # Added + COLOR_46 = 46 # Added + COUNT = 47 # Store the count of colors + + + + class FontSize: + SIZE = 0 + + class Text: + TEXT_TOKEN_0 = 0 + TEXT_TOKEN_1 = 1 + TEXT_TOKEN_2 = 2 + TEXT_TOKEN_3 = 3 + TEXT_TOKEN_4 = 4 + TEXT_TOKEN_5 = 5 + TEXT_TOKEN_6 = 6 + TEXT_TOKEN_7 = 7 + TEXT_TOKEN_8 = 8 + TEXT_TOKEN_9 = 9 + TEXT_TOKEN_COUNT = 10 + + class Ca: + VALUE = 0 + + class Justify: + VALUE = 0 + + class Tracking: + VALUE = 0 + + class LineHeight: + VALUE = 0 + + class LetterSpacing: + VALUE = 0 + + class FillColor: + R = 0 + G = 1 + B = 2 + + class G: + VALUE = 0 + + class Alignment: + A = 0 + + class AlignmentK: + VALUE1 = 0 + VALUE2 = 1 + + class AlignmentIx: + VALUE = 0 + + class GradientFill: + OPACITY = 0 + FILL_RULE = 1 + START_POINT_X = 2 + START_POINT_Y = 3 + END_POINT_X = 4 + END_POINT_Y = 5 + GRADIENT_TYPE = 6 + HIGHLIGHT_LENGTH = 7 + HIGHLIGHT_ANGLE = 8 + COLOR_POINTS = 9 + # Original colors (up to 12 values for RGBA * 3 color stops) + ORIGINAL_COLOR_0 = 10 + ORIGINAL_COLOR_1 = 11 + ORIGINAL_COLOR_2 = 12 + ORIGINAL_COLOR_3 = 13 + ORIGINAL_COLOR_4 = 14 + ORIGINAL_COLOR_5 = 15 + ORIGINAL_COLOR_6 = 16 + ORIGINAL_COLOR_7 = 17 + ORIGINAL_COLOR_8 = 18 + ORIGINAL_COLOR_9 = 19 + ORIGINAL_COLOR_10 = 20 + ORIGINAL_COLOR_11 = 21 + ORIGINAL_COLOR_12 = 22 # Added + ORIGINAL_COLOR_13 = 23 # Added + ORIGINAL_COLOR_14 = 24 # Added + ORIGINAL_COLOR_15 = 25 # Added + ORIGINAL_COLOR_16 = 26 # Added + ORIGINAL_COLOR_17 = 27 # Added + ORIGINAL_COLOR_18 = 28 # Added + ORIGINAL_COLOR_19 = 29 # Added + ORIGINAL_COLOR_20 = 30 # Added + ORIGINAL_COLOR_21 = 31 # Added + ORIGINAL_COLOR_22 = 32 # Added + ORIGINAL_COLOR_23 = 33 # Added + + class GradientStroke: + OPACITY = 0 + WIDTH = 1 + LINE_CAP = 2 + LINE_JOIN = 3 + MITER_LIMIT = 4 + ML2 = 5 + ML2_IX = 6 + START_POINT_X = 7 + START_POINT_Y = 8 + END_POINT_X = 9 + END_POINT_Y = 10 + GRADIENT_TYPE = 11 + HIGHLIGHT_LENGTH = 12 + HIGHLIGHT_ANGLE = 13 + COLOR_POINTS = 14 + # Original colors (up to 18 values for RGBA * 4.5 color stops) + ORIGINAL_COLOR_0 = 15 + ORIGINAL_COLOR_1 = 16 + ORIGINAL_COLOR_2 = 17 + ORIGINAL_COLOR_3 = 18 + ORIGINAL_COLOR_4 = 19 + ORIGINAL_COLOR_5 = 20 + ORIGINAL_COLOR_6 = 21 + ORIGINAL_COLOR_7 = 22 + ORIGINAL_COLOR_8 = 23 + ORIGINAL_COLOR_9 = 24 + ORIGINAL_COLOR_10 = 25 + ORIGINAL_COLOR_11 = 26 + ORIGINAL_COLOR_12 = 27 + ORIGINAL_COLOR_13 = 28 + ORIGINAL_COLOR_14 = 29 + ORIGINAL_COLOR_15 = 30 + ORIGINAL_COLOR_16 = 31 + ORIGINAL_COLOR_17 = 32 + ORIGINAL_COLOR_18 = 33 # Added + ORIGINAL_COLOR_19 = 34 # Added + ORIGINAL_COLOR_20 = 35 # Added + ORIGINAL_COLOR_21 = 36 # Added + ORIGINAL_COLOR_22 = 37 # Added + ORIGINAL_COLOR_23 = 38 # Added + + class StartPointCmd: + X = 0 + Y = 1 + + class EndPointCmd: + X = 0 + Y = 1 + + class OriginalColorsCmd: + COLOR_1 = 0 + COLOR_2 = 1 + COLOR_3 = 2 + COLOR_4 = 3 + COLOR_5 = 4 + COLOR_6 = 5 + COLOR_7 = 6 + COLOR_8 = 7 + COLOR_9 = 8 + COLOR_10 = 9 + COLOR_11 = 10 + COLOR_12 = 11 + + class ColorPoints: + VALUE = 0 + + class Effect: + TYPE = 0 + INDEX = 1 + NP = 2 + ENABLED = 3 + + class LayerEffect: # Add new Index class + INDEX = 0 + VALUE = 1 + + class Dropdown: + INDEX = 0 + VALUE = 1 + + class NO_VALUE: + INDEX = 0 + VALUE = 1 + + class Ignored: + INDEX = 0 + VALUE = 1 + + class Slider: + INDEX = 0 + VALUE = 1 + + class Color: + NAME_INDEX = 0 # Using NAME_INDEX to avoid confusion with INDEX + INDEX = 1 + R = 2 + G = 3 + B = 4 + + class Merge: + # mergeå‘½ä»¤ēš„nameä¼šå­˜å‚ØåœØstring_paramsäø­ + pass + + class MergeMode: + MODE = 0 + + class Mask: + INDEX = 0 + INV = 1 + MODE = 2 # mode will be stored as string + # nm will be stored in string_params + + class MaskPt: + A = 0 + IX = 1 + + class MaskPtK: + C = 0 # closed + + class MaskPtKValues: # For i, o, v + V1 = 0 + V2 = 1 + V3 = 2 + V4 = 3 + V5 = 4 + V6 = 5 + V7 = 6 + V8 = 7 + V9 = 8 + V10 = 9 + V11 = 10 + V12 = 11 + V13 = 12 + V14 = 13 + V15 = 14 + V16 = 15 + V17 = 16 + V18 = 17 + V19 = 18 + V20 = 19 + COUNT = 20 + + class MaskO: # For mask_o + A = 0 + K = 1 + IX = 2 + + class MaskX: # For mask_x + A = 0 + K = 1 + IX = 2 + + + class MaskPtKeyframe: + INDEX = 0 + T = 1 + + class MaskPtKfI: + X = 0 + Y = 1 + + class MaskPtKfO: + X = 0 + Y = 1 + + class MaskPtKfShape: + INDEX = 0 + C = 1 # closed + + + class MaskPtKfShapeValues: # For shape_i, shape_o, shape_v + V1 = 0 + V2 = 1 + V3 = 2 + V4 = 3 + V5 = 4 + V6 = 5 + V7 = 6 + V8 = 7 + V9 = 8 + V10 = 9 + V11 = 10 + V12 = 11 + V13 = 12 + V14 = 13 + V15 = 14 + V16 = 15 + V17 = 16 + V18 = 17 + V19 = 18 + V20 = 19 + COUNT = 20 # Add this to store the count + + class TrPosition: + X = 0 + Y = 1 + + class TrAnchor: + X = 0 + Y = 1 + + class TrRotation: + VALUE = 0 + + class TrStartOpacity: + VALUE = 0 + + class TrEndOpacity: + VALUE = 0 + class ZigZag: + NAME_INDEX = 0 # Will store in string_params + IX = 1 + + class Frequency: + VALUE = 0 + + class Amplitude: + VALUE = 0 + + class PointType: + VALUE = 0 + + class Animator: + # nm will be stored in string_params + pass + + class RangeSelector: + T = 0 + R = 1 + B = 2 + SH = 3 + RN = 4 + + class RangeStart: + A = 0 + + class RangeStartKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + class Amount: + A = 0 + K = 1 + IX = 2 + + class MaxEase: + A = 0 + K = 1 + IX = 2 + + class MinEase: + A = 0 + K = 1 + IX = 2 + + class Radius: + VALUE = 0 + + class RangeEnd: + A = 0 + + class RangeEndKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + #class RangeOffset: + # A = 0 + + class RangeOffsetKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + class SM: + A = 0 + K = 1 + IX = 2 + + class OpacityAnimators: + A = 0 + K = 1 + IX = 2 + class ScaleAnimators: + A = 0 + K_X = 1 + K_Y = 2 + K_Z = 3 + IX = 4 + + class RotationAnimators: + A = 0 + K = 1 + IX = 2 + + class PositionAnimators: + A = 0 + K_X = 1 + K_Y = 2 + K_Z = 3 + IX = 4 + + class TrackingAnimators: + A = 0 + K = 1 + IX = 2 + class Dashes: + # Container command, no parameters + pass + + class Dash: + TYPE = 0 # Store type as numeric (0 for "d", 1 for "g", 2 for "o") + LENGTH = 1 # dash length + V_IX = 2 # v_ix parameter + + class DashAnimated: + TYPE = 0 # Store type as numeric + V_IX = 1 # v_ix parameter + + class DashKeyframe: + T = 0 + S = 1 + I_X = 2 + I_Y = 3 + O_X = 4 + O_Y = 5 + + + class DashOffset: + O = 0 # offset value + + # Parameter dimension (fixed length for all commands) + PARAM_DIM = 50 + PAD_VAL = -2001 + + def __init__(self, commands, params, seq_len=None, PAD_VAL=-2001, flattened_data=None): + """Initialize LottieTensor""" + self.PAD_VAL = PAD_VAL + + self.commands = commands.reshape(-1, 1).long() + self.params = params.float() + self.seq_len = torch.tensor(len(commands)) if seq_len is None else seq_len + + self.sos_token = torch.tensor([LottieTensor.CMD_SOS]).unsqueeze(-1).long() + self.eos_token = self.pad_token = torch.tensor([LottieTensor.CMD_EOS]).unsqueeze(-1).long() + + # Store original string values + self.string_params = {} + + @staticmethod + def _parse_easing_value(value_str: str) -> int: + return NotImplementedError + + + @staticmethod + def _parse_multi_easing_values(value_str: str) -> List[int]: + return NotImplementedError + + + @staticmethod + def from_sequence(sequence: str) -> 'LottieTensor': + return NotImplementedError + + + + @staticmethod + def _parse_attributes(attrs_str: str) -> Dict[str, str]: + """Parse attribute string to dictionary - no change needed as it returns strings""" + attrs = {} + + # First, handle special attributes with quotes that might contain special characters + # Handle ch attribute specially (for char command) + ch_match = re.search(r'ch="([^"]*)"', attrs_str) + if ch_match: + attrs['ch'] = ch_match.group(1) + # Remove the ch attribute from the string to avoid re-parsing + attrs_str = attrs_str[:ch_match.start()] + attrs_str[ch_match.end():] + + # Handle name attribute specially if it contains quotes + name_match = re.search(r'name="([^"]*)"', attrs_str) + if name_match: + attrs['name'] = name_match.group(1) + # Remove the name attribute from the string to avoid re-parsing + attrs_str = attrs_str[:name_match.start()] + attrs_str[name_match.end():] + else: + # Try without quotes + name_match = re.search(r'name=([^\s]+)', attrs_str) + if name_match: + attrs['name'] = name_match.group(1) + attrs_str = attrs_str[:name_match.start()] + attrs_str[name_match.end():] + + # Parse remaining attributes + # Pattern for key=value or key="value" + pattern = r'([^\s=]+)=(?:"([^"]*)"|([^\s]*))' + for match in re.finditer(pattern, attrs_str): + key, quoted_val, unquoted_val = match.groups() + if key not in ['name', 'ch']: # Skip if we already handled these + attrs[key] = quoted_val if quoted_val is not None else unquoted_val + + return attrs + + + @staticmethod + def _extract_array_values(array_str: str, max_values: int) -> List[int]: + """Extract values from array string and return as int list""" + values = [0] * max_values + + if array_str: + # Handle quoted array + if array_str.startswith('"') and array_str.endswith('"'): + array_str = array_str[1:-1] + + # Handle bracketed array + if array_str.startswith("[") and array_str.endswith("]"): + try: + array_str = array_str.strip('[]') + parts = array_str.split(',') + for i, part in enumerate(parts): + if i >= max_values: + break + values[i] = round(float(part.strip())) + except ValueError: + pass + # Handle space-separated values + else: + parts = array_str.split() + for i, part in enumerate(parts): + if i >= max_values: + break + try: + values[i] = round(float(part)) + except ValueError: + pass + + return values + + @staticmethod + def _format_value(value, preserve_int=True): + """Format value as integer with proper rounding""" + val = float(value) + + # Handle special case for very small values + if abs(val) < 1e-10: + return 0 + + # Always round to integer + return val + + + + def to_sequence(self) -> str: + """Convert LottieTensor to sequence string""" + lines = [] + current_context = None + string_params = getattr(self, 'string_params', {}) + + for i in range(self.seq_len.item()): + cmd_idx = int(self.commands[i].item()) + + # Skip padding and special tokens + if cmd_idx in [LottieTensor.CMD_PAD, LottieTensor.CMD_EOS, LottieTensor.CMD_SOS]: + continue + + cmd = LottieTensor.COMMANDS[cmd_idx] + if not cmd: # Skip empty command entries + continue + + cmd_key = f"{i}" + + # Handle end tags + if cmd.startswith('/'): + lines.append(f"({cmd})") + # Reset context + if cmd in ["/position", "/scale", "/opacity", "/rotation", "/keyframe", "/anchor", "/path", "/width_animated", + "/range_start", "/range_end", "/range_offset", "/scale_animators", "/rotation_animators", + "/position_x", "/position_y", "/position_z", "/tm", "/start", "/end", "/offset", "/color_animated", "/size", "/rounded"]: + current_context = None + continue + + # Update context + if cmd in ["position", "scale", "opacity", "rotation", "anchor"]: + current_context = cmd + elif cmd == "size": + # Check if size is animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = "size" + elif cmd in ["ellipse_size", "rect_size"]: + # Check if size is animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = "size" + + elif cmd in ["position_x", "position_y", "position_z"]: + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = cmd + elif cmd == "path": + # Check if path is animated (would be stored in string_params) + if f"{cmd_key}_animated" in string_params: + current_context = "path" + elif cmd == "width_keyframe": + current_context = "width" + elif cmd == "start": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "trim_start" + elif cmd == "end": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "trim_end" + elif cmd == "offset": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "trim_offset" + elif cmd == "mask_x": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.MaskX.A] > 0.5: + current_context = "mask_x" + + + + elif cmd == "scale_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.ScaleAnimators.A] > 0.5: + current_context = "scale_animators" + elif cmd == "rotation_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.RotationAnimators.A] > 0.5: + current_context = "rotation_animators" + + elif cmd == "opacity_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.OpacityAnimators.A] > 0.5: + current_context = "opacity_animators" + + elif cmd == "position_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.PositionAnimators.A] > 0.5: + current_context = "position_animators" + + elif cmd == "tracking_animators": + params = self.params[i].tolist() + if params[LottieTensor.Index.TrackingAnimators.A] > 0.5: + current_context = "tracking_animators" + + elif cmd == "rect_rounded": + # Check if animated + params = self.params[i].tolist() + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + current_context = "rect_rounded" + + elif cmd in ["ellipse_size", "rect_size"]: + # Check if ellipse/rect size is animated + params = self.params[i].tolist() + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + current_context = "size" + + + # Extract parameters + params = self.params[i].tolist() + + # Format line based on command type + if cmd_idx == LottieTensor.CMD_ANIMATION: + # Use stored string values if available + #v = string_params.get(f"{cmd_key}_v", "5.12.1") + v = "5.12.1" + #nm = string_params.get(f"{cmd_key}_nm", "Comp 1") + #markers = string_params.get(f"{cmd_key}_markers", "[]") + #props = string_params.get(f"{cmd_key}_props", "{}") + + fr = LottieTensor._format_value(params[LottieTensor.Index.Animation.FR]) + ip = LottieTensor._format_value(params[LottieTensor.Index.Animation.IP]) + op = LottieTensor._format_value(params[LottieTensor.Index.Animation.OP]) + w = LottieTensor._format_value(params[LottieTensor.Index.Animation.W]) + h = LottieTensor._format_value(params[LottieTensor.Index.Animation.H]) + ddd = int(params[LottieTensor.Index.Animation.DDD]) + lines.append(f'({cmd} v="{v}" fr={fr} ip={ip} op={op} w={w} h={h} ddd={ddd})') + + elif cmd_idx in [LottieTensor.CMD_FONTS, LottieTensor.CMD_FONTS_END, LottieTensor.CMD_CHARS, + LottieTensor.CMD_CHARS_END, LottieTensor.CMD_CHAR_SHAPES, + LottieTensor.CMD_CHAR_SHAPES_END, LottieTensor.CMD_TEXT_KEYFRAMES, + LottieTensor.CMD_TEXT_KEYFRAMES_END, LottieTensor.CMD_TEXT_DATA, + LottieTensor.CMD_TEXT_DATA_END, LottieTensor.CMD_OPACITY_ANIMATED_END, + LottieTensor.CMD_END_END, LottieTensor.CMD_START_END, + LottieTensor.CMD_OFFSET_END, LottieTensor.CMD_OPACITY_ANIMATORS_END]: + lines.append(f"({cmd})") + continue + + elif cmd_idx in [LottieTensor.CMD_POSITION_X, LottieTensor.CMD_POSITION_Y, LottieTensor.CMD_POSITION_Z]: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + current_context = cmd # Set context + else: + val = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + lines.append(f"({cmd} {val})") + # line based on command type + # Keep all existing formatting logic but update text_keyframe + elif cmd_idx == LottieTensor.CMD_TEXT_KEYFRAME: + # Initialize tokenizer if not already done + if LottieTensor.tokenizer is None: + LottieTensor.init_tokenizer() + t = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.T]) + + # Retrieve all stored attributes + # Retrieve numeric values + font_size = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FONT_SIZE]) + ca = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.CA]) + justify = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.JUSTIFY]) + tracking = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.TRACKING]) + line_height = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.LINE_HEIGHT]) + letter_spacing = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.LETTER_SPACING]) + + # Retrieve fill_color from numeric params + fill_r = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FILL_COLOR_R]/255) + fill_g = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FILL_COLOR_G]/255) + fill_b = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.FILL_COLOR_B]/255) + fill_color = f"[{fill_r},{fill_g},{fill_b}]" + + # Retrieve string values + #font_family = string_params.get(f"{cmd_key}_font_family", "") + #text = string_params.get(f"{cmd_key}_text", "") + # Decode font_family from tokens + font_family = "" + font_family_count = int(params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT]) if params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT] > -2000 else 0 + if font_family_count > 0: + font_family_tokens = [] + for i in range(min(font_family_count, 10)): + token_val = params[LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START + i] + if token_val > -2000: + font_family_tokens.append(int(token_val)) + if font_family_tokens: + try: + font_family = LottieTensor.tokenizer.decode(font_family_tokens) + except: + font_family = "" + + # Decode text from tokens + text = "" + text_count = int(params[LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT]) if params[LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT] > -2000 else 0 + if text_count > 0: + text_tokens = [] + for i in range(min(text_count, 15)): + token_val = params[LottieTensor.Index.TextKeyframe.TEXT_TOKENS_START + i] + if token_val > -2000: + text_tokens.append(int(token_val)) + if text_tokens: + try: + text = LottieTensor.tokenizer.decode(text_tokens) + except: + text = "" + # Build the output line + line = f'({cmd} t={t} font_size={font_size} font_family="{font_family}" text="{text}" ca={ca} justify={justify} tracking={tracking} line_height={line_height} letter_spacing={letter_spacing} fill_color={fill_color}' + + # Add stroke_color if present + if params[LottieTensor.Index.TextKeyframe.HAS_STROKE_COLOR] > 0.5: + stroke_r = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_R]/255) + stroke_g = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_G]/255) + stroke_b = LottieTensor._format_value(params[LottieTensor.Index.TextKeyframe.STROKE_COLOR_B]/255) + stroke_color = f"[{stroke_r},{stroke_g},{stroke_b}]" + line += f' stroke_color={stroke_color}' + + # Add stroke_width if present and not zero + stroke_width = params[LottieTensor.Index.TextKeyframe.STROKE_WIDTH] if params[LottieTensor.Index.TextKeyframe.STROKE_WIDTH] > -2000 else 0 + if abs(stroke_width) > 1e-6: + line += f' stroke_width={LottieTensor._format_value(stroke_width)}' + + # Add offset if true + if params[LottieTensor.Index.TextKeyframe.OFFSET] > 0.5: + line += ' offset=true' + + # Add wrap_position if present (ę–°å¢ž) + wrap_pos_x = params[LottieTensor.Index.TextKeyframe.WRAP_POSITION_X] + wrap_pos_y = params[LottieTensor.Index.TextKeyframe.WRAP_POSITION_Y] + if wrap_pos_x > -2000 and wrap_pos_y > -2000: + line += f' wrap_position=[{LottieTensor._format_value(wrap_pos_x)},{LottieTensor._format_value(wrap_pos_y)}]' + + # Add wrap_size if present (ę–°å¢ž) + wrap_size_x = params[LottieTensor.Index.TextKeyframe.WRAP_SIZE_X] + wrap_size_y = params[LottieTensor.Index.TextKeyframe.WRAP_SIZE_Y] + if wrap_size_x > -2000 and wrap_size_y > -2000: + line += f' wrap_size=[{LottieTensor._format_value(wrap_size_x)},{LottieTensor._format_value(wrap_size_y)}]' + + line += ')' + lines.append(line) + + + elif cmd_idx == LottieTensor.CMD_STAR: + #name = string_params.get(f"{cmd_key}_name", "None") + d = int(params[LottieTensor.Index.Star.D]) if params[LottieTensor.Index.Star.D] > -2000 else 1 + sy = int(params[LottieTensor.Index.Star.SY]) if params[LottieTensor.Index.Star.SY] > -2000 else 1 + + lines.append(f'({cmd} d={d} sy={sy})') + + elif cmd_idx == LottieTensor.CMD_INNER_RADIUS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_OUTER_RADIUS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_INNER_ROUNDNESS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_OUTER_ROUNDNESS: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_POINTS_STAR: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'(points_star {val})') + + elif cmd_idx == LottieTensor.CMD_STAR_ROTATION: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + + + elif cmd_idx == LottieTensor.CMD_MORE_OPTIONS: + g = int(params[LottieTensor.Index.MoreOptions.G]) if params[LottieTensor.Index.MoreOptions.G] > -2000 else 1 + alignment_a = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_A]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_A] > -2000 else 0 + alignment_k1 = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_K1]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_K1] > -2000 else 0 + alignment_k2 = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_K2]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_K2] > -2000 else 0 + alignment_ix = int(params[LottieTensor.Index.MoreOptions.ALIGNMENT_IX]) if params[LottieTensor.Index.MoreOptions.ALIGNMENT_IX] > -2000 else 2 + + lines.append(f'({cmd} g {g} alignment a={alignment_a} alignment_k {alignment_k1} {alignment_k2} alignment_ix {alignment_ix})') + + + elif cmd_idx == LottieTensor.CMD_LAYER: + + index = LottieTensor._format_value(params[LottieTensor.Index.Layer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.Layer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.Layer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.Layer.START_TIME]) + + line = f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time}' + + if params[LottieTensor.Index.Layer.DDD] > -2000: + ddd = int(params[LottieTensor.Index.Layer.DDD]) + line += f' ddd={ddd}' + + if params[LottieTensor.Index.Layer.HD] > -2000 and params[LottieTensor.Index.Layer.HD] > 0.5: + line += f' hd=true' + + if params[LottieTensor.Index.Layer.CP] > -2000: + cp = "true" if params[LottieTensor.Index.Layer.CP] > 0.5 else "false" + line += f' cp={cp}' + + if params[LottieTensor.Index.Layer.CT] > -2000: + ct = int(params[LottieTensor.Index.Layer.CT]) + line += f' ct={ct}' + + if params[LottieTensor.Index.Layer.HAS_MASK] > -2000: + hasMask = "true" if params[LottieTensor.Index.Layer.HAS_MASK] > 0.5 else "false" + line += f' hasMask={hasMask}' + + masksProperties = string_params.get(f"{cmd_key}_masksProperties", "") + if masksProperties: + line += f' masksProperties={masksProperties}' + + if params[LottieTensor.Index.Layer.AO] > -2000: + ao = int(params[LottieTensor.Index.Layer.AO]) + line += f' ao={ao}' + + if params[LottieTensor.Index.Layer.TT] > -2000: + tt = int(params[LottieTensor.Index.Layer.TT]) + line += f' tt={tt}' + + if params[LottieTensor.Index.Layer.TP] > -2000: + tp = int(params[LottieTensor.Index.Layer.TP]) + line += f' tp={tp}' + + if params[LottieTensor.Index.Layer.TD] > -2000: + td = int(params[LottieTensor.Index.Layer.TD]) + line += f' td={td}' + + line += ')' + lines.append(line) + + + + elif cmd_idx == LottieTensor.CMD_NULL_LAYER: + + index = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.NullLayer.START_TIME]) + + line = f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time}' + + if params[LottieTensor.Index.PrecompLayer.HD] > -2000 and params[LottieTensor.Index.PrecompLayer.HD] > 0.5: + line += f' hd=true' + + if params[LottieTensor.Index.PrecompLayer.CP] > -2000: + cp = "true" if params[LottieTensor.Index.PrecompLayer.CP] > 0.5 else "false" + line += f' cp={cp}' + + + if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > -2000: + hasMask = "true" if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > 0.5 else "false" + line += f' hasMask={hasMask}' + + + if params[LottieTensor.Index.PrecompLayer.AO] > -2000: + ao = int(params[LottieTensor.Index.PrecompLayer.AO]) + line += f' ao={ao}' + + if params[LottieTensor.Index.PrecompLayer.TT] > -2000: + tt = int(params[LottieTensor.Index.PrecompLayer.TT]) + line += f' tt={tt}' + + if params[LottieTensor.Index.PrecompLayer.TP] > -2000: + tp = int(params[LottieTensor.Index.PrecompLayer.TP]) + line += f' tp={tp}' + + if params[LottieTensor.Index.PrecompLayer.TD] > -2000: + td = int(params[LottieTensor.Index.PrecompLayer.TD]) + line += f' td={td}' + + + line += ')' + lines.append(line) + + elif cmd_idx == LottieTensor.CMD_PRECOMP_LAYER: + + index = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.PrecompLayer.START_TIME]) + + + line = f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time}' + + + + if params[LottieTensor.Index.PrecompLayer.H] > -2000: + h = int(params[LottieTensor.Index.PrecompLayer.H]) + line += f' h={h}' + + if params[LottieTensor.Index.PrecompLayer.W] > -2000: + w = int(params[LottieTensor.Index.PrecompLayer.W]) + line += f' w={w}' + + if params[LottieTensor.Index.PrecompLayer.DDD] > -2000: + ddd = int(params[LottieTensor.Index.PrecompLayer.DDD]) + line += f' ddd={ddd}' + + if params[LottieTensor.Index.PrecompLayer.HD] > -2000 and params[LottieTensor.Index.PrecompLayer.HD] > 0.5: + line += f' hd=true' + + if params[LottieTensor.Index.PrecompLayer.CP] > -2000: + cp = "true" if params[LottieTensor.Index.PrecompLayer.CP] > 0.5 else "false" + line += f' cp={cp}' + + if params[LottieTensor.Index.PrecompLayer.CT] > -2000: + ct = int(params[LottieTensor.Index.PrecompLayer.CT]) + line += f' ct={ct}' + + if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > -2000: + hasMask = "true" if params[LottieTensor.Index.PrecompLayer.HAS_MASK] > 0.5 else "false" + line += f' hasMask={hasMask}' + + masksProperties = string_params.get(f"{cmd_key}_masksProperties", "") + if masksProperties: + line += f' masksProperties={masksProperties}' + + if params[LottieTensor.Index.PrecompLayer.AO] > -2000: + ao = int(params[LottieTensor.Index.PrecompLayer.AO]) + line += f' ao={ao}' + + if params[LottieTensor.Index.PrecompLayer.TT] > -2000: + tt = int(params[LottieTensor.Index.PrecompLayer.TT]) + line += f' tt={tt}' + + if params[LottieTensor.Index.PrecompLayer.TP] > -2000: + tp = int(params[LottieTensor.Index.PrecompLayer.TP]) + line += f' tp={tp}' + + if params[LottieTensor.Index.PrecompLayer.TD] > -2000: + td = int(params[LottieTensor.Index.PrecompLayer.TD]) + line += f' td={td}' + + + line += ')' + lines.append(line) + + + elif cmd_idx == LottieTensor.CMD_REFERENCE_ID: + tokenizer = LottieTensor.get_tokenizer() + + id_count = int(params[LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT]) if params[LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT] > -2000 else 0 + id_tokens = [] + for i in range(id_count): + if params[LottieTensor.Index.ReferenceId.ID_TOKEN_0 + i] > -2000: + id_tokens.append(int(params[LottieTensor.Index.ReferenceId.ID_TOKEN_0 + i])) + + reference_id = tokenizer.decode(id_tokens, skip_special_tokens=True) if id_tokens else "comp_0" + lines.append(f'({cmd} "{reference_id}")') + + + + elif cmd_idx == LottieTensor.CMD_DIMENSIONS: + width = LottieTensor._format_value(params[LottieTensor.Index.Dimensions.WIDTH]) + height = LottieTensor._format_value(params[LottieTensor.Index.Dimensions.HEIGHT]) + lines.append(f'({cmd} width={width} height={height})') + + + elif cmd_idx == LottieTensor.CMD_STROKE: + color_animated = params[LottieTensor.Index.Stroke.COLOR_ANIMATED] > 0.5 + + line = f'({cmd}' + + if color_animated: + line += ' color_animated=true' + else: + r = LottieTensor._format_value(params[LottieTensor.Index.Stroke.R] / 255) + g = LottieTensor._format_value(params[LottieTensor.Index.Stroke.G] / 255) + b = LottieTensor._format_value(params[LottieTensor.Index.Stroke.B] / 255) + a = LottieTensor._format_value(params[LottieTensor.Index.Stroke.A] / 255) + line += f' r={r} g={g} b={b} a={a}' + + color_dim = int(params[LottieTensor.Index.Stroke.COLOR_DIM]) if params[LottieTensor.Index.Stroke.COLOR_DIM] > -2000 else 4 + has_c_a = "True" if params[LottieTensor.Index.Stroke.HAS_C_A] > 0.5 else "False" + has_c_ix = "True" if params[LottieTensor.Index.Stroke.HAS_C_IX] > 0.5 else "False" + c_ix = int(params[LottieTensor.Index.Stroke.C_IX]) if params[LottieTensor.Index.Stroke.C_IX] > -2000 else 3 + bm = int(params[LottieTensor.Index.Stroke.BM]) if params[LottieTensor.Index.Stroke.BM] > -2000 else 0 + lc = int(params[LottieTensor.Index.Stroke.LC]) if params[LottieTensor.Index.Stroke.LC] > -2000 else 1 + lj = int(params[LottieTensor.Index.Stroke.LJ]) if params[LottieTensor.Index.Stroke.LJ] > -2000 else 1 + ml = int(params[LottieTensor.Index.Stroke.ML]) if params[LottieTensor.Index.Stroke.ML] > -2000 else 4 + + line += f' color_dim={color_dim} has_c_a={has_c_a} has_c_ix={has_c_ix}' + + if not color_animated: + line += f' c_ix={c_ix}' + + line += f' bm={bm} lc={lc} lj={lj} ml={ml}' + + width_animated = params[LottieTensor.Index.Stroke.WIDTH_ANIMATED] > 0.5 + if width_animated: + line += ' width_animated=true' + current_context = "width" + + line += ')' + + lines.append(line) + + if width_animated: + lines.append('(width_animated true)') + + if color_animated: + current_context = "stroke_color" + + + elif cmd_idx == LottieTensor.CMD_DASHES: + dashes_str = string_params.get(f"{cmd_key}_dashes", "") + if dashes_str: + lines.append(f'({cmd} {dashes_str})') + else: + lines.append(f'({cmd})') + + + elif cmd_idx == LottieTensor.CMD_DASH: + type_map = {0: "d", 1: "g", 2: "o"} + type_val = int(params[LottieTensor.Index.Dash.TYPE]) if params[LottieTensor.Index.Dash.TYPE] > -2000 else 0 + dash_type = type_map.get(type_val, "d") + + # 除仄100ę¢å¤åŽŸå€¼ + length = LottieTensor._format_value(params[LottieTensor.Index.Dash.LENGTH] / 10, preserve_int=False) + v_ix = int(params[LottieTensor.Index.Dash.V_IX]) if params[LottieTensor.Index.Dash.V_IX] > -2000 else 1 + + lines.append(f'({cmd} type="{dash_type}" length={length} v_ix={v_ix})') + + + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED: + type_map = {0: "d", 1: "g", 2: "o"} + type_val = int(params[LottieTensor.Index.DashAnimated.TYPE]) if params[LottieTensor.Index.DashAnimated.TYPE] > -2000 else 2 + dash_type = type_map.get(type_val, "o") + + v_ix = int(params[LottieTensor.Index.DashAnimated.V_IX]) if params[LottieTensor.Index.DashAnimated.V_IX] > -2000 else 7 + + name = string_params.get(f"{cmd_key}_name", "") + + lines.append(f'({cmd} type="{dash_type}" name="{name}" v_ix={v_ix})') + current_context = "dash_animated" + + elif cmd_idx == LottieTensor.CMD_DASH_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.DashKeyframe.T]) + s = LottieTensor._format_value(params[LottieTensor.Index.DashKeyframe.S] / 10, preserve_int=False) + + i_x = params[LottieTensor.Index.DashKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.DashKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.DashKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.DashKeyframe.O_Y]/100 + + has_easing = (i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000) + + if has_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_DASH_OFFSET: + o = LottieTensor._format_value(params[LottieTensor.Index.DashOffset.O] / 10, preserve_int=False) + lines.append(f'({cmd} {o})') + + + elif cmd_idx == LottieTensor.CMD_DASHES_END: + lines.append(f'({cmd})') + elif cmd_idx == LottieTensor.CMD_DASH_ANIMATED_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_SIZE_END: + lines.append(f'({cmd})') + elif cmd_idx == LottieTensor.CMD_COLOR_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.T]) + r = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S1]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S2]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S3]/255) + a = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1]/255) + + i_x = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X]/100, preserve_int=False) + i_y = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y]/100, preserve_int=False) + o_x = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X]/100, preserve_int=False) + o_y = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y]/100, preserve_int=False) + + lines.append(f'({cmd} t={t} r={r} g={g} b={b} a={a} i_x={i_x} i_y={i_y} o_x={o_x} o_y={o_y})') + + elif cmd_idx == LottieTensor.CMD_OPACITY_ANIMATED: + lines.append(f'({cmd} true)') + current_context = "opacity_animated" + + elif cmd_idx == LottieTensor.CMD_OPACITY_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.T]) + + keyframe_line = f'({cmd} t={t}' + + if params[LottieTensor.Index.Keyframe.S1] > -2000: + s = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S1]) + keyframe_line += f' s={s}' + + i_x = params[LottieTensor.Index.Keyframe.I_X]/100 + i_y = params[LottieTensor.Index.Keyframe.I_Y]/100 + o_x = params[LottieTensor.Index.Keyframe.O_X]/100 + o_y = params[LottieTensor.Index.Keyframe.O_Y]/100 + + if i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + + keyframe_line += ')' + lines.append(keyframe_line) + + elif cmd_idx == LottieTensor.CMD_WIDTH_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.WidthKeyframe.T]) + + keyframe_line = f'({cmd} t={t}' + + if params[LottieTensor.Index.WidthKeyframe.S] > -2000: + s = LottieTensor._format_value(params[LottieTensor.Index.WidthKeyframe.S] / 10, preserve_int=False) + keyframe_line += f' s={s}' + + i_x = params[LottieTensor.Index.WidthKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.WidthKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.WidthKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.WidthKeyframe.O_Y]/100 + + has_easing = (i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000) + + if has_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + + keyframe_line += ")" + lines.append(keyframe_line) + + + elif cmd_idx == LottieTensor.CMD_TRANSFORM: + lines.append(f"({cmd})") + + elif cmd_idx == LottieTensor.CMD_POSITION: + if cmd == "position" and current_context not in ["position", "scale", "opacity", "rotation", "anchor"]: + x = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + y = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f"({cmd} {x} {y})") + else: + if params[LottieTensor.Index.Transform.ANIMATED] == 2.0: + lines.append(f"({cmd} separated=true)") + elif params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + z = LottieTensor._format_value(params[LottieTensor.Index.Transform.Z]) + if params[LottieTensor.Index.Transform.Z] > -2000 and abs(params[LottieTensor.Index.Transform.Z]) > 1e-6: + lines.append(f"({cmd} {x} {y} {z})") + else: + lines.append(f"({cmd} {x} {y})") + + + elif cmd_idx == LottieTensor.CMD_POSITION_X: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + value = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f"({cmd} {value})") + else: + lines.append(f"({cmd})") + + elif cmd_idx == LottieTensor.CMD_POSITION_Y: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + value = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f"({cmd} {value})") + else: + lines.append(f"({cmd})") + + elif cmd_idx == LottieTensor.CMD_POSITION_Z: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + value = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f"({cmd} {value})") + else: + lines.append(f"({cmd})") + + + elif cmd_idx == LottieTensor.CMD_SCALE: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + z = LottieTensor._format_value(params[LottieTensor.Index.Transform.Z]) + if params[LottieTensor.Index.Transform.Z] > -2000: + lines.append(f"({cmd} {x} {y} {z})") + else: + lines.append(f"({cmd} {x} {y})") + + elif cmd_idx == LottieTensor.CMD_ROTATION: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + angle = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + lines.append(f"({cmd} {angle})") + + elif cmd_idx == LottieTensor.CMD_OPACITY: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + val = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + lines.append(f"({cmd} {val})") + + elif cmd_idx == LottieTensor.CMD_ANCHOR: + if params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f"({cmd} animated=true)") + else: + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + z = LottieTensor._format_value(params[LottieTensor.Index.Transform.Z]) + if params[LottieTensor.Index.Transform.Z] > -2000 and abs(params[LottieTensor.Index.Transform.Z]) > 1e-6: + lines.append(f"({cmd} {x} {y} {z})") + else: + lines.append(f"({cmd} {x} {y})") + + elif cmd_idx == LottieTensor.CMD_TM: + a = int(params[LottieTensor.Index.Tm.A]) if params[LottieTensor.Index.Tm.A] > -2000 else 1 + + lines.append(f'({cmd} a={a})') + + if a > 0.5: + current_context = "tm" + else: + current_context = "tm_static" + + elif cmd_idx == LottieTensor.CMD_VALUE: + val = LottieTensor._format_value(params[LottieTensor.Index.Value.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.T]) + + keyframe_line = f'({cmd} t={t}' + + is_hold = params[LottieTensor.Index.Keyframe.H_FLAG] > 0.5 + + has_s = params[LottieTensor.Index.Keyframe.S1] > -2000 + + if has_s and current_context != "path": + if current_context in ["opacity", "rotation", "position_x", "position_y", "position_z", "tm", "width", + "trim_start", "trim_end", "trim_offset", "mask_x", "rotation_animators", "opacity_animators", "tracking_animators"]: + s = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.S1], preserve_int=False) + s_str = f'"{s}"' + else: + s1 = params[LottieTensor.Index.Keyframe.S1] + s2 = params[LottieTensor.Index.Keyframe.S2] + s3 = params[LottieTensor.Index.Keyframe.S3] + + s_parts = [] + s_parts.append(str(LottieTensor._format_value(s1, preserve_int=False))) + s_parts.append(str(LottieTensor._format_value(s2, preserve_int=False))) + + if s3 > -2000 and abs(s3) > 1e-6: + s_parts.append(str(LottieTensor._format_value(s3, preserve_int=False))) + + s_str = f'"{" ".join(s_parts)}"' + + keyframe_line += f' s={s_str}' + + + has_e = params[LottieTensor.Index.Keyframe.E1] > -2000 + + if has_e: # Output e regardless of hold flag + if current_context in ["trim_start", "trim_end", "trim_offset"]: + e_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) + keyframe_line += f' e="{e_val}"' + elif current_context == "rotation": + e_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) + keyframe_line += f' e="[{e_val}]"' + elif current_context == "scale": + e1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) if params[LottieTensor.Index.Keyframe.E1] > -2000 else 0 + e2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E2], preserve_int=False) if params[LottieTensor.Index.Keyframe.E2] > -2000 else 0 + e3 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E3], preserve_int=False) if params[LottieTensor.Index.Keyframe.E3] > -2000 else 0 + keyframe_line += f' e="[{e1}, {e2}, {e3}]"' + + + if is_hold: + keyframe_line += ' h=1' + + if current_context in ["position", "anchor", "scale_animators", "position_animators", "size"]: + has_multi_easing = ( + params[LottieTensor.Index.Keyframe.I_X2] > -2000 or + params[LottieTensor.Index.Keyframe.I_Y2] > -2000 or + params[LottieTensor.Index.Keyframe.O_X2] > -2000 or + params[LottieTensor.Index.Keyframe.O_Y2] > -2000 + ) + + if has_multi_easing: + i_x_vals = [] + i_y_vals = [] + o_x_vals = [] + o_y_vals = [] + + i_x1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_X] > -2000 else 0 + i_x2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_X2] > -2000 else i_x1 + i_x_vals = [i_x1, i_x2] + + i_y1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_Y] > -2000 else 0 + i_y2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_Y2] > -2000 else i_y1 + i_y_vals = [i_y1, i_y2] + + o_x1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_X] > -2000 else 0 + o_x2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_X2] > -2000 else o_x1 + o_x_vals = [o_x1, o_x2] + + o_y1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_Y] > -2000 else 0 + o_y2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y2] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_Y2] > -2000 else o_y1 + o_y_vals = [o_y1, o_y2] + + i_x3 = params[LottieTensor.Index.Keyframe.I_X3] + i_y3 = params[LottieTensor.Index.Keyframe.I_Y3] + o_x3 = params[LottieTensor.Index.Keyframe.O_X3] + o_y3 = params[LottieTensor.Index.Keyframe.O_Y3] + + has_third_dim = ( + (i_x3 > -2000 and abs(i_x3) > 1e-6) or + (i_y3 > -2000 and abs(i_y3) > 1e-6) or + (o_x3 > -2000 and abs(o_x3) > 1e-6) or + (o_y3 > -2000 and abs(o_y3) > 1e-6) + ) + + if has_third_dim: + i_x_vals.append(LottieTensor._format_value(i_x3 / 100, preserve_int=False) if i_x3 > -2000 else i_x1) + i_y_vals.append(LottieTensor._format_value(i_y3 / 100, preserve_int=False) if i_y3 > -2000 else i_y1) + o_x_vals.append(LottieTensor._format_value(o_x3 / 100, preserve_int=False) if o_x3 > -2000 else o_x1) + o_y_vals.append(LottieTensor._format_value(o_y3 / 100, preserve_int=False) if o_y3 > -2000 else o_y1) + + keyframe_line += f' i_x="{" ".join(str(v) for v in i_x_vals)}" i_y="{" ".join(str(v) for v in i_y_vals)}" o_x="{" ".join(str(v) for v in o_x_vals)}" o_y="{" ".join(str(v) for v in o_y_vals)}"' + + elif params[LottieTensor.Index.Keyframe.I_X] > -2000 or params[LottieTensor.Index.Keyframe.I_Y] > -2000 or params[LottieTensor.Index.Keyframe.O_X] > -2000 or params[LottieTensor.Index.Keyframe.O_Y] > -2000: + # Fallback to single values if no multi-dimensional values found - DIVIDE BY 100 + i_x_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_X] > -2000 else 0 + i_y_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.I_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.I_Y] > -2000 else 0 + o_x_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_X] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_X] > -2000 else 0 + o_y_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.O_Y] / 100, preserve_int=False) if params[LottieTensor.Index.Keyframe.O_Y] > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + else: + # For single-dimensional properties, parse single easing values - DIVIDE BY 100 + i_x = params[LottieTensor.Index.Keyframe.I_X] + i_y = params[LottieTensor.Index.Keyframe.I_Y] + o_x = params[LottieTensor.Index.Keyframe.O_X] + o_y = params[LottieTensor.Index.Keyframe.O_Y] + + if i_x > -2000 or i_y > -2000 or o_x > -2000 or o_y > -2000: + i_x_val = LottieTensor._format_value(i_x / 100, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y / 100, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x / 100, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y / 100, preserve_int=False) if o_y > -2000 else 0 + keyframe_line += f' i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val}' + + # Add to/ti parameters (they are separate from hold flag) + has_to = any(params[LottieTensor.Index.Keyframe.TO1 + j] > -2000 for j in range(3)) + has_ti = any(params[LottieTensor.Index.Keyframe.TI1 + j] > -2000 for j in range(3)) + + if has_to: + to_values = [] + for i in range(3): + val = params[LottieTensor.Index.Keyframe.TO1 + i] + if val > -2000: + to_values.append(LottieTensor._format_value(val, preserve_int=False)) + else: + break # Stop at first padding value + + # Only output non-zero values, but always include at least 2 dimensions if any exist + while len(to_values) > 2 and abs(to_values[-1]) < 1e-10: + to_values.pop() # Remove trailing zeros + + # Ensure we have at least 2 values if we have any + while len(to_values) < 2: + to_values.append(0) + + keyframe_line += f' to="[{", ".join(str(v) for v in to_values)}]"' + + if has_ti: + ti_values = [] + for i in range(3): + val = params[LottieTensor.Index.Keyframe.TI1 + i] + if val > -2000: + ti_values.append(LottieTensor._format_value(val, preserve_int=False)) + else: + break # Stop at first padding value + + # Only output non-zero values, but always include at least 2 dimensions if any exist + while len(ti_values) > 2 and abs(ti_values[-1]) < 1e-10: + ti_values.pop() # Remove trailing zeros + + # Ensure we have at least 2 values if we have any + while len(ti_values) < 2: + ti_values.append(0) + + keyframe_line += f' ti="[{", ".join(str(v) for v in ti_values)}]"' + + # Check and output e parameter based on context + has_e = params[LottieTensor.Index.Keyframe.E1] > -2000 + + if has_e: # Output e regardless of hold flag + if current_context == "rotation": + # For rotation, output single e value + e_val = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) + keyframe_line += f' e="[{e_val}]"' + elif current_context == "scale": + # For scale, output three e values + e1 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E1], preserve_int=False) if params[LottieTensor.Index.Keyframe.E1] > -2000 else 0 + e2 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E2], preserve_int=False) if params[LottieTensor.Index.Keyframe.E2] > -2000 else 0 + e3 = LottieTensor._format_value(params[LottieTensor.Index.Keyframe.E3], preserve_int=False) if params[LottieTensor.Index.Keyframe.E3] > -2000 else 0 + keyframe_line += f' e="[{e1}, {e2}, {e3}]"' + # Add other contexts as needed + + keyframe_line += ")" + lines.append(keyframe_line) + + + + elif cmd_idx == LottieTensor.CMD_GROUP: + + ix = int(params[LottieTensor.Index.Group.IX]) if params[LottieTensor.Index.Group.IX] > -2000 else 1 + cix = int(params[LottieTensor.Index.Group.CIX]) if params[LottieTensor.Index.Group.CIX] > -2000 else 2 + bm = int(params[LottieTensor.Index.Group.BM]) if params[LottieTensor.Index.Group.BM] > -2000 else 0 + hd = "true" if params[LottieTensor.Index.Group.HD] > 0.5 else "false" + np = int(params[LottieTensor.Index.Group.NP]) if params[LottieTensor.Index.Group.NP] > -2000 else 0 + + lines.append(f'({cmd} ix={ix} cix={cix} bm={bm} hd={hd} np={np})') + + + elif cmd_idx == LottieTensor.CMD_PATH: + + ix = int(params[LottieTensor.Index.Path.IX]) if params[LottieTensor.Index.Path.IX] > -2000 else 1 + ind = int(params[LottieTensor.Index.Path.IND]) if params[LottieTensor.Index.Path.IND] > -2000 else 0 + ks_ix = int(params[LottieTensor.Index.Path.KS_IX]) if params[LottieTensor.Index.Path.KS_IX] > -2000 else 2 + closed = "true" if params[LottieTensor.Index.Path.CLOSED] > 0.5 else "false" + hd = "true" if params[LottieTensor.Index.Path.HD] > 0.5 else "false" # Add HD + + if params[LottieTensor.Index.Path.ANIMATED] > 0.5: + lines.append(f'({cmd} ix={ix} ind={ind} ks_ix={ks_ix} animated="true" hd={hd})') + current_context = "path" + else: + lines.append(f'({cmd} ix={ix} ind={ind} ks_ix={ks_ix} closed={closed} hd={hd})') + + + elif cmd_idx == LottieTensor.CMD_POINT: + # Check if this is a valid point (not padding) + if params[LottieTensor.Index.Point.X] > -2000 and params[LottieTensor.Index.Point.Y] > -2000: + x = LottieTensor._format_value(params[LottieTensor.Index.Point.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Point.Y]) + in_x = LottieTensor._format_value(params[LottieTensor.Index.Point.IN_X]) + in_y = LottieTensor._format_value(params[LottieTensor.Index.Point.IN_Y]) + out_x = LottieTensor._format_value(params[LottieTensor.Index.Point.OUT_X]) + out_y = LottieTensor._format_value(params[LottieTensor.Index.Point.OUT_Y]) + lines.append(f"({cmd} x={x} y={y} in_x={in_x} in_y={in_y} out_x={out_x} out_y={out_y})") + # else: skip padding points + + + elif cmd_idx == LottieTensor.CMD_FILL: + #name = string_params.get(f"{cmd_key}_name", "Fill") + + color_dim = int(params[LottieTensor.Index.Fill.COLOR_DIM]) if params[LottieTensor.Index.Fill.COLOR_DIM] > -2000 else 3 + has_c_a = "True" if params[LottieTensor.Index.Fill.HAS_C_A] > 0.5 else "False" + has_c_ix = "True" if params[LottieTensor.Index.Fill.HAS_C_IX] > 0.5 else "False" + c_ix = int(params[LottieTensor.Index.Fill.C_IX]) if params[LottieTensor.Index.Fill.C_IX] > -2000 else 4 + bm = int(params[LottieTensor.Index.Fill.BM]) if params[LottieTensor.Index.Fill.BM] > -2000 else 0 + fill_rule = int(params[LottieTensor.Index.Fill.FILL_RULE]) if params[LottieTensor.Index.Fill.FILL_RULE] > -2000 else 1 + has_o_a = "True" if params[LottieTensor.Index.Fill.HAS_O_A] > 0.5 else "False" + has_o_ix = "True" if params[LottieTensor.Index.Fill.HAS_O_IX] > 0.5 else "False" + o_ix = int(params[LottieTensor.Index.Fill.O_IX]) if params[LottieTensor.Index.Fill.O_IX] > -2000 else 5 + + color_animated = params[LottieTensor.Index.Fill.COLOR_ANIMATED] > 0.5 + opacity_animated = params[LottieTensor.Index.Fill.OPACITY_ANIMATED] > 0.5 + + line_parts = [f'({cmd}'] + + # Handle color output + if color_animated: + # Output color keyframes with easing + color_keyframes_json = string_params.get(f"{cmd_key}_color_keyframes", "[]") + color_keyframes = json.loads(color_keyframes_json) + for i, kf in enumerate(color_keyframes): + line_parts.append(f' c_kf_{i}_t={LottieTensor._format_value(kf["t"])}') + line_parts.append(f' c_kf_{i}_r={LottieTensor._format_value(kf["r"]/255)}') + line_parts.append(f' c_kf_{i}_g={LottieTensor._format_value(kf["g"]/255)}') + line_parts.append(f' c_kf_{i}_b={LottieTensor._format_value(kf["b"]/255)}') + # Add easing parameters if they exist and are non-zero (divide by 100 for float output) + if "i_x" in kf and (abs(kf["i_x"]) > 1e-6 or abs(kf.get("i_y", 0)) > 1e-6 or + abs(kf.get("o_x", 0)) > 1e-6 or abs(kf.get("o_y", 0)) > 1e-6): + line_parts.append(f' c_kf_{i}_i_x={LottieTensor._format_value(kf["i_x"] / 100, preserve_int=False)}') + line_parts.append(f' c_kf_{i}_i_y={LottieTensor._format_value(kf.get("i_y", 0) / 100, preserve_int=False)}') + line_parts.append(f' c_kf_{i}_o_x={LottieTensor._format_value(kf.get("o_x", 0) / 100, preserve_int=False)}') + line_parts.append(f' c_kf_{i}_o_y={LottieTensor._format_value(kf.get("o_y", 0) / 100, preserve_int=False)}') + line_parts.append(f' c_kf_count={len(color_keyframes)}') + line_parts.append(' color_animated=true') + else: + # Output static color + r = LottieTensor._format_value(params[LottieTensor.Index.Fill.R]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.Fill.G]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.Fill.B]/255) + line_parts.append(f' r={r} g={g} b={b} color_animated=false') + + # Add common color parameters + line_parts.append(f' color_dim={color_dim} has_c_a={has_c_a} has_c_ix={has_c_ix} c_ix={c_ix} bm={bm} fill_rule={fill_rule}') + + # Handle opacity output + if opacity_animated: + # Output opacity keyframes (divide by 100 for float output) + opacity_keyframes_json = string_params.get(f"{cmd_key}_opacity_keyframes", "[]") + opacity_keyframes = json.loads(opacity_keyframes_json) + for i, kf in enumerate(opacity_keyframes): + line_parts.append(f' o_kf_{i}_t={LottieTensor._format_value(kf["t"])}') + line_parts.append(f' o_kf_{i}_s={LottieTensor._format_value(kf["s"])}') + line_parts.append(f' o_kf_{i}_i_x={LottieTensor._format_value(kf["i_x"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_{i}_i_y={LottieTensor._format_value(kf["i_y"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_{i}_o_x={LottieTensor._format_value(kf["o_x"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_{i}_o_y={LottieTensor._format_value(kf["o_y"] / 100, preserve_int=False)}') + line_parts.append(f' o_kf_count={len(opacity_keyframes)}') + line_parts.append(' opacity_animated=true') + else: + # Output static opacity + opacity = LottieTensor._format_value(params[LottieTensor.Index.Fill.OPACITY]) + line_parts.append(f' opacity={opacity} opacity_animated=false') + + # Add opacity-related parameters + line_parts.append(f' has_o_a={has_o_a} has_o_ix={has_o_ix} o_ix={o_ix})') + + lines.append(''.join(line_parts)) + + + + elif cmd_idx == LottieTensor.CMD_BEZIER: + closed = "true" if params[LottieTensor.Index.Bezier.CLOSED] > 0.5 else "false" + lines.append(f'({cmd} closed={closed})') + + elif cmd_idx == LottieTensor.CMD_ELLIPSE: + #name = string_params.get(f"{cmd_key}_name", "Ellipse Path 1") + lines.append(f'({cmd})') + + + elif cmd_idx == LottieTensor.CMD_SIZE: + # Check if size is animated - also check for PAD_VAL + if params[LottieTensor.Index.Transform.ANIMATED] > -2000 and params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "size" + else: + # äæ®ę”¹ļ¼šä½æē”Ø Transform.X 和 Transform.Y + x = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + lines.append(f'({cmd} {x} {y})') + + + elif cmd_idx == LottieTensor.CMD_RECT: + #name = string_params.get(f"{cmd_key}_name", "Rectangle Path 1") + hd = "true" if params[LottieTensor.Index.Rect.HD] > 0.5 else "false" + d = int(params[LottieTensor.Index.Rect.D]) if params[LottieTensor.Index.Rect.D] > -2000 else 1 + lines.append(f'({cmd} hd={hd} d={d})') + + elif cmd_idx == LottieTensor.CMD_ROUNDED: + rounded = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + ix = int(params[LottieTensor.Index.SingleValue.IX]) if params[LottieTensor.Index.SingleValue.IX] > -2000 else 4 + lines.append(f'({cmd} {rounded} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_TRIM: + #name = string_params.get(f"{cmd_key}_name", "Trim Paths 1") + ix = int(params[LottieTensor.Index.Trim.IX]) if params[LottieTensor.Index.Trim.IX] > -2000 else 1 + lines.append(f'({cmd} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_END: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "trim_end" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_START: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "trim_start" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_OFFSET: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "trim_offset" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + + + elif cmd_idx == LottieTensor.CMD_MULTIPLE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_REPEATER: + #name = string_params.get(f"{cmd_key}_name", "Repeater 1") + ix = int(params[LottieTensor.Index.Repeater.IX]) if params[LottieTensor.Index.Repeater.IX] > -2000 else 1 + lines.append(f'({cmd} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_COPIES: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + ix = int(params[LottieTensor.Index.SingleValue.IX]) if params[LottieTensor.Index.SingleValue.IX] > -2000 else 1 + lines.append(f'({cmd} {val} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_REPEATER_OFFSET: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + ix = int(params[LottieTensor.Index.SingleValue.IX]) if params[LottieTensor.Index.SingleValue.IX] > -2000 else 2 + lines.append(f'({cmd} {val} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_COMPOSITE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_REPEATER_TRANSFORM: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_TR_P_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_A_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_SCALE: + val1 = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + val2 = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f'({cmd} {val1} {val2})') + + elif cmd_idx == LottieTensor.CMD_TR_S_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 3 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_R_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 4 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_SO_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 5 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_EO_IX: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 6 + lines.append(f'({cmd} {val})') + + + elif cmd_idx == LottieTensor.CMD_TRANSFORM_SHAPE: + #name = string_params.get(f"{cmd_key}_name", "Transform") + + hd = "true" if params[LottieTensor.Index.TransformShape.HD] > 0.5 else "false" + position_x = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.POSITION_X]) + position_y = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.POSITION_Y]) + scale_x = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SCALE_X]) + scale_y = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SCALE_Y]) + rotation = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.ROTATION]) + opacity = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.OPACITY]) + anchor_x = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.ANCHOR_X]) + anchor_y = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.ANCHOR_Y]) + + # Build the output line + line = f'({cmd} hd={hd} position="{position_x} {position_y}" scale="{scale_x} {scale_y}" rotation="{rotation}" opacity="{opacity}" anchor="{anchor_x} {anchor_y}"' + + # Only add skew if it's not 0 or PAD_VAL + if params[LottieTensor.Index.TransformShape.SKEW] > -2000 and abs(params[LottieTensor.Index.TransformShape.SKEW]) > 1e-6: + skew = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SKEW]) + line += f' skew="{skew}"' + + # Only add skew_axis if it's not 0 or PAD_VAL + if params[LottieTensor.Index.TransformShape.SKEW_AXIS] > -2000 and abs(params[LottieTensor.Index.TransformShape.SKEW_AXIS]) > 1e-6: + skew_axis = LottieTensor._format_value(params[LottieTensor.Index.TransformShape.SKEW_AXIS]) + line += f' skew_axis="{skew_axis}"' + + line += ')' + lines.append(line) + + elif cmd_idx == LottieTensor.CMD_PARENT: + parent_index = int(params[LottieTensor.Index.Parent.PARENT_INDEX]) if params[LottieTensor.Index.Parent.PARENT_INDEX] > -2000 else 0 + lines.append(f'({cmd} {parent_index})') + + elif cmd_idx == LottieTensor.CMD_ASSET: + # Get tokenizer + tokenizer = LottieTensor.get_tokenizer() + + # Decode ID from tokens + id_count = int(params[LottieTensor.Index.Asset.ID_TOKEN_COUNT]) if params[LottieTensor.Index.Asset.ID_TOKEN_COUNT] > -2000 else 0 + id_tokens = [] + for i in range(id_count): + if params[LottieTensor.Index.Asset.ID_TOKEN_0 + i] > -2000: + id_tokens.append(int(params[LottieTensor.Index.Asset.ID_TOKEN_0 + i])) + + asset_id = tokenizer.decode(id_tokens, skip_special_tokens=True) if id_tokens else "comp_0" + fr = LottieTensor._format_value(params[LottieTensor.Index.Asset.FR]) + + lines.append(f'({cmd} id="{asset_id}" fr={fr})') + + elif cmd_idx == LottieTensor.CMD_FONT: + # Get tokenizer + tokenizer = LottieTensor.get_tokenizer() + + # Decode family from tokens + family_count = int(params[LottieTensor.Index.Font.FAMILY_TOKEN_COUNT]) if params[LottieTensor.Index.Font.FAMILY_TOKEN_COUNT] > -2000 else 0 + family_tokens = [] + for i in range(family_count): + if params[LottieTensor.Index.Font.FAMILY_TOKEN_0 + i] > -2000: + family_tokens.append(int(params[LottieTensor.Index.Font.FAMILY_TOKEN_0 + i])) + + # Decode style from tokens + style_count = int(params[LottieTensor.Index.Font.STYLE_TOKEN_COUNT]) if params[LottieTensor.Index.Font.STYLE_TOKEN_COUNT] > -2000 else 0 + style_tokens = [] + for i in range(style_count): + if params[LottieTensor.Index.Font.STYLE_TOKEN_0 + i] > -2000: + style_tokens.append(int(params[LottieTensor.Index.Font.STYLE_TOKEN_0 + i])) + + family = tokenizer.decode(family_tokens, skip_special_tokens=True) if family_tokens else "" + style = tokenizer.decode(style_tokens, skip_special_tokens=True) if style_tokens else "" + ascent = LottieTensor._format_value(params[LottieTensor.Index.Font.ASCENT]) + + lines.append(f'({cmd} family="{family}" style="{style}" ascent={ascent})') + + elif cmd_idx == LottieTensor.CMD_CHAR: + # Get tokenizer + tokenizer = LottieTensor.get_tokenizer() + + # Decode ch from tokens + ch_count = int(params[LottieTensor.Index.Char.CH_TOKEN_COUNT]) if params[LottieTensor.Index.Char.CH_TOKEN_COUNT] > -2000 else 0 + ch_tokens = [] + for i in range(ch_count): + if params[LottieTensor.Index.Char.CH_TOKEN_0 + i] > -2000: + ch_tokens.append(int(params[LottieTensor.Index.Char.CH_TOKEN_0 + i])) + + # Decode style from tokens + style_count = int(params[LottieTensor.Index.Char.STYLE_TOKEN_COUNT]) if params[LottieTensor.Index.Char.STYLE_TOKEN_COUNT] > -2000 else 0 + style_tokens = [] + for i in range(style_count): + if params[LottieTensor.Index.Char.STYLE_TOKEN_0 + i] > -2000: + style_tokens.append(int(params[LottieTensor.Index.Char.STYLE_TOKEN_0 + i])) + + # Decode family from tokens + family_count = int(params[LottieTensor.Index.Char.FAMILY_TOKEN_COUNT]) if params[LottieTensor.Index.Char.FAMILY_TOKEN_COUNT] > -2000 else 0 + family_tokens = [] + for i in range(family_count): + if params[LottieTensor.Index.Char.FAMILY_TOKEN_0 + i] > -2000: + family_tokens.append(int(params[LottieTensor.Index.Char.FAMILY_TOKEN_0 + i])) + + ch = tokenizer.decode(ch_tokens, skip_special_tokens=True) if ch_tokens else "" + style = tokenizer.decode(style_tokens, skip_special_tokens=True) if style_tokens else "" + family = tokenizer.decode(family_tokens, skip_special_tokens=True) if family_tokens else "" + size = LottieTensor._format_value(params[LottieTensor.Index.Char.SIZE]) + w = LottieTensor._format_value(params[LottieTensor.Index.Char.W]) + + lines.append(f'({cmd} ch="{ch}" size={size} style="{style}" w={w} family="{family}")') + + + elif cmd_idx == LottieTensor.CMD_TEXT_LAYER: + #name = string_params.get(f"{cmd_key}_name", "Text Layer") + index = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.TextLayer.START_TIME]) + hasMask = "True" if params[LottieTensor.Index.TextLayer.HAS_MASK] > 0.5 else "False" # ę–°å¢ž + lines.append(f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time} hasMask={hasMask})') # 修改 + + + elif cmd_idx == LottieTensor.CMD_FONT_SIZE: + size = LottieTensor._format_value(params[LottieTensor.Index.FontSize.SIZE]) + lines.append(f'({cmd} {size})') + + elif cmd_idx == LottieTensor.CMD_FONT_FAMILY: + family = string_params.get(f"{cmd_key}_family", "") + lines.append(f'({cmd} "{family}")') + + elif cmd_idx == LottieTensor.CMD_TEXT: + text = string_params.get(f"{cmd_key}_text", "") + lines.append(f'({cmd} "{text}")') + + elif cmd_idx == LottieTensor.CMD_CA: + value = LottieTensor._format_value(params[LottieTensor.Index.Ca.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_JUSTIFY: + value = LottieTensor._format_value(params[LottieTensor.Index.Justify.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_TRACKING: + value = LottieTensor._format_value(params[LottieTensor.Index.Tracking.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_LINE_HEIGHT: + value = LottieTensor._format_value(params[LottieTensor.Index.LineHeight.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_LETTER_SPACING: + value = LottieTensor._format_value(params[LottieTensor.Index.LetterSpacing.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_FILL_COLOR: + r = LottieTensor._format_value(params[LottieTensor.Index.FillColor.R]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.FillColor.G]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.FillColor.B]/255) + lines.append(f'({cmd} {r} {g} {b})') + + elif cmd_idx == LottieTensor.CMD_G: + value = LottieTensor._format_value(params[LottieTensor.Index.G.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT: + a = LottieTensor._format_value(params[LottieTensor.Index.Alignment.A]) + lines.append(f'({cmd} a={a})') + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT_K: + val1 = LottieTensor._format_value(params[LottieTensor.Index.AlignmentK.VALUE1]) + val2 = LottieTensor._format_value(params[LottieTensor.Index.AlignmentK.VALUE2]) + lines.append(f'({cmd} {val1} {val2})') + + elif cmd_idx == LottieTensor.CMD_ALIGNMENT_IX: + value = LottieTensor._format_value(params[LottieTensor.Index.AlignmentIx.VALUE]) + lines.append(f'({cmd} {value})') + elif cmd_idx == LottieTensor.CMD_EFFECTS: + lines.append(f'({cmd})') + + + elif cmd_idx == LottieTensor.CMD_EFFECT: + #name = string_params.get(f"{cmd_key}_name", "") + match_name = string_params.get(f"{cmd_key}_match_name", "") + type_val = int(params[LottieTensor.Index.Effect.TYPE]) if params[LottieTensor.Index.Effect.TYPE] > -2000 else 0 + index = int(params[LottieTensor.Index.Effect.INDEX]) if params[LottieTensor.Index.Effect.INDEX] > -2000 else 1 + np = int(params[LottieTensor.Index.Effect.NP]) if params[LottieTensor.Index.Effect.NP] > -2000 else 0 + enabled = int(params[LottieTensor.Index.Effect.ENABLED]) if params[LottieTensor.Index.Effect.ENABLED] > -2000 else 1 + + line = f'({cmd} type={type_val} index={index}' + if np > 0: # Only output np if it's non-zero + line += f' np={np}' + line += f' match_name="{match_name}"' + if enabled != 1: # Only output enabled if it's not the default value + line += f' enabled={enabled}' + line += ')' + lines.append(line) + + # Add CMD_LAYER_EFFECT output: + elif cmd_idx == LottieTensor.CMD_LAYER_EFFECT: + #name = string_params.get(f"{cmd_key}_name", "") + match_name = string_params.get(f"{cmd_key}_match_name", "") + index = int(params[LottieTensor.Index.LayerEffect.INDEX]) if params[LottieTensor.Index.LayerEffect.INDEX] > -2000 else 1 + value = LottieTensor._format_value(params[LottieTensor.Index.LayerEffect.VALUE]) + lines.append(f'({cmd} index={index} value={value} match_name="{match_name}")') + + + elif cmd_idx == LottieTensor.CMD_DROPDOWN: + #name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.Dropdown.INDEX]) if params[LottieTensor.Index.Dropdown.INDEX] > -2000 else 1 + value = int(params[LottieTensor.Index.Dropdown.VALUE]) if params[LottieTensor.Index.Dropdown.VALUE] > -2000 else 0 + lines.append(f'({cmd} index={index} value={value})') + + elif cmd_idx == LottieTensor.CMD_NO_VALUE: + #@name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.NO_VALUE.INDEX]) if params[LottieTensor.Index.NO_VALUE.INDEX] > -2000 else 1 + value = int(params[LottieTensor.Index.NO_VALUE.VALUE]) if params[LottieTensor.Index.NO_VALUE.VALUE] > -2000 else 0 + lines.append(f'({cmd} index={index} value={value})') + + + elif cmd_idx == LottieTensor.CMD_IGNORED: + #name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.Ignored.INDEX]) if params[LottieTensor.Index.Ignored.INDEX] > -2000 else 1 + value = LottieTensor._format_value(params[LottieTensor.Index.Ignored.VALUE]) + lines.append(f'({cmd} index={index} value={value})') + + elif cmd_idx == LottieTensor.CMD_SLIDER: + #name = string_params.get(f"{cmd_key}_name", "") + index = int(params[LottieTensor.Index.Slider.INDEX]) if params[LottieTensor.Index.Slider.INDEX] > -2000 else 1 + value = LottieTensor._format_value(params[LottieTensor.Index.Slider.VALUE]) + lines.append(f'({cmd} index={index} value={value})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_FILL: + #name = string_params.get(f"{cmd_key}_name", "Gradient Fill 1") + lines.append(f'({cmd})') + current_context = "gradient_fill" # Set context for subsequent commands + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "gradient_fill": + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_FILL_RULE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_START_POINT: + x = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + y = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_END_POINT: + x = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE1]) + y = LottieTensor._format_value(params[LottieTensor.Index.TwoValues.VALUE2]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_TYPE: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 1 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_HIGHLIGHT_LENGTH: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_HIGHLIGHT_ANGLE: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_ORIGINAL_COLORS: + count = int(params[LottieTensor.Index.OriginalColors.COUNT]) if params[LottieTensor.Index.OriginalColors.COUNT] > -2000 else 0 + + color_values = [] + for i in range(count): + if params[LottieTensor.Index.OriginalColors.COLOR_0 + i] > -2000: + color_values.append(LottieTensor._format_value(params[LottieTensor.Index.OriginalColors.COLOR_0 + i])/255) + + colors_str = ", ".join(str(v) for v in color_values) + lines.append(f'({cmd} [{colors_str}])') + + + elif cmd_idx == LottieTensor.CMD_COLOR_POINTS: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_FILL_END: + lines.append(f'({cmd})') + current_context = None # Reset context + + elif cmd_idx == LottieTensor.CMD_GRADIENT_STROKE: + #name = string_params.get(f"{cmd_key}_name", "Gradient Stroke 1") + lines.append(f'({cmd})') + current_context = "gradient_stroke" # Set context for subsequent commands + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "gradient_stroke": + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + current_context = "gradient_stroke" # Set context for subsequent commands + + + elif cmd_idx == LottieTensor.CMD_WIDTH: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + # 除仄100ę¢å¤åŽŸå€¼ + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE] / 10, preserve_int=False) + lines.append(f'({cmd} {val})') + + #if current_context == "gradient_stroke": + # Check if value exists (not PAD_VAL) + # if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + # val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + # lines.append(f'({cmd} {val})') + # else: + # No value, output just the command + # lines.append(f'({cmd})') + #else: + # Handle width in other contexts if needed + # lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_LINE_CAP: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_LINE_JOIN: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 2 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_MITER_LIMIT: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 0 + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_GRADIENT_STROKE_END: + lines.append(f'({cmd})') + current_context = None # Reset context + + elif cmd_idx == LottieTensor.CMD_COLOR: + #name = string_params.get(f"{cmd_key}_name", "Color") + index = int(params[LottieTensor.Index.Color.INDEX]) if params[LottieTensor.Index.Color.INDEX] > -2000 else 1 + r = LottieTensor._format_value(params[LottieTensor.Index.Color.R]/255) + g = LottieTensor._format_value(params[LottieTensor.Index.Color.G]/255) + b = LottieTensor._format_value(params[LottieTensor.Index.Color.B]/255) + lines.append(f'({cmd} index={index} r={r} g={g} b={b})') + + + elif cmd_idx == LottieTensor.CMD_MERGE: + #name = string_params.get(f"{cmd_key}_name", "Merge Paths 1") + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MERGE_MODE: + mode = int(params[LottieTensor.Index.MergeMode.MODE]) if params[LottieTensor.Index.MergeMode.MODE] > -2000 else 1 + lines.append(f'({cmd} {mode})') + + + elif cmd_idx == LottieTensor.CMD_SOLID_LAYER: + #name = string_params.get(f"{cmd_key}_name", "Solid Layer") + #color = string_params.get(f"{cmd_key}_color", "#000000") + r = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_R]))) + g = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_G]))) + b = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_B]))) + a = int(min(255, max(0, params[LottieTensor.Index.SolidLayer.COLOR_A]))) + + # You can use RGB values directly or convert back to hex if needed + color_rgb = (r, g, b, a) + # Or convert back to hex format if required: + color = f"#{r:02x}{g:02x}{b:02x}{a:02x}" + + index = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.INDEX]) + in_point = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.IN_POINT]) + out_point = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.OUT_POINT]) + start_time = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.START_TIME]) + width = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.WIDTH]) + height = LottieTensor._format_value(params[LottieTensor.Index.SolidLayer.HEIGHT]) + hasMask = "True" if params[LottieTensor.Index.SolidLayer.HAS_MASK] > 0.5 else "False" + + lines.append(f'({cmd} index={index} in_point={in_point} out_point={out_point} start_time={start_time} color="{color}" width={width} height={height} hasMask={hasMask})') + + elif cmd_idx == LottieTensor.CMD_MASKS_PROPERTIES: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK: + #nm = string_params.get(f"{cmd_key}_nm", "Mask 1") + + index = int(params[LottieTensor.Index.Mask.INDEX]) if params[LottieTensor.Index.Mask.INDEX] > -2000 else 0 + inv = "true" if params[LottieTensor.Index.Mask.INV] > 0.5 else "false" + + # Convert mode value back to string + mode_val = int(params[LottieTensor.Index.Mask.MODE]) if params[LottieTensor.Index.Mask.MODE] > -2000 else 0 + mode_map = {0: "a", 1: "s", 2: "i", 3: "n"} + mode = mode_map.get(mode_val, "a") + + lines.append(f'({cmd} index={index} inv={inv} mode={mode})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT: + a = int(params[LottieTensor.Index.MaskPt.A]) if params[LottieTensor.Index.MaskPt.A] > -2000 else 1 + ix = int(params[LottieTensor.Index.MaskPt.IX]) if params[LottieTensor.Index.MaskPt.IX] > -2000 else 1 + lines.append(f'({cmd} a={a} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_MASK_PT_K_C: + c = "true" if params[LottieTensor.Index.MaskPtK.C] > 0.5 else "false" + lines.append(f'({cmd} {c})') # Changed from c={c} to just {c} + + + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_K_I, LottieTensor.CMD_MASK_PT_K_O, LottieTensor.CMD_MASK_PT_K_V]: + count = int(params[LottieTensor.Index.MaskPtKValues.COUNT]) if params[LottieTensor.Index.MaskPtKValues.COUNT] > -2000 else 0 + + if count == 0: + # Fallback: find last non-padding value + for i in range(19, -1, -1): + val = params[LottieTensor.Index.MaskPtKValues.V1 + i] + if val > -2000: + count = i + 1 + break + + values = [] + for i in range(count): + val = params[LottieTensor.Index.MaskPtKValues.V1 + i] + if val > -2000: + values.append(LottieTensor._format_value(val)) + else: + values.append(0.0) + + lines.append(f'({cmd} {" ".join(str(v) for v in values)})') + + elif cmd_idx == LottieTensor.CMD_MASK_O: + a = int(params[LottieTensor.Index.MaskO.A]) if params[LottieTensor.Index.MaskO.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MaskO.K]) if params[LottieTensor.Index.MaskO.K] > -2000 else 100 + ix = int(params[LottieTensor.Index.MaskO.IX]) if params[LottieTensor.Index.MaskO.IX] > -2000 else 3 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_MASK_X: + a = int(params[LottieTensor.Index.MaskX.A]) if params[LottieTensor.Index.MaskX.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MaskX.K]) if params[LottieTensor.Index.MaskX.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.MaskX.IX]) if params[LottieTensor.Index.MaskX.IX] > -2000 else 4 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_MASK_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_K: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_K_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASKS_PROPERTIES_END: + lines.append(f'({cmd})') + + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_K_ARRAY, LottieTensor.CMD_MASK_PT_K_ARRAY_END, + LottieTensor.CMD_MASK_PT_KF_S, LottieTensor.CMD_MASK_PT_KF_S_END, + LottieTensor.CMD_MASK_PT_KF_SHAPE_END, LottieTensor.CMD_MASK_PT_KEYFRAME_END]: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KEYFRAME: + index = int(params[LottieTensor.Index.MaskPtKeyframe.INDEX]) if params[LottieTensor.Index.MaskPtKeyframe.INDEX] > -2000 else 0 + t = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKeyframe.T]) + lines.append(f'({cmd} index={index} t={t})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_I: + x = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfI.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfI.Y]) + lines.append(f'({cmd} x={x} y={y})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_O: + x = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfO.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.MaskPtKfO.Y]) + lines.append(f'({cmd} x={x} y={y})') + + elif cmd_idx == LottieTensor.CMD_MASK_PT_KF_SHAPE: + index = int(params[LottieTensor.Index.MaskPtKfShape.INDEX]) if params[LottieTensor.Index.MaskPtKfShape.INDEX] > -2000 else 0 + c = "true" if params[LottieTensor.Index.MaskPtKfShape.C] > 0.5 else "false" + lines.append(f'({cmd} index={index} c={c})') + + + elif cmd_idx in [LottieTensor.CMD_MASK_PT_KF_SHAPE_I, LottieTensor.CMD_MASK_PT_KF_SHAPE_O, + LottieTensor.CMD_MASK_PT_KF_SHAPE_V]: + # Get the count from params, not from string_params + count = int(params[LottieTensor.Index.MaskPtKfShapeValues.COUNT]) if params[LottieTensor.Index.MaskPtKfShapeValues.COUNT] > -2000 else 0 + + if count == 0: + # If no count stored, find the last non-padding value + for i in range(19, -1, -1): # Check V1 through V20 + if i < LottieTensor.PARAM_DIM: + val = params[LottieTensor.Index.MaskPtKfShapeValues.V1 + i] + if val > -2000: + count = i + 1 + break + if count == 0: + count = 8 # Default to 8 if no valid values found + + # Output the exact number of values + values = [] + for i in range(count): + if i < 20: + val = params[LottieTensor.Index.MaskPtKfShapeValues.V1 + i] + if val > -2000: + values.append(LottieTensor._format_value(val)) + else: + values.append(0.0) + else: + values.append(0.0) + + lines.append(f'({cmd} {" ".join(str(v) for v in values)})') + + elif cmd_idx == LottieTensor.CMD_TR_POSITION: + x = LottieTensor._format_value(params[LottieTensor.Index.TrPosition.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.TrPosition.Y]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_TR_ANCHOR: + x = LottieTensor._format_value(params[LottieTensor.Index.TrAnchor.X]) + y = LottieTensor._format_value(params[LottieTensor.Index.TrAnchor.Y]) + lines.append(f'({cmd} {x} {y})') + + elif cmd_idx == LottieTensor.CMD_TR_ROTATION: + val = LottieTensor._format_value(params[LottieTensor.Index.TrRotation.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_START_OPACITY: + val = LottieTensor._format_value(params[LottieTensor.Index.TrStartOpacity.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_TR_END_OPACITY: + if params[LottieTensor.Index.TrEndOpacity.VALUE] > -2000: + val = LottieTensor._format_value(params[LottieTensor.Index.TrEndOpacity.VALUE]) + lines.append(f'({cmd} {val})') + else: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ZIG_ZAG: + #name = string_params.get(f"{cmd_key}_name", "Zig Zag 1") + ix = int(params[LottieTensor.Index.ZigZag.IX]) if params[LottieTensor.Index.ZigZag.IX] > -2000 else 2 + lines.append(f'({cmd} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_FREQUENCY: + value = LottieTensor._format_value(params[LottieTensor.Index.Frequency.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_AMPLITUDE: + value = LottieTensor._format_value(params[LottieTensor.Index.Amplitude.VALUE]) + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_POINT_TYPE: + value = int(params[LottieTensor.Index.PointType.VALUE]) if params[LottieTensor.Index.PointType.VALUE] > -2000 else 2 + lines.append(f'({cmd} {value})') + + elif cmd_idx == LottieTensor.CMD_ZIG_ZAG_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATORS: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATOR: + #nm = string_params.get(f"{cmd_key}_nm", "Animator 1") + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_RANGE_SELECTOR: + t = int(params[LottieTensor.Index.RangeSelector.T]) if params[LottieTensor.Index.RangeSelector.T] > -2000 else 0 + r = int(params[LottieTensor.Index.RangeSelector.R]) if params[LottieTensor.Index.RangeSelector.R] > -2000 else 1 + b = int(params[LottieTensor.Index.RangeSelector.B]) if params[LottieTensor.Index.RangeSelector.B] > -2000 else 1 + sh = int(params[LottieTensor.Index.RangeSelector.SH]) if params[LottieTensor.Index.RangeSelector.SH] > -2000 else 1 + rn = int(params[LottieTensor.Index.RangeSelector.RN]) if params[LottieTensor.Index.RangeSelector.RN] > -2000 else 0 + lines.append(f'({cmd} t={t} r={r} b={b} sh={sh} rn={rn})') + + elif cmd_idx == LottieTensor.CMD_RANGE_START: + a = int(params[LottieTensor.Index.RangeStart.A]) if params[LottieTensor.Index.RangeStart.A] > -2000 else 0 + lines.append(f'({cmd} a={a})') + if a > 0.5: + current_context = "range_start" + + + elif cmd_idx == LottieTensor.CMD_RANGE_START_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.RangeStartKeyframe.T]) + s = LottieTensor._format_value(params[LottieTensor.Index.RangeStartKeyframe.S]) + + # Check if this is the last keyframe (no easing values or all zeros) + i_x = params[LottieTensor.Index.RangeStartKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.RangeStartKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.RangeStartKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.RangeStartKeyframe.O_Y]/100 + + has_meaningful_easing = ( + (i_x > -2000 and abs(i_x) > 1e-6) or + (i_y > -2000 and abs(i_y) > 1e-6) or + (o_x > -2000 and abs(o_x) > 1e-6) or + (o_y > -2000 and abs(o_y) > 1e-6) + ) + + if has_meaningful_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + elif cmd_idx == LottieTensor.CMD_RANGE_START_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_AMOUNT: + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 100 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 4 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_MAX_EASE: + a = int(params[LottieTensor.Index.MaxEase.A]) if params[LottieTensor.Index.MaxEase.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MaxEase.K]) if params[LottieTensor.Index.MaxEase.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.MaxEase.IX]) if params[LottieTensor.Index.MaxEase.IX] > -2000 else 7 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_MIN_EASE: + a = int(params[LottieTensor.Index.MinEase.A]) if params[LottieTensor.Index.MinEase.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.MinEase.K]) if params[LottieTensor.Index.MinEase.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.MinEase.IX]) if params[LottieTensor.Index.MinEase.IX] > -2000 else 8 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_PROPERTIES: + lines.append(f'({cmd})') + current_context = "animator_properties" + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_PROPERTIES_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_OPACITY and current_context == "animator_properties": + # Special handling for opacity within animator_properties + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 9 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + elif cmd_idx == LottieTensor.CMD_RANGE_SELECTOR_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATOR_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_ANIMATORS_END: + lines.append(f'({cmd})') + + elif cmd_idx == LottieTensor.CMD_RADIUS: + val = LottieTensor._format_value(params[LottieTensor.Index.Radius.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_RANGE_END: + a = int(params[LottieTensor.Index.RangeEnd.A]) if params[LottieTensor.Index.RangeEnd.A] > -2000 else 0 + lines.append(f'({cmd} a={a})') + if a > 0.5: + current_context = "range_end" + + + elif cmd_idx == LottieTensor.CMD_RANGE_END_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.RangeEndKeyframe.T]) + s = LottieTensor._format_value(params[LottieTensor.Index.RangeEndKeyframe.S]) + + # Check if this is the last keyframe (no easing values or all zeros) + i_x = params[LottieTensor.Index.RangeEndKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.RangeEndKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.RangeEndKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.RangeEndKeyframe.O_Y]/100 + + has_meaningful_easing = ( + (i_x > -2000 and abs(i_x) > 1e-6) or + (i_y > -2000 and abs(i_y) > 1e-6) or + (o_x > -2000 and abs(o_x) > 1e-6) or + (o_y > -2000 and abs(o_y) > 1e-6) + ) + + if has_meaningful_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + elif cmd_idx == LottieTensor.CMD_RANGE_END_END: + lines.append(f'({cmd})') + current_context = None + + # Fix position output in animator_properties context + + elif cmd_idx == LottieTensor.CMD_POSITION and current_context == "animator_properties": + # Special handling for position within animator_properties + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 2 + + # Check if we have array values stored + x = params[LottieTensor.Index.Transform.X] + y = params[LottieTensor.Index.Transform.Y] + z = params[LottieTensor.Index.Transform.Z] + + if x > -2000 or y > -2000: # Changed condition - check x or y + # Format as array + x_val = LottieTensor._format_value(x) if x > -2000 else 0 + y_val = LottieTensor._format_value(y) if y > -2000 else 0 + + # Only include z if it's meaningful + if z > -2000 and abs(z) > 1e-6: + lines.append(f'({cmd} a={a} k=[{x_val}, {y_val}, {LottieTensor._format_value(z)}] ix={ix})') + else: + lines.append(f'({cmd} a={a} k=[{x_val}, {y_val}, 0] ix={ix})') + else: + # Format as single value + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 0 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + elif cmd_idx == LottieTensor.CMD_ML2: + val = int(params[LottieTensor.Index.SingleValue.VALUE]) if params[LottieTensor.Index.SingleValue.VALUE] > -2000 else 4 + lines.append(f'({cmd} {val})') + + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: + t = LottieTensor._format_value(params[LottieTensor.Index.RangeOffsetKeyframe.T]) + s = LottieTensor._format_value(params[LottieTensor.Index.RangeOffsetKeyframe.S]) + + # Check if this is the last keyframe (no easing values or all zeros) + i_x = params[LottieTensor.Index.RangeOffsetKeyframe.I_X]/100 + i_y = params[LottieTensor.Index.RangeOffsetKeyframe.I_Y]/100 + o_x = params[LottieTensor.Index.RangeOffsetKeyframe.O_X]/100 + o_y = params[LottieTensor.Index.RangeOffsetKeyframe.O_Y]/100 + + has_meaningful_easing = ( + (i_x > -2000 and abs(i_x) > 1e-6) or + (i_y > -2000 and abs(i_y) > 1e-6) or + (o_x > -2000 and abs(o_x) > 1e-6) or + (o_y > -2000 and abs(o_y) > 1e-6) + ) + + if has_meaningful_easing: + i_x_val = LottieTensor._format_value(i_x, preserve_int=False) if i_x > -2000 else 0 + i_y_val = LottieTensor._format_value(i_y, preserve_int=False) if i_y > -2000 else 0 + o_x_val = LottieTensor._format_value(o_x, preserve_int=False) if o_x > -2000 else 0 + o_y_val = LottieTensor._format_value(o_y, preserve_int=False) if o_y > -2000 else 0 + lines.append(f'({cmd} t={t} s={s} i_x={i_x_val} i_y={i_y_val} o_x={o_x_val} o_y={o_y_val})') + else: + lines.append(f'({cmd} t={t} s={s})') + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_S_M: + a = int(params[LottieTensor.Index.SM.A]) if params[LottieTensor.Index.SM.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.SM.K]) if params[LottieTensor.Index.SM.K] > -2000 else 100 + ix = int(params[LottieTensor.Index.SM.IX]) if params[LottieTensor.Index.SM.IX] > -2000 else 6 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + # 修改 CMD_OPACITY_ANIMATORS ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_OPACITY_ANIMATORS: + a = int(params[LottieTensor.Index.OpacityAnimators.A]) if params[LottieTensor.Index.OpacityAnimators.A] > -2000 else 0 + + if a > 0.5: + # Animated case - only output a + lines.append(f'({cmd} a={a})') + current_context = "opacity_animators" + else: + # Static case with k value + k = LottieTensor._format_value(params[LottieTensor.Index.OpacityAnimators.K]) if params[LottieTensor.Index.OpacityAnimators.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.OpacityAnimators.IX]) if params[LottieTensor.Index.OpacityAnimators.IX] > -2000 else 9 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + # 添加 CMD_POSITION_ANIMATORS ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_POSITION_ANIMATORS: + a = int(params[LottieTensor.Index.PositionAnimators.A]) if params[LottieTensor.Index.PositionAnimators.A] > -2000 else 0 + ix = int(params[LottieTensor.Index.PositionAnimators.IX]) if params[LottieTensor.Index.PositionAnimators.IX] > -2000 else 2 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "position_animators" + else: + # Static case with k value + k_x = params[LottieTensor.Index.PositionAnimators.K_X] + k_y = params[LottieTensor.Index.PositionAnimators.K_Y] + k_z = params[LottieTensor.Index.PositionAnimators.K_Z] + + if k_x > -2000 and k_y > -2000: + # Format as array + x_val = LottieTensor._format_value(k_x) if k_x > -2000 else 0 + y_val = LottieTensor._format_value(k_y) if k_y > -2000 else 0 + z_val = LottieTensor._format_value(k_z) if k_z > -2000 else 0 + lines.append(f'({cmd} a={a} k=[{x_val}, {y_val}, {z_val}] ix={ix})') + else: + lines.append(f'({cmd} a={a} k=[0.0, 0.0, 0.0] ix={ix})') + + # 添加 CMD_TRACKING_ANIMATORS ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_TRACKING_ANIMATORS: + a = int(params[LottieTensor.Index.TrackingAnimators.A]) if params[LottieTensor.Index.TrackingAnimators.A] > -2000 else 0 + k = LottieTensor._format_value(params[LottieTensor.Index.TrackingAnimators.K]) if params[LottieTensor.Index.TrackingAnimators.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.TrackingAnimators.IX]) if params[LottieTensor.Index.TrackingAnimators.IX] > -2000 else 89 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "tracking_animators" + else: + # Static case + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + # ę·»åŠ ē»“ęŸå‘½ä»¤ēš„č¾“å‡ŗļ¼š + elif cmd_idx == LottieTensor.CMD_POSITION_ANIMATORS_END: + lines.append(f'({cmd})') + current_context = None + + + # Add output formatting (after CMD_OPACITY_ANIMATORS, around line 5590) + elif cmd_idx == LottieTensor.CMD_SCALE_ANIMATORS: + a = int(params[LottieTensor.Index.ScaleAnimators.A]) if params[LottieTensor.Index.ScaleAnimators.A] > -2000 else 0 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "scale_animators" + else: + # Static case with k value + k_x = params[LottieTensor.Index.ScaleAnimators.K_X] + k_y = params[LottieTensor.Index.ScaleAnimators.K_Y] + k_z = params[LottieTensor.Index.ScaleAnimators.K_Z] + + if k_x > -2000 and k_y > -2000 and k_z > -2000: + # Check if all values are the same + if abs(k_x - k_y) < 1e-6 and abs(k_y - k_z) < 1e-6: + # Output single value + lines.append(f'({cmd} a={a} k={LottieTensor._format_value(k_x)})') + else: + # Output array + lines.append(f'({cmd} a={a} k=[{LottieTensor._format_value(k_x)}, {LottieTensor._format_value(k_y)}, {LottieTensor._format_value(k_z)}])') + else: + lines.append(f'({cmd} a={a} k=100)') + + elif cmd_idx == LottieTensor.CMD_SCALE_ANIMATORS_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_ROTATION_ANIMATORS: + a = int(params[LottieTensor.Index.RotationAnimators.A]) if params[LottieTensor.Index.RotationAnimators.A] > -2000 else 0 + + if a > 0.5: + # Animated case + lines.append(f'({cmd} a={a})') + current_context = "rotation_animators" + else: + # Static case with k value + k = LottieTensor._format_value(params[LottieTensor.Index.RotationAnimators.K]) if params[LottieTensor.Index.RotationAnimators.K] > -2000 else 0 + lines.append(f'({cmd} a={a} k={k})') + + elif cmd_idx == LottieTensor.CMD_WIDTH_ANIMATED: + # This is a standalone width_animated command + # The context should already be set from the stroke command + # No parameters needed for this command + pass + + elif cmd_idx == LottieTensor.CMD_RANGE_OFFSET: + # Use Amount indices for range_offset + a = int(params[LottieTensor.Index.Amount.A]) if params[LottieTensor.Index.Amount.A] > -2000 else 0 + + if a > 0.5: # This should be checking a, not params[LottieTensor.Index.Amount.A] again + # Animated case - only output a + lines.append(f'({cmd} a={a})') + current_context = "range_offset" + else: + # Static case - output a, k, and ix + k = LottieTensor._format_value(params[LottieTensor.Index.Amount.K]) if params[LottieTensor.Index.Amount.K] > -2000 else 0 + ix = int(params[LottieTensor.Index.Amount.IX]) if params[LottieTensor.Index.Amount.IX] > -2000 else 3 + lines.append(f'({cmd} a={a} k={k} ix={ix})') + + + + elif cmd_idx == LottieTensor.CMD_ROTATION_ANIMATORS_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_RECT_SIZE: + # Output rect_size with two values + if params[LottieTensor.Index.Transform.ANIMATED] > -2000 and params[LottieTensor.Index.Transform.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "size" + else: + # äæ®ę”¹ļ¼šä½æē”Ø Transform.X 和 Transform.Y + val1 = LottieTensor._format_value(params[LottieTensor.Index.Transform.X]) + val2 = LottieTensor._format_value(params[LottieTensor.Index.Transform.Y]) + lines.append(f'({cmd} {val1} {val2})') + + + + elif cmd_idx == LottieTensor.CMD_ELLIPSE_SIZE: + # Output rect_size with two values + # ę£€ęŸ„ę˜Æå¦ę˜ÆåŠØē”» + animated_val = params[LottieTensor.Index.Transform.ANIMATED] + if animated_val > -2000 and animated_val > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "size" # ē”®äæč®¾ē½®äøŠäø‹ę–‡ + else: + # é™ę€å€¼ - ę£€ęŸ„ X 和 Y ę˜Æå¦äøŗęœ‰ę•ˆå€¼ + x_val = params[LottieTensor.Index.Transform.X] + y_val = params[LottieTensor.Index.Transform.Y] + # å¦‚ęžœ X 和 Y éƒ½ę˜Æé»˜č®¤å€¼ 0 äø” ANIMATED ęœŖč®¾ē½®ļ¼ŒåÆčƒ½ę˜Æę•°ę®äø¢å¤± + val1 = LottieTensor._format_value(x_val if x_val > -2000 else 0) + val2 = LottieTensor._format_value(y_val if y_val > -2000 else 0) + lines.append(f'({cmd} {val1} {val2})') + + + elif cmd_idx == LottieTensor.CMD_RECT_ROUNDED: + if params[LottieTensor.Index.SingleValue.ANIMATED] > 0.5: + lines.append(f'({cmd} animated=true)') + current_context = "rect_rounded" # Set context for keyframes + else: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_RECT_ROUNDED_END: + lines.append(f'({cmd})') + current_context = None + + elif cmd_idx == LottieTensor.CMD_SKEW: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + elif cmd_idx == LottieTensor.CMD_SKEW_AXIS: + if params[LottieTensor.Index.SingleValue.VALUE] > -2000: + val = LottieTensor._format_value(params[LottieTensor.Index.SingleValue.VALUE]) + lines.append(f'({cmd} {val})') + + else: + # Default case for any unhandled commands + lines.append(f"({cmd})") + + return '\n'.join(lines) + + # Keep other methods unchanged + def to_tensor(self) -> torch.Tensor: + """Convert LottieTensor to a single tensor""" + return torch.cat([self.commands.float(), self.params], dim=1) + + @staticmethod + def from_tensor(tensor: torch.Tensor) -> 'LottieTensor': + """Create LottieTensor from tensor""" + commands = tensor[:, 0:1].long() + params = tensor[:, 1:1+LottieTensor.PARAM_DIM].float() + + return LottieTensor(commands, params, PAD_VAL=-2001) + + + def pad(self, seq_len: int): + """Pad sequence to specified length""" + pad_len = max(seq_len - len(self.commands), 0) + if pad_len > 0: + pad_commands = torch.ones((pad_len, 1)) * LottieTensor.CMD_PAD + pad_params = torch.ones((pad_len, self.PARAM_DIM)) * self.PAD_VAL + + self.commands = torch.cat([self.commands, pad_commands.long()]) + self.params = torch.cat([self.params, pad_params]) + return self + + @staticmethod + def _clamp_value(value: float, min_val: float = -2000, max_val: float = 2000) -> float: + """Clamp a value between min and max bounds""" + return max(min_val, min(max_val, value)) + + @staticmethod + def _index_clamp_value(value: float, min_val: float = -100, max_val: float = 100) -> float: + """Clamp a value between min and max bounds""" + return max(min_val, min(max_val, value)) + + + @classmethod + def init_tokenizer(cls, model_path=None): + """Initialize tokenizer once for the class - ę”ÆęŒå¤šč·Æå¾„fallback""" + if cls.tokenizer is None: + from transformers import AutoTokenizer + if model_path is None: + # å°čÆ•å¤šäøŖåÆčƒ½ēš„č·Æå¾„ + possible_paths = [ + '/mnt/jfs-test/Qwen2.5-VL-3B-Instruct', + '/data/models/Qwen2.5-VL-3B-Instruct', + 'Qwen/Qwen2.5-VL-3B-Instruct', # HuggingFace Hub + ] + for path in possible_paths: + try: + cls.tokenizer = AutoTokenizer.from_pretrained(path) + # åŖåœØäø»čæ›ēØ‹ę‰“å°äø€ę¬” + import os + if os.environ.get('RANK', '0') == '0': + print(f"Tokenizer loaded successfully from: {path}") + return + except Exception as e: + continue + raise ValueError(f"Failed to load tokenizer from any known path: {possible_paths}") + else: + cls.tokenizer = AutoTokenizer.from_pretrained(model_path) + + @classmethod + def get_tokenizer(cls): + if cls.tokenizer is None: + from transformers import AutoTokenizer + cls.tokenizer = AutoTokenizer.from_pretrained('/mnt/jfs-test/Qwen2.5-VL-3B-Instruct') + return cls.tokenizer + + + @staticmethod + def get_param_offset(cmd_idx: int, param_idx: int) -> int: + """ + Get the offset for a parameter based on its command and parameter index. + Returns the offset to add to the parameter value. + """ + cache_key = (cmd_idx, param_idx) + if cache_key in LottieTensor._OFFSET_CACHE: + return LottieTensor._OFFSET_CACHE[cache_key] + + + TIME_OFFSET = 155000 + SPACE_OFFSET = 159100 + AMPLITUDE_OFFSET = 161200 + ANCHOR_OFFSET = 161300 + ANIMATED_OFFSET = 165400 + H_FLAG_OFFSET = 165402 + OFFSET_VAL_OFFSET = 165404 + CA_OFFSET = 165406 + JUSTIFY_OFFSET = 165409 + TEXT_TRACKING_OFFSET = 165416 + HAS_STROKE_COLOR_OFFSET = 166017 + IX_OFFSET = 166019 + BM_OFFSET = 167020 + CLOSED_OFFSET = 167041 + DIRECTION_OFFSET = 167043 + STAR_TYPE_OFFSET = 167049 + MULTIPLE_OFFSET = 167055 + COMPOSITE_OFFSET = 167061 + SKEW_OFFSET = 167067 + SKEW_AXIS_OFFSET = 167118 + SCALE_OFFSET = 167169 + ROTATION_OFFSET = 170170 + EASE_OFFSET = 171611 + SMOOTH_OFFSET = 171812 + TRACKING_OFFSET = 171913 + INDEX_OFFSET = 172014 + DDD_OFFSET = 173015 + HD_OFFSET = 173017 + CP_OFFSET = 173019 + HAS_MASK_OFFSET = 173070 + AO_OFFSET = 173072 + TT_OFFSET = 173074 + TP_OFFSET = 173080 + TD_OFFSET = 173181 + CT_OFFSET = 173184 + NUMBER_OFFSET = 173186 + DIM_OFFSET = 173687 + HAS_C_A_OFFSET = 173698 + HAS_C_IX_OFFSET = 173700 + HAS_O_A_OFFSET = 173702 + HAS_O_IX_OFFSET = 173704 + FILL_RULE_OFFSET = 173706 + TYPE_OFFSET = 173711 + TEXT_RANGE_UNITS_OFFSET = 173752 + INV_OFFSET = 173763 + MODE_OFFSET = 173765 + TEXT_SHAPE_TYPE_OFFSET = 173776 + TEXT_RANDOM_OFFSET = 173787 + COLOR_POINTS_OFFSET = 173789 + ROUND_OFFSET = 173840 + RADIUS_OFFSET = 174941 + FREQUENCY_OFFSET = 175242 + SPEED_OFFSET = 175393 + FONT_OFFSET = 177394 + COLOR_OFFSET = 179495 + LINE_CAP_OFFSET = 179752 + LINE_JOIN_OFFSET = 179757 + MITER_LIMIT_OFFSET = 179760 + EFFECT_OFFSET = 179861 + OPACITY_OFFSET = 181112 + WIDTH_VALUE_OFFSET = 181300 + + NO_OFFSET = 0 + + # Time dictionary parameters - all time-related values + time_params = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.IP, + LottieTensor.Index.Animation.OP + ], + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.IN_POINT, + LottieTensor.Index.Layer.OUT_POINT, + LottieTensor.Index.Layer.START_TIME + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.IN_POINT, + LottieTensor.Index.NullLayer.OUT_POINT, + LottieTensor.Index.NullLayer.START_TIME + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.IN_POINT, + LottieTensor.Index.PrecompLayer.OUT_POINT, + LottieTensor.Index.PrecompLayer.START_TIME + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.IN_POINT, + LottieTensor.Index.TextLayer.OUT_POINT, + LottieTensor.Index.TextLayer.START_TIME + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.IN_POINT, + LottieTensor.Index.SolidLayer.OUT_POINT, + LottieTensor.Index.SolidLayer.START_TIME + ], + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.T + ], + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.T + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.T + ], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.T + ], + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.T + ], + LottieTensor.CMD_MASK_PT_KEYFRAME: [ + LottieTensor.Index.MaskPtKeyframe.T + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.T + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.T + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.T + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.T + ], + } + + # Space dictionary parameters - all spatial/positional values + space_params = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.W, + LottieTensor.Index.Animation.H + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.W, + LottieTensor.Index.PrecompLayer.H + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.WIDTH, + LottieTensor.Index.SolidLayer.HEIGHT + ], + LottieTensor.CMD_DIMENSIONS: [ + LottieTensor.Index.Dimensions.WIDTH, + LottieTensor.Index.Dimensions.HEIGHT + ], + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.STROKE_WIDTH, + LottieTensor.Index.TextKeyframe.WRAP_POSITION_X, + LottieTensor.Index.TextKeyframe.WRAP_POSITION_Y, + LottieTensor.Index.TextKeyframe.WRAP_SIZE_X, + LottieTensor.Index.TextKeyframe.WRAP_SIZE_Y + ], + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.S1, LottieTensor.Index.Keyframe.S2, LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.E1, LottieTensor.Index.Keyframe.E2, LottieTensor.Index.Keyframe.E3, + LottieTensor.Index.Keyframe.TO1, LottieTensor.Index.Keyframe.TO2, LottieTensor.Index.Keyframe.TO3, + LottieTensor.Index.Keyframe.TI1, LottieTensor.Index.Keyframe.TI2, LottieTensor.Index.Keyframe.TI3 + ], + #LottieTensor.CMD_WIDTH_KEYFRAME: [ + # LottieTensor.Index.WidthKeyframe.S, + #], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.S1, + ], + LottieTensor.CMD_POSITION: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y, LottieTensor.Index.Transform.Z, + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_POSITION_X: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Y: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Z: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POINT: [ + LottieTensor.Index.Point.X, LottieTensor.Index.Point.Y, + LottieTensor.Index.Point.IN_X, LottieTensor.Index.Point.IN_Y, + LottieTensor.Index.Point.OUT_X, LottieTensor.Index.Point.OUT_Y + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.POSITION_X, LottieTensor.Index.TransformShape.POSITION_Y, + ], + LottieTensor.CMD_SIZE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_RECT_SIZE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_ELLIPSE_SIZE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_START_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_END_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_POINTS_STAR: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_START: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_END: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OFFSET: [ + LottieTensor.Index.SingleValue.VALUE + ], + #LottieTensor.CMD_WIDTH: [ + # LottieTensor.Index.SingleValue.VALUE + #], + #LottieTensor.CMD_DASH: [ + # LottieTensor.Index.Dash.LENGTH + #], + #LottieTensor.CMD_DASH_OFFSET: [ + # LottieTensor.Index.DashOffset.O + #], + #LottieTensor.CMD_DASH_KEYFRAME: [ + # LottieTensor.Index.DashKeyframe.S, + # ], + LottieTensor.CMD_TR_POSITION: [ + LottieTensor.Index.TrPosition.X, LottieTensor.Index.TrPosition.Y + ], + LottieTensor.CMD_REPEATER_OFFSET: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.S, + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.S, + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.S, + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.K_X, + LottieTensor.Index.PositionAnimators.K_Y, + LottieTensor.Index.PositionAnimators.K_Z + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.K + ], + LottieTensor.CMD_MASK_PT_K_I: list(range(20)), + LottieTensor.CMD_MASK_PT_K_O: list(range(20)), + LottieTensor.CMD_MASK_PT_K_V: list(range(20)), + LottieTensor.CMD_MASK_PT_KF_I: [ + LottieTensor.Index.MaskPtKfI.X, LottieTensor.Index.MaskPtKfI.Y + ], + LottieTensor.CMD_MASK_PT_KF_O: [ + LottieTensor.Index.MaskPtKfO.X, LottieTensor.Index.MaskPtKfO.Y + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE_I: list(range(20)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_O: list(range(20)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_V: list(range(20)), + LottieTensor.CMD_VALUE: [ + LottieTensor.Index.Value.VALUE + ], + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.ALIGNMENT_K1, + LottieTensor.Index.MoreOptions.ALIGNMENT_K2 + ], + LottieTensor.CMD_ALIGNMENT_K: [ + LottieTensor.Index.AlignmentK.VALUE1, + LottieTensor.Index.AlignmentK.VALUE2 + ], + LottieTensor.CMD_CHAR: [ + LottieTensor.Index.Char.W + ], + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.START_POINT_X, + LottieTensor.Index.GradientFill.START_POINT_Y, + LottieTensor.Index.GradientFill.END_POINT_X, + LottieTensor.Index.GradientFill.END_POINT_Y, + LottieTensor.Index.GradientFill.HIGHLIGHT_LENGTH, + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.WIDTH, + LottieTensor.Index.GradientStroke.START_POINT_X, + LottieTensor.Index.GradientStroke.START_POINT_Y, + LottieTensor.Index.GradientStroke.END_POINT_X, + LottieTensor.Index.GradientStroke.END_POINT_Y, + LottieTensor.Index.GradientStroke.HIGHLIGHT_LENGTH, + ], + LottieTensor.CMD_HIGHLIGHT_LENGTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + width_value_params = { + LottieTensor.CMD_WIDTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.LENGTH + ], + LottieTensor.CMD_DASH_OFFSET: [ + LottieTensor.Index.DashOffset.O + ], + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.S, + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.S, + ], + } + + amplitude_params = { + LottieTensor.CMD_AMPLITUDE: [ + LottieTensor.Index.Amplitude.VALUE + ], + } + + anchor_params = { + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.ANCHOR_X, LottieTensor.Index.TransformShape.ANCHOR_Y, + ], + LottieTensor.CMD_TR_ANCHOR: [ + LottieTensor.Index.TrAnchor.X, LottieTensor.Index.TrAnchor.Y + ], + LottieTensor.CMD_ANCHOR: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y, LottieTensor.Index.Transform.Z + ], + } + + animated_params = { + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.ALIGNMENT_A, + ], + LottieTensor.CMD_ELLIPSE_SIZE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_RECT_SIZE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION_X: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION_Y: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_POSITION_Z: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_SCALE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_ROTATION: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_OPACITY: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_ANCHOR: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_SIZE: [ + LottieTensor.Index.Transform.ANIMATED + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.ANIMATED, + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.COLOR_ANIMATED, + LottieTensor.Index.Fill.OPACITY_ANIMATED, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.WIDTH_ANIMATED, + LottieTensor.Index.Stroke.COLOR_ANIMATED + ], + LottieTensor.CMD_RECT_ROUNDED: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_START: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_END: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_OFFSET: [ + LottieTensor.Index.SingleValue.ANIMATED + ], + LottieTensor.CMD_MASK_PT: [ + LottieTensor.Index.MaskPt.A, + ], + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.A, + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.A, + ], + LottieTensor.CMD_TM: [ + LottieTensor.Index.Tm.A + ], + LottieTensor.CMD_RANGE_START: [ + LottieTensor.Index.RangeStart.A + ], + LottieTensor.CMD_RANGE_END: [ + LottieTensor.Index.RangeEnd.A + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.A, + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.A, + ], + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.A, + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.A, + ], + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.A, + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.A, + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.A, + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.A, + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.A, + ], + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.A, + ], + LottieTensor.CMD_ALIGNMENT: [ + LottieTensor.Index.Alignment.A + ], + } + + h_flag_params = { + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.H_FLAG + ], + } + + offset_val_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.OFFSET, + ], + } + + ca_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.CA, + ], + LottieTensor.CMD_CA: [ + LottieTensor.Index.Ca.VALUE + ], + } + + justify_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.JUSTIFY, + ], + LottieTensor.CMD_JUSTIFY: [ + LottieTensor.Index.Justify.VALUE + ], + } + + text_tracking_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.TRACKING, + ], + LottieTensor.CMD_TRACKING: [ + LottieTensor.Index.Tracking.VALUE + ], + } + + has_stroke_color_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.HAS_STROKE_COLOR, + ], + } + + ix_params = { + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.IX, + LottieTensor.Index.Path.KS_IX, + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.IX, + LottieTensor.Index.Group.CIX, + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.C_IX, + LottieTensor.Index.Fill.O_IX, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.C_IX, + ], + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.IX, + ], + LottieTensor.CMD_ROUNDED: [ + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_TRIM: [ + LottieTensor.Index.Trim.IX + ], + LottieTensor.CMD_REPEATER: [ + LottieTensor.Index.Repeater.IX + ], + LottieTensor.CMD_COPIES: [ + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_REPEATER_OFFSET: [ + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_TR_P_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_A_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_S_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_R_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_SO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_EO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.ML2_IX, + ], + LottieTensor.CMD_MASK_PT: [ + LottieTensor.Index.MaskPt.IX, + ], + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.IX, + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.IX, + ], + LottieTensor.CMD_ZIG_ZAG: [ + LottieTensor.Index.ZigZag.IX + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.IX + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.IX + ], + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.IX + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.IX + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.IX + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.IX + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.IX + ], + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.IX + ], + LottieTensor.CMD_ALIGNMENT_IX: [ + LottieTensor.Index.AlignmentIx.VALUE + ], + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.ALIGNMENT_IX + ], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.V_IX + ], + LottieTensor.CMD_DASH_ANIMATED: [ + LottieTensor.Index.DashAnimated.V_IX, + ], + } + + bm_params = { + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.BM, + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.BM, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.BM, + ], + } + + closed_params = { + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.CLOSED, + ], + LottieTensor.CMD_BEZIER: [ + LottieTensor.Index.Bezier.CLOSED + ], + LottieTensor.CMD_MASK_PT_K_C: [ + LottieTensor.Index.MaskPtK.C + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE: [ + LottieTensor.Index.MaskPtKfShape.C, + ], + } + + direction_params = { + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.D, + ], + LottieTensor.CMD_STAR: [ + LottieTensor.Index.Star.D, + ], + } + + star_type_params = { + LottieTensor.CMD_STAR: [ + LottieTensor.Index.Star.SY + ], + } + + multiple_params = { + LottieTensor.CMD_MULTIPLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + composite_params = { + LottieTensor.CMD_COMPOSITE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + skew_params = { + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.SKEW, + ], + LottieTensor.CMD_SKEW: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + skew_axis_params = { + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.SKEW_AXIS, + ], + LottieTensor.CMD_SKEW_AXIS: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + scale_params = { + LottieTensor.CMD_SCALE: [ + LottieTensor.Index.Transform.X, LottieTensor.Index.Transform.Y, LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.SCALE_X, LottieTensor.Index.TransformShape.SCALE_Y, + ], + LottieTensor.CMD_TR_SCALE: [ + LottieTensor.Index.TwoValues.VALUE1, LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.K_X, + LottieTensor.Index.ScaleAnimators.K_Y, + LottieTensor.Index.ScaleAnimators.K_Z + ], + } + + rotation_params = { + LottieTensor.CMD_ROTATION: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.ROTATION + ], + LottieTensor.CMD_STAR_ROTATION: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_ROTATION: [ + LottieTensor.Index.TrRotation.VALUE + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.K + ], + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.HIGHLIGHT_ANGLE, + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.HIGHLIGHT_ANGLE + ], + LottieTensor.CMD_HIGHLIGHT_ANGLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + ease_params = { + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.K + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.K + ], + } + + smooth_params = { + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.K + ], + } + + tracking_params = { + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.K + ], + } + + index_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.INDEX, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.INDEX, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.INDEX, + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.INDEX, + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.INDEX, + ], + LottieTensor.CMD_PARENT: [ + LottieTensor.Index.Parent.PARENT_INDEX + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.IND, + ], + LottieTensor.CMD_COLOR: [ + LottieTensor.Index.Color.INDEX + ], + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.INDEX, + ], + LottieTensor.CMD_MASK_PT_KEYFRAME: [ + LottieTensor.Index.MaskPtKeyframe.INDEX + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE: [ + LottieTensor.Index.MaskPtKfShape.INDEX, + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.INDEX, + ], + LottieTensor.CMD_LAYER_EFFECT: [ + LottieTensor.Index.LayerEffect.INDEX, + ], + LottieTensor.CMD_DROPDOWN: [ + LottieTensor.Index.Dropdown.INDEX, + ], + LottieTensor.CMD_NO_VALUE: [ + LottieTensor.Index.NO_VALUE.INDEX, + ], + LottieTensor.CMD_IGNORED: [ + LottieTensor.Index.Ignored.INDEX, + ], + LottieTensor.CMD_SLIDER: [ + LottieTensor.Index.Slider.INDEX, + ], + } + + ddd_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.DDD, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.DDD, + ], + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.DDD + ], + } + + hd_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.HD, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.HD, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.HD, + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.HD, + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.HD, + ], + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.HD, + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.HD + ], + } + + cp_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.CP, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.CP, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.CP, + ], + } + + has_mask_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.HAS_MASK, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.HAS_MASK, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.HAS_MASK, + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.HAS_MASK, + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.HAS_MASK + ], + } + + ao_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.AO, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.AO, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.AO, + ], + } + + tt_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.TT, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.TT, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.TT, + ], + } + + tp_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.TP, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.TP, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.TP, + ], + } + + td_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.TD, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.TD, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.TD, + ], + } + + ct_params = { + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.CT, + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.CT, + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.CT, + ], + } + + number_params = { + LottieTensor.CMD_COPIES: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.K + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.K + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.NP + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.NP, + ], + } + + dim_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.COLOR_DIM, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.COLOR_DIM, + ], + } + + has_c_a_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_C_A, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.HAS_C_A, + ], + } + + has_c_ix_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_C_IX, + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.HAS_C_IX, + ], + } + + has_o_a_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_O_A, + ], + } + + has_o_ix_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.HAS_O_IX, + ], + } + + fill_rule_params = { + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.FILL_RULE, + ], + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.FILL_RULE, + ], + LottieTensor.CMD_FILL_RULE: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + type_params = { + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.GRADIENT_TYPE, + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.GRADIENT_TYPE, + ], + LottieTensor.CMD_GRADIENT_TYPE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_POINT_TYPE: [ + LottieTensor.Index.PointType.VALUE + ], + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.T, + ], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.TYPE, + ], + LottieTensor.CMD_DASH_ANIMATED: [ + LottieTensor.Index.DashAnimated.TYPE, + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.TYPE, + ], + } + + text_range_units = { + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.R, + ], + } + + inv_params = { + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.INV, + ], + } + + mode_params = { + LottieTensor.CMD_MERGE_MODE: [ + LottieTensor.Index.MergeMode.MODE + ], + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.MODE, + ], + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.B, + ], + } + + text_shape_type = { + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.SH, + ], + } + + text_random = { + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.RN + ], + } + + color_points_params = { + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.COLOR_POINTS + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.COLOR_POINTS + ], + LottieTensor.CMD_COLOR_POINTS: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + round_params = { + LottieTensor.CMD_ROUNDED: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_RECT_ROUNDED: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_INNER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + } + + radius_params = { + LottieTensor.CMD_INNER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_RADIUS: [ + LottieTensor.Index.Radius.VALUE + ], + } + + frequency_params = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.FR, + ], + LottieTensor.CMD_FREQUENCY: [ + LottieTensor.Index.Frequency.VALUE + ], + LottieTensor.CMD_ASSET: [ + LottieTensor.Index.Asset.FR + ], + } + + speed_params = { + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.I_X, LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, LottieTensor.Index.Keyframe.O_Y, + LottieTensor.Index.Keyframe.I_X2, LottieTensor.Index.Keyframe.I_Y2, + LottieTensor.Index.Keyframe.O_X2, LottieTensor.Index.Keyframe.O_Y2, + LottieTensor.Index.Keyframe.I_X3, LottieTensor.Index.Keyframe.I_Y3, + LottieTensor.Index.Keyframe.O_X3, LottieTensor.Index.Keyframe.O_Y3, + ], + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.I_X, LottieTensor.Index.WidthKeyframe.I_Y, + LottieTensor.Index.WidthKeyframe.O_X, LottieTensor.Index.WidthKeyframe.O_Y + ], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.I_X, LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.I_X, LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.I_X, LottieTensor.Index.DashKeyframe.I_Y, + LottieTensor.Index.DashKeyframe.O_X, LottieTensor.Index.DashKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.I_X, LottieTensor.Index.RangeStartKeyframe.I_Y, + LottieTensor.Index.RangeStartKeyframe.O_X, LottieTensor.Index.RangeStartKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.I_X, LottieTensor.Index.RangeEndKeyframe.I_Y, + LottieTensor.Index.RangeEndKeyframe.O_X, LottieTensor.Index.RangeEndKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.I_X, LottieTensor.Index.RangeOffsetKeyframe.I_Y, + LottieTensor.Index.RangeOffsetKeyframe.O_X, LottieTensor.Index.RangeOffsetKeyframe.O_Y + ], + } + + font_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.FONT_SIZE, + LottieTensor.Index.TextKeyframe.LINE_HEIGHT, + LottieTensor.Index.TextKeyframe.LETTER_SPACING + ], + LottieTensor.CMD_FONT_SIZE: [ + LottieTensor.Index.FontSize.SIZE + ], + LottieTensor.CMD_LINE_HEIGHT: [ + LottieTensor.Index.LineHeight.VALUE + ], + LottieTensor.CMD_LETTER_SPACING: [ + LottieTensor.Index.LetterSpacing.VALUE + ], + LottieTensor.CMD_FONT: [ + LottieTensor.Index.Font.ASCENT + ], + LottieTensor.CMD_CHAR: [ + LottieTensor.Index.Char.SIZE + ], + } + + color_params = { + LottieTensor.CMD_TEXT_KEYFRAME: [ + LottieTensor.Index.TextKeyframe.FILL_COLOR_R, + LottieTensor.Index.TextKeyframe.FILL_COLOR_G, + LottieTensor.Index.TextKeyframe.FILL_COLOR_B, + LottieTensor.Index.TextKeyframe.STROKE_COLOR_R, + LottieTensor.Index.TextKeyframe.STROKE_COLOR_G, + LottieTensor.Index.TextKeyframe.STROKE_COLOR_B + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.R, + LottieTensor.Index.Stroke.G, + LottieTensor.Index.Stroke.B, + LottieTensor.Index.Stroke.A + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.R, + LottieTensor.Index.Fill.G, + LottieTensor.Index.Fill.B, + ], + LottieTensor.CMD_FILL_COLOR: [ + LottieTensor.Index.FillColor.R, + LottieTensor.Index.FillColor.G, + LottieTensor.Index.FillColor.B + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.COLOR_R, + LottieTensor.Index.SolidLayer.COLOR_G, + LottieTensor.Index.SolidLayer.COLOR_B, + LottieTensor.Index.SolidLayer.COLOR_A + ], + LottieTensor.CMD_COLOR: [ + LottieTensor.Index.Color.R, + LottieTensor.Index.Color.G, + LottieTensor.Index.Color.B + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.S2, + LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.E1 + ], + LottieTensor.CMD_ORIGINAL_COLORS: list(range(LottieTensor.Index.OriginalColors.COUNT)), + LottieTensor.CMD_GRADIENT_FILL: list(range(LottieTensor.Index.GradientFill.ORIGINAL_COLOR_0, + LottieTensor.Index.GradientFill.ORIGINAL_COLOR_23 + 1)), + LottieTensor.CMD_GRADIENT_STROKE: list(range(LottieTensor.Index.GradientStroke.ORIGINAL_COLOR_0, + LottieTensor.Index.GradientStroke.ORIGINAL_COLOR_23 + 1)), + } + + line_cap_params = { + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.LC, + ], + LottieTensor.CMD_LINE_CAP: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.LINE_CAP, + ], + } + line_join_params = { + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.LJ, + ], + LottieTensor.CMD_LINE_JOIN: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.LINE_JOIN, + ], + } + + miter_limit_params = { + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.ML + ], + LottieTensor.CMD_MITER_LIMIT: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_ML2: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.MITER_LIMIT, + LottieTensor.Index.GradientStroke.ML2 + ], + } + + enabled_params = { + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.ENABLED + ], + } + + effect_params = { + LottieTensor.CMD_LAYER_EFFECT: [ + LottieTensor.Index.LayerEffect.VALUE + ], + LottieTensor.CMD_DROPDOWN: [ + LottieTensor.Index.Dropdown.VALUE + ], + LottieTensor.CMD_NO_VALUE: [ + LottieTensor.Index.NO_VALUE.VALUE + ], + LottieTensor.CMD_IGNORED: [ + LottieTensor.Index.Ignored.VALUE + ], + LottieTensor.CMD_SLIDER: [ + LottieTensor.Index.Slider.VALUE + ], + } + + opacity_params = { + LottieTensor.CMD_GRADIENT_FILL: [ + LottieTensor.Index.GradientFill.OPACITY + ], + LottieTensor.CMD_GRADIENT_STROKE: [ + LottieTensor.Index.GradientStroke.OPACITY + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.OPACITY + ], + LottieTensor.CMD_OPACITY: [ + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.OPACITY, + ], + LottieTensor.CMD_TR_START_OPACITY: [ + LottieTensor.Index.TrStartOpacity.VALUE + ], + LottieTensor.CMD_TR_END_OPACITY: [ + LottieTensor.Index.TrEndOpacity.VALUE + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.K + ], + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.K + ], + } + + # Tokenizer parameters (no offset) + tokenizer_params = { + LottieTensor.CMD_TEXT_KEYFRAME: list(range(LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START, + LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT + 1)), + LottieTensor.CMD_ASSET: list(range(LottieTensor.Index.Asset.ID_TOKEN_0, + LottieTensor.Index.Asset.ID_TOKEN_COUNT + 1)), + LottieTensor.CMD_REFERENCE_ID: list(range(LottieTensor.Index.ReferenceId.ID_TOKEN_0, + LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT + 1)), + LottieTensor.CMD_FONT: list(range(LottieTensor.Index.Font.FAMILY_TOKEN_0, + LottieTensor.Index.Font.STYLE_TOKEN_COUNT + 1)), + LottieTensor.CMD_CHAR: list(range(LottieTensor.Index.Char.CH_TOKEN_0, + LottieTensor.Index.Char.FAMILY_TOKEN_COUNT + 1)), + } + # 添加 width_value_params ēš„åˆ¤ę–­ļ¼ˆåœØå…¶ä»–åˆ¤ę–­ä¹‹å‰ļ¼‰ + if cmd_idx in width_value_params and param_idx in width_value_params[cmd_idx]: + return WIDTH_VALUE_OFFSET + + elif cmd_idx in tokenizer_params and param_idx in tokenizer_params[cmd_idx]: + return NO_OFFSET # No offset for tokenizer tokens + elif cmd_idx in time_params and param_idx in time_params[cmd_idx]: + return TIME_OFFSET + elif cmd_idx in space_params and param_idx in space_params[cmd_idx]: + return SPACE_OFFSET + elif cmd_idx in amplitude_params and param_idx in amplitude_params[cmd_idx]: + return AMPLITUDE_OFFSET + elif cmd_idx in anchor_params and param_idx in anchor_params[cmd_idx]: + return ANCHOR_OFFSET + elif cmd_idx in animated_params and param_idx in animated_params[cmd_idx]: + return ANIMATED_OFFSET + elif cmd_idx in h_flag_params and param_idx in h_flag_params[cmd_idx]: + return H_FLAG_OFFSET + elif cmd_idx in offset_val_params and param_idx in offset_val_params[cmd_idx]: + return OFFSET_VAL_OFFSET + elif cmd_idx in ca_params and param_idx in ca_params[cmd_idx]: + return CA_OFFSET + elif cmd_idx in justify_params and param_idx in justify_params[cmd_idx]: + return JUSTIFY_OFFSET + elif cmd_idx in text_tracking_params and param_idx in text_tracking_params[cmd_idx]: + return TEXT_TRACKING_OFFSET + elif cmd_idx in has_stroke_color_params and param_idx in has_stroke_color_params[cmd_idx]: + return HAS_STROKE_COLOR_OFFSET + elif cmd_idx in ix_params and param_idx in ix_params[cmd_idx]: + return IX_OFFSET + elif cmd_idx in bm_params and param_idx in bm_params[cmd_idx]: + return BM_OFFSET + elif cmd_idx in closed_params and param_idx in closed_params[cmd_idx]: + return CLOSED_OFFSET + elif cmd_idx in direction_params and param_idx in direction_params[cmd_idx]: + return DIRECTION_OFFSET + elif cmd_idx in star_type_params and param_idx in star_type_params[cmd_idx]: + return STAR_TYPE_OFFSET + elif cmd_idx in multiple_params and param_idx in multiple_params[cmd_idx]: + return MULTIPLE_OFFSET + elif cmd_idx in composite_params and param_idx in composite_params[cmd_idx]: + return COMPOSITE_OFFSET + elif cmd_idx in skew_params and param_idx in skew_params[cmd_idx]: + return SKEW_OFFSET + elif cmd_idx in skew_axis_params and param_idx in skew_axis_params[cmd_idx]: + return SKEW_AXIS_OFFSET + elif cmd_idx in scale_params and param_idx in scale_params[cmd_idx]: + return SCALE_OFFSET + elif cmd_idx in rotation_params and param_idx in rotation_params[cmd_idx]: + return ROTATION_OFFSET + elif cmd_idx in ease_params and param_idx in ease_params[cmd_idx]: + return EASE_OFFSET + elif cmd_idx in smooth_params and param_idx in smooth_params[cmd_idx]: + return SMOOTH_OFFSET + elif cmd_idx in tracking_params and param_idx in tracking_params[cmd_idx]: + return TRACKING_OFFSET + elif cmd_idx in index_params and param_idx in index_params[cmd_idx]: + return INDEX_OFFSET + elif cmd_idx in ddd_params and param_idx in ddd_params[cmd_idx]: + return DDD_OFFSET + elif cmd_idx in hd_params and param_idx in hd_params[cmd_idx]: + return HD_OFFSET + elif cmd_idx in cp_params and param_idx in cp_params[cmd_idx]: + return CP_OFFSET + elif cmd_idx in has_mask_params and param_idx in has_mask_params[cmd_idx]: + return HAS_MASK_OFFSET + elif cmd_idx in ao_params and param_idx in ao_params[cmd_idx]: + return AO_OFFSET + elif cmd_idx in tt_params and param_idx in tt_params[cmd_idx]: + return TT_OFFSET + elif cmd_idx in tp_params and param_idx in tp_params[cmd_idx]: + return TP_OFFSET + elif cmd_idx in td_params and param_idx in td_params[cmd_idx]: + return TD_OFFSET + elif cmd_idx in ct_params and param_idx in ct_params[cmd_idx]: + return CT_OFFSET + elif cmd_idx in number_params and param_idx in number_params[cmd_idx]: + return NUMBER_OFFSET + elif cmd_idx in dim_params and param_idx in dim_params[cmd_idx]: + return DIM_OFFSET + elif cmd_idx in has_c_a_params and param_idx in has_c_a_params[cmd_idx]: + return HAS_C_A_OFFSET + elif cmd_idx in has_c_ix_params and param_idx in has_c_ix_params[cmd_idx]: + return HAS_C_IX_OFFSET + elif cmd_idx in has_o_a_params and param_idx in has_o_a_params[cmd_idx]: + return HAS_O_A_OFFSET + elif cmd_idx in has_o_ix_params and param_idx in has_o_ix_params[cmd_idx]: + return HAS_O_IX_OFFSET + elif cmd_idx in fill_rule_params and param_idx in fill_rule_params[cmd_idx]: + return FILL_RULE_OFFSET + elif cmd_idx in type_params and param_idx in type_params[cmd_idx]: + return TYPE_OFFSET + elif cmd_idx in text_range_units and param_idx in text_range_units[cmd_idx]: + return TEXT_RANGE_UNITS_OFFSET + elif cmd_idx in inv_params and param_idx in inv_params[cmd_idx]: + return INV_OFFSET + elif cmd_idx in mode_params and param_idx in mode_params[cmd_idx]: + return MODE_OFFSET + elif cmd_idx in text_shape_type and param_idx in text_shape_type[cmd_idx]: + return TEXT_SHAPE_TYPE_OFFSET + elif cmd_idx in text_random and param_idx in text_random[cmd_idx]: + return TEXT_RANDOM_OFFSET + elif cmd_idx in color_points_params and param_idx in color_points_params[cmd_idx]: + return COLOR_POINTS_OFFSET + elif cmd_idx in round_params and param_idx in round_params[cmd_idx]: + return ROUND_OFFSET + elif cmd_idx in radius_params and param_idx in radius_params[cmd_idx]: + return RADIUS_OFFSET + elif cmd_idx in frequency_params and param_idx in frequency_params[cmd_idx]: + return FREQUENCY_OFFSET + elif cmd_idx in speed_params and param_idx in speed_params[cmd_idx]: + return SPEED_OFFSET + elif cmd_idx in font_params and param_idx in font_params[cmd_idx]: + return FONT_OFFSET + elif cmd_idx in color_params and param_idx in color_params[cmd_idx]: + return COLOR_OFFSET + elif cmd_idx in line_cap_params and param_idx in line_cap_params[cmd_idx]: + return LINE_CAP_OFFSET + elif cmd_idx in line_join_params and param_idx in line_join_params[cmd_idx]: + return LINE_JOIN_OFFSET + elif cmd_idx in miter_limit_params and param_idx in miter_limit_params[cmd_idx]: + return MITER_LIMIT_OFFSET + elif cmd_idx in effect_params and param_idx in effect_params[cmd_idx]: + return EFFECT_OFFSET + elif cmd_idx in opacity_params and param_idx in opacity_params[cmd_idx]: + return OPACITY_OFFSET + + else: + return 0 # Default to no offset if not found + + LottieTensor._OFFSET_CACHE[cache_key] = offset + return offset + + + @staticmethod + def get_command_param_indices(cmd_idx: int) -> List[int]: + """ + Get the list of parameter indices for a command in their fixed order. + Returns empty list for commands without parameters. + """ + param_orders = { + LottieTensor.CMD_ANIMATION: [ + LottieTensor.Index.Animation.FR, + LottieTensor.Index.Animation.IP, + LottieTensor.Index.Animation.OP, + LottieTensor.Index.Animation.W, + LottieTensor.Index.Animation.H, + LottieTensor.Index.Animation.DDD + ], + LottieTensor.CMD_LAYER: [ + LottieTensor.Index.Layer.INDEX, + LottieTensor.Index.Layer.IN_POINT, + LottieTensor.Index.Layer.OUT_POINT, + LottieTensor.Index.Layer.START_TIME, + LottieTensor.Index.Layer.DDD, + LottieTensor.Index.Layer.HD, + LottieTensor.Index.Layer.CP, + LottieTensor.Index.Layer.HAS_MASK, + LottieTensor.Index.Layer.AO, + LottieTensor.Index.Layer.TT, + LottieTensor.Index.Layer.TP, + LottieTensor.Index.Layer.TD, + LottieTensor.Index.Layer.CT + ], + LottieTensor.CMD_NULL_LAYER: [ + LottieTensor.Index.NullLayer.INDEX, + LottieTensor.Index.NullLayer.IN_POINT, + LottieTensor.Index.NullLayer.OUT_POINT, + LottieTensor.Index.NullLayer.START_TIME, + LottieTensor.Index.NullLayer.CT, + LottieTensor.Index.NullLayer.HD, + LottieTensor.Index.NullLayer.HAS_MASK, + LottieTensor.Index.NullLayer.AO, + LottieTensor.Index.NullLayer.TT, + LottieTensor.Index.NullLayer.TP, + LottieTensor.Index.NullLayer.TD, + LottieTensor.Index.NullLayer.CP + ], + LottieTensor.CMD_PRECOMP_LAYER: [ + LottieTensor.Index.PrecompLayer.INDEX, + LottieTensor.Index.PrecompLayer.IN_POINT, + LottieTensor.Index.PrecompLayer.OUT_POINT, + LottieTensor.Index.PrecompLayer.START_TIME, + LottieTensor.Index.PrecompLayer.W, + LottieTensor.Index.PrecompLayer.H, + LottieTensor.Index.PrecompLayer.CT, + LottieTensor.Index.PrecompLayer.HAS_MASK, + LottieTensor.Index.PrecompLayer.AO, + LottieTensor.Index.PrecompLayer.TT, + LottieTensor.Index.PrecompLayer.TP, + LottieTensor.Index.PrecompLayer.TD, + LottieTensor.Index.PrecompLayer.DDD, + LottieTensor.Index.PrecompLayer.HD, + LottieTensor.Index.PrecompLayer.CP + ], + LottieTensor.CMD_TEXT_LAYER: [ + LottieTensor.Index.TextLayer.INDEX, + LottieTensor.Index.TextLayer.IN_POINT, + LottieTensor.Index.TextLayer.OUT_POINT, + LottieTensor.Index.TextLayer.START_TIME, + LottieTensor.Index.TextLayer.HAS_MASK + ], + LottieTensor.CMD_SOLID_LAYER: [ + LottieTensor.Index.SolidLayer.INDEX, + LottieTensor.Index.SolidLayer.IN_POINT, + LottieTensor.Index.SolidLayer.OUT_POINT, + LottieTensor.Index.SolidLayer.START_TIME, + LottieTensor.Index.SolidLayer.WIDTH, + LottieTensor.Index.SolidLayer.HEIGHT, + LottieTensor.Index.SolidLayer.HAS_MASK, + LottieTensor.Index.SolidLayer.COLOR_R, + LottieTensor.Index.SolidLayer.COLOR_G, + LottieTensor.Index.SolidLayer.COLOR_B, + LottieTensor.Index.SolidLayer.COLOR_A + ], + LottieTensor.CMD_TRANSFORM: [], + LottieTensor.CMD_POSITION: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, + LottieTensor.Index.Transform.Y, + LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_POSITION_X: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Y: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_POSITION_Z: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_SCALE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, + LottieTensor.Index.Transform.Y, + LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_ROTATION: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_OPACITY: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X + ], + LottieTensor.CMD_ANCHOR: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, + LottieTensor.Index.Transform.Y, + LottieTensor.Index.Transform.Z + ], + LottieTensor.CMD_KEYFRAME: [ + LottieTensor.Index.Keyframe.T, + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.S2, + LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.I_X, + LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, + LottieTensor.Index.Keyframe.O_Y, + LottieTensor.Index.Keyframe.TO1, + LottieTensor.Index.Keyframe.TO2, + LottieTensor.Index.Keyframe.TO3, + LottieTensor.Index.Keyframe.TI1, + LottieTensor.Index.Keyframe.TI2, + LottieTensor.Index.Keyframe.TI3, + LottieTensor.Index.Keyframe.I_X2, + LottieTensor.Index.Keyframe.I_X3, + LottieTensor.Index.Keyframe.I_Y2, + LottieTensor.Index.Keyframe.I_Y3, + LottieTensor.Index.Keyframe.O_X2, + LottieTensor.Index.Keyframe.O_X3, + LottieTensor.Index.Keyframe.O_Y2, + LottieTensor.Index.Keyframe.O_Y3, + LottieTensor.Index.Keyframe.H_FLAG, + LottieTensor.Index.Keyframe.E1, + LottieTensor.Index.Keyframe.E2, + LottieTensor.Index.Keyframe.E3 + ], + LottieTensor.CMD_GROUP: [ + LottieTensor.Index.Group.IX, + LottieTensor.Index.Group.CIX, + LottieTensor.Index.Group.BM, + LottieTensor.Index.Group.HD, + LottieTensor.Index.Group.NP + ], + LottieTensor.CMD_PATH: [ + LottieTensor.Index.Path.IX, + LottieTensor.Index.Path.IND, + LottieTensor.Index.Path.KS_IX, + LottieTensor.Index.Path.CLOSED, + LottieTensor.Index.Path.HD, + LottieTensor.Index.Path.ANIMATED + ], + LottieTensor.CMD_POINT: [ + LottieTensor.Index.Point.X, + LottieTensor.Index.Point.Y, + LottieTensor.Index.Point.IN_X, + LottieTensor.Index.Point.IN_Y, + LottieTensor.Index.Point.OUT_X, + LottieTensor.Index.Point.OUT_Y + ], + LottieTensor.CMD_FILL: [ + LottieTensor.Index.Fill.R, + LottieTensor.Index.Fill.G, + LottieTensor.Index.Fill.B, + LottieTensor.Index.Fill.COLOR_DIM, + LottieTensor.Index.Fill.HAS_C_A, + LottieTensor.Index.Fill.HAS_C_IX, + LottieTensor.Index.Fill.C_IX, + LottieTensor.Index.Fill.BM, + LottieTensor.Index.Fill.FILL_RULE, + LottieTensor.Index.Fill.OPACITY, + LottieTensor.Index.Fill.COLOR_ANIMATED, + LottieTensor.Index.Fill.OPACITY_ANIMATED, + LottieTensor.Index.Fill.HAS_O_A, + LottieTensor.Index.Fill.HAS_O_IX, + LottieTensor.Index.Fill.O_IX + ], + LottieTensor.CMD_STROKE: [ + LottieTensor.Index.Stroke.R, + LottieTensor.Index.Stroke.G, + LottieTensor.Index.Stroke.B, + LottieTensor.Index.Stroke.COLOR_DIM, + LottieTensor.Index.Stroke.HAS_C_A, + LottieTensor.Index.Stroke.HAS_C_IX, + LottieTensor.Index.Stroke.C_IX, + LottieTensor.Index.Stroke.BM, + LottieTensor.Index.Stroke.LC, + LottieTensor.Index.Stroke.LJ, + LottieTensor.Index.Stroke.ML, + LottieTensor.Index.Stroke.WIDTH_ANIMATED, + LottieTensor.Index.Stroke.COLOR_ANIMATED, + LottieTensor.Index.Stroke.A + ], + LottieTensor.CMD_TRANSFORM_SHAPE: [ + LottieTensor.Index.TransformShape.POSITION_X, + LottieTensor.Index.TransformShape.POSITION_Y, + LottieTensor.Index.TransformShape.SCALE_X, + LottieTensor.Index.TransformShape.SCALE_Y, + LottieTensor.Index.TransformShape.ROTATION, + LottieTensor.Index.TransformShape.OPACITY, + LottieTensor.Index.TransformShape.ANCHOR_X, + LottieTensor.Index.TransformShape.ANCHOR_Y, + LottieTensor.Index.TransformShape.SKEW, + LottieTensor.Index.TransformShape.SKEW_AXIS, + LottieTensor.Index.TransformShape.HD + ], + LottieTensor.CMD_RECT: [ + LottieTensor.Index.Rect.HD, + LottieTensor.Index.Rect.D + ], + LottieTensor.CMD_ELLIPSE: [], + LottieTensor.CMD_BEZIER: [ + LottieTensor.Index.Bezier.CLOSED + ], + LottieTensor.CMD_SIZE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, # äæ®ę”¹ļ¼šä»Ž TwoValues.VALUE1 改为 Transform.X + LottieTensor.Index.Transform.Y # äæ®ę”¹ļ¼šä»Ž TwoValues.VALUE2 改为 Transform.Y + ], + LottieTensor.CMD_RECT_SIZE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, # 修改 + LottieTensor.Index.Transform.Y # 修改 + ], + LottieTensor.CMD_ELLIPSE_SIZE: [ + LottieTensor.Index.Transform.ANIMATED, + LottieTensor.Index.Transform.X, # 修改 + LottieTensor.Index.Transform.Y # 修改 + ], + + LottieTensor.CMD_ROUNDED: [ + LottieTensor.Index.SingleValue.VALUE, + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_RECT_ROUNDED: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TRIM: [ + LottieTensor.Index.Trim.IX + ], + LottieTensor.CMD_START: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_END: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OFFSET: [ + LottieTensor.Index.SingleValue.ANIMATED, + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_PARENT: [ + LottieTensor.Index.Parent.PARENT_INDEX + ], + LottieTensor.CMD_REFERENCE_ID: list(range(11)), # 11 tokens + LottieTensor.CMD_DIMENSIONS: [ + LottieTensor.Index.Dimensions.WIDTH, + LottieTensor.Index.Dimensions.HEIGHT + ], + LottieTensor.CMD_ASSET: list(range(12)), # FR + 10 tokens + count + LottieTensor.CMD_TEXT_KEYFRAME: list(range(47)), # All text keyframe params + LottieTensor.CMD_FONT: list(range(23)), # All font params + LottieTensor.CMD_CHAR: list(range(35)), # All char params + LottieTensor.CMD_WIDTH_KEYFRAME: [ + LottieTensor.Index.WidthKeyframe.T, + LottieTensor.Index.WidthKeyframe.S, + LottieTensor.Index.WidthKeyframe.I_X, + LottieTensor.Index.WidthKeyframe.I_Y, + LottieTensor.Index.WidthKeyframe.O_X, + LottieTensor.Index.WidthKeyframe.O_Y + ], + LottieTensor.CMD_COLOR_KEYFRAME: [ + LottieTensor.Index.Keyframe.T, + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.S2, + LottieTensor.Index.Keyframe.S3, + LottieTensor.Index.Keyframe.E1, + LottieTensor.Index.Keyframe.I_X, + LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, + LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_OPACITY_KEYFRAME: [ + LottieTensor.Index.Keyframe.T, + LottieTensor.Index.Keyframe.S1, + LottieTensor.Index.Keyframe.I_X, + LottieTensor.Index.Keyframe.I_Y, + LottieTensor.Index.Keyframe.O_X, + LottieTensor.Index.Keyframe.O_Y + ], + LottieTensor.CMD_OPACITY_ANIMATED: [], + LottieTensor.CMD_TM: [ + LottieTensor.Index.Tm.A + ], + LottieTensor.CMD_VALUE: [ + LottieTensor.Index.Value.VALUE + ], + LottieTensor.CMD_SKEW: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_SKEW_AXIS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_STAR: [ + LottieTensor.Index.Star.D, + LottieTensor.Index.Star.SY + ], + LottieTensor.CMD_INNER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_RADIUS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_INNER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_OUTER_ROUNDNESS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_POINTS_STAR: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_STAR_ROTATION: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MULTIPLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_REPEATER: [ + LottieTensor.Index.Repeater.IX + ], + LottieTensor.CMD_COPIES: [ + LottieTensor.Index.SingleValue.VALUE, + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_REPEATER_OFFSET: [ + LottieTensor.Index.SingleValue.VALUE, + LottieTensor.Index.SingleValue.IX + ], + LottieTensor.CMD_COMPOSITE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_REPEATER_TRANSFORM: [], + LottieTensor.CMD_TR_P_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_A_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_SCALE: [ + LottieTensor.Index.TwoValues.VALUE1, + LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_TR_S_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_R_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_SO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_TR_EO_IX: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MORE_OPTIONS: [ + LottieTensor.Index.MoreOptions.G, + LottieTensor.Index.MoreOptions.ALIGNMENT_A, + LottieTensor.Index.MoreOptions.ALIGNMENT_K1, + LottieTensor.Index.MoreOptions.ALIGNMENT_K2, + LottieTensor.Index.MoreOptions.ALIGNMENT_IX + ], + LottieTensor.CMD_GRADIENT_FILL: [], + LottieTensor.CMD_START_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, + LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_END_POINT: [ + LottieTensor.Index.TwoValues.VALUE1, + LottieTensor.Index.TwoValues.VALUE2 + ], + LottieTensor.CMD_GRADIENT_TYPE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_HIGHLIGHT_LENGTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_HIGHLIGHT_ANGLE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_ORIGINAL_COLORS: list(range(48)), # All color values + count + LottieTensor.CMD_COLOR_POINTS: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_GRADIENT_STROKE: [], + LottieTensor.CMD_WIDTH: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_LINE_CAP: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_LINE_JOIN: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MITER_LIMIT: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_ML2: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_COLOR: [ + LottieTensor.Index.Color.INDEX, + LottieTensor.Index.Color.R, + LottieTensor.Index.Color.G, + LottieTensor.Index.Color.B + ], + LottieTensor.CMD_EFFECT: [ + LottieTensor.Index.Effect.TYPE, + LottieTensor.Index.Effect.INDEX, + LottieTensor.Index.Effect.NP, + LottieTensor.Index.Effect.ENABLED + ], + LottieTensor.CMD_LAYER_EFFECT: [ + LottieTensor.Index.LayerEffect.INDEX, + LottieTensor.Index.LayerEffect.VALUE + ], + LottieTensor.CMD_DROPDOWN: [ + LottieTensor.Index.Dropdown.INDEX, + LottieTensor.Index.Dropdown.VALUE + ], + LottieTensor.CMD_NO_VALUE: [ + LottieTensor.Index.NO_VALUE.INDEX, + LottieTensor.Index.NO_VALUE.VALUE + ], + LottieTensor.CMD_IGNORED: [ + LottieTensor.Index.Ignored.INDEX, + LottieTensor.Index.Ignored.VALUE + ], + LottieTensor.CMD_SLIDER: [ + LottieTensor.Index.Slider.INDEX, + LottieTensor.Index.Slider.VALUE + ], + LottieTensor.CMD_FILL_RULE: [ + LottieTensor.Index.SingleValue.VALUE + ], + LottieTensor.CMD_MERGE: [], + LottieTensor.CMD_MERGE_MODE: [ + LottieTensor.Index.MergeMode.MODE + ], + LottieTensor.CMD_MASKS_PROPERTIES: [], + LottieTensor.CMD_MASK: [ + LottieTensor.Index.Mask.INDEX, + LottieTensor.Index.Mask.INV, + LottieTensor.Index.Mask.MODE + ], + LottieTensor.CMD_MASK_PT: [ + LottieTensor.Index.MaskPt.A, + LottieTensor.Index.MaskPt.IX + ], + LottieTensor.CMD_MASK_PT_K: [], + LottieTensor.CMD_MASK_PT_K_C: [ + LottieTensor.Index.MaskPtK.C + ], + LottieTensor.CMD_MASK_PT_K_I: list(range(21)), # V1-V20 + COUNT + LottieTensor.CMD_MASK_PT_K_O: list(range(21)), + LottieTensor.CMD_MASK_PT_K_V: list(range(21)), + LottieTensor.CMD_MASK_O: [ + LottieTensor.Index.MaskO.A, + LottieTensor.Index.MaskO.K, + LottieTensor.Index.MaskO.IX + ], + LottieTensor.CMD_MASK_X: [ + LottieTensor.Index.MaskX.A, + LottieTensor.Index.MaskX.K, + LottieTensor.Index.MaskX.IX + ], + LottieTensor.CMD_MASK_PT_K_ARRAY: [], + LottieTensor.CMD_MASK_PT_KEYFRAME: [ + LottieTensor.Index.MaskPtKeyframe.INDEX, + LottieTensor.Index.MaskPtKeyframe.T + ], + LottieTensor.CMD_MASK_PT_KF_I: [ + LottieTensor.Index.MaskPtKfI.X, + LottieTensor.Index.MaskPtKfI.Y + ], + LottieTensor.CMD_MASK_PT_KF_O: [ + LottieTensor.Index.MaskPtKfO.X, + LottieTensor.Index.MaskPtKfO.Y + ], + LottieTensor.CMD_MASK_PT_KF_S: [], + LottieTensor.CMD_MASK_PT_KF_SHAPE: [ + LottieTensor.Index.MaskPtKfShape.INDEX, + LottieTensor.Index.MaskPtKfShape.C + ], + LottieTensor.CMD_MASK_PT_KF_SHAPE_I: list(range(21)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_O: list(range(21)), + LottieTensor.CMD_MASK_PT_KF_SHAPE_V: list(range(21)), + LottieTensor.CMD_TR_POSITION: [ + LottieTensor.Index.TrPosition.X, + LottieTensor.Index.TrPosition.Y + ], + LottieTensor.CMD_TR_ANCHOR: [ + LottieTensor.Index.TrAnchor.X, + LottieTensor.Index.TrAnchor.Y + ], + LottieTensor.CMD_TR_ROTATION: [ + LottieTensor.Index.TrRotation.VALUE + ], + LottieTensor.CMD_TR_START_OPACITY: [ + LottieTensor.Index.TrStartOpacity.VALUE + ], + LottieTensor.CMD_TR_END_OPACITY: [ + LottieTensor.Index.TrEndOpacity.VALUE + ], + LottieTensor.CMD_ZIG_ZAG: [ + LottieTensor.Index.ZigZag.IX + ], + LottieTensor.CMD_FREQUENCY: [ + LottieTensor.Index.Frequency.VALUE + ], + LottieTensor.CMD_AMPLITUDE: [ + LottieTensor.Index.Amplitude.VALUE + ], + LottieTensor.CMD_POINT_TYPE: [ + LottieTensor.Index.PointType.VALUE + ], + LottieTensor.CMD_ANIMATORS: [], + LottieTensor.CMD_ANIMATOR: [], + LottieTensor.CMD_RANGE_SELECTOR: [ + LottieTensor.Index.RangeSelector.T, + LottieTensor.Index.RangeSelector.R, + LottieTensor.Index.RangeSelector.B, + LottieTensor.Index.RangeSelector.SH, + LottieTensor.Index.RangeSelector.RN + ], + LottieTensor.CMD_RANGE_START: [ + LottieTensor.Index.RangeStart.A + ], + LottieTensor.CMD_RANGE_START_KEYFRAME: [ + LottieTensor.Index.RangeStartKeyframe.T, + LottieTensor.Index.RangeStartKeyframe.S, + LottieTensor.Index.RangeStartKeyframe.I_X, + LottieTensor.Index.RangeStartKeyframe.I_Y, + LottieTensor.Index.RangeStartKeyframe.O_X, + LottieTensor.Index.RangeStartKeyframe.O_Y + ], + LottieTensor.CMD_AMOUNT: [ + LottieTensor.Index.Amount.A, + LottieTensor.Index.Amount.K, + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_MAX_EASE: [ + LottieTensor.Index.MaxEase.A, + LottieTensor.Index.MaxEase.K, + LottieTensor.Index.MaxEase.IX + ], + LottieTensor.CMD_MIN_EASE: [ + LottieTensor.Index.MinEase.A, + LottieTensor.Index.MinEase.K, + LottieTensor.Index.MinEase.IX + ], + LottieTensor.CMD_ANIMATOR_PROPERTIES: [], + LottieTensor.CMD_RADIUS: [ + LottieTensor.Index.Radius.VALUE + ], + LottieTensor.CMD_RANGE_END: [ + LottieTensor.Index.RangeEnd.A + ], + LottieTensor.CMD_RANGE_END_KEYFRAME: [ + LottieTensor.Index.RangeEndKeyframe.T, + LottieTensor.Index.RangeEndKeyframe.S, + LottieTensor.Index.RangeEndKeyframe.I_X, + LottieTensor.Index.RangeEndKeyframe.I_Y, + LottieTensor.Index.RangeEndKeyframe.O_X, + LottieTensor.Index.RangeEndKeyframe.O_Y + ], + LottieTensor.CMD_RANGE_OFFSET: [ + LottieTensor.Index.Amount.A, + LottieTensor.Index.Amount.K, + LottieTensor.Index.Amount.IX + ], + LottieTensor.CMD_RANGE_OFFSET_KEYFRAME: [ + LottieTensor.Index.RangeOffsetKeyframe.T, + LottieTensor.Index.RangeOffsetKeyframe.S, + LottieTensor.Index.RangeOffsetKeyframe.I_X, + LottieTensor.Index.RangeOffsetKeyframe.I_Y, + LottieTensor.Index.RangeOffsetKeyframe.O_X, + LottieTensor.Index.RangeOffsetKeyframe.O_Y + ], + LottieTensor.CMD_S_M: [ + LottieTensor.Index.SM.A, + LottieTensor.Index.SM.K, + LottieTensor.Index.SM.IX + ], + LottieTensor.CMD_OPACITY_ANIMATORS: [ + LottieTensor.Index.OpacityAnimators.A, + LottieTensor.Index.OpacityAnimators.K, + LottieTensor.Index.OpacityAnimators.IX + ], + LottieTensor.CMD_SCALE_ANIMATORS: [ + LottieTensor.Index.ScaleAnimators.A, + LottieTensor.Index.ScaleAnimators.K_X, + LottieTensor.Index.ScaleAnimators.K_Y, + LottieTensor.Index.ScaleAnimators.K_Z, + LottieTensor.Index.ScaleAnimators.IX + ], + LottieTensor.CMD_ROTATION_ANIMATORS: [ + LottieTensor.Index.RotationAnimators.A, + LottieTensor.Index.RotationAnimators.K, + LottieTensor.Index.RotationAnimators.IX + ], + LottieTensor.CMD_POSITION_ANIMATORS: [ + LottieTensor.Index.PositionAnimators.A, + LottieTensor.Index.PositionAnimators.K_X, + LottieTensor.Index.PositionAnimators.K_Y, + LottieTensor.Index.PositionAnimators.K_Z, + LottieTensor.Index.PositionAnimators.IX + ], + LottieTensor.CMD_TRACKING_ANIMATORS: [ + LottieTensor.Index.TrackingAnimators.A, + LottieTensor.Index.TrackingAnimators.K, + LottieTensor.Index.TrackingAnimators.IX + ], + LottieTensor.CMD_DASHES: [], + LottieTensor.CMD_DASH: [ + LottieTensor.Index.Dash.TYPE, + LottieTensor.Index.Dash.LENGTH, + LottieTensor.Index.Dash.V_IX + ], + LottieTensor.CMD_DASH_ANIMATED: [ + LottieTensor.Index.DashAnimated.TYPE, + LottieTensor.Index.DashAnimated.V_IX + ], + LottieTensor.CMD_DASH_KEYFRAME: [ + LottieTensor.Index.DashKeyframe.T, + LottieTensor.Index.DashKeyframe.S, + LottieTensor.Index.DashKeyframe.I_X, + LottieTensor.Index.DashKeyframe.I_Y, + LottieTensor.Index.DashKeyframe.O_X, + LottieTensor.Index.DashKeyframe.O_Y + ], + LottieTensor.CMD_DASH_OFFSET: [ + LottieTensor.Index.DashOffset.O + ], + LottieTensor.CMD_WIDTH_ANIMATED: [], + # All end commands have empty param lists + LottieTensor.CMD_POSITION_END: [], + LottieTensor.CMD_SCALE_END: [], + LottieTensor.CMD_ROTATION_END: [], + LottieTensor.CMD_OPACITY_END: [], + LottieTensor.CMD_ANCHOR_END: [], + LottieTensor.CMD_GROUP_END: [], + LottieTensor.CMD_TRANSFORM_END: [], + LottieTensor.CMD_LAYER_END: [], + LottieTensor.CMD_PATH_END: [], + LottieTensor.CMD_RECT_END: [], + LottieTensor.CMD_ELLIPSE_END: [], + LottieTensor.CMD_STAR_END: [], + LottieTensor.CMD_TRIM_END: [], + LottieTensor.CMD_REPEATER_END: [], + LottieTensor.CMD_REPEATER_TRANSFORM_END: [], + LottieTensor.CMD_GRADIENT_FILL_END: [], + LottieTensor.CMD_GRADIENT_STROKE_END: [], + LottieTensor.CMD_MERGE_END: [], + LottieTensor.CMD_ROUNDED_CORNERS_END: [], + LottieTensor.CMD_TWIST_END: [], + LottieTensor.CMD_BEZIER_END: [], + LottieTensor.CMD_TEXT_LAYER_END: [], + LottieTensor.CMD_TEXT_DATA_END: [], + LottieTensor.CMD_SOLID_LAYER_END: [], + LottieTensor.CMD_NULL_LAYER_END: [], + LottieTensor.CMD_PRECOMP_LAYER_END: [], + LottieTensor.CMD_POSITION_X_END: [], + LottieTensor.CMD_POSITION_Y_END: [], + LottieTensor.CMD_POSITION_Z_END: [], + LottieTensor.CMD_SCALE_X_END: [], + LottieTensor.CMD_SCALE_Y_END: [], + LottieTensor.CMD_SCALE_Z_END: [], + LottieTensor.CMD_ROTATION_X_END: [], + LottieTensor.CMD_ROTATION_Y_END: [], + LottieTensor.CMD_ROTATION_Z_END: [], + LottieTensor.CMD_EFFECTS_END: [], + LottieTensor.CMD_EFFECT_END: [], + LottieTensor.CMD_KEYFRAME_END: [], + LottieTensor.CMD_WIDTH_ANIMATED_END: [], + LottieTensor.CMD_FONTS_END: [], + LottieTensor.CMD_CHARS_END: [], + LottieTensor.CMD_CHAR_END: [], + LottieTensor.CMD_CHAR_SHAPES_END: [], + LottieTensor.CMD_TEXT_KEYFRAMES_END: [], + LottieTensor.CMD_TEXT_DOC_END: [], + LottieTensor.CMD_MORE_OPTIONS_END: [], + LottieTensor.CMD_OPACITY_ANIMATED_END: [], + LottieTensor.CMD_MASKS_PROPERTIES_END: [], + LottieTensor.CMD_MASK_END: [], + LottieTensor.CMD_MASK_PT_END: [], + LottieTensor.CMD_MASK_PT_K_END: [], + LottieTensor.CMD_TM_END: [], + LottieTensor.CMD_MASK_PT_K_ARRAY_END: [], + LottieTensor.CMD_MASK_PT_KEYFRAME_END: [], + LottieTensor.CMD_MASK_PT_KF_S_END: [], + LottieTensor.CMD_MASK_PT_KF_SHAPE_END: [], + LottieTensor.CMD_VALUE_END: [], + LottieTensor.CMD_ZIG_ZAG_END: [], + LottieTensor.CMD_ANIMATORS_END: [], + LottieTensor.CMD_ANIMATOR_END: [], + LottieTensor.CMD_RANGE_SELECTOR_END: [], + LottieTensor.CMD_RANGE_START_END: [], + LottieTensor.CMD_RANGE_END_END: [], + LottieTensor.CMD_END_END: [], + LottieTensor.CMD_START_END: [], + LottieTensor.CMD_OFFSET_END: [], + LottieTensor.CMD_RANGE_OFFSET_END: [], + LottieTensor.CMD_SCALE_ANIMATORS_END: [], + LottieTensor.CMD_ROTATION_ANIMATORS_END: [], + LottieTensor.CMD_POSITION_ANIMATORS_END: [], + LottieTensor.CMD_OPACITY_ANIMATORS_END: [], + LottieTensor.CMD_COLOR_ANIMATED_END: [], + LottieTensor.CMD_DASHES_END: [], + LottieTensor.CMD_DASH_ANIMATED_END: [], + LottieTensor.CMD_SIZE_END: [], + LottieTensor.CMD_RECT_ROUNDED_END: [], + LottieTensor.CMD_ANIMATOR_PROPERTIES_END: [], + LottieTensor.CMD_ASSET_END: [], + LottieTensor.CMD_EFFECTS: [], + LottieTensor.CMD_FONTS: [], + LottieTensor.CMD_CHARS: [], + LottieTensor.CMD_CHAR_SHAPES: [], + LottieTensor.CMD_TEXT_KEYFRAMES: [], + LottieTensor.CMD_TEXT_DATA: [], + LottieTensor.CMD_DOCUMENT: [], + LottieTensor.CMD_TEXT_DOC: [], + } + + # Commands without parameters + empty_param_cmds = {k for k, v in param_orders.items() if not v} + + return param_orders.get(cmd_idx, []) + + + @staticmethod + def get_vocab_range_for_offset(offset: int) -> tuple: + return NotImplementedError + + + + def flatten_to_list(lottie_tensor: 'LottieTensor', max_length: int = None) -> List[int]: + return NotImplementedError + + + @staticmethod + def from_list(flattened: List[int]) -> 'LottieTensor': + if LottieTensor.tokenizer is None: + try: + LottieTensor.init_tokenizer() + except Exception as e: + print(f"Warning: Failed to initialize tokenizer in from_list: {e}") + + COMMAND_OFFSET = 151936 + NUMBER_OFFSET = 173186 + NUM_COMMANDS = len(LottieTensor.COMMANDS) + + # SIZEē±»å‘½ä»¤é›†åˆ + SIZE_COMMANDS = { + LottieTensor.CMD_SIZE, + LottieTensor.CMD_ELLIPSE_SIZE, + LottieTensor.CMD_RECT_SIZE + } + + ANIMATED_THRESHOLD = 0.5 + + PARAM_DEFAULTS = {} + + for i in [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 22]: + PARAM_DEFAULTS[(LottieTensor.CMD_KEYFRAME, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_WIDTH_KEYFRAME, i)] = 0.0 + + for i in range(5, 9): + PARAM_DEFAULTS[(LottieTensor.CMD_COLOR_KEYFRAME, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_OPACITY_KEYFRAME, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_POINT, i)] = 0.0 + + PARAM_DEFAULTS[(LottieTensor.CMD_ANIMATION, 5)] = 0.0 + + for i in range(4, 13): + PARAM_DEFAULTS[(LottieTensor.CMD_LAYER, i)] = 0.0 + + for i in range(4, 12): + PARAM_DEFAULTS[(LottieTensor.CMD_NULL_LAYER, i)] = 0.0 + + for i in range(4, 15): + PARAM_DEFAULTS[(LottieTensor.CMD_PRECOMP_LAYER, i)] = 0.0 + + for i in range(3, 15): + PARAM_DEFAULTS[(LottieTensor.CMD_FILL, i)] = 0.0 + + for i in range(3, 14): + PARAM_DEFAULTS[(LottieTensor.CMD_STROKE, i)] = 0.0 + + for i in range(1, 5): + PARAM_DEFAULTS[(LottieTensor.CMD_GROUP, i)] = 0.0 + + for i in range(4, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_PATH, i)] = 0.0 + + for i in range(8, 11): + PARAM_DEFAULTS[(LottieTensor.CMD_TRANSFORM_SHAPE, i)] = 0.0 + + for i in range(1, 4): + PARAM_DEFAULTS[(LottieTensor.CMD_POSITION, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_SCALE, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_ANCHOR, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_ROTATION, 1)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_OPACITY, 1)] = 0.0 + + PARAM_DEFAULTS[(LottieTensor.CMD_RECT, 1)] = 0.0 + + for i in range(21): + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_K_I, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_K_O, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_K_V, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_KF_SHAPE_I, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_KF_SHAPE_O, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_MASK_PT_KF_SHAPE_V, i)] = 0.0 + + for i in range(2, 6): + PARAM_DEFAULTS[(LottieTensor.CMD_DASH_KEYFRAME, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_RANGE_START_KEYFRAME, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_RANGE_END_KEYFRAME, i)] = 0.0 + PARAM_DEFAULTS[(LottieTensor.CMD_RANGE_OFFSET_KEYFRAME, i)] = 0.0 + + TOKENIZER_COMMANDS = { + LottieTensor.CMD_FONT: { + 'regular': [LottieTensor.Index.Font.ASCENT], + 'token_groups': [ + (LottieTensor.Index.Font.FAMILY_TOKEN_COUNT, + LottieTensor.Index.Font.FAMILY_TOKEN_0, 10), + (LottieTensor.Index.Font.STYLE_TOKEN_COUNT, + LottieTensor.Index.Font.STYLE_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_CHAR: { + 'regular': [ + LottieTensor.Index.Char.SIZE, + LottieTensor.Index.Char.W + ], + 'token_groups': [ + (LottieTensor.Index.Char.CH_TOKEN_COUNT, + LottieTensor.Index.Char.CH_TOKEN_0, 10), + (LottieTensor.Index.Char.STYLE_TOKEN_COUNT, + LottieTensor.Index.Char.STYLE_TOKEN_0, 10), + (LottieTensor.Index.Char.FAMILY_TOKEN_COUNT, + LottieTensor.Index.Char.FAMILY_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_ASSET: { + 'regular': [LottieTensor.Index.Asset.FR], + 'token_groups': [ + (LottieTensor.Index.Asset.ID_TOKEN_COUNT, + LottieTensor.Index.Asset.ID_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_REFERENCE_ID: { + 'regular': [], + 'token_groups': [ + (LottieTensor.Index.ReferenceId.ID_TOKEN_COUNT, + LottieTensor.Index.ReferenceId.ID_TOKEN_0, 10) + ] + }, + LottieTensor.CMD_TEXT_KEYFRAME: { + 'regular': list(range(LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START)), + 'token_groups': [ + (LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKEN_COUNT, + LottieTensor.Index.TextKeyframe.FONT_FAMILY_TOKENS_START, 10), + (LottieTensor.Index.TextKeyframe.TEXT_TOKEN_COUNT, + LottieTensor.Index.TextKeyframe.TEXT_TOKENS_START, 15) + ] + } + } + + def is_command_token(token): + """Check if a token is a command token.""" + return COMMAND_OFFSET <= token < COMMAND_OFFSET + NUM_COMMANDS + + def get_default_value(cmd_idx, param_pos, params): + """Get default value for a parameter.""" + + # SIZEē±»å‘½ä»¤ēš„ē‰¹ę®Šå¤„ē† + if cmd_idx in SIZE_COMMANDS: + animated_val = params[LottieTensor.Index.Transform.ANIMATED] + is_animated = animated_val != LottieTensor.PAD_VAL and animated_val >= ANIMATED_THRESHOLD + + if param_pos in {1, 2}: + if is_animated: + return LottieTensor.PAD_VAL + else: + return 0.0 + + key = (cmd_idx, param_pos) + return PARAM_DEFAULTS.get(key, LottieTensor.PAD_VAL) + + def has_following_keyframe(flattened_list, start_idx, cmd_idx): + SIZE_END_COMMANDS = { + LottieTensor.CMD_SIZE: LottieTensor.CMD_SIZE_END, + LottieTensor.CMD_ELLIPSE_SIZE: LottieTensor.CMD_SIZE_END, + LottieTensor.CMD_RECT_SIZE: LottieTensor.CMD_SIZE_END, + } + + end_cmd = SIZE_END_COMMANDS.get(cmd_idx, LottieTensor.CMD_SIZE_END) + + context_end_cmds = { + end_cmd, + LottieTensor.CMD_FILL, + LottieTensor.CMD_STROKE, + LottieTensor.CMD_GROUP_END, + LottieTensor.CMD_ELLIPSE_END, + LottieTensor.CMD_RECT_END, + } + + for k in range(start_idx, len(flattened_list)): + if is_command_token(flattened_list[k]): + cmd = flattened_list[k] - COMMAND_OFFSET + if cmd in context_end_cmds: + return False + if cmd == LottieTensor.CMD_KEYFRAME: + return True + return False + + commands = [] + params_list = [] + + i = 0 + while i < len(flattened): + if is_command_token(flattened[i]): + cmd_idx = flattened[i] - COMMAND_OFFSET + commands.append(cmd_idx) + cmd_start_i = i + i += 1 + + params = [LottieTensor.PAD_VAL] * LottieTensor.PARAM_DIM + + if cmd_idx in TOKENIZER_COMMANDS: + cmd_info = TOKENIZER_COMMANDS[cmd_idx] + regular_params = cmd_info['regular'] + + for param_idx in regular_params: + if i < len(flattened) and not is_command_token(flattened[i]): + offset = LottieTensor.get_param_offset(cmd_idx, param_idx) + params[param_idx] = float(flattened[i] - offset) + i += 1 + + for count_idx, token_start, max_tokens in cmd_info['token_groups']: + if i < len(flattened) and not is_command_token(flattened[i]): + count = flattened[i] - NUMBER_OFFSET + params[count_idx] = float(count) + i += 1 + + for j in range(int(max(0, count))): + if i < len(flattened) and not is_command_token(flattened[i]): + if token_start + j < LottieTensor.PARAM_DIM: + params[token_start + j] = float(flattened[i]) + i += 1 + else: + break + else: + param_indices = LottieTensor.get_command_param_indices(cmd_idx) + + param_pos = 0 + while (param_pos < len(param_indices) and + i < len(flattened) and + not is_command_token(flattened[i])): + + param_idx = param_indices[param_pos] + offset = LottieTensor.get_param_offset(cmd_idx, param_idx) + params[param_idx] = float(flattened[i] - offset) + param_pos += 1 + i += 1 + + if cmd_idx in SIZE_COMMANDS: + animated_val = params[LottieTensor.Index.Transform.ANIMATED] + + if param_pos == 1 and animated_val != LottieTensor.PAD_VAL and animated_val >= ANIMATED_THRESHOLD: + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + + elif animated_val == LottieTensor.PAD_VAL or animated_val < ANIMATED_THRESHOLD: + if has_following_keyframe(flattened, i, cmd_idx): + params[LottieTensor.Index.Transform.ANIMATED] = 1.0 + elif param_pos >= 2: + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + + elif param_pos >= 2 and (animated_val == LottieTensor.PAD_VAL or animated_val < ANIMATED_THRESHOLD): + params[LottieTensor.Index.Transform.ANIMATED] = 0.0 + + # Fill in defaults for remaining parameters + for j in range(param_pos, len(param_indices)): + param_idx = param_indices[j] + + default_val = get_default_value(cmd_idx, j, params) + + if default_val != LottieTensor.PAD_VAL: + params[param_idx] = default_val + + params_list.append(params) + else: + i += 1 + + if commands: + commands_tensor = torch.tensor(commands).reshape(-1, 1).long() + params_tensor = torch.tensor(params_list).float() + else: + commands_tensor = torch.zeros((0, 1)).long() + params_tensor = torch.zeros((0, LottieTensor.PARAM_DIM)).float() + + return LottieTensor(commands_tensor, params_tensor) + + diff --git a/lottie/objects/nvector.py b/lottie/objects/nvector.py new file mode 100644 index 0000000..0ec470e --- /dev/null +++ b/lottie/objects/nvector.py @@ -0,0 +1,148 @@ +import operator +import math + + +def vop(op, a, b): + return list(map(op, a, b)) + + +class NVector(): + def __init__(self, *components): + self.components = list(components) + + def __str__(self): + return str(self.components) + + def __repr__(self): + return "" % self + + def __len__(self): + return len(self.components) + + def to_list(self): + return list(self.components) + + def __add__(self, other): + return type(self)(*vop(operator.add, self.components, other.components)) + + def __sub__(self, other): + return type(self)(*vop(operator.sub, self.components, other.components)) + + def __mul__(self, scalar): + if isinstance(scalar, NVector): + return type(self)(*vop(operator.mul, self.components, scalar.components)) + return type(self)(*(c * scalar for c in self.components)) + + def __truediv__(self, scalar): + return type(self)(*(c / scalar for c in self.components)) + + def __iadd__(self, other): + self.components = vop(operator.add, self.components, other.components) + return self + + def __isub__(self, other): + self.components = vop(operator.sub, self.components, other.components) + return self + + def __imul__(self, scalar): + if isinstance(scalar, NVector): + self.components = vop(operator.mul, self.components, scalar.components) + else: + self.components = [c * scalar for c in self.components] + return self + + def __itruediv__(self, scalar): + self.components = [c / scalar for c in self.components] + return self + + def __neg__(self): + return type(self)(*(-c for c in self.components)) + + def __getitem__(self, key): + if isinstance(key, slice): + return NVector(*self.components[key]) + return self.components[key] + + def __setitem__(self, key, value): + self.components[key] = value + + def __eq__(self, other): + return self.components == other.components + + def __abs__(self): + return type(self)(*(abs(c) for c in self.components)) + + @property + def length(self): + return math.sqrt(sum(map(lambda x: x**2, self.components))) + + def dot(self, other): + return sum(map(operator.mul, self.components, other.components)) + + def clone(self): + return NVector(*self.components) + + def lerp(self, other, t): + return self * (1-t) + other * t + + @property + def x(self): + return self.components[0] + + @x.setter + def x(self, v): + self.components[0] = v + + @property + def y(self): + return self.components[1] + + @y.setter + def y(self, v): + self.components[1] = v + + @property + def z(self): + return self.components[2] + + @z.setter + def z(self, v): + self.components[2] = v + + def element_scaled(self, other): + return type(self)(*vop(operator.mul, self.components, other.components)) + + def cross(self, other): + """ + @pre len(self) == len(other) == 3 + """ + a = self + b = other + return type(self)( + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ) + + @property + def polar_angle(self): + """ + @pre len(self) == 2 + """ + return math.atan2(self.y, self.x) + + +def Point(x, y): + return NVector(x, y) + + +def Size(x, y): + return NVector(x, y) + + +def Point3D(x, y, z): + return NVector(x, y, z) + + +def PolarVector(length, theta): + return NVector(length * math.cos(theta), length * math.sin(theta)) diff --git a/lottie/objects/properties.py b/lottie/objects/properties.py new file mode 100644 index 0000000..368ac65 --- /dev/null +++ b/lottie/objects/properties.py @@ -0,0 +1,686 @@ +import math +from functools import reduce +from .base import LottieObject, LottieProp, PseudoList, PseudoBool +from .easing import KeyframeBezierHandle, Linear +from .nvector import NVector +from .bezier import Bezier +from .color import Color + + +class KeyframeBezier: + NEWTON_ITERATIONS = 4 + NEWTON_MIN_SLOPE = 0.001 + SUBDIVISION_PRECISION = 0.0000001 + SUBDIVISION_MAX_ITERATIONS = 10 + SPLINE_TABLE_SIZE = 11 + SAMPLE_STEP_SIZE = 1.0 / (SPLINE_TABLE_SIZE - 1.0) + + def __init__(self, h1, h2): + self.h1 = h1 + self.h2 = h2 + self._sample_values = None + + @classmethod + def from_keyframe(cls, keyframe): + return cls(keyframe.out_value, keyframe.in_value) + + def bezier(self): + bez = Bezier() + bez.add_point(NVector(0, 0), outp=NVector(self.h1.x, self.h1.y)) + bez.add_point(NVector(1, 1), inp=NVector(self.h2.x-1, self.h2.y-1)) + return bez + + def _a(self, c1, c2): + return 1 - 3 * c2 + 3 * c1 + + def _b(self, c1, c2): + return 3 * c2 - 6 * c1 + + def _c(self, c1): + return 3 * c1 + + def _bezier_component(self, t, c1, c2): + return ((self._a(c1, c2) * t + self._b(c1, c2)) * t + self._c(c1)) * t + + def point_at(self, t): + return NVector( + self._bezier_component(t, self.h1.x, self.h2.x), + self._bezier_component(t, self.h1.y, self.h2.y) + ) + + def _slope_component(self, t, c1, c2): + return 3 * self._a(c1, c2) * t * t + 2 * self._b(c1, c2) * t + self._c(c1) + + def slope_at(self, t): + return NVector( + self._slope_component(t, self.h1.x, self.h2.x), + self._slope_component(t, self.h1.y, self.h2.y) + ) + + def _binary_subdivide(self, x, interval_start, interval_end): + current_x = None + t = None + i = 0 + for i in range(self.SUBDIVISION_MAX_ITERATIONS): + if current_x is not None and abs(current_x) < self.SUBDIVISION_PRECISION: + break + t = interval_start + (interval_end - interval_start) / 2.0 + current_x = self._bezier_component(t, self.h1.x, self.h2.x) - x + if current_x > 0.0: + interval_end = t + else: + interval_start = t + return t + + def _newton_raphson(self, x, t_guess): + for i in range(self.NEWTON_ITERATIONS): + slope = self._slope_component(t_guess, self.h1.x, self.h2.x) + if slope == 0: + return t_guess + current_x = self._bezier_component(t_guess, self.h1.x, self.h2.x) - x + t_guess -= current_x / slope + return t_guess + + def _get_sample_values(self): + if self._sample_values is None: + self._sample_values = [ + self._bezier_component(i * self.SAMPLE_STEP_SIZE, self.h1.x, self.h2.x) + for i in range(self.SPLINE_TABLE_SIZE) + ] + return self._sample_values + + def t_for_x(self, x): + sample_values = self._get_sample_values() + interval_start = 0 + current_sample = 1 + last_sample = self.SPLINE_TABLE_SIZE - 1 + while current_sample != last_sample and sample_values[current_sample] <= x: + interval_start += self.SAMPLE_STEP_SIZE + current_sample += 1 + current_sample -= 1 + + dist = (x - sample_values[current_sample]) / (sample_values[current_sample+1] - sample_values[current_sample]) + t_guess = interval_start + dist * self.SAMPLE_STEP_SIZE + initial_slope = self._slope_component(t_guess, self.h1.x, self.h2.x) + if initial_slope >= self.NEWTON_MIN_SLOPE: + return self._newton_raphson(x, t_guess) + if initial_slope == 0: + return t_guess + return self._binary_subdivide(x, interval_start, interval_start + self.SAMPLE_STEP_SIZE) + + def y_at_x(self, x): + t = self.t_for_x(x) + return self._bezier_component(t, self.h1.y, self.h2.y) + + +## @ingroup Lottie +class Keyframe(LottieObject): + _props = [ + LottieProp("time", "t", float, False), + LottieProp("in_value", "i", KeyframeBezierHandle, False), + LottieProp("out_value", "o", KeyframeBezierHandle, False), + LottieProp("jump", "h", PseudoBool), + ] + + def __init__(self, time=0, easing_function=None): + """! + @param time Start time of keyframe segment + @param easing_function Callable that performs the easing + """ + ## Start time of keyframe segment. + self.time = time + ## Bezier curve easing in value. + self.in_value = None + ## Bezier curve easing out value. + self.out_value = None + ## Jump to the end value + self.jump = None + + if easing_function: + easing_function(self) + + def bezier(self): + if self.jump: + bez = Bezier() + bez.add_point(NVector(0, 0)) + bez.add_point(NVector(1, 0)) + bez.add_point(NVector(1, 1)) + return bez + else: + return KeyframeBezier.from_keyframe(self).bezier() + + def lerp_factor(self, ratio): + return KeyframeBezier.from_keyframe(self).y_at_x(ratio) + + def __str__(self): + return "%s %s" % (self.time, self.start) + + +## @ingroup Lottie +class OffsetKeyframe(Keyframe): + """! + Keyframe for MultiDimensional values + + @par Bezier easing + @parblock + Imagine a quadratic bezier, with starting point at (0, 0) and end point at (1, 1). + + @p out_value and @p in_value are the other two handles for a quadratic bezier, + expressed as absoulte values in this 0-1 space. + + See also https://cubic-bezier.com/ + @endparblock + """ + _props = [ + LottieProp("start", "s", NVector, False), + LottieProp("end", "e", NVector, False), + LottieProp("in_tan", "ti", NVector, False), + LottieProp("out_tan", "to", NVector, False), + ] + + def __init__(self, time=0, start=None, end=None, easing_function=None, in_tan=None, out_tan=None): + Keyframe.__init__(self, time, easing_function) + ## Start value of keyframe segment. + self.start = start + ## End value of keyframe segment. + self.end = end + ## In Spatial Tangent. Only for spatial properties. (for bezier smoothing on position) + self.in_tan = in_tan + ## Out Spatial Tangent. Only for spatial properties. (for bezier smoothing on position) + self.out_tan = out_tan + + def interpolated_value(self, ratio, next_start=None): + end = next_start if self.end is None else self.end + if end is None: + return self.start + if not self.in_value or not self.out_value: + return self.start + if ratio == 1: + return end + if ratio == 0: + return self.start + if self.in_tan and self.out_tan: + bezier = Bezier() + bezier.add_point(self.start, NVector(0, 0), self.out_tan) + bezier.add_point(end, self.in_tan, NVector(0, 0)) + return bezier.point_at(ratio) + + lerpv = self.lerp_factor(ratio) + return self.start.lerp(end, lerpv) + + def interpolated_tangent_angle(self, ratio, next_start=None): + end = next_start if self.end is None else self.end + if end is None or not self.in_tan or not self.out_tan: + return 0 + + bezier = Bezier() + bezier.add_point(self.start, NVector(0, 0), self.out_tan) + bezier.add_point(end, self.in_tan, NVector(0, 0)) + return bezier.tangent_angle_at(ratio) + + def __repr__(self): + return "<%s.%s %s %s%s>" % ( + type(self).__module__, + type(self).__name__, + self.time, + self.start, + (" -> %s" % self.end) if self.end is not None else "" + ) + + +class AnimatableMixin: + keyframe_type = Keyframe + + def __init__(self, value=None): + ## Non-animated value + self.value = value + ## Property index + self.property_index = None + ## Whether it's animated + self.animated = False + ## Keyframe list + self.keyframes = None + + def clear_animation(self, value): + """! + Sets a fixed value, removing animated keyframes + """ + self.value = value + self.animated = False + self.keyframes = None + + def add_keyframe(self, time, value, interp=Linear(), *args, **kwargs): + """! + @param time The time this keyframe appears in + @param value The value the property should have at @p time + @param interp The easing callable used to update the tangents of the previous keyframe + @param args Extra arguments to pass the keyframe constructor + @param kwargs Extra arguments to pass the keyframe constructor + @note Always call add_keyframe with increasing @p time value + """ + if not self.animated: + self.value = None + self.keyframes = [] + self.animated = True + else: + if self.keyframes[-1].time == time: + if value != self.keyframes[-1].start: + self.keyframes[-1].start = value + return + else: + self.keyframes[-1].end = value.clone() + + self.keyframes.append(self.keyframe_type( + time, + value, + None, + interp, + *args, + **kwargs + )) + + def get_value(self, time=0): + """! + @brief Returns the value of the property at the given frame/time + """ + if not self.animated: + return self.value + + if not self.keyframes: + return None + + return self._get_value_helper(time)[0] + + def _get_value_helper(self, time): + val = self.keyframes[0].start + for i in range(len(self.keyframes)): + k = self.keyframes[i] + if time - k.time <= 0: + if k.start is not None: + val = k.start + + kp = self.keyframes[i-1] if i > 0 else None + if kp: + t = (time - kp.time) / (k.time - kp.time) + end = kp.end + if end is None: + end = val + if end is not None: + val = kp.interpolated_value(t, end) + return val, end, kp, t + return val, None, None, None + if k.end is not None: + val = k.end + return val, None, None, None + + def to_dict(self): + d = super().to_dict() + if self.animated: + if "k" not in d: + return d + last = d["k"][-1] + last.pop("i", None) + last.pop("o", None) + return d + + def __repr__(self): + if self.keyframes and len(self.keyframes) > 1: + val = "%s -> %s" % (self.keyframes[0].start, self.keyframes[-2].end) + else: + val = self.value + return "<%s.%s %s>" % (type(self).__module__, type(self).__name__, val) + + def __str__(self): + if self.animated: + return "animated" + return str(self.value) + + @classmethod + def merge_keyframes(cls, items, conversion): + """ + @todo Remove similar functionality from SVG/sif parsers + """ + keyframes = [] + for animatable in items: + if animatable.animated: + keyframes.extend(animatable.keyframes) + + # TODO properly interpolate tangents + new_kframes = [] + for keyframe in sorted(keyframes, key=lambda kf: kf.time): + if new_kframes and new_kframes[-1].time == keyframe.time: + continue + kfcopy = keyframe.clone() + kfcopy.start = conversion(*(i.get_value(keyframe.time) for i in items)) + new_kframes.append(kfcopy) + + for i in range(0, len(new_kframes) - 1): + new_kframes[i].end = new_kframes[i+1].start + + return new_kframes + + @classmethod + def load(cls, lottiedict): + obj = super().load(lottiedict) + if "a" not in lottiedict: + obj.animated = prop_animated(lottiedict) + return obj + + +def prop_animated(l): + if "a" in l: + return l["a"] + if "k" not in l: + return False + if isinstance(l["k"], list) and l["k"] and isinstance(l["k"][0], dict): + return True + return False + + +def prop_not_animated(l): + return not prop_animated(l) + + +## @ingroup Lottie +class MultiDimensional(AnimatableMixin, LottieObject): + """! + An animatable property that holds a NVector + """ + keyframe_type = OffsetKeyframe + _props = [ + LottieProp("value", "k", NVector, False, prop_not_animated), + LottieProp("property_index", "ix", int, False), + LottieProp("animated", "a", PseudoBool, False), + LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), + ] + + def get_tangent_angle(self, time=0): + """! + @brief Returns the value tangent angle of the property at the given frame/time + """ + if not self.keyframes or len(self.keyframes) < 2: + return 0 + + val, end, kp, t = self._get_value_helper(time) + if kp: + return kp.interpolated_tangent_angle(t, end) + + if self.keyframes[0].time >= time: + end = self.keyframes[0].end if self.keyframes[0].end is not None else self.keyframes[1].start + return self.keyframes[0].interpolated_tangent_angle(0, end) + + return 0 + + +class PositionValue(MultiDimensional): + _props = [ + LottieProp("value", "k", NVector, False, prop_not_animated), + LottieProp("property_index", "ix", int, False), + LottieProp("animated", "a", PseudoBool, False), + LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), + ] + + @classmethod + def load(cls, lottiedict): + obj = super().load(lottiedict) + if lottiedict.get("s", False): + cls._load_split(lottiedict, obj) + + return obj + + @classmethod + def _load_split(cls, lottiedict, obj): + components = [ + Value.load(lottiedict.get("x", {})), + Value.load(lottiedict.get("y", {})), + ] + if "z" in lottiedict: + components.append(Value.load(lottiedict.get("z", {}))) + + has_anim = any(x for x in components if x.animated) + if not has_anim: + obj.value = NVector(*(a.value for a in components)) + obj.animated = False + obj.keyframes = None + return + + obj.animated = True + obj.value = None + obj.keyframes = cls.merge_keyframes(components, NVector) + + +class ColorValue(AnimatableMixin, LottieObject): + """! + An animatable property that holds a Color + """ + keyframe_type = OffsetKeyframe + _props = [ + LottieProp("value", "k", Color, False, prop_not_animated), + LottieProp("property_index", "ix", int, False), + LottieProp("animated", "a", PseudoBool, False), + LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), + ] + + +## @ingroup Lottie +class GradientColors(LottieObject): + """! + Represents colors and offsets in a gradient + + Colors are represented as a flat list interleaving offsets and color components in weird ways + There are two possible layouts: + + Without alpha, the colors are a sequence of offset, r, g, b + + With alpha, same as above but at the end of the list there is a sequence of offset, alpha + + Examples: + + For the gradient [0, red], [0.5, yellow], [1, green] + The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0] + + For the gradient [0, red at 80% opacity], [0.5, yellow at 70% opacity], [1, green at 60% opacity] + The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0, 0, 0.8, 0.5, 0.7, 1, 0.6] + """ + _props = [ + LottieProp("colors", "k", MultiDimensional), + LottieProp("count", "p", int), + ] + + def __init__(self, stops=[]): + ## Animatable colors, as a vector containing [offset, r, g, b] values as a flat array + self.colors = MultiDimensional(NVector()) + ## Number of colors + self.count = 0 + if stops: + self.set_stops(stops) + + @staticmethod + def color_to_stops(self, colors): + """ + Converts a list of colors (Color) to tuples (offset, color) + """ + return [ + (i / (len(colors)-1), color) + for i, color in enumerate(colors) + ] + + def set_stops(self, stops, keyframe=None): + """! + @param stops iterable of (offset, Color) tuples + @param keyframe keyframe index (or None if not animated) + """ + flat = self._flatten_stops(stops) + if self.colors.animated and keyframe is not None: + if keyframe > 1: + self.colors.keyframes[keyframe-1].end = flat + self.colors.keyframes[keyframe].start = flat + else: + self.colors.clear_animation(flat) + self.count = len(stops) + + def _flatten_stops(self, stops): + flattened_colors = NVector(*reduce( + lambda a, b: a + b, + ( + [off] + color.components[:3] + for off, color in stops + ) + )) + + if any(len(c) > 3 for o, c in stops): + flattened_colors.components += reduce( + lambda a, b: a + b, + ( + [off] + [self._get_alpha(color)] + for off, color in stops + ) + ) + return flattened_colors + + def _get_alpha(self, color): + if len(color) > 3: + return color[3] + return 1 + + def _add_to_flattened(self, offset, color, flattened): + flat = [offset] + list(color[:3]) + rgb_size = 4 * self.count + + if len(flattened) == rgb_size: + # No alpha + flattened.extend(flat) + if self.count == 0 and len(color) > 3: + flattened.append(offset) + flattened.append(color[3]) + else: + flattened[rgb_size:rgb_size] = flat + flattened.append(offset) + flattened.append(self._get_alpha(color)) + + def add_color(self, offset, color, keyframe=None): + if self.colors.animated: + if keyframe is None: + for kf in self.colors.keyframes: + if kf.start: + self._add_to_flattened(offset, color, kf.start.components) + if kf.end: + self._add_to_flattened(offset, color, kf.end.components) + else: + if keyframe > 1: + self._add_to_flattened(offset, color, self.colors.keyframes[keyframe-1].end.components) + self._add_to_flattened(offset, color, self.colors.keyframes[keyframe].start.components) + else: + self._add_to_flattened(offset, color, self.colors.value.components) + self.count += 1 + + def add_keyframe(self, time, stops, ease=Linear()): + """! + @param time Frame time + @param stops Iterable of (offset, Color) tuples + @param ease Easing function + """ + self.colors.add_keyframe(time, self._flatten_stops(stops), ease) + + def get_stops(self, keyframe=None): + if keyframe is not None: + colors = self.colors.keyframes[keyframe].start + else: + colors = self.colors.value + return self._stops_from_flat(colors) + + def _stops_from_flat(self, colors): + if len(colors) == 4 * self.count: + for i in range(self.count): + off = i * 4 + yield colors[off], Color(*colors[off+1:off+4]) + else: + for i in range(self.count): + off = i * 4 + aoff = self.count * 4 + i * 2 + 1 + yield colors[off], Color(colors[off+1], colors[off+2], colors[off+3], colors[aoff]) + + def stops_at(self, time): + return self._stops_from_flat(self.colors.get_value(time)) + + +## @ingroup Lottie +class Value(AnimatableMixin, LottieObject): + """! + An animatable property that holds a float + """ + keyframe_type = OffsetKeyframe + _props = [ + LottieProp("value", "k", float, False, prop_not_animated), + LottieProp("property_index", "ix", int, False), + LottieProp("animated", "a", PseudoBool, False), + LottieProp("keyframes", "k", keyframe_type, True, prop_animated), + ] + + def __init__(self, value=0): + super().__init__(value) + + def add_keyframe(self, time, value, ease=Linear()): + super().add_keyframe(time, NVector(value), ease) + + def get_value(self, time=0): + v = super().get_value(time) + if self.animated and self.keyframes: + return v[0] + return v + + +## @ingroup Lottie +class ShapePropKeyframe(Keyframe): + """! + Keyframe holding Bezier objects + """ + _props = [ + LottieProp("start", "s", Bezier, PseudoList), + LottieProp("end", "e", Bezier, PseudoList), + ] + + def __init__(self, time=0, start=None, end=None, easing_function=None): + Keyframe.__init__(self, time, easing_function) + ## Start value of keyframe segment. + self.start = start + ## End value of keyframe segment. + self.end = end + + def interpolated_value(self, ratio, next_start=None): + end = next_start if self.end is None else self.end + if end is None: + return self.start + if not self.in_value or not self.out_value: + return self.start + if ratio == 1: + return end + if ratio == 0 or len(self.start.vertices) != len(end.vertices): + return self.start + + lerpv = self.lerp_factor(ratio) + bez = Bezier() + bez.closed = self.start.closed + for i in range(len(self.start.vertices)): + bez.vertices.append(self.start.vertices[i].lerp(end.vertices[i], lerpv)) + bez.in_tangents.append(self.start.in_tangents[i].lerp(end.in_tangents[i], lerpv)) + bez.out_tangents.append(self.start.out_tangents[i].lerp(end.out_tangents[i], lerpv)) + return bez + + +## @ingroup Lottie +class ShapeProperty(AnimatableMixin, LottieObject): + """! + An animatable property that holds a Bezier + """ + keyframe_type = ShapePropKeyframe + _props = [ + LottieProp("value", "k", Bezier, False, prop_not_animated), + #LottieProp("expression", "x", str, False), + LottieProp("property_index", "ix", float, False), + LottieProp("animated", "a", PseudoBool, False), + LottieProp("keyframes", "k", keyframe_type, True, prop_animated), + ] + + def __init__(self, bezier=None): + super().__init__(bezier or Bezier()) diff --git a/lottie/objects/shapes.py b/lottie/objects/shapes.py new file mode 100644 index 0000000..a3b78ae --- /dev/null +++ b/lottie/objects/shapes.py @@ -0,0 +1,847 @@ +import math +from .base import LottieObject, LottieProp, LottieEnum, NVector +from .properties import Value, MultiDimensional, GradientColors, ShapeProperty, Bezier, ColorValue +from .color import Color +from .helpers import Transform + + +class BoundingBox: + """! + Shape bounding box + """ + def __init__(self, x1=None, y1=None, x2=None, y2=None): + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + + def include(self, x, y): + """! + Expands the box to include the point at x, y + """ + if x is not None: + if self.x1 is None or self.x1 > x: + self.x1 = x + if self.x2 is None or self.x2 < x: + self.x2 = x + if y is not None: + if self.y1 is None or self.y1 > y: + self.y1 = y + if self.y2 is None or self.y2 < y: + self.y2 = y + + def expand(self, other): + """! + Expands the bounding box to include another bounding box + """ + self.include(other.x1, other.y1) + self.include(other.x2, other.y2) + + def center(self): + """! + Center point of the bounding box + """ + return NVector((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2) + + def isnull(self): + """! + Whether the box is default-initialized + """ + return self.x1 is None or self.y2 is None + + def __repr__(self): + return "" % (self.x1, self.y1, self.x2, self.y2) + + @property + def width(self): + if self.isnull(): + return 0 + return self.x2 - self.x1 + + @property + def height(self): + if self.isnull(): + return 0 + return self.y2 - self.y1 + + def size(self): + return NVector(self.width, self.height) + + +## @ingroup Lottie +class ShapeElement(LottieObject): + """! + Base class for all elements of ShapeLayer and Group + """ + _props = [ + #LottieProp("match_name", "mn", str, False), + LottieProp("hidden", "hd", bool, False), + LottieProp("name", "nm", str, False), + LottieProp("type", "ty", str, False), + LottieProp("property_index", "cix", int, False), + LottieProp("bm", "bm", int, False), + ] + ## %Shape type. + type = None + _shape_classses = None + + def __init__(self): + # After Effect's Match Name. Used for expressions. + #self.match_name = "" + + ## After Effect's Name. Used for expressions. + self.name = None + ## Property index + self.property_index = None + ## Hide element + self.hidden = None + ## @todo figure out? + self.bm = None + + def bounding_box(self, time=0): + """! + Bounding box of the shape element at the given time + """ + return BoundingBox() + + @classmethod + def _load_get_class(cls, lottiedict): + if not ShapeElement._shape_classses: + ShapeElement._shape_classses = {} + ShapeElement._load_sub(ShapeElement._shape_classses) + return ShapeElement._shape_classses[lottiedict["ty"]] + + @classmethod + def _load_sub(cls, dict): + for sc in cls.__subclasses__(): + if sc.type: + dict[sc.type] = sc + sc._load_sub(dict) + + def __str__(self): + return self.name or super().__str__() + + +## @ingroup Lottie +class Shape(ShapeElement): + """! + Drawable shape + """ + _props = [ + LottieProp("direction", "d", float, False), + ] + + def __init__(self): + ShapeElement.__init__(self) + ## After Effect's Direction. Direction how the shape is drawn. Used for trim path for example. + self.direction = 1 + + def to_bezier(self): + """! + Returns a Path corresponding to this Shape + """ + raise NotImplementedError() + + +## @ingroup Lottie +class Rect(Shape): + """! + A simple rectangle shape + """ + _props = [ + LottieProp("position", "p", MultiDimensional, False), + LottieProp("size", "s", MultiDimensional, False), + LottieProp("rounded", "r", Value, False), + ] + ## %Shape type. + type = "rc" + + def __init__(self, pos=None, size=None, rounded=0): + Shape.__init__(self) + ## Rect's position + self.position = MultiDimensional(pos or NVector(0, 0)) + ## Rect's size + self.size = MultiDimensional(size or NVector(0, 0)) + ## Rect's rounded corners + self.rounded = Value(rounded) + + def bounding_box(self, time=0): + pos = self.position.get_value(time) + sz = self.size.get_value(time) + + return BoundingBox( + pos[0] - sz[0]/2, + pos[1] - sz[1]/2, + pos[0] + sz[0]/2, + pos[1] + sz[1]/2, + ) + + def to_bezier(self): + """! + Returns a Shape corresponding to this rect + """ + shape = Path() + kft = set() + if self.position.animated: + kft |= set(kf.time for kf in self.position.keyframes) + if self.size.animated: + kft |= set(kf.time for kf in self.size.keyframes) + if self.rounded.animated: + kft |= set(kf.time for kf in self.rounded.keyframes) + if not kft: + shape.shape.value = self._bezier_t(0) + else: + for time in sorted(kft): + shape.shape.add_keyframe(time, self._bezier_t(time)) + return shape + + def _bezier_t(self, time): + bezier = Bezier() + bb = self.bounding_box(time) + rounded = self.rounded.get_value(time) + tl = NVector(bb.x1, bb.y1) + tr = NVector(bb.x2, bb.y1) + br = NVector(bb.x2, bb.y2) + bl = NVector(bb.x1, bb.y2) + + if not self.rounded.animated and rounded == 0: + bezier.add_point(tl) + bezier.add_point(tr) + bezier.add_point(br) + bezier.add_point(bl) + else: + hh = NVector(rounded/2, 0) + vh = NVector(0, rounded/2) + hd = NVector(rounded, 0) + vd = NVector(0, rounded) + bezier.add_point(tl+vd, outp=-vh) + bezier.add_point(tl+hd, -hh) + bezier.add_point(tr-hd, outp=hh) + bezier.add_point(tr+vd, -vh) + bezier.add_point(br-vd, outp=vh) + bezier.add_point(br-hd, hh) + bezier.add_point(bl+hd, outp=-hh) + bezier.add_point(bl-vd, vh) + + bezier.close() + return bezier + + +## @ingroup Lottie +class StarType(LottieEnum): + Star = 1 + Polygon = 2 + + +## @ingroup Lottie +class Star(Shape): + """! + Star shape + """ + _props = [ + LottieProp("position", "p", MultiDimensional, False), + LottieProp("inner_radius", "ir", Value, False), + LottieProp("inner_roundness", "is", Value, False), + LottieProp("outer_radius", "or", Value, False), + LottieProp("outer_roundness", "os", Value, False), + LottieProp("rotation", "r", Value, False), + LottieProp("points", "pt", Value, False), + LottieProp("star_type", "sy", StarType, False), + ] + ## %Shape type. + type = "sr" + + def __init__(self): + Shape.__init__(self) + ## Star's position + self.position = MultiDimensional(NVector(0, 0)) + ## Star's inner radius. (Star only) + self.inner_radius = Value() + ## Star's inner roundness. (Star only) + self.inner_roundness = Value() + ## Star's outer radius. + self.outer_radius = Value() + ## Star's outer roundness. + self.outer_roundness = Value() + ## Star's rotation. + self.rotation = Value() + ## Star's number of points. + self.points = Value(5) + ## Star's type. Polygon or Star. + self.star_type = StarType.Star + + def bounding_box(self, time=0): + pos = self.position.get_value(time) + r = self.outer_radius.get_value(time) + + return BoundingBox( + pos[0] - r, + pos[1] - r, + pos[0] + r, + pos[1] + r, + ) + + def to_bezier(self): + """! + Returns a Shape corresponding to this star + """ + shape = Path() + kft = set() + if self.position.animated: + kft |= set(kf.time for kf in self.position.keyframes) + if self.inner_radius.animated: + kft |= set(kf.time for kf in self.inner_radius.keyframes) + if self.inner_roundness.animated: + kft |= set(kf.time for kf in self.inner_roundness.keyframes) + if self.points.animated: + kft |= set(kf.time for kf in self.points.keyframes) + if self.rotation.animated: + kft |= set(kf.time for kf in self.rotation.keyframes) + # TODO inner_roundness / outer_roundness + if not kft: + shape.shape.value = self._bezier_t(0) + else: + for time in sorted(kft): + shape.shape.add_keyframe(time, self._bezier_t(time)) + return shape + + def _bezier_t(self, time): + bezier = Bezier() + pos = self.position.get_value(time) + r1 = self.inner_radius.get_value(time) + r2 = self.outer_radius.get_value(time) + rot = -(self.rotation.get_value(time)) * math.pi / 180 + math.pi + p = self.points.get_value(time) + halfd = -math.pi / p + + for i in range(int(p)): + main_angle = rot + i * halfd * 2 + dx = r2 * math.sin(main_angle) + dy = r2 * math.cos(main_angle) + bezier.add_point(NVector(pos.x + dx, pos.y + dy)) + + if self.star_type == StarType.Star: + dx = r1 * math.sin(main_angle+halfd) + dy = r1 * math.cos(main_angle+halfd) + bezier.add_point(NVector(pos.x + dx, pos.y + dy)) + + bezier.close() + return bezier + + +## @ingroup Lottie +class Ellipse(Shape): + """! + Ellipse shape + """ + _props = [ + LottieProp("position", "p", MultiDimensional, False), + LottieProp("size", "s", MultiDimensional, False), + ] + ## %Shape type. + type = "el" + + def __init__(self, position=None, size=None): + Shape.__init__(self) + ## Ellipse's position + self.position = MultiDimensional(position or NVector(0, 0)) + ## Ellipse's size + self.size = MultiDimensional(size or NVector(0, 0)) + + def bounding_box(self, time=0): + pos = self.position.get_value(time) + sz = self.size.get_value(time) + + return BoundingBox( + pos[0] - sz[0]/2, + pos[1] - sz[1]/2, + pos[0] + sz[0]/2, + pos[1] + sz[1]/2, + ) + + def to_bezier(self): + """! + Returns a Shape corresponding to this ellipse + """ + shape = Path() + kft = set() + if self.position.animated: + kft |= set(kf.time for kf in self.position.keyframes) + if self.size.animated: + kft |= set(kf.time for kf in self.size.keyframes) + if not kft: + shape.shape.value = self._bezier_t(0) + else: + for time in sorted(kft): + shape.shape.add_keyframe(time, self._bezier_t(time)) + return shape + + def _bezier_t(self, time): + from ..utils.ellipse import Ellipse as EllipseConverter + + bezier = Bezier() + position = self.position.get_value(time) + radii = self.size.get_value(time) / 2 + + el = EllipseConverter(position, radii, 0) + points = el.to_bezier(0, math.pi*2) + for point in points[1:]: + bezier.add_point(point.vertex, point.in_tangent, point.out_tangent) + + bezier.close() + return bezier + + +## @ingroup Lottie +class Path(Shape): + """! + Animatable Bezier curve + """ + _props = [ + LottieProp("shape", "ks", ShapeProperty, False), + LottieProp("index", "ind", int, False), + ] + ## %Shape type. + type = "sh" + + def __init__(self, bezier=None): + Shape.__init__(self) + ## Shape's vertices + self.shape = ShapeProperty(bezier or Bezier()) + ## @todo Index? + self.index = None + + def bounding_box(self, time=0): + pos = self.shape.get_value(time) + + bb = BoundingBox() + for v in pos.vertices: + bb.include(*v) + + return bb + + def to_bezier(self): + return self.clone() + + +## @ingroup Lottie +class Group(ShapeElement): + """! + ShapeElement that can contain other shapes + @note Shapes inside the same group will create "holes" in other shapes + """ + _props = [ + LottieProp("number_of_properties", "np", float, False), + LottieProp("shapes", "it", ShapeElement, True), + ] + ## %Shape type. + type = "gr" + + def __init__(self): + ShapeElement.__init__(self) + ## Group number of properties. Used for expressions. + self.number_of_properties = None + ## Group list of items + self.shapes = [TransformShape()] + + @property + def transform(self): + return self.shapes[-1] + + def bounding_box(self, time=0): + bb = BoundingBox() + for v in self.shapes: + bb.expand(v.bounding_box(time)) + + if not bb.isnull(): + mat = self.transform.to_matrix(time) + points = [ + mat.apply(NVector(bb.x1, bb.y1)), + mat.apply(NVector(bb.x1, bb.y2)), + mat.apply(NVector(bb.x2, bb.y2)), + mat.apply(NVector(bb.x2, bb.y1)), + ] + x1 = min(p.x for p in points) + x2 = max(p.x for p in points) + y1 = min(p.y for p in points) + y2 = max(p.y for p in points) + return BoundingBox(x1, y1, x2, y2) + return bb + + def add_shape(self, shape): + self.shapes.insert(-1, shape) + return shape + + def insert_shape(self, index, shape): + self.shapes.insert(index, shape) + return shape + + @classmethod + def load(cls, lottiedict): + object = ShapeElement.load(lottiedict) + + shapes = [] + transform = None + for obj in object.shapes: + if isinstance(obj, TransformShape): + if not transform: + transform = obj + else: + shapes.append(obj) + + object.shapes = shapes + object.shapes.append(transform) + return object + + +## @ingroup Lottie +class FillRule(LottieEnum): + NonZero = 1 + EvenOdd = 2 + + +## @ingroup Lottie +class Fill(ShapeElement): + """! + Solid fill color + """ + _props = [ + LottieProp("opacity", "o", Value, False), + LottieProp("color", "c", ColorValue, False), + LottieProp("fill_rule", "r", FillRule, False), + ] + ## %Shape type. + type = "fl" + + def __init__(self, color=None): + ShapeElement.__init__(self) + ## Fill Opacity + self.opacity = Value(100) + ## Fill Color + self.color = ColorValue(color or Color(1, 1, 1)) + ## Fill rule + self.fill_rule = None + + +## @ingroup Lottie +class GradientType(LottieEnum): + Linear = 1 + Radial = 2 + + +## @ingroup Lottie +class Gradient(LottieObject): + _props = [ + LottieProp("start_point", "s", MultiDimensional, False), + LottieProp("end_point", "e", MultiDimensional, False), + LottieProp("gradient_type", "t", GradientType, False), + LottieProp("highlight_length", "h", Value, False), + LottieProp("highlight_angle", "a", Value, False), + LottieProp("colors", "g", GradientColors, False), + ] + + def __init__(self, colors=[]): + ## Fill Opacity + self.opacity = Value(100) + ## Gradient Start Point + self.start_point = MultiDimensional(NVector(0, 0)) + ## Gradient End Point + self.end_point = MultiDimensional(NVector(0, 0)) + ## Gradient Type + self.gradient_type = GradientType.Linear + ## Gradient Highlight Length. Only if type is Radial + self.highlight_length = Value() + ## Highlight Angle. Only if type is Radial + self.highlight_angle = Value() + ## Gradient Colors + self.colors = GradientColors(colors) + + +## @ingroup Lottie +class GradientFill(ShapeElement, Gradient): + """! + Gradient fill + """ + _props = [ + LottieProp("opacity", "o", Value, False), + LottieProp("fill_rule", "r", FillRule, False), + ] + ## %Shape type. + type = "gf" + + def __init__(self, colors=[]): + ShapeElement.__init__(self) + Gradient.__init__(self, colors) + ## Fill Opacity + self.opacity = Value(100) + ## Fill rule + self.fill_rule = None + + +## @ingroup Lottie +class LineJoin(LottieEnum): + Miter = 1 + Round = 2 + Bevel = 3 + + +## @ingroup Lottie +class LineCap(LottieEnum): + Butt = 1 + Round = 2 + Square = 3 + + +## @ingroup Lottie +class StrokeDashType(LottieEnum): + Dash = "d" + Gap = "g" + Offset = "o" + + +## @ingroup Lottie +class StrokeDash(LottieObject): + _props = [ + LottieProp("name", "nm", str, False), + LottieProp("type", "n", StrokeDashType, False), + LottieProp("length", "v", Value, False), + ] + + def __init__(self, length=0, type=StrokeDashType.Dash): + self.name = type.name.lower() + self.type = type + self.length = Value(length) + + def __str__(self): + return self.name or super().__str__() + + +## @ingroup Lottie +class BaseStroke(LottieObject): + _props = [ + LottieProp("line_cap", "lc", LineCap, False), + LottieProp("line_join", "lj", LineJoin, False), + LottieProp("miter_limit", "ml", float, False), + LottieProp("opacity", "o", Value, False), + LottieProp("width", "w", Value, False), + LottieProp("dashes", "d", StrokeDash, True), + ] + + def __init__(self, width=1): + ## Stroke Line Cap + self.line_cap = LineCap.Round + ## Stroke Line Join + self.line_join = LineJoin.Round + ## Stroke Miter Limit. Only if Line Join is set to Miter. + self.miter_limit = 0 + ## Stroke Opacity + self.opacity = Value(100) + ## Stroke Width + self.width = Value(width) + ## Dashes + self.dashes = None + + +## @ingroup Lottie +class Stroke(ShapeElement, BaseStroke): + """! + Solid stroke + """ + _props = [ + LottieProp("color", "c", MultiDimensional, False), + ] + ## %Shape type. + type = "st" + + def __init__(self, color=None, width=1): + ShapeElement.__init__(self) + BaseStroke.__init__(self, width) + ## Stroke Color + self.color = ColorValue(color or Color(0, 0, 0)) + + +## @ingroup Lottie +class GradientStroke(ShapeElement, BaseStroke, Gradient): + """! + Gradient stroke + """ + ## %Shape type. + type = "gs" + + def __init__(self, stroke_width=1): + ShapeElement.__init__(self) + BaseStroke.__init__(self, stroke_width) + Gradient.__init__(self) + + def bounding_box(self, time=0): + return BoundingBox() + + +## @ingroup Lottie +class TransformShape(ShapeElement, Transform): + """! + Group transform + """ + ## %Shape type. + type = "tr" + + def __init__(self): + ShapeElement.__init__(self) + Transform.__init__(self) + self.anchor_point = MultiDimensional(NVector(0, 0)) + + +## @ingroup Lottie +class Composite(LottieEnum): + Above = 1 + Below = 2 + + +## @ingroup Lottie +class RepeaterTransform(Transform): + _props = [ + LottieProp("start_opacity", "so", Value, False), + LottieProp("end_opacity", "eo", Value, False), + ] + + def __init__(self): + Transform.__init__(self) + self.start_opacity = Value(100) + self.end_opacity = Value(100) + + +## @ingroup Lottie +class Modifier(ShapeElement): + pass + + +## @ingroup Lottie +class TrimMultipleShapes(LottieEnum): + Simultaneously = 1 + Individually = 2 + + +## @ingroup Lottie +## @todo Implement SIF Export +class Trim(Modifier): + """ + Trims shapes into a segment + """ + _props = [ + LottieProp("start", "s", Value, False), + LottieProp("end", "e", Value, False), + LottieProp("offset", "o", Value, False), + LottieProp("multiple", "m", TrimMultipleShapes, False), + ] + ## %Shape type. + type = "tm" + + def __init__(self): + ShapeElement.__init__(self) + ## Start of the segment, as a percentage + self.start = Value(0) + ## End of the segment, as a percentage + self.end = Value(100) + ## start/end offset, as an angle (0, 360) + self.offset = Value(0) + ## @todo? + self.multiple = None + + +## @ingroup Lottie +class Repeater(Modifier): + """ + Duplicates previous shapes in a group + """ + _props = [ + LottieProp("copies", "c", Value, False), + LottieProp("offset", "o", Value, False), + LottieProp("composite", "m", Composite, False), + LottieProp("transform", "tr", RepeaterTransform, False), + ] + ## %Shape type. + type = "rp" + + def __init__(self, copies=1): + Modifier.__init__(self) + ## Number of Copies + self.copies = Value(copies) + ## Offset of Copies + self.offset = Value() + ## Composite of copies + self.composite = Composite.Above + ## Transform values for each repeater copy + self.transform = RepeaterTransform() + + +## @ingroup Lottie +## @todo Implement SIF Export +class RoundedCorners(Modifier): + """ + Rounds corners of other shapes + """ + _props = [ + LottieProp("radius", "r", Value, False), + ] + ## %Shape type. + type = "rd" + + def __init__(self): + Modifier.__init__(self) + ## Rounded Corner Radius + self.radius = Value() + + +## @ingroup Lottie +## @ingroup LottieCheck +## @note marked as unsupported by lottie +class Merge(ShapeElement): + _props = [ + LottieProp("merge_mode", "mm", float, False), + ] + ## %Shape type. + type = "mm" + + def __init__(self): + ShapeElement.__init__(self) + ## Merge Mode + self.merge_mode = 1 + + +## @ingroup Lottie +## @note marked as unsupported by lottie +class Twist(ShapeElement): + _props = [ + LottieProp("angle", "a", Value, False), + LottieProp("center", "c", MultiDimensional, False), + ] + ## %Shape type. + type = "tw" + + def __init__(self): + ShapeElement.__init__(self) + self.angle = Value(0) + self.center = MultiDimensional(NVector(0, 0)) + + + +class ZigZag(ShapeElement): + """ + Zig Zag shape modifier + """ + _props = [ + LottieProp("frequency", "r", Value, False), + LottieProp("amplitude", "s", Value, False), + LottieProp("point_type", "pt", Value, False), + ] + ## %Shape type. + type = "zz" + + def __init__(self): + ShapeElement.__init__(self) + ## Number of ridges per segment + self.frequency = Value(5) + ## Distance between peaks and troughs + self.amplitude = Value(10) + ## Point type (1 = corner, 2 = smooth) + self.point_type = Value(1) diff --git a/lottie/objects/text.py b/lottie/objects/text.py new file mode 100644 index 0000000..d91dc95 --- /dev/null +++ b/lottie/objects/text.py @@ -0,0 +1,211 @@ +from .base import LottieObject, LottieProp, LottieEnum +from .properties import Value, MultiDimensional +from .nvector import NVector +from .helpers import Transform + + +## @ingroup Lottie +## @ingroup LottieCheck +class MaskedPath(LottieObject): + _props = [ + LottieProp("mask", "m", float), + LottieProp("f", "f", Value), + LottieProp("l", "l", Value), + LottieProp("r", "r", float), + ] + + def __init__(self): + ## Type? + self.mask = None + ## First? + self.f = None + ## Last? + self.l = None + ## ?? + self.r = None + + +## @ingroup Lottie +## @ingroup LottieCheck +class TextAnimatorDataProperty(Transform): + _props = [ + LottieProp("rx", "rx", Value), + LottieProp("ry", "ry", Value), + LottieProp("stroke_width", "sw", Value), + LottieProp("stroke_color", "sc", MultiDimensional), + LottieProp("fill_color", "fc", MultiDimensional), + LottieProp("fh", "fh", Value), + LottieProp("fs", "fs", Value), + LottieProp("fb", "fb", Value), + LottieProp("tracking", "t", Value), + LottieProp("scale", "s", MultiDimensional), + ] + + def __init__(self): + super().__init__() + ## Angle? + self.rx = Value() + ## Angle? + self.ry = Value() + ## Stroke width + self.stroke_width = Value() + ## Stroke color + self.stroke_color = MultiDimensional() + ## Fill color + self.fill_color = MultiDimensional() + self.fh = Value() + ## 0-100? + self.fs = Value() + ## 0-100? + self.fb = Value() + ## Tracking + self.tracking = Value() + + +## @ingroup Lottie +## @ingroup LottieCheck +class TextMoreOptions(LottieObject): + _props = [ + LottieProp("alignment", "a", MultiDimensional), + LottieProp("g", "g", float), + ] + + def __init__(self): + self.alignment = MultiDimensional(NVector(0, 0)) + self.g = None + + +## @ingroup Lottie +class TextJustify(LottieEnum): + Left = 0 + Right = 1 + Center = 2 + + +## @ingroup Lottie +class TextDocument(LottieObject): + """! + @see http://docs.aenhancers.com/other/textdocument/ + + Note that for multi-line text, lines are separated by \\r + """ + _props = [ + LottieProp("font_family", "f", str), + LottieProp("color", "fc", NVector), + LottieProp("font_size", "s", float), + LottieProp("line_height", "lh", float), + LottieProp("wrap_size", "sz", NVector), + LottieProp("text", "t", str), + LottieProp("justify", "j", TextJustify), + # ls? + ] + + def __init__(self, text="", font_size=10, color=None, font_family=""): + self.font_family = font_family + ## Text color + self.color = color or NVector(0, 0, 0) + ## Line height when wrapping + self.line_height = None + ## Text alignment + self.justify = TextJustify.Left + ## Size of the box containing the text + self.wrap_size = None + ## Text + self.text = text + ## Font Size + self.font_size = font_size + + +## @ingroup Lottie +class TextDataKeyframe(LottieObject): + _props = [ + LottieProp("start", "s", TextDocument), + LottieProp("time", "t", float), + ] + + def __init__(self, time=0, start=None): + ## Start value of keyframe segment. + self.start = start + ## Start time of keyframe segment. + self.time = time + + +## @ingroup Lottie +class TextData(LottieObject): + _props = [ + LottieProp("keyframes", "k", TextDataKeyframe, True), + ] + + def __init__(self): + self.keyframes = [] + + def get_value(self, time): + for kf in self.keyframes: + if kf.time >= time: + return kf.start + return None + + +## @ingroup Lottie +class TextAnimatorData(LottieObject): + _props = [ + LottieProp("properties", "a", TextAnimatorDataProperty, True), + LottieProp("data", "d", TextData, False), + LottieProp("more_options", "m", TextMoreOptions, False), + LottieProp("masked_path", "p", MaskedPath), + ] + + def __init__(self): + self.properties = [] + self.data = TextData() + self.more_options = TextMoreOptions() + self.masked_path = MaskedPath() + + def add_keyframe(self, time, item): + self.data.keyframes.append(TextDataKeyframe(time, item)) + + def get_value(self, time): + return self.data.get_value(time) + + +## @ingroup Lottie +class FontPathOrigin(LottieEnum): + Unknown = 0 + CssUrl = 1 + ScriptUrl = 2 + FontUrl = 3 + + +## @ingroup Lottie +class Font(LottieObject): + _props = [ + LottieProp("ascent", "ascent", float), + LottieProp("font_family", "fFamily", str), + LottieProp("name", "fName", str), + LottieProp("font_style", "fStyle", str), + LottieProp("path", "fPath", str), + LottieProp("weight", "fWeight", str), + LottieProp("origin", "origin", FontPathOrigin), + ] + + def __init__(self, font_family="sans", font_style="Regular", name=None): + self.ascent = None + self.font_family = font_family + self.font_style = font_style + self.name = name or "%s-%s" % (font_family, font_style) + self.path = None + self.weight = None + self.origin = None + + +## @ingroup Lottie +class FontList(LottieObject): + _props = [ + LottieProp("list", "list", Font, True), + ] + + def __init__(self): + self.list = [] + + def append(self, font): + self.list.append(font) diff --git a/lottie/parsers/__init__.py b/lottie/parsers/__init__.py new file mode 100644 index 0000000..e693c9e --- /dev/null +++ b/lottie/parsers/__init__.py @@ -0,0 +1,2 @@ +from . import svg, tgs, sif +__all__ = ["svg", "tgs", "sif"] diff --git a/lottie/parsers/baseporter.py b/lottie/parsers/baseporter.py new file mode 100644 index 0000000..1ff7b32 --- /dev/null +++ b/lottie/parsers/baseporter.py @@ -0,0 +1,140 @@ +import sys +import os +import pkgutil +import argparse +import importlib + + +class Baseporter: + def __init__(self, name, extensions, callback, extra_options=[], generic_options=set(), slug=None): + self.name = name + self.extensions = extensions + self.callback = callback + self.extra_options = extra_options + self.generic_options = generic_options + self.slug = slug if slug is not None else extensions[0] + + def process(self, *a, **kw): + return self.callback(*a, **kw) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.slug) + + def argparse_options(self, ns): + o_options = {} + for opt in self.extra_options: + o_options[opt.dest] = getattr(ns, opt.nsvar(self.slug)) + for opt in self.generic_options: + o_options[opt] = getattr(ns, opt) + return o_options + + +class ExtraOption: + def __init__(self, name, **kwargs): + self.name = name + self.kwargs = kwargs + if "action" not in self.kwargs: + self.kwargs["metavar"] = self.name + self.dest = kwargs.pop("dest", name) + + def add_argument(self, slug, parser): + opt = "--%s-%s" % (slug, self.name.replace("_", "-")) + parser.add_argument(opt, dest=self.nsvar(slug), **self.kwargs) + + def nsvar(self, slug): + return "%s_%s" % (slug, self.dest) + + +def _add_options(parser, ie, object): + if not object.extra_options: + return + + suf = " %sing options" % ie + group = parser.add_argument_group(object.name + suf) + for op in object.extra_options: + op.add_argument(object.slug, group) + + +class Loader: + def __init__(self, module_path, module_name, ie): + self._loaded = False + self._registry = {} + self._module_path = os.path.dirname(module_path) + self._module_name = module_name.replace(".base", "") + self._ie = ie + self._failed = {} + + def load_modules(self): + self._loaded = True + + for _, modname, _ in pkgutil.iter_modules([self._module_path]): + if modname == "base": + continue + + full_modname = "." + modname + try: + importlib.import_module(full_modname, self._module_name) + except ImportError as e: + self._failed[modname] = e.name + + @property + def failed_modules(self): + if not self._loaded: + self.load_modules() + + return self._failed + + @property + def items(self): + if not self._loaded: + self.load_modules() + return self._registry + + def __iter__(self): + return iter(self.items.values()) + + def get(self, slug): + return self.items.get(slug, None) + + def __getitem__(self, key): + return self.get(key) + + def get_from_filename(self, filename): + return self.get_from_extension(os.path.splitext(filename)[1][1:]) + + def get_from_extension(self, ext): + for p in self.items.values(): + if ext in p.extensions: + return p + return None + + def set_options(self, parser): + for exporter in self.items.values(): + _add_options(parser, self._ie, exporter) + + def keys(self): + return self.items.keys() + + def decorator(self, name, extensions, extra_options=[], generic_options=set(), slug=None): + def decorator(callback): + porter = Baseporter(name, extensions, callback, extra_options, generic_options, slug) + self._registry[porter.slug] = porter + return callback + return decorator + + +class IoProgressReporter: + def report_progress(self, title, value, total): + sys.stderr.write("\r%s %s/%s" % (title, value, total)) + sys.stderr.flush() + + def report_message(self, message): + sys.stderr.write("\r" + message + "\n") + sys.stderr.flush() + + +IoProgressReporter.instance = IoProgressReporter() + + +def io_progress(): + return IoProgressReporter.instance diff --git a/lottie/parsers/pixel.py b/lottie/parsers/pixel.py new file mode 100644 index 0000000..cdbdde9 --- /dev/null +++ b/lottie/parsers/pixel.py @@ -0,0 +1,275 @@ +from PIL import Image +from .. import objects +from .. import NVector, Color +from ..utils import color + + +class Polygen: + def __init__(self, x, y): + self.vertices = [ + NVector(x, y), + NVector(x+1, y), + NVector(x+1, y+1), + NVector(x, y+1), + ] + self._has_x = False + self._has_y = False + + def add_pixel_x(self, x, y): + i = self.vertices.index(NVector(x, y)) + if len(self.vertices) > i and self.vertices[i+1] == NVector(x, y+1): + self._has_x = True + self.vertices.insert(i+1, NVector(x+1, y)) + self.vertices.insert(i+2, NVector(x+1, y+1)) + else: + raise ValueError() + + def add_pixel_x_neg(self, x, y): + i = self.vertices.index(NVector(x+1, y)) + if i > 0 and self.vertices[i-1] == NVector(x+1, y+1): + self._has_x = True + self.vertices.insert(i, NVector(x, y)) + self.vertices.insert(i, NVector(x, y+1)) + else: + raise ValueError() + + def add_pixel_y(self, x, y): + i = self.vertices.index(NVector(x, y)) + if i > 0 and self.vertices[i-1] == NVector(x+1, y): + self._has_y = True + if i > 1 and self.vertices[i-2] == NVector(x+1, y+1): + self.vertices[i-1] = NVector(x, y+1) + else: + self.vertices.insert(i, NVector(x, y+1)) + self.vertices.insert(i, NVector(x+1, y+1)) + else: + raise ValueError() + + def _to_rect(self, id1, id2): + p1 = self.vertices[id1] + p2 = self.vertices[id2] + return objects.Rect((p1+p2)/2, p2-p1) + + def to_shape(self): + if not self._has_x or not self._has_y: + return self._to_rect(0, int(len(self.vertices)/2)) + bez = objects.Bezier() + bez.closed = True + for point in self.vertices: + if len(bez.vertices) > 1 and ( + bez.vertices[-1].x == bez.vertices[-2].x == point.x or + bez.vertices[-1].y == bez.vertices[-2].y == point.y + ): + bez.vertices[-1] = point + else: + bez.add_point(point) + + if len(bez.vertices) > 2 and bez.vertices[0].x == bez.vertices[-1].x == bez.vertices[-2].x: + bez.vertices.pop() + bez.out_tangents.pop() + bez.in_tangents.pop() + return objects.Path(bez) + + +def pixel_add_layer_paths(animation, raster): + layer = animation.add_layer(objects.ShapeLayer()) + groups = {} + processed = set() + xneg_candidates = set() + + def avail(x, y): + rid = (x, y) + return not ( + x < 0 or x >= raster.width or y >= raster.height or + rid in processed or raster.getpixel(rid) != colort + ) + + def recurse(gen, x, y, xneg): + processed.add((x, y)) + if avail(x+1, y): + gen.add_pixel_x(x+1, y) + recurse(gen, x+1, y, False) + if avail(x, y+1): + gen.add_pixel_y(x, y+1) + recurse(gen, x, y+1, True) + if xneg and avail(x-1, y): + xneg_candidates.add((x-1, y)) + + for y in range(raster.height): + for x in range(raster.width): + pid = (x, y) + colort = raster.getpixel(pid) + if colort[-1] == 0 or pid in processed: + continue + + gen = Polygen(x, y) + xneg_candidates = set() + recurse(gen, x, y, False) + xneg_candidates -= processed + while xneg_candidates: + p = next(iter(sorted(xneg_candidates, key=lambda t: (t[1], t[0])))) + gen.add_pixel_x_neg(*p) + recurse(gen, p[0], p[1], True) + processed.add(p) + xneg_candidates -= processed + + g = groups.setdefault(colort, set()) + g.add(gen.to_shape()) + + for colort, rects in groups.items(): + g = layer.add_shape(objects.Group()) + g.shapes = list(rects) + g.shapes + g.name = "".join("%02x" % c for c in colort) + fill = g.add_shape(objects.Fill()) + fill.color.value = color.from_uint8(*colort[:3]) + fill.opacity.value = colort[-1] / 255 * 100 + stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1)) + stroke.opacity.value = fill.opacity.value + return layer + + +def pixel_add_layer_rects(animation, raster): + layer = animation.add_layer(objects.ShapeLayer()) + last_rects = {} + groups = {} + + def merge_up(): + if last_rect and last_rect._start in last_rects: + yrect = last_rects[last_rect._start] + if yrect.size.value.x == last_rect.size.value.x and yrect._color == last_rect._color: + groups[last_rect._color].remove(last_rect) + yrect.position.value.y += 0.5 + yrect.size.value.y += 1 + rects[last_rect._start] = yrect + + def group(colort): + return groups.setdefault(colort, set()) + + for y in range(raster.height): + rects = {} + last_color = None + last_rect = None + for x in range(raster.width): + colort = raster.getpixel((x, y)) + if colort[-1] == 0: + continue + yrect = last_rects.get(x, None) + if colort == last_color: + last_rect.position.value.x += 0.5 + last_rect.size.value.x += 1 + elif yrect and colort == yrect._color and yrect.size.value.x == 1: + yrect.position.value.y += 0.5 + yrect.size.value.y += 1 + rects[x] = yrect + last_color = last_rect = colort = None + else: + merge_up() + g = group(colort) + last_rect = objects.Rect() + g.add(last_rect) + last_rect.size.value = NVector(1, 1) + last_rect.position.value = NVector(x + 0.5, y + 0.5) + rects[x] = last_rect + last_rect._start = x + last_rect._color = colort + last_color = colort + merge_up() + last_rects = rects + + for colort, rects in groups.items(): + g = layer.add_shape(objects.Group()) + g.shapes = list(rects) + g.shapes + g.name = "".join("%02x" % c for c in colort) + fill = g.add_shape(objects.Fill()) + fill.color.value = color.from_uint8(*colort[:3]) + fill.opacity.value = colort[-1] / 255 * 100 + stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1)) + stroke.opacity.value = fill.opacity.value + return layer + + +def _vectorizing_func(filenames, frame_delay, framerate, callback): + if not isinstance(filenames, list): + filenames = [filenames] + + animation = objects.Animation(0, framerate) + nframes = 0 + + for filename in filenames: + raster = Image.open(filename) + if nframes == 0: + animation.width = raster.width + animation.height = raster.height + if not hasattr(raster, "is_animated"): + raster.n_frames = 1 + raster.seek = lambda x: None + for frame in range(raster.n_frames): + raster.seek(frame) + new_im = Image.new("RGBA", raster.size) + new_im.paste(raster) + callback(animation, new_im, nframes + frame) + new_im.close() + nframes += raster.n_frames + + animation.out_point = frame_delay * nframes + #animation._nframes = nframes + + return animation + + +def raster_to_embedded_assets(filenames, frame_delay=1, framerate=60, embed_format=None): + """! + @brief Loads external assets + """ + def callback(animation, raster, frame): + asset = objects.assets.Image.embedded(raster, embed_format) + animation.assets.append(asset) + layer = animation.add_layer(objects.ImageLayer(asset.id)) + layer.in_point = frame * frame_delay + layer.out_point = layer.in_point + frame_delay + + return _vectorizing_func(filenames, frame_delay, framerate, callback) + + +def raster_to_linked_assets(filenames, frame_delay=1, framerate=60): + """! + @brief Loads external assets + """ + animation = objects.Animation(frame_delay * len(filenames), framerate) + + for frame, filename in enumerate(filenames): + asset = objects.assets.Image.linked(filename) + animation.assets.append(asset) + layer = animation.add_layer(objects.ImageLayer(asset.id)) + layer.in_point = frame * frame_delay + layer.out_point = layer.in_point + frame_delay + + return animation + + +def pixel_to_animation(filenames, frame_delay=1, framerate=60): + """! + @brief Converts pixel art to vector + """ + def callback(animation, raster, frame): + layer = pixel_add_layer_rects(animation, raster.convert("RGBA")) + layer.in_point = frame * frame_delay + layer.out_point = layer.in_point + frame_delay + + return _vectorizing_func(filenames, frame_delay, framerate, callback) + + +def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60): + """! + @brief Converts pixel art to vector paths + + Slower and yields larger files compared to pixel_to_animation, + but it produces a single shape for each area with the same color. + Mostly useful when you want to add your own animations to the loaded image + """ + def callback(animation, raster, frame): + layer = pixel_add_layer_paths(animation, raster.convert("RGBA")) + layer.in_point = frame * frame_delay + layer.out_point = layer.in_point + frame_delay + + return _vectorizing_func(filenames, frame_delay, framerate, callback) diff --git a/lottie/parsers/raster.py b/lottie/parsers/raster.py new file mode 100644 index 0000000..9310b04 --- /dev/null +++ b/lottie/parsers/raster.py @@ -0,0 +1,248 @@ +# NOTE: requires pillow, pypotrace>=0.2, numpy, scipy to be installed +from PIL import Image +import potrace +import numpy +import enum +from scipy.cluster.vq import kmeans +from .. import objects +from ..nvector import NVector +from .pixel import _vectorizing_func + + +class QuanzationMode(enum.Enum): + Nearest = 1 + Exact = 2 + + +class RasterImage: + def __init__(self, data): + self.data = data + + @classmethod + def from_pil(cls, image): + return cls(numpy.array(image)) + + #@classmethod + #def open(cls, filename): + #return cls.from_pil(Image.open(filename)) + + def k_means(self, n_colors): + """! + Returns a list of centroids + """ + colors = [] + for row in range(self.data.shape[0]): + for column in range(self.data.shape[1]): + if self.get_alpha(row, column) == 255: + colors.append(self.data[row][column]) + + colors = numpy.array(colors, numpy.float) + return kmeans(colors, n_colors+1)[0] + + def get_alpha(self, row, column): + if self.data.shape[2] >= 4: + return self.data[row][column][3] + return 255 + + def quantize(self, codebook, quantization_mode=QuanzationMode.Nearest): + """! + Returns a list of tuple [color, data] where for each color in codebook + data is a bit mask for the image + + You can get codebook from k_means + """ + if codebook is None or len(codebook) == 0: + return [(numpy.array([0., 0., 0., 255.]), self.mono())] + + mono_data = [] + for c in codebook: + mono_data.append((c, numpy.zeros(self.data.shape[:2]))) + + for row in range(self.data.shape[0]): + for column in range(self.data.shape[1]): + if self.get_alpha(row, column) == 255: + if quantization_mode == QuanzationMode.Nearest: + min_norm = 511 # (norm of [255, 255, 255, 255]) + 1 + best = None + for color, bitmap in mono_data: + norm = numpy.linalg.norm(self.data[row][column] - color) + if norm < min_norm: + min_norm = norm + best = bitmap + if norm == 0: + break + best[row][column] = 1 + else: + for color, bitmap in mono_data: + if numpy.array_equal(color, self.data[row][column]): + bitmap[row][column] = 1 + break + + return mono_data + + def mono(self): + """! + Returns a bit mask of opaque pixels + """ + mono_data = numpy.zeros(self.data.shape[:2]) + for row in range(self.data.shape[0]): + for column in range(self.data.shape[1]): + mono_data[row][column] = int(self.data[row][column][3] == 255) + return mono_data + + +class Vectorizer: + def __init__(self): + self.palette = None + self.layers = {} + + def _create_layer(self, animation, layer_name): + layer = animation.add_layer(objects.ShapeLayer()) + if layer_name: + self.layers[layer_name] = layer + layer.name = layer_name + return layer + + def prepare_layer(self, animation, layer_name=None): + layer = self._create_layer(animation, layer_name) + layer._max_verts = {} + if self.palette is None: + group = layer.add_shape(objects.Group()) + group.name = "bitmap" + layer._max_verts[group.name] = 0 + group.add_shape(objects.Path()) + group.add_shape(objects.Fill(NVector(0, 0, 0))) + else: + for color in self.palette: + group = layer.add_shape(objects.Group()) + group.name = "color_%s" % "".join("%02x" % int(c) for c in color) + layer._max_verts[group.name] = 0 + fcol = color/255 + fill = group.add_shape(objects.Fill(NVector(*fcol))) + if len(fcol) > 3 and fcol[3] < 1: + fill.opacity.value = fcol[3] * 100 + return layer + + def raster_to_layer(self, animation, raster, layer_name=None, mode=QuanzationMode.Nearest): + layer = self.prepare_layer(animation, layer_name) + mono_data = raster.quantize(self.palette, mode) + for (color, bitmap), group in zip(mono_data, layer.shapes): + self.raster_to_shapes(group, bitmap) + return layer + + def raster_to_shapes(self, group, mono_data): + shapes = [] + for bezier in self.raster_to_bezier(mono_data): + shape = group.insert_shape(0, objects.Path()) + shapes.append(shape) + shape.shape.value = bezier + return shapes + + def raster_to_bezier(self, mono_data): + bmp = potrace.Bitmap(mono_data) + path = bmp.trace() + shapes = [] + for curve in path: + bezier = objects.Bezier() + shapes.append(bezier) + bezier.add_point(NVector(*curve.start_point)) + for segment in curve: + if segment.is_corner: + bezier.add_point(NVector(*segment.c)) + bezier.add_point(NVector(*segment.end_point)) + else: + sp = NVector(*bezier.vertices[-1]) + ep = NVector(*segment.end_point) + c1 = NVector(*segment.c1) - sp + c2 = NVector(*segment.c2) - ep + bezier.out_tangents[-1] = c1 + bezier.add_point(ep, c2) + return shapes + + def _frame_keyframe(self, layer, group, time, shapes, beziers): + if shapes: + # TODO handle multiple shapes + nverts = len(beziers[0].vertices) + if nverts > layer._max_verts[group.name]: + layer._max_verts[group.name] = nverts + for shape, bezier in zip(shapes, beziers): + shape.shape.add_keyframe(time, bezier) + + def raster_to_frame(self, animation, raster, layer_name, time, mode=QuanzationMode.Nearest): + mono_data = raster.quantize(self.palette, mode) + + if layer_name not in self.layers: + layer = self.prepare_layer(animation, layer_name) + for (color, bitmap), group in zip(mono_data, layer.shapes): + shapes = self.raster_to_shapes(group, bitmap) + beziers = [s.shape.value for s in shapes] + self._frame_keyframe(layer, group, time, shapes, beziers) + else: + layer = self.layers[layer_name] + + for (color, bitmap), group in zip(mono_data, layer.shapes): + shapes = [s for s in group.shapes if isinstance(s, objects.Path)] + beziers = self.raster_to_bezier(bitmap) + self._frame_keyframe(layer, group, time, shapes, beziers) + + def adjust_missing_vertices(self, layer_name): + layer = self.layers[layer_name] + for group in layer.shapes: + # TODO handle multiple shapes + shape = group.shapes[0] + nverts = layer._max_verts[group.name] + if shape.shape.animated: + for kf in shape.shape.keyframes: + bezier = kf.start + count = nverts - len(bezier.vertices) + bezier.vertices += [bezier.vertices[-1]] * count + bezier.in_tangents += [NVector(0, 0)] * count + bezier.out_tangents += [NVector(0, 0)] * count + + def duplicate_start_frame(self, layer_name, time): + layer = self.layers[layer_name] + for group in layer.shapes: + shape = group.shapes[0] + bezier = shape.shape.keyframes[0].start + group.shapes[0].shape.add_keyframe(time, bezier) + + +def color2numpy(vcolor): + l = (vcolor * 255).components + if len(l) == 3: + l.append(255) + return numpy.array(l, numpy.uint8) + + +def raster_to_animation(filenames, n_colors=1, frame_delay=1, + looping=True, framerate=60, palette=[], + mode=QuanzationMode.Nearest): + + vc = Vectorizer() + + def callback(animation, raster, frame): + raster = RasterImage.from_pil(raster) + if vc.palette is None: + if palette: + vc.palette = [color2numpy(c) for c in palette] + elif n_colors > 1: + vc.palette = raster.k_means(n_colors) + #vc.raster_to_frame(animation, raster, "anim", frame * frame_delay, mode) + layer = vc.raster_to_layer(animation, raster, "frame_%s" % frame, mode) + layer.in_point = frame * frame_delay + layer.out_point = (frame + 1) * frame_delay + + animation = _vectorizing_func(filenames, frame_delay, framerate, callback) + + #vc.adjust_missing_vertices("anim") + #if looping and animation._nframes > 1: + #animation.out_point += frame_delay + #vc.duplicate_start_frame("anim", animation.out_point) + #elif animation._nframes == 1: + #for g in animation.find("anim").shapes: + #for shape in g.find_all(objects.Path): + #shape.shape.clear_animation(shape.shape.get_value(0)) + + #animation.find("anim").out_point = animation.out_point + + return animation diff --git a/lottie/parsers/sif/__init__.py b/lottie/parsers/sif/__init__.py new file mode 100644 index 0000000..1973c60 --- /dev/null +++ b/lottie/parsers/sif/__init__.py @@ -0,0 +1,5 @@ +from . import builder, importer +from .importer import parse_sif_file +from .builder import to_sif + +__all__ = ["builder", "importer", "parse_sif_file", "to_sif"] diff --git a/lottie/parsers/sif/__pycache__/__init__.cpython-310.pyc b/lottie/parsers/sif/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..75bba37 Binary files /dev/null and b/lottie/parsers/sif/__pycache__/__init__.cpython-310.pyc differ diff --git a/lottie/parsers/sif/__pycache__/api.cpython-310.pyc b/lottie/parsers/sif/__pycache__/api.cpython-310.pyc new file mode 100644 index 0000000..e591c3b Binary files /dev/null and b/lottie/parsers/sif/__pycache__/api.cpython-310.pyc differ diff --git a/lottie/parsers/sif/__pycache__/ast.cpython-310.pyc b/lottie/parsers/sif/__pycache__/ast.cpython-310.pyc new file mode 100644 index 0000000..b4a05bf Binary files /dev/null and b/lottie/parsers/sif/__pycache__/ast.cpython-310.pyc differ diff --git a/lottie/parsers/sif/__pycache__/builder.cpython-310.pyc b/lottie/parsers/sif/__pycache__/builder.cpython-310.pyc new file mode 100644 index 0000000..3ae1866 Binary files /dev/null and b/lottie/parsers/sif/__pycache__/builder.cpython-310.pyc differ diff --git a/lottie/parsers/sif/__pycache__/importer.cpython-310.pyc b/lottie/parsers/sif/__pycache__/importer.cpython-310.pyc new file mode 100644 index 0000000..6708ae2 Binary files /dev/null and b/lottie/parsers/sif/__pycache__/importer.cpython-310.pyc differ diff --git a/lottie/parsers/sif/api.py b/lottie/parsers/sif/api.py new file mode 100644 index 0000000..0388288 --- /dev/null +++ b/lottie/parsers/sif/api.py @@ -0,0 +1,3 @@ +from .sif.nodes import * +from . import ast +from .ast_impl.base import SifKeyframe, Interpolation diff --git a/lottie/parsers/sif/ast.py b/lottie/parsers/sif/ast.py new file mode 100644 index 0000000..feecb15 --- /dev/null +++ b/lottie/parsers/sif/ast.py @@ -0,0 +1,2 @@ +from .ast_impl.nodes import * +from .ast_impl.base import * diff --git a/lottie/parsers/sif/ast_impl/__init__.py b/lottie/parsers/sif/ast_impl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lottie/parsers/sif/ast_impl/__pycache__/__init__.cpython-310.pyc b/lottie/parsers/sif/ast_impl/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7e9afbb Binary files /dev/null and b/lottie/parsers/sif/ast_impl/__pycache__/__init__.cpython-310.pyc differ diff --git a/lottie/parsers/sif/ast_impl/__pycache__/base.cpython-310.pyc b/lottie/parsers/sif/ast_impl/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..5d0deaf Binary files /dev/null and b/lottie/parsers/sif/ast_impl/__pycache__/base.cpython-310.pyc differ diff --git a/lottie/parsers/sif/ast_impl/__pycache__/nodes.cpython-310.pyc b/lottie/parsers/sif/ast_impl/__pycache__/nodes.cpython-310.pyc new file mode 100644 index 0000000..26d1ed4 Binary files /dev/null and b/lottie/parsers/sif/ast_impl/__pycache__/nodes.cpython-310.pyc differ diff --git a/lottie/parsers/sif/ast_impl/base.py b/lottie/parsers/sif/ast_impl/base.py new file mode 100644 index 0000000..a4da03e --- /dev/null +++ b/lottie/parsers/sif/ast_impl/base.py @@ -0,0 +1,120 @@ +from xml.dom import minidom +import enum +from lottie.parsers.sif.sif.core import TypeDescriptor, ObjectRegistry, SifNodeMeta, FrameTime +from lottie.parsers.sif.xml.utils import xml_child_elements, xml_first_element_child + + +class SifAstNode: + _subclasses = None + _tag = None + + @staticmethod + def from_dom(xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry): + if xml.tagName == param.typename: + return SifValue.from_dom(xml, param, registry) + + xmltype = xml.getAttribute("type") + if xmltype != param.typename and xmltype != "weighted_" + param.typename: + raise ValueError("Invalid type %s (should be %s)" % (xmltype, param.typename)) + + return SifAstNode.ast_node_types()[xml.tagName].from_dom(xml, param, registry) + + @staticmethod + def ast_node_types(): + if SifAstNode._subclasses is None: + from . import nodes + SifAstNode._subclasses = {} + SifAstNode._gather_ast_types(SifAstNode) + return SifAstNode._subclasses + + @staticmethod + def _gather_ast_types(cls): + for subcls in cls.__subclasses__(): + if subcls._tag: + SifAstNode._subclasses[subcls._tag] = subcls + SifAstNode._gather_ast_types(subcls) + + def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor): + element = dom.createElement(self._tag) + element.setAttribute("type", param.typename) + return element + + +class SifValue(SifAstNode): + def __init__(self, value=None): + self.value = value + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.value) + + @classmethod + def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry): + return SifValue(param.value_from_xml_element(xml, registry)) + + def to_dom(self, dom: minidom.Document, param: TypeDescriptor): + return param.value_to_xml_element(self.value, dom) + + +class Interpolation(enum.Enum): + Auto = "auto" + Linear = "linear" + Clamped = "clamped" + Ease = "halt" + Constant = "constant" + + +class SifKeyframe: + def __init__(self, value, time: FrameTime, before=Interpolation.Clamped, after=Interpolation.Clamped): + self.value = value + self.time = time + self.before = before + self.after = after + + @classmethod + def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry): + return cls( + param.value_from_xml_element(xml_first_element_child(xml), registry), + FrameTime.parse_string(xml.getAttribute("time"), registry), + Interpolation(xml.getAttribute("before")), + Interpolation(xml.getAttribute("after")) + ) + + def to_dom(self, dom: minidom.Document, param: TypeDescriptor): + element = dom.createElement("waypoint") + element.setAttribute("time", str(self.time)) + element.setAttribute("before", self.before.value) + element.setAttribute("after", self.after.value) + element.appendChild(param.value_to_xml_element(self.value, dom)) + return element + + def __repr__(self): + return "" % (self.time, self.value) + + +class SifAnimated(SifAstNode): + _tag = "animated" + + def __init__(self): + self.keyframes = [] + + @classmethod + def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry): + obj = SifAnimated() + for waypoint in xml_child_elements(xml, "waypoint"): + obj.keyframes.append(SifKeyframe.from_dom(waypoint, param, registry)) + return obj + + def to_dom(self, dom: minidom.Document, param: TypeDescriptor): + element = self._prepare_to_dom(dom, param) + for kf in self.keyframes: + element.appendChild(kf.to_dom(dom, param)) + return element + + def add_keyframe(self, *args, **kwargs): + if not kwargs and len(args) == 1 and isinstance(args[0], SifKeyframe): + keyframe = args[0] + else: + keyframe = SifKeyframe(*args, **kwargs) + + self.keyframes.append(keyframe) + return keyframe diff --git a/lottie/parsers/sif/ast_impl/nodes.py b/lottie/parsers/sif/ast_impl/nodes.py new file mode 100644 index 0000000..928af47 --- /dev/null +++ b/lottie/parsers/sif/ast_impl/nodes.py @@ -0,0 +1,321 @@ +from xml.dom import minidom +import enum + +from lottie.nvector import NVector +from lottie.parsers.sif.ast_impl.base import SifAstNode, TypeDescriptor, ObjectRegistry +from lottie.parsers.sif.xml.animatable import XmlAnimatable +from lottie.parsers.sif.xml.wrappers import XmlBoneReference, XmlSifElement, XmlList +from lottie.parsers.sif.sif.nodes import Segment, WeightedVector, Bline +from lottie.parsers.sif.sif.enums import Smooth +from lottie.parsers.sif.sif.core import SifNodeMeta, FrameTime + + +class SifAstComplex(SifAstNode, metaclass=SifNodeMeta): + _nodes = [] + + def __init__(self, **kw): + for node in self._nodes: + node.initialize_object(kw, self) + + @classmethod + def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry): + outcls = cls.get_class_from_dom(xml, param, registry) + instance = outcls() + for node in outcls._nodes: + node.from_xml(instance, xml, registry) + return instance + + @classmethod + def get_class_from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry): + return cls + + def to_dom(self, dom: minidom.Document, param: TypeDescriptor): + element = self._prepare_to_dom(dom, param) + for node in self._nodes: + node.to_xml(self, element, dom, param) + return element + + +class SifAstBoneLink(SifAstComplex): + _tag = "bone_link" + + _nodes = [ + XmlBoneReference("bone"), + XmlAnimatable("base_value", "vector", NVector(0, 0)), + XmlAnimatable("translate", "bool", True), + XmlAnimatable("rotate", "bool", True), + XmlAnimatable("skew", "bool", True), + XmlAnimatable("scale_x", "bool", True), + XmlAnimatable("scale_y", "bool", True), + ] + + +class SifAstBoneInfluence(SifAstComplex): + _tag = "boneinfluence" + + _nodes = [ + # TODO bone_weight_list + XmlAnimatable("link", "vector", NVector(0, 0)), + ] + + +class SifSegCalcTangent(SifAstComplex): + _tag = "segcalctangent" + + _nodes = [ + XmlSifElement("segment", Segment), + XmlAnimatable("amount", "real", .5), + ] + + +class SifSegCalcVertex(SifAstComplex): + _tag = "segcalcvertex" + + _nodes = [ + XmlSifElement("segment", Segment), + XmlAnimatable("amount", "real", .5), + ] + + +class WeightedAverage(SifAstComplex): + _tag = "weighted_average" + + _nodes = [ + XmlList(WeightedVector, "vectors", "entry"), + ] + + def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor): + element = dom.createElement(self._tag) + element.setAttribute("type", "weighted_vector") + return element + + +class SifAdd(SifAstComplex): + _tag = "add" + + _nodes = [ + XmlAnimatable("lhs", "_recurse"), + XmlAnimatable("rhs", "_recurse"), + XmlAnimatable("scalar", "real", 1.), + ] + + +class SifAnimatedFile(SifAstComplex): + _tag = "animated_file" + + _nodes = [ + XmlAnimatable("filename", "string"), + ] + + +class Accuracy(enum.Enum): + Rough = 0 + Normal = 1 + Fine = 2 + Extreme = 3 + + +class DerivativeOrder: + FirstDerivative = 0 + SecondDerivative = 1 + + +class SifDerivative(SifAstComplex): + _tag = "derivative" + + _nodes = [ + XmlAnimatable("link", "_recurse"), + XmlAnimatable("interval", "real", 0.01), + XmlAnimatable("accuracy", "integer", Accuracy.Normal, Accuracy), + XmlAnimatable("order", "integer", DerivativeOrder.FirstDerivative, DerivativeOrder), + ] + + +class SifDynamic(SifAstComplex): + _tag = "dynamic" + + _nodes = [ + XmlAnimatable("tip_static", "vector", NVector(0, 0)), + XmlAnimatable("origin", "vector", NVector(0, 0)), + XmlAnimatable("force", "vector", NVector(0, 0)), + XmlAnimatable("torque", "real", 0.), + XmlAnimatable("damping", "real", 0.4), + XmlAnimatable("friction", "real", 0.4), + XmlAnimatable("spring", "real", 30.), + XmlAnimatable("torsion", "real", 30.), + XmlAnimatable("mass", "real", 0.3), + XmlAnimatable("inertia", "real", 0.3), + XmlAnimatable("spring_rigid", "bool", False), + XmlAnimatable("torsion_rigid", "bool", False), + XmlAnimatable("origin_drags_tip", "bool", True), + ] + + +class SifGreyed(SifAstComplex): + _tag = "greyed" + + _nodes = [ + XmlAnimatable("link", "_recurse"), + ] + + +class SifLinear(SifAstComplex): + _tag = "linear" + + _nodes = [ + XmlAnimatable("slope", "vector", NVector(0, 0)), + XmlAnimatable("offset", "vector", NVector(0, 0)), + ] + + +class SifRadialComposite(SifAstComplex): + _tag = "radial_composite" + + _nodes = [ + XmlAnimatable("radius", "real", 0.), + XmlAnimatable("theta", "angle", 0.), + ] + + +class SifComposite(SifAstComplex): + _tag = "composite" + + @classmethod + def get_class_from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry): + type = xml.getAttribute("type") + if type == "vector": + return SifVectorComposite + return None + + def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor): + element = dom.createElement("composite") + element.setAttribute("type", param.typename) + return element + + +class SifVectorComposite(SifComposite): + _type = "vector" + + _nodes = [ + XmlAnimatable("x", "real", 0.), + XmlAnimatable("y", "real", 0.), + ] + + +class SifRandom(SifAstComplex): + _tag = "random" + + _nodes = [ + XmlAnimatable("link", "_recurse"), + XmlAnimatable("radius", "real", 0.), + XmlAnimatable("seed", "integer", 0), + XmlAnimatable("speed", "real", 1.), + XmlAnimatable("smooth", "integer", Smooth.Cubic, Smooth), + XmlAnimatable("loop", "real", 0.), + ] + + +class SifReference(SifAstComplex): + _tag = "link" + + _nodes = [ + XmlAnimatable("reference", "_recurse"), + ] + + +class SifScale(SifAstComplex): + _tag = "scale" + + _nodes = [ + XmlAnimatable("link", "_recurse"), + XmlAnimatable("scalar", "real", 1.), + ] + + +class SifStep(SifAstComplex): + _tag = "step" + + _nodes = [ + XmlAnimatable("link", "_recurse"), + XmlAnimatable("duration", "time", FrameTime(1, FrameTime.Unit.Seconds)), + XmlAnimatable("start_time", "time", FrameTime(0, FrameTime.Unit.Seconds)), + XmlAnimatable("intersection", "real", 0.5), + ] + + +class SifSubtract(SifAstComplex): + _tag = "subtract" + + _nodes = [ + XmlAnimatable("lhs", "_recurse"), + XmlAnimatable("rhs", "_recurse"), + XmlAnimatable("scalar", "real", 1.), + ] + + +class SifSwitch(SifAstComplex): + _tag = "switch" + + _nodes = [ + XmlAnimatable("link_off", "_recurse"), + XmlAnimatable("link_on", "_recurse"), + XmlAnimatable("switch", "bool", False), + ] + + +class SifTimedSwap(SifAstComplex): + _tag = "timed_swap" + + _nodes = [ + XmlAnimatable("before", "_recurse"), + XmlAnimatable("after", "_recurse"), + XmlAnimatable("time", "time", FrameTime(0, FrameTime.Unit.Seconds)), + XmlAnimatable("length", "time", FrameTime(0, FrameTime.Unit.Seconds)), + ] + + +class SifTimeLoop(SifAstComplex): + _tag = "timeloop" + + _nodes = [ + XmlAnimatable("link", "_recurse"), + XmlAnimatable("link_time", "time", FrameTime(0, FrameTime.Unit.Seconds)), + XmlAnimatable("local_time", "time", FrameTime(0, FrameTime.Unit.Seconds)), + XmlAnimatable("duration", "time", FrameTime(0, FrameTime.Unit.Seconds)), + ] + + +class SifPower(SifAstComplex): + _tag = "power" + + _nodes = [ + XmlAnimatable("base", "real", 1.), + XmlAnimatable("power", "real", 1.), + XmlAnimatable("epsilon", "real", 0.000001), + XmlAnimatable("infinite", "real", 999999.), + ] + + +class SifBlineCalcTangent(SifAstComplex): + _tag = "blinecalctangent" + + _nodes = [ + XmlSifElement("bline", Bline), + XmlAnimatable("loop", "bool", False), + XmlAnimatable("amount", "real", 0.5), + XmlAnimatable("offset", "angle", 0.), + XmlAnimatable("scale", "real", 1.), + XmlAnimatable("fixed_length", "bool", False), + XmlAnimatable("homogeneous", "bool", False), + ] + + +class SifBlineCalcVertex(SifAstComplex): + _tag = "blinecalcvertex" + + _nodes = [ + XmlSifElement("bline", Bline), + XmlAnimatable("loop", "bool", False), + XmlAnimatable("amount", "real", 0.5), + XmlAnimatable("homogeneous", "bool", False), + ] diff --git a/lottie/parsers/sif/builder.py b/lottie/parsers/sif/builder.py new file mode 100644 index 0000000..ccfa410 --- /dev/null +++ b/lottie/parsers/sif/builder.py @@ -0,0 +1,411 @@ +import math +from xml.dom import minidom + +from ... import objects +from ...nvector import NVector +from ...utils import restructure +from . import api, ast + + +blend_modes = { + objects.BlendMode.Normal: api.BlendMethod.Composite, + objects.BlendMode.Multiply: api.BlendMethod.Multiply, + objects.BlendMode.Screen: api.BlendMethod.Screen, + objects.BlendMode.Overlay: api.BlendMethod.Overlay, + objects.BlendMode.Darken: api.BlendMethod.Darken, + objects.BlendMode.Lighten: api.BlendMethod.Lighten, + objects.BlendMode.HardLight: api.BlendMethod.HardLight, + objects.BlendMode.Difference: api.BlendMethod.Difference, + objects.BlendMode.Hue: api.BlendMethod.Hue, + objects.BlendMode.Saturation: api.BlendMethod.Saturation, + objects.BlendMode.Color: api.BlendMethod.Color, + objects.BlendMode.Luminosity: api.BlendMethod.Luminosity, + objects.BlendMode.Exclusion: api.BlendMethod.Difference, + objects.BlendMode.SoftLight: api.BlendMethod.Multiply, + objects.BlendMode.ColorDodge: api.BlendMethod.Composite, + objects.BlendMode.ColorBurn: api.BlendMethod.Composite, +} + + +class SifBuilder(restructure.AbstractBuilder): + def __init__(self, gamma=1.0): + """ + @todo Add gamma option to lottie_convert.py + """ + super().__init__() + self.canvas = api.Canvas() + self.canvas.version = "1.2" + self.canvas.gamma_r = self.canvas.gamma_g = self.canvas.gamma_b = gamma + self.autoid = objects.base.Index() + + def _on_animation(self, animation: objects.Animation): + if animation.name: + self.canvas.name = animation.name + self.canvas.width = animation.width + self.canvas.height = animation.height + self.canvas.xres = animation.width + self.canvas.yres = animation.height + self.canvas.view_box = NVector(0, 0, animation.width, animation.height) + self.canvas.fps = animation.frame_rate + self.canvas.begin_time = api.FrameTime.frame(animation.in_point) + self.canvas.end_time = api.FrameTime.frame(animation.out_point) + self.canvas.antialias = True + return self.canvas + + def _on_precomp(self, id, dom_parent, layers): + g = dom_parent.add_layer(api.GroupLayer()) + g.desc = id + for layer_builder in layers: + self.process_layer(layer_builder, g) + + def _on_layer(self, layer_builder, dom_parent): + layer = self.layer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent) + if not layer_builder.lottie.name: + layer.desc = layer_builder.lottie.__class__.__name__ + + bm = getattr(layer_builder.lottie, "blend_mode", None) + if bm is None: + bm = objects.BlendMode.Normal + layer.blend_method = blend_modes[bm] + + layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1 + + in_point = getattr(layer_builder.lottie, "in_point", 0) + layer.time_offset.value = api.FrameTime.frame(in_point) + + #layer.canvas.end_time = api.FrameTime.frame(out_point) + return layer + + def layer_from_lottie(self, type, lottie, dom_parent): + g = dom_parent.add_layer(type()) + if lottie.name: + g.desc = lottie.name + g.active = not lottie.hidden + transf = getattr(lottie, "transform", None) + if transf: + self.set_transform(g, transf) + + if isinstance(lottie, objects.NullLayer): + g.amount.value = 1 + + return g + + def _get_scale(self, transform): + def func(keyframe): + t = keyframe.time if keyframe else 0 + scale_x, scale_y = transform.scale.get_value(t)[:2] + scale_x /= 100 + scale_y /= 100 + skew = transform.skew.get_value(t) if transform.skew else 0 + c = math.cos(skew * math.pi / 180) + if c != 0: + scale_y *= 1 / c + return NVector(scale_x, scale_y) + return func + + def set_transform(self, group, transform): + composite = group.transformation + + if transform.position: + composite.offset = self.process_vector(transform.position) + + if transform.scale: + keyframes = self._merge_keyframes([transform.scale, transform.skew]) + composite.scale = self.process_vector_ext(keyframes, self._get_scale(transform)) + + composite.skew_angle = self.process_scalar(transform.skew or objects.Value(0)) + + if transform.rotation: + composite.angle = self.process_scalar(transform.rotation) + + if transform.opacity: + group.amount = self.process_scalar(transform.opacity, 1/100) + + if transform.anchor_point: + group.origin = self.process_vector(transform.anchor_point) + + # TODO get z_depth from position + composite.z_depth = 0 + + def process_vector(self, multidim): + def getter(keyframe): + if keyframe is None: + v = multidim.value + else: + v = keyframe.start + return NVector(v[0], v[1]) + + return self.process_vector_ext(multidim.keyframes, getter) + + def process_vector_ext(self, kframes, getter): + if kframes is not None: + wrap = ast.SifAnimated() + for i in range(len(kframes)): + keyframe = kframes[i] + waypoint = wrap.add_keyframe(getter(keyframe), api.FrameTime.frame(keyframe.time)) + + if i > 0: + prev = kframes[i-1] + if prev.jump: + waypoint.before = api.Interpolation.Constant + elif prev.in_value and prev.in_value.x < 1: + waypoint.before = api.Interpolation.Ease + else: + waypoint.before = api.Interpolation.Linear + else: + waypoint.before = api.Interpolation.Linear + + if keyframe.jump: + waypoint.after = api.Interpolation.Constant + elif keyframe.out_value and keyframe.out_value.x > 0: + waypoint.after = api.Interpolation.Ease + else: + waypoint.after = api.Interpolation.Linear + else: + wrap = api.SifValue(getter(None)) + + return wrap + + def process_scalar(self, value, mult=None): + def getter(keyframe): + if keyframe is None: + v = value.value + else: + v = keyframe.start[0] + if mult is not None: + v *= mult + return v + return self.process_vector_ext(value.keyframes, getter) + + def _on_shape(self, shape, group, dom_parent): + layers = [] + if not hasattr(shape, "to_bezier"): + return [] + + if group.stroke: + sif_shape = self.build_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape) + self.apply_group_stroke(sif_shape, group.stroke) + layers.append(sif_shape) + + if group.fill: + sif_shape = self.build_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape) + layers.append(sif_shape) + self.apply_group_fill(sif_shape, group.fill) + + return layers + + def _merge_keyframes(self, props): + keyframes = {} + for prop in props: + if prop is not None and prop.animated: + keyframes.update({kf.time: kf for kf in prop.keyframes}) + return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None + + def apply_origin(self, sif_shape, lottie_shape): + if hasattr(lottie_shape, "position"): + sif_shape.origin.value = lottie_shape.position.get_value() + else: + sif_shape.origin.value = lottie_shape.bounding_box().center() + + def apply_group_fill(self, sif_shape, fill): + ## @todo gradients? + if hasattr(fill, "colors"): + return + + def getter(keyframe): + if keyframe is None: + v = fill.color.value + else: + v = keyframe.start + return self.canvas.make_color(*v) + + sif_shape.color = self.process_vector_ext(fill.color.keyframes, getter) + + def get_op(keyframe): + if keyframe is None: + v = fill.opacity.value + else: + v = keyframe.start[0] + v /= 100 + return v + + sif_shape.amount = self.process_vector_ext(fill.opacity.keyframes, get_op) + + def apply_group_stroke(self, sif_shape, stroke): + self.apply_group_fill(sif_shape, stroke) + sif_shape.sharp_cusps.value = stroke.line_join == objects.LineJoin.Miter + round_cap = stroke.line_cap == objects.LineCap.Round + sif_shape.round_tip_0.value = round_cap + sif_shape.round_tip_1.value = round_cap + sif_shape.width = self.process_scalar(stroke.width, 0.5) + + def build_path(self, type, path, dom_parent, lottie_shape): + layer = self.layer_from_lottie(type, lottie_shape, dom_parent) + self.apply_origin(layer, lottie_shape) + startbez = path.shape.get_value() + layer.bline.loop = startbez.closed + nverts = len(startbez.vertices) + for point in range(nverts): + self.bezier_point(path, point, layer.bline, layer.origin.value) + return layer + + def bezier_point(self, lottie_path, point_index, sif_parent, offset): + composite = api.BlinePoint() + + def get_point(keyframe): + if keyframe is None: + bezier = lottie_path.shape.value + else: + bezier = keyframe.start + if not bezier: + #elem.parentNode.parentNode.removeChild(elem.parentNode) + return + vert = bezier.vertices[point_index] + return NVector(vert[0], vert[1]) - offset + + composite.point = self.process_vector_ext(lottie_path.shape.keyframes, get_point) + composite.split.value = True + composite.split_radius.value = True + composite.split_angle.value = True + + def get_tangent(keyframe): + if keyframe is None: + bezier = lottie_path.shape.value + else: + bezier = keyframe.start + if not bezier: + #elem.parentNode.parentNode.removeChild(elem.parentNode) + return + + inp = getattr(bezier, which_point)[point_index] + return NVector(inp.x, inp.y) * 3 * mult + + mult = -1 + which_point = "in_tangents" + composite.t1 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent) + + mult = 1 + which_point = "out_tangents" + composite.t2 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent) + sif_parent.points.append(composite) + + def _on_shapegroup(self, shape_group, dom_parent): + if shape_group.empty(): + return + + layer = self.layer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent) + + self.shapegroup_process_children(shape_group, layer) + + def _modifier_inner_group(self, modifier, shapegroup, dom_parent): + layer = dom_parent.add_layer(api.GroupLayer()) + self.shapegroup_process_child(modifier.child, shapegroup, layer) + return layer + + def _on_shape_modifier(self, modifier, shapegroup, dom_parent): + layer = dom_parent.add_layer(api.GroupLayer()) + if modifier.lottie.name: + layer.desc = modifier.lottie.name + + inner = self._modifier_inner_group(modifier, shapegroup, layer) + if isinstance(modifier.lottie, objects.Repeater): + self.build_repeater(modifier.lottie, inner, layer) + + def _build_repeater_defs(self, shape, name_id): + dup = api.Duplicate() + dup.id = name_id + self.canvas.defs.append(dup) + self.canvas.register_as(dup, name_id) + + def getter(keyframe): + if keyframe is None: + v = shape.copies.value + else: + v = keyframe.start[0] + + return v - 1 + + setattr(dup, "from", self.process_vector_ext(shape.copies.keyframes, getter)) + dup.to.value = 0 + dup.step.value = -1 + return dup + + def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite): + power = ast.SifPower() + setattr(scalecomposite, "xy"[comp], power) + + def getter(keyframe): + if keyframe is None: + v = shape.transform.scale.value + else: + v = keyframe.start + v = v[comp] / 100 + return v + + power.base = self.process_vector_ext(shape.transform.scale.keyframes, getter) + + # HACK work around an issue in Synfig + power.power = ast.SifAdd() + power.power.lhs.value = api.ValueReference(name_id) + power.power.rhs.value = 0.000001 + + def _build_repeater_transform(self, shape, inner, name_id): + offset_id = name_id + "_origin" + origin = api.ExportedValue(offset_id, self.process_vector(shape.transform.anchor_point), "vector") + self.canvas.defs.append(origin) + self.canvas.register_as(origin, offset_id) + inner.origin = origin + + composite = inner.transformation + + composite.offset = ast.SifAdd() + composite.offset.rhs.value = api.ValueReference(offset_id) + composite.offset.lhs = ast.SifScale() + composite.offset.lhs.scalar.value = api.ValueReference(name_id) + composite.offset.lhs.link = self.process_vector(shape.transform.position) + + composite.angle = ast.SifScale() + composite.angle.scalar.value = api.ValueReference(name_id) + composite.angle.link = self.process_scalar(shape.transform.rotation) + + composite.scale = ast.SifVectorComposite() + self._build_repeater_transform_scale_component(shape, name_id, 0, composite.scale) + self._build_repeater_transform_scale_component(shape, name_id, 1, composite.scale) + + def _build_repeater_amount(self, shape, inner, name_id): + inner.amount = ast.SifSubtract() + inner.amount.lhs = self.process_scalar(shape.transform.start_opacity, 0.01) + + inner.amount.rhs = ast.SifScale() + inner.amount.rhs.scalar.value = api.ValueReference(name_id) + + def getter(keyframe): + if keyframe is None: + t = 0 + end = shape.transform.end_opacity.value + else: + t = keyframe.time + end = keyframe.start[0] + start = shape.transform.start_opacity.get_value(t) + n = shape.copies.get_value(t) + v = (start - end) / (n - 1) / 100 if n > 0 else 0 + return v + inner.amount.rhs.link = self.process_vector_ext(shape.transform.end_opacity.keyframes, getter) + + def build_repeater(self, shape, inner, dom_parent): + name_id = "duplicate_%s" % next(self.autoid) + dup = self._build_repeater_defs(shape, name_id) + self._build_repeater_transform(shape, inner, name_id) + self._build_repeater_amount(shape, inner, name_id) + inner.desc = "Transformation for " + (dom_parent.desc or "duplicate") + + # duplicate layer + duplicate = dom_parent.add_layer(api.DuplicateLayer()) + duplicate.index = dup + duplicate.desc = shape.name + + +def to_sif(animation): + builder = SifBuilder() + builder.process(animation) + return builder.canvas diff --git a/lottie/parsers/sif/converter.py b/lottie/parsers/sif/converter.py new file mode 100644 index 0000000..c2909a0 --- /dev/null +++ b/lottie/parsers/sif/converter.py @@ -0,0 +1,528 @@ +import math +from ... import objects +from ...objects import easing +from . import api, ast +from ... import NVector, PolarVector + +try: + from ...utils import font + has_font = True +except ImportError: + has_font = False + + +def convert(canvas: api.Canvas): + return Converter().convert(canvas) + + +class Converter: + def __init__(self): + pass + + def _animated(self, sifval): + return isinstance(sifval, ast.SifAnimated) + + def convert(self, canvas: api.Canvas): + self.canvas = canvas + self.animation = objects.Animation( + self._time(canvas.end_time), + canvas.fps + ) + self.animation.in_point = self._time(canvas.begin_time) + self.animation.width = canvas.width + self.animation.height = canvas.height + self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1]) + self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3]) + self.target_size = NVector(canvas.width, canvas.height) + self.shape_layer = self.animation.add_layer(objects.ShapeLayer()) + self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b) + self._process_layers(canvas.layers, self.shape_layer) + return self.animation + + def _time(self, t: api.FrameTime): + return self.canvas.time_to_frames(t) + + def _process_layers(self, layers, parent): + old_gamma = self.gamma + + for layer in reversed(layers): + if not layer.active: + continue + elif isinstance(layer, api.GroupLayerBase): + parent.add_shape(self._convert_group(layer)) + elif isinstance(layer, api.RectangleLayer): + parent.add_shape(self._convert_fill(layer, self._convert_rect)) + elif isinstance(layer, api.CircleLayer): + parent.add_shape(self._convert_fill(layer, self._convert_circle)) + elif isinstance(layer, api.StarLayer): + parent.add_shape(self._convert_fill(layer, self._convert_star)) + elif isinstance(layer, api.PolygonLayer): + parent.add_shape(self._convert_fill(layer, self._convert_polygon)) + elif isinstance(layer, api.RegionLayer): + parent.add_shape(self._convert_fill(layer, self._convert_bline)) + elif isinstance(layer, api.AbstractOutline): + parent.add_shape(self._convert_outline(layer, self._convert_bline)) + elif isinstance(layer, api.GradientLayer): + parent.add_shape(self._convert_gradient(layer, parent)) + elif isinstance(layer, api.TransformDown): + shape = self._convert_transform_down(layer) + parent.add_shape(shape) + parent = shape + elif isinstance(layer, api.TextLayer): + if has_font: + parent.add_shape(self._convert_fill(layer, self._convert_text)) + elif isinstance(layer, api.ColorCorrectLayer): + self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value) + + self.gamma = old_gamma + + def _convert_group(self, layer: api.GroupLayer): + shape = objects.Group() + self._set_name(shape, layer) + shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin)) + self._convert_transform(layer.transformation, shape.transform) + self._process_layers(layer.layers, shape) + shape.transform.opacity = self._adjust_animated( + self._convert_scalar(layer.amount), + lambda x: x*100 + ) + return shape + + def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform): + if isinstance(sif_transform, api.BoneLinkTransform): + base_transform = sif_transform.base_value + else: + base_transform = sif_transform + + position = self._adjust_coords(self._convert_vector(base_transform.offset)) + rotation = self._adjust_angle(self._convert_scalar(base_transform.angle)) + scale = self._adjust_animated( + self._convert_vector(base_transform.scale), + lambda x: x * 100 + ) + + lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle)) + + if isinstance(sif_transform, api.BoneLinkTransform): + lottie_transform.position = position + lottie_transform.rotation = rotation + lottie_transform.scale = scale + #bone = sif_transform.bone + #b_pos = self._adjust_coords(self._convert_vector(bone.origin)) + #old_anchor = lottie_transform.anchor_point + + #if sif_transform.translate: + #self._mix_animations_into( + #[position, b_pos, old_anchor], + #lottie_transform.position, + #lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2 + #) + #else: + #lottie_transform.position = position + + #lottie_transform.anchor_point = b_pos + #lottie_transform.anchor_point.value += NVector(100,0) + + #if sif_transform.rotate: + #b_rot = self._convert_scalar(bone.angle) + #self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b) + #else: + #lottie_transform.rotation = rotation + + #if sif_transform.scale_y: + #b_scale = self._convert_scalar(bone.scalelx) + #self._mix_animations_into( + #scale, b_scale, lottie_transform.scale, + #lambda a, b: NVector(a.x, a.y * b) + #) + #else: + #lottie_transform.scale = scale + else: + lottie_transform.position = position + lottie_transform.rotation = rotation + lottie_transform.scale = scale + + def _mix_animations_into(self, animations, output, mix): + if not any(x.animated for x in animations): + output.value = mix(*(x.value for x in animations)) + else: + for vals in self._mix_animations(*animations): + time = vals.pop(0) + output.add_keyframe(time, mix(*vals)) + + def _convert_fill(self, layer, converter): + shape = objects.Group() + self._set_name(shape, layer) + shape.add_shape(converter(layer)) + if layer.invert.value: + shape.add_shape(objects.Rect(self.target_size/2, self.target_size)) + + fill = objects.Fill() + fill.color = self._convert_color(layer.color) + fill.opacity = self._adjust_animated( + self._convert_scalar(layer.amount), + lambda x: x * 100 + ) + shape.add_shape(fill) + return shape + + def _convert_linecap(self, lc: api.LineCap): + if lc == api.LineCap.Rounded: + return objects.LineCap.Round + if lc == api.LineCap.Squared: + return objects.LineCap.Square + return objects.LineCap.Butt + + def _convert_cusp(self, lc: api.CuspStyle): + if lc == api.CuspStyle.Miter: + return objects.LineJoin.Miter + if lc == api.CuspStyle.Bevel: + return objects.LineJoin.Bevel + return objects.LineJoin.Round + + def _convert_outline(self, layer: api.AbstractOutline, converter): + shape = objects.Group() + self._set_name(shape, layer) + shape.add_shape(converter(layer)) + stroke = objects.Stroke() + stroke.color = self._convert_color(layer.color) + stroke.line_cap = self._convert_linecap(layer.start_tip) + stroke.line_join = self._convert_cusp(layer.cusp_type) + stroke.width = self._adjust_scalar(self._convert_scalar(layer.width)) + shape.add_shape(stroke) + return shape + + def _convert_rect(self, layer: api.RectangleLayer): + rect = objects.Rect() + p1 = self._adjust_coords(self._convert_vector(layer.point1)) + p2 = self._adjust_coords(self._convert_vector(layer.point2)) + if p1.animated or p2.animated: + for time, p1v, p2v in self._mix_animations(p1, p2): + rect.position.add_keyframe(time, (p1v + p2v) / 2) + rect.size.add_keyframe(time, abs(p2v - p1v)) + pass + else: + rect.position.value = (p1.value + p2.value) / 2 + rect.size.value = abs(p2.value - p1.value) + rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel)) + return rect + + def _convert_circle(self, layer: api.CircleLayer): + shape = objects.Ellipse() + shape.position = self._adjust_coords(self._convert_vector(layer.origin)) + radius = self._adjust_scalar(self._convert_scalar(layer.radius)) + shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2) + return shape + + def _convert_star(self, layer: api.StarLayer): + shape = objects.Star() + shape.position = self._adjust_coords(self._convert_vector(layer.origin)) + shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2)) + shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1)) + shape.rotation = self._adjust_animated( + self._convert_scalar(layer.angle), + lambda x: 90-x + ) + shape.points = self._convert_scalar(layer.points) + if layer.regular_polygon.value: + shape.star_type = objects.StarType.Polygon + return shape + + def _mix_animations(self, *animatable): + times = set() + for v in animatable: + self._force_animated(v) + for kf in v.keyframes: + times.add(kf.time) + + for time in sorted(times): + yield [time] + [v.get_value(time) for v in animatable] + + def _force_animated(self, lottieval): + if not lottieval.animated: + v = lottieval.value + lottieval.add_keyframe(0, v) + lottieval.add_keyframe(self.animation.out_point, v) + + def _convert_easing_part(self, interp: api.Interpolation): + if interp == api.Interpolation.Linear: + return easing.Linear() + return easing.Sigmoid() + + def _convert_easing(self, start: api.Interpolation, end: api.Interpolation): + if api.Interpolation.Constant in (start, end): + return easing.Jump() + if start == end: + return self._convert_easing_part(start) + return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end)) + + def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin): + if self._animated(v): + if len(v.keyframes) == 1: + lot.value = self._convert_ast_value(v.keyframes[0].value) + else: + for i, kf in enumerate(v.keyframes): + if i+1 < len(v.keyframes): + start = kf.after + end = v.keyframes[i+1].before + ease = self._convert_easing(start, end) + else: + ease = easing.Linear() + + lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease) + else: + lot.value = self._convert_ast_value(v) + return lot + + def _convert_ast_value(self, v): + if isinstance(v, ast.SifRadialComposite): + return self._polar(v.radius.value, v.theta.value, 1) + elif isinstance(v, ast.SifValue): + return v.value + elif isinstance(v, ast.SifVectorComposite): + return NVector(v.x.value, v.y.value) + else: + return v + + def _converted_vector_values(self, v): + if isinstance(v, ast.SifRadialComposite): + return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)] + return self._convert_vector(v) + + def _convert_color(self, v: ast.SifAstNode): + return self._adjust_animated( + self._convert_animatable(v, objects.ColorValue()), + self._color_gamma + ) + + def _convert_vector(self, v: ast.SifAstNode): + return self._convert_animatable(v, objects.MultiDimensional()) + + def _convert_scalar(self, v: ast.SifAstNode): + return self._convert_animatable(v, objects.Value()) + + def _color_gamma(self, color): + color = color.clone() + for i in range(3): + color[i] = color[i] ** (1/self.gamma[i]) + return color + + def _adjust_animated(self, lottieval, transform): + if lottieval.animated: + for kf in lottieval.keyframes: + if kf.start is not None: + kf.start = transform(kf.start) + if kf.end is not None: + kf.end = transform(kf.end) + else: + lottieval.value = transform(lottieval.value) + return lottieval + + def _adjust_scalar(self, lottieval: objects.Value): + return self._adjust_animated(lottieval, self._scalar_mult) + + def _adjust_angle(self, lottieval: objects.Value): + return self._adjust_animated(lottieval, lambda x: -x) + + def _adjust_add_dimension(self, lottieval, transform): + to_val = objects.MultiDimensional() + to_val.animated = lottieval.animated + if lottieval.animated: + to_val.keyframes = [] + for kf in lottieval.keyframes: + if kf.start is not None: + kf.start = transform(kf.start[0]) + if kf.end is not None: + kf.end = transform(kf.end[0]) + to_val.keyframes.append(kf) + else: + to_val.value = transform(lottieval.value) + return to_val + + def _scalar_mult(self, x): + return x * 60 + + def _adjust_coords(self, lottieval: objects.MultiDimensional): + return self._adjust_animated(lottieval, self._coord) + + def _coord(self, val: NVector): + return NVector( + self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5), + self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5), + ) + + def _convert_polygon(self, layer: api.PolygonLayer): + lot = objects.Path() + animatables = [self._convert_vector(layer.origin)] + [ + self._convert_vector(p) + for p in layer.points + ] + animated = any(x.animated for x in animatables) + if not animated: + lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value) + else: + for values in self._mix_animations(*animatables): + time = values[0] + origin = values[1] + points = values[2:] + lot.shape.add_keyframe(time, self._polygon(points, origin)) + return lot + + def _polygon(self, points, origin): + bezier = objects.Bezier() + bezier.closed = True + for point in points: + bezier.add_point(self._coord(point+origin)) + return bezier + + def _convert_bline(self, layer: api.AbstractOutline): + lot = objects.Path() + closed = layer.bline.loop + animatables = [ + self._convert_vector(layer.origin) + ] + for p in layer.bline.points: + animatables += [ + self._convert_vector(p.point), + self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0), + self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0), + self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0), + self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0) + ] + animated = any(x.animated for x in animatables) + if not animated: + lot.shape.value = self._bezier( + closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points + ) + else: + for values in self._mix_animations(*animatables): + time = values[0] + origin = values[1] + values = values[2:] + lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points)) + return lot + + def _bezier(self, closed, values, origin, points): + chunk_size = 5 + bezier = objects.Bezier() + bezier.closed = closed + for i in range(0, len(values), chunk_size): + point, r1, a1, r2, a2 = values[i:i+chunk_size] + sifvert = point+origin + vert = self._coord(sifvert) + if not points[i//chunk_size].split_radius.value: + r2 = r1 + if not points[i//chunk_size].split_angle.value: + a2 = a1 + t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert + t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert + bezier.add_point(vert, t1, t2) + return bezier + + def _polar(self, radius, angle, dir): + offset_angle = 0 + if dir == 1: + offset_angle += 180 + return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180) + + def _convert_transform_down(self, tl: api.TransformDown): + group = objects.Group() + self._set_name(group, tl) + + if isinstance(tl, api.TranslateLayer): + group.transform.anchor_point.value = self.target_size / 2 + group.transform.position = self._adjust_coords(self._convert_vector(tl.origin)) + elif isinstance(tl, api.RotateLayer): + group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin)) + group.transform.position = group.transform.anchor_point.clone() + group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount)) + elif isinstance(tl, api.ScaleLayer): + group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center)) + group.transform.position = group.transform.anchor_point.clone() + group.transform.scale = self._adjust_add_dimension( + self._convert_scalar(tl.amount), + self._zoom_to_scale + ) + + return group + + def _zoom_to_scale(self, value): + zoom = math.e ** value * 100 + return NVector(zoom, zoom) + + def _set_name(self, lottie, sif): + lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__ + + def _convert_gradient(self, layer: api.GradientLayer, parent): + group = objects.Group() + + parent_shapes = parent.shapes + parent.shapes = [] + if isinstance(parent, objects.Group): + parent.shapes.append(parent_shapes[-1]) + + self._gradient_gather_shapes(parent_shapes, group) + + gradient = objects.GradientFill() + self._set_name(gradient, layer) + group.add_shape(gradient) + gradient.colors = self._convert_gradient_stops(layer.gradient) + gradient.opacity = self._adjust_animated( + self._convert_scalar(layer.amount), + lambda x: x * 100 + ) + + if isinstance(layer, api.LinearGradient): + gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1)) + gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2)) + gradient.gradient_type = objects.GradientType.Linear + elif isinstance(layer, api.RadialGradient): + gradient.gradient_type = objects.GradientType.Radial + gradient.start_point = self._adjust_coords(self._convert_vector(layer.center)) + radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45) + if not radius.animated and not gradient.start_point.animated: + gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value) + else: + for time, c, r in self._mix_animations(gradient.start_point.clone(), radius): + gradient.end_point.add_keyframe(time, c + NVector(r + r)) + + return group + + def _gradient_gather_shapes(self, shapes, output: objects.Group): + for shape in shapes: + if isinstance(shape, objects.Shape): + output.add_shape(shape) + elif isinstance(shape, objects.Group): + self._gradient_gather_shapes(shape.shapes, output) + + def _convert_gradient_stops(self, sif_gradient): + stops = objects.GradientColors() + if not self._animated(sif_gradient): + stops.set_stops(self._flatten_gradient_colors(sif_gradient.value)) + stops.count = len(sif_gradient.value) + else: + # TODO easing + for kf in sif_gradient.keyframes: + stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value)) + stops.count = len(kf.value) + + return stops + + def _flatten_gradient_colors(self, stops): + return [ + (stop.pos, self._color_gamma(stop.color)) + for stop in stops + ] + + def _convert_text(self, layer: api.TextLayer): + shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center)) + shape.refresh() + trans = shape.wrapped.transform + trans.anchor_point.value = shape.wrapped.bounding_box().center() + trans.anchor_point.value.x /= 2 + trans.position = self._adjust_coords(self._convert_vector(layer.origin)) + trans.scale = self._adjust_animated( + self._convert_vector(layer.size), + lambda v: v * 100 + ) + return shape diff --git a/lottie/parsers/sif/importer.py b/lottie/parsers/sif/importer.py new file mode 100644 index 0000000..4c04fa2 --- /dev/null +++ b/lottie/parsers/sif/importer.py @@ -0,0 +1,7 @@ +from ..tgs import open_maybe_gzipped + + +def parse_sif_file(file): + from .converter import convert + from . import api + return convert(open_maybe_gzipped(file, api.Canvas.from_xml_file)) diff --git a/lottie/parsers/sif/sif/__init__.py b/lottie/parsers/sif/sif/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lottie/parsers/sif/sif/__pycache__/__init__.cpython-310.pyc b/lottie/parsers/sif/sif/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3832b3e Binary files /dev/null and b/lottie/parsers/sif/sif/__pycache__/__init__.cpython-310.pyc differ diff --git a/lottie/parsers/sif/sif/__pycache__/core.cpython-310.pyc b/lottie/parsers/sif/sif/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000..a350af8 Binary files /dev/null and b/lottie/parsers/sif/sif/__pycache__/core.cpython-310.pyc differ diff --git a/lottie/parsers/sif/sif/__pycache__/enums.cpython-310.pyc b/lottie/parsers/sif/sif/__pycache__/enums.cpython-310.pyc new file mode 100644 index 0000000..3df46cc Binary files /dev/null and b/lottie/parsers/sif/sif/__pycache__/enums.cpython-310.pyc differ diff --git a/lottie/parsers/sif/sif/__pycache__/frame_time.cpython-310.pyc b/lottie/parsers/sif/sif/__pycache__/frame_time.cpython-310.pyc new file mode 100644 index 0000000..92875ae Binary files /dev/null and b/lottie/parsers/sif/sif/__pycache__/frame_time.cpython-310.pyc differ diff --git a/lottie/parsers/sif/sif/__pycache__/nodes.cpython-310.pyc b/lottie/parsers/sif/sif/__pycache__/nodes.cpython-310.pyc new file mode 100644 index 0000000..c3608d1 Binary files /dev/null and b/lottie/parsers/sif/sif/__pycache__/nodes.cpython-310.pyc differ diff --git a/lottie/parsers/sif/sif/core.py b/lottie/parsers/sif/sif/core.py new file mode 100644 index 0000000..df7265c --- /dev/null +++ b/lottie/parsers/sif/sif/core.py @@ -0,0 +1,165 @@ +from xml.dom import minidom +import enum +from uuid import uuid4 + +from lottie.nvector import NVector +from lottie.parsers.sif.xml.utils import xml_text, str_to_bool +from lottie.parsers.sif.xml.utils import xml_child_elements, value_from_xml_string, xml_make_text, value_to_xml_string +from lottie.parsers.sif.sif.frame_time import FrameTime + + +class ObjectRegistry: + def __init__(self): + self.registry = {} + + def register_as(self, object, key): + self.registry[key] = object + + def register(self, object): + guid = getattr(object, "guid", None) + if guid is None: + guid = self.guid() + object.guid = guid + self.registry[guid] = object + + @classmethod + def guid(cls): + return str(uuid4()).replace("-", "").upper() + + def get_object(self, guid): + return self.registry[guid] + + +def noop(x): + return x + + +class TypeDescriptor: + _type_tag_names = { + "bone_object": "bone" + } + + def __init__(self, typename, default=None, type_wrapper=noop): + self.typename = typename + self.type_wrapper = type_wrapper + self.default_value = default + + def value_to_xml_element(self, value, dom: minidom.Document): + element = dom.createElement(self.tag_name) + + if self.typename == "vector": + element.appendChild(xml_make_text(dom, "x", str(value.x))) + element.appendChild(xml_make_text(dom, "y", str(value.y))) + if hasattr(value, "guid"): + element.setAttribute("guid", value.guid) + elif self.typename == "color": + element.appendChild(xml_make_text(dom, "r", str(value[0]))) + element.appendChild(xml_make_text(dom, "g", str(value[1]))) + element.appendChild(xml_make_text(dom, "b", str(value[2]))) + element.appendChild(xml_make_text(dom, "a", str(value[3]))) + if hasattr(value, "guid"): + element.setAttribute("guid", value.guid) + elif self.typename == "gradient": + for point in value: + element.appendChild(point.to_dom(dom)) + elif self.typename == "bool": + element.setAttribute("value", "true" if value else "false") + elif self.typename == "bone_object": + element.setAttribute("guid", value.guid) + element.setAttribute("type", self.typename) + elif self.typename == "string": + element.appendChild(dom.createTextNode(value)) + else: + if isinstance(value, enum.Enum): + value = value.value + element.setAttribute("value", str(value)) + + return element + + @property + def tag_name(self): + return self._type_tag_names.get(self.typename, self.typename) + + def value_from_xml_element(self, xml: minidom.Element, registry: ObjectRegistry): + if xml.tagName != self.tag_name: + raise ValueError("Wrong value type (%s instead of %s)" % (xml.tagName, self.tag_name)) + + guid = xml.getAttribute("guid") + if guid and guid in registry.registry: + value = registry.registry[guid] + elif self.typename == "vector": + value = NVector( + float(xml_text(xml.getElementsByTagName("x")[0])), + float(xml_text(xml.getElementsByTagName("y")[0])) + ) + if xml.getAttribute("guid"): + value.guid = xml.getAttribute("guid") + registry.register(value) + elif self.typename == "color": + value = NVector( + float(xml_text(xml.getElementsByTagName("r")[0])), + float(xml_text(xml.getElementsByTagName("g")[0])), + float(xml_text(xml.getElementsByTagName("b")[0])), + float(xml_text(xml.getElementsByTagName("a")[0])) + ) + elif self.typename == "gradient": + value = [ + GradientPoint.from_dom(sub, registry) + for sub in xml_child_elements(xml, GradientPoint.type.typename) + ] + elif self.typename == "real" or self.typename == "angle": + value = float(xml.getAttribute("value")) + elif self.typename == "integer": + value = int(xml.getAttribute("value")) + elif self.typename == "time": + value = FrameTime.parse_string(xml.getAttribute("value"), registry) + elif self.typename == "bool": + value = str_to_bool(xml.getAttribute("value")) + elif self.typename == "string": + return xml_text(xml) + elif self.typename == "bone_object": + # Already done above but this forces the guid to be present + return registry.get_object(xml.getAttribute("guid")) + else: + raise ValueError("Unsupported type %s" % self.typename) + + return self.type_wrapper(value) + + +class GradientPoint: + type = TypeDescriptor("color") + + def __init__(self, pos: float, color: NVector): + self.pos = pos + self.color = color + + def to_dom(self, dom: minidom.Document): + element = self.type.value_to_xml_element(self.color, dom) + element.setAttribute("pos", value_to_xml_string(self.pos, float)) + return element + + @classmethod + def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry): + return GradientPoint( + value_from_xml_string(xml.getAttribute("pos"), float, registry), + cls.type.value_from_xml_element(xml, registry) + ) + + def __repr__(self): + return "" % (self.pos, self.color) + + +class SifNodeMeta(type): + def __new__(cls, name, bases, attr): + props = [] + for base in bases: + if type(base) == cls: + props += base._nodes + attr["_nodes"] = props + attr.get("_nodes", []) + if "_tag" not in attr: + attr["_tag"] = name.lower() + attr["_nodemap"] = { + node.att_name: node + for node in attr["_nodes"] + } + return super().__new__(cls, name, bases, attr) diff --git a/lottie/parsers/sif/sif/enums.py b/lottie/parsers/sif/sif/enums.py new file mode 100644 index 0000000..83094a0 --- /dev/null +++ b/lottie/parsers/sif/sif/enums.py @@ -0,0 +1,9 @@ +import enum + + +class Smooth(enum.Enum): + NearestNeighbour = 0 + Linear = 1 + Cosine = 2 + Spline = 3 + Cubic = 4 diff --git a/lottie/parsers/sif/sif/frame_time.py b/lottie/parsers/sif/sif/frame_time.py new file mode 100644 index 0000000..32b7fc2 --- /dev/null +++ b/lottie/parsers/sif/sif/frame_time.py @@ -0,0 +1,46 @@ +import enum + + +class FrameTime: + class Unit(enum.Enum): + Frame = "f" + Seconds = "s" + + def __init__(self, value, unit): + self.value = value + self.unit = unit + + def __eq__(self, other): + return self.value == other.value and self.unit == other.unit + + def __ne__(self, other): + return self.value == other.value and self.unit == other.unit + + def __str__(self): + return "%s%s" % (self.value, self.unit.value) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self) + + @classmethod + def frame(cls, amount): + return cls(amount, cls.Unit.Frame) + + @classmethod + def seconds(cls, amount): + return cls(amount, cls.Unit.Seconds) + + @classmethod + def parse_string(cls, value_str, canvas): + if " " in value_str: + value = 0 + unit = cls.Unit.Frame + for sub in value_str.split(): + sv = float(sub[:-1]) + if sub[-1] == "s": + sv *= canvas.fps + value += sv + else: + value = float(value_str[:-1]) + unit = cls.Unit(value_str[-1]) + return FrameTime(value, unit) diff --git a/lottie/parsers/sif/sif/nodes.py b/lottie/parsers/sif/sif/nodes.py new file mode 100644 index 0000000..bcb1808 --- /dev/null +++ b/lottie/parsers/sif/sif/nodes.py @@ -0,0 +1,1180 @@ +import enum +from lottie.parsers.sif.sif.core import SifNodeMeta, FrameTime +from lottie.parsers.sif.sif.enums import Smooth +from lottie.parsers.sif.xml.utils import * +from lottie.parsers.sif.xml.core_nodes import * +from lottie.parsers.sif.xml.animatable import * +from lottie.parsers.sif.xml.wrappers import * + + +class SifNode(metaclass=SifNodeMeta): + def __init__(self, **kw): + for node in self._nodes: + node.initialize_object(kw, self) + + def __setattr__(self, name, value): + if name in self._nodemap: + value = self._nodemap[name].clean(value) + return super().__setattr__(name, value) + + @staticmethod + def static_from_dom(cls, xml: minidom.Element, registry: ObjectRegistry): + instance = cls() + for node in cls._nodes: + node.from_xml(instance, xml, registry) + return instance + + @classmethod + def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry): + return SifNode.static_from_dom(cls, xml, registry) + + def to_dom(self, dom: minidom.Document): + element = dom.createElement(self._tag) + for node in self._nodes: + node.to_xml(self, element, dom) + return element + + +class AbstractTransform(SifNode): + _nodes = [ + XmlFixedAttribute("type", "transformation") + ] + + @classmethod + def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry): + if xml.tagName == "bone_link": + return SifNode.static_from_dom(BoneLinkTransform, xml, registry) + + if xml.tagName != "composite": + raise ValueError("Invalid transform element: %s" % xml.tagName) + return SifNode.static_from_dom(SifTransform, xml, registry) + + +class SifTransform(AbstractTransform): + _tag = "composite" + + _nodes = [ + XmlAnimatable("offset", "vector", NVector(0, 0)), + XmlAnimatable("angle", "angle", 0.), + XmlAnimatable("skew_angle", "angle", 0.), + XmlAnimatable("scale", "vector", NVector(1, 1)), + ] + + +class BlinePoint(SifNode): + _tag = "composite" + + _nodes = [ + XmlFixedAttribute("type", "bline_point"), + XmlAnimatable("point", "vector", NVector(0, 0)), + XmlAnimatable("width", "real", 1.), + XmlAnimatable("origin", "real", .5), + XmlAnimatable("split", "bool", False), + XmlAnimatable("t1", "vector"), + XmlAnimatable("t2", "vector"), + XmlAnimatable("split_radius", "bool", True), + XmlAnimatable("split_angle", "bool", False), + ] + + +class Bline(SifNode): + _nodes = [ + XmlAttribute("loop", bool_str, False), + XmlFixedAttribute("type", "bline_point"), + XmlList(BlinePoint, "points", "entry"), + ] + + +class BlendMethod(enum.Enum): + Composite = 0 + Straight = 1 + Onto = 13 + StraightOnto = 21 + Behind = 12 + Screen = 16 + Overlay = 20 + HardLight = 17 + Multiply = 6 + Divide = 7 + Add = 4 + Subtract = 5 + Difference = 18 + Lighten = 2 + Darken = 3 + Color = 8 + Hue = 9 + Saturation = 10 + Luminosity = 11 + AlphaOver = 19 + AlphaBrighten = 14 + AlphaDarken = 15 + Alpha = 23 + + +class BlurType(enum.Enum): + Box = 0 + FastGaussian = 1 + CrossHatch = 2 + Gaussian = 3 + Disc = 4 + + +class WindingStyle(enum.Enum): + NonZero = 0 + EvenOdd = 1 + + +class Def(SifNode): + _nodes = [ + XmlAttribute("guid", str), + XmlAttribute("id", str), + ] + _subclasses = None + + @classmethod + def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry): + actual_class = cls + if cls == Def: + actual_class = Def.def_types()[xml.tagName] + + obj = SifNode.static_from_dom(actual_class, xml, registry) + if obj.id: + registry.register_as(obj, obj.id) + if obj.guid: + registry.register(obj) + return obj + + @staticmethod + def tags(): + return list(Def.def_types().keys()) + + @staticmethod + def def_types(): + if Def._subclasses is None: + Def._subclasses = {} + Def._gather_def_types(Def) + return Def._subclasses + + @staticmethod + def _gather_def_types(cls): + for subcls in cls.__subclasses__(): + Def._subclasses[subcls._tag] = subcls + Def._gather_def_types(subcls) + + +class Duplicate(Def): + _nodes = [ + XmlFixedAttribute("type", "real"), + XmlAnimatable("from", "real", 1.), + XmlAnimatable("to", "real", 1.), + XmlAnimatable("step", "real", 1.), + ] + + @property + def from_(self): + return getattr(self, "from") + + @from_.setter + def from_(self, value): + setattr(self, "from", value) + + +class ExportedValue(Def): + def __init__(self, id, value, typename): + self.id = id + self.value = value + self.type = TypeDescriptor(typename) + + def to_dom(self, dom: minidom.Document): + element = self.value.to_dom(dom, self.type) + element.setAttribute("id", self.id) + return element + + +class Layer(SifNode): + _types = None + + _version = "0.1" + _layer_type = None + + _nodes = [ + XmlAttribute("type", str), + XmlAttribute("active", bool_str, True), + XmlAttribute("version", str), + XmlAttribute("exclude_from_rendering", bool_str, False), + XmlAttribute("desc", str, ""), + ] + + def __init__(self, **kw): + kw.setdefault("version", self._version) + super().__init__(**kw) + self.type = self._layer_type + + def __repr__(self): + return "<%s.%s %r>" % (__name__, self.__class__.__name__, self.desc or self.type) + + def to_dom(self, dom: minidom.Document): + element = dom.createElement("layer") + for node in self._nodes: + node.to_xml(self, element, dom) + return element + + @classmethod + def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry): + actual_class = cls + if cls == Layer: + type = xml.getAttribute("type") + actual_class = Layer.layer_types().get(type, Layer) + + return SifNode.static_from_dom(actual_class, xml, registry) + + @staticmethod + def layer_types(): + if Layer._types is None: + Layer._types = {} + Layer._gather_layer_types(Layer) + return Layer._types + + @staticmethod + def _gather_layer_types(cls): + for subcls in cls.__subclasses__(): + if subcls._layer_type: + Layer._types[subcls._layer_type] = subcls + Layer._gather_layer_types(subcls) + + +class DrawableLayer(Layer): + _nodes = [ + XmlParam("z_depth", "real", 0.), + XmlParam("amount", "real", 1.), + XmlParam("blend_method", "integer", BlendMethod.Composite, BlendMethod, static=True), + ] + + +class GroupLayerBase(DrawableLayer): + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + XmlParamSif("transformation", AbstractTransform, SifTransform), + XmlWrapperParam("canvas", XmlWrapper("canvas", XmlList(Layer))), + + XmlParam("time_dilation", "real", 1.), + XmlParam("time_offset", "time", FrameTime(0, FrameTime.Unit.Frame)), + XmlParam("children_lock", "bool", False, static=True), + XmlParam("outline_grow", "real", 0.), + ] + + def add_layer(self, layer: Layer): + self.layers.append(layer) + return layer + + +class FilterGroupLayer(GroupLayerBase): + _layer_type = "filter_group" + + _nodes = [ + ] + + +class GroupLayer(GroupLayerBase): + _layer_type = "group" + _version = "0.3" + + _nodes = [ + XmlParam("z_range", "bool", False, static=True), + XmlParam("z_range_position", "real", 0.), + XmlParam("z_range_depth", "real", 0.), + XmlParam("z_range_blur", "real", 0.), + ] + + +class SwitchLayer(GroupLayerBase): + _layer_type = "switch" + + _nodes = [ + XmlParam("layer_name", "string"), + XmlParam("layer_depth", "integer", -1), + ] + + +class RectangleLayer(DrawableLayer): + _layer_type = "rectangle" + + _nodes = [ + XmlParam("color", "color", NVector(0, 0, 0, 1)), + XmlParam("point1", "vector", NVector(0, 0)), + XmlParam("point2", "vector", NVector(0, 0)), + XmlParam("expand", "real", 0.), + XmlParam("invert", "bool", False), + XmlParam("feather_x", "real", 0.), + XmlParam("feather_y", "real", 0.), + XmlParam("bevel", "real", 0.), + XmlParam("bevCircle", "bool", True), + ] + + +class CircleLayer(DrawableLayer): + _layer_type = "circle" + + _nodes = [ + XmlParam("color", "color", NVector(0, 0, 0, 1)), + XmlParam("radius", "real", 0.), + XmlParam("feather", "real", 0.), + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("invert", "bool", False), + ] + + +class SimpleCircleLayer(DrawableLayer): + _layer_type = "simple_circle" + + _nodes = [ + XmlParam("color", "color", NVector(0, 0, 0, 1)), + XmlParam("radius", "real", 0.), + XmlParam("center", "vector", NVector(0, 0)), + ] + + +class ComplexShape(DrawableLayer): + _nodes = [ + XmlParam("color", "color", NVector(0, 0, 0, 1)), + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("invert", "bool", False), + XmlParam("antialias", "bool", True), + XmlParam("feather", "real", 0.), + XmlParam("blurtype", "integer", BlurType.FastGaussian, BlurType), + XmlParam("winding_style", "integer", WindingStyle.NonZero, WindingStyle), + ] + + +class StarLayer(ComplexShape): + _layer_type = "star" + + _nodes = [ + XmlParam("radius1", "real", 0.), + XmlParam("radius2", "real", 0.), + XmlParam("angle", "angle", 0.), + XmlParam("points", "integer", 5), + XmlParam("regular_polygon", "bool", False), + ] + + +class LineCap(enum.Enum): + Rounded = 1 + Squared = 2 + Peak = 3 + Flat = 4 + InnerRounded = 5 + OffPeak = 6 + + +class CuspStyle(enum.Enum): + Miter = 0 + Round = 1 + Bevel = 2 + + +class AbstractOutline(ComplexShape): + _nodes = [ + XmlParam("width", "real", 0.1), + XmlParam("expand", "real", 0.), + XmlParamSif("bline", Bline), + ] + + +class OutlineLayer(AbstractOutline): + _layer_type = "outline" + + _nodes = [ + XmlParam("sharp_cusps", "bool", True), + XmlParam("round_tip[0]", "bool", True), + XmlParam("round_tip[1]", "bool", True), + XmlParam("homogeneous_width", "bool", True), + ] + + @property + def start_tip(self): + return LineCap.Rounded if self.round_tip_0 else LineCap.Flat + + @property + def end_tip(self): + return LineCap.Rounded if self.round_tip_1 else LineCap.Flat + + @property + def cusp_type(self): + return CuspStyle.Miter if self.sharp_cusps else CuspStyle.Round + + +class AdvancedOutlineLayer(AbstractOutline): + _layer_type = "advanced_outline" + + _nodes = [ + XmlParam("start_tip", "integer", LineCap.Rounded, LineCap), + XmlParam("end_tip", "integer", LineCap.Rounded, LineCap), + XmlParam("cusp_type", "integer", CuspStyle.Miter, CuspStyle), + XmlParam("smoothness", "real", 1.), + XmlParam("homogeneous", "bool", False), + # TODO wplist + ] + + +class PolygonLayer(ComplexShape): + _layer_type = "polygon" + + _nodes = [ + XmlDynamicListParam("vector_list", "vector", "points"), + ] + + +class RegionLayer(ComplexShape): + _layer_type = "region" + + _nodes = [ + XmlParamSif("bline", Bline), + ] + + +class FontStyle(enum.Enum): + Normal = 0 + Oblique = 1 + Italic = 2 + + +class TextLayer(DrawableLayer): + _layer_type = "text" + + _nodes = [ + XmlParam("text", "string"), + XmlParam("color", "color", NVector(0, 0, 0, 1)), + XmlParam("family", "string"), + XmlParam("style", "integer", FontStyle.Normal, FontStyle), + XmlParam("weight", "integer", 400), + XmlParam("compress", "real", 1.), + XmlParam("vcompress", "real", 1.), + XmlParam("size", "vector", NVector(1, 1)), + XmlParam("orient", "vector", NVector(.5, .5)), + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("use_kerning", "bool", False), + XmlParam("grid_fit", "bool", False), + XmlParam("invert", "bool", False), + ] + + +class TransformDown(Layer): + pass + + +class TranslateLayer(TransformDown): + _layer_type = "translate" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + ] + + +class RotateLayer(TransformDown): + _layer_type = "rotate" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("amount", "angle", 0.), + ] + + +class ScaleLayer(TransformDown): + _layer_type = "zoom" + + _nodes = [ + XmlParam("center", "vector", NVector(0, 0)), + XmlParam("amount", "real", 0.), + ] + + +class GradientLayer(DrawableLayer): + _nodes = [ + XmlParam("gradient", "gradient", []), + XmlParam("loop", "bool", False), + XmlParam("zigzag", "bool", False), + ] + + +class RadialGradient(GradientLayer): + _layer_type = "radial_gradient" + + _nodes = [ + XmlParam("center", "vector", NVector(0, 0)), + XmlParam("radius", "real", 1), + ] + + +class LinearGradient(GradientLayer): + _layer_type = "linear_gradient" + + _nodes = [ + XmlParam("p1", "vector", NVector(0, 0)), + XmlParam("p2", "vector", NVector(0, 0)), + ] + + +class ConicalLinearGradient(DrawableLayer): + _layer_type = "conical_gradient" + + _nodes = [ + XmlParam("gradient", "gradient", []), + XmlParam("symmetric", "bool", False), + XmlParam("center", "vector", NVector(0, 0)), + XmlParam("angle", "angle", 0.), + ] + + +class CurveGradient(GradientLayer): + _layer_type = "curve_gradient" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("width", "real", 0.0833333358), + XmlParamSif("bline", Bline), + XmlParam("perpendicular", "bool", False), + XmlParam("fast", "bool", True), + ] + + +class NoiseLayer(DrawableLayer): + _layer_type = "noise" + + _nodes = [ + XmlParam("gradient", "gradient", []), + XmlParam("seed", "integer", 0), + XmlParam("size", "vector", NVector(1, 1)), + XmlParam("smooth", "integer", Smooth.Cosine, Smooth), + XmlParam("detail", "integer", 4), + XmlParam("speed", "integer", 0.), + XmlParam("turbulent", "bool", False), + XmlParam("do_alpha", "bool", False), + XmlParam("super_sample", "bool", False), + ] + + +class SpiralGradient(DrawableLayer): + _layer_type = "spiral_gradient" + + _nodes = [ + XmlParam("gradient", "gradient", []), + XmlParam("center", "vector", NVector(0, 0)), + XmlParam("radius", "real", 0.5), + XmlParam("angle", "real", 0), + XmlParam("clockwise", "bool", False), + ] + + +class BoneRoot(SifNode): + _tag = "bone_root" + + _nodes = [ + XmlFixedAttribute("type", "bone_object"), + XmlAttribute("guid", str) + ] + + @classmethod + def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry): + if xml.tagName == "bone_root": + val = SifNode.static_from_dom(BoneRoot, xml, registry) + else: + val = SifNode.static_from_dom(Bone, xml, registry) + registry.register(val) + return val + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.guid) + + +class Bone(BoneRoot): + _tag = "bone" + + _nodes = [ + XmlWrapper("name", XmlSimpleElement("string", att_name="name")), + XmlBoneReference("parent"), + XmlAnimatable("origin", "vector", NVector(0, 0)), + XmlAnimatable("angle", "angle", 0.), + XmlAnimatable("scalelx", "real", 1.), + XmlAnimatable("width", "real", .1), + XmlAnimatable("scalex", "real", 1.), + XmlAnimatable("tipwidth", "real", .1), + XmlAnimatable("bone_depth", "real", 0.), + XmlAnimatable("length", "real", 1.), + ] + + +class BoneLinkTransform(AbstractTransform): + _tag = "bone_link" + + _nodes = [ + XmlBoneReference("bone"), + XmlSifElement("base_value", SifTransform), + XmlAnimatable("translate", "bool", True), + XmlAnimatable("rotate", "bool", True), + XmlAnimatable("skew", "bool", True), + XmlAnimatable("scale_x", "bool", True), + XmlAnimatable("scale_y", "bool", True), + ] + + +class SkeletonLayer(Layer): + _layer_type = "skeleton" + + _nodes = [ + XmlParam("z_depth", "real", 0.), + XmlParam("amount", "real", 1.), + XmlParam("name", "string"), + XmlStaticListParam("bones", "bone_object") + ] + + +class SubsamplingType(enum.Enum): + Constant = 0 + Linear = 1 + Hyperbolic = 2 + + +class MotionBlurLayer(Layer): + _layer_type = "MotionBlur" + + _nodes = [ + XmlParam("aperture", "time", FrameTime(1, FrameTime.Unit.Seconds)), + XmlParam("subsamples_factor", "real", 1.), + XmlParam("subsampling_type", "integer", SubsamplingType.Hyperbolic, SubsamplingType), + XmlParam("subsample_start", "real", 0.), + XmlParam("subsample_end", "real", 1.), + ] + + +class BlurLayer(DrawableLayer): + _layer_type = "blur" + + _nodes = [ + XmlParam("size", "vector", NVector(1, 1)), + XmlParam("type", "integer", BlurType.FastGaussian, BlurType), + ] + + +class RadialBlurLayer(DrawableLayer): + _layer_type = "radial_blur" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("size", "real", .2), + XmlParam("fade_out", "bool", False), + ] + + +class CurveWarpLayer(Layer): + _layer_type = "curve_warp" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("perp_width", "real", 1.), + XmlParam("start_point", "vector", NVector(0, 0)), + XmlParam("end_point", "vector", NVector(0, 0)), + XmlParamSif("bline", Bline), + XmlParam("fast", "bool", True), + ] + + +class InsideOutLayer(Layer): + _layer_type = "inside_out" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + ] + + +class NoiseDistortLayer(DrawableLayer): + _layer_type = "noise_distort" + + _nodes = [ + XmlParam("displacement", "vector", NVector(0.25, 0.25)), + XmlParam("size", "vector", NVector(1, 1)), + XmlParam("seed", "integer", 0), + XmlParam("smooth", "integer", Smooth.Cosine, Smooth), + XmlParam("detail", "integer", 4), + XmlParam("speed", "real", 0.), + XmlParam("turbulent", "bool", False), + ] + + +class SkeletonDeformationLayer(DrawableLayer): + _layer_type = "skeleton_deformation" + + _nodes = [ + XmlParam("displacement", "vector", NVector(0.25, 0.25)), + XmlParam("point1", "vector", NVector(0, 0)), + XmlParam("point2", "vector", NVector(0, 0)), + XmlParam("x_subdivisions", "integer", 32), + XmlParam("y_subdivisions", "integer", 32), + # TODO bones (pair_bone_object_bone_object) + ] + + +class DistortType(enum.Enum): + Spherize = 0 + VerticalBar = 1 + HorizontalBar = 2 + + +class SpherizeLayer(Layer): + _layer_type = "spherize" + + _nodes = [ + XmlParam("center", "vector", NVector(0., 0.)), + XmlParam("radius", "real", 1.), + XmlParam("amount", "real", 1.), + XmlParam("clip", "bool", False), + XmlParam("type", "integer", DistortType.Spherize, DistortType), + ] + + +class StretchLayer(Layer): + _layer_type = "stretch" + + _nodes = [ + XmlParam("amount", "vector", NVector(1., 1.)), + XmlParam("center", "vector", NVector(0., 0.)), + ] + + +class TwirlLayer(Layer): + _layer_type = "twirl" + + _nodes = [ + XmlParam("center", "vector", NVector(0., 0.)), + XmlParam("radius", "real", 1.), + XmlParam("rotations", "real", 0.), + XmlParam("distort_inside", "bool", True), + XmlParam("distort_outside", "bool", False), + ] + + +class WarpLayer(Layer): + _layer_type = "warp" + + _nodes = [ + XmlParam("src_tl", "vector", NVector(0., 0.)), + XmlParam("src_br", "vector", NVector(0., 0.)), + XmlParam("dest_tl", "vector", NVector(0., 0.)), + XmlParam("dest_tr", "vector", NVector(0., 0.)), + XmlParam("dest_bl", "vector", NVector(0., 0.)), + XmlParam("dest_br", "vector", NVector(0., 0.)), + XmlParam("clip", "bool", True), + XmlParam("interpolation", "integer", Smooth.Cubic, Smooth), + ] + + +class MetaballsLayer(DrawableLayer): + _layer_type = "metaballs" + + _nodes = [ + XmlParam("gradient", "gradient", []), + XmlDynamicListParam("centers", "vector"), + XmlDynamicListParam("radii", "real"), + XmlDynamicListParam("weights", "real"), + XmlParam("threshold", "real", 0.), + XmlParam("threshold1", "real", 1.), + XmlParam("positive", "bool", False), + ] + + +class ClampLayer(Layer): + _layer_type = "clamp" + + _nodes = [ + XmlParam("invert_negative", "bool", False), + XmlParam("clamp_ceiling", "bool", False), + XmlParam("ceiling", "real", 1.), + XmlParam("floor", "real", 0.), + ] + + +class ColorCorrectLayer(Layer): + _layer_type = "colorcorrect" + + _nodes = [ + XmlParam("hue_adjust", "angle", 0.), + XmlParam("brightness", "real", 0.), + XmlParam("contrast", "real", 1.), + XmlParam("exposure", "real", 0.), + XmlParam("gamma", "real", 1.), + ] + + +class HalftoneType(enum.Enum): + Symmetric = 0 + LightOnDark = 2 + Diamond = 3 + Stripe = 4 + + +class Halftone2Layer(DrawableLayer): + _layer_type = "halftone2" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("angle", "angle", 0.), + XmlParam("size", "vector", NVector(0.25, 0.25)), + XmlParam("color_light", "color", NVector(1, 1, 1, 1)), + XmlParam("color_dark", "color", NVector(0, 0, 0, 1)), + XmlParam("type", "integer", HalftoneType.Symmetric, HalftoneType), + ] + + +class Halftone3Layer(DrawableLayer): + _layer_type = "halftone3" + + _nodes = [ + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("size", "vector", NVector(0.25, 0.25)), + XmlParam("type", "integer", HalftoneType.Symmetric, HalftoneType), + XmlParam("subtractive", "bool", True), + + XmlParam("color[0]", "color", NVector(0, 1, 1, 1)), + XmlParam("tone[0].origin", "vector", NVector(0, 0)), + XmlParam("tone[0].angle", "angle", 0.), + + XmlParam("color[1]", "color", NVector(1, 0, 1, 1)), + XmlParam("tone[1].origin", "vector", NVector(0, 0)), + XmlParam("tone[1].angle", "angle", 30.), + + XmlParam("color[2]", "color", NVector(1, 1, 0, 1)), + XmlParam("tone[2].origin", "vector", NVector(0, 0)), + XmlParam("tone[2].angle", "angle", 60.), + ] + + +class LumakeyLayer(DrawableLayer): + _layer_type = "lumakey" + + _nodes = [ + ] + + +class JuliaLayer(DrawableLayer): + _layer_type = "julia" + + _nodes = [ + XmlParam("icolor", "color", NVector(0, 0, 0, 1)), + XmlParam("ocolor", "color", NVector(0, 0, 0, 1)), + XmlParam("color_shift", "real", 0.), + XmlParam("iterations", "integer", 32), + XmlParam("seed", "vector", NVector(0, 0)), + XmlParam("bailout", "real", 2.), + XmlParam("distort_inside", "bool", True), + XmlParam("shade_inside", "bool", True), + XmlParam("solid_inside", "bool", False), + XmlParam("invert_inside", "bool", False), + XmlParam("color_inside", "bool", True), + XmlParam("distort_outside", "bool", True), + XmlParam("shade_outside", "bool", True), + XmlParam("solid_outside", "bool", False), + XmlParam("invert_outside", "bool", False), + XmlParam("color_outside", "bool", False), + XmlParam("color_cycle", "bool", False), + XmlParam("smooth_outside", "bool", True), + XmlParam("broken", "bool", False), + ] + + +class MandelbrotLayer(Layer): + _layer_type = "mandelbrot" + + _nodes = [ + XmlParam("iterations", "integer", 32), + XmlParam("bailout", "real", 2.), + XmlParam("broken", "bool", False), + XmlParam("distort_inside", "bool", True), + XmlParam("shade_inside", "bool", True), + XmlParam("solid_inside", "bool", False), + XmlParam("invert_inside", "bool", False), + XmlParam("distort_outside", "bool", True), + XmlParam("shade_outside", "bool", True), + XmlParam("solid_outside", "bool", False), + XmlParam("invert_outside", "bool", False), + XmlParam("smooth_outside", "bool", True), + + XmlParam("gradient_inside", "gradient", []), + XmlParam("gradient_offset_inside", "real", 0.), + XmlParam("gradient_loop_inside", "bool", True), + + XmlParam("gradient_outside", "gradient", []), + XmlParam("gradient_offset_outside", "real", 0.), + XmlParam("gradient_loop_outside", "bool", True), + XmlParam("gradient_scale_outside", "real", 1.), + ] + + +class CheckerboardLayer(DrawableLayer): + _layer_type = "checker_board" + + _nodes = [ + XmlParam("color", "color", NVector(0, 0, 0, 1)), + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("size", "vector", NVector(0.25, 0.25)), + XmlParam("antialias", "bool", True), + ] + + +class SolidColorLayer(DrawableLayer): + _layer_type = "SolidColor" + + _nodes = [ + XmlParam("color", "color", NVector(0, 0, 0, 1)), + ] + + +class DuplicateLayer(DrawableLayer): + _layer_type = "duplicate" + + _nodes = [ + XmlParam("index", "real"), + ] + + +class ImportedImageLayer(DrawableLayer): + _layer_type = "import" + + _nodes = [ + XmlParam("tl", "vector", NVector(0, 0)), + XmlParam("br", "vector", NVector(0, 0)), + XmlParam("c", "integer", Smooth.Linear, Smooth), + XmlParam("gamma_adjust", "real", 1.), + XmlParam("filename", "string"), + XmlParam("time_offset", "time", FrameTime(0, FrameTime.Unit.Frame)), + ] + + +class PlantLayer(DrawableLayer): + _layer_type = "plant" + + _nodes = [ + XmlParamSif("bline", Bline), + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("gradient", "gradient", []), + XmlParam("split_angle", "angle", 10.), + XmlParam("gravity", "vector", NVector(0, -.1)), + XmlParam("velocity", "real", .3), + XmlParam("perp_velocity", "real", 0.), + XmlParam("size", "real", 0.015), + XmlParam("size_as_alpha", "bool", False), + XmlParam("reverse", "bool", True), + XmlParam("step", "real", 0.01), + XmlParam("seed", "integer", 0), + XmlParam("splits", "integer", 5), + XmlParam("sprouts", "integer", 5), + XmlParam("random_factor", "real", 0.2), + XmlParam("drag", "real", 0.1), + XmlParam("use_width", "bool", True), + ] + + +class SoundLayer(Layer): + _layer_type = "sound" + + _nodes = [ + XmlParam("z_depth", "real", 0.), + XmlParam("filename", "string"), + XmlParam("delay", "time", FrameTime(0, FrameTime.Unit.Seconds)), + XmlParam("volume", "real", 1.), + ] + + +class SuperSampleLayer(Layer): + _layer_type = "super_sample" + + _nodes = [ + XmlParam("width", "integer", 2), + XmlParam("height", "integer", 2), + XmlParam("scanline", "bool", False), + XmlParam("alpha_aware", "bool", True), + ] + + +class XorPatternLayer(DrawableLayer): + _layer_type = "xor_pattern" + + _nodes = [ + XmlParam("origin", "vecor", NVector(0, 0)), + XmlParam("size", "vecor", NVector(0.25, 0.25)), + ] + + +class BevelLayer(DrawableLayer): + _layer_type = "bevel" + + _nodes = [ + XmlParam("type", "integer", BlurType.FastGaussian, BlurType), + XmlParam("color1", "color", NVector(1, 1, 1, 1)), + XmlParam("color2", "color", NVector(0, 0, 0, 1)), + XmlParam("angle", "angle", 135.), + XmlParam("depth", "real", .2), + XmlParam("softness", "real", .1), + XmlParam("use_luma", "bool", False), + XmlParam("solid", "bool", False), + ] + + +class ShadeLayer(DrawableLayer): + _layer_type = "shade" + + _nodes = [ + XmlParam("type", "integer", BlurType.FastGaussian, BlurType), + XmlParam("color", "color", NVector(1, 1, 1, 1)), + XmlParam("origin", "vector", NVector(0, 0)), + XmlParam("size", "vector", NVector(0.1, 0.1)), + XmlParam("invert", "bool", False), + ] + + +class FreeTimeLayer(Layer): + _layer_type = "freetime" + + _nodes = [ + XmlParam("z_depth", "real", 0.), + XmlParam("time", "time", FrameTime(0, FrameTime.Unit.Seconds)), + ] + + +class StroboscopeLayer(Layer): + _layer_type = "stroboscope" + + _nodes = [ + XmlParam("z_depth", "real", 0.), + XmlParam("frequency", "real", 2.), + ] + + +class TimeLoopLayer(Layer): + _layer_type = "timeloop" + + _nodes = [ + XmlParam("z_depth", "real", 0.), + XmlParam("link_time", "time", FrameTime(0, FrameTime.Unit.Seconds), static=True), + XmlParam("local_time", "time", FrameTime(0, FrameTime.Unit.Seconds), static=True), + XmlParam("duration", "time", FrameTime(0, FrameTime.Unit.Seconds), static=True), + XmlParam("only_for_positive_duration", "bool", False, static=True), + XmlParam("symmetrical", "bool", True, static=True), + ] + + +class Keyframe(SifNode): + _nodes = [ + XmlAttribute("active", bool_str, True), + XmlAttribute("time", FrameTime, FrameTime(0, FrameTime.Unit.Frame)), + ] + + +class Canvas(SifNode, ObjectRegistry): + _nodes = [ + XmlAttribute("version"), + XmlAttribute("width", float, 512), + XmlAttribute("height", float, 512), + XmlAttribute("xres", float, 2834.645752), + XmlAttribute("yres", float, 2834.645752), + XmlAttribute("gamma-r", float, 1.), + XmlAttribute("gamma-g", float, 1.), + XmlAttribute("gamma-b", float, 1.), + XmlAttribute("view-box", NVector), + XmlAttribute("antialias", bool, True), + XmlAttribute("fps", float, 60), + XmlAttribute("begin-time", FrameTime, FrameTime(0, FrameTime.Unit.Frame)), + XmlAttribute("end-time", FrameTime, FrameTime(3, FrameTime.Unit.Seconds)), + XmlAttribute("bgcolor", NVector, NVector(0, 0, 0, 0)), + XmlSimpleElement("name"), + XmlMeta("background_first_color", NVector, NVector(0.88, 0.88, 0.88)), + XmlMeta("background_rendering", bool, False), + XmlMeta("background_second_color", NVector, NVector(0.65, 0.65, 0.65)), + XmlMeta("background_size", NVector, NVector(15, 15)), + XmlMeta("grid_color", NVector, NVector(0.62, 0.62, 0.62)), + XmlMeta("grid_show", bool, False), + XmlMeta("grid_size", NVector, NVector(0.25, 0.25)), + XmlMeta("grid_snap", bool, False), + XmlMeta("guide_color", NVector, NVector(0.4, 0.4, 1)), + XmlMeta("guide_show", bool, True), + XmlMeta("guide_snap", bool, False), + XmlMeta("jack_offset", float, 0), + XmlMeta("onion_skin", bool, False), + XmlMeta("onion_skin_future", int, 0), + XmlMeta("onion_skin_past", int, 1), + XmlList(Keyframe), + XmlWrapper("defs", XmlList(Def, "defs", None, Def.tags())), + XmlWrapper("bones", XmlList(BoneRoot, "bones", None, {"bone", "bone_root"})), + XmlList(Layer), + ] + + def __init__(self, **kw): + SifNode.__init__(self, **kw) + ObjectRegistry.__init__(self) + + def to_xml(self): + dom = minidom.Document() + dom.appendChild(self.to_dom(dom)) + return dom + + @classmethod + def from_xml_file(cls, xml): + if isinstance(xml, str): + with open(xml, "r") as file: + return cls.from_xml(minidom.parse(file)) + return cls.from_xml(minidom.parse(xml)) + + @classmethod + def from_xml_string(cls, xml): + return cls.from_xml(minidom.parseString(xml)) + + @classmethod + def from_xml(cls, xml: minidom.Document): + obj = cls.from_dom(xml.documentElement, None) + xml.unlink() + return obj + + @classmethod + def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry = None): + instance = cls() + for node in cls._nodes: + node.from_xml(instance, xml, instance) + return instance + + def time_to_frames(self, time: FrameTime): + if time.unit == FrameTime.Unit.Frame: + return time.value + elif time.unit == FrameTime.Unit.Seconds: + return time.value * self.fps + + def add_layer(self, layer: Layer): + self.layers.append(layer) + return layer + + def make_color(self, r, g, b, a=1): + """ + Applies Gamma to the rgb values + """ + return NVector( + r ** self.gamma_r, + g ** self.gamma_g, + b ** self.gamma_b, + a + ) + + +class Segment(SifNode): + _nodes = [ + XmlAnimatable("p1", "vector"), + XmlAnimatable("t1", "vector"), + XmlAnimatable("p2", "vector"), + XmlAnimatable("t2", "vector") + ] + + +class WeightedVector(SifNode): + _tag = "weighted_vector" + + _nodes = [ + XmlAnimatable("weight", "real", 1.), + XmlAnimatable("value", "vector"), + ] diff --git a/lottie/parsers/sif/xml/__init__.py b/lottie/parsers/sif/xml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lottie/parsers/sif/xml/__pycache__/__init__.cpython-310.pyc b/lottie/parsers/sif/xml/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..8cdb68a Binary files /dev/null and b/lottie/parsers/sif/xml/__pycache__/__init__.cpython-310.pyc differ diff --git a/lottie/parsers/sif/xml/__pycache__/animatable.cpython-310.pyc b/lottie/parsers/sif/xml/__pycache__/animatable.cpython-310.pyc new file mode 100644 index 0000000..a37aa2d Binary files /dev/null and b/lottie/parsers/sif/xml/__pycache__/animatable.cpython-310.pyc differ diff --git a/lottie/parsers/sif/xml/__pycache__/core_nodes.cpython-310.pyc b/lottie/parsers/sif/xml/__pycache__/core_nodes.cpython-310.pyc new file mode 100644 index 0000000..7aa4fe3 Binary files /dev/null and b/lottie/parsers/sif/xml/__pycache__/core_nodes.cpython-310.pyc differ diff --git a/lottie/parsers/sif/xml/__pycache__/utils.cpython-310.pyc b/lottie/parsers/sif/xml/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..fd75966 Binary files /dev/null and b/lottie/parsers/sif/xml/__pycache__/utils.cpython-310.pyc differ diff --git a/lottie/parsers/sif/xml/__pycache__/wrappers.cpython-310.pyc b/lottie/parsers/sif/xml/__pycache__/wrappers.cpython-310.pyc new file mode 100644 index 0000000..ccc4a14 Binary files /dev/null and b/lottie/parsers/sif/xml/__pycache__/wrappers.cpython-310.pyc differ diff --git a/lottie/parsers/sif/xml/animatable.py b/lottie/parsers/sif/xml/animatable.py new file mode 100644 index 0000000..b770a06 --- /dev/null +++ b/lottie/parsers/sif/xml/animatable.py @@ -0,0 +1,160 @@ +from xml.dom import minidom +import copy +import enum + +from .core_nodes import XmlDescriptor, XmlSimpleElement, ValueReference +from .utils import * +from lottie.nvector import NVector +from lottie.parsers.sif.ast_impl.base import SifAstNode, SifValue +from lottie.parsers.sif.sif.core import ObjectRegistry, TypeDescriptor, noop + + +class XmlAnimatable(XmlDescriptor): + def __init__(self, name, typename, default=None, type_wrapper=noop): + super().__init__(name) + self.type = TypeDescriptor(typename, default, type_wrapper) + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry, param: TypeDescriptor = None): + cn = xml_first_element_child(parent, self.name) + if cn: + value = SifAstNode.from_dom(xml_first_element_child(cn), self.type_for(param), registry) + else: + value = self.default() + + setattr(obj, self.att_name, value) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document, type: TypeDescriptor = None): + value = getattr(obj, self.att_name) + if isinstance(value, SifValue) and isinstance(value.value, ValueReference): + parent.setAttribute(self.name, ":" + value.value.id) + return + param = parent.appendChild(dom.createElement(self.name)) + param.appendChild(value.to_dom(dom, self.type_for(type))) + return param + + def from_python(self, value): + if not isinstance(value, SifAstNode): + raise ValueError("%s isn't a valid value for %s" % (value, self.name)) + return value + + def default(self): + return SifValue(copy.deepcopy(self.type.default_value)) + + def type_for(self, param: TypeDescriptor): + if param is not None and self.type.typename == "_recurse": + return param + return self.type + + +class XmlParam(XmlDescriptor): + def __init__(self, name, typename, default=None, type_wrapper=noop, static=False): + super().__init__(name) + self.type = TypeDescriptor(typename, default, type_wrapper) + self.static = static + + def _def(self): + from ..api import Def + return Def + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + for cn in xml_child_elements(parent, "param"): + if cn.getAttribute("name") == self.name: + use = cn.getAttribute("use") + if use: + value = registry.get_object(use) + else: + value_node = xml_first_element_child(cn) + if self.static: + value = self.type.value_from_xml_element(value_node, registry) + else: + value = SifAstNode.from_dom(value_node, self.type, registry) + break + else: + value = self.default() + + setattr(obj, self.att_name, value) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + param = parent.appendChild(dom.createElement("param")) + param.setAttribute("name", self.name) + value = getattr(obj, self.att_name) + if isinstance(value, self._def()): + param.setAttribute("use", ":" + value.id) + else: + if self.static: + elem = self.type.value_to_xml_element(value, dom) + else: + elem = value.to_dom(dom, self.type) + param.appendChild(elem) + return param + + def from_python(self, value): + if self.static: + return self.type.type_wrapper(value) + if not isinstance(value, (SifAstNode, self._def())): + raise ValueError("%s isn't a valid value for %s" % (value, self.name)) + return value + + def default(self): + if self.static: + return copy.deepcopy(self.type.default_value) + return SifValue(copy.deepcopy(self.type.default_value)) + + +class XmlDynamicListParam(XmlDescriptor): + _tag = "dynamic_list" + + def __init__(self, name, typename, att_name=None): + super().__init__(name) + self.type = TypeDescriptor(typename) + if att_name is not None: + self.att_name = att_name + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + param = parent.appendChild(dom.createElement("param")) + param.setAttribute("name", self.name) + dyl = param.appendChild(dom.createElement(self._tag)) + dyl.setAttribute("type", self.type.typename) + values = getattr(obj, self.att_name) + for val in values: + entry = dyl.appendChild(dom.createElement("entry")) + entry.appendChild(self._value_to_dom(val, dom)) + + def _value_to_dom(self, val, dom: minidom.Document): + return val.to_dom(dom, self.type) + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + values = [] + + for cn in xml_child_elements(parent, "param"): + if cn.getAttribute("name") == self.name: + list = xml_first_element_child(cn) + if list.getAttribute("type") != self.type.typename: + raise ValueError( + "Wrong type for %s: got %s instead of %s" % + (self.name, self.type.typename, list.getAttribute("type")) + ) + for entry in xml_child_elements(list, "entry"): + values.append(self._value_from_dom(xml_first_element_child(entry), registry)) + break + + setattr(obj, self.att_name, values) + + def _value_from_dom(self, element, registry): + return SifAstNode.from_dom(element, self.type, registry) + + def from_python(self, value): + return value + + def default(self): + return [] + + +class XmlStaticListParam(XmlDynamicListParam): + _tag = "static_list" + + def _value_to_dom(self, val, dom: minidom.Document): + return self.type.value_to_xml_element(val, dom) + + def _value_from_dom(self, element: minidom.Element, registry: ObjectRegistry): + return self.type.value_from_xml_element(element, registry) diff --git a/lottie/parsers/sif/xml/core_nodes.py b/lottie/parsers/sif/xml/core_nodes.py new file mode 100644 index 0000000..70b1bcf --- /dev/null +++ b/lottie/parsers/sif/xml/core_nodes.py @@ -0,0 +1,142 @@ +from xml.dom import minidom +import copy +from uuid import uuid4 + +from .utils import * +from lottie.parsers.sif.sif.core import ObjectRegistry + + +class XmlDescriptor: + def __init__(self, name): + self.name = name + self.att_name = name.replace("-", "_").replace("[", "_").replace("]", "").replace(".", "_") + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + raise NotImplementedError + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + raise NotImplementedError + + def from_python(self, value): + raise NotImplementedError + + def initialize_object(self, dict, obj): + if self.att_name in dict: + setattr(obj, self.att_name, self.from_python(dict[self.att_name])) + else: + setattr(obj, self.att_name, self.default()) + + def clean(self, value): + return self.from_python(value) + + def default(self): + return None + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.name) + + +class TypedXmlDescriptor(XmlDescriptor): + def __init__(self, name, type=str, default_value=None, att_name=None): + super().__init__(name) + self.type = type + self.default_value = default_value + if att_name is not None: + self.att_name = att_name + + def from_python(self, value): + if value is None and self.default_value is None: + return None + if not value_isinstance(value, self.type): + return self.type(value) + return value + + def default(self): + return copy.deepcopy(self.default_value) + + +class ValueReference: + def __init__(self, id, value=None): + self.value = value + self.id = id + + @classmethod + def from_registry(cls, id, registry: ObjectRegistry): + return cls(id, registry.get_object(id)) + + +class XmlAttribute(TypedXmlDescriptor): + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + xml_str = parent.getAttribute(self.name) + if xml_str: + if xml_str.startswith(":") and xml_str[1:] in registry.registry: + value = ValueReference.from_registry(xml_str[1:], registry) + else: + value = value_from_xml_string(xml_str, self.type, registry) + setattr(obj, self.att_name, value) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + value = getattr(obj, self.att_name) + if value is not None: + if isinstance(value, ValueReference): + xml_str = ":" + value.id + else: + xml_str = value_to_xml_string(value, self.type) + parent.setAttribute(self.name, xml_str) + + +class XmlFixedAttribute(XmlDescriptor): + def __init__(self, name, value, type=str): + super().__init__(name) + self.value = value + self.type = type + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + parent.setAttribute(self.name, value_to_xml_string(self.value, self.type)) + + def from_python(self, value): + if value != self.value: + raise ValueError("Value of %s should be %s, got %s" % (self.name, self.value, value)) + return value + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + xml_str = parent.getAttribute(self.name) + setattr(obj, self.att_name, value_from_xml_string(xml_str, self.type, registry)) + + def default(self): + return self.value + + +class XmlSimpleElement(TypedXmlDescriptor): + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + cn = xml_first_element_child(parent, self.name, allow_none=True) + if cn: + value = value_from_xml_string(xml_text(cn), self.type, registry) + else: + value = self.default_value + + setattr(obj, self.att_name, value) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + value = getattr(obj, self.att_name) + if value is not None: + parent.appendChild(xml_make_text(dom, self.name, value_to_xml_string(value, self.type))) + + +class XmlMeta(TypedXmlDescriptor): + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + for cn in xml_child_elements(parent, "meta"): + if cn.getAttribute("name") == self.name: + value = value_from_xml_string(cn.getAttribute("content"), self.type, registry) + break + else: + value = self.default_value + + setattr(obj, self.att_name, value) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + value = getattr(obj, self.att_name) + if value is not None: + meta = parent.appendChild(dom.createElement("meta")) + meta.setAttribute("name", self.name) + meta.setAttribute("content", value_to_xml_string(value, self.type)) diff --git a/lottie/parsers/sif/xml/utils.py b/lottie/parsers/sif/xml/utils.py new file mode 100644 index 0000000..7331280 --- /dev/null +++ b/lottie/parsers/sif/xml/utils.py @@ -0,0 +1,85 @@ +from xml.dom import minidom +from distutils.util import strtobool + +from lottie.nvector import NVector +from lottie.parsers.sif.sif.frame_time import FrameTime + + +class _tag: + def __init__(self, type): + self.type = type + + def __call__(self, v): + return self.type(v) + + +bool_str = _tag(bool) + + +def str_to_bool(strval): + return bool(strtobool(strval)) + + +def value_from_xml_string(xml_str, type, registry): + if type in (bool_str, bool): + return str_to_bool(xml_str) + elif type is NVector: + return NVector(*map(float, xml_str.split())) + if type is FrameTime: + return FrameTime.parse_string(xml_str, registry) + return type(xml_str) + + +def value_to_xml_string(value, type): + if type is bool: + return "1" if value else "0" + if type is bool_str: + return "true" if value else "false" + elif type is NVector: + return " ".join(map(str, value)) + return str(value) + + +def value_isinstance(value, type): + if isinstance(type, _tag): + type = type.type + return isinstance(value, type) + + +def xml_text(node): + return "".join( + x.nodeValue + for x in node.childNodes + if x.nodeType in {minidom.Node.TEXT_NODE, minidom.Node.CDATA_SECTION_NODE} + ) + + +def xml_make_text(dom: minidom.Document, tag_name, text): + e = dom.createElement(tag_name) + e.appendChild(dom.createTextNode(text)) + return e + + +def xml_element_matches(ch: minidom.Node, tagname=None): + if ch.nodeType != minidom.Node.ELEMENT_NODE: + return False + + if tagname is not None and ch.tagName != tagname: + return False + + return True + + +def xml_child_elements(xml: minidom.Node, tagname=None): + for ch in xml.childNodes: + if xml_element_matches(ch, tagname): + yield ch + + +def xml_first_element_child(xml: minidom.Node, tagname=None, allow_none=False): + for ch in xml_child_elements(xml, tagname): + return ch + + if allow_none: + return None + raise ValueError("No %s in %s" % (tagname or "child element", getattr(xml, "tagName", "node"))) diff --git a/lottie/parsers/sif/xml/wrappers.py b/lottie/parsers/sif/xml/wrappers.py new file mode 100644 index 0000000..a811654 --- /dev/null +++ b/lottie/parsers/sif/xml/wrappers.py @@ -0,0 +1,217 @@ +from .utils import * +from .core_nodes import XmlDescriptor, ObjectRegistry + + +class XmlParamSif(XmlDescriptor): + def __init__(self, name, child_node, default_ctor=None): + super().__init__(name) + self.child_node = child_node + self.default_ctor = default_ctor or child_node + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + for cn in xml_child_elements(parent, "param"): + if cn.getAttribute("name") == self.name: + value = self.child_node.from_dom(xml_first_element_child(cn), registry) + break + else: + value = self.default() + + setattr(obj, self.att_name, value) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + param = parent.appendChild(dom.createElement("param")) + param.setAttribute("name", self.name) + param.appendChild(getattr(obj, self.att_name).to_dom(dom)) + return param + + def clean(self, value): + if not isinstance(value, self.child_node): + raise ValueError("%s isn't a valid value for %s" % (value, self.name)) + return value + + def default(self): + return self.default_ctor() + + +class SifNodeList: + def __init__(self, type): + self._items = [] + self._type = type + + def __len__(self): + return len(self._items) + + def __iter__(self): + return iter(self._items) + + def __getitem__(self, name): + return self._items[name] + + def __getslice__(self, i, j): + return self._items[i:j] + + def __setitem__(self, key, value: "Layer"): + self.validate(value) + self._items[key] = value + + def append(self, value: "Layer"): + self.validate(value) + self._items.append(value) + + def __str__(self): + return str(self._items) + + def __repr__(self): + return "" % self._items + + def validate(self, value): + if not isinstance(value, self._type): + raise ValueError("Not a valid object: %s" % value) + + +class XmlSifElement(XmlDescriptor): + def __init__(self, name, child_node, nested=True): + super().__init__(name) + self.child_node = child_node + self.nested = nested + + def default(self): + return self.child_node() + + def from_python(self, value): + if not isinstance(value, self.child_node): + raise ValueError("Invalid value for %s: %s" % (self.name, value)) + return value + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + cn = xml_first_element_child(parent, self.name, allow_none=True) + if cn: + if self.nested: + element = xml_first_element_child(cn) + else: + element = cn + value = self.child_node.from_dom(element, registry) + else: + value = self.default() + + setattr(obj, self.att_name, value) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + value = getattr(obj, self.att_name) + if self.nested: + node = dom.createElement(self.name) + parent.appendChild(node) + else: + node = parent + node.appendChild(value.to_dom(dom)) + + +class XmlList(XmlDescriptor): + def __init__(self, child_node, name=None, wrapper_tag=None, tags=None): + super().__init__(wrapper_tag or child_node._tag) + self.child_node = child_node + self.att_name = self.att_name + "s" if name is None else name + self.wrapper_tag = wrapper_tag + if tags is None: + self.tags = {self.name} + else: + self.tags = tags + + def default(self): + return SifNodeList(self.child_node) + + def clean(self, value): + return value + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + values = self.default() + for cn in xml_child_elements(parent): + if cn.tagName in self.tags: + value_node = cn + if self.wrapper_tag: + value_node = xml_first_element_child(cn) + values.append(self.child_node.from_dom(value_node, registry)) + + setattr(obj, self.att_name, values) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + for value in getattr(obj, self.att_name): + value_node = value.to_dom(dom) + if self.wrapper_tag: + wrapper = dom.createElement(self.wrapper_tag) + wrapper.appendChild(value_node) + value_node = wrapper + parent.appendChild(value_node) + + +class XmlWrapper(XmlDescriptor): + def __init__(self, name, wrapped: XmlDescriptor): + super().__init__(name) + self.wrapped = wrapped + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + wrapper = parent.appendChild(dom.createElement(self.name)) + self.wrapped.to_xml(obj, wrapper, dom) + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + wrapper = xml_first_element_child(parent, self.name, True) + if wrapper: + return self.wrapped.from_xml(obj, wrapper, registry) + return self.default() + + def from_python(self, value): + return self.wrapped.from_python(value) + + def initialize_object(self, dict, obj): + return self.wrapped.initialize_object(dict, obj) + + def clean(self, value): + return self.wrapped.clean(value) + + def default(self): + return self.wrapped.default() + + +class XmlWrapperParam(XmlWrapper): + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + wrapper = parent.appendChild(dom.createElement("param")) + wrapper.setAttribute("name", self.name) + self.wrapped.to_xml(obj, wrapper, dom) + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + for wrapper in xml_child_elements(parent, "param"): + if wrapper.getAttribute("name") == self.name: + return self.wrapped.from_xml(obj, wrapper, registry) + return self.default() + + +class XmlBoneReference(XmlDescriptor): + def __init__(self, name): + super().__init__(name) + + def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document): + value = getattr(obj, self.name, None) + if not value: + return + + node = parent.appendChild(dom.createElement(self.name)) + value_node = node.appendChild(dom.createElement("bone_valuenode")) + value_node.setAttribute("type", value.type) + value_node.setAttribute("guid", value.guid) + + return node + + def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry): + node = xml_first_element_child(parent, self.name, True) + value = None + if node: + value_node = xml_first_element_child(node, "bone_valuenode", True) + if value_node: + value = registry.get_object(value_node.getAttribute("guid")) + if value.type != value_node.getAttribute("type"): + raise ValueError("Bone type %s is not %s" % (value.type, value_node.getAttribute("type"))) + + setattr(obj, self.att_name, value) + + def from_python(self, value): + return value diff --git a/lottie/parsers/svg/__init__.py b/lottie/parsers/svg/__init__.py new file mode 100644 index 0000000..9d3254f --- /dev/null +++ b/lottie/parsers/svg/__init__.py @@ -0,0 +1,3 @@ +from .importer import parse_svg_etree, parse_svg_file +from . import builder, importer +__all__ = ["builder", "importer", "parse_svg_etree", "parse_svg_file"] diff --git a/lottie/parsers/svg/__pycache__/__init__.cpython-310.pyc b/lottie/parsers/svg/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..2608a80 Binary files /dev/null and b/lottie/parsers/svg/__pycache__/__init__.cpython-310.pyc differ diff --git a/lottie/parsers/svg/__pycache__/builder.cpython-310.pyc b/lottie/parsers/svg/__pycache__/builder.cpython-310.pyc new file mode 100644 index 0000000..431db4f Binary files /dev/null and b/lottie/parsers/svg/__pycache__/builder.cpython-310.pyc differ diff --git a/lottie/parsers/svg/__pycache__/handler.cpython-310.pyc b/lottie/parsers/svg/__pycache__/handler.cpython-310.pyc new file mode 100644 index 0000000..2eec4f4 Binary files /dev/null and b/lottie/parsers/svg/__pycache__/handler.cpython-310.pyc differ diff --git a/lottie/parsers/svg/__pycache__/importer.cpython-310.pyc b/lottie/parsers/svg/__pycache__/importer.cpython-310.pyc new file mode 100644 index 0000000..7909131 Binary files /dev/null and b/lottie/parsers/svg/__pycache__/importer.cpython-310.pyc differ diff --git a/lottie/parsers/svg/__pycache__/svgdata.cpython-310.pyc b/lottie/parsers/svg/__pycache__/svgdata.cpython-310.pyc new file mode 100644 index 0000000..59e21f2 Binary files /dev/null and b/lottie/parsers/svg/__pycache__/svgdata.cpython-310.pyc differ diff --git a/lottie/parsers/svg/builder.py b/lottie/parsers/svg/builder.py new file mode 100644 index 0000000..1d4fa78 --- /dev/null +++ b/lottie/parsers/svg/builder.py @@ -0,0 +1,719 @@ +import re +import math +from xml.etree import ElementTree + +from .handler import SvgHandler, NameMode +from ... import objects +from ...nvector import NVector +from ...utils import restructure +from ...utils.transform import TransformMatrix +try: + from ...utils import font + has_font = True +except ImportError: + has_font = False + + +class PrecompTime: + def __init__(self, pcl: objects.PreCompLayer): + self.pcl = pcl + + def get_time_offset(self, time, lot): + remap = time + if self.pcl.time_remapping: + remapf = self.pcl.time_remapping.get_value(time) + remap = lot.in_point * (1-remapf) + lot.out_point * remapf + + return remap - self.pcl.start_time + + +class SvgBuilder(SvgHandler, restructure.AbstractBuilder): + merge_paths = True + namestart = ( + r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" + + r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" + + r"\uFDF0-\uFFFD\U00010000-\U000EFFFF" + ) + namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040" + id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart)) + + def __init__(self, time=0): + super().__init__() + self.svg = ElementTree.Element("svg") + self.dom = ElementTree.ElementTree(self.svg) + self.svg.attrib["xmlns"] = self.ns_map["svg"] + self.ids = set() + self.idc = 0 + self.name_mode = NameMode.Inkscape + self.actual_time = time + self.precomp_times = [] + self._precomps = {} + self._assets = {} + self._current_layer = [] + + @property + def time(self): + time = self.actual_time + if self.precomp_times: + for pct in self.precomp_times: + time = pct.get_time_offset(time, self._current_layer[-1]) + return time + + def gen_id(self, prefix="id"): + while True: + self.idc += 1 + id = "%s_%s" % (prefix, self.idc) + if id not in self.ids: + break + self.ids.add(id) + return id + + def set_clean_id(self, dom, n): + idn = n.replace(" ", "_") + if self.id_re.match(idn) and idn not in self.ids: + self.ids.add(idn) + else: + idn = self.gen_id(dom.tag) + + dom.attrib["id"] = idn + return idn + + def set_id(self, dom, lottieobj, inkscape_qual=None, force=False): + n = getattr(lottieobj, "name", None) + if n is None or self.name_mode == NameMode.NoName: + if force: + id = self.gen_id(dom.tag) + dom.attrib["id"] = id + return id + return None + + idn = self.set_clean_id(dom, n) + if inkscape_qual is None: + inkscape_qual = self.qualified("inkscape", "label") + if inkscape_qual: + dom.attrib[inkscape_qual] = n + return idn + + def _on_animation(self, animation: objects.Animation): + self.svg.attrib["width"] = str(animation.width) + self.svg.attrib["height"] = str(animation.height) + self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height) + self.svg.attrib["version"] = "1.1" + self.set_id(self.svg, animation, self.qualified("sodipodi", "docname")) + self.defs = ElementTree.SubElement(self.svg, "defs") + if self.name_mode == NameMode.Inkscape: + self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96" + self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96" + namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview")) + namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true" + namedview.attrib["borderlayer"] = "true" + namedview.attrib["bordercolor"] = "#666666" + namedview.attrib["pagecolor"] = "#ffffff" + self.svg.attrib["style"] = "fill: none; stroke: none" + + self._current_layer = [animation] + return self.svg + + def _mask_to_def(self, mask): + svgmask = ElementTree.SubElement(self.defs, "mask") + mask_id = self.gen_id() + svgmask.attrib["id"] = mask_id + svgmask.attrib["mask-type"] = "alpha" + path = ElementTree.SubElement(svgmask, "path") + path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time)) + path.attrib["fill"] = "#fff" + path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100) + return mask_id + + def _matte_source_to_def(self, layer_builder): + svgmask = ElementTree.SubElement(self.defs, "mask") + if not layer_builder.matte_id: + layer_builder.matte_id = self.gen_id() + svgmask.attrib["id"] = layer_builder.matte_id + matte_mode = layer_builder.matte_target.lottie.matte_mode + + mask_type = "alpha" + if matte_mode == objects.MatteMode.Luma: + mask_type = "luminance" + svgmask.attrib["mask-type"] = mask_type + return svgmask + + def _on_masks(self, masks): + if len(masks) == 1: + return self._mask_to_def(masks[0]) + mask_ids = list(map(self._mask_to_def, masks)) + mask_def = ElementTree.SubElement(self.defs, "mask") + mask_id = self.gen_id() + mask_def.attrib["id"] = mask_id + g = mask_def + for mid in mask_ids: + g = ElementTree.SubElement(g, "g") + g.attrib["mask"] = "url(#%s)" % mid + full = ElementTree.SubElement(g, "rect") + full.attrib["fill"] = "#fff" + full.attrib["width"] = self.svg.attrib["width"] + full.attrib["height"] = self.svg.attrib["height"] + full.attrib["x"] = "0" + full.attrib["y"] = "0" + return mask_id + + def _on_layer(self, layer_builder, dom_parent): + lot = layer_builder.lottie + self._current_layer.append(lot) + + if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time): + self._current_layer.pop() + return None + + if layer_builder.matte_target: + dom_parent = self._matte_source_to_def(layer_builder) + + g = self.group_from_lottie(lot, dom_parent, True) + + if lot.masks: + g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks) + elif layer_builder.matte_source: + matte_id = layer_builder.matte_source.matte_id + if not matte_id: + matte_id = layer_builder.matte_source.matte_id = self.gen_id() + g.attrib["mask"] = "url(#%s)" % matte_id + + if isinstance(lot, objects.PreCompLayer): + self.precomp_times.append(PrecompTime(lot)) + + for layer in self._precomps.get(lot.reference_id, []): + self.process_layer(layer, g) + + self.precomp_times.pop() + elif isinstance(lot, objects.NullLayer): + g.attrib["opacity"] = "1" + elif isinstance(lot, objects.ImageLayer): + use = ElementTree.SubElement(g, "use") + use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id] + elif isinstance(lot, objects.TextLayer): + self._on_text_layer(g, lot) + elif isinstance(lot, objects.SolidColorLayer): + rect = ElementTree.SubElement(g, "rect") + rect.attrib["width"] = str(lot.width) + rect.attrib["height"] = str(lot.height) + rect.attrib["fill"] = lot.color + + if not lot.name: + g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__ + if layer_builder.shapegroup: + g.attrib["style"] = self.group_to_style(layer_builder.shapegroup) + self._split_stroke(layer_builder.shapegroup, g, dom_parent) + #if lot.hidden: + #g.attrib.setdefault("style", "") + #g.attrib["style"] += "display: none;" + + return g + + def _on_text_layer(self, g, lot): + text = ElementTree.SubElement(g, "text") + doc = lot.data.get_value(self.time) + if doc: + text.attrib["font-family"] = doc.font_family + text.attrib["font-size"] = str(doc.font_size) + if doc.line_height: + text.attrib["line-height"] = "%s%%" % doc.line_height + if doc.justify == objects.text.TextJustify.Left: + text.attrib["text-align"] = "start" + elif doc.justify == objects.text.TextJustify.Center: + text.attrib["text-align"] = "center" + elif doc.justify == objects.text.TextJustify.Right: + text.attrib["text-align"] = "end" + + text.attrib["fill"] = color_to_css(doc.color) + text.text = doc.text + + def _on_layer_end(self, out_layer): + self._current_layer.pop() + + def _on_precomp(self, id, dom_parent, layers): + self._precomps[id] = layers + + def _on_asset(self, asset): + if isinstance(asset, objects.assets.Image): + img = ElementTree.SubElement(self.defs, "image") + xmlid = self.set_clean_id(img, asset.id) + self._assets[asset.id] = xmlid + if asset.is_embedded: + url = asset.image + else: + url = asset.image_path + asset.image + img.attrib[self.qualified("xlink", "href")] = url + img.attrib["width"] = str(asset.width) + img.attrib["height"] = str(asset.height) + + def _get_value(self, prop, default=NVector(0, 0)): + if prop: + v = prop.get_value(self.time) + else: + v = default + + if v is None: + return default + if isinstance(v, NVector): + return v.clone() + return v + + def set_transform(self, dom, transform, auto_orient=False): + mat = transform.to_matrix(self.time, auto_orient) + dom.attrib["transform"] = mat.to_css_2d() + + if transform.opacity is not None: + op = transform.opacity.get_value(self.time) + if op != 100: + dom.attrib["opacity"] = str(op/100) + + def _get_group_stroke(self, group): + style = {} + if group.stroke: + if isinstance(group.stroke, objects.GradientStroke): + style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke) + else: + style["stroke"] = color_to_css(group.stroke.color.get_value(self.time)) + + style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100 + style["stroke-width"] = group.stroke.width.get_value(self.time) + if group.stroke.miter_limit is not None: + style["stroke-miterlimit"] = group.stroke.miter_limit + + if group.stroke.line_cap == objects.LineCap.Round: + style["stroke-linecap"] = "round" + elif group.stroke.line_cap == objects.LineCap.Butt: + style["stroke-linecap"] = "butt" + elif group.stroke.line_cap == objects.LineCap.Square: + style["stroke-linecap"] = "square" + + if group.stroke.line_join == objects.LineJoin.Round: + style["stroke-linejoin"] = "round" + elif group.stroke.line_join == objects.LineJoin.Bevel: + style["stroke-linejoin"] = "bevel" + elif group.stroke.line_join == objects.LineJoin.Miter: + style["stroke-linejoin"] = "miter" + + if group.stroke.dashes: + dasharray = [] + last = 0 + last_mode = objects.StrokeDashType.Dash + for dash in group.stroke.dashes: + if last_mode == dash.type: + last += dash.length.get_value(self.time) + else: + if last_mode != objects.StrokeDashType.Offset: + dasharray.append(str(last)) + last = 0 + last_mode = dash.type + style["stroke-dasharray"] = " ".join(dasharray) + return style + + def _style_to_css(self, style): + return ";".join(map( + lambda x: ":".join(map(str, x)), + style.items() + )) + + def _split_stroke(self, group, fill_layer, out_parent): + if not group.stroke:# or group.stroke_above: + return + + style = self._get_group_stroke(group) + if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0: + return + + if group.stroke_above: + if fill_layer.attrib.get("style", ""): + fill_layer.attrib["style"] += ";" + else: + fill_layer.attrib["style"] = "" + fill_layer.attrib["style"] += self._style_to_css(style) + return fill_layer + + g = ElementTree.Element("g") + self.set_clean_id(g, "stroke") + use = ElementTree.Element("use") + for i, e in enumerate(out_parent): + if e is fill_layer: + out_parent.insert(i, g) + out_parent.remove(fill_layer) + break + else: + return + + g.append(use) + g.append(fill_layer) + + use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"] + use.attrib["style"] = self._style_to_css(style) + return g + + def group_to_style(self, group): + style = {} + if group.fill: + style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100 + if isinstance(group.fill, objects.GradientFill): + style["fill"] = "url(#%s)" % self.process_gradient(group.fill) + else: + style["fill"] = color_to_css(group.fill.color.get_value(self.time)) + + if group.fill.fill_rule: + style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero" + + if group.lottie.hidden: + style["display"] = "none" + #if group.stroke_above: + #style.update(self._get_group_stroke(group)) + + return self._style_to_css(style) + + def process_gradient(self, gradient): + spos = gradient.start_point.get_value(self.time) + epos = gradient.end_point.get_value(self.time) + + if gradient.gradient_type == objects.GradientType.Linear: + dom = ElementTree.SubElement(self.defs, "linearGradient") + dom.attrib["x1"] = str(spos[0]) + dom.attrib["y1"] = str(spos[1]) + dom.attrib["x2"] = str(epos[0]) + dom.attrib["y2"] = str(epos[1]) + elif gradient.gradient_type == objects.GradientType.Radial: + dom = ElementTree.SubElement(self.defs, "radialGradient") + dom.attrib["cx"] = str(spos[0]) + dom.attrib["cy"] = str(spos[1]) + dom.attrib["r"] = str((epos-spos).length) + a = gradient.highlight_angle.get_value(self.time) * math.pi / 180 + l = gradient.highlight_length.get_value(self.time) + dom.attrib["fx"] = str(spos[0] + math.cos(a) * l) + dom.attrib["fy"] = str(spos[1] + math.sin(a) * l) + + id = self.set_id(dom, gradient, force=True) + dom.attrib["gradientUnits"] = "userSpaceOnUse" + + for off, color in gradient.colors.stops_at(self.time): + stop = ElementTree.SubElement(dom, "stop") + stop.attrib["offset"] = "%s%%" % (off * 100) + stop.attrib["stop-color"] = color_to_css(color[:3]) + if len(color) > 3: + stop.attrib["stop-opacity"] = str(color[3]) + + return id + + def group_from_lottie(self, lottie, dom_parent, layer): + g = ElementTree.SubElement(dom_parent, "g") + if layer and self.name_mode == NameMode.Inkscape: + g.attrib[self.qualified("inkscape", "groupmode")] = "layer" + self.set_id(g, lottie, force=True) + self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False)) + return g + + def _on_shapegroup(self, group, dom_parent): + if group.empty(): + return + + if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger): + path = self.build_path(group.paths.paths, dom_parent) + self.set_id(path, group.paths.paths[0], force=True) + path.attrib["style"] = self.group_to_style(group) + self.set_transform(path, group.lottie.transform) + return self._split_stroke(group, path, dom_parent) + + g = self.group_from_lottie(group.lottie, dom_parent, group.layer) + g.attrib["style"] = self.group_to_style(group) + self.shapegroup_process_children(group, g) + return self._split_stroke(group, g, dom_parent) + + def _on_merged_path(self, shape, shapegroup, out_parent): + path = self.build_path(shape.paths, out_parent) + self.set_id(path, shape.paths[0]) + path.attrib["style"] = self.group_to_style(shapegroup) + #self._split_stroke(shapegroup, path, out_parent) + return path + + def _on_shape(self, shape, shapegroup, out_parent): + if isinstance(shape, objects.Rect): + svgshape = self.build_rect(shape, out_parent) + elif isinstance(shape, objects.Ellipse): + svgshape = self.build_ellipse(shape, out_parent) + elif isinstance(shape, objects.Star): + svgshape = self.build_path([shape.to_bezier()], out_parent) + elif isinstance(shape, objects.Path): + svgshape = self.build_path([shape], out_parent) + elif has_font and isinstance(shape, font.FontShape): + svgshape = self.build_text(shape, out_parent) + else: + return + self.set_id(svgshape, shape, force=True) + if "style" not in svgshape.attrib: + svgshape.attrib["style"] = "" + svgshape.attrib["style"] += self.group_to_style(shapegroup) + #self._split_stroke(shapegroup, svgshape, out_parent) + + if shape.hidden: + svgshape.attrib["style"] += "display: none;" + return svgshape + + def build_rect(self, shape, parent): + rect = ElementTree.SubElement(parent, "rect") + size = shape.size.get_value(self.time) + pos = shape.position.get_value(self.time) + rect.attrib["width"] = str(size[0]) + rect.attrib["height"] = str(size[1]) + rect.attrib["x"] = str(pos[0] - size[0] / 2) + rect.attrib["y"] = str(pos[1] - size[1] / 2) + rect.attrib["rx"] = str(shape.rounded.get_value(self.time)) + return rect + + def build_ellipse(self, shape, parent): + ellipse = ElementTree.SubElement(parent, "ellipse") + size = shape.size.get_value(self.time) + pos = shape.position.get_value(self.time) + ellipse.attrib["rx"] = str(size[0] / 2) + ellipse.attrib["ry"] = str(size[1] / 2) + ellipse.attrib["cx"] = str(pos[0]) + ellipse.attrib["cy"] = str(pos[1]) + return ellipse + + def build_path(self, shapes, parent): + path = ElementTree.SubElement(parent, "path") + d = "" + for shape in shapes: + bez = shape.shape.get_value(self.time) + if isinstance(bez, list): + bez = bez[0] + if not bez.vertices: + continue + if d: + d += "\n" + d += self._bezier_to_d(bez) + + path.attrib["d"] = d + return path + + def _bezier_tangent(self, tangent): + _tangent_threshold = 0.5 + if tangent.length < _tangent_threshold: + return NVector(0, 0) + return tangent + + def _bezier_to_d(self, bez): + d = "M %s,%s " % tuple(bez.vertices[0].components[:2]) + for i in range(1, len(bez.vertices)): + qfrom = bez.vertices[i-1] + h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom + qto = bez.vertices[i] + h2 = self._bezier_tangent(bez.in_tangents[i]) + qto + + d += "C %s,%s %s,%s %s,%s " % ( + h1[0], h1[1], + h2[0], h2[1], + qto[0], qto[1], + ) + if bez.closed: + qfrom = bez.vertices[-1] + h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom + qto = bez.vertices[0] + h2 = self._bezier_tangent(bez.in_tangents[0]) + qto + d += "C %s,%s %s,%s %s,%s Z" % ( + h1[0], h1[1], + h2[0], h2[1], + qto[0], qto[1], + ) + + return d + + def _on_shape_modifier(self, shape, shapegroup, out_parent): + if isinstance(shape.lottie, objects.Repeater): + svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent) + elif isinstance(shape.lottie, objects.RoundedCorners): + svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent) + elif isinstance(shape.lottie, objects.Trim): + svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent) + else: + return self.shapegroup_process_child(shape.child, shapegroup, out_parent) + return svgshape + + def build_repeater(self, shape, child, shapegroup, out_parent): + original = self.shapegroup_process_child(child, shapegroup, out_parent) + if not original: + return + + ncopies = int(round(shape.copies.get_value(self.time))) + if ncopies == 1: + return + + out_parent.remove(original) + + g = ElementTree.SubElement(out_parent, "g") + self.set_clean_id(g, "repeater") + + for copy in range(ncopies-1): + use = ElementTree.SubElement(g, "use") + use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"] + + orig_wrapper = ElementTree.SubElement(g, "g") + orig_wrapper.append(original) + + transform = objects.Transform() + so = shape.transform.start_opacity.get_value(self.time) + eo = shape.transform.end_opacity.get_value(self.time) + position = shape.transform.position.get_value(self.time) + rotation = shape.transform.rotation.get_value(self.time) + anchor_point = shape.transform.anchor_point.get_value(self.time) + for i in range(ncopies-1, -1, -1): + of = i / (ncopies-1) + transform.opacity.value = so * of + eo * (1 - of) + self.set_transform(g[i], transform) + transform.position.value += position + transform.rotation.value += rotation + transform.anchor_point.value += anchor_point + + return g + + def build_rouded_corners(self, shape, child, shapegroup, out_parent): + round_amount = shape.radius.get_value(self.time) + return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount) + + def _build_rouded_corners_shape(self, shape, round_amount): + if not isinstance(shape, objects.Shape): + return [shape] + path = shape.to_bezier() + bezier = path.shape.get_value(self.time).rounded(round_amount) + path.shape.clear_animation(bezier) + return [path] + + def build_trim_path(self, shape, child, shapegroup, out_parent): + start = max(0, min(1, shape.start.get_value(self.time) / 100)) + end = max(0, min(1, shape.end.get_value(self.time) / 100)) + offset = shape.offset.get_value(self.time) / 360 % 1 + + multidata = {} + length = 0 + + if shape.multiple == objects.TrimMultipleShapes.Individually: + for visishape in reversed(list(self._modifier_foreach_shape(child))): + bez = visishape.to_bezier().shape.get_value(self.time) + local_length = bez.rough_length() + multidata[visishape] = (bez, length, local_length) + length += local_length + + return self._modifier_process( + child, shapegroup, out_parent, self._build_trim_path_shape, + start+offset, end+offset, multidata, length + ) + + def _modifier_foreach_shape(self, shape): + if isinstance(shape, restructure.RestructuredShapeGroup): + for child in shape.children: + for chsh in self._modifier_foreach_shape(child): + yield chsh + elif isinstance(shape, restructure.RestructuredPathMerger): + for p in shape.paths: + yield p + elif isinstance(shape, objects.Shape): + yield shape + + def _modifier_process(self, child, shapegroup, out_parent, callback, *args): + children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args) + return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children] + + def _trim_offlocal(self, t, local_start, local_length, total_length): + gt = (t * total_length - local_start) / local_length + return max(0, min(1, gt)) + + def _build_trim_path_shape(self, shape, start, end, multidata, total_length): + if not isinstance(shape, objects.Shape): + return [shape] + + if multidata: + bezier, local_start, local_length = multidata[shape] + if end > 1: + lstart = self._trim_offlocal(start, local_start, local_length, total_length) + lend = self._trim_offlocal(end-1, local_start, local_length, total_length) + out = [] + if lstart < 1: + out.append(objects.Path(bezier.segment(lstart, 1))) + if lend > 0: + out.append(objects.Path(bezier.segment(0, lend))) + return out + + lstart = self._trim_offlocal(start, local_start, local_length, total_length) + lend = self._trim_offlocal(end, local_start, local_length, total_length) + if lend <= 0 or lstart >= 1: + return [] + if lstart <= 0 and lend >= 1: + return [objects.Path(bezier)] + seg = bezier.segment(lstart, lend) + return [objects.Path(seg)] + + path = shape.to_bezier() + bezier = path.shape.get_value(self.time) + if end > 1: + bez1 = bezier.segment(start, 1) + bez2 = bezier.segment(0, end-1) + return [objects.Path(bez1), objects.Path(bez2)] + else: + seg = bezier.segment(start, end) + return [objects.Path(seg)] + + def _modifier_process_children(self, shapegroup, out_parent, callback, *args): + children = [] + for shape in shapegroup.children: + children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args)) + shapegroup.children = children + + def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args): + if isinstance(shape, restructure.RestructuredShapeGroup): + self._modifier_process_children(shape, out_parent, callback, *args) + return [shape] + elif isinstance(shape, restructure.RestructuredPathMerger): + paths = [] + for p in shape.paths: + paths.extend(callback(p, *args)) + shape.paths = paths + if paths: + return [shape] + return [] + else: + return callback(shape, *args) + + def _custom_object_supported(self, shape): + if has_font and isinstance(shape, font.FontShape): + return True + return False + + def build_text(self, shape, parent): + text = ElementTree.SubElement(parent, "text") + if "family" in shape.query: + text.attrib["font-family"] = shape.query["family"] + if "weight" in shape.query: + text.attrib["font-weight"] = str(shape.query.weight_to_css()) + slant = int(shape.query.get("slant", 0)) + if slant > 0 and slant < 110: + text.attrib["font-style"] = "italic" + elif slant >= 110: + text.attrib["font-style"] = "oblique" + + text.attrib["font-size"] = str(shape.size) + + text.attrib["white-space"] = "pre" + + pos = shape.style.position + text.attrib["x"] = str(pos.x) + text.attrib["y"] = str(pos.y) + text.text = shape.text + + return text + + +def color_to_css(color): + #if len(color) == 4: + #return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3] + return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3])) + + +def to_svg(animation, time): + builder = SvgBuilder(time) + builder.process(animation) + return builder.dom diff --git a/lottie/parsers/svg/handler.py b/lottie/parsers/svg/handler.py new file mode 100644 index 0000000..16b9b7f --- /dev/null +++ b/lottie/parsers/svg/handler.py @@ -0,0 +1,38 @@ +import enum +from xml.etree import ElementTree + + +class SvgHandler: + ns_map = { + "dc": "http://purl.org/dc/elements/1.1/", + "cc": "http://creativecommons.org/ns#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "svg": "http://www.w3.org/2000/svg", + "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", + "inkscape": "http://www.inkscape.org/namespaces/inkscape", + "xlink": "http://www.w3.org/1999/xlink", + } + + def init_etree(self): + for n, u in self.ns_map.items(): + ElementTree.register_namespace(n, u) + + def qualified(self, ns, name): + return "{%s}%s" % (self.ns_map[ns], name) + + def simplified(self, name): + for k, v in self.ns_map.items(): + name = name.replace("{%s}" % v, k+":") + return name + + def unqualified(self, name): + return name.split("}")[-1] + + def __init__(self): + self.init_etree() + + +class NameMode(enum.Enum): + NoName = 0 + Id = 1 + Inkscape = 2 diff --git a/lottie/parsers/svg/importer.py b/lottie/parsers/svg/importer.py new file mode 100644 index 0000000..91f7ee9 --- /dev/null +++ b/lottie/parsers/svg/importer.py @@ -0,0 +1,1300 @@ +import re +import math +import colorsys +from xml.etree import ElementTree +from ... import objects +from ...nvector import NVector +from .svgdata import color_table, css_atrrs +from .handler import SvgHandler, NameMode +from ...utils.ellipse import Ellipse +from ...utils.transform import TransformMatrix +from ...utils.color import Color + +try: + from ...utils import font + has_font = True +except ImportError: + has_font = False + +nocolor = {"none"} + + +class SvgGradientCoord: + def __init__(self, name, comp, value, percent): + self.name = name + self.comp = comp + self.value = value + self.percent = percent + + def to_value(self, bbox, default=None): + if self.value is None: + return default + + if not self.percent: + return self.value + + if self.comp == "w": + return (bbox.x2 - bbox.x1) * self.value + + if self.comp == "x": + return bbox.x1 + (bbox.x2 - bbox.x1) * self.value + + return bbox.y1 + (bbox.y2 - bbox.y1) * self.value + + def parse(self, attr, default_percent): + if attr is None: + return + if attr.endswith("%"): + self.percent = True + self.value = float(attr[:-1])/100 + else: + self.percent = default_percent + self.value = float(attr) + + +class SvgGradient: + def __init__(self): + self.colors = [] + self.coords = [] + self.matrix = TransformMatrix() + + def add_color(self, offset, color): + self.colors.append((offset, color[:4])) + + def to_lottie(self, gradient_shape, shape, time=0): + """! + @param gradient_shape Should be a GradientFill or GradientStroke + @param shape ShapeElement to apply the gradient to + @param time Time to fetch properties from @p shape + + """ + for off, col in self.colors: + gradient_shape.colors.add_color(off, col) + + def add_coord(self, value): + setattr(self, value.name, value) + self.coords.append(value) + + def parse_attrs(self, attrib): + relunits = attrib.get("gradientUnits", "") != "userSpaceOnUse" + for c in self.coords: + c.parse(attrib.get(c.name, None), relunits) + + +class SvgLinearGradient(SvgGradient): + def __init__(self): + super().__init__() + self.add_coord(SvgGradientCoord("x1", "x", 0, True)) + self.add_coord(SvgGradientCoord("y1", "y", 0, True)) + self.add_coord(SvgGradientCoord("x2", "x", 1, True)) + self.add_coord(SvgGradientCoord("y2", "y", 0, True)) + + def to_lottie(self, gradient_shape, shape, time=0): + bbox = shape.bounding_box(time) + gradient_shape.start_point.value = self.matrix.apply(NVector( + self.x1.to_value(bbox), + self.y1.to_value(bbox), + )) + gradient_shape.end_point.value = self.matrix.apply(NVector( + self.x2.to_value(bbox), + self.y2.to_value(bbox), + )) + gradient_shape.gradient_type = objects.GradientType.Linear + + super().to_lottie(gradient_shape, shape, time) + + +class SvgRadialGradient(SvgGradient): + def __init__(self): + super().__init__() + self.add_coord(SvgGradientCoord("cx", "x", 0.5, True)) + self.add_coord(SvgGradientCoord("cy", "y", 0.5, True)) + self.add_coord(SvgGradientCoord("fx", "x", None, True)) + self.add_coord(SvgGradientCoord("fy", "y", None, True)) + self.add_coord(SvgGradientCoord("r", "w", 0.5, True)) + + def to_lottie(self, gradient_shape, shape, time=0): + bbox = shape.bounding_box(time) + cx = self.cx.to_value(bbox) + cy = self.cy.to_value(bbox) + gradient_shape.start_point.value = self.matrix.apply(NVector(cx, cy)) + r = self.r.to_value(bbox) + gradient_shape.end_point.value = self.matrix.apply(NVector(cx+r, cy)) + + fx = self.fx.to_value(bbox, cx) - cx + fy = self.fy.to_value(bbox, cy) - cy + gradient_shape.highlight_angle.value = math.atan2(fy, fx) * 180 / math.pi + gradient_shape.highlight_length.value = math.hypot(fx, fy) + + gradient_shape.gradient_type = objects.GradientType.Radial + + super().to_lottie(gradient_shape, shape, time) + + +def parse_color(color, current_color=Color(0, 0, 0, 1)): + """! + Parses CSS colors + + @see https://www.w3.org/wiki/CSS/Properties/color + """ + # #fff + if re.match(r"^#[0-9a-fA-F]{6}$", color): + return Color(int(color[1:3], 16) / 0xff, int(color[3:5], 16) / 0xff, int(color[5:7], 16) / 0xff, 1) + # #112233 + if re.match(r"^#[0-9a-fA-F]{3}$", color): + return Color(int(color[1], 16) / 0xf, int(color[2], 16) / 0xf, int(color[3], 16) / 0xf, 1) + # rgba(123, 123, 123, 0.7) + match = re.match(r"^rgba\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9.eE]+)\s*\)$", color) + if match: + return Color(int(match[1])/255, int(match[2])/255, int(match[3])/255, float(match[4])) + # rgb(123, 123, 123) + match = re.match(r"^rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)$", color) + if match: + return Color(int(match[1])/255, int(match[2])/255, int(match[3])/255, 1) + # rgb(60%, 30%, 20%) + match = re.match(r"^rgb\s*\(\s*([0-9]+)%\s*,\s*([0-9]+)%\s*,\s*([0-9]+)%\s*\)$", color) + if match: + return Color(int(match[1])/100, int(match[2])/100, int(match[3])/100, 1) + # rgba(60%, 30%, 20%, 0.7) + match = re.match(r"^rgba\s*\(\s*([0-9]+)%\s*,\s*([0-9]+)%\s*,\s*([0-9]+)%\s*,\s*([0-9.eE]+)\s*\)$", color) + if match: + return Color(int(match[1])/100, int(match[2])/100, int(match[3])/100, float(match[4])) + # transparent + if color == "transparent": + return Color(0, 0, 0, 0) + # hsl(60, 30%, 20%) + match = re.match(r"^hsl\s*\(\s*([0-9]+)\s*,\s*([0-9]+)%\s*,\s*([0-9]+)%\s*\)$", color) + if match: + return Color(*(colorsys.hls_to_rgb(int(match[1])/360, int(match[3])/100, int(match[2])/100) + (1,))) + # hsla(60, 30%, 20%, 0.7) + match = re.match(r"^hsla\s*\(\s*([0-9]+)\s*,\s*([0-9]+)%\s*,\s*([0-9]+)%\s*,\s*([0-9.eE]+)\s*\)$", color) + if match: + return Color(*(colorsys.hls_to_rgb(int(match[1])/360, int(match[3])/100, int(match[2])/100) + (float(match[4]),))) + # currentColor + if color in {"currentColor", "inherit"}: + return current_color.clone() + # red + return Color(*color_table[color]) + + +class SvgDefsParent: + def __init__(self): + self.items = {} + + def insert(self, dummy, shape): + self.items[shape.name] = shape + + def __getitem__(self, key): + return self.items[key] + + def __setitem__(self, key, value): + self.items[key] = value + + def __contains__(self, key): + return key in self.items + + @property + def shapes(self): + return self + + +class SvgParser(SvgHandler): + def __init__(self, name_mode=NameMode.Inkscape): + self.init_etree() + self.name_mode = name_mode + self.current_color = Color(0, 0, 0, 1) + self.gradients = {} + self.max_time = 0 + self.defs = SvgDefsParent() + self.dpi = 96 + + def _get_name(self, element, inkscapequal): + if self.name_mode == NameMode.Inkscape: + return element.attrib.get(inkscapequal, element.attrib.get("id")) + return self._get_id(element) + + def _get_id(self, element): + if self.name_mode != NameMode.NoName: + return element.attrib.get("id") + return None + + def parse_etree(self, etree, layer_frames=0, *args, **kwargs): + animation = objects.Animation(*args, **kwargs) + self.animation = animation + self.max_time = 0 + self.document = etree + + svg = etree.getroot() + + self._get_dpi(svg) + + if "width" in svg.attrib and "height" in svg.attrib: + animation.width = int(round(self._parse_unit(svg.attrib["width"]))) + animation.height = int(round(self._parse_unit(svg.attrib["height"]))) + else: + _, _, animation.width, animation.height = map(int, svg.attrib["viewBox"].split(" ")) + animation.name = self._get_name(svg, self.qualified("sodipodi", "docname")) + + if layer_frames: + for frame in svg: + if self.unqualified(frame.tag) == "g": + layer = objects.ShapeLayer() + layer.in_point = self.max_time + animation.add_layer(layer) + self._parseshape_g(frame, layer, {}) + self.max_time += layer_frames + layer.out_point = self.max_time + else: + self._svg_to_layer(animation, svg) + + if self.max_time: + animation.out_point = self.max_time + + self._fix_viewbox(svg, (layer for layer in animation.layers if not layer.parent_index)) + + return animation + + def etree_to_layer(self, animation, etree): + svg = etree.getroot() + self._get_dpi(svg) + layer = self._svg_to_layer(animation, svg) + self._fix_viewbox(svg, [layer]) + return layer + + def _get_dpi(self, svg): + self.dpi = float(svg.attrib.get(self.qualified("inkscape", "export-xdpi"), self.dpi)) + + def _svg_to_layer(self, animation, svg): + self.animation = animation + layer = objects.ShapeLayer() + animation.add_layer(layer) + self.parse_children(svg, layer, self.parse_style(svg, {})) + if self.max_time: + for sublayer in layer.find_all(objects.Layer): + sublayer.out_point = self.max_time + return layer + + def _fix_viewbox(self, svg, layers): + if "viewBox" in svg.attrib: + vbx, vby, vbw, vbh = map(float, svg.attrib["viewBox"].split()) + if vbx != 0 or vby != 0 or vbw != self.animation.width or vbh != self.animation.height: + for layer in layers: + layer.transform.position.value = -NVector(vbx, vby) + layer.transform.scale.value = NVector(self.animation.width / vbw, self.animation.height / vbh) * 100 + + def _parse_unit(self, value): + if not isinstance(value, str): + return value + + mult = 1 + cmin = 2.54 + if value.endswith("px"): + value = value[:-2] + elif value.endswith("vw"): + value = value[:-2] + mult = self.animation.width * 0.01 + elif value.endswith("vh"): + value = value[:-2] + mult = self.animation.height * 0.01 + elif value.endswith("vmin"): + value = value[:-4] + mult = min(self.animation.width, self.animation.height) * 0.01 + elif value.endswith("vmax"): + value = value[:-4] + mult = max(self.animation.width, self.animation.height) * 0.01 + elif value.endswith("in"): + value = value[:-2] + mult = self.dpi + elif value.endswith("pc"): + value = value[:-2] + mult = self.dpi / 6 + elif value.endswith("pt"): + value = value[:-2] + mult = self.dpi / 72 + elif value.endswith("cm"): + value = value[:-2] + mult = self.dpi / cmin + elif value.endswith("mm"): + value = value[:-2] + mult = self.dpi / cmin / 10 + elif value.endswith("Q"): + value = value[:-1] + mult = self.dpi / cmin / 40 + + return float(value) * mult + + def parse_color(self, color): + return parse_color(color, self.current_color) + + def parse_transform(self, element, group, dest_trans): + bb = group.bounding_box() + if not bb.isnull(): + itcx = self.qualified("inkscape", "transform-center-x") + if itcx in element.attrib: + cx = float(element.attrib[itcx]) + cy = float(element.attrib[self.qualified("inkscape", "transform-center-y")]) + bbx, bby = bb.center() + cx += bbx + cy = bby - cy + dest_trans.anchor_point.value = NVector(cx, cy) + dest_trans.position.value = NVector(cx, cy) + #else: + #c = bb.center() + #dest_trans.anchor_point.value = c + #dest_trans.position.value = c.clone() + + if "transform" not in element.attrib: + return + + matrix = TransformMatrix() + read_matrix = False + + for t in re.finditer(r"([a-zA-Z]+)\s*\(([^\)]*)\)", element.attrib["transform"]): + name = t[1] + params = list(map(float, t[2].strip().replace(",", " ").split())) + if name == "translate": + dest_trans.position.value += NVector( + params[0], + (params[1] if len(params) > 1 else 0), + ) + elif name == "scale": + xfac = params[0] + dest_trans.scale.value[0] = (dest_trans.scale.value[0] / 100 * xfac) * 100 + yfac = params[1] if len(params) > 1 else xfac + dest_trans.scale.value[1] = (dest_trans.scale.value[1] / 100 * yfac) * 100 + elif name == "rotate": + ang = params[0] + x = y = 0 + if len(params) > 2: + x = params[1] + y = params[2] + ap = NVector(x, y) + dap = ap - dest_trans.position.value + dest_trans.position.value += dap + dest_trans.anchor_point.value += dap + dest_trans.rotation.value = ang + else: + read_matrix = True + self._apply_transform_element_to_matrix(matrix, t) + + if read_matrix: + dest_trans.position.value -= dest_trans.anchor_point.value + dest_trans.anchor_point.value = NVector(0, 0) + trans = matrix.extract_transform() + dest_trans.skew_axis.value = math.degrees(trans["skew_axis"]) + dest_trans.skew.value = -math.degrees(trans["skew_angle"]) + dest_trans.position.value += trans["translation"] + dest_trans.rotation.value -= math.degrees(trans["angle"]) + dest_trans.scale.value *= trans["scale"] + + def parse_style(self, element, parent_style): + style = parent_style.copy() + for att in css_atrrs & set(element.attrib.keys()): + if att in element.attrib: + style[att] = element.attrib[att] + if "style" in element.attrib: + style.update(**dict(map( + lambda x: map(lambda y: y.strip(), x.split(":")), + filter(bool, element.attrib["style"].split(";")) + ))) + return style + + def apply_common_style(self, style, transform): + opacity = float(style.get("opacity", 1)) + transform.opacity.value = opacity * 100 + + def apply_visibility(self, style, object): + if style.get("display", "inline") == "none" or style.get("visibility", "visible") == "hidden": + object.hidden = True + + def add_shapes(self, element, shapes, shape_parent, parent_style): + style = self.parse_style(element, parent_style) + + group = objects.Group() + self.apply_common_style(style, group.transform) + self.apply_visibility(style, group) + group.name = self._get_name(element, self.qualified("inkscape", "label")) + + shape_parent.shapes.insert(0, group) + for shape in shapes: + group.add_shape(shape) + + self._add_style_shapes(style, group) + + self.parse_transform(element, group, group.transform) + + return group + + def _add_style_shapes(self, style, group): + stroke_color = style.get("stroke", "none") + if stroke_color not in nocolor: + if stroke_color.startswith("url"): + stroke = self.get_color_url(stroke_color, objects.GradientStroke, group) + opacity = 1 + else: + stroke = objects.Stroke() + color = self.parse_color(stroke_color) + stroke.color.value = color + opacity = color[3] + group.add_shape(stroke) + + stroke.opacity.value = opacity * float(style.get("stroke-opacity", 1)) * 100 + + stroke.width.value = self._parse_unit(style.get("stroke-width", 1)) + + linecap = style.get("stroke-linecap") + if linecap == "round": + stroke.line_cap = objects.shapes.LineCap.Round + elif linecap == "butt": + stroke.line_cap = objects.shapes.LineCap.Butt + elif linecap == "square": + stroke.line_cap = objects.shapes.LineCap.Square + + linejoin = style.get("stroke-linejoin") + if linejoin == "round": + stroke.line_join = objects.shapes.LineJoin.Round + elif linejoin == "bevel": + stroke.line_join = objects.shapes.LineJoin.Bevel + elif linejoin in {"miter", "arcs", "miter-clip"}: + stroke.line_join = objects.shapes.LineJoin.Miter + + stroke.miter_limit = self._parse_unit(style.get("stroke-miterlimit", 0)) + + dash_array = style.get("stroke-dasharray") + if dash_array and dash_array != "none": + values = list(map(self._parse_unit, dash_array.replace(",", " ").split())) + if len(values) % 2: + values += values + + stroke.dashes = [] + for i in range(0, len(values), 2): + stroke.dashes.append(objects.StrokeDash(values[i], objects.StrokeDashType.Dash)) + stroke.dashes.append(objects.StrokeDash(values[i+1], objects.StrokeDashType.Gap)) + + fill_color = style.get("fill", "inherit") + if fill_color not in nocolor: + if fill_color.startswith("url"): + fill = self.get_color_url(fill_color, objects.GradientFill, group) + opacity = 1 + else: + color = self.parse_color(fill_color) + fill = objects.Fill(color) + opacity = color[3] + opacity *= float(style.get("fill-opacity", 1)) + fill.opacity.value = opacity * 100 + + if style.get("fill-rule", "") == "evenodd": + fill.fill_rule = objects.FillRule.EvenOdd + + group.add_shape(fill) + + def _parseshape_use(self, element, shape_parent, parent_style): + link = element.attrib[self.qualified("xlink", "href")] + if link.startswith("#"): + id = link[1:] + base_element = self.document.find(".//*[@id='%s']" % id) + use_style = self.parse_style(element, parent_style) + used = objects.Group() + shape_parent.add_shape(used) + used.name = "use" + used.transform.position.value.x = float(element.attrib.get("x", 0)) + used.transform.position.value.y = float(element.attrib.get("y", 0)) + self.parse_transform(element, used, used.transform) + self.parse_shape(base_element, used, use_style) + return used + + def _parseshape_g(self, element, shape_parent, parent_style): + group = objects.Group() + shape_parent.shapes.insert(0, group) + style = self.parse_style(element, parent_style) + self.apply_common_style(style, group.transform) + self.apply_visibility(style, group) + group.name = self._get_name(element, self.qualified("inkscape", "label")) + self.parse_children(element, group, style) + self.parse_transform(element, group, group.transform) + if group.hidden: # Lottie web doesn't seem to support .hd + group.transform.opacity.value = 0 + return group + + def _parseshape_ellipse(self, element, shape_parent, parent_style): + ellipse = objects.Ellipse() + ellipse.position.value = NVector( + self._parse_unit(element.attrib["cx"]), + self._parse_unit(element.attrib["cy"]) + ) + ellipse.size.value = NVector( + self._parse_unit(element.attrib["rx"]) * 2, + self._parse_unit(element.attrib["ry"]) * 2 + ) + self.add_shapes(element, [ellipse], shape_parent, parent_style) + return ellipse + + def _parseshape_anim_ellipse(self, ellipse, element, animations): + self._merge_animations(element, animations, "cx", "cy", "position") + self._merge_animations(element, animations, "rx", "ry", "size", lambda x, y: NVector(x, y) * 2) + self._apply_animations(ellipse.position, "position", animations) + self._apply_animations(ellipse.size, "size", animations) + + def _parseshape_circle(self, element, shape_parent, parent_style): + ellipse = objects.Ellipse() + ellipse.position.value = NVector( + self._parse_unit(element.attrib["cx"]), + self._parse_unit(element.attrib["cy"]) + ) + r = self._parse_unit(element.attrib["r"]) * 2 + ellipse.size.value = NVector(r, r) + self.add_shapes(element, [ellipse], shape_parent, parent_style) + return ellipse + + def _parseshape_anim_circle(self, ellipse, element, animations): + self._merge_animations(element, animations, "cx", "cy", "position") + self._apply_animations(ellipse.position, "position", animations) + self._apply_animations(ellipse.size, "r", animations, lambda r: NVector(r, r) * 2) + + def _parseshape_rect(self, element, shape_parent, parent_style): + rect = objects.Rect() + w = self._parse_unit(element.attrib.get("width", 0)) + h = self._parse_unit(element.attrib.get("height", 0)) + rect.position.value = NVector( + self._parse_unit(element.attrib.get("x", 0)) + w / 2, + self._parse_unit(element.attrib.get("y", 0)) + h / 2 + ) + rect.size.value = NVector(w, h) + rx = self._parse_unit(element.attrib.get("rx", 0)) + ry = self._parse_unit(element.attrib.get("ry", 0)) + rect.rounded.value = (rx + ry) / 2 + self.add_shapes(element, [rect], shape_parent, parent_style) + return rect + + def _parseshape_anim_rect(self, rect, element, animations): + self._merge_animations(element, animations, "width", "height", "size", lambda x, y: NVector(x, y)) + self._apply_animations(rect.size, "size", animations) + self._merge_animations(element, animations, "x", "y", "position") + self._merge_animations(element, animations, "position", "size", "position", lambda p, s: p + s / 2) + self._apply_animations(rect.position, "position", animations) + self._merge_animations(element, animations, "rx", "ry", "rounded", lambda x, y: (x + y) / 2) + self._apply_animations(rect.rounded, "rounded", animations) + + def _parseshape_line(self, element, shape_parent, parent_style): + line = objects.Path() + line.shape.value.add_point(NVector( + self._parse_unit(element.attrib["x1"]), + self._parse_unit(element.attrib["y1"]) + )) + line.shape.value.add_point(NVector( + self._parse_unit(element.attrib["x2"]), + self._parse_unit(element.attrib["y2"]) + )) + return self.add_shapes(element, [line], shape_parent, parent_style) + + def _parseshape_anim_line(self, group, element, animations): + line = group.shapes[0] + self._merge_animations(element, animations, "x1", "y1", "p1") + self._merge_animations(element, animations, "x2", "y2", "p2") + self._apply_animations(line.vertices[0], "p1", animations) + self._apply_animations(line.vertices[1], "p2", animations) + + def _handle_poly(self, element): + line = objects.Path() + coords = list(map(float, element.attrib["points"].replace(",", " ").split())) + for i in range(0, len(coords), 2): + line.shape.value.add_point(coords[i:i+2]) + return line + + def _parseshape_polyline(self, element, shape_parent, parent_style): + line = self._handle_poly(element) + return self.add_shapes(element, [line], shape_parent, parent_style) + + def _parseshape_polygon(self, element, shape_parent, parent_style): + line = self._handle_poly(element) + line.shape.value.close() + return self.add_shapes(element, [line], shape_parent, parent_style) + + def _parseshape_path(self, element, shape_parent, parent_style): + d_parser = PathDParser(element.attrib.get("d", "")) + d_parser.parse() + paths = [] + for path in d_parser.paths: + p = objects.Path() + p.shape.value = path + paths.append(p) + #if len(d_parser.paths) > 1: + #paths.append(objects.shapes.Merge()) + return self.add_shapes(element, paths, shape_parent, parent_style) + + def parse_children(self, element, shape_parent, parent_style): + for child in element: + tag = self.unqualified(child.tag) + if not self.parse_shape(child, shape_parent, parent_style): + handler = getattr(self, "_parse_" + tag, None) + if handler: + handler(child) + + def parse_shape(self, element, shape_parent, parent_style): + handler = getattr(self, "_parseshape_" + self.unqualified(element.tag), None) + if handler: + out = handler(element, shape_parent, parent_style) + self.parse_animations(out, element) + if element.attrib.get("id"): + self.defs.items[element.attrib["id"]] = out + return out + return None + + def _parse_defs(self, element): + self.parse_children(element, self.defs, {}) + + def _apply_transform_element_to_matrix(self, matrix, t): + name = t[1] + params = list(map(float, t[2].strip().replace(",", " ").split())) + if name == "translate": + matrix.translate( + params[0], + (params[1] if len(params) > 1 else 0), + ) + elif name == "scale": + xfac = params[0] + yfac = params[1] if len(params) > 1 else xfac + matrix.scale(xfac, yfac) + elif name == "rotate": + ang = params[0] + x = y = 0 + if len(params) > 2: + x = params[1] + y = params[2] + matrix.translate(-x, -y) + matrix.rotate(math.radians(ang)) + matrix.translate(x, y) + else: + matrix.rotate(math.radians(ang)) + elif name == "skewX": + matrix.skew(math.radians(params[0]), 0) + elif name == "skewY": + matrix.skew(0, math.radians(params[0])) + elif name == "matrix": + m = TransformMatrix() + m.a, m.b, m.c, m.d, m.tx, m.ty = params + matrix *= m + + def _transform_to_matrix(self, transform): + matrix = TransformMatrix() + + for t in re.finditer(r"([a-zA-Z]+)\s*\(([^\)]*)\)", transform): + self._apply_transform_element_to_matrix(matrix, t) + + return matrix + + def _gradient(self, element, grad): + grad.matrix = self._transform_to_matrix(element.attrib.get("gradientTransform", "")) + + id = element.attrib["id"] + if id in self.gradients: + grad.colors = self.gradients[id].colors + grad.parse_attrs(element.attrib) + href = element.attrib.get(self.qualified("xlink", "href")) + if href: + srcid = href.strip("#") + if srcid in self.gradients: + src = self.gradients[srcid] + else: + src = grad.__class__() + self.gradients[srcid] = src + grad.colors = src.colors + + for stop in element.findall("./%s" % self.qualified("svg", "stop")): + off = float(stop.attrib["offset"].strip("%")) + if stop.attrib["offset"].endswith("%"): + off /= 100 + style = self.parse_style(stop, {}) + color = self.parse_color(style["stop-color"]) + if "stop-opacity" in style: + color[3] = float(style["stop-opacity"]) + grad.add_color(off, color) + self.gradients[id] = grad + + def _parse_linearGradient(self, element): + self._gradient(element, SvgLinearGradient()) + + def _parse_radialGradient(self, element): + self._gradient(element, SvgRadialGradient()) + + def get_color_url(self, color, gradientclass, shape): + match = re.match(r"""url\(['"]?#([^)'"]+)['"]?\)""", color) + if not match: + return None + id = match[1] + if id not in self.gradients: + return None + grad = self.gradients[id] + outgrad = gradientclass() + grad.to_lottie(outgrad, shape) + if self.name_mode != NameMode.NoName: + grad.name = id + return outgrad + + ## @todo Parse single font property, fallback family etc + def _parse_text_style(self, style, font_style=None): + if "font-family" in style: + font_style.query.family(style["font-family"].strip("'\"")) + + if "font-style" in style: + if style["font-style"] == "oblique": + font_style.query.custom("slant", 110) + elif style["font-style"] == "italic": + font_style.query.custom("slant", 100) + + if "font-weight" in style: + if style["font-weight"] in {"bold", "bolder"}: + font_style.query.weight(200) + elif style["font-weight"] == "lighter": + font_style.query.weight(50) + elif style["font-weight"].isdigit(): + font_style.query.css_weight(int(style["font-weight"])) + + if "font-size" in style: + fz = style["font-size"] + fz_names = { + "xx-small": 8, + "x-small": 16, + "small": 32, + "medium": 64, + "large": 128, + "x-large": 256, + "xx-large": 512, + } + if fz in fz_names: + font_style.size = fz_names[fz] + elif fz == "smaller": + font_style.size /= 2 + elif fz == "larger": + font_style.size *= 2 + elif fz.endswith("px"): + font_style.size = float(fz[:-2]) + elif fz.isnumeric(): + font_style.size = float(fz) + + if "text-align" in style: + ta = style["text-align"] + if ta in ("left", "start"): + font_style.justify = font.TextJustify.Left + elif ta == "center": + font_style.justify = font.TextJustify.Center + elif ta in ("right", "end"): + font_style.justify = font.TextJustify.Right + + def _parse_text_elem(self, element, style, group, parent_style, font_style): + self._parse_text_style(style, font_style) + + if "x" in element.attrib or "y" in element.attrib: + font_style.position = NVector( + float(element.attrib["x"]), + float(element.attrib["y"]), + ) + + childpos = NVector(0, font_style.position.y) + + if element.text: + fs = font.FontShape(element.text, font_style) + fs.refresh() + group.add_shape(fs) + childpos.x = fs.wrapped.next_x + + for child in element: + if child.tag == self.qualified("svg", "tspan"): + child_style = font_style.clone() + child_style.position = childpos.clone() + fs = self._parseshape_text(child, group, parent_style, child_style) + childpos.x = fs.next_x + if child.tail: + child_style = font_style.clone() + child_style.position = childpos.clone() + fs = font.FontShape(child.tail, child_style) + fs.refresh() + group.add_shape(fs) + childpos.x = fs.wrapped.next_x + + group.next_x = childpos.x + + def _parseshape_text(self, element, shape_parent, parent_style, font_style=None): + group = objects.Group() + + style = self.parse_style(element, parent_style) + self.apply_common_style(style, group.transform) + self.apply_visibility(style, group) + group.name = self._get_id(element) + + if has_font: + if font_style is None: + font_style = font.FontStyle("", 64) + self._parse_text_elem(element, style, group, style, font_style) + + style.setdefault("fill", "none") + self._add_style_shapes(style, group) + + ## @todo text-anchor when it doesn't match text-align + #if element.tag == self.qualified("svg", "text"): + #dx = 0 + #dy = 0 + + #ta = style.get("text-anchor", style.get("text-align", "")) + #if ta == "middle": + #dx -= group.bounding_box().width / 2 + #elif ta == "end": + #dx -= group.bounding_box().width + + #if dx or dy: + #ng = objects.Group() + #ng.add_shape(group) + #group.transform.position.value.x += dx + #group.transform.position.value.y += dy + #group = ng + + shape_parent.shapes.insert(0, group) + self.parse_transform(element, group, group.transform) + return group + + def parse_animations(self, lottie, element): + animations = {} + for child in element: + if self.unqualified(child.tag) == "animate": + att = child.attrib["attributeName"] + + from_val = child.attrib["from"] + if att == "d": + ## @todo + continue + else: + from_val = float(from_val) + if "to" in child.attrib: + to_val = float(child.attrib["to"]) + elif "by" in child.attrib: + to_val = float(child.attrib["by"]) + from_val + + begin = self.parse_animation_time(child.attrib.get("begin", 0)) or 0 + if "dur" in child.attrib: + end = (self.parse_animation_time(child.attrib["dur"]) or 0) + begin + elif "end" in child.attrib: + end = self.parse_animation_time(child.attrib["dur"]) or 0 + else: + continue + + if att not in animations: + animations[att] = {} + animations[att][begin] = from_val + animations[att][end] = to_val + if self.max_time < end: + self.max_time = end + + tag = self.unqualified(element.tag) + handler = getattr(self, "_parseshape_anim_" + tag, None) + if handler: + handler(lottie, element, animations) + + def parse_animation_time(self, value): + """! + @see https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#Clock-value + """ + if not value: + return None + try: + seconds = 0 + if ":" in value: + mult = 1 + for elem in reversed(value.split(":")): + seconds += float(elem) * mult + mult *= 60 + elif value.endswith("s"): + seconds = float(value[:-1]) + elif value.endswith("ms"): + seconds = float(value[:-2]) / 1000 + elif value.endswith("min"): + seconds = float(value[:-3]) * 60 + elif value.endswith("h"): + seconds = float(value[:-1]) * 60 * 60 + else: + seconds = float(value) + return seconds * self.animation.frame_rate + except ValueError: + pass + return None + + def _merge_animations(self, element, animations, val1, val2, dest, merge=NVector): + if val1 not in animations and val2 not in animations: + return + + dict1 = list(sorted(animations.pop(val1, {}).items())) + dict2 = list(sorted(animations.pop(val2, {}).items())) + + x = float(element.attrib[val1]) + y = float(element.attrib[val2]) + values = {} + while dict1 or dict2: + if not dict1 or (dict2 and dict1[0][0] > dict2[0][0]): + t, y = dict2.pop(0) + elif not dict2 or dict1[0][0] < dict2[0][0]: + t, x = dict1.pop(0) + else: + t, x = dict1.pop(0) + t, y = dict2.pop(0) + + values[t] = merge(x, y) + + animations[dest] = values + + def _apply_animations(self, animatable, name, animations, transform=lambda v: v): + if name in animations: + for t, v in animations[name].items(): + animatable.add_keyframe(t, transform(v)) + + +class PathDParser: + _re = re.compile("|".join(( + r"[a-zA-Z]", + r"[-+]?[0-9]*\.?[0-9]*[eE][-+]?[0-9]+", + r"[-+]?[0-9]*\.?[0-9]+", + ))) + + def __init__(self, d_string): + self.path = objects.properties.Bezier() + self.paths = [] + self.p = NVector(0, 0) + self.la = None + self.la_type = None + self.tokens = list(map(self.d_subsplit, self._re.findall(d_string))) + self.add_p = True + self.implicit = "M" + + def d_subsplit(self, tok): + if tok.isalpha(): + return tok + return float(tok) + + def next_token(self): + if self.tokens: + self.la = self.tokens.pop(0) + if isinstance(self.la, str): + self.la_type = 0 + else: + self.la_type = 1 + else: + self.la = None + return self.la + + def next_vec(self): + x = self.next_token() + y = self.next_token() + return NVector(x, y) + + def cur_vec(self): + x = self.la + y = self.next_token() + return NVector(x, y) + + def parse(self): + self.next_token() + while self.la is not None: + if self.la_type == 0: + parser = "_parse_" + self.la + self.next_token() + getattr(self, parser)() + else: + parser = "_parse_" + self.implicit + getattr(self, parser)() + + def _push_path(self): + self.path = objects.properties.Bezier() + self.add_p = True + + def _parse_M(self): + if self.la_type != 1: + self.next_token() + return + self.p = self.cur_vec() + self.implicit = "L" + if not self.add_p: + self._push_path() + self.next_token() + + def _parse_m(self): + if self.la_type != 1: + self.next_token() + return + self.p += self.cur_vec() + self.implicit = "l" + if not self.add_p: + self._push_path() + self.next_token() + + def _rpoint(self, point, rel=None): + return (point - (rel or self.p)) if point is not None else NVector(0, 0) + + def _do_add_p(self, outp=None): + if self.add_p: + self.paths.append(self.path) + self.path.add_point(self.p.clone(), NVector(0, 0), self._rpoint(outp)) + self.add_p = False + elif outp: + rp = self.path.vertices[-1] + self.path.out_tangents[-1] = self._rpoint(outp, rp) + + def _parse_L(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + self.p = self.cur_vec() + self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) + self.implicit = "L" + self.next_token() + + def _parse_l(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + self.p += self.cur_vec() + self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) + self.implicit = "l" + self.next_token() + + def _parse_H(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + self.p[0] = self.la + self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) + self.implicit = "H" + self.next_token() + + def _parse_h(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + self.p[0] += self.la + self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) + self.implicit = "h" + self.next_token() + + def _parse_V(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + self.p[1] = self.la + self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) + self.implicit = "V" + self.next_token() + + def _parse_v(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + self.p[1] += self.la + self.path.add_point(self.p.clone(), NVector(0, 0), NVector(0, 0)) + self.implicit = "v" + self.next_token() + + def _parse_C(self): + if self.la_type != 1: + self.next_token() + return + pout = self.cur_vec() + self._do_add_p(pout) + pin = self.next_vec() + self.p = self.next_vec() + self.path.add_point( + self.p.clone(), + (pin - self.p), + NVector(0, 0) + ) + self.implicit = "C" + self.next_token() + + def _parse_c(self): + if self.la_type != 1: + self.next_token() + return + pout = self.p + self.cur_vec() + self._do_add_p(pout) + pin = self.p + self.next_vec() + self.p += self.next_vec() + self.path.add_point( + self.p.clone(), + (pin - self.p), + NVector(0, 0) + ) + self.implicit = "c" + self.next_token() + + def _parse_S(self): + if self.la_type != 1: + self.next_token() + return + pin = self.cur_vec() + self._do_add_p() + handle = self.path.in_tangents[-1] + self.path.out_tangents[-1] = (-handle) + self.p = self.next_vec() + self.path.add_point( + self.p.clone(), + (pin - self.p), + NVector(0, 0) + ) + self.implicit = "S" + self.next_token() + + def _parse_s(self): + if self.la_type != 1: + self.next_token() + return + pin = self.cur_vec() + self.p + self._do_add_p() + handle = self.path.in_tangents[-1] + self.path.out_tangents[-1] = (-handle) + self.p += self.next_vec() + self.path.add_point( + self.p.clone(), + (pin - self.p), + NVector(0, 0) + ) + self.implicit = "s" + self.next_token() + + def _parse_Q(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + pin = self.cur_vec() + self.p = self.next_vec() + self.path.add_point( + self.p.clone(), + (pin - self.p), + NVector(0, 0) + ) + self.implicit = "Q" + self.next_token() + + def _parse_q(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + pin = self.p + self.cur_vec() + self.p += self.next_vec() + self.path.add_point( + self.p.clone(), + (pin - self.p), + NVector(0, 0) + ) + self.implicit = "q" + self.next_token() + + def _parse_T(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + handle = self.p - self.path.in_tangents[-1] + self.p = self.cur_vec() + self.path.add_point( + self.p.clone(), + (handle - self.p), + NVector(0, 0) + ) + self.implicit = "T" + self.next_token() + + def _parse_t(self): + if self.la_type != 1: + self.next_token() + return + self._do_add_p() + handle = -self.path.in_tangents[-1] + self.p + self.p += self.cur_vec() + self.path.add_point( + self.p.clone(), + (handle - self.p), + NVector(0, 0) + ) + self.implicit = "t" + self.next_token() + + def _parse_A(self): + if self.la_type != 1: + self.next_token() + return + r = self.cur_vec() + xrot = self.next_token() + large = self.next_token() + sweep = self.next_token() + dest = self.next_vec() + self._do_arc(r[0], r[1], xrot, large, sweep, dest) + self.implicit = "A" + self.next_token() + + def _do_arc(self, rx, ry, xrot, large, sweep, dest): + self._do_add_p() + if self.p == dest: + return + + if rx == 0 or ry == 0: + # Straight line + self.p = dest + self.path.add_point( + self.p.clone(), + NVector(0, 0), + NVector(0, 0) + ) + return + + ellipse, theta1, deltatheta = Ellipse.from_svg_arc(self.p, rx, ry, xrot, large, sweep, dest) + points = ellipse.to_bezier(theta1, deltatheta) + + self._do_add_p() + self.path.out_tangents[-1] = points[0].out_tangent + for point in points[1:-1]: + self.path.add_point( + point.vertex, + point.in_tangent, + point.out_tangent, + ) + self.path.add_point( + dest.clone(), + points[-1].in_tangent, + NVector(0, 0), + ) + self.p = dest + + def _parse_a(self): + if self.la_type != 1: + self.next_token() + return + r = self.cur_vec() + xrot = self.next_token() + large = self.next_token() + sweep = self.next_token() + dest = self.p + self.next_vec() + self._do_arc(r[0], r[1], xrot, large, sweep, dest) + self.implicit = "a" + self.next_token() + + def _parse_Z(self): + if self.path.vertices: + self.p = self.path.vertices[0].clone() + self.path.close() + self._push_path() + + def _parse_z(self): + self._parse_Z() + + +def parse_svg_etree(etree, layer_frames=0, *args, **kwargs): + parser = SvgParser() + return parser.parse_etree(etree, layer_frames, *args, **kwargs) + + +def parse_svg_file(file, layer_frames=0, *args, **kwargs): + return parse_svg_etree(ElementTree.parse(file), layer_frames, *args, **kwargs) diff --git a/lottie/parsers/svg/svgdata.py b/lottie/parsers/svg/svgdata.py new file mode 100644 index 0000000..e58d7f6 --- /dev/null +++ b/lottie/parsers/svg/svgdata.py @@ -0,0 +1,211 @@ +color_table = { + "aliceblue": [0.9411764705882353, 0.9725490196078431, 1.0, 1], + "antiquewhite": [0.9803921568627451, 0.9215686274509803, 0.8431372549019608, 1], + "aqua": [0.0, 1.0, 1.0, 1], + "aquamarine": [0.4980392156862745, 1.0, 0.8313725490196079, 1], + "azure": [0.9411764705882353, 1.0, 1.0, 1], + "beige": [0.9607843137254902, 0.9607843137254902, 0.8627450980392157, 1], + "bisque": [1.0, 0.8941176470588236, 0.7686274509803922, 1], + "black": [0.0, 0.0, 0.0, 1], + "blanchedalmond": [1.0, 0.9215686274509803, 0.803921568627451, 1], + "blue": [0.0, 0.0, 1.0, 1], + "blueviolet": [0.5411764705882353, 0.16862745098039217, 0.8862745098039215, 1], + "brown": [0.6470588235294118, 0.16470588235294117, 0.16470588235294117, 1], + "burlywood": [0.8705882352941177, 0.7215686274509804, 0.5294117647058824, 1], + "cadetblue": [0.37254901960784315, 0.6196078431372549, 0.6274509803921569, 1], + "chartreuse": [0.4980392156862745, 1.0, 0.0, 1], + "chocolate": [0.8235294117647058, 0.4117647058823529, 0.11764705882352941, 1], + "coral": [1.0, 0.4980392156862745, 0.3137254901960784, 1], + "cornflowerblue": [0.39215686274509803, 0.5843137254901961, 0.9294117647058824, 1], + "cornsilk": [1.0, 0.9725490196078431, 0.8627450980392157, 1], + "crimson": [0.8627450980392157, 0.0784313725490196, 0.23529411764705882, 1], + "cyan": [0.0, 1.0, 1.0, 1], + "darkblue": [0.0, 0.0, 0.5450980392156862, 1], + "darkcyan": [0.0, 0.5450980392156862, 0.5450980392156862, 1], + "darkgoldenrod": [0.7215686274509804, 0.5254901960784314, 0.043137254901960784, 1], + "darkgray": [0.6627450980392157, 0.6627450980392157, 0.6627450980392157, 1], + "darkgreen": [0.0, 0.39215686274509803, 0.0, 1], + "darkgrey": [0.6627450980392157, 0.6627450980392157, 0.6627450980392157, 1], + "darkkhaki": [0.7411764705882353, 0.7176470588235294, 0.4196078431372549, 1], + "darkmagenta": [0.5450980392156862, 0.0, 0.5450980392156862, 1], + "darkolivegreen": [0.3333333333333333, 0.4196078431372549, 0.1843137254901961, 1], + "darkorange": [1.0, 0.5490196078431373, 0.0, 1], + "darkorchid": [0.6, 0.19607843137254902, 0.8, 1], + "darkred": [0.5450980392156862, 0.0, 0.0, 1], + "darksalmon": [0.9137254901960784, 0.5882352941176471, 0.47843137254901963, 1], + "darkseagreen": [0.5607843137254902, 0.7372549019607844, 0.5607843137254902, 1], + "darkslateblue": [0.2823529411764706, 0.23921568627450981, 0.5450980392156862, 1], + "darkslategray": [0.1843137254901961, 0.30980392156862746, 0.30980392156862746, 1], + "darkslategrey": [0.1843137254901961, 0.30980392156862746, 0.30980392156862746, 1], + "darkturquoise": [0.0, 0.807843137254902, 0.8196078431372549, 1], + "darkviolet": [0.5803921568627451, 0.0, 0.8274509803921568, 1], + "deeppink": [1.0, 0.0784313725490196, 0.5764705882352941, 1], + "deepskyblue": [0.0, 0.7490196078431373, 1.0, 1], + "dimgray": [0.4117647058823529, 0.4117647058823529, 0.4117647058823529, 1], + "dimgrey": [0.4117647058823529, 0.4117647058823529, 0.4117647058823529, 1], + "dodgerblue": [0.11764705882352941, 0.5647058823529412, 1.0, 1], + "firebrick": [0.6980392156862745, 0.13333333333333333, 0.13333333333333333, 1], + "floralwhite": [1.0, 0.9803921568627451, 0.9411764705882353, 1], + "forestgreen": [0.13333333333333333, 0.5450980392156862, 0.13333333333333333, 1], + "fuchsia": [1.0, 0.0, 1.0, 1], + "gainsboro": [0.8627450980392157, 0.8627450980392157, 0.8627450980392157, 1], + "ghostwhite": [0.9725490196078431, 0.9725490196078431, 1.0, 1], + "gold": [1.0, 0.8431372549019608, 0.0, 1], + "goldenrod": [0.8549019607843137, 0.6470588235294118, 0.12549019607843137, 1], + "gray": [0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1], + "green": [0.0, 0.5019607843137255, 0.0, 1], + "greenyellow": [0.6784313725490196, 1.0, 0.1843137254901961, 1], + "grey": [0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1], + "honeydew": [0.9411764705882353, 1.0, 0.9411764705882353, 1], + "hotpink": [1.0, 0.4117647058823529, 0.7058823529411765, 1], + "indianred": [0.803921568627451, 0.3607843137254902, 0.3607843137254902, 1], + "indigo": [0.29411764705882354, 0.0, 0.5098039215686274, 1], + "ivory": [1.0, 1.0, 0.9411764705882353, 1], + "khaki": [0.9411764705882353, 0.9019607843137255, 0.5490196078431373, 1], + "lavender": [0.9019607843137255, 0.9019607843137255, 0.9803921568627451, 1], + "lavenderblush": [1.0, 0.9411764705882353, 0.9607843137254902, 1], + "lawngreen": [0.48627450980392156, 0.9882352941176471, 0.0, 1], + "lemonchiffon": [1.0, 0.9803921568627451, 0.803921568627451, 1], + "lightblue": [0.6784313725490196, 0.8470588235294118, 0.9019607843137255, 1], + "lightcoral": [0.9411764705882353, 0.5019607843137255, 0.5019607843137255, 1], + "lightcyan": [0.8784313725490196, 1.0, 1.0, 1], + "lightgoldenrodyellow": [0.9803921568627451, 0.9803921568627451, 0.8235294117647058, 1], + "lightgray": [0.8274509803921568, 0.8274509803921568, 0.8274509803921568, 1], + "lightgreen": [0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1], + "lightgrey": [0.8274509803921568, 0.8274509803921568, 0.8274509803921568, 1], + "lightpink": [1.0, 0.7137254901960784, 0.7568627450980392, 1], + "lightsalmon": [1.0, 0.6274509803921569, 0.47843137254901963, 1], + "lightseagreen": [0.12549019607843137, 0.6980392156862745, 0.6666666666666666, 1], + "lightskyblue": [0.5294117647058824, 0.807843137254902, 0.9803921568627451, 1], + "lightslategray": [0.4666666666666667, 0.5333333333333333, 0.6, 1], + "lightslategrey": [0.4666666666666667, 0.5333333333333333, 0.6, 1], + "lightsteelblue": [0.6901960784313725, 0.7686274509803922, 0.8705882352941177, 1], + "lightyellow": [1.0, 1.0, 0.8784313725490196, 1], + "lime": [0.0, 1.0, 0.0, 1], + "limegreen": [0.19607843137254902, 0.803921568627451, 0.19607843137254902, 1], + "linen": [0.9803921568627451, 0.9411764705882353, 0.9019607843137255, 1], + "magenta": [1.0, 0.0, 1.0, 1], + "maroon": [0.5019607843137255, 0.0, 0.0, 1], + "mediumaquamarine": [0.4, 0.803921568627451, 0.6666666666666666, 1], + "mediumblue": [0.0, 0.0, 0.803921568627451, 1], + "mediumorchid": [0.7294117647058823, 0.3333333333333333, 0.8274509803921568, 1], + "mediumpurple": [0.5764705882352941, 0.4392156862745098, 0.8588235294117647, 1], + "mediumseagreen": [0.23529411764705882, 0.7019607843137254, 0.44313725490196076, 1], + "mediumslateblue": [0.4823529411764706, 0.40784313725490196, 0.9333333333333333, 1], + "mediumspringgreen": [0.0, 0.9803921568627451, 0.6039215686274509, 1], + "mediumturquoise": [0.2823529411764706, 0.8196078431372549, 0.8, 1], + "mediumvioletred": [0.7803921568627451, 0.08235294117647059, 0.5215686274509804, 1], + "midnightblue": [0.09803921568627451, 0.09803921568627451, 0.4392156862745098, 1], + "mintcream": [0.9607843137254902, 1.0, 0.9803921568627451, 1], + "mistyrose": [1.0, 0.8941176470588236, 0.8823529411764706, 1], + "moccasin": [1.0, 0.8941176470588236, 0.7098039215686275, 1], + "navajowhite": [1.0, 0.8705882352941177, 0.6784313725490196, 1], + "navy": [0.0, 0.0, 0.5019607843137255, 1], + "oldlace": [0.9921568627450981, 0.9607843137254902, 0.9019607843137255, 1], + "olive": [0.5019607843137255, 0.5019607843137255, 0.0, 1], + "olivedrab": [0.4196078431372549, 0.5568627450980392, 0.13725490196078433, 1], + "orange": [1.0, 0.6470588235294118, 0.0, 1], + "orangered": [1.0, 0.27058823529411763, 0.0, 1], + "orchid": [0.8549019607843137, 0.4392156862745098, 0.8392156862745098, 1], + "palegoldenrod": [0.9333333333333333, 0.9098039215686274, 0.6666666666666666, 1], + "palegreen": [0.596078431372549, 0.984313725490196, 0.596078431372549, 1], + "paleturquoise": [0.6862745098039216, 0.9333333333333333, 0.9333333333333333, 1], + "palevioletred": [0.8588235294117647, 0.4392156862745098, 0.5764705882352941, 1], + "papayawhip": [1.0, 0.9372549019607843, 0.8352941176470589, 1], + "peachpuff": [1.0, 0.8549019607843137, 0.7254901960784313, 1], + "peru": [0.803921568627451, 0.5215686274509804, 0.24705882352941178, 1], + "pink": [1.0, 0.7529411764705882, 0.796078431372549, 1], + "plum": [0.8666666666666667, 0.6274509803921569, 0.8666666666666667, 1], + "powderblue": [0.6901960784313725, 0.8784313725490196, 0.9019607843137255, 1], + "purple": [0.5019607843137255, 0.0, 0.5019607843137255, 1], + "red": [1.0, 0.0, 0.0, 1], + "rosybrown": [0.7372549019607844, 0.5607843137254902, 0.5607843137254902, 1], + "royalblue": [0.2549019607843137, 0.4117647058823529, 0.8823529411764706, 1], + "saddlebrown": [0.5450980392156862, 0.27058823529411763, 0.07450980392156863, 1], + "salmon": [0.9803921568627451, 0.5019607843137255, 0.4470588235294118, 1], + "sandybrown": [0.9568627450980393, 0.6431372549019608, 0.3764705882352941, 1], + "seagreen": [0.1803921568627451, 0.5450980392156862, 0.3411764705882353, 1], + "seashell": [1.0, 0.9607843137254902, 0.9333333333333333, 1], + "sienna": [0.6274509803921569, 0.3215686274509804, 0.17647058823529413, 1], + "silver": [0.7529411764705882, 0.7529411764705882, 0.7529411764705882, 1], + "skyblue": [0.5294117647058824, 0.807843137254902, 0.9215686274509803, 1], + "slateblue": [0.41568627450980394, 0.35294117647058826, 0.803921568627451, 1], + "slategray": [0.4392156862745098, 0.5019607843137255, 0.5647058823529412, 1], + "slategrey": [0.4392156862745098, 0.5019607843137255, 0.5647058823529412, 1], + "snow": [1.0, 0.9803921568627451, 0.9803921568627451, 1], + "springgreen": [0.0, 1.0, 0.4980392156862745, 1], + "steelblue": [0.27450980392156865, 0.5098039215686274, 0.7058823529411765, 1], + "tan": [0.8235294117647058, 0.7058823529411765, 0.5490196078431373, 1], + "teal": [0.0, 0.5019607843137255, 0.5019607843137255, 1], + "thistle": [0.8470588235294118, 0.7490196078431373, 0.8470588235294118, 1], + "tomato": [1.0, 0.38823529411764707, 0.2784313725490196, 1], + "turquoise": [0.25098039215686274, 0.8784313725490196, 0.8156862745098039, 1], + "violet": [0.9333333333333333, 0.5098039215686274, 0.9333333333333333, 1], + "wheat": [0.9607843137254902, 0.8705882352941177, 0.7019607843137254, 1], + "white": [1.0, 1.0, 1.0, 1], + "whitesmoke": [0.9607843137254902, 0.9607843137254902, 0.9607843137254902, 1], + "yellow": [1.0, 1.0, 0.0, 1], + "yellowgreen": [0.6039215686274509, 0.803921568627451, 0.19607843137254902, 1], +} + +css_atrrs = { + "fill", + "alignment-baseline", + "baseline-shift", + "clip-path", + "clip-rule", + "color", + "color-interpolation", + "color-interpolation-filters", + "color-rendering", + "cursor", + "direction", + "display", + "dominant-baseline", + "fill-opacity", + "fill-rule", + "filter", + "flood-color", + "flood-opacity", + "font-family", + "font-size", + "font-size-adjust", + "font-stretch", + "font-style", + "font-variant", + "font-weight", + "glyph-orientation-horizontal", + "glyph-orientation-vertical", + "image-rendering", + "letter-spacing", + "lighting-color", + "marker-end", + "marker-mid", + "marker-start", + "mask", + "opacity", + "overflow", + "paint-order", + "pointer-events", + "shape-rendering", + "stop-color", + "stop-opacity", + "stroke", + "stroke-dasharray", + "stroke-dashoffset", + "stroke-linecap", + "stroke-linejoin", + "stroke-miterlimit", + "stroke-opacity", + "stroke-width", + "text-anchor", + "text-decoration", + "text-overflow", + "text-rendering", + "unicode-bidi", + "vector-effect", + "visibility", + "white-space", + "word-spacing", + "writing-mode" +} diff --git a/lottie/parsers/tgs.py b/lottie/parsers/tgs.py new file mode 100644 index 0000000..86de27d --- /dev/null +++ b/lottie/parsers/tgs.py @@ -0,0 +1,41 @@ +import io +import json +import gzip +from ..objects import Animation + + +def parse_tgs_json(file): + """! + Reads both tgs and lottie files, returns the json structure + """ + return open_maybe_gzipped(file, json.load) + + +def open_maybe_gzipped(file, on_open): + if isinstance(file, str): + with open(file, "r") as fileobj: + return open_maybe_gzipped(fileobj, on_open) + + if isinstance(file, io.TextIOBase): + binfile = file.buffer + else: + binfile = file + + mn = binfile.read(2) + binfile.seek(0) + if mn == b'\x1f\x8b': # gzip magic number + final_file = gzip.open(binfile, "rb") + elif isinstance(file, io.TextIOBase): + final_file = file + else: + final_file = io.TextIOWrapper(file) + + return on_open(final_file) + + +def parse_tgs(filename): + """! + Reads both tgs and lottie files + """ + lottie = parse_tgs_json(filename) + return Animation.load(lottie) diff --git a/lottie/utils/__init__.py b/lottie/utils/__init__.py new file mode 100644 index 0000000..5db2491 --- /dev/null +++ b/lottie/utils/__init__.py @@ -0,0 +1,7 @@ +__all__ = ["animation", "ellipse", "ik", "linediff", "restructure", "script", "stripper"] + +try: + from . import font + __all__ += ["font"] +except ImportError: + pass diff --git a/lottie/utils/animation.py b/lottie/utils/animation.py new file mode 100644 index 0000000..31061a4 --- /dev/null +++ b/lottie/utils/animation.py @@ -0,0 +1,544 @@ +import random +import math +from ..nvector import NVector +from ..objects.shapes import Path +from .. import objects +from ..objects import easing +from ..objects import properties + + +def shake(position_prop, x_radius, y_radius, start_time, end_time, n_frames, interp=easing.Linear()): + if not isinstance(position_prop, list): + position_prop = [position_prop] + + n_frames = int(round(n_frames)) + frame_time = (end_time - start_time) / n_frames + startpoints = list(map( + lambda pp: pp.get_value(start_time), + position_prop + )) + + for i in range(n_frames): + x = (random.random() * 2 - 1) * x_radius + y = (random.random() * 2 - 1) * y_radius + for pp, start in zip(position_prop, startpoints): + px = start[0] + x + py = start[1] + y + pp.add_keyframe(start_time + i * frame_time, NVector(px, py), interp) + + for pp, start in zip(position_prop, startpoints): + pp.add_keyframe(end_time, start, interp) + + +def rot_shake(rotation_prop, angles, start_time, end_time, n_frames): + frame_time = (end_time - start_time) / n_frames + start = rotation_prop.get_value(start_time) + + for i in range(0, n_frames): + a = angles[i % len(angles)] * math.sin(i/n_frames * math.pi) + rotation_prop.add_keyframe(start_time + i * frame_time, start + a) + rotation_prop.add_keyframe(end_time, start) + + +def spring_pull(position_prop, point, start_time, end_time, falloff=15, oscillations=7): + start = position_prop.get_value(start_time) + d = start-point + + delta = (end_time - start_time) / oscillations + + for i in range(oscillations): + time_x = i / oscillations + factor = math.cos(time_x * math.pi * oscillations) * (1-time_x**(1/falloff)) + p = point + d * factor + position_prop.add_keyframe(start_time + delta * i, p) + + position_prop.add_keyframe(end_time, point) + + +def follow_path(position_prop, bezier, start_time, end_time, n_keyframes, + reverse=False, offset=NVector(0, 0), start_t=0, rotation_prop=None, rotation_offset=0): + delta = (end_time - start_time) / (n_keyframes-1) + fact = start_t + factd = 1 / (n_keyframes-1) + + if rotation_prop: + start_rot = rotation_prop.get_value(start_time) if rotation_offset is None else rotation_offset + + for i in range(n_keyframes): + time = start_time + i * delta + + if fact > 1 + factd/2: + fact -= 1 + if time != start_time: + easing.Jump()(position_prop.keyframes[-1]) + if rotation_prop: + easing.Jump()(rotation_prop.keyframes[-1]) + + f = 1 - fact if reverse else fact + position_prop.add_keyframe(time, bezier.point_at(f)+offset) + + if rotation_prop: + rotation_prop.add_keyframe(time, bezier.tangent_angle_at(f) / math.pi * 180 + start_rot) + + fact += factd + + +def generate_path_appear(bezier, appear_start, appear_end, n_keyframes, reverse=False): + obj = Path() + beziers = [] + maxp = 0 + + time_delta = (appear_end - appear_start) / n_keyframes + for i in range(n_keyframes+1): + time = appear_start + i * time_delta + t2 = (time - appear_start) / (appear_end - appear_start) + + if reverse: + t2 = 1 - t2 + segment = bezier.segment(t2, 1) + segment.reverse() + else: + segment = bezier.segment(0, t2) + + beziers.append(segment) + if len(segment.vertices) > maxp: + maxp = len(segment.vertices) + + obj.shape.add_keyframe(time, segment) + + for segment in beziers: + deltap = maxp - len(segment.vertices) + if deltap > 0: + segment.vertices += [segment.vertices[-1]] * deltap + segment.in_tangents += [NVector(0, 0)] * deltap + segment.out_tangents += [NVector(0, 0)] * deltap + + return obj + + +def generate_path_disappear(bezier, disappear_start, disappear_end, n_keyframes, reverse=False): + obj = Path() + beziers = [] + maxp = 0 + + time_delta = (disappear_end - disappear_start) / n_keyframes + for i in range(n_keyframes+1): + time = disappear_start + i * time_delta + t1 = (time - disappear_start) / (disappear_end - disappear_start) + if reverse: + t1 = 1 - t1 + segment = bezier.segment(0, t1) + else: + segment = bezier.segment(1, t1) + segment.reverse() + + beziers.append(segment) + if len(segment.vertices) > maxp: + maxp = len(segment.vertices) + + obj.shape.add_keyframe(time, segment) + + for segment in beziers: + deltap = maxp - len(segment.vertices) + if deltap > 0: + segment.vertices += [segment.vertices[-1]] * deltap + segment.in_tangents += [NVector(0, 0)] * deltap + segment.out_tangents += [NVector(0, 0)] * deltap + + return obj + + +def generate_path_segment(bezier, appear_start, appear_end, disappear_start, disappear_end, n_keyframes, reverse=False): + obj = Path() + beziers = [] + maxp = 0 + + # HACK: For some reson reversed works better + if not reverse: + bezier.reverse() + + time_delta = (appear_end - appear_start) / n_keyframes + for i in range(n_keyframes+1): + time = appear_start + i * time_delta + t1 = (time - disappear_start) / (disappear_end - disappear_start) + t2 = (time - appear_start) / (appear_end - appear_start) + + t1 = max(0, min(1, t1)) + t2 = max(0, min(1, t2)) + + #if reverse: + if True: + t1 = 1 - t1 + t2 = 1 - t2 + segment = bezier.segment(t2, t1) + segment.reverse() + #else: + #segment = bezier.segment(t1, t2) + #segment.reverse() + + beziers.append(segment) + if len(segment.vertices) > maxp: + maxp = len(segment.vertices) + + obj.shape.add_keyframe(time, segment) + + for segment in beziers: + deltap = maxp - len(segment.vertices) + if deltap > 0: + segment.split_self_chunks(deltap+1) + + # HACK: Restore + if not reverse: + bezier.reverse() + return obj + + +class PointDisplacer: + def __init__(self, time_start, time_end, n_frames): + """! + @param time_start When the animation shall start + @param time_end When the animation shall end + @param n_frames Number of frames in the animation + """ + ## When the animation shall start + self.time_start = time_start + ## When the animation shall end + self.time_end = time_end + ## Number of frames in the animation + self.n_frames = n_frames + ## Length of a frame + self.time_delta = (time_end - time_start) / n_frames + + def animate_point(self, prop): + startpos = prop.get_value(self.time_start) + for f in range(self.n_frames+1): + p = self._on_displace(startpos, f) + prop.add_keyframe(self.frame_time(f), startpos+p) + + def _on_displace(self, startpos, f): + raise NotImplementedError() + + def animate_bezier(self, prop): + initial = prop.get_value(self.time_start) + + for f in range(self.n_frames+1): + bezier = objects.Bezier() + bezier.closed = initial.closed + + for pi in range(len(initial.vertices)): + startpos = initial.vertices[pi] + dp = self._on_displace(startpos, f) + t1sp = initial.in_tangents[pi] + startpos + t1fin = initial.in_tangents[pi] + self._on_displace(t1sp, f) - dp + t2sp = initial.out_tangents[pi] + startpos + t2fin = initial.out_tangents[pi] + self._on_displace(t2sp, f) - dp + + bezier.add_point(dp + startpos, t1fin, t2fin) + + prop.add_keyframe(self.frame_time(f), bezier) + + def frame_time(self, f): + return f * self.time_delta + self.time_start + + def _init_lerp(self, val_from, val_to, easing): + self._kf = properties.OffsetKeyframe(0, NVector(val_from), NVector(val_to), easing) + + def _lerp_get(self, offset): + return self._kf.interpolated_value(offset / self.n_frames)[0] + + +class SineDisplacer(PointDisplacer): + def __init__( + self, + wavelength, + amplitude, + time_start, + time_end, + n_frames, + speed=1, + axis=90, + ): + """! + Displaces points as if they were following a sine wave + + @param wavelength Distance between consecutive peaks + @param amplitude Distance from a peak to the original position + @param time_start When the animation shall start + @param time_end When the animation shall end + @param n_frames Number of keyframes to add + @param speed Number of peaks a point will go through in the given time + If negative, it will go the other way + @param axis Wave peak direction + """ + super().__init__(time_start, time_end, n_frames) + + self.wavelength = wavelength + self.amplitude = amplitude + self.speed_f = math.pi * 2 * speed + self.axis = axis / 180 * math.pi + + def _on_displace(self, startpos, f): + off = -math.sin(startpos[0]/self.wavelength*math.pi*2-f*self.speed_f/self.n_frames) * self.amplitude + return NVector(off * math.cos(self.axis), off * math.sin(self.axis)) + + +class MultiSineDisplacer(PointDisplacer): + def __init__( + self, + waves, + time_start, + time_end, + n_frames, + speed=1, + axis=90, + amplitude_scale=1, + ): + """! + Displaces points as if they were following a sine wave + + @param waves List of tuples (wavelength, amplitude) + @param time_start When the animation shall start + @param time_end When the animation shall end + @param n_frames Number of keyframes to add + @param speed Number of peaks a point will go through in the given time + If negative, it will go the other way + @param axis Wave peak direction + @param amplitude_scale Multiplies the resulting amplitude by this factor + """ + super().__init__(time_start, time_end, n_frames) + + self.waves = waves + self.speed_f = math.pi * 2 * speed + self.axis = axis / 180 * math.pi + self.amplitude_scale = amplitude_scale + + def _on_displace(self, startpos, f): + off = 0 + for wavelength, amplitude in self.waves: + off -= math.sin(startpos[0]/wavelength*math.pi*2-f*self.speed_f/self.n_frames) * amplitude + + off *= self.amplitude_scale + return NVector(off * math.cos(self.axis), off * math.sin(self.axis)) + + +class DepthRotationAxis: + def __init__(self, x, y, keep): + self.x = x / x.length + self.y = y / y.length + self.keep = keep / keep.length # should be the cross product + + def rot_center(self, center, point): + return ( + self.x * self.x.dot(center) + + self.y * self.y.dot(center) + + self.keep * self.keep.dot(point) + ) + + def extract_component(self, vector, axis): + return sum(vector.element_scaled(axis).components) + + @classmethod + def from_points(cls, keep_point, center=NVector(0, 0, 0)): + keep = keep_point - center + keep /= keep.length + # Hughes-Moller to find x and y + if abs(keep.x) > abs(keep.z): + y = NVector(-keep.y, keep.x, 0) + else: + y = NVector(0, -keep.z, keep.y) + y /= y.length + x = y.cross(keep) + return cls(x, y, keep) + + +class DepthRotation: + axis_x = DepthRotationAxis(NVector(0, 0, 1), NVector(0, 1, 0), NVector(1, 0, 0)) + axis_y = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 0, 1), NVector(0, 1, 0)) + axis_z = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 1, 0), NVector(0, 0, 1)) + + def __init__(self, center): + self.center = center + + def rotate3d_y(self, point, angle): + return self.rotate3d(point, angle, self.axis_y) + # Hard-coded version: + #c = NVector(self.center.x, point.y, self.center.z) + #rad = angle * math.pi / 180 + #delta = point - c + #pol_l = delta.length + #pol_a = math.atan2(delta.z, delta.x) + #dest_a = pol_a + rad + #return NVector( + # c.x + pol_l * math.cos(dest_a), + # point.y, + # c.z + pol_l * math.sin(dest_a) + #) + + def rotate3d_x(self, point, angle): + return self.rotate3d(point, angle, self.axis_x) + # Hard-coded version: + #c = NVector(point.x, self.center.y, self.center.z) + #rad = angle * math.pi / 180 + #delta = point - c + #pol_l = delta.length + #pol_a = math.atan2(delta.y, delta.z) + #dest_a = pol_a + rad + #return NVector( + # point.x, + # c.y + pol_l * math.sin(dest_a), + # c.z + pol_l * math.cos(dest_a), + #) + + def rotate3d_z(self, point, angle): + return self.rotate3d(point, angle, self.axis_z) + + def rotate3d(self, point, angle, axis): + c = axis.rot_center(self.center, point) + rad = angle * math.pi / 180 + delta = point - c + pol_l = delta.length + pol_a = math.atan2( + axis.extract_component(delta, axis.y), + axis.extract_component(delta, axis.x) + ) + dest_a = pol_a + rad + return c + axis.x * pol_l * math.cos(dest_a) + axis.y * pol_l * math.sin(dest_a) + + +class DepthRotationDisplacer(PointDisplacer): + axis_x = DepthRotation.axis_x + axis_y = DepthRotation.axis_y + axis_z = DepthRotation.axis_z + + def __init__(self, center, time_start, time_end, n_frames, axis, + depth=0, angle=360, anglestart=0, ease=easing.Linear()): + super().__init__(time_start, time_end, n_frames) + self.rotation = DepthRotation(center) + if isinstance(axis, NVector): + axis = DepthRotationAxis.from_points(axis) + self.axis = axis + self.depth = depth + self._angle = angle + self.anglestart = anglestart + self.ease = ease + self._init_lerp(0, angle, ease) + + @property + def angle(self): + return self._angle + + @angle.setter + def angle(self, value): + self._angle = value + self._init_lerp(0, value, self.ease) + + def _on_displace(self, startpos, f): + angle = self.anglestart + self._lerp_get(f) + if len(startpos) < 3: + startpos = NVector(*(startpos.components + [self.depth])) + return self.rotation.rotate3d(startpos, angle, self.axis) - startpos + + +class EnvelopeDeformation(PointDisplacer): + def __init__(self, topleft, bottomright): + self.topleft = topleft + self.size = bottomright - topleft + self.keyframes = [] + + @property + def time_start(self): + return self.keyframes[0][0] + + def add_reset_keyframe(self, time): + self.add_keyframe( + time, + self.topleft.clone(), + NVector(self.topleft.x + self.size.x, self.topleft.y), + NVector(self.topleft.x + self.size.x, self.topleft.y + self.size.y), + NVector(self.topleft.x, self.topleft.y + self.size.y), + ) + + def add_keyframe(self, time, tl, tr, br, bl): + self.keyframes.append([ + time, + tl.clone(), + tr.clone(), + br.clone(), + bl.clone() + ]) + + def _on_displace(self, startpos, f): + _, tl, tr, br, bl = self.keyframes[f] + relp = startpos - self.topleft + relp.x /= self.size.x + relp.y /= self.size.y + + x1 = tl.lerp(tr, relp.x) + x2 = bl.lerp(br, relp.x) + + #return x1.lerp(x2, relp.y) + return x1.lerp(x2, relp.y) - startpos + + @property + def n_frames(self): + return len(self.keyframes)-1 + + def frame_time(self, f): + return self.keyframes[f][0] + + +class DisplacerDampener(PointDisplacer): + """! + Given a displacer and a function that returns a factor for a point, + multiplies the effect of the displacer by the factor + """ + def __init__(self, displacer, dampener): + self.displacer = displacer + self.dampener = dampener + + @property + def time_start(self): + return self.displacer.time_start + + def _on_displace(self, startpos, f): + disp = self.displacer._on_displace(startpos, f) + damp = self.dampener(startpos) + return disp * damp + + @property + def n_frames(self): + return self.displacer.n_frames + + def frame_time(self, f): + return self.displacer.frame_time(f) + + +class FollowDisplacer(PointDisplacer): + def __init__( + self, + origin, + range, + offset_func, + time_start, time_end, n_frames, + falloff_exp=1, + ): + """! + @brief Uses a custom offset function, and applies a falloff to the displacement + + @param origin Origin point for the falloff + @param range Radius after which the points will not move + @param offset_func Function returning an offset given a ratio of the time + @param time_start When the animation shall start + @param time_end When the animation shall end + @param n_frames Number of frames in the animation + @param falloff_exp Exponent for the falloff + """ + super().__init__(time_start, time_end, n_frames) + self.origin = origin + self.range = range + self.offset_func = offset_func + self.falloff_exp = falloff_exp + + def _on_displace(self, startpos, f): + influence = 1 - min(1, (startpos - self.origin).length / self.range) ** self.falloff_exp + return self.offset_func(f / self.n_frames) * influence diff --git a/lottie/utils/color.py b/lottie/utils/color.py new file mode 100644 index 0000000..6dcfbe1 --- /dev/null +++ b/lottie/utils/color.py @@ -0,0 +1,447 @@ +import enum +import math +import colorsys +from ..nvector import NVector + + +def from_uint8(r, g, b, a=255): + return Color(r, g, b, a) / 255 + + +class ColorMode(enum.Enum): + ## sRGB, Components in [0, 1] + RGB = enum.auto() + ## HSV, components in [0, 1] + HSV = enum.auto() + ## HSL, components in [0, 1] + HSL = enum.auto() + ## CIE XYZ with Illuminant D65. Components in [0, 1] + XYZ = enum.auto() + ## CIE L*u*v* + LUV = enum.auto() + ## CIE Lch(uv), polar version of LUV where C is the radius and H an angle in radians + LCH_uv = enum.auto() + ## CIE L*a*b* + LAB = enum.auto() + ## CIE LCh(ab), polar version of LAB where C is the radius and H an angle in radians + #LCH_ab = enum.auto() + + +def _clamp(x): + return max(0, min(1, x)) + + +class Conversion: + _conv_paths = { + (ColorMode.RGB, ColorMode.RGB): [], + (ColorMode.RGB, ColorMode.HSV): [], + (ColorMode.RGB, ColorMode.HSL): [], + (ColorMode.RGB, ColorMode.XYZ): [], + (ColorMode.RGB, ColorMode.LUV): [ColorMode.XYZ], + (ColorMode.RGB, ColorMode.LAB): [ColorMode.XYZ], + (ColorMode.RGB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.RGB, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.HSV, ColorMode.RGB): [], + (ColorMode.HSV, ColorMode.HSV): [], + (ColorMode.HSV, ColorMode.HSL): [], + (ColorMode.HSV, ColorMode.XYZ): [ColorMode.RGB], + (ColorMode.HSV, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSV, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSV, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.HSV, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.HSL, ColorMode.RGB): [], + (ColorMode.HSL, ColorMode.HSV): [], + (ColorMode.HSL, ColorMode.HSL): [], + (ColorMode.HSL, ColorMode.XYZ): [ColorMode.RGB], + (ColorMode.HSL, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSL, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ], + (ColorMode.HSL, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.HSL, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.XYZ, ColorMode.RGB): [], + (ColorMode.XYZ, ColorMode.HSV): [ColorMode.RGB], + (ColorMode.XYZ, ColorMode.HSL): [ColorMode.RGB], + (ColorMode.XYZ, ColorMode.XYZ): [], + (ColorMode.XYZ, ColorMode.LUV): [], + (ColorMode.XYZ, ColorMode.LAB): [], + (ColorMode.XYZ, ColorMode.LCH_uv): [ColorMode.LUV], + #(ColorMode.XYZ, ColorMode.LCH_ab): [ColorMode.LAB], + + (ColorMode.LCH_uv, ColorMode.RGB): [ColorMode.LUV, ColorMode.XYZ], + (ColorMode.LCH_uv, ColorMode.HSV): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LCH_uv, ColorMode.HSL): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LCH_uv, ColorMode.XYZ): [ColorMode.LUV], + (ColorMode.LCH_uv, ColorMode.LUV): [], + (ColorMode.LCH_uv, ColorMode.LAB): [ColorMode.LUV, ColorMode.XYZ], + (ColorMode.LCH_uv, ColorMode.LCH_uv): [], + #(ColorMode.LCH_uv, ColorMode.LCH_ab): [ColorMode.LUV, ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.LUV, ColorMode.RGB): [ColorMode.XYZ], + (ColorMode.LUV, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LUV, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LUV, ColorMode.XYZ): [], + (ColorMode.LUV, ColorMode.LUV): [], + (ColorMode.LUV, ColorMode.LAB): [ColorMode.XYZ], + (ColorMode.LUV, ColorMode.LCH_uv): [], + #(ColorMode.LUV, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB], + + (ColorMode.LAB, ColorMode.RGB): [ColorMode.XYZ], + (ColorMode.LAB, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LAB, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB], + (ColorMode.LAB, ColorMode.XYZ): [], + (ColorMode.LAB, ColorMode.LUV): [ColorMode.XYZ], + (ColorMode.LAB, ColorMode.LAB): [], + (ColorMode.LAB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.LAB, ColorMode.LCH_ab): [], + + #(ColorMode.LCH_ab, ColorMode.RGB): [ColorMode.LAB, ColorMode.XYZ], + #(ColorMode.LCH_ab, ColorMode.HSV): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB], + #(ColorMode.LCH_ab, ColorMode.HSL): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB], + #(ColorMode.LCH_ab, ColorMode.XYZ): [ColorMode.LAB], + #(ColorMode.LCH_ab, ColorMode.LUV): [ColorMode.LAB, ColorMode.XYZ], + #(ColorMode.LCH_ab, ColorMode.LAB): [], + #(ColorMode.LCH_ab, ColorMode.LCH_uv): [ColorMode.LAB, ColorMode.XYZ, ColorMode.LUV], + #(ColorMode.LCH_ab, ColorMode.LCH_ab): [], + } + + @staticmethod + def rgb_to_hsv(r, g, b): + return colorsys.rgb_to_hsv(r, g, b) + + @staticmethod + def hsv_to_rgb(r, g, b): + return colorsys.hsv_to_rgb(r, g, b) + + @staticmethod + def hsl_to_hsv(h, s_hsl, l): + v = l + s_hsl * min(l, 1 - l) + s_hsv = 0 if v == 0 else 2 - 2 * l / v + return (h, s_hsv, v) + + @staticmethod + def hsv_to_hsl(h, s_hsv, v): + l = v - v * s_hsv / 2 + s_hsl = 0 if l in (0, 1) else (v - l) / min(l, 1 - l) + return (h, s_hsl, l) + + @staticmethod + def rgb_to_hsl(r, g, b): + h, l, s = colorsys.rgb_to_hls(r, g, b) + return (h, s, l) + + @staticmethod + def hsl_to_rgb(h, s, l): + return colorsys.hls_to_rgb(h, l, s) + + # http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip + #@staticmethod + #def rgb_to_hcl(r, g, b, gamma=3, y0=100): + #maxc = max(r, g, b) + #minc = min(r, g, b) + #if maxc > 0: + #alpha = 1/y0 * minc / maxc + #else: + #alpha = 0 + #q = math.e ** (alpha * gamma) + #h = math.atan2(g - b, r - g) + #if h < 0: + #h += 2*math.pi + #h /= 2*math.pi + #c = q / 3 * (abs(r-g) + abs(g-b) + abs(b-r)) + #l = (q * maxc + (q-1) * minc) / 2 + #return (h, c, l) + + #@staticmethod + #def hcl_to_rgb(h, c, l, gamma=3, y0=100): + #h *= 2*math.pi + + #q = math.e ** ((1 - 2*c / 4*l) * gamma / y0) + #minc = (4*l - 3*c) / (4*q - 2) + #maxc = minc + 3*c / 2*q + + #if h <= math.pi * 1 / 3: + #tan = math.tan(3/2*h) + #r = maxc + #b = minc + #g = (r * tan + b) / (1 + tan) + #elif h <= math.pi * 2 / 3: + #tan = math.tan(3/4*(h-math.pi)) + #g = maxc + #b = minc + #r = (g * (1+tan) - b) / tan + #elif h <= math.pi * 3 / 3: + #tan = math.tan(3/4*(h-math.pi)) + #g = maxc + #r = minc + #b = g * (1+tan) - r * tan + #elif h <= math.pi * 4 / 3: + #tan = math.tan(3/2*(h+math.pi)) + #b = maxc + #r = minc + #g = (r * tan + b) / (1 + tan) + #elif h <= math.pi * 5 / 3: + #tan = math.tan(3/4*h) + #b = maxc + #g = minc + #r = (g * (1+tan) - b) / tan + #else: + #tan = math.tan(3/4*h) + #r = maxc + #g = minc + #b = g * (1+tan) - r * tan + + #return _clamp(r), _clamp(g), _clamp(b) + + @staticmethod + def rgb_to_xyz(r, g, b): + def _gamma(v): + return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 + rgb = (_gamma(r), _gamma(g), _gamma(b)) + matrix = [ + [0.4124564, 0.3575761, 0.1804375], + [0.2126729, 0.7151522, 0.0721750], + [0.0193339, 0.1191920, 0.9503041], + ] + return tuple( + sum(rgb[i] * c for i, c in enumerate(row)) + for row in matrix + ) + + @staticmethod + def xyz_to_rgb(x, y, z): + def _gamma1(v): + return _clamp(v * 12.92 if v <= 0.0031308 else v ** (1/2.4) * 1.055 - 0.055) + matrix = [ + [+3.2404542, -1.5371385, -0.4985314], + [-0.9692660, +1.8760108, +0.0415560], + [+0.0556434, -0.2040259, +1.0572252], + ] + xyz = (x, y, z) + return tuple(map(_gamma1, ( + sum(xyz[i] * c for i, c in enumerate(row)) + for row in matrix + ))) + + @staticmethod + def xyz_to_luv(x, y, z): + u1r = 0.2009 + v1r = 0.4610 + yr = 100 + + kap = (29/3)**3 + eps = (6/29)**3 + + try: + u1 = 4*x / (x + 15*y + 3*z) + v1 = 9*y / (x + 15*y + 3*z) + except ZeroDivisionError: + return 0, 0, 0 + + y_r = y/yr + l = 166 * y_r ** (1/3) - 16 if y_r > eps else kap * y_r + u = 13 * l * (u1 - u1r) + v = 13 * l * (v1 - v1r) + return l, u, v + + @staticmethod + def luv_to_xyz(l, u, v): + u1r = 0.2009 + v1r = 0.4610 + yr = 100 + + kap = (29/3)**3 + + if l == 0: + u1 = u1r + v1 = v1r + else: + u1 = u / (13 * l) + u1r + v1 = v / (13 * l) + v1r + + y = yr * l / kap if l <= 8 else yr * ((l + 16) / 116) ** 3 + x = y * 9*u1 / (4*v1) + z = y * (12 - 3*u1 - 20*v1) / (4*v1) + return x, y, z + + @staticmethod + def luv_to_lch_uv(l, u, v): + c = math.hypot(u, v) + h = math.atan2(v, u) + if h < 0: + h += math.tau + return l, c, h + + @staticmethod + def lch_uv_to_luv(l, c, h): + u = math.cos(h) * c + v = math.sin(h) * c + return l, u, v + + @staticmethod + def xyz_to_lab(x, y, z): + # D65 Illuminant aka sRGB(1,1,1) + xn = 0.950489 + yn = 1 + zn = 108.8840 + + delta = 6 / 29 + + def f(t): + return t ** (1/3) if t > delta ** 3 else t / (3*delta**2) + 4/29 + + fy = f(y/yn) + l = 116 * fy - 16 + a = 500 * (f(x/xn) - fy) + b = 200 * (fy - f(z/zn)) + + return l, a, b + + @staticmethod + def lab_to_xyz(l, a, b): + # D65 Illuminant aka sRGB(1,1,1) + xn = 0.950489 + yn = 1 + zn = 108.8840 + + delta = 6 / 29 + + def f1(t): + return t**3 if t > delta else 3*delta**2*(t-4/29) + + l1 = (l+16) / 116 + x = xn * f1(l1+a/500) + y = yn * f1(l1) + z = zn * f1(l1-b/200) + + return x, y, z + + #@staticmethod + #def lab_to_lch_ab(l, a, b): + #c = math.hypot(a, b) + #h = math.atan2(b, a) + #if h < 0: + #h += math.tau + #return l, c, h + + #@staticmethod + #def lch_ab_to_lab(l, c, h): + #a = math.cos(h) * c + #b = math.sin(h) * c + #return l, a, b + + @staticmethod + def conv_func(mode_from, mode_to): + return getattr(Conversion, "%s_to_%s" % (mode_from.name.lower(), mode_to.name.lower()), None) + + @staticmethod + def convert(tuple, mode_from, mode_to): + if mode_from == mode_to: + return tuple + + if len(tuple) == 4: + alpha = tuple[3] + tuple = tuple[:3] + else: + alpha = None + + func = Conversion.conv_func(mode_from, mode_to) + if func: + return func(*tuple) + + if (mode_from, mode_to) in Conversion._conv_paths: + steps = Conversion._conv_paths[(mode_from, mode_to)] + [mode_to] + for step in steps: + func = Conversion.conv_func(mode_from, step) + if not func: + raise ValueError("Missing definition for conversion from %s to %s" % (mode_from, step)) + tuple = func(*tuple) + mode_from = step + if alpha is not None: + tuple += (alpha,) + return tuple + + raise ValueError("No conversion path from %s to %s" % (mode_from, mode_to)) + + +class Color(NVector): + Mode = ColorMode + + def __init__(self, c1=0, c2=0, c3=0, a=1, *, mode=ColorMode.RGB): + if isinstance(a, ColorMode): + raise TypeError("Please update the Color constructor") + super().__init__(c1, c2, c3, a) + self._mode = mode + + @property + def mode(self): + return self._mode + + def convert(self, v): + if v == self._mode: + return self + + self.components = list(Conversion.convert(self.components, self._mode, v)) + + self._mode = v + return self + + def clone(self): + return Color(*self.components, mode=self._mode) + + def converted(self, mode): + return self.clone().convert(mode) + + def to_rgb(self): + return self.converted(ColorMode.RGB) + + def __repr__(self): + return "<%s %s [%.3f, %.3f, %.3f, %.3f]>" % ( + (self.__class__.__name__, self.mode.name) + tuple(self.components) + ) + + def component_names(self): + comps = None + + if self._mode == ColorMode.RGB: + comps = ({"r", "red"}, {"g", "green"}, {"b", "blue"}) + elif self._mode == ColorMode.HSV: + comps = ({"h", "hue"}, {"s", "saturation"}, {"v", "value"}) + elif self._mode == ColorMode.HSL: + comps = ({"h", "hue"}, {"s", "saturation"}, {"l", "lightness"}) + elif self._mode == ColorMode.LCH_uv: # in (ColorMode.LCH_uv, ColorMode.LCH_ab): + comps = ({"l", "luma", "luminance"}, {"c", "choma"}, {"h", "hue"}) + elif self._mode == ColorMode.XYZ: + comps = "xyz" + elif self._mode == ColorMode.LUV: + comps = "luv" + elif self._mode == ColorMode.LAB: + comps = "lab" + + return comps + + def _attrindex(self, name): + comps = self.component_names() + + if comps: + for i, vals in enumerate(comps): + if name in vals: + return i + + return None + + def __getattr__(self, name): + if name not in vars(self) and name not in {"_mode", "components"}: + i = self._attrindex(name) + if i is not None: + return self.components[i] + raise AttributeError(name) + + def __setattr__(self, name, value): + if name not in vars(self) and name not in {"_mode", "components"}: + i = self._attrindex(name) + if i is not None: + self.components[i] = value + return + return super().__setattr__(name, value) diff --git a/lottie/utils/ellipse.py b/lottie/utils/ellipse.py new file mode 100644 index 0000000..8302aa9 --- /dev/null +++ b/lottie/utils/ellipse.py @@ -0,0 +1,124 @@ +import math + +from ..nvector import NVector +from ..objects.bezier import BezierPoint + + +## @todo Just output a Bezier object +class Ellipse: + def __init__(self, center, radii, xrot): + """ + @param center 2D vector, center of the ellipse + @param radii 2D vector, x/y radius of the ellipse + @param xrot Angle between the main axis of the ellipse and the x axis (in radians) + """ + self.center = center + self.radii = radii + self.xrot = xrot + + def point(self, t): + return NVector( + self.center[0] + + self.radii[0] * math.cos(self.xrot) * math.cos(t) + - self.radii[1] * math.sin(self.xrot) * math.sin(t), + + self.center[1] + + self.radii[0] * math.sin(self.xrot) * math.cos(t) + + self.radii[1] * math.cos(self.xrot) * math.sin(t) + ) + + def derivative(self, t): + return NVector( + - self.radii[0] * math.cos(self.xrot) * math.sin(t) + - self.radii[1] * math.sin(self.xrot) * math.cos(t), + + - self.radii[0] * math.sin(self.xrot) * math.sin(t) + + self.radii[1] * math.cos(self.xrot) * math.cos(t) + ) + + def to_bezier(self, anglestart, angle_delta): + points = [] + angle1 = anglestart + angle_left = abs(angle_delta) + step = math.pi / 2 + sign = -1 if anglestart+angle_delta < angle1 else 1 + + # We need to fix the first handle + firststep = min(angle_left, step) * sign + alpha = self._alpha(firststep) + q1 = self.derivative(angle1) * alpha + points.append(BezierPoint(self.point(angle1), NVector(0, 0), q1)) + + # Then we iterate until the angle has been completed + tolerance = step / 2 + while angle_left > tolerance: + lstep = min(angle_left, step) + step_sign = lstep * sign + angle2 = angle1 + step_sign + angle_left -= abs(lstep) + + alpha = self._alpha(step_sign) + p2 = self.point(angle2) + q2 = self.derivative(angle2) * alpha + + points.append(BezierPoint(p2, -q2, q2)) + angle1 = angle2 + return points + + def _alpha(self, step): + return math.sin(step) * (math.sqrt(4+3*math.tan(step/2)**2) - 1) / 3 + + @classmethod + def from_svg_arc(cls, start, rx, ry, xrot, large, sweep, dest): + rx = abs(rx) + ry = abs(ry) + + x1 = start[0] + y1 = start[1] + x2 = dest[0] + y2 = dest[1] + phi = math.pi * xrot / 180 + + x1p, y1p = _matrix_mul(phi, (start-dest)/2, -1) + + cr = x1p ** 2 / rx**2 + y1p**2 / ry**2 + if cr > 1: + s = math.sqrt(cr) + rx *= s + ry *= s + + dq = rx**2 * y1p**2 + ry**2 * x1p**2 + pq = (rx**2 * ry**2 - dq) / dq + cpm = math.sqrt(max(0, pq)) + if large == sweep: + cpm = -cpm + cp = NVector(cpm * rx * y1p / ry, -cpm * ry * x1p / rx) + c = _matrix_mul(phi, cp) + NVector((x1+x2)/2, (y1+y2)/2) + theta1 = _angle(NVector(1, 0), NVector((x1p - cp[0]) / rx, (y1p - cp[1]) / ry)) + deltatheta = _angle( + NVector((x1p - cp[0]) / rx, (y1p - cp[1]) / ry), + NVector((-x1p - cp[0]) / rx, (-y1p - cp[1]) / ry) + ) % (2*math.pi) + + if not sweep and deltatheta > 0: + deltatheta -= 2*math.pi + elif sweep and deltatheta < 0: + deltatheta += 2*math.pi + + return cls(c, NVector(rx, ry), phi), theta1, deltatheta + + +def _matrix_mul(phi, p, sin_mul=1): + c = math.cos(phi) + s = math.sin(phi) * sin_mul + + xr = c * p.x - s * p.y + yr = s * p.x + c * p.y + return NVector(xr, yr) + + +def _angle(u, v): + arg = math.acos(max(-1, min(1, u.dot(v) / (u.length * v.length)))) + if u[0] * v[1] - u[1] * v[0] < 0: + return -arg + return arg diff --git a/lottie/utils/file.py b/lottie/utils/file.py new file mode 100644 index 0000000..f2f278c --- /dev/null +++ b/lottie/utils/file.py @@ -0,0 +1,13 @@ +from contextlib import contextmanager + + +@contextmanager +def open_file(file_or_name, mode="w"): + if isinstance(file_or_name, str): + obj = open(file_or_name, mode) + try: + yield obj + finally: + obj.close() + else: + yield file_or_name diff --git a/lottie/utils/font.py b/lottie/utils/font.py new file mode 100644 index 0000000..f87a47f --- /dev/null +++ b/lottie/utils/font.py @@ -0,0 +1,831 @@ +import os +import sys +import subprocess +import fontTools.pens.basePen +import fontTools.ttLib +import fontTools.t1Lib +from fontTools.pens.boundsPen import ControlBoundsPen +import enum +import math +from xml.etree import ElementTree +from ..nvector import NVector +from ..objects.bezier import Bezier, BezierPoint +from ..objects.shapes import Path, Group, Fill, Stroke +from ..objects.text import TextJustify +from ..objects.base import LottieProp, CustomObject +from ..objects.layers import ShapeLayer + + +class BezierPen(fontTools.pens.basePen.BasePen): + def __init__(self, glyphSet, offset=NVector(0, 0)): + super().__init__(glyphSet) + self.beziers = [] + self.current = Bezier() + self.offset = offset + + def _point(self, pt): + return self.offset + NVector(pt[0], -pt[1]) + + def _moveTo(self, pt): + self._endPath() + + def _endPath(self): + if len(self.current.points): + self.beziers.append(self.current) + self.current = Bezier() + + def _closePath(self): + self.current.close() + self._endPath() + + def _lineTo(self, pt): + if len(self.current.points) == 0: + self.current.points.append(self._point(self._getCurrentPoint())) + + self.current.points.append(self._point(pt)) + + def _curveToOne(self, pt1, pt2, pt3): + if len(self.current.points) == 0: + cp = self._point(self._getCurrentPoint()) + self.current.points.append( + BezierPoint( + cp, + None, + self._point(pt1) - cp + ) + + ) + else: + self.current.points[-1].out_tangent = self._point(pt1) - self.current.points[-1].vertex + + dest = self._point(pt3) + self.current.points.append( + BezierPoint( + dest, + self._point(pt2) - dest, + None, + ) + ) + + +class SystemFont: + def __init__(self, family): + self.family = family + self.files = {} + self.styles = set() + self._renderers = {} + + def add_file(self, styles, file): + self.styles |= set(styles) + key = self._key(styles) + self.files.setdefault(key, file) + + def filename(self, styles): + return self.files[self._key(styles)] + + def _key(self, styles): + if isinstance(styles, str): + return (styles,) + return tuple(sorted(styles)) + + def __getitem__(self, styles): + key = self._key(styles) + if key in self._renderers: + return self._renderers[key] + fr = RawFontRenderer(self.files[key]) + self._renderers[key] = fr + return fr + + def __repr__(self): + return "" % self.family + + +class FontQuery: + """! + @see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN21 + https://manpages.ubuntu.com/manpages/cosmic/man1/fc-pattern.1.html + """ + def __init__(self, str=""): + self._query = {} + if isinstance(str, FontQuery): + self._query = str._query.copy() + elif str: + chunks = str.split(":") + family = chunks.pop(0) + self._query = dict( + chunk.split("=") + for chunk in chunks + if chunk + ) + self.family(family) + + def family(self, name): + self._query["family"] = name + return self + + def weight(self, weight): + self._query["weight"] = weight + return self + + def css_weight(self, weight): + """! + Weight from CSS weight value. + + Weight is different between CSS and fontconfig + This creates some interpolations to ensure known values are translated properly + @see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN178 + https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping + """ + if weight < 200: + v = max(0, weight - 100) / 100 * 40 + elif weight < 500: + v = -weight**3 / 200000 + weight**2 * 11/2000 - weight * 17/10 + 200 + elif weight < 700: + v = -weight**2 * 3/1000 + weight * 41/10 - 1200 + else: + v = (weight - 700) / 200 * 10 + 200 + return self.weight(int(round(v))) + + def style(self, *styles): + self._query["style"] = " ".join(styles) + return self + + def charset(self, *hex_ranges): + self._query["charset"] = " ".join(hex_ranges) + return self + + def char(self, char): + return self.charset("%x" % ord(char)) + + def custom(self, property, value): + self._query[property] = value + return self + + def clone(self): + return FontQuery(self) + + def __getitem__(self, key): + return self._query.get(key, "") + + def __contains__(self, item): + return item in self._query + + def get(self, key, default=None): + return self._query.get(key, default) + + def __str__(self): + return self._query.get("family", "") + ":" + ":".join( + "%s=%s" % (p, v) + for p, v in self._query.items() + if p != "family" + ) + + def __repr__(self): + return "" % str(self) + + def weight_to_css(self): + x = int(self["weight"]) + if x < 40: + v = x / 40 * 100 + 100 + elif x < 100: + v = x**3/300 - x**2 * 11/15 + x*167/3 - 3200/3 + elif x < 200: + v = (2050 - 10 * math.sqrt(5) * math.sqrt(1205 - 6 * x)) / 3 + else: + v = (x - 200) * 200 / 10 + 700 + return int(round(v)) + + +class _SystemFontList: + def __init__(self): + self.fonts = None + + def _lazy_load(self): + if self.fonts is None: + self.load() + + def load(self): + self.fonts = {} + self.load_fc_list() + + def cmd(self, *a): + p = subprocess.Popen(a, stdout=subprocess.PIPE) + out, err = p.communicate() + out = out.decode("utf-8").strip() + return out, p.returncode + + def load_fc_list(self): + out, returncode = self.cmd("fc-list", r'--format=%{file}\t%{family[0]}\t%{style[0]}\n') + if returncode == 0: + for line in out.splitlines(): + file, family, styles = line.split("\t") + self._get(family).add_file(styles.split(" "), file) + + def best(self, query): + """! + Returns the renderer best matching the name + """ + out, returncode = self.cmd("fc-match", r"--format=%{family}\t%{style}", str(query)) + if returncode == 0: + return self._font_from_match(out) + + def _font_from_match(self, out): + fam, style = out.split("\t") + fam = fam.split(",")[0] + style = style.split(",")[0].split() + return self[fam][style] + + def all(self, query): + """! + Yields all the renderers matching a query + """ + out, returncode = self.cmd("fc-match", "-s", r"--format=%{family}\t%{style}\n", str(query)) + if returncode == 0: + for line in out.splitlines(): + try: + yield self._font_from_match(line) + except (fontTools.ttLib.TTLibError, fontTools.t1Lib.T1Error): + pass + + def default(self): + """! + Returns the default fornt renderer + """ + return self.best() + + def _get(self, family): + self._lazy_load() + if family in self.fonts: + return self.fonts[family] + font = SystemFont(family) + self.fonts[family] = font + return font + + def __getitem__(self, key): + self._lazy_load() + return self.fonts[key] + + def __iter__(self): + self._lazy_load() + return iter(self.fonts.values()) + + def keys(self): + self._lazy_load() + return self.fonts.keys() + + def __contains__(self, item): + self._lazy_load() + return item in self.fonts + + +## Dictionary of system fonts +fonts = _SystemFontList() + + +def collect_kerning_pairs(font): + if "GPOS" not in font: + return {} + + gpos_table = font["GPOS"].table + + unique_kern_lookups = set() + for item in gpos_table.FeatureList.FeatureRecord: + if item.FeatureTag == "kern": + feature = item.Feature + unique_kern_lookups |= set(feature.LookupListIndex) + + kerning_pairs = {} + for kern_lookup_index in sorted(unique_kern_lookups): + lookup = gpos_table.LookupList.Lookup[kern_lookup_index] + if lookup.LookupType in {2, 9}: + for pairPos in lookup.SubTable: + if pairPos.LookupType == 9: # extension table + if pairPos.ExtensionLookupType == 8: # contextual + continue + elif pairPos.ExtensionLookupType == 2: + pairPos = pairPos.ExtSubTable + + if pairPos.Format != 1: + continue + + firstGlyphsList = pairPos.Coverage.glyphs + for ps_index, _ in enumerate(pairPos.PairSet): + for pairValueRecordItem in pairPos.PairSet[ps_index].PairValueRecord: + secondGlyph = pairValueRecordItem.SecondGlyph + valueFormat = pairPos.ValueFormat1 + + if valueFormat == 5: # RTL kerning + kernValue = "<%d 0 %d 0>" % ( + pairValueRecordItem.Value1.XPlacement, + pairValueRecordItem.Value1.XAdvance) + elif valueFormat == 0: # RTL pair with value <0 0 0 0> + kernValue = "<0 0 0 0>" + elif valueFormat == 4: # LTR kerning + kernValue = pairValueRecordItem.Value1.XAdvance + else: + print( + "\tValueFormat1 = %d" % valueFormat, + file=sys.stdout) + continue # skip the rest + + kerning_pairs[(firstGlyphsList[ps_index], secondGlyph)] = kernValue + return kerning_pairs + + +class GlyphMetrics: + def __init__(self, glyph, lsb, aw, xmin, xmax): + self.glyph = glyph + self.lsb = lsb + self.advance = aw + self.xmin = xmin + self.xmax = xmax + self.width = xmax - xmin + self.advance = xmax + + def draw(self, pen): + return self.glyph.draw(pen) + + +class Font: + def __init__(self, wrapped): + self.wrapped = wrapped + if isinstance(self.wrapped, fontTools.ttLib.TTFont): + self.cmap = self.wrapped.getBestCmap() or {} + else: + self.cmap = {} + + self.glyphset = self.wrapped.getGlyphSet() + + @classmethod + def open(cls, filename): + try: + f = fontTools.ttLib.TTFont(filename) + except fontTools.ttLib.TTLibError: + f = fontTools.t1Lib.T1Font(filename) + f.parse() + + return cls(f) + + def getGlyphSet(self): + return self.wrapped.getGlyphSet() + + def getBestCmap(self): + return {} + + def glyph_name(self, codepoint): + if isinstance(codepoint, str): + if len(codepoint) != 1: + return "" + codepoint = ord(codepoint) + + if codepoint in self.cmap: + return self.cmap[codepoint] + + return self.calculated_glyph_name(codepoint) + + @staticmethod + def calculated_glyph_name(codepoint): + from fontTools import agl # Adobe Glyph List + if codepoint in agl.UV2AGL: + return agl.UV2AGL[codepoint] + elif codepoint <= 0xFFFF: + return "uni%04X" % codepoint + else: + return "u%X" % codepoint + + def scale(self): + if isinstance(self.wrapped, fontTools.ttLib.TTFont): + return 1 / self.wrapped["head"].unitsPerEm + elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): + return self.wrapped["FontMatrix"][0] + + def yMax(self): + if isinstance(self.wrapped, fontTools.ttLib.TTFont): + return self.wrapped["head"].yMax + elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): + return self.wrapped["FontBBox"][3] + + def glyph(self, glyph_name): + if isinstance(self.wrapped, fontTools.ttLib.TTFont): + glyph = self.glyphset[glyph_name] + + xmin = getattr(glyph._glyph, "xMin", glyph.lsb) + xmax = getattr(glyph._glyph, "xMax", glyph.width) + return GlyphMetrics(glyph, glyph.lsb, glyph.width, xmin, xmax) + elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): + glyph = self.glyphset[glyph_name] + bounds_pen = ControlBoundsPen(self.glyphset) + bounds = bounds_pen.bounds + glyph.draw(bounds_pen) + if not hasattr(glyph, "width"): + advance = bounds[2] + else: + advance = glyph.width + return GlyphMetrics(glyph, bounds[0], advance, bounds[0], bounds[2]) + + def __contains__(self, key): + if isinstance(self.wrapped, fontTools.t1Lib.T1Font): + return key in self.wrapped.font + return key in self.wrapped + + def __getitem__(self, key): + return self.wrapped[key] + + +class FontRenderer: + tab_width = 4 + + @property + def font(self): + raise NotImplementedError + + def get_query(self): + raise NotImplementedError + + def kerning(self, c1, c2): + return 0 + + def text_to_chars(self, text): + return text + + def _on_missing(self, char, size, pos, group): + """! + - Character as string + - Font size + - [in, out] Character position + - Group shape + """ + + def glyph_name(self, ch): + return self.font.glyph_name(ch) + + def scale(self, size): + return size * self.font.scale() + + def line_height(self, size): + return self.font.yMax() * self.scale(size) + + def ex(self, size): + return self.font.glyph("x").advance * self.scale(size) + + def glyph_beziers(self, glyph, offset=NVector(0, 0)): + pen = BezierPen(self.font.glyphset, offset) + glyph.draw(pen) + return pen.beziers + + def glyph_shapes(self, glyph, offset=NVector(0, 0)): + beziers = self.glyph_beziers(glyph, offset) + return [ + Path(bez) + for bez in beziers + ] + + def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i): + chname = self.glyph_name(ch) + + if chname in self.font.glyphset: + glyphdata = self.font.glyph(chname) + #pos.x += glyphdata.lsb * scale + glyph_shapes = self.glyph_shapes(glyphdata, pos / scale) + + if glyph_shapes: + if len(glyph_shapes) > 1: + glyph_shape_group = line.add_shape(Group()) + glyph_shape = glyph_shape_group + else: + glyph_shape_group = line + glyph_shape = glyph_shapes[0] + + for sh in glyph_shapes: + sh.shape.value.scale(scale) + glyph_shape_group.add_shape(sh) + + glyph_shape.name = ch + + kerning = 0 + if use_kerning and i < len(chars) - 1: + nextcname = chars[i+1] + kerning = self.kerning(chname, nextcname) + + pos.x += (glyphdata.advance + kerning) * scale + return True + return False + + def render(self, text, size, pos=None, use_kerning=True): + """! + Renders some text + + @param text String to render + @param size Font size (in pizels) + @param[in,out] pos Text position + @param use_kerning Whether to honour kerning info from the font file + + @returns a Group shape, augmented with some extra attributes: + - line_height Line height + - next_x X position of the next character + """ + scale = self.scale(size) + line_height = self.line_height(size) + group = Group() + group.name = text + if pos is None: + pos = NVector(0, 0) + start_x = pos.x + line = Group() + group.add_shape(line) + #group.transform.scale.value = NVector(100, 100) * scale + + chars = self.text_to_chars(text) + for i, ch in enumerate(chars): + if ch == "\n": + line.next_x = pos.x + pos.x = start_x + pos.y += line_height + line = Group() + group.add_shape(line) + continue + elif ch == "\t": + chname = self.glyph_name(ch) + if chname in self.font.glyphset: + width = self.font.glyph(chname).advance + else: + width = self.ex(size) + pos.x += width * scale * self.tab_width + continue + + self._on_character(ch, size, pos, scale, line, use_kerning, chars, i) + + group.line_height = line_height + group.next_x = line.next_x = pos.x + return group + + +class RawFontRenderer(FontRenderer): + def __init__(self, filename): + self.filename = filename + self._font = Font.open(filename) + self._kerning = None + + @property + def font(self): + return self._font + + def kerning(self, c1, c2): + if self._kerning is None: + self._kerning = collect_kerning_pairs(self.font) + return self._kerning.get((c1, c2), 0) + + def __repr__(self): + return "" % self.filename + + def get_query(self): + return self.filename + + +class FallbackFontRenderer(FontRenderer): + def __init__(self, query, max_attempts=10): + self.query = FontQuery(query) + self._best = None + self._bq = None + self._fallback = {} + self.max_attempts = max_attempts + + @property + def font(self): + return self.best.font + + def get_query(self): + return self.query + + def ex(self, size): + best = self.best + if "x" not in self.font.glyphset: + best = fonts.best(self.query.clone().char("x")) + return best.ex(size) + + @property + def best(self): + cq = str(self.query) + if self._best is None or self._bq != cq: + self._best = fonts.best(self.query) + self._bq = cq + return self._best + + def fallback_renderer(self, char): + if char in self._fallback: + return self._fallback[char] + + if len(char) != 1: + return None + + codepoint = ord(char) + name = Font.calculated_glyph_name(codepoint) + for i, font in enumerate(fonts.all(self.query.clone().char(char))): + # For some reason fontconfig sometimes returns a font that doesn't + # actually contain the glyph + if name in font.font.glyphset or codepoint in font.cmap: + self._fallback[char] = font + return font + + if i > self.max_attempts: + self._fallback[char] = None + return None + + def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i): + if self.best._on_character(char, size, pos, scale, group, use_kerning, chars, i): + return True + + font = self.fallback_renderer(char) + if not font: + return False + + child = font.render(char, size, pos) + if len(child.shapes) == 2: + group.add_shape(child.shapes[0]) + else: + group.add_shape(child) + + def __repr__(self): + return "" % self.query + + +class EmojiRenderer(FontRenderer): + _split = None + + def __init__(self, wrapped, emoji_dir): + if not os.path.isdir(emoji_dir): + raise Exception("Not a valid directory: %s" % emoji_dir) + self.wrapped = wrapped + self.emoji_dir = emoji_dir + self._svgs = {} + + @property + def font(self): + return self.wrapped.font + + def _get_svg(self, char): + from ..parsers.svg import parse_svg_file + + if char in self._svgs: + return self._svgs[char] + + basename = "-".join("%x" % ord(cp) for cp in char) + ".svg" + filename = os.path.join(self.emoji_dir, basename) + if not os.path.isfile(filename): + self._svgs[char] = None + return None + + svga = parse_svg_file(filename) + svgshape = Group() + svgshape.name = basename + for layer in svga.layers: + if isinstance(layer, ShapeLayer): + for shape in layer.shapes: + svgshape.add_shape(shape) + + self._svgs[char] = svgshape + svgshape._bbox = svgshape.bounding_box() + return svgshape + + def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i): + svgshape = self._get_svg(char) + if svgshape: + target_height = self.line_height(size) + scale = target_height / svgshape._bbox.height + shape_group = Group() + shape_group = svgshape.clone() + shape_group.transform.scale.value *= scale + offset = NVector( + -svgshape._bbox.x1 + svgshape._bbox.width * 0.075, + -svgshape._bbox.y2 + svgshape._bbox.height * 0.1 + ) + shape_group.transform.position.value = pos + offset * scale + group.add_shape(shape_group) + pos.x += svgshape._bbox.width * scale + return True + return self.wrapped._on_character(char, size, pos, scale, group, use_kerning, chars, i) + + def get_query(self): + return self.wrapped.get_query() + + @staticmethod + def _get_splitter(): + if EmojiRenderer._split is None: + try: + import grapheme + EmojiRenderer._split = grapheme.graphemes + except ImportError: + sys.stderr.write("Install `grapheme` for better Emoji support\n") + EmojiRenderer._split = lambda x: x + return EmojiRenderer._split + + @staticmethod + def emoji_split(string): + return EmojiRenderer._get_splitter()(string) + + def text_to_chars(self, string): + return list(self.emoji_split(string)) + + +class FontStyle: + def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None): + self.emoji_svg = emoji_svg + self._set_query(query) + self.size = size + self.justify = justify + self.position = position.clone() if position else NVector(0, 0) + self.use_kerning = use_kerning + + def _set_query(self, query): + if isinstance(query, str) and os.path.isfile(query): + self._renderer = RawFontRenderer(query) + else: + self._renderer = FallbackFontRenderer(query) + + if self.emoji_svg: + self._renderer = EmojiRenderer(self._renderer, self.emoji_svg) + + @property + def query(self): + return self._renderer.get_query() + + @query.setter + def query(self, value): + if str(value) != str(self.query): + self._set_query(value) + + @property + def renderer(self): + return self._renderer + + def render(self, text, pos=NVector(0, 0)): + group = self._renderer.render(text, self.size, self.position+pos, self.use_kerning) + for subg in group.shapes[:-1]: + width = subg.next_x - self.position.x - pos.x + if self.justify == TextJustify.Center: + subg.transform.position.value.x -= width / 2 + elif self.justify == TextJustify.Right: + subg.transform.position.value.x -= width + return group + + def clone(self): + return FontStyle(str(self._renderer.query), self.size, self.justify, NVector(*self.position), self.use_kerning) + + @property + def ex(self): + return self._renderer.ex(self.size) + + @property + def line_height(self): + return self._renderer.line_height(self.size) + + +def _propfac(a): + return property(lambda s: s._get(a), lambda s, v: s._set(a, v)) + + +class FontShape(CustomObject): + _props = [ + LottieProp("query_string", "_query", str), + LottieProp("size", "_size", float), + LottieProp("justify", "_justify", TextJustify), + LottieProp("text", "_text", str), + LottieProp("position", "_position", NVector), + ] + wrapped_lottie = Group + + def __init__(self, text="", query="", size=64, justify=TextJustify.Left): + CustomObject.__init__(self) + if isinstance(query, FontStyle): + self.style = query + else: + self.style = FontStyle(query, size, justify) + self.text = text + self.hidden = None + + def _get(self, a): + return getattr(self.style, a) + + def _set(self, a, v): + return setattr(self.style, a, v) + + query = _propfac("query") + size = _propfac("size") + justify = _propfac("justify") + position = _propfac("position") + + @property + def query_string(self): + return str(self.query) + + @query_string.setter + def query_string(self, v): + self.query = v + + def _build_wrapped(self): + g = self.style.render(self.text) + self.line_height = g.line_height + return g + + def bounding_box(self, time=0): + return self.wrapped.bounding_box(time) diff --git a/lottie/utils/ik.py b/lottie/utils/ik.py new file mode 100644 index 0000000..28f5a88 --- /dev/null +++ b/lottie/utils/ik.py @@ -0,0 +1,97 @@ +from..nvector import NVector + + +# FABRIK +class Chain: + def __init__(self, tail, fixed_tail=True, tolerance=0.5, max_iter=8): + self.joints = [tail.clone()] + self.fixed_tail = fixed_tail + self.lengths = [] + self.total_length = 0 + self.tolerance = tolerance + self.max_iter = max_iter + + def add_joint(self, point): + length = (point - self.joints[-1]).length + self.lengths.append(length) + self.total_length += length + self.joints.append(point.clone()) + + def add_joints(self, head, n): + delta = head - self.joints[-1] + self.total_length += delta.length + segment = delta / n + seglen = segment.length + for i in range(n): + self.lengths.append(seglen) + self.joints.append(self.joints[-1] + segment) + + def backward(self, target): + """! + target -> -> start + """ + self.joints[-1] = target + for i in range(len(self.joints)-2, -1, -1): + r = self.joints[i+1] - self.joints[i] + l = self.lengths[i] / r.length + self.joints[i] = self.joints[i+1].lerp(self.joints[i], l) + + def forward(self, target): + """! + start -> -> tail + """ + self.joints[0] = target + for i in range(0, len(self.joints)-1): + r = self.joints[i+1] - self.joints[i] + l = self.lengths[i] / r.length + self.joints[i+1] = self.joints[i].lerp(self.joints[i+1], l) + + def reach(self, target): + if not self.fixed_tail: + self.backward(target) + return + + distance = (target - self.joints[0]).length + if distance >= self.total_length: + for i in range(len(self.joints)-1): + r = target - self.joints[i] + l = self.lengths[i] / r.length + self.joints[i+1] = self.joints[i].lerp(target, l) + return + + base = self.joints[0] + + distance = (target - self.joints[-1]).length + n_it = 0 + while distance > self.tolerance and n_it < self.max_iter: + self.backward(target) + self.forward(base) + distance = (target - self.joints[-1]).length + n_it += 1 + + +class Octopus: + def __init__(self, master): + self.chains = {"master": master} + self.master = master + + @property + def base(self): + return self.master.joints[-1] + + def add_chain(self, name): + ch = Chain(self.base) + self.chains[name] = ch + return ch + + def reach(self, target_map): + centroid = NVector(0, 0) + for chain, target in target_map.items(): + self.chains[chain].backward(target) + centroid += self.chains[chain].joints[0] + centroid /= len(target_map) + + self.master.reach(centroid) + + for chain in target_map.keys(): + self.chains[chain].forward(self.base) diff --git a/lottie/utils/linediff.py b/lottie/utils/linediff.py new file mode 100644 index 0000000..1a6d77a --- /dev/null +++ b/lottie/utils/linediff.py @@ -0,0 +1,38 @@ +from io import StringIO +from difflib import SequenceMatcher +from ..exporters import prettyprint + + +def difflines_str(a, b, widtha=None, widthb=None): + lines_a = a.splitlines() + lines_b = b.splitlines() + ia = 0 + ib = 0 + + if widtha is None: + widtha = max(map(len, lines_a)) + if widthb is None: + widthb = max(map(len, lines_b)) + + for ja, jb, size in SequenceMatcher(None, lines_a, lines_b, False).get_matching_blocks(): + sideprinter(lines_a[ia:ja], lines_b[ib:jb], widtha, widthb, "\x1b[31m>", "=", "<\x1b[m") + ia = ja+size + ib = jb+size + sideprinter(lines_a[ja:ia], lines_b[jb:ib], widtha, widthb, "\x1b[m ", "|", " \x1b[m") + + +def sideprinter(left, right, widtha=40, widthb=40, prefix="", infix=" | ", suffix=""): + if len(left) > len(right): + right += [""] * (len(left) - len(right)) + else: + left += [""] * (len(right) - len(left)) + for l, r in zip(left, right): + print("".join([prefix, l[:widtha].ljust(widtha), infix, r[:widthb].ljust(widthb), suffix])) + + +def difflines(a, b, widtha=None, widthb=None): + ioa = StringIO() + prettyprint(a, ioa) + iob = StringIO() + prettyprint(b, iob) + difflines_str(ioa.getvalue(), iob.getvalue(), widtha, widthb) diff --git a/lottie/utils/restructure.py b/lottie/utils/restructure.py new file mode 100644 index 0000000..1c3bef4 --- /dev/null +++ b/lottie/utils/restructure.py @@ -0,0 +1,229 @@ +from .. import objects + + +class RestructuredLayer: + def __init__(self, lottie): + self.lottie = lottie + self.children_pre = [] + self.children_post = [] + self.structured = False + self.shapegroup = None + self.matte_target = False + self.matte_source = None + self.matte_id = None + + def add(self, child): + c = self.children_pre if self.structured else self.children_post + c.insert(0, child) + + +class RestructuredShapeGroup: + def __init__(self, lottie): + self.lottie = lottie + self.children = [] + self.fill = None + self.stroke = None + self.layer = False + self.paths = None + self.stroke_above = False + + def empty(self): + return not self.children + + def finalize(self, thresh=6): + for g in self.subgroups: + if g.layer: + self.layer = True + for gg in self.subgroups: + gg.layer = True + return + nchild = len(self.children) + self.layer = nchild > thresh and self.lottie.name + + @property + def subgroups(self): + for g in self.children: + if isinstance(g, RestructuredShapeGroup): + yield g + + def add(self, child): + self.children.insert(0, child) + + +class RestructuredModifier: + def __init__(self, lottie, child): + self.child = child + self.lottie = lottie + + +class RestructuredPathMerger: + def __init__(self): + self.paths = [] + + def append(self, path): + self.paths.append(path) + + +class RestructuredAnimation: + def __init__(self): + self.layers = [] + self.precomp = {} + + +class AbstractBuilder: + merge_paths = False + + def _on_animation(self, animation): + raise NotImplementedError() + + def _on_shapegroup(self, shapegroup, out_parent): + raise NotImplementedError() + + def _on_shape(self, shape, shapegroup, out_parent): + raise NotImplementedError() + + def _on_merged_path(self, shape, shapegroup, out_parent): + raise NotImplementedError() + + def _on_shape_modifier(self, shape, shapegroup, out_parent): + raise NotImplementedError() + + def process(self, animation: objects.Animation): + out_parent = self._on_animation(animation) + + restructured = self.restructure_animation(animation, self.merge_paths) + for id, layers in restructured.precomp.items(): + self._on_precomp(id, out_parent, layers) + + for asset in animation.assets or []: + self._on_asset(asset) + + for layer_builder in restructured.layers: + self.process_layer(layer_builder, out_parent) + + def _on_layer(self, layer_builder, out_parent): + raise NotImplementedError() + + def _on_precomp(self, id, out_parent, layers): + raise NotImplementedError() + + def _on_asset(self, asset): + pass + + def process_layer(self, layer_builder, out_parent): + out_layer = self._on_layer(layer_builder, out_parent) + + if out_layer is None: + return + + for c in layer_builder.children_pre: + self.process_layer(c, out_layer) + + shapegroup = getattr(layer_builder, "shapegroup", None) + if shapegroup: + self.shapegroup_process_children(shapegroup, out_layer) + + for c in layer_builder.children_post: + self.process_layer(c, out_layer) + + self._on_layer_end(out_layer) + + def _on_layer_end(self, out_layer): + pass + + def shapegroup_process_child(self, shape, shapegroup, out_parent): + if isinstance(shape, RestructuredShapeGroup): + return self._on_shapegroup(shape, out_parent) + elif isinstance(shape, RestructuredPathMerger): + return self._on_merged_path(shape, shapegroup, out_parent) + elif isinstance(shape, RestructuredModifier): + return self._on_shape_modifier(shape, shapegroup, out_parent) + else: + return self._on_shape(shape, shapegroup, out_parent) + + def shapegroup_process_children(self, shapegroup, out_parent): + for shape in shapegroup.children: + self.shapegroup_process_child(shape, shapegroup, out_parent) + + def restructure_animation(self, animation, merge_paths): + restr = RestructuredAnimation() + restr.layers = self.restructure_layer_list(animation.layers, merge_paths) + if animation.assets: + for asset in animation.assets: + if isinstance(asset, objects.Precomp): + restr.precomp[asset.id] = self.restructure_layer_list(asset.layers, merge_paths) + return restr + + def restructure_layer_list(self, layer_list, merge_paths): + layers = {} + flat_layers = [] + prev = None + for layer in layer_list: + laybuilder = RestructuredLayer(layer) + flat_layers.append(laybuilder) + + if layer.index is not None: + layers[layer.index] = laybuilder + + if isinstance(layer, objects.ShapeLayer): + laybuilder.shapegroup = RestructuredShapeGroup(layer) + laybuilder.layer = True + for shape in layer.shapes: + self.restructure_shapegroup(shape, laybuilder.shapegroup, merge_paths) + laybuilder.shapegroup.finalize() + + if layer.matte_mode not in {None, objects.MatteMode.Normal}: + laybuilder.matte_source = prev + if prev: + prev.matte_target = laybuilder + + prev = laybuilder + + top_layers = [] + for layer in flat_layers: + layer.structured = True + if layer.lottie.parent_index is not None: + layers[layer.lottie.parent_index].add(layer) + else: + top_layers.insert(0, layer) + + return top_layers + + def restructure_shapegroup(self, shape, shape_group, merge_paths): + if isinstance(shape, (objects.Fill, objects.GradientFill)): + if not shape_group.fill: + shape_group.fill = shape + elif isinstance(shape, objects.BaseStroke): + if not shape_group.stroke or shape_group.stroke.width.get_value(0) < shape.width.get_value(0): + shape_group.stroke = shape + if not shape_group.fill: + shape_group.stroke_above = True + elif isinstance(shape, (objects.Path)): + if merge_paths: + if not shape_group.paths: + shape_group.paths = RestructuredPathMerger() + shape_group.add(shape_group.paths) + shape_group.paths.append(shape) + else: + shape_group.add(shape) + elif isinstance(shape, (objects.Group)): + subgroup = RestructuredShapeGroup(shape) + shape_group.add(subgroup) + merge_paths = self.merge_paths and not any(isinstance(p, objects.Group) for p in shape.shapes) + for subshape in shape.shapes: + self.restructure_shapegroup(subshape, subgroup, merge_paths) + subgroup.finalize() + elif isinstance(shape, (objects.Modifier)): + if shape_group.children: + ch = shape_group.children.pop(0) + shape_group.add(RestructuredModifier(shape, ch)) + elif isinstance(shape, (objects.ShapeElement)): + shape_group.add(shape) + elif isinstance(shape, (objects.base.CustomObject)): + if self._custom_object_supported(shape): + shape_group.add(shape) + else: + self.restructure_shapegroup(shape.wrapped, shape_group, self.merge_paths) + + def _custom_object_supported(self, shape): + return False diff --git a/lottie/utils/script.py b/lottie/utils/script.py new file mode 100644 index 0000000..92b09e9 --- /dev/null +++ b/lottie/utils/script.py @@ -0,0 +1,82 @@ +import os +import sys +import argparse +import inspect +from ..exporters import exporters +from .stripper import float_strip + + +def _get_caller(): + return inspect.getmodule(inspect.currentframe().f_back.f_back) + + +def _get_parser(caller, basename, path, formats, verbosity): + if basename is None: + basename = os.path.splitext(os.path.basename(caller.__file__))[0] + + parser = argparse.ArgumentParser( + conflict_handler='resolve' + ) + + parser.add_argument( + "--name", + "-n", + default=basename, + help="Output basename", + ) + parser.add_argument( + "--path", + default=path, + help="Output path", + ) + parser.add_argument( + "--formats", "-f", + nargs="+", + choices=list(sum((e.extensions for e in exporters), [])), + default=formats, + help="Formates to render", + metavar="format" + ) + parser.add_argument( + "--verbosity", + type=int, + default=int(verbosity) + ) + from .. import __version__ + parser.add_argument( + "--version", "-v", + action="version", + version="%(prog)s - python-lottie script " + __version__ + ) + exporters.set_options(parser) + + return parser + + +def get_parser(basename=None, path="/tmp", formats=["html"], verbosity=1): + caller = _get_caller() + return _get_parser(caller, basename, path, formats, verbosity) + + +def run(animation, ns): + for fmt in ns.formats: + if ns.path == "" and ns.name == "-": + outfile = sys.stdout + else: + absname = os.path.abspath(os.path.join(ns.path, ns.name + "." + fmt)) + if ns.verbosity: + sys.stderr.write("file://%s\n" % absname) + outfile = absname + exporter = exporters.get_from_extension(fmt) + exporter.process(animation, outfile, **exporter.argparse_options(ns)) + + +def script_main(animation, basename=None, path="/tmp", formats=["html"], verbosity=1, strip=float_strip): + """ + Sets up a script to output an animation into various formats + """ + caller = _get_caller() + if caller and caller.__name__ == "__main__": + parser = _get_parser(caller, basename, path, formats, verbosity) + strip(animation) + run(animation, parser.parse_args()) diff --git a/lottie/utils/stripper.py b/lottie/utils/stripper.py new file mode 100644 index 0000000..676be2c --- /dev/null +++ b/lottie/utils/stripper.py @@ -0,0 +1,52 @@ +from ..objects.base import LottieObject, ObjectVisitor +from ..objects.bezier import Bezier +from ..objects.helpers import Transform +from ..nvector import NVector + + +class Strip(ObjectVisitor): + def __init__(self, float_round, remove_attributes={}): + self.float_round = float_round + self.remove_attributes = remove_attributes + + def round(self, fl): + return round(fl, self.float_round) + + def nvector(self, value): + value.components = list(map(self.round, value.components)) + return value + + def visit_property(self, object, property, value): + if isinstance(value, Bezier): + for l in ["vertices", "in_tangents", "out_tangents"]: + try: + setattr(value, l, [self.nvector(NVector(p.x, p.y)) for p in getattr(value, l)]) + except: + print("An exception occurred") + elif property.lottie in self.remove_attributes: + property.set(object, None) + elif isinstance(value, float): + property.set(object, round(value, 3)) + elif isinstance(value, NVector): + self.nvector(value) + + +class TransformStip(Strip): + def visit(self, object): + if isinstance(object, Transform): + self.transform_unset(object, "anchor_point", NVector(0, 0)) + self.transform_unset(object, "position", NVector(0, 0)) + #self.transform_unset(object, "scale", NVector(100, 100)) + self.transform_unset(object, "rotation", 0) + #self.transform_unset(object, "opacity", 100) + self.transform_unset(object, "skew", 0) + self.transform_unset(object, "skew_axis", 0) + + def transform_unset(self, object, prop_name, value): + prop = getattr(object, prop_name) + if not prop.animated and prop.value == value: + setattr(object, prop_name, None) + + +heavy_strip = TransformStip(3, {"ind", "ix", "nm", "mn"}) +float_strip = Strip(3) diff --git a/lottie/utils/transform.py b/lottie/utils/transform.py new file mode 100644 index 0000000..6a66b66 --- /dev/null +++ b/lottie/utils/transform.py @@ -0,0 +1,205 @@ +import math +from ..nvector import NVector + + +def _sign(x): + if x < 0: + return -1 + return 1 + + +class TransformMatrix: + scalar = float + + def __init__(self): + """ Creates an Identity matrix """ + self.to_identity() + + def clone(self): + m = TransformMatrix() + m._mat = list(self._mat) + return m + + return self + + def __getitem__(self, key): + row, col = key + return self._mat[row*4+col] + + def __setitem__(self, key, value): + row, col = key + self._mat[row*4+col] = self.scalar(value) + + @property + def a(self): + return self[0, 0] + + @a.setter + def a(self, v): + self[0, 0] = self.scalar(v) + + @property + def b(self): + return self[0, 1] + + @b.setter + def b(self, v): + self[0, 1] = self.scalar(v) + + @property + def c(self): + return self[1, 0] + + @c.setter + def c(self, v): + self[1, 0] = self.scalar(v) + + @property + def d(self): + return self[1, 1] + + @d.setter + def d(self, v): + self[1, 1] = self.scalar(v) + + @property + def tx(self): + return self[3, 0] + + @tx.setter + def tx(self, v): + self[3, 0] = self.scalar(v) + + @property + def ty(self): + return self[3, 1] + + @ty.setter + def ty(self, v): + self[3, 1] = self.scalar(v) + + def __str__(self): + return str(self._mat) + + def scale(self, x, y=None): + if y is None: + y = x + + m = TransformMatrix() + m.a = x + m.d = y + self *= m + return self + + def translate(self, x, y=None): + if y is None: + x, y = x + m = TransformMatrix() + m.tx = x + m.ty = y + self *= m + return self + + def skew(self, x_rad, y_rad): + m = TransformMatrix() + m.c = math.tan(x_rad) + m.b = math.tan(y_rad) + self *= m + return self + + def skew_from_axis(self, skew, axis): + self.rotate(axis) + m = TransformMatrix() + m.c = math.tan(skew) + self *= m + self.rotate(-axis) + return self + + def row(self, i): + return NVector(self[i, 0], self[i, 1], self[i, 2], self[i, 3]) + + def column(self, i): + return NVector(self[0, i], self[1, i], self[2, i], self[3, i]) + + def to_identity(self): + self._mat = [ + 1., 0., 0., 0., + 0., 1., 0., 0., + 0., 0., 1., 0., + 0., 0., 0., 1., + ] + + def apply(self, vector): + vector3 = NVector(vector.x, vector.y, 0, 1) + return NVector( + self.column(0).dot(vector3), + self.column(1).dot(vector3), + ) + + @classmethod + def rotation(cls, radians): + m = cls() + m.a = math.cos(radians) + m.b = -math.sin(radians) + m.c = math.sin(radians) + m.d = math.cos(radians) + + return m + + def __mul__(self, other): + m = TransformMatrix() + for row in range(4): + for col in range(4): + m[row, col] = self.row(row).dot(other.column(col)) + return m + + def __imul__(self, other): + m = self * other + self._mat = m._mat + return self + + def rotate(self, radians): + self *= TransformMatrix.rotation(radians) + return self + + def extract_transform(self): + a = self.a + b = self.b + c = self.c + d = self.d + tx = self.tx + ty = self.ty + + dest_trans = { + "translation": NVector(tx, ty), + "angle": 0, + "scale": NVector(1, 1), + "skew_axis": 0, + "skew_angle": 0, + } + + delta = a * d - b * c + if a != 0 or b != 0: + r = math.hypot(a, b) + dest_trans["angle"] = - _sign(b) * math.acos(a/r) + sx = r + sy = delta / r + dest_trans["skew_axis"] = 0 + else: + r = math.hypot(c, d) + dest_trans["angle"] = math.pi / 2 + _sign(d) * math.acos(c / r) + sx = delta / r + sy = r + dest_trans["skew_axis"] = math.pi / 2 + + dest_trans["scale"] = NVector(sx, sy) + + skew = math.atan2((a * c + b * d), r * r) + dest_trans["skew_angle"] = skew + + return dest_trans + + def to_css_2d(self): + return "matrix(%s, %s, %s, %s, %s, %s)" % ( + self.a, self.b, self.c, self.d, self.tx, self.ty + ) diff --git a/mmlottie_bench/put_mmlottie_bench_here b/mmlottie_bench/put_mmlottie_bench_here new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d64002f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +# Core dependencies +transformers==4.51.3 +safetensors==0.7.0 + +# Data processing +numpy==1.24.1 +pandas==2.3.3 +Pillow==10.1.0 +datasets==3.5.0 + +# Vision and video +decord==0.6.0 +opencv-python==4.12.0.88 +qwen-vl-utils==0.0.11 + +# Hugging Face +huggingface-hub==0.36.0 + +# Web interface +gradio==5.37.0 + +# System utilities +psutil==7.2.1