fix(#71): device specific sleep and setup images
Some checks failed
tests / ci (push) Has been cancelled

This commit is contained in:
Benjamin Nussbaum 2025-09-24 22:33:21 +02:00
parent 6ae3e023d4
commit 3e5ba47a12
35 changed files with 614 additions and 20 deletions

View file

@ -0,0 +1,197 @@
<?php
namespace App\Console\Commands;
use App\Models\DeviceModel;
use Bnussbau\TrmnlPipeline\Stages\BrowserStage;
use Bnussbau\TrmnlPipeline\Stages\ImageStage;
use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use RuntimeException;
use Wnx\SidecarBrowsershot\BrowsershotLambda;
class GenerateDefaultImagesCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'images:generate-defaults {--force : Force regeneration of existing images}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate default images (setup-logo and sleep) for all device models from Blade templates';
/**
* Execute the console command.
*/
public function handle(): int
{
$this->info('Starting generation of default images for all device models...');
$deviceModels = DeviceModel::all();
if ($deviceModels->isEmpty()) {
$this->warn('No device models found in the database.');
return self::SUCCESS;
}
$this->info("Found {$deviceModels->count()} device models to process.");
// Create the target directory
$targetDir = 'images/default-screens';
if (! Storage::disk('public')->exists($targetDir)) {
Storage::disk('public')->makeDirectory($targetDir);
$this->info("Created directory: {$targetDir}");
}
$successCount = 0;
$skipCount = 0;
$errorCount = 0;
foreach ($deviceModels as $deviceModel) {
$this->info("Processing device model: {$deviceModel->label} (ID: {$deviceModel->id})");
try {
// Process setup-logo
$setupResult = $this->transformImage('setup-logo', $deviceModel, $targetDir);
if ($setupResult) {
++$successCount;
} else {
++$skipCount;
}
// Process sleep
$sleepResult = $this->transformImage('sleep', $deviceModel, $targetDir);
if ($sleepResult) {
++$successCount;
} else {
++$skipCount;
}
} catch (Exception $e) {
$this->error("Error processing device model {$deviceModel->label}: ".$e->getMessage());
++$errorCount;
}
}
$this->info("\nGeneration completed!");
$this->info("Successfully processed: {$successCount} images");
$this->info("Skipped (already exist): {$skipCount} images");
$this->info("Errors: {$errorCount} images");
return self::SUCCESS;
}
/**
* Transform a single image for a device model using Blade templates
*/
private function transformImage(string $imageType, DeviceModel $deviceModel, string $targetDir): bool
{
// Generate filename: {width}_{height}_{bit_depth}_{rotation}.{extension}
$extension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
$filename = "{$deviceModel->width}_{$deviceModel->height}_{$deviceModel->bit_depth}_{$deviceModel->rotation}.{$extension}";
$targetPath = "{$targetDir}/{$imageType}_{$filename}";
// Check if target already exists and force is not set
if (Storage::disk('public')->exists($targetPath) && ! $this->option('force')) {
$this->line(" Skipping {$imageType} - already exists: {$filename}");
return false;
}
try {
// Create custom Browsershot instance if using AWS Lambda
$browsershotInstance = null;
if (config('app.puppeteer_mode') === 'sidecar-aws') {
$browsershotInstance = new BrowsershotLambda();
}
// Generate HTML from Blade template
$html = $this->generateHtmlFromTemplate($imageType, $deviceModel);
// dump($html);
$browserStage = new BrowserStage($browsershotInstance);
$browserStage->html($html);
$browserStage
->width($deviceModel->width)
->height($deviceModel->height);
$browserStage->setBrowsershotOption('waitUntil', 'networkidle0');
if (config('app.puppeteer_docker')) {
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
}
$outputPath = Storage::disk('public')->path($targetPath);
$imageStage = new ImageStage();
$imageStage->format($extension)
->width($deviceModel->width)
->height($deviceModel->height)
->colors($deviceModel->colors)
->bitDepth($deviceModel->bit_depth)
->rotation($deviceModel->rotation)
// ->offsetX($deviceModel->offset_x)
// ->offsetY($deviceModel->offset_y)
->outputPath($outputPath);
(new TrmnlPipeline())->pipe($browserStage)
->pipe($imageStage)
->process();
if (! file_exists($outputPath)) {
throw new RuntimeException('Image file was not created: '.$outputPath);
}
if (filesize($outputPath) === 0) {
throw new RuntimeException('Image file is empty: '.$outputPath);
}
$this->line(" ✓ Generated {$imageType}: {$filename}");
return true;
} catch (Exception $e) {
$this->error(" ✗ Failed to generate {$imageType} for {$deviceModel->label}: ".$e->getMessage());
return false;
}
}
/**
* Generate HTML from Blade template for the given image type and device model
*/
private function generateHtmlFromTemplate(string $imageType, DeviceModel $deviceModel): string
{
// Map image type to template name
$templateName = match ($imageType) {
'setup-logo' => 'default-screens.setup',
'sleep' => 'default-screens.sleep',
default => throw new InvalidArgumentException("Invalid image type: {$imageType}")
};
// Determine device properties from DeviceModel
$deviceVariant = $deviceModel->name ?? 'og';
$colorDepth = $deviceModel->color_depth ?? '1bit'; // Use the accessor method
$scaleLevel = $deviceModel->scale_level; // Use the accessor method
$darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode
// Render the Blade template
return view($templateName, [
'noBleed' => false,
'darkMode' => $darkMode,
'deviceVariant' => $deviceVariant,
'colorDepth' => $colorDepth,
'scaleLevel' => $scaleLevel,
])->render();
}
}

View file

@ -31,6 +31,10 @@ final class DeviceModel extends Model
return null;
}
if ($this->bit_depth === 3) {
return '2bit';
}
// if higher then 4 return 4bit
if ($this->bit_depth > 4) {
return '4bit';

View file

@ -12,6 +12,7 @@ use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Ramsey\Uuid\Uuid;
use RuntimeException;
use Wnx\SidecarBrowsershot\BrowsershotLambda;
@ -255,4 +256,151 @@ class ImageGenerationService
}
}
}
/**
* Get device-specific default image path for setup or sleep mode
*/
public static function getDeviceSpecificDefaultImage(Device $device, string $imageType): ?string
{
// Validate image type
if (! in_array($imageType, ['setup-logo', 'sleep'])) {
return null;
}
// If device has a DeviceModel, try to find device-specific image
if ($device->deviceModel) {
$model = $device->deviceModel;
$extension = $model->mime_type === 'image/bmp' ? 'bmp' : 'png';
$filename = "{$model->width}_{$model->height}_{$model->bit_depth}_{$model->rotation}.{$extension}";
$deviceSpecificPath = "images/default-screens/{$imageType}_{$filename}";
if (Storage::disk('public')->exists($deviceSpecificPath)) {
return $deviceSpecificPath;
}
}
// Fallback to original hardcoded images
$fallbackPath = "images/{$imageType}.bmp";
if (Storage::disk('public')->exists($fallbackPath)) {
return $fallbackPath;
}
// Try PNG fallback
$fallbackPathPng = "images/{$imageType}.png";
if (Storage::disk('public')->exists($fallbackPathPng)) {
return $fallbackPathPng;
}
return null;
}
/**
* Generate a default screen image from Blade template
*/
public static function generateDefaultScreenImage(Device $device, string $imageType): string
{
// Validate image type
if (! in_array($imageType, ['setup-logo', 'sleep'])) {
throw new InvalidArgumentException("Invalid image type: {$imageType}");
}
$uuid = Uuid::uuid4()->toString();
try {
// Get image generation settings from DeviceModel if available, otherwise use device settings
$imageSettings = self::getImageSettings($device);
$fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png';
$outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension);
// Generate HTML from Blade template
$html = self::generateDefaultScreenHtml($device, $imageType);
// Create custom Browsershot instance if using AWS Lambda
$browsershotInstance = null;
if (config('app.puppeteer_mode') === 'sidecar-aws') {
$browsershotInstance = new BrowsershotLambda();
}
$browserStage = new BrowserStage($browsershotInstance);
$browserStage->html($html);
if (config('app.puppeteer_window_size_strategy') === 'v2') {
$browserStage
->width($imageSettings['width'])
->height($imageSettings['height']);
} else {
$browserStage->useDefaultDimensions();
}
if (config('app.puppeteer_wait_for_network_idle')) {
$browserStage->setBrowsershotOption('waitUntil', 'networkidle0');
}
if (config('app.puppeteer_docker')) {
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
}
$imageStage = new ImageStage();
$imageStage->format($fileExtension)
->width($imageSettings['width'])
->height($imageSettings['height'])
->colors($imageSettings['colors'])
->bitDepth($imageSettings['bit_depth'])
->rotation($imageSettings['rotation'])
->offsetX($imageSettings['offset_x'])
->offsetY($imageSettings['offset_y'])
->outputPath($outputPath);
(new TrmnlPipeline())->pipe($browserStage)
->pipe($imageStage)
->process();
if (! file_exists($outputPath)) {
throw new RuntimeException('Image file was not created: '.$outputPath);
}
if (filesize($outputPath) === 0) {
throw new RuntimeException('Image file is empty: '.$outputPath);
}
Log::info("Device $device->id: generated default screen image: $uuid for type: $imageType");
return $uuid;
} catch (Exception $e) {
Log::error('Failed to generate default screen image: '.$e->getMessage());
throw new RuntimeException('Failed to generate default screen image: '.$e->getMessage(), 0, $e);
}
}
/**
* Generate HTML from Blade template for default screens
*/
private static function generateDefaultScreenHtml(Device $device, string $imageType): string
{
// Map image type to template name
$templateName = match ($imageType) {
'setup-logo' => 'default-screens.setup',
'sleep' => 'default-screens.sleep',
default => throw new InvalidArgumentException("Invalid image type: {$imageType}")
};
// Determine device properties from DeviceModel or device settings
$deviceVariant = $device->deviceVariant();
$deviceOrientation = $device->rotate > 0 ? 'portrait' : 'landscape';
$colorDepth = $device->colorDepth() ?? '1bit';
$scaleLevel = $device->scaleLevel();
$darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode
// Render the Blade template
return view($templateName, [
'noBleed' => false,
'darkMode' => $darkMode,
'deviceVariant' => $deviceVariant,
'deviceOrientation' => $deviceOrientation,
'colorDepth' => $colorDepth,
'scaleLevel' => $scaleLevel,
])->render();
}
}