diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..040cd9c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,25 @@ +name: Publish to PyPI + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Hatch + uses: pypa/hatch@a3c83ab3d481fbc2dc91dd0088628817488dd1d5 + + - name: Build + run: hatch build + + - name: Publish to PyPI + env: + HATCH_INDEX_USER: __token__ + HATCH_INDEX_AUTH: ${{ secrets.PYPI_TOKEN }} + run: hatch publish diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..fb39c4c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Hatch + uses: pypa/hatch@a3c83ab3d481fbc2dc91dd0088628817488dd1d5 + + - name: Run tests + run: hatch test -a + + - name: Check formatting and lint + run: hatch fmt --check \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8333c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info +*.spec +.ruff_cache + +# Virtual environments +.venv + +# Testing +.pytest_cache +coverage.xml +.coverage* + +# Misc +dircat.md +test.md \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..597e03b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Romelium + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbf9868 --- /dev/null +++ b/README.md @@ -0,0 +1,227 @@ +

+ FrameSVG +

+ +

+ Convert animated GIFs to animated SVGs. +

+ +

+ Build Status + MIT License + PyPI + Python Versions +

+ + + + + +`framesvg` is a command-line tool and Python library that converts animated GIFs into animated SVGs. It leverages the power of [VTracer](https://www.visioncortex.org/vtracer/) for raster-to-vector conversion, producing smooth, scalable, and *true vector* animations. This is a significant improvement over embedding raster images (like GIFs) directly within SVGs, as `framesvg` generates genuine vector output that plays automatically and scales beautifully. Ideal for readmes, documentation, and web graphics. + +

+Preferred to be viewed on GitHub +

+ +
+ +## Why Use framesvg? + +* **True Vector Output:** Unlike simply embedding a GIF within an SVG, `framesvg` creates a true vector animation. This means: + * **Scalability:** The SVG can be resized to any dimensions without losing quality. + * **Smaller File Size (Potentially):** For many GIFs, the resulting SVG will be smaller, especially for graphics with large areas of solid color or simple shapes. Complex, photographic GIFs may be larger, however. +* **Automatic Playback:** The generated SVGs are designed to play automatically in any environment that supports SVG animations (web browsers, github, many image viewers, etc.). +* **Easy to Use:** Simple command-line interface and a clean Python API. +* **Customizable:** Control the frame rate and fine-tune the VTracer conversion process for optimal results. + +## Examples + +The following examples demonstrate the conversion of GIFs (left) to SVGs (right) using `framesvg`. + +

+ + + + + + + + +

+ +### More Examples + +

+ + + + + + +

+ +### Complex Examples (Transparent Backgrounds) + +These examples demonstrate `binary` color mode. All bright colors in `binary` color mode turns transparent. (If they appear dark, it is due to the transparency. They will look correct on light backgrounds) + +

+ + + + +

+ +## Installation + +### Using pip (Recommended) + +The easiest way to install `framesvg` is via pip: + +```bash +pip install framesvg +``` + +This installs both the command-line tool and the Python library. + +### From Source + +1. **Clone the repository:** + + ```bash + git clone https://github.com/romelium/framesvg + cd framesvg + ``` + +2. **Install:** + + ```bash + pip install . + ``` + +## Usage + +### Command-Line Interface + +```bash +framesvg input.gif [output.svg] [options] +``` + +* **`input.gif`:** (Required) Path to the input GIF file. +* **`output.svg`:** (Optional) Path to save the output SVG file. If omitted, the output file will have the same name as the input, but with a `.svg` extension. + +**Options:** + +* **`-f`, `--fps `:** Sets the frames per second (FPS) for the animation. (Default: 10). Lower values can reduce file size. +* **`-l`, `--log-level `:** Sets the logging level. (Default: INFO). Choices: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`, `NONE`. `DEBUG` provides detailed output for troubleshooting. + +* **VTracer Options:** These options control the raster-to-vector conversion process performed by VTracer. Refer to the [VTracer Documentation](https://www.visioncortex.org/vtracer-docs) and [Online Demo](https://www.visioncortex.org/vtracer/) for detailed explanations. + + * `-c`, `--colormode `: Color mode. (Default: `color`). Choices: `color`, `binary`. + * `-i`, `--hierarchical `: Hierarchy mode. (Default: `stacked`). Choices: `stacked`, `cutout`. + * `-m`, `--mode `: Conversion mode. (Default: `polygon`). Choices: `spline`, `polygon`, `none`. `spline` creates smoother curves, but `polygon` often results in smaller files. + * `-s`, `--filter-speckle `: Reduces noise and small details. (Default: 4). *This is a key parameter for controlling file size.* Higher values = smaller files, but less detail. + * `-p`, `--color-precision `: Number of significant bits for color quantization. (Default: 8). Lower values = smaller files, but fewer colors. + * `-d`, `--layer-difference `: Controls the number of layers. (Default: 16). Higher values can reduce file size. + * `--corner-threshold `: Angle threshold for corner detection. (Default: 60). + * `--length-threshold `: Minimum path length. (Default: 4.0). + * `--max-iterations `: Maximum number of optimization iterations. (Default: 10). + * `--splice-threshold `: Angle threshold for splitting splines. (Default: 45). + * `--path-precision `: Number of decimal places for path coordinates. (Default: 8). + +**Command-Line Examples:** + +```bash +# Basic conversion with default settings +framesvg input.gif + +# Specify output file and set FPS to 24 +framesvg input.gif output.svg -f 24 + +# Optimize for smaller file size (less detail) +framesvg input.gif -s 8 -p 3 -d 128 + +# Enable debug logging +framesvg input.gif -l DEBUG +``` + +### Python API + +```python +from framesvg import gif_to_animated_svg_write, gif_to_animated_svg + +# Example 1: Convert and save to a file +gif_to_animated_svg_write("input.gif", "output.svg", fps=30) + +# Example 2: Get the SVG as a string +animated_svg_string = gif_to_animated_svg("input.gif", fps=12) +print(f"Generated SVG length: {len(animated_svg_string)}") +# ... do something with the string (e.g., save to file, display in a web app) + +# Example 3: Customize VTracer options +custom_options = { + "mode": "spline", + "filter_speckle": 2, +} +gif_to_animated_svg_write("input.gif", "output_custom.svg", vtracer_options=custom_options) +``` + +### API Reference + +* **`gif_to_animated_svg_write(gif_path, output_svg_path, vtracer_options=None, fps=10.0, image_loader=None, vtracer_instance=None)`:** + * `gif_path` (str): Path to the input GIF file. + * `output_svg_path` (str): Path to save the output SVG file. + * `vtracer_options` (dict, optional): A dictionary of VTracer options. If `None`, uses `DEFAULT_VTRACER_OPTIONS`. + * `fps` (float, optional): Frames per second. Defaults to 10.0. + * `image_loader` (ImageLoader, optional): Custom image loader. + * `vtracer_instance` (VTracer, optional): Custom VTracer instance. + * Raises: `FileNotFoundError`, `NotAnimatedGifError`, `NoValidFramesError`, `DimensionError`, `ExtractionError`, `FramesvgError`, `IsADirectoryError`. + +* **`gif_to_animated_svg(gif_path, vtracer_options=None, fps=10.0, image_loader=None, vtracer_instance=None)`:** + * `gif_path` (str): Path to the input GIF file. + * `vtracer_options` (dict, optional): A dictionary of VTracer options. If `None`, uses `DEFAULT_VTRACER_OPTIONS`. + * `fps` (float, optional): Frames per second. Defaults to 10.0. + * `image_loader` (ImageLoader, optional): Custom image loader. + * `vtracer_instance` (VTracer, optional): Custom VTracer instance. + * Returns: The animated SVG as a string. + * Raises: `FileNotFoundError`, `NotAnimatedGifError`, `NoValidFramesError`, `DimensionError`, `ExtractionError`, `FramesvgError`. + +## Tips for Optimizing Large File Size (> 1MB) + +* **[Online Demo](https://www.visioncortex.org/vtracer/):** Use this to visualize tweaking values. Experiment to find the best balance between size and quality. +* **`filter-speckle`:** *This is the most impactful setting for reducing file size, especially on complex images.* Increasing it removes small details. +* **`--mode polygon`:** Use the default polygon mode unless smooth curves (spline mode) are absolutely necessary. Polygon mode can significantly reduce file size by a factor of 5 or more. +* **`layer-difference`:** Increase this to reduce the number of layers. +* **`color-precision`:** Reduce the number of colors by lowering this value. + +## Dev + +### Install Hatch (Recommended) + +follow [this](https://hatch.pypa.io/latest/install) + +or just + +```bash +pip install hatch +``` + +### Format and lint + +```bash +hatch fmt +``` + +### Testing +```bash +hatch test +``` + +### Other Hatch Commands + +```bash +hatch -h +``` + +## Contributing + +Contributions are welcome! Please submit pull requests or open issues on the [GitHub repository](https://github.com/romelium/framesvg). diff --git a/images/Black And White Loop GIF by Pi-Slices.gif b/images/Black And White Loop GIF by Pi-Slices.gif new file mode 100644 index 0000000..091a542 Binary files /dev/null and b/images/Black And White Loop GIF by Pi-Slices.gif differ diff --git a/images/Black And White Loop GIF by Pi-Slices.svg b/images/Black And White Loop GIF by Pi-Slices.svg new file mode 100644 index 0000000..a7c1b29 --- /dev/null +++ b/images/Black And White Loop GIF by Pi-Slices.svg @@ -0,0 +1,12413 @@ + +Generated Animation +30 frames at 24.000000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/Code Coding GIF by EscuelaDevRock Git.gif b/images/Code Coding GIF by EscuelaDevRock Git.gif new file mode 100644 index 0000000..2e904d9 Binary files /dev/null and b/images/Code Coding GIF by EscuelaDevRock Git.gif differ diff --git a/images/Code Coding GIF by EscuelaDevRock Git.svg b/images/Code Coding GIF by EscuelaDevRock Git.svg new file mode 100644 index 0000000..a9b9804 --- /dev/null +++ b/images/Code Coding GIF by EscuelaDevRock Git.svg @@ -0,0 +1,70 @@ + +Generated Animation +4 frames at 7.690000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/Code Coding GIF by EscuelaDevRock GitHub.gif b/images/Code Coding GIF by EscuelaDevRock GitHub.gif new file mode 100644 index 0000000..6ba20b8 Binary files /dev/null and b/images/Code Coding GIF by EscuelaDevRock GitHub.gif differ diff --git a/images/Code Coding GIF by EscuelaDevRock GitHub.svg b/images/Code Coding GIF by EscuelaDevRock GitHub.svg new file mode 100644 index 0000000..d289c4b --- /dev/null +++ b/images/Code Coding GIF by EscuelaDevRock GitHub.svg @@ -0,0 +1,76 @@ + +Generated Animation +4 frames at 7.690000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/Code Coding GIF by EscuelaDevRock Sublime.gif b/images/Code Coding GIF by EscuelaDevRock Sublime.gif new file mode 100644 index 0000000..98b6e47 Binary files /dev/null and b/images/Code Coding GIF by EscuelaDevRock Sublime.gif differ diff --git a/images/Code Coding GIF by EscuelaDevRock Sublime.svg b/images/Code Coding GIF by EscuelaDevRock Sublime.svg new file mode 100644 index 0000000..44d2d46 --- /dev/null +++ b/images/Code Coding GIF by EscuelaDevRock Sublime.svg @@ -0,0 +1,41 @@ + +Generated Animation +4 frames at 7.690000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/Code Coding GIF by EscuelaDevRock VSCode.gif b/images/Code Coding GIF by EscuelaDevRock VSCode.gif new file mode 100644 index 0000000..c3fafda Binary files /dev/null and b/images/Code Coding GIF by EscuelaDevRock VSCode.gif differ diff --git a/images/Code Coding GIF by EscuelaDevRock VSCode.svg b/images/Code Coding GIF by EscuelaDevRock VSCode.svg new file mode 100644 index 0000000..43a1626 --- /dev/null +++ b/images/Code Coding GIF by EscuelaDevRock VSCode.svg @@ -0,0 +1,35 @@ + +Generated Animation +4 frames at 7.690000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/Good_Morning_GIF_by_Hello_All.gif b/images/Good_Morning_GIF_by_Hello_All.gif new file mode 100644 index 0000000..ddfb749 Binary files /dev/null and b/images/Good_Morning_GIF_by_Hello_All.gif differ diff --git a/images/Good_Morning_GIF_by_Hello_All.svg b/images/Good_Morning_GIF_by_Hello_All.svg new file mode 100644 index 0000000..0e0600f --- /dev/null +++ b/images/Good_Morning_GIF_by_Hello_All.svg @@ -0,0 +1,3189 @@ + +Generated Animation +48 frames at 12.500000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/black and white loop GIF by Sculpture.gif b/images/black and white loop GIF by Sculpture.gif new file mode 100644 index 0000000..7ae3b97 Binary files /dev/null and b/images/black and white loop GIF by Sculpture.gif differ diff --git a/images/black and white loop GIF by Sculpture.svg b/images/black and white loop GIF by Sculpture.svg new file mode 100644 index 0000000..1673cdb --- /dev/null +++ b/images/black and white loop GIF by Sculpture.svg @@ -0,0 +1,2119 @@ + +Generated Animation +22 frames at 24.000000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/framesvg.gif b/images/framesvg.gif new file mode 100644 index 0000000..ce65509 Binary files /dev/null and b/images/framesvg.gif differ diff --git a/images/framesvg.svg b/images/framesvg.svg new file mode 100644 index 0000000..e582884 --- /dev/null +++ b/images/framesvg.svg @@ -0,0 +1,966 @@ + +Generated Animation +60 frames at 24.000000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icon_loading_GIF.gif b/images/icon_loading_GIF.gif new file mode 100644 index 0000000..0fab0dd Binary files /dev/null and b/images/icon_loading_GIF.gif differ diff --git a/images/icon_loading_GIF.svg b/images/icon_loading_GIF.svg new file mode 100644 index 0000000..5df0913 --- /dev/null +++ b/images/icon_loading_GIF.svg @@ -0,0 +1,5042 @@ + +Generated Animation +25 frames at 16.666000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/kyubey.gif b/images/kyubey.gif new file mode 100644 index 0000000..690206e Binary files /dev/null and b/images/kyubey.gif differ diff --git a/images/kyubey.svg b/images/kyubey.svg new file mode 100644 index 0000000..e2fd99c --- /dev/null +++ b/images/kyubey.svg @@ -0,0 +1,189 @@ + +Generated Animation +8 frames at 12.000000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/voila hands GIF by brontron.gif b/images/voila hands GIF by brontron.gif new file mode 100644 index 0000000..17d3532 Binary files /dev/null and b/images/voila hands GIF by brontron.gif differ diff --git a/images/voila hands GIF by brontron.svg b/images/voila hands GIF by brontron.svg new file mode 100644 index 0000000..dcb58cf --- /dev/null +++ b/images/voila hands GIF by brontron.svg @@ -0,0 +1,2268 @@ + +Generated Animation +31 frames at 12.500000 FPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2badde1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,70 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +version = "0.1.0" +name = "framesvg" +description = "Convert animated GIFs to animated SVGs." +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +authors = [ + {name = "Romelium", email = "author@romelium.cc" } +] +maintainers = [ + {name = "Romelium", email = "maintainer@romelium.cc"}, +] +keywords = ["gif", "svg", "animation", "vector", "vtracer", "image-processing"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", + "Topic :: Utilities", +] + +dependencies = [ + "pillow>=10.0.0", + "vtracer>=0.6.0", +] + +[project.urls] +Homepage = "https://github.com/romelium/framesvg" +Repository = "https://github.com/romelium/framesvg.git" +Issues = "https://github.com/romelium/framesvg/issues" + +[project.scripts] +framesvg = "framesvg:main" # Create python CLI program in Python scripts + +[tool.hatch.build.targets.wheel] +packages = ["src/framesvg"] +only-include = ["src/framesvg"] + +[tool.hatch.build.targets.sdist] +exclude = [ + "/.github", + "/docs", + "/tests", + "/images", + "/.gitignore", +] + +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] diff --git a/src/framesvg/__init__.py b/src/framesvg/__init__.py new file mode 100644 index 0000000..22c26fc --- /dev/null +++ b/src/framesvg/__init__.py @@ -0,0 +1,414 @@ +from __future__ import annotations + +import argparse +import io +import logging +import os +import re +import sys +from typing import Literal, Protocol, TypedDict + +from PIL import Image + + +class VTracerOptions(TypedDict, total=False): + colormode: Literal["color", "binary"] | None + hierarchical: Literal["stacked", "cutout"] | None + mode: Literal["spline", "polygon", "none"] | None + filter_speckle: int | None + color_precision: int | None + layer_difference: int | None + corner_threshold: int | None + length_threshold: float | None + max_iterations: int | None + splice_threshold: int | None + path_precision: int | None + + +DEFAULT_FPS = 10.0 + +DEFAULT_VTRACER_OPTIONS: VTracerOptions = { + "colormode": "color", + "hierarchical": "stacked", + "mode": "polygon", + "filter_speckle": 4, + "color_precision": 8, + "layer_difference": 16, + "corner_threshold": 60, + "length_threshold": 4.0, + "max_iterations": 10, + "splice_threshold": 45, + "path_precision": 8, +} + + +class FramesvgError(Exception): + """Base class for exceptions.""" + + +class NotAnimatedGifError(FramesvgError): + """Input GIF is not animated.""" + + def __init__(self, gif_path: str): + super().__init__(f"{gif_path} is not an animated GIF.") + + +class NoValidFramesError(FramesvgError): + """No valid SVG frames generated.""" + + def __init__(self): + super().__init__("No valid SVG frames were generated.") + + +class DimensionError(FramesvgError): + """SVG dimensions could not be determined.""" + + def __init__(self): + super().__init__("Could not determine SVG dimensions.") + + +class ExtractionError(FramesvgError): + """SVG content could not be extracted.""" + + def __init__(self): + super().__init__("Could not extract SVG content.") + + +class FrameOutOfRangeError(FramesvgError): + """Frame out of Range.""" + + def __init__(self, frame_number, max_frames): + super().__init__(f"Frame number {frame_number} is out of range. Must be between 0 and {max_frames -1}") + + +class ImageWrapper(Protocol): + is_animated: bool + n_frames: int + format: str | None + + def seek(self, frame: int) -> None: ... + def save(self, fp, img_format) -> None: ... + def close(self) -> None: ... + + +class ImageLoader(Protocol): + def open(self, filepath: str) -> ImageWrapper: ... + + +class VTracer(Protocol): + def convert_raw_image_to_svg(self, image_bytes: bytes, img_format: str, options: VTracerOptions) -> str: ... + + +class PILImageLoader: + def open(self, filepath: str) -> ImageWrapper: + return Image.open(filepath) + + +class DefaultVTracer: + def convert_raw_image_to_svg(self, image_bytes: bytes, img_format: str, options: VTracerOptions) -> str: + import vtracer + + return vtracer.convert_raw_image_to_svg(image_bytes, img_format=img_format, **options) + + +_DEFAULT_IMAGE_LOADER = PILImageLoader() +_DEFAULT_VTRACER = DefaultVTracer() + + +def load_image_wrapper(filepath: str, image_loader: ImageLoader) -> ImageWrapper: + """Loads an image and returns an ImageWrapper.""" + try: + return image_loader.open(filepath) + except FileNotFoundError: + logging.exception("File not found: %s", filepath) + raise + except Exception: + logging.exception("Error loading image: %s", filepath) + raise + + +def is_animated_gif(img: ImageWrapper, filepath: str) -> None: + """Checks if the image is an animated GIF.""" + if not img.is_animated: + raise NotAnimatedGifError(filepath) + + +def extract_svg_dimensions_from_content(svg_content: str) -> dict[str, int] | None: + """Extracts width and height from SVG.""" + dims: dict[str, int] = {"width": 0, "height": 0} + view_box_pattern = re.compile(r'viewBox=["\'](\d+)\s+(\d+)\s+(\d+)\s+(\d+)["\']') + width_pattern = re.compile(r"]*width=[\"'](\d+)") + height_pattern = re.compile(r"]*height=[\"'](\d+)") + + match = view_box_pattern.search(svg_content) + if match: + dims["width"], dims["height"] = int(match.group(3)), int(match.group(4)) + else: + match_width = width_pattern.search(svg_content) + if match_width: + dims["width"] = int(match_width.group(1)) + match_height = height_pattern.search(svg_content) + if match_height: + dims["height"] = int(match_height.group(1)) + + if dims["width"] <= 0 or dims["height"] <= 0: + return None + return dims + + +def extract_inner_svg_content_from_full_svg(full_svg_content: str) -> str: + """Extracts content within tags.""" + start_pos = full_svg_content.find("", start_pos) + 1 + end_pos = full_svg_content.rfind("") + if start_pos == -1 or end_pos == -1: + return "" + + return full_svg_content[start_pos:end_pos] + + +def process_gif_frame( + img: ImageWrapper, + frame_number: int, + vtracer_instance: VTracer, + vtracer_options: VTracerOptions, +) -> tuple[str, dict[str, int] | None]: + """Processes single GIF frame, converting to SVG.""" + if not 0 <= frame_number < img.n_frames: + raise FrameOutOfRangeError(frame_number, img.n_frames) + + img.seek(frame_number) + with io.BytesIO() as img_byte_arr: + img_byte_arr.name = "temp.gif" + img.save(img_byte_arr, img_format="GIF") + img_bytes = img_byte_arr.getvalue() + + svg_content = vtracer_instance.convert_raw_image_to_svg(img_bytes, img_format="GIF", options=vtracer_options) + dims = extract_svg_dimensions_from_content(svg_content) + inner_svg = extract_inner_svg_content_from_full_svg(svg_content) if dims else "" + + return inner_svg, dims + + +def process_gif_frames( + img: ImageWrapper, + vtracer_instance: VTracer, + vtracer_options: VTracerOptions, +) -> tuple[list[str], dict[str, int]]: + """Processes all GIF frames.""" + frames: list[str] = [] + max_dims = {"width": 0, "height": 0} + + for i in range(img.n_frames): + inner_svg_content, dims = process_gif_frame(img, i, vtracer_instance, vtracer_options) + + if dims: + max_dims["width"] = max(max_dims["width"], dims["width"]) + max_dims["height"] = max(max_dims["height"], dims["height"]) + if inner_svg_content: + frames.append(inner_svg_content) + + if not frames: + raise NoValidFramesError + + return frames, max_dims + + +def create_animated_svg_string(frames: list[str], max_dims: dict[str, int], fps: float) -> str: + """Generates animated SVG string.""" + if not frames: + msg = "No frames to generate SVG." + raise ValueError(msg) + frame_duration = 1.0 / fps + total_duration = frame_duration * len(frames) + + svg_str = ( + '\n' + f'' + f"Generated Animation\n" + f"{len(frames)} frames at {fps:.6f} FPS\n" + '\n' + ) + + for i, frame_content in enumerate(frames): + start_fraction = i / len(frames) + end_fraction = (i + 1) / len(frames) + svg_str += ( + f'\n' + f"{frame_content}\n" + f'\n' + "\n" + ) + + svg_str += "\n\n" + return svg_str + + +def save_svg_to_file(svg_string: str, output_path: str) -> None: + """Writes SVG string to file.""" + if os.path.isdir(output_path): + msg = "'%s' is a directory, not a file." + logging.exception(msg, output_path) + raise IsADirectoryError(msg, output_path) + try: + with open(output_path, "w", encoding="utf-8") as f: + f.write(svg_string) + except Exception: + logging.exception("Error writing SVG to file: %s", output_path) + raise + + +def gif_to_animated_svg( + gif_path: str, + vtracer_options: VTracerOptions | None = None, + fps: float = DEFAULT_FPS, + image_loader: ImageLoader | None = None, + vtracer_instance: VTracer | None = None, +) -> str: + """Main function to convert GIF to animated SVG.""" + image_loader = image_loader or _DEFAULT_IMAGE_LOADER + vtracer_instance = vtracer_instance or _DEFAULT_VTRACER + + options = DEFAULT_VTRACER_OPTIONS.copy() + if vtracer_options: + options.update(vtracer_options) + + img = load_image_wrapper(gif_path, image_loader) + try: + is_animated_gif(img, gif_path) + frames, max_dims = process_gif_frames(img, vtracer_instance, options) + return create_animated_svg_string(frames, max_dims, fps) + finally: + img.close() + + +def gif_to_animated_svg_write( + gif_path: str, + output_svg_path: str, + vtracer_options: VTracerOptions | None = None, + fps: float = DEFAULT_FPS, + image_loader: ImageLoader | None = None, + vtracer_instance: VTracer | None = None, +) -> None: + """Converts and writes to file.""" + svg = gif_to_animated_svg(gif_path, vtracer_options, fps, image_loader, vtracer_instance) + save_svg_to_file(svg, output_svg_path) + + +def validate_positive_int(value: str) -> int: + """Validates positive integer input.""" + try: + int_value = int(value) + if int_value <= 0: + msg = f"{value} is not a positive integer." + raise argparse.ArgumentTypeError(msg) + except ValueError as e: + msg = f"{value} is not a valid integer." + raise argparse.ArgumentTypeError(msg) from e + else: + return int_value + + +def validate_positive_float(value: str) -> float: + """Validates positive float input.""" + try: + float_value = float(value) + if float_value <= 0: + msg = f"{value} is not a positive float." + raise argparse.ArgumentTypeError(msg) + except ValueError as e: + msg = f"{value} is not a valid float." + raise argparse.ArgumentTypeError(msg) from e + else: + return float_value + + +def parse_cli_arguments(args: list[str]) -> argparse.Namespace: + """Parses command-line arguments.""" + parser = argparse.ArgumentParser(description="Convert an animated GIF to an animated SVG.") + parser.add_argument("gif_path", help="Path to the input GIF file.") + parser.add_argument( + "output_svg_path", + nargs="?", + help="Output. Defaults to input filename with .svg.", + ) + parser.add_argument( + "-f", + "--fps", + type=validate_positive_float, + default=DEFAULT_FPS, + help=f"Frames per second (default: {DEFAULT_FPS}).", + ) + parser.add_argument( + "-l", + "--log-level", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NONE"], + default="INFO", + help="Set the logging level (default: INFO).", + ) + + # VTracer options + parser.add_argument("-c", "--colormode", choices=["color", "binary"], help="Color mode.") + parser.add_argument("-i", "--hierarchical", choices=["stacked", "cutout"], help="Hierarchical mode.") + parser.add_argument("-m", "--mode", choices=["spline", "polygon", "none"], help="Mode.") + parser.add_argument("-s", "--filter-speckle", type=validate_positive_int, help="Filter speckle.") + parser.add_argument("-p", "--color-precision", type=validate_positive_int, help="Color precision.") + parser.add_argument("-d", "--layer-difference", type=validate_positive_int, help="Layer difference.") + parser.add_argument("--corner-threshold", type=validate_positive_int, help="Corner threshold.") + parser.add_argument("--length-threshold", type=validate_positive_float, help="Length threshold.") + parser.add_argument("--max-iterations", type=validate_positive_int, help="Max iterations.") + parser.add_argument("--splice-threshold", type=validate_positive_int, help="Splice threshold.") + parser.add_argument("--path-precision", type=validate_positive_int, help="Path precision.") + + return parser.parse_args(args) + + +def main() -> None: + """Main entry point.""" + try: + args = parse_cli_arguments(sys.argv[1:]) + + if args.log_level != "NONE": + logging.basicConfig(level=args.log_level, format="%(levelname)s: %(message)s") + + output_path = args.output_svg_path + if output_path is None: + base, _ = os.path.splitext(args.gif_path) + output_path = base + ".svg" + + vtracer_options = { + k: v + for k, v in vars(args).items() + if k + in [ + "colormode", + "hierarchical", + "mode", + "filter_speckle", + "color_precision", + "layer_difference", + "corner_threshold", + "length_threshold", + "max_iterations", + "splice_threshold", + "path_precision", + ] + and v is not None + } + + gif_to_animated_svg_write(args.gif_path, output_path, vtracer_options, args.fps) + logging.info("Converted %s to %s", args.gif_path, output_path) + except SystemExit as e: + sys.exit(e.code) + except FramesvgError: + logging.exception("An error occurred during processing.") + sys.exit(1) + except Exception: + logging.exception("An unexpected error occurred.") + sys.exit(1) diff --git a/src/framesvg/py.typed b/src/framesvg/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..33bf89a --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,772 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING +from unittest.mock import Mock + +import pytest +from PIL import Image, ImageCms, ImageDraw + +from framesvg import ( + DEFAULT_FPS, + DEFAULT_VTRACER_OPTIONS, + FrameOutOfRangeError, + NotAnimatedGifError, + NoValidFramesError, + VTracerOptions, + gif_to_animated_svg_write, + gif_to_animated_svg, + create_animated_svg_string, + extract_inner_svg_content_from_full_svg, + extract_svg_dimensions_from_content, + is_animated_gif, + load_image_wrapper, + parse_cli_arguments, + process_gif_frame, + process_gif_frames, + save_svg_to_file, +) + +if TYPE_CHECKING: + from pathlib import Path + + +def create_mock_image( + *, + is_animated: bool = True, + n_frames: int = 2, + width: int = 100, + height: int = 100, + img_format: str = "GIF", + close_raises: bool = False, +): + """Creates a mock Image object.""" + mock_img = Mock() + mock_img.is_animated = is_animated + mock_img.n_frames = n_frames + mock_img.width = width + mock_img.height = height + mock_img.format = img_format + mock_img.info = {"duration": 100} + + if close_raises: + mock_img.close.side_effect = Exception("Mock close error") + else: + mock_img.close = Mock() # Ensure close is callable + return mock_img + + +def create_mock_vtracer( + return_svg: str | list[str] = '', + raise_error: bool = False, # noqa: FBT001 FBT002 +): + """Creates a mock VTracer.""" + mock_vtracer = Mock() + if raise_error: + mock_vtracer.convert_raw_image_to_svg.side_effect = Exception("VTracer error") + elif isinstance(return_svg, list): + mock_vtracer.convert_raw_image_to_svg.side_effect = return_svg + else: + mock_vtracer.convert_raw_image_to_svg.return_value = return_svg + return mock_vtracer + + +def create_temp_gif( + tmp_path: Path, + *, + is_animated: bool = True, + num_frames: int = 2, + widths: tuple[int, ...] = (100,), + heights: tuple[int, ...] = (100,), + durations: tuple[int, ...] = (100,), + corrupt: bool = False, + use_palette: bool = False, + add_transparency: bool = False, + color_profile: bool = False, +) -> str: + """Creates a temp GIF.""" + gif_path = tmp_path / "test.gif" + images = [] + + for i in range(max(1, num_frames if is_animated else 1)): + width = widths[i % len(widths)] + height = heights[i % len(heights)] + if use_palette: + img = Image.new("P", (width, height), color=0) + img.putpalette( + [ + 0, + 0, + 0, # Black + 255, + 255, + 255, # White + 255, + 0, + 0, # Red + 0, + 255, + 0, # Green + 0, + 0, + 255, # Blue + ] + * 51 + ) # Repeat the palette to fill 256 entries + + else: + img = Image.new( + "RGB", + (width, height), + color=(i * 50 % 256, i * 100 % 256, i * 150 % 256), + ) + + if add_transparency and i % 2 == 0: # Make every other frame have some transparency + alpha = Image.new("L", (width, height), color=128) # semi-transparent + if use_palette: + img.putalpha(alpha) # P mode needs special handling for alpha + else: + img = img.convert("RGBA") + img.putalpha(alpha) + + if color_profile: + # Create a simple sRGB color profile + profile = ImageCms.createProfile("sRGB") + img.info["icc_profile"] = ImageCms.ImageCmsProfile(profile).tobytes() + + draw = ImageDraw.Draw(img) + draw.text((10, 10), text=f"frame {i}", fill=(0, 0, 0)) + images.append(img) + + if is_animated and num_frames > 1: + images[0].save( + gif_path, + "GIF", + save_all=True, + append_images=images[1:], + duration=[durations[i % len(durations)] for i in range(num_frames)], + loop=0, + transparency=0 if add_transparency else None, # Specify transparency color index + ) + else: + images[0].save(gif_path, "GIF") + + if corrupt: + with open(gif_path, "wb") as f: + f.write(b"CORRUPT") + + return str(gif_path) + + +@pytest.fixture +def sample_svg_content(): + return ( + '\n' + '\n' + '\n' + "" + ) + + +@pytest.fixture +def sample_frames(): + return [ + '', + '', + '', + ] + + +@pytest.fixture(params=[True, False]) +def animated_gif_state(request): + return request.param + + +@pytest.fixture(params=[1, 2, 5, 10]) +def frame_number_count(request): + return request.param + + +@pytest.fixture(params=[(100, 100), (200, 150), (150, 200), (300, 300)]) +def image_dimensions(request): + return request.param + + +@pytest.fixture(params=[(50,), (100,), (200,)]) +def duration_values(request): + return request.param + + +@pytest.fixture +def mock_image_instance(animated_gif_state, frame_number_count, image_dimensions): + width, height = image_dimensions + return create_mock_image(is_animated=animated_gif_state, n_frames=frame_number_count, width=width, height=height) + + +@pytest.fixture +def mock_vtracer_instance_for_tests(): + return create_mock_vtracer() + + +@pytest.fixture +def mock_image_loader_instance(): + return Mock() + + +def test_load_image_wrapper_success(tmp_path): + gif_path = create_temp_gif(tmp_path) + + # Create mock objects locally + mock_loader = Mock() + mock_image = create_mock_image(is_animated=True, img_format="GIF") + mock_loader.open.return_value = mock_image + + img_wrapper = load_image_wrapper(gif_path, mock_loader) + + assert img_wrapper.is_animated + assert img_wrapper.format == "GIF" # check the format too. + mock_loader.open.assert_called_once_with(gif_path) + + +def test_load_image_wrapper_file_not_found(): + mock_loader = Mock() + mock_loader.open.side_effect = FileNotFoundError + with pytest.raises(FileNotFoundError): + load_image_wrapper("nonexistent.gif", mock_loader) + + +def test_load_image_wrapper_general_exception(tmp_path): + mock_loader = Mock() + gif_path = create_temp_gif(tmp_path) + mock_loader.open.side_effect = Exception("Some error") + with pytest.raises(Exception, match="Some error"): # Keep generic, as PIL can raise many + load_image_wrapper(gif_path, mock_loader) + + +def test_load_image_wrapper_corrupt_file(tmp_path): + mock_loader = Mock() + gif_path = create_temp_gif(tmp_path, corrupt=True) + mock_loader.open.side_effect = Image.UnidentifiedImageError + with pytest.raises(Image.UnidentifiedImageError): + load_image_wrapper(gif_path, mock_loader) + + +def test_load_image_wrapper_unsupported_format(tmp_path): + mock_loader = Mock() + # Create a text file with .gif extension + path = tmp_path / "fake.gif" + path.write_text("Not a GIF") + mock_loader.open.side_effect = Image.UnidentifiedImageError + with pytest.raises(Image.UnidentifiedImageError): + load_image_wrapper(str(path), mock_loader) + + +@pytest.mark.parametrize("system_error", [PermissionError, OSError]) +def test_load_image_wrapper_system_errors(system_error): + mock_loader = Mock() + mock_loader.open.side_effect = system_error + with pytest.raises(system_error): + load_image_wrapper("test.gif", mock_loader) + + +@pytest.mark.parametrize( + ("svg_str_input", "expected_output"), + [ + ('', {"width": 100, "height": 200}), + ('', {"width": 300, "height": 400}), + ( + '', + {"width": 200, "height": 300}, + ), + ( + '', + {"width": 200, "height": 300}, + ), + ("", None), + ('', None), + ('', None), + ('', None), + ('', None), + ], +) +def test_extract_svg_dimensions_from_content_variations(svg_str_input, expected_output): + if expected_output is None: + assert extract_svg_dimensions_from_content(svg_str_input) is None + else: + assert extract_svg_dimensions_from_content(svg_str_input) == expected_output + + +@pytest.mark.parametrize( + ("full_svg_input", "expected_inner_content"), + [ + ("content", "content"), + ("", ""), + ("nestedcontent", "nestedcontent"), + ("not svg content", ""), + ("unclosed", ""), + ("", ""), + ], +) +def test_extract_inner_svg_content_from_full_svg_variations(full_svg_input, expected_inner_content): + assert extract_inner_svg_content_from_full_svg(full_svg_input) == expected_inner_content + + +def test_process_gif_frame_success(mock_image_instance, mock_vtracer_instance_for_tests): + inner_svg, dims = process_gif_frame( + mock_image_instance, 0, mock_vtracer_instance_for_tests, DEFAULT_VTRACER_OPTIONS + ) + assert isinstance(inner_svg, str) + assert isinstance(dims, dict) + assert "width" in dims + assert "height" in dims + + +def test_process_gif_frame_vtracer_error(mock_image_instance, mock_vtracer_instance_for_tests): + mock_vtracer_instance_for_tests.convert_raw_image_to_svg.side_effect = Exception("Simulated VTracer error") + with pytest.raises(Exception, match="Simulated VTracer error"): + process_gif_frame(mock_image_instance, 0, mock_vtracer_instance_for_tests, DEFAULT_VTRACER_OPTIONS) + + +@pytest.mark.parametrize("selected_frame", [-1, 0, 1, 2]) +def test_process_gif_frame_number_variations(mock_image_instance, mock_vtracer_instance_for_tests, selected_frame): + if 0 <= selected_frame < mock_image_instance.n_frames: + inner_svg, dims = process_gif_frame( + mock_image_instance, + selected_frame, + mock_vtracer_instance_for_tests, + DEFAULT_VTRACER_OPTIONS, + ) + assert isinstance(inner_svg, str) + assert isinstance(dims, dict) + else: + with pytest.raises(FrameOutOfRangeError): + process_gif_frame( + mock_image_instance, + selected_frame, + mock_vtracer_instance_for_tests, + DEFAULT_VTRACER_OPTIONS, + ) + + +def test_check_if_animated_gif_positive(mock_image_instance): + mock_image_instance.is_animated = True # Ensure it's set to True + # Test should pass without raising an exception + is_animated_gif(mock_image_instance, "test.gif") + + +def test_check_if_animated_gif_negative(mock_image_instance): + mock_image_instance.is_animated = False # Set to False + with pytest.raises(NotAnimatedGifError, match="test.gif is not an animated GIF."): + is_animated_gif(mock_image_instance, "test.gif") + + +def test_create_animated_svg_string_comprehensive_tests(sample_frames): + fps_list = [1.0, 10.0, 24.0, 60.0] + dimension_list = [ + {"width": 100, "height": 100}, + {"width": 200, "height": 150}, + {"width": 1920, "height": 1080}, + ] + + for test_fps in fps_list: + for test_dims in dimension_list: + svg_string_result = create_animated_svg_string(sample_frames, test_dims, test_fps) + + assert '' in svg_string_result + + assert f'width="{test_dims["width"]}"' in svg_string_result + assert f'height="{test_dims["height"]}"' in svg_string_result + assert f'viewBox="0 0 {test_dims["width"]} {test_dims["height"]}"' in svg_string_result + + expected_duration = len(sample_frames) / test_fps + assert f'dur="{expected_duration:.6f}s"' in svg_string_result + + for frame_content in sample_frames: + assert frame_content in svg_string_result + + +def test_create_animated_svg_string_edge_cases_tests(): + test_dims = {"width": 100, "height": 100} + + with pytest.raises(ValueError, match="No frames to generate SVG."): + create_animated_svg_string([], test_dims, DEFAULT_FPS) + + single_frame_svg = create_animated_svg_string([""], test_dims, DEFAULT_FPS) + assert "" in single_frame_svg + assert 'repeatCount="indefinite"' in single_frame_svg + + high_fps_svg = create_animated_svg_string([""], test_dims, 1000.0) + assert 'dur="0.001000s"' in high_fps_svg + + low_fps_svg = create_animated_svg_string([""], test_dims, 0.1) + assert 'dur="10.000000s"' in low_fps_svg + + +def test_process_gif_frames_integration_tests(mock_image_instance, mock_vtracer_instance_for_tests): + frames, max_dims = process_gif_frames(mock_image_instance, mock_vtracer_instance_for_tests, DEFAULT_VTRACER_OPTIONS) + assert isinstance(frames, list) + assert isinstance(max_dims, dict) + assert len(frames) > 0 + assert all(isinstance(frame, str) for frame in frames) + assert max_dims["width"] > 0 + assert max_dims["height"] > 0 + + mock_image_instance.n_frames = 0 + with pytest.raises(NoValidFramesError): + process_gif_frames(mock_image_instance, mock_vtracer_instance_for_tests, DEFAULT_VTRACER_OPTIONS) + + mock_image_instance.n_frames = 2 # Restore + + mock_vtracer = create_mock_vtracer(return_svg="") + with pytest.raises(NoValidFramesError): + process_gif_frames(mock_image_instance, mock_vtracer, DEFAULT_VTRACER_OPTIONS) + + mock_vtracer = create_mock_vtracer(return_svg=['', ""]) + frames, max_dims = process_gif_frames(mock_image_instance, mock_vtracer, DEFAULT_VTRACER_OPTIONS) + assert len(frames) == 1 + + +def test_gif_to_animated_svg_comprehensive( + tmp_path, mock_image_loader_instance, mock_vtracer_instance_for_tests +): + frame_counts = [2, 5, 10] + dimension_sets = [(100, 100), (200, 150), (150, 200)] + fps_values = [10.0, 24.0, 60.0] + vtracer_options_set: VTracerOptions = { + "colormode": "color", + "filter_speckle": 4, + "corner_threshold": 60, + } + + for num_frames in frame_counts: + for width, height in dimension_sets: + for fps in fps_values: + gif_path = create_temp_gif(tmp_path, num_frames=num_frames, widths=(width,), heights=(height,)) + + # Set up the mock_image_loader correctly. Important! + mock_image = create_mock_image(is_animated=True, n_frames=num_frames, width=width, height=height) + mock_image_loader_instance.open.return_value = mock_image + + svg_string = gif_to_animated_svg( + gif_path, + vtracer_options=vtracer_options_set, + fps=fps, + image_loader=mock_image_loader_instance, + vtracer_instance=mock_vtracer_instance_for_tests, + ) + + assert svg_string.startswith("" in svg_string + assert f"{num_frames} frames at {fps:.6f} FPS" in svg_string + assert 'repeatCount="indefinite"' in svg_string + mock_image_loader_instance.open.assert_called_with(gif_path) + + +def test_gif_to_animated_svg_error_handling(tmp_path): + gif_path = create_temp_gif(tmp_path, is_animated=False) + with pytest.raises(NotAnimatedGifError): + gif_to_animated_svg(gif_path) + + gif_path = create_temp_gif(tmp_path, corrupt=True) + with pytest.raises(Image.UnidentifiedImageError): + gif_to_animated_svg(gif_path) + + with pytest.raises(FileNotFoundError): + gif_to_animated_svg("nonexistent.gif") + + +@pytest.mark.parametrize( + ("cli_args", "expected_parsed_args"), + [ + ( + ["input.gif"], + { + "gif_path": "input.gif", + "output_svg_path": None, + "fps": DEFAULT_FPS, + "log_level": "INFO", + }, + ), + ( + ["input.gif", "output.svg"], + { + "gif_path": "input.gif", + "output_svg_path": "output.svg", + "fps": DEFAULT_FPS, + "log_level": "INFO", + }, + ), + ( + ["input.gif", "--fps", "30"], + { + "gif_path": "input.gif", + "output_svg_path": None, + "fps": 30.0, + "log_level": "INFO", + }, + ), + ( + ["input.gif", "--log-level", "DEBUG"], + { + "gif_path": "input.gif", + "output_svg_path": None, + "fps": DEFAULT_FPS, + "log_level": "DEBUG", + }, + ), + ( + ["input.gif", "--colormode", "binary", "--filter-speckle", "2"], + { + "gif_path": "input.gif", + "output_svg_path": None, + "fps": DEFAULT_FPS, + "log_level": "INFO", + "colormode": "binary", + "filter_speckle": 2, + }, + ), + ( + [ + "input.gif", + "output.svg", + "--fps", + "60", + "--log-level", + "ERROR", + "--mode", + "polygon", + ], + { + "gif_path": "input.gif", + "output_svg_path": "output.svg", + "fps": 60.0, + "log_level": "ERROR", + "mode": "polygon", + }, + ), + ( + ["input.gif", "-f", "24"], + { + "gif_path": "input.gif", + "output_svg_path": None, + "fps": 24.0, + "log_level": "INFO", + }, + ), + ( + [ + "input.gif", + "--colormode", + "color", + "--hierarchical", + "stacked", + "--mode", + "spline", + "--filter-speckle", + "4", + "--color-precision", + "6", + "--layer-difference", + "16", + "--corner-threshold", + "100", + "--length-threshold", + "4.0", + "--max-iterations", + "10", + "--splice-threshold", + "45", + "--path-precision", + "8", + ], + { + "gif_path": "input.gif", + "output_svg_path": None, + "fps": DEFAULT_FPS, + "log_level": "INFO", + "colormode": "color", + "hierarchical": "stacked", + "mode": "spline", + "filter_speckle": 4, + "color_precision": 6, + "layer_difference": 16, + "corner_threshold": 100, + "length_threshold": 4.0, + "max_iterations": 10, + "splice_threshold": 45, + "path_precision": 8, + }, + ), + ( + ["input.gif", "output file.svg"], + { + "gif_path": "input.gif", + "output_svg_path": "output file.svg", + "fps": DEFAULT_FPS, + "log_level": "INFO", + }, + ), + ( + ["input.gif", "--fps", "abc"], + { + "gif_path": "input.gif", + "fps": "abc", + "output_svg_path": None, + "log_level": "INFO", + }, + ), # Should raise error. + ], +) +def test_parse_cli_arguments_comprehensive(cli_args, expected_parsed_args): + if cli_args == []: + with pytest.raises(SystemExit): + parse_cli_arguments(cli_args) + return + + if ( + "fps" in expected_parsed_args + and isinstance(expected_parsed_args["fps"], str) + and expected_parsed_args["fps"] == "abc" + ): + with pytest.raises(SystemExit): + parse_cli_arguments(cli_args) + return + + parsed_args = parse_cli_arguments(cli_args) + for key, expected_value in expected_parsed_args.items(): + assert ( + getattr(parsed_args, key) == expected_value + ), f"Mismatch: key '{key}': expected {expected_value}, got {getattr(parsed_args, key)}" + + +@pytest.mark.parametrize( + ("cli_arguments", "expected_error_message"), + [ + ([], "the following arguments are required: gif_path"), + (["in.gif", "--fps", "-1"], "argument -f/--fps: -1 is not a positive float."), + (["in.gif", "--fps", "0"], "argument -f/--fps: 0 is not a positive float."), + ( + ["in.gif", "--fps", "abc"], + "argument -f/--fps: abc is not a valid float.", + ), + ( + ["in.gif", "--log-level", "INVALID"], + "argument -l/--log-level: invalid choice: 'INVALID'", + ), + ( + ["in.gif", "--colormode", "invalid"], + "argument -c/--colormode: invalid choice: 'invalid'", + ), + ( + ["in.gif", "--filter-speckle", "-1"], + "argument -s/--filter-speckle: -1 is not a positive integer.", + ), + ( + ["in.gif", "--filter-speckle", "0"], + "argument -s/--filter-speckle: 0 is not a positive integer.", + ), + ( + ["in.gif", "--filter-speckle", "abc"], + "argument -s/--filter-speckle: abc is not a valid integer.", + ), + ], +) +def test_parse_cli_arguments_validation_invalid(cli_arguments, expected_error_message, capsys): + with pytest.raises(SystemExit) as excinfo: + parse_cli_arguments(cli_arguments) + assert excinfo.value.code == 2 + captured_output = capsys.readouterr() + assert expected_error_message in captured_output.err + + +def test_save_svg_to_file_success(tmp_path, sample_svg_content): + output_file_path = str(tmp_path / "output.svg") + save_svg_to_file(sample_svg_content, output_file_path) + + assert os.path.exists(output_file_path) + with open(output_file_path, encoding="utf-8") as f: + written_file_content = f.read() + assert written_file_content == sample_svg_content + + +def test_save_svg_to_file_errors(tmp_path, sample_svg_content): + nonexistent_directory = str(tmp_path / "nonexistent_dir") + with pytest.raises(FileNotFoundError): + save_svg_to_file(sample_svg_content, nonexistent_directory + "/output.svg") + + read_only_directory = tmp_path / "readonly" + read_only_directory.mkdir() + output_svg_file_ro = read_only_directory / "output.svg" + output_svg_file_ro.touch() + + os.chmod(output_svg_file_ro, 0o444) + + with pytest.raises(PermissionError): + save_svg_to_file(sample_svg_content, str(output_svg_file_ro)) + + output_directory = tmp_path / "output_dir" + output_directory.mkdir() + with pytest.raises(IsADirectoryError): + save_svg_to_file(sample_svg_content, str(output_directory)) + + +def test_gif_to_animated_svg_write_integration(tmp_path): + gif_input_path = create_temp_gif(tmp_path, num_frames=3) + svg_output_path = tmp_path / "output.svg" + gif_to_animated_svg_write(gif_input_path, str(svg_output_path)) + assert svg_output_path.exists() + assert svg_output_path.stat().st_size > 0 + + output_directory = tmp_path / "output_dir" + output_directory.mkdir() + with pytest.raises(IsADirectoryError): + gif_to_animated_svg_write(gif_input_path, str(output_directory)) + + +def test_gif_to_animated_svg_closes_image(tmp_path, mock_vtracer_instance_for_tests): + """Test image gets closed even with processing or vtracer errors""" + gif_path = create_temp_gif(tmp_path) + mock_image = create_mock_image() + mock_image_loader = Mock(return_value=mock_image) # Mock Image Loader + mock_image_loader.open.return_value = mock_image # + + # Test case 1: Successful conversion. + gif_to_animated_svg( + gif_path, image_loader=mock_image_loader, vtracer_instance=mock_vtracer_instance_for_tests + ) + mock_image.close.assert_called() # Image Close should have been called. + + # reset call count. + mock_image.close.reset_mock() + + # Test case 2: vtracer failure. + mock_vtracer_fail = create_mock_vtracer(raise_error=True) + with pytest.raises(Exception, match="VTracer error"): # Keep as generic exception. + gif_to_animated_svg(gif_path, image_loader=mock_image_loader, vtracer_instance=mock_vtracer_fail) + mock_image.close.assert_called() + + # Test Case 3: GIF not animated. + mock_image.close.reset_mock() # reset. + mock_image.is_animated = False + with pytest.raises(NotAnimatedGifError): + gif_to_animated_svg(gif_path, image_loader=mock_image_loader) + mock_image.close.assert_called() + + # Test Case 4: No valid frames + mock_image.close.reset_mock() # reset. + mock_image.is_animated = True + mock_vtracer_no_valid = create_mock_vtracer(return_svg="") + with pytest.raises(NoValidFramesError): + gif_to_animated_svg(gif_path, image_loader=mock_image_loader, vtracer_instance=mock_vtracer_no_valid) + mock_image.close.assert_called() + + # Test Case 5: Image closing raises error + mock_image.close.reset_mock() # reset. + mock_image.is_animated = True + mock_image.close.side_effect = Exception("Simulated close error") # Set close to raise error. + with pytest.raises(Exception, match="Simulated close error"): + gif_to_animated_svg( + gif_path, image_loader=mock_image_loader, vtracer_instance=mock_vtracer_instance_for_tests + ) + mock_image.close.assert_called() # should still be called.