Files
BarangaySystem/app/Support/PaletteExtractor.php
2026-06-06 18:43:00 +08:00

146 lines
4.4 KiB
PHP

<?php
namespace App\Support;
class PaletteExtractor
{
/**
* Extract a usable brand palette from an image file.
* Returns ['primary' => '#rrggbb', 'accent' => '#rrggbb', 'tint' => '#rrggbb']
* or null if extraction fails (e.g. GD missing, unreadable file).
*/
public static function extract(string $path): ?array
{
if (!extension_loaded('gd') || !is_readable($path)) {
return null;
}
$info = @getimagesize($path);
if (!$info) {
return null;
}
$img = match ($info[2]) {
IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
IMAGETYPE_PNG => @imagecreatefrompng($path),
IMAGETYPE_GIF => @imagecreatefromgif($path),
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : null,
default => null,
};
if (!$img) {
return null;
}
$w = imagesx($img);
$h = imagesy($img);
$stepX = max(1, (int) floor($w / 60));
$stepY = max(1, (int) floor($h / 60));
$buckets = [];
for ($y = 0; $y < $h; $y += $stepY) {
for ($x = 0; $x < $w; $x += $stepX) {
$rgba = imagecolorat($img, $x, $y);
$a = ($rgba >> 24) & 0x7F;
if ($a > 90) {
continue; // mostly transparent
}
$r = ($rgba >> 16) & 0xFF;
$g = ($rgba >> 8) & 0xFF;
$b = $rgba & 0xFF;
$sum = $r + $g + $b;
if ($sum < 60 || $sum > 720) {
continue; // too dark or too light
}
$max = max($r, $g, $b);
$min = min($r, $g, $b);
if (($max - $min) < 25) {
continue; // too gray
}
// quantize to 32-step buckets
$key = (($r >> 5) << 10) | (($g >> 5) << 5) | ($b >> 5);
if (!isset($buckets[$key])) {
$buckets[$key] = ['r' => 0, 'g' => 0, 'b' => 0, 'n' => 0];
}
$buckets[$key]['r'] += $r;
$buckets[$key]['g'] += $g;
$buckets[$key]['b'] += $b;
$buckets[$key]['n']++;
}
}
imagedestroy($img);
if (empty($buckets)) {
return null;
}
uasort($buckets, fn ($a, $b) => $b['n'] <=> $a['n']);
$candidates = [];
foreach ($buckets as $bucket) {
$candidates[] = [
'r' => (int) round($bucket['r'] / $bucket['n']),
'g' => (int) round($bucket['g'] / $bucket['n']),
'b' => (int) round($bucket['b'] / $bucket['n']),
'n' => $bucket['n'],
];
if (count($candidates) >= 8) {
break;
}
}
$primary = $candidates[0];
$accent = null;
foreach (array_slice($candidates, 1) as $c) {
$dr = $c['r'] - $primary['r'];
$dg = $c['g'] - $primary['g'];
$db = $c['b'] - $primary['b'];
if (sqrt($dr * $dr + $dg * $dg + $db * $db) > 60) {
$accent = $c;
break;
}
}
if (!$accent) {
$accent = self::shift($primary, 30);
}
$tint = self::mixWithWhite($primary, 0.92);
return [
'primary' => self::toHex($primary),
'accent' => self::toHex($accent),
'tint' => self::toHex($tint),
];
}
private static function toHex(array $c): string
{
return sprintf('#%02x%02x%02x',
max(0, min(255, $c['r'])),
max(0, min(255, $c['g'])),
max(0, min(255, $c['b']))
);
}
private static function shift(array $c, int $delta): array
{
return [
'r' => ($c['r'] + $delta) % 256,
'g' => ($c['g'] + $delta * 2) % 256,
'b' => ($c['b'] + $delta * 3) % 256,
];
}
private static function mixWithWhite(array $c, float $whiteRatio): array
{
$w = max(0.0, min(1.0, $whiteRatio));
return [
'r' => (int) round($c['r'] * (1 - $w) + 255 * $w),
'g' => (int) round($c['g'] * (1 - $w) + 255 * $w),
'b' => (int) round($c['b'] * (1 - $w) + 255 * $w),
];
}
}