streams/include/photo_factory.php

565 lines
14 KiB
PHP
Raw Normal View History

2019-04-16 06:25:49 +00:00
<?php
use Zotlabs\Photo\PhotoDriver;
use Zotlabs\Photo\PhotoGd;
use Zotlabs\Photo\PhotoImagick;
2020-08-17 04:55:59 +00:00
use Zotlabs\Lib\Img_cache;
use Zotlabs\Lib\Hashpath;
2019-04-16 06:25:49 +00:00
/**
* @brief Return a PhotoDriver object.
*
* Use this factory when manipulating images.
*
* Return a photo driver object implementing ImageMagick or GD.
*
* @param string $data Image data
* @param string $type Mimetype
* @return null|PhotoDriver
* NULL if unsupported image type or failure, otherwise photo driver object
*/
function photo_factory($data, $type = null) {
$ph = null;
$m = null;
$unsupported_types = [
'image/bmp',
'image/vnd.microsoft.icon',
'image/tiff',
'image/svg+xml',
];
2020-05-26 06:43:40 +00:00
if ($type && in_array(strtolower($type), $unsupported_types)) {
2019-04-16 06:25:49 +00:00
logger('Unsupported image type ' . $type);
return null;
}
$ignore_imagick = get_config('system','ignore_imagick');
2019-04-16 06:25:49 +00:00
2020-05-26 06:43:40 +00:00
if (class_exists('Imagick') && !$ignore_imagick) {
$ph = new PhotoImagick($data, $type);
// As of August 2020, webp support is still poor in both imagick and gd. Both claim to support it,
// but both may require additional configuration. If it's a webp and the imagick driver failed,
// we'll try again with GD just in case that one handles it. If not, you may need to install libwebp
// which should make imagick work and/or re-compile php-gd with the option to include that library.
if (! $ph->is_valid() && $type === 'image/webp') {
$ph = null;
}
2019-04-16 06:25:49 +00:00
}
2020-05-26 06:43:40 +00:00
if (! $ph) {
2019-04-16 06:25:49 +00:00
$ph = new PhotoGd($data, $type);
}
return $ph;
}
/**
* @brief Guess image mimetype from filename or from Content-Type header.
*
* @param string $filename
* Image filename
* @param string $headers (optional)
* Headers to check for Content-Type (from curl request)
* @return null|string Guessed mimetype
*/
2020-05-26 06:43:40 +00:00
2019-04-16 06:25:49 +00:00
function guess_image_type($filename, $headers = '') {
2020-05-26 06:43:40 +00:00
// logger('Photo: guess_image_type: '.$filename . ($headers?' from curl headers':''), LOGGER_DEBUG);
2019-04-16 06:25:49 +00:00
$type = null;
$m = null;
if($headers) {
$hdrs = [];
$h = explode("\n", $headers);
foreach ($h as $l) {
list($k, $v) = array_map('trim', explode(':', trim($l), 2));
$hdrs[strtolower($k)] = $v;
}
2020-05-26 06:43:40 +00:00
logger('Curl headers: ' . print_r($hdrs, true), LOGGER_DEBUG);
if (array_key_exists('content-type', $hdrs)) {
2019-04-16 06:25:49 +00:00
$ph = photo_factory('');
$types = $ph->supportedTypes();
2020-05-26 06:43:40 +00:00
if (array_key_exists($hdrs['content-type'], $types))
2019-04-16 06:25:49 +00:00
$type = $hdrs['content-type'];
}
}
2020-05-26 06:43:40 +00:00
if (is_null($type)) {
2019-04-16 06:25:49 +00:00
$ignore_imagick = get_config('system', 'ignore_imagick');
// Guessing from extension? Isn't that... dangerous?
if(class_exists('Imagick') && file_exists($filename) && is_readable($filename) && !$ignore_imagick) {
/**
* Well, this not much better,
* but at least it comes from the data inside the image,
* we won't be tricked by a manipulated extension
2020-05-26 06:43:40 +00:00
*/
2019-04-16 06:25:49 +00:00
$image = new Imagick($filename);
$type = $image->getImageMimeType();
}
2020-05-26 06:43:40 +00:00
if (is_null($type)) {
2019-04-16 06:25:49 +00:00
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$ph = photo_factory('');
$types = $ph->supportedTypes();
2020-05-26 06:43:40 +00:00
foreach ($types as $m => $e) {
if ($ext === $e) {
2019-04-16 06:25:49 +00:00
$type = $m;
}
}
}
2020-05-26 06:43:40 +00:00
if (is_null($type) && (strpos($filename, 'http') === false)) {
2019-04-16 06:25:49 +00:00
$size = getimagesize($filename);
$ph = photo_factory('');
$types = $ph->supportedTypes();
$type = ((array_key_exists($size['mime'], $types)) ? $size['mime'] : 'image/jpeg');
}
2020-05-26 06:43:40 +00:00
if (is_null($type)) {
if (strpos(strtolower($filename),'jpg') !== false) {
2019-04-16 06:25:49 +00:00
$type = 'image/jpeg';
2020-05-26 06:43:40 +00:00
}
elseif (strpos(strtolower($filename),'jpeg') !== false) {
2019-04-16 06:25:49 +00:00
$type = 'image/jpeg';
2020-05-26 06:43:40 +00:00
}
elseif (strpos(strtolower($filename),'gif') !== false) {
2019-04-16 06:25:49 +00:00
$type = 'image/gif';
2020-05-26 06:43:40 +00:00
}
elseif (strpos(strtolower($filename),'png') !== false) {
2019-04-16 06:25:49 +00:00
$type = 'image/png';
2020-05-26 06:43:40 +00:00
}
2019-04-16 06:25:49 +00:00
}
}
2020-05-26 06:43:40 +00:00
2019-04-16 06:25:49 +00:00
logger('Photo: guess_image_type: filename = ' . $filename . ' type = ' . $type, LOGGER_DEBUG);
return $type;
}
/**
* @brief Delete thing photo from database.
*
* @param string $url
* @param string $ob_hash
* @return void
*/
function delete_thing_photo($url, $ob_hash) {
$hash = basename($url);
$dbhash = substr($hash, 0, strpos($hash, '-'));
2019-04-16 06:25:49 +00:00
// hashes should be 32 bytes.
if ((! $ob_hash) || (strlen($dbhash) < 16)) {
2019-04-16 06:25:49 +00:00
return;
2020-05-26 06:43:40 +00:00
}
2019-04-16 06:25:49 +00:00
if (strpos($url,'/xp/') !== false && strpos($url,'.obj') !== false) {
$xppath = 'cache/xp/' . substr($hash,0,2) . '/' . substr($hash,2,2) . '/' . $hash;
if (file_exists($xppath)) {
unlink($xppath);
}
$xppath = str_replace('-4','-5',$xppath);
if (file_exists($xppath)) {
unlink($xppath);
}
$xppath = str_replace('-5','-6',$xppath);
if (file_exists($xppath)) {
unlink($xppath);
}
}
else {
q("delete from photo where xchan = '%s' and photo_usage = %d and resource_id = '%s'",
dbesc($ob_hash),
intval(PHOTO_THING),
dbesc($dbhash)
);
}
2019-04-16 06:25:49 +00:00
}
/**
* @brief Fetches a photo from external site and prepares its miniatures.
*
* @param string $photo
* external URL to fetch base image
* @param string $xchan
* channel unique hash
* @param boolean $thing
* TRUE if this is a thing URL
* @param boolean $force
* TRUE if ignore image modification date check (force fetch)
*
* @return array of results
* * \e string \b 0 => local URL to full image
* * \e string \b 1 => local URL to standard thumbnail
* * \e string \b 2 => local URL to micro thumbnail
* * \e string \b 3 => image type
* * \e boolean \b 4 => TRUE if fetch failure
* * \e string \b 5 => modification date
*/
function import_xchan_photo($photo, $xchan, $thing = false, $force = false) {
logger('Updating channel photo from ' . $photo . ' for ' . $xchan, LOGGER_DEBUG);
2020-08-17 04:55:59 +00:00
$flags = (($thing) ? PHOTO_THING : PHOTO_XCHAN);
$album = (($thing) ? 'Things' : 'Contact Photos');
$modified = EMPTY_STR;
$matches = null;
$failed = true;
$img_str = EMPTY_STR;
$hash = photo_new_resource();
$os_storage = false;
2020-05-06 04:34:31 +00:00
if (! $thing) {
$r = q("select resource_id, edited, mimetype from photo where xchan = '%s' and photo_usage = %d and imgscale = 4 limit 1",
dbesc($xchan),
intval(PHOTO_XCHAN)
);
if ($r) {
$r = array_shift($r);
2020-08-17 04:55:59 +00:00
$hash = $r['resource_id'];
$modified = $r['edited'];
$type = $r['mimetype'];
}
else {
$os_storage = true;
2019-04-16 06:25:49 +00:00
}
}
2020-05-06 04:34:31 +00:00
if ($photo) {
2019-04-16 06:25:49 +00:00
2020-05-06 04:34:31 +00:00
if ($modified === EMPTY_STR || $force) {
2019-04-16 06:25:49 +00:00
$result = z_fetch_url($photo, true);
2020-05-06 04:34:31 +00:00
}
else {
$h = [ 'headers' => [ 'If-Modified-Since: ' . gmdate('D, d M Y H:i:s', strtotime($modified . 'Z')) . ' GMT' ] ];
2019-04-16 06:25:49 +00:00
$result = z_fetch_url($photo, true, 0, $h);
}
2020-05-06 04:34:31 +00:00
if ($result['success']) {
2019-04-16 06:25:49 +00:00
$img_str = $result['body'];
$type = guess_image_type($photo, $result['header']);
2020-08-17 04:55:59 +00:00
$modified = gmdate('Y-m-d H:i:s', (preg_match('/last-modified: (.+) \S+/i', $result['header'], $matches) ? strtotime($matches[1] . 'Z') : time()));
2020-05-06 04:34:31 +00:00
if (! is_null($type)) {
$failed = false;
}
}
elseif ($result['return_code'] == 304) {
2019-04-16 06:25:49 +00:00
$photo = z_root() . '/photo/' . $hash . '-4';
$thumb = z_root() . '/photo/' . $hash . '-5';
$micro = z_root() . '/photo/' . $hash . '-6';
2020-05-06 04:34:31 +00:00
$failed = false;
2019-04-16 06:25:49 +00:00
}
}
2020-05-06 04:34:31 +00:00
if (! $failed && $result['return_code'] != 304) {
2019-04-16 06:25:49 +00:00
$img = photo_factory($img_str, $type);
2020-05-06 04:34:31 +00:00
if ($img->is_valid()) {
2019-04-16 06:25:49 +00:00
$width = $img->getWidth();
$height = $img->getHeight();
2020-05-06 04:34:31 +00:00
if ($width && $height) {
if (($width / $height) > 1.2) {
2019-04-16 06:25:49 +00:00
// crop out the sides
$margin = $width - $height;
$img->cropImage(300, ($margin / 2), 0, $height, $height);
2020-05-06 04:34:31 +00:00
}
2020-05-26 06:43:40 +00:00
elseif (($height / $width) > 1.2) {
2019-04-16 06:25:49 +00:00
// crop out the bottom
$margin = $height - $width;
$img->cropImage(300, 0, 0, $width, $width);
2020-05-06 04:34:31 +00:00
}
else {
2019-04-16 06:25:49 +00:00
$img->scaleImageSquare(300);
}
2020-05-06 04:34:31 +00:00
}
else {
$failed = true;
2019-04-16 06:25:49 +00:00
}
$p = [
2020-05-06 04:34:31 +00:00
'xchan' => $xchan,
'resource_id' => $hash,
'filename' => basename($photo),
'album' => $album,
'photo_usage' => $flags,
'imgscale' => 4,
'edited' => $modified,
2019-04-16 06:25:49 +00:00
];
$r = $img->save($p);
2020-05-06 04:34:31 +00:00
if ($r === false) {
$failed = true;
}
2019-04-16 06:25:49 +00:00
$img->scaleImage(80);
$p['imgscale'] = 5;
$r = $img->save($p);
2020-05-06 04:34:31 +00:00
if ($r === false) {
$failed = true;
}
2019-04-16 06:25:49 +00:00
$img->scaleImage(48);
$p['imgscale'] = 6;
$r = $img->save($p);
2020-05-06 04:34:31 +00:00
if ($r === false) {
$failed = true;
}
2019-04-16 06:25:49 +00:00
$photo = z_root() . '/photo/' . $hash . '-4';
$thumb = z_root() . '/photo/' . $hash . '-5';
$micro = z_root() . '/photo/' . $hash . '-6';
2020-05-06 04:34:31 +00:00
}
else {
2019-04-16 06:25:49 +00:00
logger('Invalid image from ' . $photo);
2020-05-06 04:34:31 +00:00
$failed = true;
2019-04-16 06:25:49 +00:00
}
}
2020-05-06 04:34:31 +00:00
if ($failed) {
$default = get_default_profile_photo();
$photo = z_root() . '/' . $default;
$thumb = z_root() . '/' . get_default_profile_photo(80);
$micro = z_root() . '/' . get_default_profile_photo(48);
$type = 'image/png';
2019-04-16 06:25:49 +00:00
$modified = gmdate('Y-m-d H:i:s', filemtime($default));
}
logger('HTTP code: ' . $result['return_code'] . '; modified: ' . $modified
2020-05-06 04:34:31 +00:00
. '; failure: ' . ($failed ? 'yes' : 'no') . '; URL: ' . $photo, LOGGER_DEBUG);
2019-04-16 06:25:49 +00:00
2020-05-06 04:34:31 +00:00
return([$photo, $thumb, $micro, $type, $failed, $modified]);
2019-04-16 06:25:49 +00:00
}
2020-08-18 06:35:05 +00:00
function import_remote_xchan_photo($photo, $xchan, $thing = false) {
2020-08-18 06:35:05 +00:00
// logger('Updating channel photo from ' . $photo . ' for ' . $xchan, LOGGER_DEBUG);
$failed = true;
2020-08-25 04:36:56 +00:00
$path = Hashpath::path((($thing) ? $photo . $xchan : $xchan),'cache/xp',2);
$hash = basename($path);
2020-08-18 06:35:05 +00:00
$modified = ((file_exists($outfile)) ? @filemtime($outfile) : 0);
// Maybe it's already a cached xchan photo
if (strpos($photo, z_root() . '/xp/') === 0) {
2020-08-18 06:35:05 +00:00
return false;
}
if ($modified) {
$h = [ 'headers' => [ 'If-Modified-Since: ' . gmdate('D, d M Y H:i:s \\G\\M\\T', $modified) . ' GMT' ] ];
$recurse = 0;
$result = z_fetch_url($photo, true, $recurse, $h);
}
else {
$result = z_fetch_url($photo, true);
}
if ($result['success']) {
$type = guess_image_type($photo, $result['header']);
2020-08-25 04:36:56 +00:00
if ($type) {
$failed = false;
}
2020-08-18 06:35:05 +00:00
}
elseif ($result['return_code'] == 304) {
2020-08-25 04:36:56 +00:00
$photo = z_root() . '/xp/' . $hash . '-4' . (($thing) ? '.obj' : EMPTY_STR);
$thumb = z_root() . '/xp/' . $hash . '-5' . (($thing) ? '.obj' : EMPTY_STR);
$micro = z_root() . '/xp/' . $hash . '-6' . (($thing) ? '.obj' : EMPTY_STR);
2020-08-18 06:35:05 +00:00
$failed = false;
}
if (! $failed && $result['return_code'] != 304) {
2020-08-25 04:36:56 +00:00
$img = photo_factory($result['body'], $type);
2020-08-18 06:35:05 +00:00
if ($img->is_valid()) {
$width = $img->getWidth();
$height = $img->getHeight();
if ($width && $height) {
if (($width / $height) > 1.2) {
// crop out the sides
$margin = $width - $height;
$img->cropImage(300, ($margin / 2), 0, $height, $height);
}
elseif (($height / $width) > 1.2) {
// crop out the bottom
$margin = $height - $width;
$img->cropImage(300, 0, 0, $width, $width);
}
else {
$img->scaleImageSquare(300);
}
}
else {
$failed = true;
}
$p = [
'xchan' => $xchan,
'resource_id' => $hash,
'filename' => basename($photo),
'album' => $album,
'photo_usage' => $flags,
'imgscale' => 4,
'edited' => $modified,
];
$savepath = $path . '-' . $p['imgscale'] . (($thing) ? '.obj' : EMPTY_STR);
2020-08-25 04:36:56 +00:00
$photo = z_root() . '/xp/' . $hash . '-' . $p['imgscale'] . (($thing) ? '.obj' : EMPTY_STR);
$r = $img->saveImage($savepath);
2020-08-18 06:35:05 +00:00
if ($r === false) {
$failed = true;
}
$img->scaleImage(80);
$p['imgscale'] = 5;
$savepath = $path . '-' . $p['imgscale'] . (($thing) ? '.obj' : EMPTY_STR);
2020-08-25 04:36:56 +00:00
$thumb = z_root() . '/xp/' . $hash . '-' . $p['imgscale'] . (($thing) ? '.obj' : EMPTY_STR);
$r = $img->saveImage($savepath);
2020-08-18 06:35:05 +00:00
if ($r === false) {
$failed = true;
}
$img->scaleImage(48);
$p['imgscale'] = 6;
$savepath = $path . '-' . $p['imgscale'] . (($thing) ? '.obj' : EMPTY_STR);
2020-08-25 04:36:56 +00:00
$micro = z_root() . '/xp/' . $hash . '-' . $p['imgscale'] . (($thing) ? '.obj' : EMPTY_STR);
$r = $img->saveImage($savepath);
2020-08-18 06:35:05 +00:00
if ($r === false) {
$failed = true;
}
}
else {
logger('Invalid image from ' . $photo);
$failed = true;
}
}
if ($failed) {
$default = get_default_profile_photo();
$photo = z_root() . '/' . $default;
$thumb = z_root() . '/' . get_default_profile_photo(80);
$micro = z_root() . '/' . get_default_profile_photo(48);
$type = 'image/png';
$modified = gmdate('Y-m-d H:i:s', filemtime($default));
}
logger('HTTP code: ' . $result['return_code'] . '; modified: ' . $modified
. '; failure: ' . ($failed ? 'yes' : 'no') . '; URL: ' . $photo, LOGGER_DEBUG);
return([$photo, $thumb, $micro, $type, $failed, $modified]);
}
2019-04-16 06:25:49 +00:00
/**
* @brief Import channel photo from a URL.
*
* @param string $photo URL to a photo
* @param int $aid
* @param int $uid channel_id
* @return null|string Guessed image mimetype or null.
*/
function import_channel_photo_from_url($photo, $aid, $uid) {
$type = null;
2020-05-26 06:43:40 +00:00
if ($photo) {
2019-04-16 06:25:49 +00:00
$result = z_fetch_url($photo, true);
2020-05-26 06:43:40 +00:00
if ($result['success']) {
2019-04-16 06:25:49 +00:00
$img_str = $result['body'];
$type = guess_image_type($photo, $result['header']);
import_channel_photo($img_str, $type, $aid, $uid);
}
}
return $type;
}
/**
* @brief Import a channel photo and prepare its miniatures.
*
* @param string $photo Image data
* @param string $type
* @param int $aid
* @param int $uid channel_id
* @return boolean|string false on failure, otherwise resource_id of photo
*/
function import_channel_photo($photo, $type, $aid, $uid) {
logger('Importing channel photo for ' . $uid, LOGGER_DEBUG);
$r = q("select resource_id from photo where uid = %d and photo_usage = 1 and imgscale = 4",
intval($uid)
);
if ($r) {
$hash = $r[0]['resource_id'];
}
else {
$hash = photo_new_resource();
}
2019-04-16 06:25:49 +00:00
$photo_failure = false;
$filename = $hash;
$img = photo_factory($photo, $type);
2020-05-26 06:43:40 +00:00
if ($img->is_valid()) {
2019-04-16 06:25:49 +00:00
// config array for image save method
$p = [
2020-05-26 06:43:40 +00:00
'aid' => $aid,
'uid' => $uid,
'resource_id' => $hash,
'filename' => $filename,
'album' => t('Profile Photos'),
'photo_usage' => PHOTO_PROFILE,
'imgscale' => PHOTO_RES_PROFILE_300,
2019-04-16 06:25:49 +00:00
];
// photo size
$img->scaleImageSquare(300);
$r = $img->save($p);
2020-05-26 06:43:40 +00:00
if ($r === false) {
2019-04-16 06:25:49 +00:00
$photo_failure = true;
2020-05-26 06:43:40 +00:00
}
2019-04-16 06:25:49 +00:00
// thumb size
$img->scaleImage(80);
$p['imgscale'] = 5;
$r = $img->save($p);
2020-05-26 06:43:40 +00:00
if ($r === false) {
2019-04-16 06:25:49 +00:00
$photo_failure = true;
2020-05-26 06:43:40 +00:00
}
2019-04-16 06:25:49 +00:00
// micro size
$img->scaleImage(48);
$p['imgscale'] = 6;
$r = $img->save($p);
2020-05-26 06:43:40 +00:00
if ($r === false) {
2019-04-16 06:25:49 +00:00
$photo_failure = true;
2020-05-26 06:43:40 +00:00
}
2019-04-16 06:25:49 +00:00
2020-05-26 06:43:40 +00:00
}
else {
2019-04-16 06:25:49 +00:00
logger('Invalid image.');
$photo_failure = true;
}
2020-05-26 06:43:40 +00:00
if ($photo_failure) {
2019-04-16 06:25:49 +00:00
return false;
2020-05-26 06:43:40 +00:00
}
return $hash;
2019-04-16 06:25:49 +00:00
}