Skip to content

Commit

Permalink
Resize and crop members picture to a fixed ratio
Browse files Browse the repository at this point in the history
closes #1717

Add cropping options in settings parameters tab
Add cropping focus selection on member form
Add cropping on resizeImage()
Minimum image dimensions required from cropping

Restore and extend drag and drop picture feature
Clean CSS and template
  • Loading branch information
gagnieray authored and trasher committed Oct 12, 2023
1 parent 8d4ed04 commit 656a271
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 130 deletions.
7 changes: 6 additions & 1 deletion galette/lib/Galette/Controllers/AjaxController.php
Expand Up @@ -108,6 +108,10 @@ public function photo(Request $request, Response $response): Response
$mid = $post['member_id'];
$fsize = $post['filesize'];
$fname = $post['filename'];
$cropping = null;
if ($post['cropping'] != false) {
$cropping = $post['cropping'];
}
$tmpname = GALETTE_TEMPIMAGES_PATH . 'ajax_upload_' . $fname;

$temp = explode('base64,', $post['file']);
Expand All @@ -126,7 +130,8 @@ public function photo(Request $request, Response $response): Response
'tmp_name' => $tmpname,
'size' => $fsize
),
true
true,
$cropping
);

if ($res < 0) {
Expand Down
8 changes: 7 additions & 1 deletion galette/lib/Galette/Controllers/Crud/MembersController.php
Expand Up @@ -1596,7 +1596,13 @@ public function store(Request $request, Response $response): Response
}

if (count($error_detected) === 0) {
$files_res = $member->handleFiles($_FILES);
$cropping = null;
if ($this->preferences->pref_force_picture_ratio == 1) {
$cropping = [];
$cropping['ratio'] = isset($this->preferences->pref_member_picture_ratio) ? $this->preferences->pref_member_picture_ratio : 'square_ratio';
$cropping['focus'] = isset($post['crop_focus']) ? $post['crop_focus'] : 'center';
}
$files_res = $member->handleFiles($_FILES, $cropping);
if (is_array($files_res)) {
$error_detected = array_merge($error_detected, $files_res);
}
Expand Down
163 changes: 143 additions & 20 deletions galette/lib/Galette/Core/Picture.php
Expand Up @@ -425,12 +425,13 @@ public function delete($transaction = true)
/**
* Stores an image on the disk and in the database
*
* @param object $file the uploaded file
* @param boolean $ajax If the image cames from an ajax call (dnd)
* @param object $file The uploaded file
* @param boolean $ajax If the image cames from an ajax call (dnd)
* @param array $cropping Cropping properties
*
* @return bool|int
*/
public function store($file, $ajax = false)
public function store($file, $ajax = false, $cropping = null)
{
/** TODO: fix max size (by preferences ?) */
global $zdb;
Expand Down Expand Up @@ -508,6 +509,22 @@ public function store($file, $ajax = false)
);
}

// Source image must have minimum dimensions to match the cropping process requirements
// and ensure the final picture will fit the maximum allowed resizing dimensions.
if (isset($cropping['ratio']) && isset($cropping['focus'])) {
if ($current[0] < $this->mincropsize || $current[1] < $this->mincropsize) {
$min_current = min($current[0], $current[1]);
Analog::log(
'[' . $class . '] Image is too small. The minimum image side size allowed is ' .
$this->mincropsize . 'px, but current is ' . $min_current . 'px.',
Analog::ERROR
);
return self::IMAGE_TOO_SMALL;
} else {
Analog::log('[' . $class . '] Image dimensions are OK, proceed', Analog::DEBUG);
}
}

$this->delete();

$new_file = $this->store_path .
Expand All @@ -522,7 +539,7 @@ public function store($file, $ajax = false)
if ($current[0] > $this->max_width || $current[1] > $this->max_height) {
/** FIXME: what if image cannot be resized?
Should'nt we want to stop the process here? */
$this->resizeImage($new_file, $extension);
$this->resizeImage($new_file, $extension, null, $cropping);
}

return $this->storeInDb($zdb, $this->db_id, $new_file, $extension);
Expand Down Expand Up @@ -678,16 +695,17 @@ public function missingInDb(Db $zdb)
}

/**
* Resize the image if it exceeds max allowed sizes
* Resize and eventually crop the image if it exceeds max allowed sizes
*
* @param string $source the source image
* @param string $ext file's extension
* @param string $dest the destination image.
* If null, we'll use the source image. Defaults to null
* @param string $source The source image
* @param string $ext File's extension
* @param string $dest The destination image.
* If null, we'll use the source image. Defaults to null
* @param array $cropping Cropping properties
*
* @return void|false
*/
private function resizeImage($source, $ext, $dest = null)
private function resizeImage($source, $ext, $dest = null, $cropping = null)
{
$class = get_class($this);

Expand Down Expand Up @@ -750,39 +768,144 @@ private function resizeImage($source, $ext, $dest = null)

$ratio = $cur_width / $cur_height;

// calculate image size according to ratio
if ($cur_width > $cur_height) {
$h = round($w / $ratio);
// Define cropping variables if necessary.
$thumb_cropped = false;
// Cropping is based on the smallest side of the source in order to
// provide as less focusing options as possible if the source doesn't
// fit the final ratio (center, top, bottom, left, right).
$min_size = min($cur_width, $cur_height);
// Cropping dimensions.
$crop_width = $min_size;
$crop_height = $min_size;
// Cropping focus.
$crop_x = 0;
$crop_y = 0;
if (isset($cropping['ratio']) && isset($cropping['focus'])) {
// Calculate cropping dimensions
switch ($cropping['ratio']) {
case 'portrait_ratio':
// Calculate cropping dimensions
if ($ratio < 1) {
$crop_height = ceil($crop_width * 4 / 3);
} else {
$crop_width = ceil($crop_height * 3 / 4);
}
// Calculate resizing dimensions
$w = ceil($h * 3 / 4);
break;
case 'landscape_ratio':
// Calculate cropping dimensions
if ($ratio > 1) {
$crop_width = ceil($crop_height * 4 / 3);
} else {
$crop_height = ceil($crop_width * 3 / 4);
}
// Calculate resizing dimensions
$h = ceil($w * 3 / 4);
break;
}
// Calculate focus coordinates
switch ($cropping['focus']) {
case 'center':
if ($ratio > 1) {
$crop_x = ceil(($cur_width - $crop_width) / 2);
} elseif ($ratio == 1) {
$crop_x = ceil(($cur_width - $crop_width) / 2);
$crop_y = ceil(($cur_height - $crop_height) / 2);
} else {
$crop_y = ceil(($cur_height - $crop_height) / 2);
}
break;
case 'top':
$crop_x = ceil(($cur_width - $crop_width) / 2);
break;
case 'bottom':
$crop_y = $cur_height - $crop_height;
break;
case 'right':
$crop_x = $cur_width - $crop_width;
break;
}
// Cropped image.
$thumb_cropped = imagecreatetruecolor($crop_width, $crop_height);
// Cropped ratio.
$ratio = $crop_width / $crop_height;
// Otherwise, calculate image size according to the source's ratio.
} else {
$w = round($h * $ratio);
if ($cur_width > $cur_height) {
$h = round($w / $ratio);
} else {
$w = round($h * $ratio);
}
}

// Resized image.
$thumb = imagecreatetruecolor($w, $h);

$image = false;
switch ($ext) {
case 'jpg':
$image = imagecreatefromjpeg($source);
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
// Crop
if ($thumb_cropped !== false) {
// First, crop.
imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
// Then, resize.
imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
// Resize
} else {
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
}
imagejpeg($thumb, $dest);
break;
case 'png':
$image = imagecreatefrompng($source);
// Turn off alpha blending and set alpha flag. That prevent alpha
// transparency to be saved as an arbitrary color (black in my tests)
imagealphablending($thumb, false);
imagealphablending($image, false);
imagesavealpha($thumb, true);
imagesavealpha($image, true);
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
// Crop
if ($thumb_cropped !== false) {
imagealphablending($thumb_cropped, false);
imagesavealpha($thumb_cropped, true);
// First, crop.
imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
// Then, resize.
imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
// Resize
} else {
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
}
imagepng($thumb, $dest);
break;
case 'gif':
$image = imagecreatefromgif($source);
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
// Crop
if ($thumb_cropped !== false) {
// First, crop.
imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
// Then, resize.
imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
// Resize
} else {
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
}
imagegif($thumb, $dest);
break;
case 'webp':
$image = imagecreatefromwebp($source);
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
// Crop
if ($thumb_cropped !== false) {
// First, crop.
imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
// Then, resize.
imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
// Resize
} else {
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
}
imagewebp($thumb, $dest);
break;
}
Expand Down
4 changes: 4 additions & 0 deletions galette/lib/Galette/Core/Preferences.php
Expand Up @@ -102,6 +102,8 @@
* @property string $pref_etiq_rows
* @property string $pref_etiq_corps
* @property boolean $pref_etiq_border
* @property boolean $pref_force_picture_ratio
* @property string $pref_member_picture_ratio
* @property string $pref_card_abrev
* @property string $pref_card_strip
* @property string $pref_card_tcol
Expand Down Expand Up @@ -240,6 +242,8 @@ class Preferences
'pref_etiq_corps' => 12,
'pref_etiq_border' => true,
/* Preferences for members cards */
'pref_force_picture_ratio' => false,
'pref_member_picture_ratio' => 'square_ratio',
'pref_card_abrev' => 'GALETTE',
'pref_card_strip' => 'Gestion d\'Adherents en Ligne Extrêmement Tarabiscotée',
'pref_card_tcol' => '#FFFFFF',
Expand Down
11 changes: 8 additions & 3 deletions galette/lib/Galette/Entity/Adherent.php
Expand Up @@ -2040,19 +2040,24 @@ public function getParentFields(): array
/**
* Handle files (photo and dynamics files)
*
* @param array $files Files sent
* @param array $files Files sent
* @param array $cropping Cropping properties
*
* @return array|true
*/
public function handleFiles(array $files)
public function handleFiles(array $files, array $cropping = null)
{
$this->errors = [];
// picture upload
if (isset($files['photo'])) {
if ($files['photo']['error'] === UPLOAD_ERR_OK) {
if ($files['photo']['tmp_name'] != '') {
if (is_uploaded_file($files['photo']['tmp_name'])) {
$res = $this->picture->store($files['photo']);
if ($this->preferences->pref_force_picture_ratio == 1 && isset($cropping)) {
$res = $this->picture->store($files['photo'], false, $cropping);
} else {
$res = $this->picture->store($files['photo']);
}
if ($res < 0) {
$this->errors[]
= $this->picture->getErrorMessage($res);
Expand Down
10 changes: 6 additions & 4 deletions galette/lib/Galette/IO/FileInterface.php
Expand Up @@ -54,9 +54,11 @@ interface FileInterface
public const INVALID_FILENAME = -1;
public const INVALID_EXTENSION = -2;
public const FILE_TOO_BIG = -3;
public const MIME_NOT_ALLOWED = -4;
public const NEW_FILE_EXISTS = -5;
public const INVALID_FILE = -6;
public const CANT_WRITE = -7;
public const IMAGE_TOO_SMALL = -4;
public const MIME_NOT_ALLOWED = -5;
public const NEW_FILE_EXISTS = -6;
public const INVALID_FILE = -7;
public const CANT_WRITE = -8;
public const MAX_FILE_SIZE = 2048;
public const MIN_CROP_SIZE = 267;
}
24 changes: 19 additions & 5 deletions galette/lib/Galette/IO/FileTrait.php
Expand Up @@ -76,6 +76,7 @@ trait FileTrait
protected $allowed_extensions = array();
protected $allowed_mimes = array();
protected $maxlenght;
protected $mincropsize;

public static $mime_types = array(
'txt' => 'text/plain',
Expand Down Expand Up @@ -181,18 +182,20 @@ trait FileTrait
/**
* Initialization
*
* @param string $dest File destination directory
* @param array $extensions Array of permitted extensions
* @param array $mimes Array of permitted mime types
* @param int $maxlenght Maximum lenght for each file
* @param string $dest File destination directory
* @param array $extensions Array of permitted extensions
* @param array $mimes Array of permitted mime types
* @param int $maxlenght Maximum lenght for each file
* @param int $mincropsize Minimum image side size required for cropping
*
* @return void
*/
protected function init(
$dest,
$extensions = null,
$mimes = null,
$maxlenght = null
$maxlenght = null,
$mincropsize = null
) {
if ($dest !== null && substr($dest, -1) !== '/') {
//normalize path
Expand All @@ -210,6 +213,11 @@ protected function init(
} else {
$this->maxlenght = self::MAX_FILE_SIZE;
}
if ($mincropsize !== null) {
$this->mincropsize = $mincropsize;
} else {
$this->mincropsize = self::MIN_CROP_SIZE;
}
}

/**
Expand Down Expand Up @@ -492,6 +500,12 @@ protected function getErrorMessageFromCode($code)
_T("File is too big. Maximum allowed size is %dKo")
);
break;
case self::IMAGE_TOO_SMALL:
$error = sprintf(
_T("Image is too small. The minimum image side size allowed is %spx"),
$this->mincropsize
);
break;
case self::MIME_NOT_ALLOWED:
/** FIXME: should be more descriptive */
$error = _T("Mime-Type not allowed");
Expand Down

0 comments on commit 656a271

Please sign in to comment.