feat(attachment): refactor image cropping functionality to use range objects and update related tests

This commit is contained in:
2026-08-18 12:54:47 -03:00
parent 58cc35fd61
commit 9cfd49f233
16 changed files with 496 additions and 54 deletions

View File

@@ -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) {