Files
api/services/nginx/app/modules/dynamicimages/traits/dynamicimages_image_t.php
T
Jeppe Bundgaard ee305757f2 Implement in-memory asset manipulation and enhance machine_1 drawing logic
- Add in-memory asset handling to `dynamicimages_asset_t` with methods for loading, rotating, cloning, and clearing `Imagick` instances.
- Update `dynamicimages_image_t` to use in-memory images when available for dynamic asset composition.
- Replace thumb reference in `machine_1` with new `machine_1_wash_programs_thumb_standalone_transparent` asset.
- Enhance `machine_1` to resize rotated thumb dynamically while drawing.
2026-01-27 12:35:50 +01:00

299 lines
9.8 KiB
PHP

<?php
namespace dynamicimages\traits;
use dynamicimages\classes\dynamicimages_asset;
use dynamicimages\helpers\dynamicimages_tool;
use dynamicimages\interfaces\dynamicimages_image_i;
trait dynamicimages_image_t
{
/**
* Internal Imagick canvas (temporary, changeable image object)
*/
private ?\Imagick $image = null;
private ?int $canvasWidth = null;
private ?int $canvasHeight = null;
/**
* @var dynamicimages_tool[]
*/
private array $tools = [];
/**
* @inheritDoc
*/
public function setAssets(array $assets): dynamicimages_image_i
{
$this->assets = $assets;
return $this;
}
/**
* @inheritDoc
*/
public function getAssets(): array
{
return $this->assets;
}
/**
* @inheritDoc
*/
public function addAsset(dynamicimages_asset $asset): dynamicimages_image_i
{
$this->assets[] = $asset;
return $this;
}
/**
* @inheritDoc
*/
public function setTools(array $tools): dynamicimages_image_i
{
$this->tools = $tools;
return $this;
}
/**
* @inheritDoc
*/
public function addTool(dynamicimages_tool $tool): dynamicimages_image_i
{
$this->tools[] = $tool;
return $this;
}
/**
* @inheritDoc
*/
public function getTools(): array
{
return $this->tools;
}
/**
* Debug
*/
public function debugAssets(): void
{
echo 'Assets count: ' . count($this->assets) . PHP_EOL;
foreach ($this->assets as $asset) {
echo 'Asset path: ' . $asset->getPath() . ' - (Width: ' . $asset->getWidth() . ', Height: ' . $asset->getHeight() . ')' . PHP_EOL;
}
}
public function getAssetPath(string $asset_name): string
{
return $this->module_asset_path . $asset_name;
}
public function setAssetPath(string $asset_path): dynamicimages_image_i
{
$this->module_asset_path = $asset_path;
return $this;
}
// ===== Canvas lifecycle =====
public function initImageCanvas(int $width, int $height, string $background = 'transparent'): dynamicimages_image_i
{
if (!extension_loaded('imagick')) {
throw new \RuntimeException('Imagick extension is required to initialize image canvas.');
}
if ($width <= 0 || $height <= 0) {
throw new \InvalidArgumentException('Canvas width and height must be positive integers.');
}
$bg = new \ImagickPixel($background === 'transparent' ? 'transparent' : $background);
$this->image = new \Imagick();
$this->image->newImage($width, $height, $bg, 'png'); // ensure alpha channel
$this->image->setImageFormat('png');
$this->canvasWidth = $width;
$this->canvasHeight = $height;
return $this;
}
public function clearImage(): dynamicimages_image_i
{
if ($this->image instanceof \Imagick) {
$this->image->clear();
$this->image->destroy();
}
$this->image = null;
$this->canvasWidth = null;
$this->canvasHeight = null;
return $this;
}
public function getCanvasSize(): array
{
$this->assertCanvasInitialized();
return ['width' => $this->canvasWidth, 'height' => $this->canvasHeight];
}
// ===== Explicit asset compositing =====
public function drawAsset(dynamicimages_asset $asset, int $x, int $y, float $opacity = 1.0): dynamicimages_image_i
{
$this->assertCanvasInitialized();
if ($opacity < 0.0 || $opacity > 1.0) {
throw new \InvalidArgumentException('Opacity must be between 0.0 and 1.0');
}
// If the asset has an in-memory image (e.g., rotated), use it; otherwise load from path
$memoryImage = method_exists($asset, 'getMemoryImage') ? $asset->getMemoryImage() : null;
if ($memoryImage instanceof \Imagick) {
$layer = $memoryImage; // already a clone from asset
} else {
if (!is_readable($asset->getPath())) {
throw new \RuntimeException('Asset is not readable: ' . $asset->getPath());
}
$layer = new \Imagick();
$layer->readImage($asset->getPath());
}
// Normalize to support alpha
$layer->setImageAlphaChannel(\Imagick::ALPHACHANNEL_SET);
if ($opacity < 1.0) {
$layer->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
}
$this->image->compositeImage($layer, \Imagick::COMPOSITE_DEFAULT, $x, $y);
$layer->clear();
$layer->destroy();
return $this;
}
// ===== Transformations on the temporary image object =====
public function resize(int $width, int $height): dynamicimages_image_i
{
$this->assertCanvasInitialized();
if ($width <= 0 || $height <= 0) {
throw new \InvalidArgumentException('Resize width and height must be positive integers.');
}
$this->image->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1.0);
$this->canvasWidth = $width;
$this->canvasHeight = $height;
return $this;
}
public function crop(int $width, int $height, int $x, int $y): dynamicimages_image_i
{
$this->assertCanvasInitialized();
if ($width <= 0 || $height <= 0) {
throw new \InvalidArgumentException('Crop width and height must be positive integers.');
}
$this->image->cropImage($width, $height, $x, $y);
// Reset canvas virtual page to avoid offsets
$this->image->setImagePage(0, 0, 0, 0);
$this->canvasWidth = $width;
$this->canvasHeight = $height;
return $this;
}
public function rotate(float $degrees, string $background = 'transparent'): dynamicimages_image_i
{
$this->assertCanvasInitialized();
$bg = new \ImagickPixel($background === 'transparent' ? 'transparent' : $background);
$this->image->rotateImage($bg, $degrees);
// Update stored dimensions
$this->canvasWidth = (int)$this->image->getImageWidth();
$this->canvasHeight = (int)$this->image->getImageHeight();
return $this;
}
public function setOpacity(float $opacity): dynamicimages_image_i
{
$this->assertCanvasInitialized();
if ($opacity < 0.0 || $opacity > 1.0) {
throw new \InvalidArgumentException('Opacity must be between 0.0 and 1.0');
}
$this->image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_SET);
$this->image->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
return $this;
}
private function assertCanvasInitialized(): void
{
if (!$this->image instanceof \Imagick) {
throw new \RuntimeException('Canvas is not initialized. Call initImageCanvas() first.');
}
}
/**
* Export the first asset as a base64 data URI. If no assets exist, throws an exception.
*/
public function exportAsBase64(?string $format = null, int $quality = 90): string
{
// If a canvas is initialized, export that as PNG by default
if ($this->image instanceof \Imagick) {
$img = clone $this->image;
$img->setImageFormat('png');
// Quality influences compression for PNG differently; keep as hint
if ($format !== null && strtolower($format) !== 'png') {
// For now we only support PNG for composed images as requested
}
// Strip metadata to reduce size
$img->stripImage();
$blob = $img->getImageBlob();
$img->clear();
$img->destroy();
return 'data:image/png;base64,' . base64_encode($blob);
}
// Fallback: export first asset as-is
if (empty($this->assets)) {
throw new \RuntimeException('No assets available to export.');
}
$asset = $this->assets[0];
$path = $asset->getPath();
if (!is_readable($path)) {
throw new \RuntimeException('Asset is not readable: ' . $path);
}
$imgInfo = @getimagesize($path);
$mime = is_array($imgInfo) && isset($imgInfo['mime']) ? $imgInfo['mime'] : 'application/octet-stream';
$data = file_get_contents($path);
if ($data === false) {
throw new \RuntimeException('Failed to read asset: ' . $path);
}
return 'data:' . $mime . ';base64,' . base64_encode($data);
}
public function getAsset(string $asset_name): ?dynamicimages_asset
{
foreach ($this->assets as $asset) {
if (basename($asset->getPath()) === $asset_name) {
return $asset;
}
}
return null;
}
/**
* Return the image file as a response directly to the browser
*
*/
public function outputImage(?string $format = null, int $quality = 90): void
{
$dataUri = $this->exportAsBase64($format, $quality);
// Extract mime type and base64 data
if (preg_match('/^data:(image\/[a-zA-Z0-9+.-]+);base64,(.*)$/', $dataUri, $matches)) {
$mimeType = $matches[1];
$base64Data = $matches[2];
// Decode base64 data
$imageData = base64_decode($base64Data);
if ($imageData !== false) {
// Send appropriate headers
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . strlen($imageData));
// Output the image data
echo $imageData;
exit;
}
}
}
/**
* Serve the composed picture directly to the client. Wrapper for outputImage.
*/
public function servePicture(?string $format = null, int $quality = 90): void
{
$this->outputImage($format, $quality);
}
}