Skip to content

Instantly share code, notes, and snippets.

@trk
Last active March 13, 2020 13:04
Show Gist options
  • Save trk/5aff9268e2b04ad941f60420f69a8066 to your computer and use it in GitHub Desktop.
Save trk/5aff9268e2b04ad941f60420f69a8066 to your computer and use it in GitHub Desktop.
ProcessWire responsive image class
<?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