* @link https://getkirby.com * @copyright Bastian Allgeier * @license https://opensource.org/licenses/MIT */ class Dimensions { public function __construct( public int $width, public int $height ) { } /** * Improved `var_dump` output * @codeCoverageIgnore */ public function __debugInfo(): array { return $this->toArray(); } /** * Echos the dimensions as width × height */ public function __toString(): string { return $this->width . ' × ' . $this->height; } /** * Crops the dimensions by width and height * * @return $this */ public function crop(int $width, int|null $height = null): static { $this->width = $width; $this->height = $width; if ($height !== 0 && $height !== null) { $this->height = $height; } return $this; } /** * Returns the height */ public function height(): int { return $this->height; } /** * Recalculates the width and height to fit into the given box. * * * * $dimensions = new Dimensions(1200, 768); * $dimensions->fit(500); * * echo $dimensions->width(); * // output: 500 * * echo $dimensions->height(); * // output: 320 * * * * @param int $box the max width and/or height * @param bool $force If true, the dimensions will be * upscaled to fit the box if smaller * @return $this object with recalculated dimensions */ public function fit(int $box, bool $force = false): static { if ($this->width === 0 || $this->height === 0) { $this->width = $box; $this->height = $box; return $this; } $ratio = $this->ratio(); if ($this->width > $this->height) { // wider than tall if ($this->width > $box || $force === true) { $this->width = $box; } $this->height = (int)round($this->width / $ratio); } elseif ($this->height > $this->width) { // taller than wide if ($this->height > $box || $force === true) { $this->height = $box; } $this->width = (int)round($this->height * $ratio); } elseif ($this->width > $box) { // width = height but bigger than box $this->width = $box; $this->height = $box; } return $this; } /** * Recalculates the width and height to fit the given height * * * * $dimensions = new Dimensions(1200, 768); * $dimensions->fitHeight(500); * * echo $dimensions->width(); * // output: 781 * * echo $dimensions->height(); * // output: 500 * * * * @param int|null $fit the max height * @param bool $force If true, the dimensions will be * upscaled to fit the box if smaller * @return $this object with recalculated dimensions */ public function fitHeight( int|null $fit = null, bool $force = false ): static { return $this->fitSize('height', $fit, $force); } /** * Helper for fitWidth and fitHeight methods * * @param string $ref reference (width or height) * @param int|null $fit the max width * @param bool $force If true, the dimensions will be * upscaled to fit the box if smaller * @return $this object with recalculated dimensions */ protected function fitSize( string $ref, int|null $fit = null, bool $force = false ): static { if ($fit === 0 || $fit === null) { return $this; } if ($this->$ref <= $fit && !$force) { return $this; } $ratio = $this->ratio(); $mode = $ref === 'width'; $this->width = $mode ? $fit : (int)round($fit * $ratio); $this->height = !$mode ? $fit : (int)round($fit / $ratio); return $this; } /** * Recalculates the width and height to fit the given width * * * * $dimensions = new Dimensions(1200, 768); * $dimensions->fitWidth(500); * * echo $dimensions->width(); * // output: 500 * * echo $dimensions->height(); * // output: 320 * * * * @param int|null $fit the max width * @param bool $force If true, the dimensions will be * upscaled to fit the box if smaller * @return $this object with recalculated dimensions */ public function fitWidth( int|null $fit = null, bool $force = false ): static { return $this->fitSize('width', $fit, $force); } /** * Recalculates the dimensions by the width and height * * @param int|null $width the max height * @param int|null $height the max width * @return $this */ public function fitWidthAndHeight( int|null $width = null, int|null $height = null, bool $force = false ): static { if ($this->width > $this->height) { $this->fitWidth($width, $force); // do another check for the max height if ($this->height > $height) { $this->fitHeight($height); } } else { $this->fitHeight($height, $force); // do another check for the max width if ($this->width > $width) { $this->fitWidth($width); } } return $this; } /** * Detect the dimensions for an image file */ public static function forImage(string $root): static { if (file_exists($root) === false) { return new static(0, 0); } $size = getimagesize($root); return new static($size[0] ?? 0, $size[1] ?? 1); } /** * Detect the dimensions for a svg file */ public static function forSvg(string $root): static { // avoid xml errors libxml_use_internal_errors(true); $content = file_get_contents($root); $height = 0; $width = 0; $xml = simplexml_load_string($content); if ($xml !== false) { $attr = $xml->attributes(); $rawWidth = $attr->width; $width = (int)$rawWidth; $rawHeight = $attr->height; $height = (int)$rawHeight; // use viewbox values if direct attributes are 0 // or based on percentages if (empty($attr->viewBox) === false) { $box = explode(' ', $attr->viewBox); // when using viewbox values, make sure to subtract // first two box values from last two box values // to retrieve the absolute dimensions if (Str::endsWith($rawWidth, '%') === true || $width === 0) { $width = (int)($box[2] ?? 0) - (int)($box[0] ?? 0); } if (Str::endsWith($rawHeight, '%') === true || $height === 0) { $height = (int)($box[3] ?? 0) - (int)($box[1] ?? 0); } } } return new static($width, $height); } /** * Checks if the dimensions are landscape */ public function landscape(): bool { return $this->width > $this->height; } /** * Returns a string representation of the orientation */ public function orientation(): string|false { if (!$this->ratio()) { return false; } if ($this->portrait() === true) { return 'portrait'; } if ($this->landscape() === true) { return 'landscape'; } return 'square'; } /** * Checks if the dimensions are portrait */ public function portrait(): bool { return $this->height > $this->width; } /** * Calculates and returns the ratio * * * * $dimensions = new Dimensions(1200, 768); * echo $dimensions->ratio(); * // output: 1.5625 * * */ public function ratio(): float { if ($this->width !== 0 && $this->height !== 0) { return $this->width / $this->height; } return 0.0; } /** * Resizes image * @return $this */ public function resize( int|null $width = null, int|null $height = null, bool $force = false ): static { return $this->fitWidthAndHeight($width, $height, $force); } /** * Checks if the dimensions are square */ public function square(): bool { return $this->width === $this->height; } /** * Resize and crop * * @return $this */ public function thumb(array $options = []): static { $width = $options['width'] ?? null; $height = $options['height'] ?? null; $crop = $options['crop'] ?? false; $method = $crop !== false ? 'crop' : 'resize'; if ($width === null && $height === null) { return $this; } return $this->$method($width, $height); } /** * Converts the dimensions object * to a plain PHP array */ public function toArray(): array { return [ 'width' => $this->width(), 'height' => $this->height(), 'ratio' => $this->ratio(), 'orientation' => $this->orientation(), ]; } /** * Returns the width */ public function width(): int { return $this->width; } }