Initial commit

This commit is contained in:
Paul Nicoué 2021-10-29 18:05:46 +02:00
commit 1ff19bf38f
830 changed files with 159212 additions and 0 deletions

View file

@ -0,0 +1,93 @@
<?php
namespace Kirby\Image;
/**
* Small class which hold info about the camera
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class Camera
{
/**
* Make exif data
*
* @var string|null
*/
protected $make;
/**
* Model exif data
*
* @var string|null
*/
protected $model;
/**
* Constructor
*
* @param array $exif
*/
public function __construct(array $exif)
{
$this->make = $exif['Make'] ?? null;
$this->model = $exif['Model'] ?? null;
}
/**
* Returns the make of the camera
*
* @return string
*/
public function make(): ?string
{
return $this->make;
}
/**
* Returns the camera model
*
* @return string
*/
public function model(): ?string
{
return $this->model;
}
/**
* Converts the object into a nicely readable array
*
* @return array
*/
public function toArray(): array
{
return [
'make' => $this->make,
'model' => $this->model
];
}
/**
* Returns the full make + model name
*
* @return string
*/
public function __toString(): string
{
return trim($this->make . ' ' . $this->model);
}
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return $this->toArray();
}
}

View file

@ -0,0 +1,145 @@
<?php
namespace Kirby\Image;
use Exception;
/**
* A wrapper around resizing and cropping
* via GDLib, ImageMagick or other libraries.
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class Darkroom
{
public static $types = [
'gd' => 'Kirby\Image\Darkroom\GdLib',
'im' => 'Kirby\Image\Darkroom\ImageMagick'
];
protected $settings = [];
/**
* Darkroom constructor
*
* @param array $settings
*/
public function __construct(array $settings = [])
{
$this->settings = array_merge($this->defaults(), $settings);
}
/**
* Creates a new Darkroom instance for the given
* type/driver
*
* @param string $type
* @param array $settings
* @return mixed
* @throws \Exception
*/
public static function factory(string $type, array $settings = [])
{
if (isset(static::$types[$type]) === false) {
throw new Exception('Invalid Darkroom type');
}
$class = static::$types[$type];
return new $class($settings);
}
/**
* Returns the default thumb settings
*
* @return array
*/
protected function defaults(): array
{
return [
'autoOrient' => true,
'crop' => false,
'blur' => false,
'grayscale' => false,
'height' => null,
'quality' => 90,
'width' => null,
];
}
/**
* Normalizes all thumb options
*
* @param array $options
* @return array
*/
protected function options(array $options = []): array
{
$options = array_merge($this->settings, $options);
// normalize the crop option
if ($options['crop'] === true) {
$options['crop'] = 'center';
}
// normalize the blur option
if ($options['blur'] === true) {
$options['blur'] = 10;
}
// normalize the greyscale option
if (isset($options['greyscale']) === true) {
$options['grayscale'] = $options['greyscale'];
unset($options['greyscale']);
}
// normalize the bw option
if (isset($options['bw']) === true) {
$options['grayscale'] = $options['bw'];
unset($options['bw']);
}
if ($options['quality'] === null) {
$options['quality'] = $this->settings['quality'];
}
return $options;
}
/**
* Calculates the dimensions of the final thumb based
* on the given options and returns a full array with
* all the final options to be used for the image generator
*
* @param string $file
* @param array $options
* @return array
*/
public function preprocess(string $file, array $options = [])
{
$options = $this->options($options);
$image = new Image($file);
$dimensions = $image->dimensions()->thumb($options);
$options['width'] = $dimensions->width();
$options['height'] = $dimensions->height();
return $options;
}
/**
* This method must be replaced by the driver to run the
* actual image processing job.
*
* @param string $file
* @param array $options
* @return array
*/
public function process(string $file, array $options = []): array
{
return $this->preprocess($file, $options);
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Kirby\Image\Darkroom;
ini_set('memory_limit', '512M');
use claviska\SimpleImage;
use Kirby\Image\Darkroom;
/**
* GdLib
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class GdLib extends Darkroom
{
/**
* Processes the image with the SimpleImage library
*
* @param string $file
* @param array $options
* @return array
*/
public function process(string $file, array $options = []): array
{
$options = $this->preprocess($file, $options);
$image = new SimpleImage();
$image->fromFile($file);
$image = $this->resize($image, $options);
$image = $this->autoOrient($image, $options);
$image = $this->blur($image, $options);
$image = $this->grayscale($image, $options);
$image->toFile($file, null, $options['quality']);
return $options;
}
/**
* Activates the autoOrient option in SimpleImage
* unless this is deactivated
*
* @param \claviska\SimpleImage $image
* @param $options
* @return \claviska\SimpleImage
*/
protected function autoOrient(SimpleImage $image, $options)
{
if ($options['autoOrient'] === false) {
return $image;
}
return $image->autoOrient();
}
/**
* Wrapper around SimpleImage's resize and crop methods
*
* @param \claviska\SimpleImage $image
* @param array $options
* @return \claviska\SimpleImage
*/
protected function resize(SimpleImage $image, array $options)
{
if ($options['crop'] === false) {
return $image->resize($options['width'], $options['height']);
}
return $image->thumbnail($options['width'], $options['height'] ?? $options['width'], $options['crop']);
}
/**
* Applies the correct blur settings for SimpleImage
*
* @param \claviska\SimpleImage $image
* @param array $options
* @return \claviska\SimpleImage
*/
protected function blur(SimpleImage $image, array $options)
{
if ($options['blur'] === false) {
return $image;
}
return $image->blur('gaussian', (int)$options['blur']);
}
/**
* Applies grayscale conversion if activated in the options.
*
* @param \claviska\SimpleImage $image
* @param array $options
* @return \claviska\SimpleImage
*/
protected function grayscale(SimpleImage $image, array $options)
{
if ($options['grayscale'] === false) {
return $image;
}
return $image->desaturate();
}
}

View file

@ -0,0 +1,228 @@
<?php
namespace Kirby\Image\Darkroom;
use Exception;
use Kirby\Image\Darkroom;
use Kirby\Toolkit\F;
/**
* ImageMagick
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class ImageMagick extends Darkroom
{
/**
* Activates imagemagick's auto-orient feature unless
* it is deactivated via the options
*
* @param string $file
* @param array $options
* @return string
*/
protected function autoOrient(string $file, array $options)
{
if ($options['autoOrient'] === true) {
return '-auto-orient';
}
}
/**
* Applies the blur settings
*
* @param string $file
* @param array $options
* @return string
*/
protected function blur(string $file, array $options)
{
if ($options['blur'] !== false) {
return '-blur 0x' . $options['blur'];
}
}
/**
* Keep animated gifs
*
* @param string $file
* @param array $options
* @return string
*/
protected function coalesce(string $file, array $options)
{
if (F::extension($file) === 'gif') {
return '-coalesce';
}
}
/**
* Creates the convert command with the right path to the binary file
*
* @param string $file
* @param array $options
* @return string
*/
protected function convert(string $file, array $options): string
{
return sprintf($options['bin'] . ' "%s"', $file);
}
/**
* Returns additional default parameters for imagemagick
*
* @return array
*/
protected function defaults(): array
{
return parent::defaults() + [
'bin' => 'convert',
'interlace' => false,
];
}
/**
* Applies the correct settings for grayscale images
*
* @param string $file
* @param array $options
* @return string
*/
protected function grayscale(string $file, array $options)
{
if ($options['grayscale'] === true) {
return '-colorspace gray';
}
}
/**
* Applies the correct settings for interlaced JPEGs if
* activated via options
*
* @param string $file
* @param array $options
* @return string
*/
protected function interlace(string $file, array $options)
{
if ($options['interlace'] === true) {
return '-interlace line';
}
}
/**
* Creates and runs the full imagemagick command
* to process the image
*
* @param string $file
* @param array $options
* @return array
* @throws \Exception
*/
public function process(string $file, array $options = []): array
{
$options = $this->preprocess($file, $options);
$command = [];
$command[] = $this->convert($file, $options);
$command[] = $this->strip($file, $options);
$command[] = $this->interlace($file, $options);
$command[] = $this->coalesce($file, $options);
$command[] = $this->grayscale($file, $options);
$command[] = $this->autoOrient($file, $options);
$command[] = $this->resize($file, $options);
$command[] = $this->quality($file, $options);
$command[] = $this->blur($file, $options);
$command[] = $this->save($file, $options);
// remove all null values and join the parts
$command = implode(' ', array_filter($command));
// try to execute the command
exec($command, $output, $return);
// log broken commands
if ($return !== 0) {
throw new Exception('The imagemagick convert command could not be executed: ' . $command);
}
return $options;
}
/**
* Applies the correct JPEG compression quality settings
*
* @param string $file
* @param array $options
* @return string
*/
protected function quality(string $file, array $options): string
{
return '-quality ' . $options['quality'];
}
/**
* Creates the correct options to crop or resize the image
* and translates the crop positions for imagemagick
*
* @param string $file
* @param array $options
* @return string
*/
protected function resize(string $file, array $options): string
{
// simple resize
if ($options['crop'] === false) {
return sprintf('-resize %sx%s!', $options['width'], $options['height']);
}
$gravities = [
'top left' => 'NorthWest',
'top' => 'North',
'top right' => 'NorthEast',
'left' => 'West',
'center' => 'Center',
'right' => 'East',
'bottom left' => 'SouthWest',
'bottom' => 'South',
'bottom right' => 'SouthEast'
];
// translate the gravity option into something imagemagick understands
$gravity = $gravities[$options['crop']] ?? 'Center';
$command = sprintf('-resize %sx%s^', $options['width'], $options['height']);
$command .= sprintf(' -gravity %s -crop %sx%s+0+0', $gravity, $options['width'], $options['height']);
return $command;
}
/**
* Makes sure to not process too many images at once
* which could crash the server
*
* @param string $file
* @param array $options
* @return string
*/
protected function save(string $file, array $options): string
{
return sprintf('-limit thread 1 "%s"', $file);
}
/**
* Removes all metadata from the image
*
* @param string $file
* @param array $options
* @return string
*/
protected function strip(string $file, array $options): string
{
return '-strip';
}
}

View file

@ -0,0 +1,430 @@
<?php
namespace Kirby\Image;
/**
* 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 GmbH
* @license https://opensource.org/licenses/MIT
*/
class Dimensions
{
/**
* the height of the parent object
*
* @var int
*/
public $height = 0;
/**
* the width of the parent object
*
* @var int
*/
public $width = 0;
/**
* Constructor
*
* @param int $width
* @param int $height
*/
public function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return $this->toArray();
}
/**
* Echos the dimensions as width × height
*
* @return string
*/
public function __toString(): string
{
return $this->width . ' × ' . $this->height;
}
/**
* Crops the dimensions by width and height
*
* @param int $width
* @param int|null $height
* @return $this
*/
public function crop(int $width, int $height = null)
{
$this->width = $width;
$this->height = $width;
if ($height !== 0 && $height !== null) {
$this->height = $height;
}
return $this;
}
/**
* Returns the height
*
* @return int
*/
public function height()
{
return $this->height;
}
/**
* Recalculates the width and height to fit into the given box.
*
* <code>
*
* $dimensions = new Dimensions(1200, 768);
* $dimensions->fit(500);
*
* echo $dimensions->width();
* // output: 500
*
* echo $dimensions->height();
* // output: 320
*
* </code>
*
* @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)
{
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
*
* <code>
*
* $dimensions = new Dimensions(1200, 768);
* $dimensions->fitHeight(500);
*
* echo $dimensions->width();
* // output: 781
*
* echo $dimensions->height();
* // output: 500
*
* </code>
*
* @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 $fit = null, bool $force = false)
{
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 $fit = null, bool $force = false)
{
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
*
* <code>
*
* $dimensions = new Dimensions(1200, 768);
* $dimensions->fitWidth(500);
*
* echo $dimensions->width();
* // output: 500
*
* echo $dimensions->height();
* // output: 320
*
* </code>
*
* @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 $fit = null, bool $force = false)
{
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
* @param bool $force
* @return $this
*/
public function fitWidthAndHeight(int $width = null, int $height = null, bool $force = false)
{
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
*
* @param string $root
* @return static
*/
public static function forImage(string $root)
{
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
*
* @param string $root
* @return static
*/
public static function forSvg(string $root)
{
// 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();
$width = (float)($attr->width);
$height = (float)($attr->height);
if (($width === 0.0 || $height === 0.0) && empty($attr->viewBox) === false) {
$box = explode(' ', $attr->viewBox);
$width = (float)($box[2] ?? 0);
$height = (float)($box[3] ?? 0);
}
}
return new static($width, $height);
}
/**
* Checks if the dimensions are landscape
*
* @return bool
*/
public function landscape(): bool
{
return $this->width > $this->height;
}
/**
* Returns a string representation of the orientation
*
* @return string|false
*/
public function orientation()
{
if (!$this->ratio()) {
return false;
}
if ($this->portrait()) {
return 'portrait';
}
if ($this->landscape()) {
return 'landscape';
}
return 'square';
}
/**
* Checks if the dimensions are portrait
*
* @return bool
*/
public function portrait(): bool
{
return $this->height > $this->width;
}
/**
* Calculates and returns the ratio
*
* <code>
*
* $dimensions = new Dimensions(1200, 768);
* echo $dimensions->ratio();
* // output: 1.5625
*
* </code>
*
* @return float
*/
public function ratio(): float
{
if ($this->width !== 0 && $this->height !== 0) {
return $this->width / $this->height;
}
return 0;
}
/**
* @param int|null $width
* @param int|null $height
* @param bool $force
* @return $this
*/
public function resize(int $width = null, int $height = null, bool $force = false)
{
return $this->fitWidthAndHeight($width, $height, $force);
}
/**
* Checks if the dimensions are square
*
* @return bool
*/
public function square(): bool
{
return $this->width === $this->height;
}
/**
* Resize and crop
*
* @param array $options
* @return $this
*/
public function thumb(array $options = [])
{
$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
*
* @return array
*/
public function toArray(): array
{
return [
'width' => $this->width(),
'height' => $this->height(),
'ratio' => $this->ratio(),
'orientation' => $this->orientation(),
];
}
/**
* Returns the width
*
* @return int
*/
public function width(): int
{
return $this->width;
}
}

296
kirby/src/Image/Exif.php Normal file
View file

@ -0,0 +1,296 @@
<?php
namespace Kirby\Image;
use Kirby\Toolkit\V;
/**
* Reads exif data from a given image object
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class Exif
{
/**
* the parent image object
* @var Image
*/
protected $image;
/**
* the raw exif array
* @var array
*/
protected $data = [];
/**
* the camera object with model and make
* @var Camera
*/
protected $camera;
/**
* the location object
* @var Location
*/
protected $location;
/**
* the timestamp
*
* @var string
*/
protected $timestamp;
/**
* the exposure value
*
* @var string
*/
protected $exposure;
/**
* the aperture value
*
* @var string
*/
protected $aperture;
/**
* iso value
*
* @var string
*/
protected $iso;
/**
* focal length
*
* @var string
*/
protected $focalLength;
/**
* color or black/white
* @var bool
*/
protected $isColor;
/**
* Constructor
*
* @param \Kirby\Image\Image $image
*/
public function __construct(Image $image)
{
$this->image = $image;
$this->data = $this->read();
$this->parse();
}
/**
* Returns the raw data array from the parser
*
* @return array
*/
public function data(): array
{
return $this->data;
}
/**
* Returns the Camera object
*
* @return \Kirby\Image\Camera|null
*/
public function camera()
{
if ($this->camera !== null) {
return $this->camera;
}
return $this->camera = new Camera($this->data);
}
/**
* Returns the location object
*
* @return \Kirby\Image\Location|null
*/
public function location()
{
if ($this->location !== null) {
return $this->location;
}
return $this->location = new Location($this->data);
}
/**
* Returns the timestamp
*
* @return string|null
*/
public function timestamp()
{
return $this->timestamp;
}
/**
* Returns the exposure
*
* @return string|null
*/
public function exposure()
{
return $this->exposure;
}
/**
* Returns the aperture
*
* @return string|null
*/
public function aperture()
{
return $this->aperture;
}
/**
* Returns the iso value
*
* @return int|null
*/
public function iso()
{
return $this->iso;
}
/**
* Checks if this is a color picture
*
* @return bool|null
*/
public function isColor()
{
return $this->isColor;
}
/**
* Checks if this is a bw picture
*
* @return bool|null
*/
public function isBW(): ?bool
{
return ($this->isColor !== null) ? $this->isColor === false : null;
}
/**
* Returns the focal length
*
* @return string|null
*/
public function focalLength()
{
return $this->focalLength;
}
/**
* Read the exif data of the image object if possible
*
* @return mixed
*/
protected function read(): array
{
if (function_exists('exif_read_data') === false) {
return [];
}
$data = @exif_read_data($this->image->root());
return is_array($data) ? $data : [];
}
/**
* Get all computed data
*
* @return array
*/
protected function computed(): array
{
return $this->data['COMPUTED'] ?? [];
}
/**
* Pareses and stores all relevant exif data
*/
protected function parse()
{
$this->timestamp = $this->parseTimestamp();
$this->exposure = $this->data['ExposureTime'] ?? null;
$this->iso = $this->data['ISOSpeedRatings'] ?? null;
$this->focalLength = $this->parseFocalLength();
$this->aperture = $this->computed()['ApertureFNumber'] ?? null;
$this->isColor = V::accepted($this->computed()['IsColor'] ?? null);
}
/**
* Return the timestamp when the picture has been taken
*
* @return string|int
*/
protected function parseTimestamp()
{
if (isset($this->data['DateTimeOriginal']) === true) {
return strtotime($this->data['DateTimeOriginal']);
}
return $this->data['FileDateTime'] ?? $this->image->modified();
}
/**
* Teturn the focal length
*
* @return string|null
*/
protected function parseFocalLength()
{
return $this->data['FocalLength'] ?? $this->data['FocalLengthIn35mmFilm'] ?? null;
}
/**
* Converts the object into a nicely readable array
*
* @return array
*/
public function toArray(): array
{
return [
'camera' => $this->camera() ? $this->camera()->toArray() : null,
'location' => $this->location() ? $this->location()->toArray() : null,
'timestamp' => $this->timestamp(),
'exposure' => $this->exposure(),
'aperture' => $this->aperture(),
'iso' => $this->iso(),
'focalLength' => $this->focalLength(),
'isColor' => $this->isColor()
];
}
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return array_merge($this->toArray(), [
'camera' => $this->camera(),
'location' => $this->location()
]);
}
}

339
kirby/src/Image/Image.php Normal file
View file

@ -0,0 +1,339 @@
<?php
namespace Kirby\Image;
use Kirby\Exception\Exception;
use Kirby\Http\Response;
use Kirby\Toolkit\File;
use Kirby\Toolkit\Html;
use Kirby\Toolkit\Mime;
use Kirby\Toolkit\V;
/**
* A representation of an image/media file
* with dimensions, optional exif data and
* a connection to our darkroom classes to resize/crop
* images.
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class Image extends File
{
/**
* optional url where the file is reachable
* @var string
*/
protected $url;
/**
* @var \Kirby\Image\Exif|null
*/
protected $exif;
/**
* @var \Kirby\Image\Dimensions|null
*/
protected $dimensions;
/**
* Constructor
*
* @param string|null $root
* @param string|null $url
*/
public function __construct(string $root = null, string $url = null)
{
parent::__construct($root);
$this->url = $url;
}
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return array_merge($this->toArray(), [
'dimensions' => $this->dimensions(),
'exif' => $this->exif(),
]);
}
/**
* Returns a full link to this file
* Perfect for debugging in connection with echo
*
* @return string
*/
public function __toString(): string
{
return $this->root;
}
/**
* Returns the dimensions of the file if possible
*
* @return \Kirby\Image\Dimensions
*/
public function dimensions()
{
if ($this->dimensions !== null) {
return $this->dimensions;
}
if (in_array($this->mime(), ['image/jpeg', 'image/jp2', 'image/png', 'image/gif', 'image/webp'])) {
return $this->dimensions = Dimensions::forImage($this->root);
}
if ($this->extension() === 'svg') {
return $this->dimensions = Dimensions::forSvg($this->root);
}
return $this->dimensions = new Dimensions(0, 0);
}
/*
* Automatically sends all needed headers for the file to be downloaded
* and echos the file's content
*
* @param string|null $filename Optional filename for the download
* @return string
*/
public function download($filename = null): string
{
return Response::download($this->root, $filename ?? $this->filename());
}
/**
* Returns the exif object for this file (if image)
*
* @return \Kirby\Image\Exif
*/
public function exif()
{
if ($this->exif !== null) {
return $this->exif;
}
$this->exif = new Exif($this);
return $this->exif;
}
/**
* Sends an appropriate header for the asset
*
* @param bool $send
* @return \Kirby\Http\Response|string
*/
public function header(bool $send = true)
{
$response = new Response('', $this->mime());
return $send === true ? $response->send() : $response;
}
/**
* Returns the height of the asset
*
* @return int
*/
public function height(): int
{
return $this->dimensions()->height();
}
/**
* @param array $attr
* @return string
*/
public function html(array $attr = []): string
{
return Html::img($this->url(), $attr);
}
/**
* Returns the PHP imagesize array
*
* @return array
*/
public function imagesize(): array
{
return getimagesize($this->root);
}
/**
* Checks if the dimensions of the asset are portrait
*
* @return bool
*/
public function isPortrait(): bool
{
return $this->dimensions()->portrait();
}
/**
* Checks if the dimensions of the asset are landscape
*
* @return bool
*/
public function isLandscape(): bool
{
return $this->dimensions()->landscape();
}
/**
* Checks if the dimensions of the asset are square
*
* @return bool
*/
public function isSquare(): bool
{
return $this->dimensions()->square();
}
/**
* Runs a set of validations on the image object
*
* @param array $rules
* @return bool
* @throws \Exception
*/
public function match(array $rules): bool
{
$rules = array_change_key_case($rules);
if (is_array($rules['mime'] ?? null) === true) {
$mime = $this->mime();
// determine if any pattern matches the MIME type;
// once any pattern matches, `$carry` is `true` and the rest is skipped
$matches = array_reduce($rules['mime'], function ($carry, $pattern) use ($mime) {
return $carry || Mime::matches($mime, $pattern);
}, false);
if ($matches !== true) {
throw new Exception([
'key' => 'file.mime.invalid',
'data' => compact('mime')
]);
}
}
if (is_array($rules['extension'] ?? null) === true) {
$extension = $this->extension();
if (in_array($extension, $rules['extension']) !== true) {
throw new Exception([
'key' => 'file.extension.invalid',
'data' => compact('extension')
]);
}
}
if (is_array($rules['type'] ?? null) === true) {
$type = $this->type();
if (in_array($type, $rules['type']) !== true) {
throw new Exception([
'key' => 'file.type.invalid',
'data' => compact('type')
]);
}
}
$validations = [
'maxsize' => ['size', 'max'],
'minsize' => ['size', 'min'],
'maxwidth' => ['width', 'max'],
'minwidth' => ['width', 'min'],
'maxheight' => ['height', 'max'],
'minheight' => ['height', 'min'],
'orientation' => ['orientation', 'same']
];
foreach ($validations as $key => $arguments) {
$rule = $rules[$key] ?? null;
if ($rule !== null) {
$property = $arguments[0];
$validator = $arguments[1];
if (V::$validator($this->$property(), $rule) === false) {
throw new Exception([
'key' => 'file.' . $key,
'data' => [$property => $rule]
]);
}
}
}
return true;
}
/**
* Returns the ratio of the asset
*
* @return float
*/
public function ratio(): float
{
return $this->dimensions()->ratio();
}
/**
* Returns the orientation as string
* landscape | portrait | square
*
* @return string
*/
public function orientation(): string
{
return $this->dimensions()->orientation();
}
/**
* Converts the media object to a
* plain PHP array
*
* @return array
*/
public function toArray(): array
{
return array_merge(parent::toArray(), [
'dimensions' => $this->dimensions()->toArray(),
'exif' => $this->exif()->toArray(),
]);
}
/**
* Converts the entire file array into
* a json string
*
* @return string
*/
public function toJson(): string
{
return json_encode($this->toArray());
}
/**
* Returns the url
*
* @return string
*/
public function url()
{
return $this->url;
}
/**
* Returns the width of the asset
*
* @return int
*/
public function width(): int
{
return $this->dimensions()->width();
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace Kirby\Image;
/**
* Returns the latitude and longitude values
* for exif location data if available
*
* @package Kirby Image
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class Location
{
/**
* latitude
*
* @var float|null
*/
protected $lat;
/**
* longitude
*
* @var float|null
*/
protected $lng;
/**
* Constructor
*
* @param array $exif The entire exif array
*/
public function __construct(array $exif)
{
if (isset($exif['GPSLatitude']) === true &&
isset($exif['GPSLatitudeRef']) === true &&
isset($exif['GPSLongitude']) === true &&
isset($exif['GPSLongitudeRef']) === true
) {
$this->lat = $this->gps($exif['GPSLatitude'], $exif['GPSLatitudeRef']);
$this->lng = $this->gps($exif['GPSLongitude'], $exif['GPSLongitudeRef']);
}
}
/**
* Returns the latitude
*
* @return float|null
*/
public function lat()
{
return $this->lat;
}
/**
* Returns the longitude
*
* @return float|null
*/
public function lng()
{
return $this->lng;
}
/**
* Converts the gps coordinates
*
* @param string|array $coord
* @param string $hemi
* @return float
*/
protected function gps($coord, string $hemi): float
{
$degrees = count($coord) > 0 ? $this->num($coord[0]) : 0;
$minutes = count($coord) > 1 ? $this->num($coord[1]) : 0;
$seconds = count($coord) > 2 ? $this->num($coord[2]) : 0;
$hemi = strtoupper($hemi);
$flip = ($hemi === 'W' || $hemi === 'S') ? -1 : 1;
return $flip * ($degrees + $minutes / 60 + $seconds / 3600);
}
/**
* Converts coordinates to floats
*
* @param string $part
* @return float
*/
protected function num(string $part): float
{
$parts = explode('/', $part);
if (count($parts) === 1) {
return (float)$parts[0];
}
return (float)($parts[0]) / (float)($parts[1]);
}
/**
* Converts the object into a nicely readable array
*
* @return array
*/
public function toArray(): array
{
return [
'lat' => $this->lat(),
'lng' => $this->lng()
];
}
/**
* Echos the entire location as lat, lng
*
* @return string
*/
public function __toString(): string
{
return trim(trim($this->lat() . ', ' . $this->lng(), ','));
}
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return $this->toArray();
}
}