From 9ee7bc1aac879f99ab600bf893a52ed64cf115ec Mon Sep 17 00:00:00 2001 From: Dan van Kley Date: Mon, 17 Nov 2025 20:48:06 -0500 Subject: [PATCH] Color palette support in byos_laravel --- app/Enums/ImageFormat.php | 2 + app/Enums/PaletteName.php | 31 +++++ app/Models/DeviceModel.php | 6 + app/Models/Palette.php | 32 ++++++ app/Services/ImageGenerationService.php | 28 ++++- database/factories/PaletteFactory.php | 33 ++++++ ...02_142545_good_display_spectra_6_model.php | 108 ++++++++++++++++++ 7 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 app/Enums/PaletteName.php create mode 100644 app/Models/Palette.php create mode 100644 database/factories/PaletteFactory.php create mode 100644 database/migrations/2025_11_02_142545_good_display_spectra_6_model.php diff --git a/app/Enums/ImageFormat.php b/app/Enums/ImageFormat.php index 67e9b79..763cd25 100644 --- a/app/Enums/ImageFormat.php +++ b/app/Enums/ImageFormat.php @@ -9,6 +9,7 @@ enum ImageFormat: string case BMP3_1BIT_SRGB = 'bmp3_1bit_srgb'; case PNG_8BIT_256C = 'png_8bit_256c'; case PNG_2BIT_4C = 'png_2bit_4c'; + case PNG_INDEXED = 'png_indexed'; public function label(): string { @@ -18,6 +19,7 @@ enum ImageFormat: string self::BMP3_1BIT_SRGB => 'BMP3 1-bit sRGB 2c', self::PNG_8BIT_256C => 'PNG 8-bit Grayscale Gray 256c', self::PNG_2BIT_4C => 'PNG 2-bit Grayscale 4c', + self::PNG_INDEXED => 'PNG indexed', }; } } diff --git a/app/Enums/PaletteName.php b/app/Enums/PaletteName.php new file mode 100644 index 0000000..4334be1 --- /dev/null +++ b/app/Enums/PaletteName.php @@ -0,0 +1,31 @@ + 'datetime', ]; + public function palette(): BelongsTo + { + return $this->belongsTo(Palette::class); + } + public function getColorDepthAttribute(): ?string { if (! $this->bit_depth) { diff --git a/app/Models/Palette.php b/app/Models/Palette.php new file mode 100644 index 0000000..4fd4fc1 --- /dev/null +++ b/app/Models/Palette.php @@ -0,0 +1,32 @@ +hasMany(DeviceModel::class); + } +} diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index 76be3bb..05ce381 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -6,6 +6,8 @@ use App\Enums\ImageFormat; use App\Models\Device; use App\Models\DeviceModel; use App\Models\Plugin; +use Bnussbau\TrmnlPipeline\Data\ColorType; +use Bnussbau\TrmnlPipeline\Data\RgbColor; use Bnussbau\TrmnlPipeline\Stages\BrowserStage; use Bnussbau\TrmnlPipeline\Stages\ImageStage; use Bnussbau\TrmnlPipeline\TrmnlPipeline; @@ -66,16 +68,21 @@ class ImageGenerationService ->width($imageSettings['width']) ->height($imageSettings['height']) ->colors($imageSettings['colors']) + ->colorType($imageSettings['color_type']) ->bitDepth($imageSettings['bit_depth']) ->rotation($imageSettings['rotation']) ->offsetX($imageSettings['offset_x']) ->offsetY($imageSettings['offset_y']) ->outputPath($outputPath); - // Apply dithering if requested by markup - $shouldDither = self::markupContainsDitherImage($markup); - if ($shouldDither) { - $imageStage->dither(); + // TODO: actually resolve this merge conflict +// // Apply dithering if requested by markup +// $shouldDither = self::markupContainsDitherImage($markup); +// if ($shouldDither) { +// $imageStage->dither(); +// } + if ($imageSettings['palette']) { + $imageStage->palette($imageSettings['palette']); } (new TrmnlPipeline())->pipe($browserStage) @@ -110,11 +117,20 @@ class ImageGenerationService if ($device->deviceModel) { /** @var DeviceModel $model */ $model = $device->deviceModel; + $paletteRecord = $model->palette()->firstOr(); + $palette = null; + if ($paletteRecord) { + $palette = array_map( + fn ($colorCode) => new RgbColor($colorCode), + json_decode($paletteRecord->palette) + ); + } return [ 'width' => $model->width, 'height' => $model->height, 'colors' => $model->colors, + 'color_type' => ColorType::fromString($model->color_type), 'bit_depth' => $model->bit_depth, 'scale_factor' => $model->scale_factor, 'rotation' => $model->rotation, @@ -123,6 +139,7 @@ class ImageGenerationService 'offset_y' => $model->offset_y, 'image_format' => self::determineImageFormatFromModel($model), 'use_model_settings' => true, + 'palette' => $palette, ]; } @@ -153,6 +170,9 @@ class ImageGenerationService private static function determineImageFormatFromModel(DeviceModel $model): string { // 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) { return ImageFormat::BMP3_1BIT_SRGB->value; } diff --git a/database/factories/PaletteFactory.php b/database/factories/PaletteFactory.php new file mode 100644 index 0000000..61a09d3 --- /dev/null +++ b/database/factories/PaletteFactory.php @@ -0,0 +1,33 @@ + + */ +class PaletteFactory extends Factory +{ + protected $model = Palette::class; + + /** + * Define the model's default state. + * + * @return array + */ + 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, + ]; + } +} diff --git a/database/migrations/2025_11_02_142545_good_display_spectra_6_model.php b/database/migrations/2025_11_02_142545_good_display_spectra_6_model.php new file mode 100644 index 0000000..5b91df6 --- /dev/null +++ b/database/migrations/2025_11_02_142545_good_display_spectra_6_model.php @@ -0,0 +1,108 @@ +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(); + } +};