$value) $this->$attribute = $value; } public static function fromId($id_asset, $return_format = 'object') { $asset = parent::fromId($id_asset, 'array'); if ($asset) return $return_format == 'object' ? new Image($asset) : $asset; else return false; } public static function fromIds(array $id_assets, $return_format = 'object') { if (empty($id_assets)) return []; $assets = parent::fromIds($id_assets, 'array'); if ($return_format == 'array') return $assets; else { $objects = []; foreach ($assets as $id => $asset) $objects[$id] = new Image($asset); return $objects; } } public function save() { $data = []; foreach ($this->meta as $key => $value) $data[] = [ 'id_asset' => $this->id_asset, 'variable' => $key, 'value' => $value, ]; return Registry::get('db')->insert('replace', 'assets_meta', [ 'id_asset' => 'int', 'variable' => 'string', 'value' => 'string', ], $data); } public function getExif() { return EXIF::fromFile($this->getPath()); } public function getPath() { return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename; } public function getUrl() { return ASSETSURL . '/' . $this->subdir . '/' . $this->filename; } /** * @param width: width of the thumbnail. * @param height: height of the thumbnail. * @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom'] * @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimation [false]. */ public function getThumbnailUrl($width, $height, $crop = true, $fit = true) { // First, assert the image's dimensions are properly known in the database. if (!isset($this->image_height, $this->image_width)) throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?'); // Inferring width or height? if (!$height) $height = ceil($width / $this->image_width * $this->image_height); elseif (!$width) $width = ceil($height / $this->image_height * $this->image_width); // Inferring the height from the original image's ratio? if (!$fit) $height = floor($width / ($this->image_width / $this->image_height)); // Assert we have both, now... if (empty($width) || empty($height)) throw new InvalidArgumentException('Expecting at least either width or height as argument.'); // If we're cropping, verify we're in the right mode. if ($crop) { // If the original image's aspect ratio is much wider, take a slice instead. if ($this->image_width / $this->image_height > $width / $height) $crop = 'slice'; // We won't be cropping if the thumbnail is proportional to its original. if (abs($width / $height - $this->image_width / $this->image_height) <= 0.05) $crop = false; } // Do we have an exact crop boundary for these dimensions? $crop_selector = "crop_{$width}x{$height}"; if (isset($this->meta[$crop_selector])) $crop = 'exact'; // Now, do we need to suffix the filename? if ($crop) $suffix = '_c' . (is_string($crop) && $crop !== 'center' ? substr($crop, 0, 1) : ''); else $suffix = ''; // Check whether we already resized this earlier. $thumb_selector = "thumb_{$width}x{$height}{$suffix}"; if (isset($this->meta[$thumb_selector]) && file_exists(THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$thumb_selector])) return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector]; // Do we have a custom thumbnail on file? $custom_selector = "custom_{$width}x{$height}"; if (isset($this->meta[$custom_selector])) { if (file_exists(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector])) { // Copy the custom thumbail to the general thumbnail directory. copy(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector], THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]); // Let's remember this for future reference. $this->meta[$thumb_selector] = $this->meta[$custom_selector]; $this->save(); return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$custom_selector]; } else throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!'); } // Let's try some arcane stuff... try { if (!class_exists('Imagick')) throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions."); $thumb = new Imagick(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename); // The image might have some orientation set through EXIF. Let's apply this first. self::applyRotation($thumb); // Just resizing? Easy peasy. if (!$crop) $thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); // Cropping in the center? elseif ($crop === true || $crop === 'center') $thumb->cropThumbnailImage($width, $height); // Exact cropping? We can do that. elseif ($crop === 'exact') { list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->meta[$crop_selector]); $thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos); $thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); } // Advanced cropping? Fun! else { $size = $thumb->getImageGeometry(); // Taking a horizontal slice from the top or bottom of the original image? if ($crop === 'top' || $crop === 'bottom') { $crop_width = $size['width']; $crop_height = floor($size['width'] / $width * $height); $target_x = 0; $target_y = $crop === 'top' ? 0 : $size['height'] - $crop_height; } // Otherwise, we're taking a vertical slice from the centre. else { $crop_width = floor($size['height'] / $height * $width); $crop_height = $size['height']; $target_x = floor(($size['width'] - $crop_width) / 2); $target_y = 0; } $thumb->cropImage($crop_width, $crop_height, $target_x, $target_y); $thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); } // What sort of image is this? Fall back to PNG if we must. switch ($thumb->getImageFormat()) { case 'JPEG': $ext = 'jpg'; break; case 'GIF': $ext = 'gif'; break; case 'PNG': default: $thumb->setFormat('PNG'); $ext = 'png'; break; } // So, how do we name this? $thumbfilename = substr($this->filename, 0, strrpos($this->filename, '.')) . "_{$width}x{$height}{$suffix}.$ext"; // Ensure the thumbnail subdirectory exists. if (!is_dir(THUMBSDIR . '/' . $this->subdir)) mkdir(THUMBSDIR . '/' . $this->subdir, 0755, true); // Save it in a public spot. $thumb->writeImage(THUMBSDIR . '/' . $this->subdir . '/' . $thumbfilename); $thumb->clear(); $thumb->destroy(); } // Blast! Curse your sudden but inevitable betrayal! catch (ImagickException $e) { throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage()); } // Let's remember this for future reference. $this->meta[$thumb_selector] = $thumbfilename; $this->save(); // Ah yes, you wanted a URL, didn't you... return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector]; } private static function applyRotation(Imagick $image) { switch ($image->getImageOrientation()) { // Clockwise rotation case Imagick::ORIENTATION_RIGHTTOP: $image->rotateImage("#000", 90); break; // Counter-clockwise rotation case Imagick::ORIENTATION_LEFTBOTTOM: $image->rotateImage("#000", 270); break; // Upside down? case Imagick::ORIENTATION_BOTTOMRIGHT: $image->rotateImage("#000", 180); } // Having rotated the image, make sure the EXIF data is set properly. $image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT); } public function bestColor() { // Save some computations if we can. if (isset($this->meta['best_color'])) return $this->meta['best_color']; // Find out what colour is most prominent. $color = new BestColor($this); $this->meta['best_color'] = $color->hex(); $this->save(); // There's your colour. return $this->meta['best_color']; } public function bestLabelColor() { // Save some computations if we can. if (isset($this->meta['best_color_label'])) return $this->meta['best_color_label']; // Find out what colour is most prominent. $color = new BestColor($this); $this->meta['best_color_label'] = $color->rgba(); $this->save(); // There's your colour. return $this->meta['best_color_label']; } public function width() { return $this->image_width; } public function height() { return $this->image_height; } public function isPanorama() { return $this->image_width / $this->image_height > 2; } public function isPortrait() { return $this->image_width / $this->image_height < 1; } public function isLandscape() { $ratio = $this->image_width / $this->image_height; return $ratio >= 1 && $ratio <= 2; } public function removeAllThumbnails() { foreach ($this->meta as $key => $value) { if (substr($key, 0, 6) !== 'thumb_') continue; $thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value; if (is_file($thumb_path)) unlink($thumb_path); unset($this->meta[$key]); } $this->saveMetaData(); } public function replaceThumbnail($descriptor, $tmp_file) { if (!is_file($tmp_file)) return -1; if (!isset($this->meta[$descriptor])) return -2; $image = new Imagick($tmp_file); $d = $image->getImageGeometry(); unset($image); // Check whether dimensions match. $test_descriptor = 'thumb_' . $d['width'] . 'x' . $d['height']; if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false) return -3; // Save the custom thumbnail in the assets directory. $destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor]; if (file_exists($destination) && !is_writable($destination)) return -4; if (!copy($tmp_file, $destination)) return -5; // Copy it to the thumbnail directory, overwriting the automatically generated one, too. $destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor]; if (file_exists($destination) && !is_writable($destination)) return -6; if (!copy($tmp_file, $destination)) return -7; // A little bookkeeping $this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->meta[$descriptor]; $this->saveMetaData(); return 0; } }