diff --git a/.cursor/rules/laravel-boost.mdc b/.cursor/rules/laravel-boost.mdc index b59da01..037ae20 100644 --- a/.cursor/rules/laravel-boost.mdc +++ b/.cursor/rules/laravel-boost.mdc @@ -20,7 +20,6 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/livewire (LIVEWIRE) - v3 - livewire/volt (VOLT) - v1 - larastan/larastan (LARASTAN) - v3 -- laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 - pestphp/pest (PEST) - v4 @@ -232,7 +231,7 @@ avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, h @endforeach ``` -- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: +- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: public function mount(User $user) { $this->user = $user; } @@ -544,7 +543,7 @@ $pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); - `corePlugins` is not supported in Tailwind v4. - In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - + public function mount(User $user) { $this->user = $user; } @@ -541,7 +540,7 @@ $pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); - `corePlugins` is not supported in Tailwind v4. - In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - + public function mount(User $user) { $this->user = $user; } @@ -541,7 +540,7 @@ $pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); - `corePlugins` is not supported in Tailwind v4. - In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - + public function mount(User $user) { $this->user = $user; } @@ -541,7 +540,7 @@ $pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); - `corePlugins` is not supported in Tailwind v4. - In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - +bit_depth > 4) { - return '4bit'; - } - return $this->bit_depth.'bit'; } - -/** - * Returns the scale level based on the device width. - */ - public function getScaleLevelAttribute(): ?string - { - if (! $this->width) { - return null; - } - - if ($this->width > 800 && $this->width <= 1000) { - return 'large'; - } - - if ($this->width > 1000 && $this->width <= 1400) { - return 'xlarge'; - } - - if ($this->width > 1400) { - return 'xxlarge'; - } - - return null; - } } diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 382751d..375921b 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -345,8 +345,6 @@ class Plugin extends Model if ($standalone) { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->deviceModel?->color_depth, - 'deviceVariant' => $device?->deviceModel?->name ?? 'og', - 'scaleLevel' => $device?->deviceModel?->scale_level, 'slot' => $renderedContent, ])->render(); } @@ -358,8 +356,6 @@ class Plugin extends Model if ($standalone) { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->deviceModel?->color_depth, - 'deviceVariant' => $device?->deviceModel?->name ?? 'og', - 'scaleLevel' => $device?->deviceModel?->scale_level, 'slot' => view($this->render_markup_view, [ 'size' => $size, 'data' => $this->data_payload, diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index 762d449..3a8a88d 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -6,92 +6,79 @@ use App\Enums\ImageFormat; use App\Models\Device; use App\Models\DeviceModel; use App\Models\Plugin; -use Bnussbau\TrmnlPipeline\Stages\BrowserStage; -use Bnussbau\TrmnlPipeline\Stages\ImageStage; -use Bnussbau\TrmnlPipeline\TrmnlPipeline; use Exception; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; +use Imagick; +use ImagickException; +use ImagickPixel; use Ramsey\Uuid\Uuid; use RuntimeException; +use Spatie\Browsershot\Browsershot; use Wnx\SidecarBrowsershot\BrowsershotLambda; -use function config; -use function file_exists; -use function filesize; - class ImageGenerationService { public static function generateImage(string $markup, $deviceId): string { $device = Device::with('deviceModel')->find($deviceId); $uuid = Uuid::uuid4()->toString(); + $pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png'); + $bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp'); - try { - // Get image generation settings from DeviceModel if available, otherwise use device settings - $imageSettings = self::getImageSettings($device); + // 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 PNG + if (config('app.puppeteer_mode') === 'sidecar-aws') { + try { + $browsershot = BrowsershotLambda::html($markup) + ->windowSize(800, 480); - // Create custom Browsershot instance if using AWS Lambda - $browsershotInstance = null; - if (config('app.puppeteer_mode') === 'sidecar-aws') { - $browsershotInstance = new BrowsershotLambda(); + if (config('app.puppeteer_wait_for_network_idle')) { + $browsershot->waitUntilNetworkIdle(); + } + + $browsershot->save($pngPath); + } catch (Exception $e) { + Log::error('Failed to generate PNG: '.$e->getMessage()); + throw new RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e); } - - $browserStage = new BrowserStage($browsershotInstance); - $browserStage->html($markup); - - if (config('app.puppeteer_window_size_strategy') === 'v2') { - $browserStage - ->width($imageSettings['width']) - ->height($imageSettings['height']); - } else { - // default behaviour for Framework v1 - $browserStage->useDefaultDimensions(); + } else { + try { + $browsershot = Browsershot::html($markup) + ->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : []); + if (config('app.puppeteer_wait_for_network_idle')) { + $browsershot->waitUntilNetworkIdle(); + } + if (config('app.puppeteer_window_size_strategy') === 'v2') { + $browsershot->windowSize($imageSettings['width'], $imageSettings['height']); + } else { + $browsershot->windowSize(800, 480); + } + $browsershot->save($pngPath); + } catch (Exception $e) { + Log::error('Failed to generate PNG: '.$e->getMessage()); + throw new RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e); } - - 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); - } - - $device->update(['current_screen_image' => $uuid]); - Log::info("Device $device->id: updated with new image: $uuid"); - - return $uuid; - - } catch (Exception $e) { - Log::error('Failed to generate image: '.$e->getMessage()); - throw new RuntimeException('Failed to generate image: '.$e->getMessage(), 0, $e); } + + // Validate that the PNG file was created and is valid + if (! file_exists($pngPath)) { + throw new RuntimeException('PNG file was not created: '.$pngPath); + } + + if (filesize($pngPath) === 0) { + throw new RuntimeException('PNG file is empty: '.$pngPath); + } + + // Convert image based on DeviceModel settings or fallback to device settings + self::convertImage($pngPath, $bmpPath, $imageSettings); + + $device->update(['current_screen_image' => $uuid]); + Log::info("Device $device->id: updated with new image: $uuid"); + + return $uuid; } /** @@ -120,22 +107,17 @@ class ImageGenerationService } // Fallback to device settings - $imageFormat = $device->image_format ?? ImageFormat::AUTO->value; - $mimeType = self::getMimeTypeFromImageFormat($imageFormat); - $colors = self::getColorsFromImageFormat($imageFormat); - $bitDepth = self::getBitDepthFromImageFormat($imageFormat); - return [ 'width' => $device->width ?? 800, 'height' => $device->height ?? 480, - 'colors' => $colors, - 'bit_depth' => $bitDepth, + 'colors' => 2, + 'bit_depth' => 1, 'scale_factor' => 1.0, 'rotation' => $device->rotate ?? 0, - 'mime_type' => $mimeType, + 'mime_type' => 'image/png', 'offset_x' => 0, 'offset_y' => 0, - 'image_format' => $imageFormat, + 'image_format' => $device->image_format, 'use_model_settings' => false, ]; } @@ -164,48 +146,207 @@ class ImageGenerationService } /** - * Get MIME type from ImageFormat + * Convert image based on the provided settings */ - private static function getMimeTypeFromImageFormat(string $imageFormat): string + private static function convertImage(string $pngPath, string $bmpPath, array $settings): void { - return match ($imageFormat) { - ImageFormat::BMP3_1BIT_SRGB->value => 'image/bmp', - ImageFormat::PNG_8BIT_GRAYSCALE->value, - ImageFormat::PNG_8BIT_256C->value, - ImageFormat::PNG_2BIT_4C->value => 'image/png', - ImageFormat::AUTO->value => 'image/png', // Default for AUTO - default => 'image/png', - }; + $imageFormat = $settings['image_format']; + $useModelSettings = $settings['use_model_settings'] ?? false; + + if ($useModelSettings) { + // Use DeviceModel-specific conversion + self::convertUsingModelSettings($pngPath, $bmpPath, $settings); + } else { + // Use legacy device-specific conversion + self::convertUsingLegacySettings($pngPath, $bmpPath, $imageFormat, $settings); + } } /** - * Get colors from ImageFormat + * Convert image using DeviceModel settings */ - private static function getColorsFromImageFormat(string $imageFormat): int + private static function convertUsingModelSettings(string $pngPath, string $bmpPath, array $settings): void { - return match ($imageFormat) { - ImageFormat::BMP3_1BIT_SRGB->value, - ImageFormat::PNG_8BIT_GRAYSCALE->value => 2, - ImageFormat::PNG_8BIT_256C->value => 256, - ImageFormat::PNG_2BIT_4C->value => 4, - ImageFormat::AUTO->value => 2, // Default for AUTO - default => 2, - }; + try { + $imagick = new Imagick($pngPath); + + // Apply scale factor if needed + if ($settings['scale_factor'] !== 1.0) { + $newWidth = (int) ($settings['width'] * $settings['scale_factor']); + $newHeight = (int) ($settings['height'] * $settings['scale_factor']); + $imagick->resizeImage($newWidth, $newHeight, Imagick::FILTER_LANCZOS, 1, true); + } else { + // Resize to model dimensions if different from generated size + if ($imagick->getImageWidth() !== $settings['width'] || $imagick->getImageHeight() !== $settings['height']) { + $imagick->resizeImage($settings['width'], $settings['height'], Imagick::FILTER_LANCZOS, 1, true); + } + } + + // Apply rotation + if ($settings['rotation'] !== 0) { + $imagick->rotateImage(new ImagickPixel('black'), $settings['rotation']); + } + + // Apply offset if specified + if ($settings['offset_x'] !== 0 || $settings['offset_y'] !== 0) { + $imagick->rollImage($settings['offset_x'], $settings['offset_y']); + } + + // Handle special case for 4-color, 2-bit PNG + if ($settings['colors'] === 4 && $settings['bit_depth'] === 2 && $settings['mime_type'] === 'image/png') { + self::convertTo4Color2BitPng($imagick, $settings['width'], $settings['height']); + } else { + // Set image type and color depth based on model settings + $imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE); + + if ($settings['bit_depth'] === 1) { + $imagick->quantizeImage(2, Imagick::COLORSPACE_GRAY, 0, true, false); + $imagick->setImageDepth(1); + } else { + $imagick->quantizeImage($settings['colors'], Imagick::COLORSPACE_GRAY, 0, true, false); + $imagick->setImageDepth($settings['bit_depth']); + } + } + + $imagick->stripImage(); + + // Save in the appropriate format + if ($settings['mime_type'] === 'image/bmp') { + $imagick->setFormat('BMP3'); + $imagick->writeImage($bmpPath); + } else { + $imagick->setFormat('png'); + $imagick->writeImage($pngPath); + } + + $imagick->clear(); + } catch (ImagickException $e) { + throw new RuntimeException('Failed to convert image using model settings: '.$e->getMessage(), 0, $e); + } } /** - * Get bit depth from ImageFormat + * Convert image to 4-color, 2-bit PNG using custom colormap and dithering */ - private static function getBitDepthFromImageFormat(string $imageFormat): int + private static function convertTo4Color2BitPng(Imagick $imagick, int $width, int $height): void { - return match ($imageFormat) { - ImageFormat::BMP3_1BIT_SRGB->value, - ImageFormat::PNG_8BIT_GRAYSCALE->value => 1, - ImageFormat::PNG_8BIT_256C->value => 8, - ImageFormat::PNG_2BIT_4C->value => 2, - ImageFormat::AUTO->value => 1, // Default for AUTO - default => 1, - }; + // Step 1: Create 4-color grayscale colormap in memory + $colors = ['#000000', '#555555', '#aaaaaa', '#ffffff']; + $colormap = new Imagick(); + + foreach ($colors as $color) { + $swatch = new Imagick(); + $swatch->newImage(1, 1, new ImagickPixel($color)); + $swatch->setImageFormat('png'); + $colormap->addImage($swatch); + } + + $colormap = $colormap->appendImages(true); // horizontal + $colormap->setType(Imagick::IMGTYPE_PALETTE); + $colormap->setImageFormat('png'); + + // Step 2: Resize to target dimensions without keeping aspect ratio + $imagick->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1, false); + + // Step 3: Apply Floyd–Steinberg dithering + $imagick->setOption('dither', 'FloydSteinberg'); + + // Step 4: Remap to our 4-color colormap + // $imagick->remapImage($colormap, Imagick::DITHERMETHOD_FLOYDSTEINBERG); + + // Step 5: Force 2-bit grayscale PNG + $imagick->setImageFormat('png'); + $imagick->setImageDepth(2); + $imagick->setType(Imagick::IMGTYPE_GRAYSCALE); + + // Cleanup colormap + $colormap->clear(); + } + + /** + * Convert image using legacy device settings + */ + private static function convertUsingLegacySettings(string $pngPath, string $bmpPath, string $imageFormat, array $settings): void + { + switch ($imageFormat) { + case ImageFormat::BMP3_1BIT_SRGB->value: + try { + self::convertToBmpImageMagick($pngPath, $bmpPath); + } catch (ImagickException $e) { + throw new RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e); + } + break; + case ImageFormat::PNG_8BIT_GRAYSCALE->value: + case ImageFormat::PNG_8BIT_256C->value: + try { + self::convertToPngImageMagick($pngPath, $settings['width'], $settings['height'], $settings['rotation'], quantize: $imageFormat === ImageFormat::PNG_8BIT_GRAYSCALE->value); + } catch (ImagickException $e) { + throw new RuntimeException('Failed to convert image to PNG: '.$e->getMessage(), 0, $e); + } + break; + case ImageFormat::AUTO->value: + default: + // For AUTO format, we need to check if this is a legacy device + // This would require checking if the device has a firmware version + // For now, we'll use the device's current logic + try { + self::convertToPngImageMagick($pngPath, $settings['width'], $settings['height'], $settings['rotation']); + } catch (ImagickException $e) { + throw new RuntimeException('Failed to convert image to PNG: '.$e->getMessage(), 0, $e); + } + } + } + + /** + * @throws ImagickException + */ + private static function convertToBmpImageMagick(string $pngPath, string $bmpPath): void + { + try { + $imagick = new Imagick($pngPath); + $imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE); + $imagick->quantizeImage(2, Imagick::COLORSPACE_GRAY, 0, true, false); + $imagick->setImageDepth(1); + $imagick->stripImage(); + $imagick->setFormat('BMP3'); + $imagick->writeImage($bmpPath); + $imagick->clear(); + } catch (ImagickException $e) { + Log::error('ImageMagick conversion failed for PNG: '.$pngPath.' - '.$e->getMessage()); + throw $e; + } + } + + /** + * @throws ImagickException + */ + private static function convertToPngImageMagick(string $pngPath, ?int $width, ?int $height, ?int $rotate, $quantize = true): void + { + try { + $imagick = new Imagick($pngPath); + if ($width !== 800 || $height !== 480) { + $imagick->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1, true); + } + if ($rotate !== null && $rotate !== 0) { + $imagick->rotateImage(new ImagickPixel('black'), $rotate); + } + + $imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE); + $imagick->setOption('dither', 'FloydSteinberg'); + + if ($quantize) { + $imagick->quantizeImage(2, Imagick::COLORSPACE_GRAY, 0, true, false); + } + $imagick->setImageDepth(8); + $imagick->stripImage(); + + $imagick->setFormat('png'); + $imagick->writeImage($pngPath); + $imagick->clear(); + } catch (ImagickException $e) { + Log::error('ImageMagick conversion failed for PNG: '.$pngPath.' - '.$e->getMessage()); + throw $e; + } } public static function cleanupFolder(): void diff --git a/composer.json b/composer.json index e3cbb13..8417cc6 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,9 @@ "type": "project", "description": "TRMNL Server Implementation (BYOS) for Laravel", "keywords": [ - "trmnl", - "trmnl-server", - "laravel" + "laravel", + "framework", + "trmnl" ], "license": "MIT", "require": { @@ -14,7 +14,7 @@ "ext-imagick": "*", "ext-zip": "*", "bnussbau/laravel-trmnl-blade": "2.0.*", - "bnussbau/trmnl-pipeline-php": "^0.2.0", + "intervention/image": "^3.11", "keepsuit/laravel-liquid": "^0.5.2", "laravel/framework": "^12.1", "laravel/sanctum": "^4.0", @@ -73,9 +73,7 @@ ], "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage", - "format": "vendor/bin/pint", - "analyse": "vendor/bin/phpstan analyse", - "analyze": "vendor/bin/phpstan analyse" + "format": "vendor/bin/pint" }, "extra": { "laravel": { diff --git a/composer.lock b/composer.lock index 86636bc..b81fae3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f8f7d3fd0eba117ddeb5463047ac5493", + "content-hash": "349a46b94103f479caae00ca7e6a99c2", "packages": [ { "name": "aws/aws-crt-php", @@ -241,77 +241,6 @@ ], "time": "2025-09-14T07:54:31+00:00" }, - { - "name": "bnussbau/trmnl-pipeline-php", - "version": "0.2.0", - "source": { - "type": "git", - "url": "https://github.com/bnussbau/trmnl-pipeline-php.git", - "reference": "0a85e4c935a7009c469c014c6b7f2d9783d82523" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/0a85e4c935a7009c469c014c6b7f2d9783d82523", - "reference": "0a85e4c935a7009c469c014c6b7f2d9783d82523", - "shasum": "" - }, - "require": { - "ext-imagick": "*", - "league/pipeline": "^1.0", - "php": "^8.2", - "spatie/browsershot": "^5.0" - }, - "require-dev": { - "laravel/pint": "^1.0", - "pestphp/pest": "^4.0", - "phpstan/phpstan": "^1.10", - "rector/rector": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Bnussbau\\TrmnlPipeline\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "bnussbau", - "email": "bnussbau@users.noreply.github.com", - "role": "Developer" - } - ], - "description": "Convert HTML content into optimized images for a range of e-ink devices.", - "homepage": "https://github.com/bnussbau/trmnl-pipeline-php", - "keywords": [ - "TRMNL", - "bnussbau", - "e-ink", - "trmnl-pipeline-php" - ], - "support": { - "issues": "https://github.com/bnussbau/trmnl-pipeline-php/issues", - "source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.2.0" - }, - "funding": [ - { - "url": "https://www.buymeacoffee.com/bnussbau", - "type": "buy_me_a_coffee" - }, - { - "url": "https://usetrmnl.com/?ref=laravel-trmnl", - "type": "custom" - }, - { - "url": "https://github.com/bnussbau", - "type": "github" - } - ], - "time": "2025-09-18T16:40:28+00:00" - }, { "name": "brick/math", "version": "0.14.0", @@ -1480,6 +1409,150 @@ }, "time": "2025-08-22T14:58:51+00:00" }, + { + "name": "intervention/gif", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a", + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.2.2" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2025-03-29T07:46:21+00:00" + }, + { + "name": "intervention/image", + "version": "3.11.4", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "8c49eb21a6d2572532d1bc425964264f3e496846" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/8c49eb21a6d2572532d1bc425964264f3e496846", + "reference": "8c49eb21a6d2572532d1bc425964264f3e496846", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.2", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP image manipulation", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.11.4" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2025-07-30T13:13:19+00:00" + }, { "name": "keepsuit/laravel-liquid", "version": "v0.5.4", @@ -1618,16 +1691,16 @@ }, { "name": "laravel/framework", - "version": "v12.30.0", + "version": "v12.29.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "943603722fe95b69f216bdcda7d060c9a55f18fd" + "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/943603722fe95b69f216bdcda7d060c9a55f18fd", - "reference": "943603722fe95b69f216bdcda7d060c9a55f18fd", + "url": "https://api.github.com/repos/laravel/framework/zipball/a9e4c73086f5ba38383e9c1d74b84fe46aac730b", + "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b", "shasum": "" }, "require": { @@ -1655,7 +1728,7 @@ "monolog/monolog": "^3.0", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", - "phiki/phiki": "^2.0.0", + "phiki/phiki": "v2.0.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", @@ -1834,7 +1907,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-18T15:10:15+00:00" + "time": "2025-09-16T14:15:03+00:00" }, { "name": "laravel/prompts", @@ -2611,62 +2684,6 @@ }, "time": "2024-12-10T19:59:05+00:00" }, - { - "name": "league/pipeline", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/pipeline.git", - "reference": "9069ddfdbd5582f8a563e00cffdbeffb9a0acd01" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/pipeline/zipball/9069ddfdbd5582f8a563e00cffdbeffb9a0acd01", - "reference": "9069ddfdbd5582f8a563e00cffdbeffb9a0acd01", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0 || ^10.0 || ^11.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\Pipeline\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frenky.net", - "role": "Author" - }, - { - "name": "Woody Gilk", - "email": "woody.gilk@gmail.com", - "role": "Maintainer" - } - ], - "description": "A plug and play pipeline implementation.", - "keywords": [ - "composition", - "design pattern", - "pattern", - "pipeline", - "sequential" - ], - "support": { - "issues": "https://github.com/thephpleague/pipeline/issues", - "source": "https://github.com/thephpleague/pipeline/tree/1.1.0" - }, - "time": "2025-02-06T08:48:15+00:00" - }, { "name": "league/uri", "version": "7.5.1", @@ -3822,16 +3839,16 @@ }, { "name": "phiki/phiki", - "version": "v2.0.2", + "version": "v2.0.0", "source": { "type": "git", "url": "https://github.com/phikiphp/phiki.git", - "reference": "6d735108238c03daaaef571448d8dee8187cab5e" + "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phikiphp/phiki/zipball/6d735108238c03daaaef571448d8dee8187cab5e", - "reference": "6d735108238c03daaaef571448d8dee8187cab5e", + "url": "https://api.github.com/repos/phikiphp/phiki/zipball/461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f", "shasum": "" }, "require": { @@ -3877,7 +3894,7 @@ "description": "Syntax highlighting using TextMate grammars in PHP.", "support": { "issues": "https://github.com/phikiphp/phiki/issues", - "source": "https://github.com/phikiphp/phiki/tree/v2.0.2" + "source": "https://github.com/phikiphp/phiki/tree/v2.0.0" }, "funding": [ { @@ -3889,7 +3906,7 @@ "type": "other" } ], - "time": "2025-09-17T18:32:40+00:00" + "time": "2025-08-28T18:20:27+00:00" }, { "name": "phpoption/phpoption", @@ -8528,35 +8545,35 @@ }, { "name": "laravel/boost", - "version": "v1.2.0", + "version": "v1.1.5", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24" + "reference": "4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24", - "reference": "85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24", + "url": "https://api.github.com/repos/laravel/boost/zipball/4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86", + "reference": "4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.10", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "laravel/mcp": "^0.2.0", - "laravel/prompts": "0.1.25|^0.3.6", - "laravel/roster": "^0.2.6", + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "laravel/mcp": "^0.1.1", + "laravel/prompts": "^0.1.9|^0.3", + "laravel/roster": "^0.2.5", "php": "^8.1" }, "require-dev": { - "laravel/pint": "1.20", - "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", - "pestphp/pest": "^2.36.0|^3.8.4", - "phpstan/phpstan": "^2.1.27" + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^2.0" }, "type": "library", "extra": { @@ -8578,7 +8595,7 @@ "license": [ "MIT" ], - "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", + "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.", "homepage": "https://github.com/laravel/boost", "keywords": [ "ai", @@ -8589,41 +8606,35 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-09-18T13:05:07+00:00" + "time": "2025-09-18T07:33:27+00:00" }, { "name": "laravel/mcp", - "version": "v0.2.0", + "version": "v0.1.1", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "56fade6882756d5828cc90b86611d29616c2d754" + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/56fade6882756d5828cc90b86611d29616c2d754", - "reference": "56fade6882756d5828cc90b86611d29616c2d754", + "url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713", + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713", "shasum": "" }, "require": { - "ext-json": "*", - "ext-mbstring": "*", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/container": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/http": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/json-schema": "^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/validation": "^10.49.0|^11.45.3|^12.28.1", - "php": "^8.1" + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/validation": "^10.0|^11.0|^12.0", + "php": "^8.1|^8.2" }, "require-dev": { - "laravel/pint": "1.20.0", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6.0", - "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", - "phpstan/phpstan": "^2.1.27", - "rector/rector": "^2.1.7" + "laravel/pint": "^1.14", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "phpstan/phpstan": "^2.0" }, "type": "library", "extra": { @@ -8639,6 +8650,8 @@ "autoload": { "psr-4": { "Laravel\\Mcp\\": "src/", + "Workbench\\App\\": "workbench/app/", + "Laravel\\Mcp\\Tests\\": "tests/", "Laravel\\Mcp\\Server\\": "src/Server/" } }, @@ -8646,15 +8659,10 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Rapidly build MCP servers for your Laravel applications.", + "description": "The easiest way to add MCP servers to your Laravel app.", "homepage": "https://github.com/laravel/mcp", "keywords": [ + "dev", "laravel", "mcp" ], @@ -8662,7 +8670,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-09-18T12:58:47+00:00" + "time": "2025-08-16T09:50:43+00:00" }, { "name": "laravel/pail", @@ -8811,16 +8819,16 @@ }, { "name": "laravel/roster", - "version": "v0.2.7", + "version": "v0.2.6", "source": { "type": "git", "url": "https://github.com/laravel/roster.git", - "reference": "9de07bfb52cfe4e5a1fec10b8a446d6add8376cd" + "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/9de07bfb52cfe4e5a1fec10b8a446d6add8376cd", - "reference": "9de07bfb52cfe4e5a1fec10b8a446d6add8376cd", + "url": "https://api.github.com/repos/laravel/roster/zipball/5615acdf860c5a5c61d04aba44f2d3312550c514", + "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514", "shasum": "" }, "require": { @@ -8868,7 +8876,7 @@ "issues": "https://github.com/laravel/roster/issues", "source": "https://github.com/laravel/roster" }, - "time": "2025-09-18T13:53:41+00:00" + "time": "2025-09-04T07:31:39+00:00" }, { "name": "laravel/sail", diff --git a/resources/views/trmnl-layouts/mashup.blade.php b/resources/views/trmnl-layouts/mashup.blade.php index 1d8321f..d2890fa 100644 --- a/resources/views/trmnl-layouts/mashup.blade.php +++ b/resources/views/trmnl-layouts/mashup.blade.php @@ -1,25 +1,8 @@ -@props([ - 'mashupLayout' => '1Tx1B', - 'noBleed' => false, - 'darkMode' => false, - 'deviceVariant' => 'og', - 'deviceOrientation' => null, - 'colorDepth' => '1bit', - 'scaleLevel' => null, -]) +@props(['mashupLayout' => '1Tx1B']) -@if(config('app.puppeteer_window_size_strategy') === 'v2') - - - {!! $slot !!} - - -@else - - - {!! $slot !!} - - -@endif + + + {{-- The slot is used to pass the content of the mashup --}} + {!! $slot !!} + + diff --git a/resources/views/trmnl-layouts/single.blade.php b/resources/views/trmnl-layouts/single.blade.php index 17ffe43..741ddbd 100644 --- a/resources/views/trmnl-layouts/single.blade.php +++ b/resources/views/trmnl-layouts/single.blade.php @@ -1,20 +1,7 @@ @props([ - 'noBleed' => false, - 'darkMode' => false, - 'deviceVariant' => 'og', - 'deviceOrientation' => null, 'colorDepth' => '1bit', - 'scaleLevel' => null, ]) -@if(config('app.puppeteer_window_size_strategy') === 'v2') - - {!! $slot !!} - -@else - - {!! $slot !!} - -@endif + + {!! $slot !!} + diff --git a/tests/Unit/Services/ImageGenerationServiceTest.php b/tests/Unit/Services/ImageGenerationServiceTest.php index 37ed4e2..03f08d1 100644 --- a/tests/Unit/Services/ImageGenerationServiceTest.php +++ b/tests/Unit/Services/ImageGenerationServiceTest.php @@ -99,8 +99,8 @@ it('get_image_settings uses defaults for missing device properties', function () expect($settings['mime_type'])->toBe('image/png'); expect($settings['offset_x'])->toBe(0); expect($settings['offset_y'])->toBe(0); - // image_format defaults to 'auto' when not set - expect($settings['image_format'])->toBe('auto'); + // image_format will be null if the device doesn't have it set, which is the expected behavior + expect($settings['image_format'])->toBeNull(); })->skipOnCi(); it('determine_image_format_from_model returns correct formats', function (): void {