byos_laravel/app/Console/Commands/GenerateDefaultImagesCommand.php
Benjamin Nussbaum b10bbca774
Some checks failed
tests / ci (push) Has been cancelled
fix(#124): improve timezone support
2025-12-02 16:54:57 +01:00

201 lines
7 KiB
PHP

<?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);
// Set timezone from app config (no user context in this command)
$browserStage->timezone(config('app.timezone'));
$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();
}
}