Color palette support in byos_laravel

This commit is contained in:
Dan van Kley 2025-11-17 20:48:06 -05:00
parent c157dcf3b6
commit 9ee7bc1aac
7 changed files with 236 additions and 4 deletions

View file

@ -9,6 +9,7 @@ enum ImageFormat: string
case BMP3_1BIT_SRGB = 'bmp3_1bit_srgb'; case BMP3_1BIT_SRGB = 'bmp3_1bit_srgb';
case PNG_8BIT_256C = 'png_8bit_256c'; case PNG_8BIT_256C = 'png_8bit_256c';
case PNG_2BIT_4C = 'png_2bit_4c'; case PNG_2BIT_4C = 'png_2bit_4c';
case PNG_INDEXED = 'png_indexed';
public function label(): string public function label(): string
{ {
@ -18,6 +19,7 @@ enum ImageFormat: string
self::BMP3_1BIT_SRGB => 'BMP3 1-bit sRGB 2c', self::BMP3_1BIT_SRGB => 'BMP3 1-bit sRGB 2c',
self::PNG_8BIT_256C => 'PNG 8-bit Grayscale Gray 256c', self::PNG_8BIT_256C => 'PNG 8-bit Grayscale Gray 256c',
self::PNG_2BIT_4C => 'PNG 2-bit Grayscale 4c', self::PNG_2BIT_4C => 'PNG 2-bit Grayscale 4c',
self::PNG_INDEXED => 'PNG indexed',
}; };
} }
} }

31
app/Enums/PaletteName.php Normal file
View file

@ -0,0 +1,31 @@
<?php
namespace App\Enums;
use Bnussbau\TrmnlPipeline\Data\RgbColor;
use InvalidArgumentException;
enum PaletteName: string
{
case SPECTRA_6 = 'spectra_6';
/**
* @return RgbColor[]
*/
public static function getPalette(PaletteName $palette): array
{
switch ($palette) {
case PaletteName::SPECTRA_6:
return [
RgbColor::fromComponents(0, 0, 0),
RgbColor::fromComponents(255, 255, 255),
RgbColor::fromComponents(255, 255, 0),
RgbColor::fromComponents(255, 0, 0),
RgbColor::fromComponents(0, 255, 0),
RgbColor::fromComponents(0, 0, 255),
];
default:
throw new InvalidArgumentException('Invalid palette name');
}
}
}

View file

@ -6,6 +6,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class DeviceModel extends Model final class DeviceModel extends Model
{ {
@ -25,6 +26,11 @@ final class DeviceModel extends Model
'published_at' => 'datetime', 'published_at' => 'datetime',
]; ];
public function palette(): BelongsTo
{
return $this->belongsTo(Palette::class);
}
public function getColorDepthAttribute(): ?string public function getColorDepthAttribute(): ?string
{ {
if (! $this->bit_depth) { if (! $this->bit_depth) {

32
app/Models/Palette.php Normal file
View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Palette model representing color palettes used by device models.
*
* @property int $id
* @property string $name
* @property string $description
* @property string $palette JSON single dimension array where each entry is a 24-bit number
* representing the RGB color code.
*/
final class Palette extends Model
{
use HasFactory;
public $timestamps = false;
protected $guarded = ['id'];
public function deviceModels(): HasMany
{
return $this->hasMany(DeviceModel::class);
}
}

View file

@ -6,6 +6,8 @@ use App\Enums\ImageFormat;
use App\Models\Device; use App\Models\Device;
use App\Models\DeviceModel; use App\Models\DeviceModel;
use App\Models\Plugin; use App\Models\Plugin;
use Bnussbau\TrmnlPipeline\Data\ColorType;
use Bnussbau\TrmnlPipeline\Data\RgbColor;
use Bnussbau\TrmnlPipeline\Stages\BrowserStage; use Bnussbau\TrmnlPipeline\Stages\BrowserStage;
use Bnussbau\TrmnlPipeline\Stages\ImageStage; use Bnussbau\TrmnlPipeline\Stages\ImageStage;
use Bnussbau\TrmnlPipeline\TrmnlPipeline; use Bnussbau\TrmnlPipeline\TrmnlPipeline;
@ -66,16 +68,21 @@ class ImageGenerationService
->width($imageSettings['width']) ->width($imageSettings['width'])
->height($imageSettings['height']) ->height($imageSettings['height'])
->colors($imageSettings['colors']) ->colors($imageSettings['colors'])
->colorType($imageSettings['color_type'])
->bitDepth($imageSettings['bit_depth']) ->bitDepth($imageSettings['bit_depth'])
->rotation($imageSettings['rotation']) ->rotation($imageSettings['rotation'])
->offsetX($imageSettings['offset_x']) ->offsetX($imageSettings['offset_x'])
->offsetY($imageSettings['offset_y']) ->offsetY($imageSettings['offset_y'])
->outputPath($outputPath); ->outputPath($outputPath);
// Apply dithering if requested by markup // TODO: actually resolve this merge conflict
$shouldDither = self::markupContainsDitherImage($markup); // // Apply dithering if requested by markup
if ($shouldDither) { // $shouldDither = self::markupContainsDitherImage($markup);
$imageStage->dither(); // if ($shouldDither) {
// $imageStage->dither();
// }
if ($imageSettings['palette']) {
$imageStage->palette($imageSettings['palette']);
} }
(new TrmnlPipeline())->pipe($browserStage) (new TrmnlPipeline())->pipe($browserStage)
@ -110,11 +117,20 @@ class ImageGenerationService
if ($device->deviceModel) { if ($device->deviceModel) {
/** @var DeviceModel $model */ /** @var DeviceModel $model */
$model = $device->deviceModel; $model = $device->deviceModel;
$paletteRecord = $model->palette()->firstOr();
$palette = null;
if ($paletteRecord) {
$palette = array_map(
fn ($colorCode) => new RgbColor($colorCode),
json_decode($paletteRecord->palette)
);
}
return [ return [
'width' => $model->width, 'width' => $model->width,
'height' => $model->height, 'height' => $model->height,
'colors' => $model->colors, 'colors' => $model->colors,
'color_type' => ColorType::fromString($model->color_type),
'bit_depth' => $model->bit_depth, 'bit_depth' => $model->bit_depth,
'scale_factor' => $model->scale_factor, 'scale_factor' => $model->scale_factor,
'rotation' => $model->rotation, 'rotation' => $model->rotation,
@ -123,6 +139,7 @@ class ImageGenerationService
'offset_y' => $model->offset_y, 'offset_y' => $model->offset_y,
'image_format' => self::determineImageFormatFromModel($model), 'image_format' => self::determineImageFormatFromModel($model),
'use_model_settings' => true, 'use_model_settings' => true,
'palette' => $palette,
]; ];
} }
@ -153,6 +170,9 @@ class ImageGenerationService
private static function determineImageFormatFromModel(DeviceModel $model): string private static function determineImageFormatFromModel(DeviceModel $model): string
{ {
// Map DeviceModel settings to ImageFormat // Map DeviceModel settings to ImageFormat
if ($model->mime_type === 'image/png' && $model->palette()) {
return ImageFormat::PNG_INDEXED->value;
}
if ($model->mime_type === 'image/bmp' && $model->bit_depth === 1) { if ($model->mime_type === 'image/bmp' && $model->bit_depth === 1) {
return ImageFormat::BMP3_1BIT_SRGB->value; return ImageFormat::BMP3_1BIT_SRGB->value;
} }

View file

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\Palette;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<\App\Models\Palette>
*/
class PaletteFactory extends Factory
{
protected $model = Palette::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$colors = collect(range(1, 6))
->map(fn () => mb_strtoupper($this->faker->hexColor()))
->map(fn (string $hex) => mb_ltrim($hex, '#'))
->implode(',');
return [
'name' => $this->faker->unique()->slug(),
'description' => $this->faker->sentence(),
'palette' => $colors,
];
}
}

View file

@ -0,0 +1,108 @@
<?php
use App\Enums\PaletteName;
use App\Models\DeviceModel;
use App\Models\Palette;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Add color_type column corresponding to \Bnussbau\TrmnlPipeline\Data\ColorType enum
Schema::table('device_models', function (Blueprint $table): void {
$table->string('color_type')->default('grayscale');
$table->foreignIdFor(Palette::class)->nullable()->constrained();
});
// Ensure all existing records are populated with the default value
DB::table('device_models')->whereNull('color_type')->update(['color_type' => 'grayscale']);
// Create palette table
Schema::create('palettes', function (Blueprint $table): void {
$table->id();
$table->string('name')->index();
$table->string('description');
$table->longText('palette');
});
// Insert Spectra 6 palette
DB::table('palettes')->insert([
'name' => 'spectra_6',
'description' => 'Spectra 6 Color',
'palette' => json_encode(
array_map(
fn ($color) => $color->toInt(),
PaletteName::getPalette(PaletteName::SPECTRA_6)
)
),
]);
$palette = Palette::query()->where(
column: 'name',
operator: '=',
value: PaletteName::SPECTRA_6->value
)->firstOrFail();
$deviceModels = [
[
'name' => 'good_display_spectra_6',
'label' => 'Good Display Spectra 6',
'description' => 'Dalian Good Display Spectra6 6 color 7.3 inch',
'width' => 800,
'height' => 480,
'colors' => 6,
'color_type' => 'indexed',
'bit_depth' => 3,
'scale_factor' => 1,
'rotation' => 0,
'mime_type' => 'image/png',
'offset_x' => 0,
'offset_y' => 0,
'published_at' => '2025-11-01 00:00:00',
'source' => 'api',
'palette_id' => $palette->id,
'created_at' => now(),
'updated_at' => now(),
],
];
// Upsert by unique 'name' to avoid duplicates and keep data fresh
DeviceModel::query()->upsert(
$deviceModels,
['name'],
[
'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor',
'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'source',
'created_at', 'updated_at',
]
);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Drop palette_id foreign key column and color_type
Schema::table('device_models', function (Blueprint $table): void {
$table->dropConstrainedForeignId('palette_id');
$table->dropColumn('color_type');
});
// Drop the palette table
Schema::dropIfExists('palettes');
$names = [
'good_display_spectra_6',
];
DeviceModel::query()->whereIn('name', $names)->delete();
}
};