feat(attachment): refactor image cropping functionality to use range objects and update related tests
This commit is contained in:
@@ -17,11 +17,11 @@ class AttachmentService
|
||||
public function storeCroppedImage(
|
||||
UploadedFile|string $image,
|
||||
string $path,
|
||||
float $horizontalCropPercentage,
|
||||
float $verticalCropPercentage,
|
||||
array $cropHorizontal,
|
||||
array $cropVertical,
|
||||
): Attachment {
|
||||
$this->validateCropPercentage($horizontalCropPercentage, 'horizontal');
|
||||
$this->validateCropPercentage($verticalCropPercentage, 'vertical');
|
||||
$cropHorizontal = $this->validateCropRange($cropHorizontal, 'horizontal');
|
||||
$cropVertical = $this->validateCropRange($cropVertical, 'vertical');
|
||||
|
||||
$storedPaths = [];
|
||||
|
||||
@@ -29,8 +29,8 @@ class AttachmentService
|
||||
return DB::transaction(function () use (
|
||||
$image,
|
||||
$path,
|
||||
$horizontalCropPercentage,
|
||||
$verticalCropPercentage,
|
||||
$cropHorizontal,
|
||||
$cropVertical,
|
||||
&$storedPaths,
|
||||
): Attachment {
|
||||
$original = $this->store($image, $path);
|
||||
@@ -38,8 +38,8 @@ class AttachmentService
|
||||
|
||||
$croppedContents = $this->cropImage(
|
||||
$this->imageContents($image),
|
||||
$horizontalCropPercentage,
|
||||
$verticalCropPercentage,
|
||||
$cropHorizontal,
|
||||
$cropVertical,
|
||||
);
|
||||
$cropped = $this->store(
|
||||
'data:'.$croppedContents['mime_type'].';base64,'.base64_encode($croppedContents['contents']),
|
||||
@@ -48,8 +48,8 @@ class AttachmentService
|
||||
$storedPaths[] = $cropped->path;
|
||||
|
||||
$original->update([
|
||||
'crop_horizontal_start_percent' => $horizontalCropPercentage,
|
||||
'crop_vertical_start_percent' => $verticalCropPercentage,
|
||||
'crop_horizontal' => $cropHorizontal,
|
||||
'crop_vertical' => $cropVertical,
|
||||
'cropped_attachment_id' => $cropped->id,
|
||||
]);
|
||||
|
||||
@@ -64,6 +64,51 @@ class AttachmentService
|
||||
}
|
||||
}
|
||||
|
||||
public function updateImageCrop(
|
||||
Attachment $original,
|
||||
array $cropHorizontal,
|
||||
array $cropVertical,
|
||||
): Attachment {
|
||||
if ($original->type !== AttachmentType::Image) {
|
||||
throw new AttachmentStorageException('Only image attachments can be cropped.');
|
||||
}
|
||||
|
||||
$cropHorizontal = $this->validateCropRange($cropHorizontal, 'horizontal');
|
||||
$cropVertical = $this->validateCropRange($cropVertical, 'vertical');
|
||||
$contents = Storage::disk('s3')->get($original->path);
|
||||
|
||||
if (! is_string($contents) || $contents === '') {
|
||||
throw new AttachmentStorageException('The original image could not be read from the s3 disk.');
|
||||
}
|
||||
|
||||
$croppedContents = $this->cropImage($contents, $cropHorizontal, $cropVertical);
|
||||
$directory = trim(str_replace('\\', '/', dirname($original->path)), './');
|
||||
$directory = $directory !== '' ? $directory : 'attachments';
|
||||
$previousCrop = $original->croppedAttachment;
|
||||
$cropped = $this->store(
|
||||
'data:'.$croppedContents['mime_type'].';base64,'.base64_encode($croppedContents['contents']),
|
||||
$directory,
|
||||
);
|
||||
|
||||
try {
|
||||
$original->update([
|
||||
'crop_horizontal' => $cropHorizontal,
|
||||
'crop_vertical' => $cropVertical,
|
||||
'cropped_attachment_id' => $cropped->id,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->delete($cropped);
|
||||
|
||||
throw $throwable;
|
||||
}
|
||||
|
||||
if ($previousCrop !== null && ! $previousCrop->is($cropped)) {
|
||||
$this->delete($previousCrop);
|
||||
}
|
||||
|
||||
return $original->refresh()->load('croppedAttachment');
|
||||
}
|
||||
|
||||
public function store(
|
||||
UploadedFile|string $file,
|
||||
string $path,
|
||||
@@ -168,13 +213,34 @@ class AttachmentService
|
||||
return trim($path, '/');
|
||||
}
|
||||
|
||||
protected function validateCropPercentage(float $percentage, string $axis): void
|
||||
/**
|
||||
* @param array{start_percentage?: mixed, end_percentage?: mixed} $range
|
||||
* @return array{start_percentage: float, end_percentage: float}
|
||||
*/
|
||||
protected function validateCropRange(array $range, string $axis): array
|
||||
{
|
||||
if (! is_finite($percentage) || $percentage < 0 || $percentage >= 100) {
|
||||
$start = $range['start_percentage'] ?? null;
|
||||
$end = $range['end_percentage'] ?? null;
|
||||
|
||||
if (! is_numeric($start) || ! is_numeric($end)) {
|
||||
throw new AttachmentStorageException(
|
||||
"The {$axis} crop percentage must be greater than or equal to 0 and less than 100."
|
||||
"The {$axis} crop range must contain numeric start_percentage and end_percentage values."
|
||||
);
|
||||
}
|
||||
|
||||
$start = (float) $start;
|
||||
$end = (float) $end;
|
||||
|
||||
if (! is_finite($start) || ! is_finite($end) || $start < 0 || $end > 100 || $start >= $end) {
|
||||
throw new AttachmentStorageException(
|
||||
"The {$axis} crop range must satisfy 0 <= start_percentage < end_percentage <= 100."
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'start_percentage' => $start,
|
||||
'end_percentage' => $end,
|
||||
];
|
||||
}
|
||||
|
||||
protected function imageContents(UploadedFile|string $image): string
|
||||
@@ -197,8 +263,8 @@ class AttachmentService
|
||||
*/
|
||||
protected function cropImage(
|
||||
string $contents,
|
||||
float $horizontalCropPercentage,
|
||||
float $verticalCropPercentage,
|
||||
array $cropHorizontal,
|
||||
array $cropVertical,
|
||||
): array {
|
||||
$source = @imagecreatefromstring($contents);
|
||||
|
||||
@@ -208,13 +274,15 @@ class AttachmentService
|
||||
|
||||
$width = imagesx($source);
|
||||
$height = imagesy($source);
|
||||
$x = min($width - 1, (int) floor($width * $horizontalCropPercentage / 100));
|
||||
$y = min($height - 1, (int) floor($height * $verticalCropPercentage / 100));
|
||||
$x = min($width - 1, (int) floor($width * $cropHorizontal['start_percentage'] / 100));
|
||||
$right = min($width, (int) ceil($width * $cropHorizontal['end_percentage'] / 100));
|
||||
$y = min($height - 1, (int) floor($height * $cropVertical['start_percentage'] / 100));
|
||||
$bottom = min($height, (int) ceil($height * $cropVertical['end_percentage'] / 100));
|
||||
$cropped = imagecrop($source, [
|
||||
'x' => $x,
|
||||
'y' => $y,
|
||||
'width' => $width - $x,
|
||||
'height' => $height - $y,
|
||||
'width' => $right - $x,
|
||||
'height' => $bottom - $y,
|
||||
]);
|
||||
|
||||
if ($cropped === false) {
|
||||
|
||||
Reference in New Issue
Block a user