diff --git a/README.md b/README.md index 22e68d1..3e53781 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,21 @@ This performs many things: * Push resources (require http 2 protocol). you can configure which resources will be pushed * Efficiently cache resources using http caching headers. This requires apache mod_rewite. I have not tested on other web servers * Range requests are supported for cached resources +* Insert scripts and css that have 'data-position="head"' attribute in head instead of the body +* force script and css to be ignored by the optimizer by setting 'data-ignore="true"' attribute +* connect to domains faster: automatically detect domains and add < link rel=preconnect > -1. Insert scripts and css that have 'data-position="head"' attribute in head instead of the body +# Images + +* deliver images in webp format when the browser signals it supports it +* generate responsive images automatically + +## Responsive images + +* automatically add srcset and sizes for images. Only necessary images are generated. Images smaller that the breakpoint are ignored. +* resize and crop images using a one of these methods (face detection, entropy, center or default). +* configure breakpoints used to create smaller images +* scrset images url is automatically rewritten when http cache is enabled # Javascript Improvements @@ -85,19 +98,12 @@ Add routes to customize fetch event networking startegy by using either a static 1. resize images for mobile / tablet and leverage < img srcset > (even in css?) 1. serve webp whenever the browser supports it 1. resize images for mobile / tablet in css +1. IMAGES: Implement progressive images loading with intersectionObserver [here](https://jmperezperez.com/medium-image-progressive-loading-placeholder/) and async decoding see [here](https://medium.com/dailyjs/image-loading-with-image-decode-b03652e7d2d2) 1. prerender + [Page Visibility API](http://www.w3.org/TR/page-visibility/): how should prender links be chosen? -1. IMAGES: read this [here](https://kinsta.com/blog/optimize-images-for-web/) 1. Service worker cache expiration api (using localforage or a lightweight indexDb library) -1. IMAGES: Implement progressive images loading with intersectionObserver [here](https://jmperezperez.com/medium-image-progressive-loading-placeholder/) -1. IMAGES: Implement images delivery optimization see [here](https://www.smashingmagazine.com/2017/04/content-delivery-network-optimize-images/) and [here](https://developers.google.com/web/updates/2015/09/automating-resource-selection-with-client-hints) -1. IMAGES: Implement support for element see [here] 1. Background Sync see [here](https://developers.google.com/web/updates/2015/12/background-sync) 1. Messaging API (broadcasting messages to and from all/single clients) 1. Remove < Link rel=preload > http header and use < link > HTML tag instead. see [here](https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/) -1. IMAGES: read this [here](https://kinsta.com/blog/optimize-images-for-web/) -1. IMAGES: Implement progressive images loading with intersectionObserver [here](https://jmperezperez.com/medium-image-progressive-loading-placeholder/) -1. IMAGES: Implement images delivery optimization see [here](https://www.smashingmagazine.com/2017/04/content-delivery-network-optimize-images/) and [here](https://developers.google.com/web/updates/2015/09/automating-resource-selection-with-client-hints) -1. IMAGES: Implement support for element see [here](https://www.smashingmagazine.com/2013/10/automate-your-responsive-images-with-mobify-js/) 1. CORS for PWA:https://filipbech.github.io/2017/02/service-worker-and-caching-from-other-origins | https://developers.google.com/web/updates/2016/09/foreign-fetch | https://stackoverflow.com/questions/35626269/how-to-use-service-worker-to-cache-cross-domain-resources-if-the-response-is-404 1. CSS: deduplicate, merge properties, rewrite rules, etc 1. Disk quota management see [here](https://developer.chrome.com/apps/offline_storage) and [here](https://developer.mozilla.org/fr/docs/Web/API/API_IndexedDB/Browser_storage_limits_and_eviction_criteria) and [here](https://gist.github.com/ebidel/188a513b1cd5e77d4d1453a4b6d060b0) @@ -106,18 +112,19 @@ Add routes to customize fetch event networking startegy by using either a static ## Low priority list -1. Fetch remote resources periodically (css and javascript). right now they are updated only once. +1. Fetch remote resources periodically (configurable) (css and javascript). right now they are updated only once. 1. Manage the service worker settings from the front end (unregister, delete cache, etc ...)? 1. Manage user push notification subscription from the Joomla backend (link user to his Id, etc ...)? 1. Provide push notification endpoints (get user Id, notification clicked, notification closed, etc ...) 1. Mobile apps deep link? 1. PWA: Deep links in pwa app or website. see [here](http://blog.teamtreehouse.com/registering-protocol-handlers-web-applications) and [here](https://developer.mozilla.org/en-US/docs/Web-based_protocol_handlers) -1. Integrate https://www.xarg.org/project/php-facedetect/ and https://onthe.io/learn/en/category/analytic/How-to-detect-face-in-image-with-PHP for better image optimization ? # Change History ## V2.2 +1. remove '+' '=' and ',' from the hash generation alphabet +1. Add responsive image support 1. Disabling service worker will actually uninstall it 1. Server Timing Header see [here](https://w3c.github.io/server-timing/#examples) 1. automatic preconnect < link > added, web fonts preload moved closer to < head > for faster font load diff --git a/gzip.xml b/gzip.xml index 11c6fbf..6247e36 100644 --- a/gzip.xml +++ b/gzip.xml @@ -140,7 +140,15 @@ + + + + + + + diff --git a/helper.php b/helper.php index 8cd0481..ee0b825 100644 --- a/helper.php +++ b/helper.php @@ -352,6 +352,7 @@ public static function parseURLs($body, array $options = []) { $replace[$hash] = $matches[0]; return $hash; + }, $body); $body = preg_replace_callback('#(]*)?>)(.*?)#s', function ($matches) use(&$replace) { @@ -402,10 +403,23 @@ public static function parseURLs($body, array $options = []) { $attr = strtolower($matches[2]); - if (isset($options['parse_url_attr'][$attr])) { + if ($attr == 'srcset') { + + $return = []; + + foreach (explode(',', $matches[6]) as $chunk) { + + $parts = explode(' ', $chunk); + + $name = trim($parts[0]); + + $return[] = (static::isFile($name) ? static::url($name) : $name).' '.$parts[1]; + } + + return ' '.$attr.'="'.implode(',', $return); + } - // case 'src': - // case 'href': + if (isset($options['parse_url_attr'][$attr])) { $file = static::getName($matches[6]); @@ -423,8 +437,6 @@ public static function parseURLs($body, array $options = []) { $name = preg_replace('~[#?].*$~', '', $file); - // if (is_file($name)) { - $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); $push_data = empty($types) ? false : static::canPush($name, $ext); @@ -1218,7 +1230,7 @@ public static function getName($name) { return preg_replace(static::$regReduce, '', $name); } - public static function shorten($id, $alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-@+=,') { + public static function shorten($id, $alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-@') { $base = strlen($alphabet); $short = ''; @@ -1918,8 +1930,82 @@ public static function parseImages($body, array $options = []) { $file = $newFile; } } - } - + + // responsive images? + if (!empty($options['imageresize']) && !empty($options['sizes']) && empty($attributes['srcset'])) { + + // build mq based on actual image size + $maxwidth = $sizes[0]; + $mq = array_filter ($options['sizes'], function ($size) use($maxwidth) { + + return $size < $maxwidth; + }); + + // we need to resize the images + if (!empty($mq)) { + + $image = null; + $resource = null; + $method = empty($options['imagesresizestrategy']) ? 'CROP_CENTER' : $options['imagesresizestrategy']; + $const = constant('\Image\Image::'.$method); + $hash = sha1($file); + $short_name = strtolower(str_replace('CROP_', '', $method)); + $crop = $path.$hash.'-'. $short_name.'-'.basename($file); + + $images = array_map(function ($size) use($file, $hash, $short_name, $path) { + + return $path.$hash.'-'.$short_name.'-'.$size.'-'.basename($file); + + }, $mq); + + $srcset = []; + + foreach ($images as $k => $img) { + + if (!\is_file($img)) { + + if (\is_null($image)) { + + if (!\is_file($crop)) { + + $image = new \Image\Image($file); + + // resize image to use less memory + if ($maxwidth > 1200) { + + $image->setSize(1200); + } + + $image->resizeAndCrop($mq[$k], null, $method)->save($crop); + } + + else { + + $image = new \Image\Image($crop); + } + } + + $image->setSize($mq[$k])->save($img); + } + + $srcset[] = $img.' '.$mq[$k].'w'; + } + + $srcset[] = $file.' '.$sizes[0].'w'; + $maxwidth = end($mq) + 1; + + $mq = array_map(function ($size) { + + return '(max-width: '.$size.'px)'; + }); + + $mq[] = '(min-width: '.$maxwidth.'px)'; + + $attributes['srcset'] = implode(',', $srcset); + $attributes['sizes'] = implode(',', $mq); + } + } + } if (!isset($attributes['alt'])) { @@ -1930,13 +2016,12 @@ public static function parseImages($body, array $options = []) { return $key .= '="'.$value.'"'; - }, $attributes, array_keys($attributes))). - '>'; + }, + $attributes, array_keys($attributes))).'>'; } return $matches[0]; }, $body); - // } return $body; } diff --git a/language/en-GB/en-GB.plg_system_gzip.ini b/language/en-GB/en-GB.plg_system_gzip.ini index 5bdacb5..2485978 100644 --- a/language/en-GB/en-GB.plg_system_gzip.ini +++ b/language/en-GB/en-GB.plg_system_gzip.ini @@ -105,6 +105,14 @@ PLG_GZIP_FIELD_IMAGE_RESIZE_DESCRIPTION="" PLG_GZIP_FIELD_IMAGE_SIZES_LABEL="Responsive Image Sizes" PLG_GZIP_FIELD_IMAGE_SIZES_DESCRIPTION="" +PLG_GZIP_FIELD_IMAGE_CROP_STRATEGY_LABEL="Crop method" +PLG_GZIP_FIELD_IMAGE_CROP_STRATEGY_DESCRIPTION="" + +JOPTION_IMAGE_RESIZE_CROP_DEFAULT="Default" +JOPTION_IMAGE_RESIZE_CROP_CENTER="Center" +JOPTION_IMAGE_RESIZE_CROP_ENTROPY="Entropy" +JOPTION_IMAGE_RESIZE_CROP_FACE="Face detection" + PLG_GZIP_FIELD_PWA_ENABLED_LABEL="Enable PWA" PLG_GZIP_FIELD_PWA_ENABLED_DESCRIPTION="" diff --git a/lib/Image/Image.php b/lib/Image/Image.php index 7ef86e7..ebdd117 100644 --- a/lib/Image/Image.php +++ b/lib/Image/Image.php @@ -7,13 +7,16 @@ * @author abidibo abidibo@gmail.com * * @author Thierry Bela - * extended to add face detection and namespacing - - + * extended to add face detection and namespacing, better crop area detection */ namespace Image; - + + // php 7.1 + if (!defined('IMAGETYPE_WEBP') && function_exists('imagecreatefromwebp')) { + + define('IMAGETYPE_WEBP', 'webp'); + } /** * @brief Classe per il trattamento di immagini @@ -24,21 +27,16 @@ class Image { private $_abspath; - private $_image; - private $_width; - private $_height; + private $__image; + // private $_width; + // private $_height; private $_image_type; const CROP_DEFAULT = 1; const CROP_CENTER = 2; const CROP_ENTROPY = 3; - const CROP_FACE = 4; - - const IMAGE_JPEG = IMAGETYPE_JPEG; - const IMAGE_GIF = IMAGETYPE_GIF; - const IMAGE_PNG = IMAGETYPE_PNG; - const IMAGE_WEBP = IMAGETYPE_WEBP; - + const CROP_FACE = 4; + /** * @brief Costruttore * @param string $abspath percorso assoluto del file @@ -51,21 +49,22 @@ public function __construct($abspath) { $image_info = getimagesize($abspath); $this->_abspath = $abspath; - $this->_width = $image_info[0]; - $this->_height = $image_info[1]; + // $this->_width = $image_info[0]; + // $this->_height = $image_info[1]; $this->_image_type = $image_info[2]; - + if($this->_image_type == IMAGETYPE_JPEG) { $this->_image = imagecreatefromjpeg($abspath); } - elseif($this->_type == IMAGETYPE_GIF) { + elseif($this->_image_type == IMAGETYPE_GIF) { $this->_image = imagecreatefromgif($abspath); } - elseif($this->_type == IMAGETYPE_PNG) { + elseif($this->_image_type == IMAGETYPE_PNG) { $this->_image = imagecreatefrompng($abspath); } - elseif($this->_type == IMAGETYPE_WEBP) { + elseif(function_exists('imagecreatefromwebp') && strtolower(pathinfo($abspath, PATHINFO_EXTENSION)) == 'webp') { $this->_image = imagecreatefromwebp($abspath); + $this->_image_type = IMAGETYPE_WEBP; } else { throw new InvalidArgumentException ('Unsupported image type', 400); @@ -99,6 +98,17 @@ public function getResource() { return $this->_image; } + /** + * + * + * @param resource $image + * @return void + */ + public function setResource($image) { + $this->_image = $image; + return $this; + } + /** * @brief Salva l'immagine su filesystem * @param string $abspath percorso (default il percorso originale dell'immagine) @@ -106,46 +116,63 @@ public function getResource() { * @param string $permission permessi * @return void */ - public function save($abspath = null, $type = NULL, $compression=75) { -// - // if(is_null($abspath)) { - // $abspath = $this->_abspath; - // } - - if (is_null($type)) { - - $type = $this->_image_type; - } - - switch ($type) { - - case IMAGETYPE_JPEG: + public function save($abspath = null, $compression=75) { + + $type = $this->_image_type; + + if (!is_null($abspath)) { + + $extension = strtolower(pathinfo($abspath, PATHINFO_EXTENSION)); - imagejpeg($this->_image, $abspath, $compression); - break; + switch($extension) { + + case 'jpg': + + $type = IMAGETYPE_JPEG; + break; + case 'png': + + $type = IMAGETYPE_PNG; + break; + case 'gif': + + $type = IMAGETYPE_GIF; + break; + case 'webp': - case IMAGETYPE_GIF: + if (defined('IMAGETYPE_WEBP')) { + + $type = IMAGETYPE_WEBP; + + break; + } - imagegif($this->_image, $abspath); - break; + default: - case IMAGETYPE_PNG: + throw new \InvalidArgumentException('Unsupported file type '.$extension, 400); + } + } - imagepng($this->_image, $abspath); - break; + if($type == IMAGETYPE_JPEG) { + imagejpeg($this->_image, $abspath, $compression); + } + elseif($type == IMAGETYPE_GIF) { + imagegif($this->_image, $abspath); + } + elseif($type == IMAGETYPE_PNG) { + imagepng($this->_image, $abspath); + } + elseif($type == IMAGETYPE_WEBP) { + imagewebp($this->_image, $abspath, $compression); + } + } - case IMAGETYPE_WEBP: + public function resizeAndCrop($width, $height = null, $method = Image::CROP_DEFAULT, $x0 = 0, $y0 = 0, $options = array()) { - imagewebp($this->_image, $abspath); - break; + if (\is_null($height)) { - default: - - throw new \InvalidArgumentException('Invalid image type specified '.$type, 400); + $height = $this->getHeight() * $width / $this->getWidth(); } - } - - public function resizeAndCrop($width, $height, $method = Image::CROP_DEFAULT, $x0 = 0, $y0 = 0, $options = array()) { switch ($method) { @@ -154,9 +181,9 @@ public function resizeAndCrop($width, $height, $method = Image::CROP_DEFAULT, $x $this->cropEntropy($width, $height, $options); break; - case Image::CROP_CENTER: + case Image::CROP_FACE: - $this->cropCenter($width, $height, $options); + $this->cropFace($width, $height, $options); break; case Image::CROP_CENTER: @@ -183,7 +210,18 @@ public function resizeAndCrop($width, $height, $method = Image::CROP_DEFAULT, $x * @return void */ public function crop($width, $height, $x0, $y0, $options = array()) { - $this->_image = $this->cropImage($this->_image, $width, $height, $x0, $y0, $options); + + + $size = $this->getBestFit($width, $height, $width, $height); + + $x0 = max(0, $x0 - $size['width'] / 2); + $y0 = max(0, $y0 - $size['height'] / 2); + + $x0 = min($x0, $this->getWidth() - $size['width']); + $y0 = min($y0, $this->getHeight() - $size['width']); + + $this->_image = $this->cropImage($this->_image, $size['width'], $size['height'], $x0, $y0, $options); + $this->setSize($width, $height, $options ); } /** @@ -194,11 +232,48 @@ public function crop($width, $height, $x0, $y0, $options = array()) { * @return void */ public function cropCenter($width, $height, $options = array()) { - $x0 = (imagesx($this->_image) - $width)/2; - $y0 = (imagesy($this->_image) - $height)/2; - $this->_image = $this->cropImage($this->_image, $width, $height, $x0, $y0, $options); + + $size = $this->getBestFit($width, $height, $width, $height); + + $x0 = ($this->getWidth() - $size['width'])/2; + $y0 = ($this->getHeight() - $size['height'])/2; + + $this->_image = $this->cropImage($this->_image, $size['width'], $size['height'], $x0, $y0, $options); + $this->setSize($width, $height, $options ); } + protected function getBestFit($width, $height, $regionWidth, $regionHeight) { + + // ($this->faceRects); + + // get best crop size + $scale = $width > $height ? $height / $width : $width / $height; + + // if ($crX != $crY) { + $side = min($this->getWidth(), $this->getHeight()); // * $scale; + + // crop the image with our region inside + $newWidth = $width > $height ? $side : $side * $scale; + $newHeight = $width > $height ? $side * $scale : $side; +// / max($rect['x'], $rect['y']); + // } + + $scale = min($newWidth, $newHeight) / max($regionWidth, $regionHeight); + + return ['width' => $newWidth, 'height' => $newHeight]; + + // var_dump($scale); + + // var_dump(['$crX' => $crX, '$crY' => $crY, '$scale' => $scale, '$width' => $width, '$height' => $height, '$zoneW' => $zoneW, '$zoneH' => $zoneH]); + + // $x0 = max(0, $rect['x'] + ($rect['width'] - $crX) / 2); + // $y0 = max(0, $rect['y'] + ($rect['height'] - $crY) / 2); + + // return ['$scale' => $scale, 'x' => max(0, $rect['x'] + ($rect['width'] - $crX) / 2), 'y' => max(0, $rect['y'] + ($rect['height'] - $crY) / 2), $crX, $crY]; + + +} + public function cropFace($width, $height, $options = array()) { $this->detectFaces(); @@ -211,17 +286,80 @@ public function cropFace($width, $height, $options = array()) { else { // x, y width, height - $rect = array_shift($this->faceRects); - - $x0 = max(0, $rect['x']+ ($rect['width'] - $width) / 2); //; - $y0 = max(0, $rect['y'] + ($rect['height'] - $height) / 2); - - $this->_image = $this->cropImage($this->_image, $width, $height, $x0, $y0, $options); + $x0 = null; + $y0 = null; + $x1 = null; + $y1 = null; + + foreach($this->faceRects as $rect) { + + if (is_null($x0) || $x0 > $rect['x']) { + + $x0 = $rect['x']; + } + + if (is_null($y0) || $y0 > $rect['y']) { + + $y0 = $rect['y']; + } + + if (is_null($x1) || $x1 < $rect['x'] + $rect['width']) { + + $x1 = $rect['x'] + $rect['width']; + } + + if (is_null($y1) || $y1 < $rect['y'] + $rect['height']) { + + $y1 = $rect['y'] + $rect['height']; + } + } + + + $rect = ['x' => $x0, 'width' => $x1 - $x0, 'y' => $y0, 'height' => $y1 - $y0]; + + if ($rect['y'] > 500) { + + $rect['y'] -= 500; + // $rect['width'] += 200; + } + + if ($rect['y'] > 300) { + + $rect['y'] -= 300; + // $rect['width'] += 200; + } + + else if ($rect['y'] > 200) { + + $rect['y'] -= 200; + // $rect['width'] += 200; + } + else { + + $rect['y'] = 0; + // $rect['width'] += 200; + } + + + $size = $this->getBestFit($width, $height, $rect['width'], $rect['width']); + + $x0 = max(0, $rect['x'] + ($rect['width'] - $size['width']) / 2); + $y0 = max(0, $rect['y'] + ($rect['height'] - $size['height']) / 2); + + $x0 = min($x0, $this->getWidth() - $size['width']); + $y0 = min($y0, $this->getHeight() - $size['width']); + + // $r1 = $rect['width'] / $rect['height']; + // $r2 = $width / $height; + + // make this rect feat into a scaled rectangle with width / height dimensions + + // crop and resize + $this->_image = $this->cropImage($this->_image, $size['width'], $size['height'], $x0, $y0, $options); // compute rect that contains all the faces } - // $x0 = (imagesx($this->_image) - $width)/2; - // $y0 = (imagesy($this->_image) - $height)/2; - // $this->_image = $this->cropImage($this->_image, $width, $height, $x0, $y0, $options); + + $this->setSize($width, $height, $options ); } /** @@ -233,6 +371,7 @@ public function cropFace($width, $height, $options = array()) { */ public function cropEntropy($width, $height, $options = array()) { $this->_image = $this->cropImageEntropy($this->_image, $width, $height, $options); + $this->setSize($width, $height, $options ); } protected function detectFaces() { @@ -244,16 +383,19 @@ protected function detectFaces() { $this->faceRects = []; - $maxScale = min($this->_width/$this->classifierSize[0], $this->_height/$this->classifierSize[1]); - $grayImage = array_fill(0, $this->_width, array_fill(0, $this->_height, null)); - $img = array_fill(0, $this->_width, array_fill(0, $this->_height, null)); - $squares = array_fill(0, $this->_width, array_fill(0, $this->_height, null)); + $width = $this->getWidth(); + $height = $this->getHeight(); + + $maxScale = min($width/$this->classifierSize[0], $height/$this->classifierSize[1]); + $grayImage = array_fill(0, $width, array_fill(0, $height, null)); + $img = array_fill(0, $width, array_fill(0, $height, null)); + $squares = array_fill(0, $width, array_fill(0, $height, null)); - for($i = 0; $i < $this->_width; $i++) + for($i = 0; $i < $width; $i++) { $col=0; $col2=0; - for($j = 0; $j < $this->_height; $j++) + for($j = 0; $j < $height; $j++) { $colors = imagecolorsforindex($this->_image, imagecolorat($this->_image, $i, $j)); @@ -276,9 +418,9 @@ protected function detectFaces() { $step = (int)($scale*24*$increment); $size = (int)($scale*24); - for($i = 0; $i < $this->_width-$size; $i += $step) + for($i = 0; $i < $width-$size; $i += $step) { - for($j = 0; $j < $this->_height-$size; $j += $step) + for($j = 0; $j < $height-$size; $j += $step) { $pass = true; $k = 0; @@ -354,6 +496,30 @@ public function resizeImage($image, $width, $height, $options = array()) { return $new_image; } + /** + * @brief Resize dell'immagine alle dimensioni fornite + * @param resource $image resource dell'immagine + * @param int $width Larghezza della thumb + * @param int $height Altezza della thumb + * @param array $options Opzioni. + * @return resource immagine ridimensionata + */ + public function setSize($width, $height = null, $options = array()) { + + if (is_null($height)) { + + $height = $this->getHeight() * $width / $this->getWidth(); + } + + if ($width == $this->getWidth() && $height == $this->getHeight()) { + + return $this; + } + + $this->_image = $this->resizeImage($this->_image, $width, $height, $options); + return $this; + } + /** * @brief Crop dell'immagine nella parte con maggiore entropia * @param resource $image resource immagine @@ -371,7 +537,16 @@ private function cropImageEntropy($image, $width, $height, $options) { $left_x = $this->slice($image, $width, 'h'); $top_y = $this->slice($image, $height, 'v'); - $new_image = $this->cropImage($image, $width, $height, $left_x, $top_y, $options); + $size = $this->getBestFit($width, $height, $width, $height); + + $left_x = max(0, $left_x - $size['width'] / 2); + $top_y = max(0, $top_y - $size['height'] / 2); + + $left_x = min($left_x, $this->getWidth() - $size['width']); + $top_y = min($top_y, $this->getHeight() - $size['width']); + + + $new_image = $this->cropImage($image, $size['width'], $size['height'], $left_x, $top_y, $options); return $new_image; } @@ -537,6 +712,37 @@ private function getEntropy($histogram, $area) { } } +class Rect +{ + public $x1; + public $x2; + public $y1; + public $y2; + public $weight; + + public function __construct($x1, $x2, $y1, $y2, $weight) + { + $this->x1 = $x1; + $this->x2 = $x2; + $this->y1 = $y1; + $this->y2 = $y2; + $this->weight = $weight; + } + + public static function fromString($text) + { + $tab = explode(" ", $text); + $x1 = intval($tab[0]); + $x2 = intval($tab[1]); + $y1 = intval($tab[2]); + $y2 = intval($tab[3]); + $f = floatval($tab[4]); + + return new Rect($x1, $x2, $y1, $y2, $f); + } + +} + class Feature {