Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for multiple show photo sizes, and resize on upload #888

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"google/recaptcha": "~1.1",
"soundasleep/html2text": "~0.5",
"twig/twig": "~2.4",
"ext-xmlwriter": "*"
"ext-xmlwriter": "*",
"ext-gd": "*"
},
"require-dev": {
"squizlabs/php_codesniffer": "~3.5",
Expand Down
18 changes: 16 additions & 2 deletions src/Classes/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,20 @@ final class Config
*/
public static $webcam_set_url;

/**
* The formats to resize uploaded images to.
*
* The keys of the array are format names to rename to. The values are [x,y] sizes in pixels.
* If provided, the third value in the array is the JPEG quality. If none given, defaults to 80.
* @var int[][]
*/
public static $image_resize_formats = [
'podcast_small' => [1400, 1400, 90],
'web' => [800, 800]
];

public static $default_image_format = 'web';

/**
* The path to store the original, unencoded copies of MyRadio Podcasts.
* The originals are archived here for future reencoding.
Expand All @@ -253,12 +267,12 @@ final class Config
public static $public_media_uri = '/media';

/**
* The full web address to the image that will be served for a show if there
* The local path, relative to $public_media_path, to the image that will be served for a show if there
* is not a photo for that show.
*
* @var string
*/
public static $default_show_uri = '/media/image_meta/ShowImageMetadata/22.png';
public static $default_show_image_path = 'image_meta/ShowImageMetadata/22.png';

/**
* The full web address to the image that will be served outside of term time.
Expand Down
125 changes: 125 additions & 0 deletions src/Classes/MyRadio/ImageUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php


namespace MyRadio\MyRadio;


use MyRadio\MyRadioException;

class ImageUtils
{
/**
* Given a path to an image, returns a GD image resource for it.
* Throws a MyRadioException if the type is unrecognised.
* @param string $path a path to an image, either local or remote
* @return resource a GD image resource
*/
public static function loadImage(string $path)
{
$sizeinfo = getimagesize($path);
if ($sizeinfo === false) {
throw new MyRadioException('Failed to get image details');
}
switch ($sizeinfo[2]) {
case IMAGETYPE_JPEG:
case IMAGETYPE_JPEG2000:
return imagecreatefromjpeg($path);
case IMAGETYPE_PNG:
return imagecreatefrompng($path);
default:
$type = image_type_to_extension($sizeinfo[2]);
throw new MyRadioException("Unrecognised image format $type", 400);
}
}

/**
* Crops the given image to the given dimensions, enlarging it if it's too small,
* reducing it if it's too big, and cutting off anything outside the given dimensions.
*
* If the given image is already the right size, it is returned unchanged.
*
* Note that the size of the new image may be 1px off on either axis, due to floating point rounding.
*
* @param $image resource
* @param int $newX
* @param int $newY
* @return resource
* @throws MyRadioException if the given image has a dimension 0, or the resize fails for any reason
*/
public static function cropAndResizeImage($image, int $newX, int $newY) {
// To understand this code, imagine we have an 800x600 image and we're resizing it to 1400x1400.
$oldX = imagesx($image); // 800
$oldY = imagesy($image); // 600

if ($oldX === 0 || $oldY === 0) {
throw new MyRadioException('Tried to resize an image with a 0-dimension', 400);
}

if ($oldX === $newX && $oldY === $newY) {
return $image;
}

$oldSmallest = min($oldX, $oldY); // 600
$newBiggest = max($newX, $newY); // 1400
$scaleFactor = $newBiggest / $oldSmallest; // 2.333...

// We intentionally use the old _smallest_ and new _biggest_, and ceil() everything.
// This is to ensure that, no matter the dimensions, the new image is _at least_
// as big as the target.

$newResizedX = ceil($oldX * $scaleFactor); // 1840
$newResizedY = ceil($oldY * $scaleFactor); // 1380 (~1400)

// We'll set up these variables now, we'll need them later
$srcX = 0;
$srcY = 0;
$srcW = $oldX;
$srcH = $oldY;

// Now, cut off anything outside the given dimens
// We're guaranteed that $newResizedX >= $newX
// and $newResizedY >= $newY.
// We're also guaranteed that either
// $newResizedX > $newX, or
// $newResizedY > $newY, or
// $newResizedX == $newXn AND $newResizedY == $newY
// (that is, $newResizedX > $newX AND $newResizedY > $newY will never be true)
//
// (Sketch of a) proof: if we're enlarging the image, see the example so far
// If we're reducing, imagine the opposite of this example - 1400x1400->800x600.
// Scale factor is (max(800,600) / min(1400,1400)) = 800/1400 = 0.57.
// so new dimensions are 800x800
if ($newResizedX > $newX) {
// Calculate the overspill
$overspillX = $newResizedX - $newX; // 440
// Alter it by the scale factor
$overspillX /= $scaleFactor; // ~188
// Cut off equally from either side
$srcX += ceil($overspillX / 2); // 188
$srcW -= ceil($overspillX / 2); // 612
} else if ($newResizedY > $newY) {
$overspillY = $newResizedY - $newY; // calculations not reproduced
$overspillY /= $scaleFactor;
$srcY += ceil($overspillY / 2);
$srcH -= ceil($overspillY / 2);
}

$newImage = imagecreatetruecolor($newX, $newY);
$result = imagecopyresampled(
$newImage,
$image,
0,
0,
$srcX,
$srcY,
$newX,
$newY,
$srcW,
$srcH
);
if ($result === false) {
throw new MyRadioException('Failed to resize image');
}
return $newImage;
}
}
57 changes: 49 additions & 8 deletions src/Classes/ServiceAPI/MyRadio_Show.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use MyRadio\Config;
use MyRadio\Database;
use MyRadio\MyRadio\ImageUtils;
use MyRadio\MyRadioException;
use MyRadio\MyRadio\CoreUtils;
use MyRadio\MyRadio\URLUtils;
Expand Down Expand Up @@ -109,7 +110,7 @@ class MyRadio_Show extends MyRadio_Metadata_Common
private $show_type;
private $submitted_time;
private $season_ids;
private $photo_url;
private $photo_path;
private $podcast_explicit;
private $subtype_id;

Expand Down Expand Up @@ -188,10 +189,10 @@ protected function __construct($result)
/*
* @todo Support general photo attachment?
*/
$this->photo_url = Config::$default_person_uri;
$this->photo_path = Config::$default_show_image_path;
$image_metadata = json_decode($result['image_metadata_values']);
if ($result['image_metadata_values'] !== null) {
$this->photo_url = Config::$public_media_uri.'/'.$image_metadata[0];
$this->photo_path = $image_metadata[0];
}

//Get information about Seasons
Expand Down Expand Up @@ -656,13 +657,40 @@ public function getWebpage()
}

/**
* Get the web url for the Show Photo.
* Get the web url for the Show Photo, in the default format if it exists, otherwise "original".
*
* @return string
*/
public function getShowPhoto()
{
return $this->photo_url;
try {
return $this->getShowPhotoWithFormat(Config::$default_image_format);
} catch (MyRadioException $e) {
if ($e->getCode() === 404) {
// This show doesn't have the photo resized
return $this->getLegacyShowPhoto();
} else {
throw $e;
}
}
}

public function getShowPhotoWithFormat(string $format)
{
// Strip out all non-alphanums (and _ and -) to avoid shell injection
$safeFormat = preg_replace('/[^A-Za-z0-9_\-]/', '_', $format);
$suffix = str_replace('.orig.', ".$safeFormat.", $this->photo_path);
$path = Config::$public_media_path . '/' . $suffix;
if (is_file($path)) {
return Config::$public_media_uri . '/' . $suffix;
} else {
throw new MyRadioException("This show doesn't have a photo with format $safeFormat", 404);
}
}

private function getLegacyShowPhoto()
{
return Config::$public_media_uri . '/' . $this->photo_path;
}

/**
Expand Down Expand Up @@ -748,12 +776,25 @@ public function setShowPhoto($tmp_path)
$this->getID()
]
)[0];


// Save the original file

$filetype = explode('/', getimagesize($tmp_path)['mime'])[1];
$suffix = 'image_meta/ShowImageMetadata/'.$result.'.'.$filetype;
$suffix = 'image_meta/ShowImageMetadata/'.$result.'.orig.'.$filetype;
$path = Config::$public_media_path.'/'.$suffix;
rename($tmp_path, $path);

// Now, resize it
$img = ImageUtils::loadImage($path);
foreach (Config::$image_resize_formats as $name => $size) {
$resized = ImageUtils::cropAndResizeImage($img, $size[0], $size[1]);
$resizedPath = str_replace('.orig.', ".$name.", $path);
imagejpeg(
$resized,
$resizedPath,
$size[2] ?? 80);
}

self::$db->query(
'UPDATE schedule.show_image_metadata SET effective_to=NOW()
WHERE metadata_key_id=$1
Expand All @@ -768,7 +809,7 @@ public function setShowPhoto($tmp_path)
[$suffix, $result]
);

$this->photo_url = Config::$public_media_uri.'/'.$suffix;
$this->photo_path = $suffix;
$this->updateCacheObject();
}

Expand Down
4 changes: 2 additions & 2 deletions src/Classes/ServiceAPI/MyRadio_Timeslot.php
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ public static function getCurrentAndNext($time = null, $n = 1, $filter = [1])
'desc' => 'There are currently no shows on right now, even our presenters
need a break. But it\'s okay, ' . Config::$short_name .
' Jukebox has got you covered, playing the best music for your ears!',
'photo' => Config::$default_show_uri,
'photo' => Config::$public_media_uri . '/' . Config::$default_show_image_path,
'end_time' => $next ? $next->getStartTime() : 'The End of Time',
],
];
Expand Down Expand Up @@ -707,7 +707,7 @@ public static function getCurrentAndNext($time = null, $n = 1, $filter = [1])
'desc' => 'There are currently no shows on right now, even our presenters
need a break. But it\'s okay, ' . Config::$short_name .
' Jukebox has got you covered, playing the best music for your ears!',
'photo' => Config::$default_show_uri,
'photo' => Config::$public_media_uri . '/' . Config::$default_show_image_path,
'start_time' => $lastnext->getEndTime(),
'end_time' => $nextshow ? $nextshow->getStartTime() : 'The End of Time',
];
Expand Down