mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 23:18:10 +00:00
Color palette support in byos_laravel
This commit is contained in:
parent
c157dcf3b6
commit
9ee7bc1aac
7 changed files with 236 additions and 4 deletions
|
|
@ -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
31
app/Enums/PaletteName.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
32
app/Models/Palette.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
database/factories/PaletteFactory.php
Normal file
33
database/factories/PaletteFactory.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue