675 lines
20 KiB
Markdown
675 lines
20 KiB
Markdown
# 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": "<base64 encoded PNG data>",
|
|
"frame2.png": "<base64 encoded PNG data>"
|
|
},
|
|
"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": "<base64 string>"
|
|
},
|
|
|
|
// 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": "<base64 encoded GIF>"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: 'Inter', sans-serif; padding: 16px; background: #2c2c2c; color: #fff; }
|
|
h3 { font-size: 14px; margin-bottom: 12px; color: #FFC407; }
|
|
label { display: block; font-size: 11px; color: #999; margin-bottom: 4px; text-transform: uppercase; }
|
|
input, select { width: 100%; padding: 8px; margin-bottom: 12px; background: #1a1a1a; border: 1px solid #444;
|
|
color: #fff; border-radius: 6px; font-size: 13px; }
|
|
input:focus { outline: none; border-color: #FFC407; }
|
|
button { width: 100%; padding: 10px; background: #FFC407; color: #000; border: none; border-radius: 6px;
|
|
font-weight: 600; font-size: 13px; cursor: pointer; text-transform: uppercase; }
|
|
button:hover { background: #e6b006; }
|
|
button:disabled { background: #555; color: #999; cursor: not-allowed; }
|
|
#status { margin-top: 12px; font-size: 12px; color: #888; }
|
|
.result { margin-top: 8px; padding: 8px; background: #1a1a1a; border-radius: 6px; }
|
|
.result img { max-width: 100%; border-radius: 4px; }
|
|
.result a { color: #FFC407; font-size: 12px; }
|
|
.row { display: flex; gap: 12px; }
|
|
.row > div { flex: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h3>GIF ENCODER</h3>
|
|
|
|
<label>API Endpoint</label>
|
|
<input type="text" id="endpoint" value="https://ai-sandbox.oliver.solutions/GIF-ENCODER/api.php">
|
|
|
|
<label>GIF Name</label>
|
|
<input type="text" id="gifName" value="animation">
|
|
|
|
<div class="row">
|
|
<div>
|
|
<label>Delay (ms)</label>
|
|
<input type="number" id="delay" value="500" min="50" max="5000" step="50">
|
|
</div>
|
|
<div>
|
|
<label>Quality (colors)</label>
|
|
<input type="number" id="quality" value="256" min="8" max="256">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div>
|
|
<label>Export Scale</label>
|
|
<select id="scale">
|
|
<option value="1">1x</option>
|
|
<option value="2" selected>2x</option>
|
|
<option value="3">3x</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Loop</label>
|
|
<select id="loop">
|
|
<option value="true" selected>Infinite</option>
|
|
<option value="false">Play once</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="generateBtn">Export Selection & Generate GIF</button>
|
|
<div id="status"></div>
|
|
<div id="results"></div>
|
|
|
|
<script>
|
|
const statusEl = document.getElementById('status');
|
|
const resultsEl = document.getElementById('results');
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
|
|
// ── Step 1: User clicks Generate → ask main thread to export frames ──
|
|
generateBtn.addEventListener('click', () => {
|
|
generateBtn.disabled = true;
|
|
statusEl.textContent = 'Exporting frames from Figma...';
|
|
resultsEl.innerHTML = '';
|
|
|
|
const scale = parseInt(document.getElementById('scale').value);
|
|
parent.postMessage({ pluginMessage: { type: 'EXPORT_FRAMES', scale } }, '*');
|
|
});
|
|
|
|
// ── Step 2: Receive exported PNGs from main thread → upload to API ──
|
|
window.onmessage = async (event) => {
|
|
const msg = event.data.pluginMessage;
|
|
if (!msg) return;
|
|
|
|
if (msg.type === 'ERROR') {
|
|
statusEl.textContent = 'Error: ' + msg.error;
|
|
generateBtn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (msg.type !== 'FRAMES_READY') return;
|
|
|
|
statusEl.textContent = `Got ${msg.frames.length} frames. Uploading...`;
|
|
|
|
try {
|
|
const endpoint = document.getElementById('endpoint').value;
|
|
const gifName = document.getElementById('gifName').value || 'animation';
|
|
const delay = parseInt(document.getElementById('delay').value) || 500;
|
|
const quality = parseInt(document.getElementById('quality').value) || 256;
|
|
const loop = document.getElementById('loop').value === 'true';
|
|
|
|
// Build FormData with PNG files
|
|
const formData = new FormData();
|
|
const fileNames = [];
|
|
|
|
msg.frames.forEach((frame, i) => {
|
|
// Convert Uint8Array → Blob → File for FormData
|
|
const blob = new Blob([frame.bytes], { type: 'image/png' });
|
|
const fileName = frame.name || `frame_${i}.png`;
|
|
formData.append('files[]', new File([blob], fileName, { type: 'image/png' }));
|
|
fileNames.push(fileName);
|
|
});
|
|
|
|
// Build config JSON
|
|
const config = {
|
|
groups: {
|
|
[gifName]: {
|
|
files: fileNames,
|
|
delay: delay,
|
|
loop: loop,
|
|
}
|
|
},
|
|
quality: quality,
|
|
};
|
|
formData.append('json', JSON.stringify(config));
|
|
|
|
// POST to the GIF Encoder API
|
|
// DO NOT set Content-Type header — browser sets it with the multipart boundary
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.results) {
|
|
const succeeded = data.results.filter(r => r.success);
|
|
const failed = data.results.filter(r => !r.success);
|
|
|
|
statusEl.textContent = `Done! ${succeeded.length} GIF(s) generated.`;
|
|
|
|
// Show results with preview and download link
|
|
succeeded.forEach(r => {
|
|
const div = document.createElement('div');
|
|
div.className = 'result';
|
|
div.innerHTML = `
|
|
<img src="data:image/gif;base64,${r.base64}" alt="${r.group}">
|
|
<br><a href="${r.download_url}" target="_blank">${r.group}.gif — Download</a>
|
|
`;
|
|
resultsEl.appendChild(div);
|
|
});
|
|
|
|
failed.forEach(r => {
|
|
const div = document.createElement('div');
|
|
div.className = 'result';
|
|
div.style.color = '#ff4444';
|
|
div.textContent = `${r.group}: ${r.error}`;
|
|
resultsEl.appendChild(div);
|
|
});
|
|
|
|
parent.postMessage({
|
|
pluginMessage: { type: 'GENERATION_COMPLETE', count: succeeded.length }
|
|
}, '*');
|
|
} else {
|
|
throw new Error(data.error || 'Unknown error');
|
|
}
|
|
|
|
} catch (err) {
|
|
statusEl.textContent = 'Error: ' + err.message;
|
|
parent.postMessage({
|
|
pluginMessage: { type: 'GENERATION_ERROR', error: err.message }
|
|
}, '*');
|
|
}
|
|
|
|
generateBtn.disabled = false;
|
|
};
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### 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.
|