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>
234 lines
9.2 KiB
PHP
234 lines
9.2 KiB
PHP
<?php
|
|
header('Content-Type: application/json');
|
|
|
|
// Allow CORS for Figma plugin / external scripts
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type');
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
|
|
|
|
$uploadDir = __DIR__ . '/uploads/';
|
|
$outputDir = __DIR__ . '/output/';
|
|
$pythonBin = __DIR__ . '/venv/bin/python3';
|
|
$encoderScript = __DIR__ . '/encoder.py';
|
|
|
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
|
if (!is_dir($outputDir)) mkdir($outputDir, 0755, true);
|
|
|
|
// ──────────────────────────────────────────────
|
|
// FORM-BASED (frontend UI)
|
|
// ──────────────────────────────────────────────
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|
|
|
if ($_POST['action'] === 'upload') {
|
|
$uploaded = [];
|
|
if (isset($_FILES['files'])) {
|
|
$batchId = uniqid('batch_');
|
|
$batchDir = $uploadDir . $batchId . '/';
|
|
mkdir($batchDir, 0755, true);
|
|
|
|
foreach ($_FILES['files']['tmp_name'] as $i => $tmpName) {
|
|
$originalName = $_FILES['files']['name'][$i];
|
|
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
|
if ($ext !== 'png') continue;
|
|
if ($_FILES['files']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
|
|
|
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
|
$destPath = $batchDir . $safeName;
|
|
move_uploaded_file($tmpName, $destPath);
|
|
|
|
$uploaded[] = [
|
|
'name' => $originalName,
|
|
'file' => $safeName,
|
|
'batchId' => $batchId,
|
|
'preview' => 'uploads/' . $batchId . '/' . $safeName,
|
|
];
|
|
}
|
|
}
|
|
echo json_encode(['success' => true, 'files' => $uploaded, 'batchId' => $batchId ?? null]);
|
|
exit;
|
|
}
|
|
|
|
if ($_POST['action'] === 'generate') {
|
|
$groups = json_decode($_POST['groups'], true);
|
|
$quality = intval($_POST['quality'] ?? 256);
|
|
$batchId = $_POST['batchId'] ?? '';
|
|
|
|
if (!$groups || !$batchId) {
|
|
echo json_encode(['success' => false, 'error' => 'Missing groups or batchId']);
|
|
exit;
|
|
}
|
|
|
|
$results = processGroups($groups, $batchId, $quality);
|
|
echo json_encode(['success' => true, 'results' => $results]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// JSON API (Figma plugin / scripts)
|
|
// ──────────────────────────────────────────────
|
|
// POST /api.php with JSON body or multipart with json field
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['action'])) {
|
|
$json = null;
|
|
|
|
// Check for JSON body
|
|
$rawBody = file_get_contents('php://input');
|
|
if ($rawBody) {
|
|
$json = json_decode($rawBody, true);
|
|
}
|
|
|
|
// Check for multipart with 'json' field + file uploads
|
|
if (!$json && isset($_POST['json'])) {
|
|
$json = json_decode($_POST['json'], true);
|
|
}
|
|
|
|
if ($json && isset($json['groups'])) {
|
|
$batchId = uniqid('api_');
|
|
$batchDir = $uploadDir . $batchId . '/';
|
|
mkdir($batchDir, 0755, true);
|
|
|
|
$quality = intval($json['quality'] ?? 256);
|
|
$groups = $json['groups'] ?? [];
|
|
|
|
// Handle file uploads (multipart) — files[] keyed by original name
|
|
if (isset($_FILES['files'])) {
|
|
foreach ($_FILES['files']['tmp_name'] as $i => $tmpName) {
|
|
$originalName = $_FILES['files']['name'][$i];
|
|
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
|
if ($ext !== 'png') continue;
|
|
if ($_FILES['files']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
|
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
|
move_uploaded_file($tmpName, $batchDir . $safeName);
|
|
}
|
|
}
|
|
|
|
// Handle base64 encoded files from JSON body
|
|
if (isset($json['files'])) {
|
|
foreach ($json['files'] as $fileName => $base64Data) {
|
|
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
|
|
$data = base64_decode($base64Data);
|
|
if ($data !== false) {
|
|
file_put_contents($batchDir . $safeName, $data);
|
|
}
|
|
}
|
|
}
|
|
|
|
$results = processGroups($groups, $batchId, $quality);
|
|
|
|
// For API responses, include base64 GIF data so caller doesn't need a second request
|
|
$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
|
|
. '://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['SCRIPT_NAME']);
|
|
|
|
foreach ($results as &$r) {
|
|
if ($r['success'] && isset($r['url'])) {
|
|
$filePath = __DIR__ . '/' . $r['url'];
|
|
if (file_exists($filePath)) {
|
|
$r['base64'] = base64_encode(file_get_contents($filePath));
|
|
$r['download_url'] = rtrim($baseUrl, '/') . '/' . $r['url'];
|
|
}
|
|
}
|
|
}
|
|
|
|
echo json_encode(['success' => true, 'batchId' => $batchId, 'results' => $results]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// Download
|
|
// ──────────────────────────────────────────────
|
|
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['download'])) {
|
|
$file = $outputDir . basename(dirname($_GET['download'])) . '/' . basename($_GET['download']);
|
|
if (file_exists($file) && pathinfo($file, PATHINFO_EXTENSION) === 'gif') {
|
|
header('Content-Type: image/gif');
|
|
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
|
|
readfile($file);
|
|
exit;
|
|
}
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'File not found']);
|
|
exit;
|
|
}
|
|
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Invalid request']);
|
|
|
|
// ──────────────────────────────────────────────
|
|
// Shared encoder logic
|
|
// ──────────────────────────────────────────────
|
|
function processGroups($groups, $batchId, $quality) {
|
|
global $uploadDir, $outputDir, $pythonBin, $encoderScript;
|
|
|
|
$batchDir = $uploadDir . basename($batchId) . '/';
|
|
if (!is_dir($batchDir)) return [['success' => false, 'error' => 'Batch not found']];
|
|
|
|
$outputBatchDir = $outputDir . basename($batchId) . '/';
|
|
if (!is_dir($outputBatchDir)) mkdir($outputBatchDir, 0755, true);
|
|
|
|
$results = [];
|
|
foreach ($groups as $groupName => $groupData) {
|
|
$files = $groupData['files'] ?? [];
|
|
$loop = $groupData['loop'] ?? true;
|
|
if (empty($files)) continue;
|
|
|
|
// Per-frame delays: array or single value
|
|
$delays = $groupData['delays'] ?? [];
|
|
$defaultDelay = intval($groupData['delay'] ?? 500);
|
|
|
|
$inputPaths = [];
|
|
foreach ($files as $file) {
|
|
$path = $batchDir . basename($file);
|
|
if (file_exists($path)) {
|
|
$inputPaths[] = escapeshellarg($path);
|
|
}
|
|
}
|
|
|
|
if (empty($inputPaths)) {
|
|
$results[] = ['group' => $groupName, 'success' => false, 'error' => 'No valid files'];
|
|
continue;
|
|
}
|
|
|
|
$safeGroupName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $groupName);
|
|
$outputFile = $outputBatchDir . $safeGroupName . '.gif';
|
|
|
|
$cmd = escapeshellarg($pythonBin) . ' ' . escapeshellarg($encoderScript)
|
|
. ' --input ' . implode(' ', $inputPaths)
|
|
. ' --output ' . escapeshellarg($outputFile)
|
|
. ' --quality ' . intval($quality);
|
|
|
|
// Use per-frame delays if provided, otherwise uniform
|
|
if (!empty($delays) && is_array($delays)) {
|
|
$delayStr = implode(',', array_map('intval', $delays));
|
|
$cmd .= ' --delays ' . escapeshellarg($delayStr);
|
|
} else {
|
|
$cmd .= ' --delay ' . intval($defaultDelay);
|
|
}
|
|
|
|
if (!$loop) {
|
|
$cmd .= ' --no-loop';
|
|
}
|
|
|
|
$cmd .= ' 2>&1';
|
|
$output = shell_exec($cmd);
|
|
$result = json_decode($output, true);
|
|
|
|
if ($result && $result['success']) {
|
|
$results[] = [
|
|
'group' => $groupName,
|
|
'success' => true,
|
|
'url' => 'output/' . basename($batchId) . '/' . $safeGroupName . '.gif',
|
|
'frames' => $result['frames'],
|
|
];
|
|
} else {
|
|
$results[] = [
|
|
'group' => $groupName,
|
|
'success' => false,
|
|
'error' => $result['error'] ?? $output ?? 'Unknown error',
|
|
];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|