Last active
March 13, 2020 13:04
-
-
Save trk/5aff9268e2b04ad941f60420f69a8066 to your computer and use it in GitHub Desktop.
ProcessWire responsive image class
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace ProcessWire; | |
/** | |
* HOOKS : IMAGE | |
*/ | |
foreach (["array", "json", "alt", "srcset", "sizes", "thumbnail"] as $index => $hook) { | |
wire()->addHookProperty("Pageimage::{$hook}", function(HookEvent $event) use($hook) { | |
if ($event->object instanceof Pageimage) { | |
$image = new Image($event->object); | |
$event->return = $image->{$hook}(); | |
} | |
}); | |
wire()->addHook("Pageimage::{$hook}" . $hook, function(HookEvent $event) use ($hook) { | |
if ($event->object instanceof Pageimage) { | |
$image = new Image($event->object); | |
$event->return = $image->{$hook}(); | |
} | |
}); | |
} | |
/** | |
* Class Image | |
* | |
* @author : İskender TOTOĞLU, @ukyo (community), @trk (Github) | |
* @website : https://www.altivebir.com | |
* | |
* @property string $type | |
* @property string $placeholder | |
* @property string $alt | |
* @property string $basename | |
* @property string $description | |
* @property string $ext | |
* @property string $filename | |
* @property int $filesize | |
* @property string $filesizeStr | |
* @property string $focusStr | |
* @property boolean $hasFocus | |
* @property string $hash | |
* @property int $height | |
* @property string $httpUrl | |
* @property string $name | |
* @property string $src | |
* @property string $suffixStr | |
* @property string $url | |
* @property int $width | |
* | |
* @method quality(int $quality) | |
* @method webpQuality (int $webpQuality) | |
* @method flip (string $flip) | |
* @method placeholder (string $placeholder) | |
* @method webp (bool $webp) | |
* @method http (bool $http) | |
* @method width (int $width) | |
* @method height (int $height) | |
* @method thumbWidth (int $thumbWidth) | |
* @method thumbHeight (int $thumbHeight) | |
* @method sort (bool $sort) | |
* @method class (string $class) | |
* @method tag (string $tag) | |
* @method test (bool $test) | |
* | |
* @package ProcessWire\Image | |
*/ | |
class Image extends WireData | |
{ | |
/** | |
* @var string | |
*/ | |
private $testURL = "http://placehold.it"; | |
/** | |
* @var array | |
*/ | |
private $types = [ | |
"bmp" => "image/bmp", | |
"gif" => "image/gif", | |
"ico" => "image/vnd.microsoft.icon", | |
"jpeg" => "image/jpeg", | |
"jpg" => "image/jpeg", | |
"png" => "image/png", | |
"svg" => "image/svg+xml", | |
"tif" => "image/tiff", | |
"tiff" => "image/tiff", | |
"webp" => "image/webp" | |
]; | |
/** | |
* @var array | |
*/ | |
private $props = [ | |
"basename", | |
"description", | |
"ext", | |
"filename", | |
"filesize", | |
"filesizeStr", | |
"focusStr", | |
"hasFocus", | |
"hash", | |
"height", | |
"httpUrl", | |
"name", | |
"src", | |
"suffixStr", | |
"url", | |
"width" | |
]; | |
/** | |
* @var array | |
*/ | |
protected $options = [ | |
"quality" => 70, | |
"webpQuality" => 100, | |
"flip" => "", | |
"placeholder" => "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", | |
"webp" => false, | |
"http" => false, | |
"filesize" => 0.7, | |
"width" => 1600, | |
"height" => null, | |
"thumbWidth" => 0, | |
"thumbHeight" => 260, | |
"sort" => true, | |
"class" => "", | |
"tag" => "img", | |
"test" => false | |
]; | |
/** | |
* @var Pageimage|null | |
*/ | |
protected $image = null; | |
/** | |
* @var array | |
*/ | |
protected $widths; | |
/** | |
* @inheritDoc | |
* | |
* @param Pageimage|null $image | |
*/ | |
public function __construct(Pageimage $image = null) | |
{ | |
if ($image instanceof Pageimage) { | |
foreach ($this->props as $name) { | |
if (isset($image->{$name})) { | |
if (isset($image->$name)) { | |
// set filetype by using image extension | |
if (in_array($name, ["ext"]) && isset($this->types[$image->$name])) { | |
$this->set("type", $this->types[$image->$name]); | |
} | |
if (in_array($name, ["filesize", "height", "width"])) { | |
$value = (int) $image->$name; | |
} else if (in_array($name, ["hasFocus"])) { | |
$value = (bool) $image->$name; | |
} else { | |
$value = (string) $image->$name; | |
} | |
$this->set($name, $value); | |
} | |
} | |
} | |
$this->set("alt", $image->description ?: str_replace('.' . $image->ext, '', $image->basename)); | |
$this->image = $image; | |
} | |
} | |
/** | |
* @inheritDoc | |
* | |
* @param Pageimage|null $image | |
* | |
* @return $this | |
*/ | |
public function __invoke($image = null) | |
{ | |
if ($image instanceof Pageimage) { | |
return new Image($image); | |
} | |
return $this; | |
} | |
/** | |
* @inheritDoc | |
* | |
* @param string $name | |
* @param array $arguments | |
* @return $this | |
*/ | |
public function __call($name, $arguments) | |
{ | |
if (is_string($name) && is_array($arguments)) { | |
if (array_key_exists($name, $this->options) && count($arguments)) { | |
$this->options[$name] = $arguments[0]; | |
} | |
} | |
return $this; | |
} | |
/** | |
* Return Pageimage | |
* | |
* @return Pageimage | |
*/ | |
public function getImage() | |
{ | |
return $this->image; | |
} | |
/** | |
* @return void | |
*/ | |
public function removeVariations() | |
{ | |
$this->image->removeVariations(); | |
return $this; | |
} | |
/** | |
* Return admin thumbnail | |
* | |
* @return Image | |
*/ | |
public function thumbnail() | |
{ | |
$thumbnail = $this->image->size($this->options["thumbWidth"], $this->options["thumbHeight"]); | |
return new Image($thumbnail); | |
} | |
/** | |
* Build srcset | |
* | |
* @return string | |
*/ | |
public function srcset(): string | |
{ | |
$keys = array_keys($this->widths()); | |
$items = array_map(function (int $width) { | |
$image = $this->image->width($width, null, $this->options); | |
$height = $image->height; | |
$width = $image->width; | |
if ($this->options["test"]) { | |
return "{$this->testURL}/{$width}x{$height} {$width}w"; | |
} else { | |
if ($this->options["webp"] && is_callable('imagewebp')) { | |
$image = $image->webp(); | |
} | |
$src = $this->options["http"] ? $image->httpUrl() : $image->url(); | |
return "{$src} {$width}w"; | |
} | |
}, $this->widths(), $keys); | |
/* | |
if ($this->options["placeholder"]) { | |
$items = [null => $this->options["placeholder"]] + $items; | |
} | |
*/ | |
$items = implode(", ", $items); | |
return $items; | |
} | |
/** | |
* Get maximum aspect ratio | |
* | |
* @return string | |
*/ | |
public function sizes(): string | |
{ | |
$widths = $this->calculateWidths(); | |
$breakpoint = $this->width; | |
if (isset($widths[0])) { | |
$width = $widths[0]; | |
} else { | |
$width = $this->width; | |
} | |
$ratio = round($width / ($breakpoint / 100)); | |
return "(max-width: {$breakpoint}px) {$ratio}vw, {$width}px"; | |
} | |
/** | |
* Get widths as array | |
* | |
* @return array | |
*/ | |
public function widths(): array | |
{ | |
$widths = $this->calculateWidths(); | |
if ($this->options["sort"]) { | |
sort($widths); | |
} | |
return $widths; | |
} | |
/** | |
* Calculate image widths | |
* | |
* @return array | |
*/ | |
protected function calculateWidths(): array | |
{ | |
$width = $this->options["width"] ?: $this->width; | |
$height = $this->options["height"] ?: $this->height; | |
$targetWidths = []; | |
array_push($targetWidths, $width); | |
$ratio = $height / $width; | |
$area = $height * $width; | |
$predictedFileSize = $this->filesize; | |
$pixelPrice = $predictedFileSize / $area; | |
while (true) { | |
$predictedFileSize *= $this->options["filesize"] ?: 0.5; | |
$newWidth = (int) floor(sqrt(($predictedFileSize / $pixelPrice) / $ratio)); | |
if ($this->finishedCalculating($predictedFileSize, $newWidth)) { | |
return $targetWidths; | |
} | |
array_push($targetWidths, $newWidth); | |
} | |
return $targetWidths; | |
} | |
/** | |
* Finish width calculations | |
* | |
* @param integer $predictedFileSize | |
* @param integer $newWidth | |
* @return boolean | |
*/ | |
protected function finishedCalculating(int $predictedFileSize, int $newWidth): bool | |
{ | |
if ($newWidth < 20) { | |
return true; | |
} | |
if ($predictedFileSize < (1024 * 10)) { | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Return image url | |
* | |
* @return string | |
*/ | |
public function getURL($placeholder = false): string | |
{ | |
$url = $this->options["http"] ? $this->get("httpUrl") : $this->get("url"); | |
if ($this->options["test"]) { | |
$url = "{$this->testURL}/{$this->image->width}x{$this->image->height}?text=original"; | |
} | |
if ($this->options["placeholder"] && $placeholder) { | |
$url = $this->options["placeholder"]; | |
} | |
return $url; | |
} | |
/** | |
* Return as image html element | |
* | |
* @return string | |
*/ | |
public function render(): string | |
{ | |
$class = $this->options["class"] ? " class='{$this->options['class']}'" : ""; | |
$output = ""; | |
switch ($this->options["tag"]) { | |
case "picture": | |
$output .= "<picture>"; | |
$output .= "<source media='{$this->sizes()}' srcset='{$this->srcset()}'>"; | |
$output .= "<img src='{$this->getURL(true)}' alt='{$this->image->alt}'>"; | |
$output .= "</picture>"; | |
break; | |
default; | |
$output = "<img{$class} src='{$this->getURL(true)}' sizes='{$this->sizes()}' data-srcset='{$this->srcset()}' data-src='{$this->getURL()}' alt='{$this->image->alt}' data-uk-img>"; | |
} | |
return $output; | |
} | |
/** | |
* Return image alt attribute | |
* | |
* @return string | |
*/ | |
public function alt(): string | |
{ | |
return $this->get("alt"); | |
} | |
/** | |
* Return data as array | |
* | |
* @return array | |
*/ | |
public function array(): array | |
{ | |
$data = $this->data; | |
$data["class"] = $this->options["class"] ? " class='{$this->options['class']}'" : ""; | |
$data["url"] = $this->getURL(); | |
$data["sizes"] = $this->sizes(); | |
$data["srcset"] = $this->srcset(); | |
return $data; | |
} | |
/** | |
* Return data as string | |
* | |
* @return string | |
*/ | |
public function json(): string | |
{ | |
return json_encode($this->array()); | |
} | |
/** | |
* Render | |
* | |
* @return string | |
*/ | |
public function __toString(): string | |
{ | |
return $this->render(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment