# 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. **Live URL**: https://ai-sandbox.oliver.solutions/GIF-ENCODER/ **API Endpoint**: https://ai-sandbox.oliver.solutions/GIF-ENCODER/api.php **Repo**: https://bitbucket.org/zlalani/gif-encoder/src/main/ --- ## 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 the project ```bash cd /var/www/html git clone https://zlalani@bitbucket.org/zlalani/gif-encoder.git GIF-ENCODER cd GIF-ENCODER ``` ### 2. Set up the Python virtual environment ```bash python3 -m venv venv source venv/bin/activate pip install Pillow deactivate ``` On Ubuntu, if `python3 -m venv` fails: ```bash sudo apt install python3-venv -y ``` ### 3. Create working directories and set permissions ```bash mkdir -p uploads output sudo chown -R www-data:www-data uploads output sudo chmod 775 uploads output sudo chmod +x encoder.py ``` ### 4. 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` **Ubuntu/Apache**: Edit `/etc/php/X.X/apache2/php.ini` then `sudo systemctl restart apache2` Verify `shell_exec` is NOT in the `disable_functions` list in `php.ini`. ### 5. Access the app If installed in `/var/www/html/GIF-ENCODER/`, it's available at: ``` https://your-domain.com/GIF-ENCODER/ ``` No virtual host needed — it runs from the default Apache document root. **MAMP (local dev)**: `http://localhost:8888/GIF-ENCODER/` **PHP built-in server (quick test)**: ```bash cd GIF-ENCODER php -S localhost:8000 ``` --- ## Usage — Web UI 1. Open https://ai-sandbox.oliver.solutions/GIF-ENCODER/ 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 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 https://ai-sandbox.oliver.solutions/GIF-ENCODER/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 https://ai-sandbox.oliver.solutions/GIF-ENCODER/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 https://ai-sandbox.oliver.solutions/GIF-ENCODER/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": "https://ai-sandbox.oliver.solutions/GIF-ENCODER/output/api_67be1a2f3c4d5/group_name.gif", "base64": "" } ] } ``` --- ## Figma Plugin Integration ### Architecture Figma plugins run in two separate threads: | Thread | Has access to | Limitation | |---|---|---| | **Main thread** (`code.js`) | Figma scene, `exportAsync`, `figma.*` | No `FormData`, no `Blob`, limited `fetch` | | **UI iframe** (`ui.html`) | Full browser APIs: `fetch`, `FormData`, `Blob` | No Figma scene access | The workflow: export PNGs in the main thread, send the bytes to the UI iframe via `postMessage`, then upload to the API from the iframe. ``` Figma Main Thread UI Iframe GIF Encoder API ───────────────── ───────── ─────────────── exportAsync (PNG) │ ├─ postMessage ──▶ Uint8Array → Blob FormData.append() │ ├─ POST /api.php ──▶ encoder.py │ │ ◀── JSON response ────────┘ │ (base64 GIF + URL) ◀── postMessage ────┘ figma.notify("Done!") ``` ### manifest.json ```json { "name": "GIF Encoder", "id": "your-plugin-id", "api": "1.0.0", "editorType": ["figma"], "main": "code.js", "ui": "ui.html", "documentAccess": "dynamic-page", "networkAccess": { "allowedDomains": [ "https://ai-sandbox.oliver.solutions" ], "devAllowedDomains": [ "http://localhost:8888", "http://localhost:8000" ], "reasoning": "Uploads exported PNG frames to the GIF Encoder API." } } ``` ### code.js (Main Thread) Handles frame selection, PNG export, and communication with the UI. ```javascript figma.showUI(__html__, { visible: true, width: 420, height: 500 }); figma.ui.onmessage = async (msg) => { // ── Export selected frames as PNGs ── if (msg.type === 'EXPORT_FRAMES') { const selection = figma.currentPage.selection; if (selection.length === 0) { figma.notify('Select at least one frame.'); figma.ui.postMessage({ type: 'ERROR', error: 'No frames selected' }); return; } figma.notify(`Exporting ${selection.length} frames...`); const frames = []; for (const node of selection) { const bytes = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: msg.scale || 1 }, }); frames.push({ name: node.name.replace(/[^a-zA-Z0-9._-]/g, '_') + '.png', bytes: bytes, // Uint8Array — transfers via postMessage }); } figma.ui.postMessage({ type: 'FRAMES_READY', frames }); } // ── Handle results from the API ── if (msg.type === 'GENERATION_COMPLETE') { figma.notify(`Generated ${msg.count} GIF(s)`); } if (msg.type === 'GENERATION_ERROR') { figma.notify(`Error: ${msg.error}`, { error: true }); } if (msg.type === 'CLOSE') { figma.closePlugin(); } }; ``` ### ui.html (UI Iframe) Provides the settings UI and handles the HTTP upload to the GIF Encoder API. ```html

GIF ENCODER

``` ### How It Works 1. User selects frames in Figma and clicks **Export Selection & Generate GIF**. 2. `code.js` calls `exportAsync({ format: 'PNG' })` on each selected node. 3. The raw `Uint8Array` bytes are sent to the UI iframe via `postMessage`. 4. `ui.html` converts each `Uint8Array` to a `Blob`, wraps it in `FormData`, and POSTs to the API. 5. The API returns JSON with a base64-encoded GIF and a download URL. 6. The plugin shows a preview and download link inline. ### Batch Mode: Multiple GIFs To generate multiple GIFs from one export, organise frames by naming convention. For example, if your Figma frames are named: ``` banner_frame1 banner_frame2 banner_frame3 icon_frame1 icon_frame2 ``` Group them in the config by prefix: ```javascript // In ui.html, after receiving frames: const groups = {}; msg.frames.forEach((frame) => { // Split on last underscore: "banner_frame1" → group "banner" const parts = frame.name.replace('.png', '').split('_'); parts.pop(); // remove frameN const groupName = parts.join('_') || 'default'; if (!groups[groupName]) groups[groupName] = []; groups[groupName].push(frame.name); }); // Build config with multiple groups const config = { groups: {}, quality: quality, }; Object.entries(groups).forEach(([name, files]) => { config.groups[name] = { files: files, delay: delay, loop: loop, }; }); ``` This generates one GIF per naming group in a single API call. ### Per-Frame Delays from Frame Names If different frames need different timings, encode it in the Figma frame name: ``` frame1_500ms frame2_200ms frame3_800ms ``` Then parse in `ui.html`: ```javascript const delays = []; const fileNames = []; msg.frames.forEach((frame) => { fileNames.push(frame.name); const match = frame.name.match(/_(\d+)ms/); delays.push(match ? parseInt(match[1]) : defaultDelay); }); config.groups[gifName] = { files: fileNames, delays: delays, // per-frame array loop: loop, }; ``` ### Figma Troubleshooting **CORS errors** — The API sends `Access-Control-Allow-Origin: *` by default. Make sure `https://ai-sandbox.oliver.solutions` is in `allowedDomains` in your `manifest.json`. **CSP blocked** — If requests silently fail, open the Figma developer console (Plugins > Development > Open Console) and look for CSP errors. Add the blocked domain to `allowedDomains`. **"No frames selected"** — Select actual frame/group/component nodes in Figma before clicking generate. Text and vector nodes also work with `exportAsync`. **Large exports timing out** — Lower the export scale (1x instead of 2x), export in smaller batches, or increase PHP `max_execution_time` on the server. **Uint8Array issues** — Only `Uint8Array` can cross the `postMessage` boundary in Figma plugins. Don't try to send `ArrayBuffer`, `Blob`, or other typed arrays from the main thread. --- ## General 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. **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.