# GIF Encoder Batch PNG-to-animated-GIF converter with a PHP web frontend and Python (Pillow) backend. Upload sets of PNGs, group them into GIF animations with per-frame timing control, and download the results. Includes a JSON API for integration with Figma plugins and external scripts. --- ## Features - Drag-and-drop PNG uploads - Group frames into multiple GIF sets (batch ~10 GIFs at once) - Per-frame delay control (ms) — different timing for each frame - Per-group loop toggle (infinite loop or play once) - Adjustable quality (color count 8-256) - Same PNG can be used across multiple groups - JSON API endpoint for Figma plugin / script integration - API returns base64-encoded GIFs + download URLs - CORS enabled for cross-origin requests --- ## Project Structure ``` GIF-ENCODER/ ├── index.php Frontend UI ├── process.php PHP backend (form handling + API logic) ├── api.php API endpoint (proxies to process.php) ├── encoder.py Python GIF encoder (Pillow) ├── venv/ Python virtual environment ├── assets/ │ └── style.css Styles (Montserrat, black/#FFC407) ├── uploads/ Temporary uploaded PNGs (auto-created) └── output/ Generated GIFs (auto-created) ``` --- ## Requirements - PHP 7.4+ with `shell_exec` enabled - Python 3.8+ - Apache (MAMP, XAMPP, or standalone) or PHP built-in server --- ## Installation ### 1. Clone / copy the project ```bash cd /path/to/your/webroot git clone GIF-ENCODER cd GIF-ENCODER ``` Or just copy the folder into your web server's document root. ### 2. Set up the Python virtual environment ```bash python3 -m venv venv source venv/bin/activate pip install Pillow deactivate ``` ### 3. Create working directories ```bash mkdir -p uploads output ``` These are auto-created by the app, but pre-creating them ensures permissions are right. ### 4. Set permissions ```bash chmod 775 uploads output ``` On a production server, make sure the Apache/PHP user (e.g. `www-data`) can write to these directories: ```bash chown -R www-data:www-data uploads output ``` ### 5. PHP configuration If uploading many or large PNGs, update your `php.ini`: ```ini upload_max_filesize = 64M post_max_size = 64M max_file_uploads = 100 ``` **MAMP**: Edit `/Applications/MAMP/bin/php/phpX.X.X/conf/php.ini` **Apache**: Edit `/etc/php/X.X/apache2/php.ini` then restart Apache. Verify `shell_exec` is NOT in the `disable_functions` list in `php.ini`. ### 6. Configure your web server **MAMP (local development)**: - Set the document root to the `GIF-ENCODER` folder, or place it inside your existing MAMP htdocs. - Access at `http://localhost:8888/GIF-ENCODER/` **Apache (production)**: Add a virtual host or alias pointing to the project directory: ```apache ServerName gifencoder.yourdomain.com DocumentRoot /var/www/GIF-ENCODER AllowOverride All Require all granted ``` **PHP built-in server (quick test)**: ```bash cd GIF-ENCODER php -S localhost:8000 ``` Then open `http://localhost:8000`. --- ## Usage — Web UI 1. Open the app in your browser. 2. Drag PNG files onto the upload zone (or click to browse). 3. Uploaded PNGs appear in the file pool. Drag them into GIF group cards. 4. Click **+ Add Group** to create additional GIF sets (up to 10+). 5. Adjust settings per group: - **Default delay (ms)**: Base timing for frames in that group. - **Per-frame delay**: Each frame has its own delay input below the thumbnail. - **Loop checkbox**: Toggle infinite loop on/off. 6. Set the **Quality** slider (global — controls GIF color palette size). 7. Click **Generate GIFs**. 8. Download individual GIFs from the results, or right-click to save. **Tips**: - The same PNG can be dragged into multiple groups. - Double-click a frame inside a group to remove it. --- ## Usage — Python CLI The encoder can be used standalone from the command line: ```bash # Activate the virtual environment source venv/bin/activate # Basic: uniform 500ms delay, looping python encoder.py --input frame1.png frame2.png frame3.png --output out.gif # Custom uniform delay python encoder.py --input frame1.png frame2.png frame3.png --output out.gif --delay 200 # Per-frame delays python encoder.py --input frame1.png frame2.png frame3.png --output out.gif --delays 500,200,800 # Play once (no loop) python encoder.py --input frame1.png frame2.png frame3.png --output out.gif --delay 300 --no-loop # Lower quality (fewer colors, smaller file) python encoder.py --input frame1.png frame2.png frame3.png --output out.gif --quality 64 ``` ### CLI Arguments | Argument | Default | Description | |-------------|---------|------------------------------------------------------| | `--input` | required| Space-separated list of PNG file paths | | `--output` | required| Output GIF file path | | `--delay` | 500 | Uniform delay for all frames (ms) | | `--delays` | — | Comma-separated per-frame delays (e.g. `500,200,800`)| | `--quality` | 256 | Color count for palette quantization (2-256) | | `--no-loop` | false | Play once instead of looping infinitely | --- ## Usage — API ### Endpoint ``` POST /api.php ``` CORS is enabled. Responses are JSON. ### Option A: Multipart upload (recommended for Figma) Send PNG files as multipart uploads with a `json` field containing the configuration. ```bash curl -X POST http://localhost:8000/api.php \ -F "json={\"groups\":{\"banner\":{\"files\":[\"frame1.png\",\"frame2.png\",\"frame3.png\"],\"delays\":[500,200,800],\"loop\":true}},\"quality\":256}" \ -F "files[]=@frame1.png" \ -F "files[]=@frame2.png" \ -F "files[]=@frame3.png" ``` ### Option B: JSON body with base64 files Send everything as a single JSON payload with base64-encoded file data. ```bash curl -X POST http://localhost:8000/api.php \ -H "Content-Type: application/json" \ -d '{ "files": { "frame1.png": "", "frame2.png": "" }, "groups": { "my_animation": { "files": ["frame1.png", "frame2.png"], "delays": [500, 300], "loop": true } }, "quality": 256 }' ``` ### Request Schema ```jsonc { // Base64-encoded PNG files (Option B only) "files": { "filename.png": "" }, // GIF groups to generate "groups": { "group_name": { "files": ["file1.png", "file2.png"], // filenames matching uploads "delays": [500, 200], // per-frame delays in ms (optional) "delay": 500, // uniform delay fallback (optional, default 500) "loop": true // true = infinite, false = play once (optional, default true) } }, // Global quality setting (optional, default 256) "quality": 256 } ``` ### Response Schema ```jsonc { "success": true, "batchId": "api_67be1a2f3c4d5", "results": [ { "group": "group_name", "success": true, "frames": 3, "url": "output/api_67be1a2f3c4d5/group_name.gif", "download_url": "http://yourserver.com/GIF-ENCODER/output/api_67be1a2f3c4d5/group_name.gif", "base64": "" } ] } ``` ### Figma Plugin Example (JavaScript) ```javascript async function generateGif(frames, name, delays) { const formData = new FormData(); // Add PNG files frames.forEach((blob, i) => { formData.append('files[]', blob, `${name}_frame${i}.png`); }); // Add config const config = { groups: { [name]: { files: frames.map((_, i) => `${name}_frame${i}.png`), delays: delays, loop: true } }, quality: 256 }; formData.append('json', JSON.stringify(config)); const response = await fetch('https://yourserver.com/GIF-ENCODER/api.php', { method: 'POST', body: formData }); const data = await response.json(); if (data.success && data.results[0].success) { // Use base64 data directly const gifBase64 = data.results[0].base64; // Or download via URL const gifUrl = data.results[0].download_url; } } ``` --- ## Troubleshooting **GIFs not generating / empty results** - Check that `shell_exec` is enabled in `php.ini`. - Verify the venv exists: `ls venv/bin/python3` - Test the encoder directly: `venv/bin/python3 encoder.py --input test.png --output test.gif` **Upload fails or files missing** - Check `upload_max_filesize` and `post_max_size` in `php.ini`. - Verify `uploads/` and `output/` are writable by the PHP process. **CORS errors from Figma plugin** - The API sets `Access-Control-Allow-Origin: *` by default. If you need to restrict this, edit the headers in `process.php`. **RGBA PNGs look wrong** - The encoder composites RGBA onto a white background since GIF doesn't support full alpha transparency. This is expected. **Large file sizes** - Lower the quality slider (fewer colors = smaller GIFs). - Use shorter delays — fewer unique frames = better compression.