410 lines
8.6 KiB
PHP
410 lines
8.6 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace Kirby\Image;
|
|||
|
|
|
|||
|
|
use Kirby\Toolkit\Str;
|
|||
|
|
use Stringable;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* The Dimension class is used to provide additional
|
|||
|
|
* methods for images and possibly other objects with
|
|||
|
|
* width and height to recalculate the size,
|
|||
|
|
* get the ratio or just the width and height.
|
|||
|
|
*
|
|||
|
|
* @package Kirby Image
|
|||
|
|
* @author Bastian Allgeier <bastian@getkirby.com>
|
|||
|
|
* @link https://getkirby.com
|
|||
|
|
* @copyright Bastian Allgeier
|
|||
|
|
* @license https://opensource.org/licenses/MIT
|
|||
|
|
*/
|
|||
|
|
class Dimensions implements Stringable
|
|||
|
|
{
|
|||
|
|
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.
|
|||
|
|
*
|
|||
|
|
* ```php
|
|||
|
|
* $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
|
|||
|
|
*
|
|||
|
|
* ```php
|
|||
|
|
* $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
|
|||
|
|
*
|
|||
|
|
* ```php
|
|||
|
|
* $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(Image $image): static
|
|||
|
|
{
|
|||
|
|
if ($image->exists() === false) {
|
|||
|
|
return new static(0, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$orientation = $image->exif()->orientation();
|
|||
|
|
$size = $image->imagesize();
|
|||
|
|
|
|||
|
|
return match ($orientation) {
|
|||
|
|
// 5-8 = rotated
|
|||
|
|
5, 6, 7, 8 => new static($size[1] ?? 1, $size[0] ?? 0),
|
|||
|
|
// 1 = normal; 2-4 = flipped
|
|||
|
|
default => 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
|
|||
|
|
*
|
|||
|
|
* ```php
|
|||
|
|
* $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;
|
|||
|
|
}
|
|||
|
|
}
|