PNG-to-animated-GIF batch converter with PHP frontend, Python/Pillow backend, and JSON API for Figma plugin integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
341 lines
9.1 KiB
Markdown
341 lines
9.1 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.
|
|
|
|
---
|
|
|
|
## 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 <repo-url> 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
|
|
<VirtualHost *:80>
|
|
ServerName gifencoder.yourdomain.com
|
|
DocumentRoot /var/www/GIF-ENCODER
|
|
|
|
<Directory /var/www/GIF-ENCODER>
|
|
AllowOverride All
|
|
Require all granted
|
|
</Directory>
|
|
</VirtualHost>
|
|
```
|
|
|
|
**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": "<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": "http://yourserver.com/GIF-ENCODER/output/api_67be1a2f3c4d5/group_name.gif",
|
|
"base64": "<base64 encoded GIF>"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 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.
|