getClientOriginalName() : 'file'; $extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION)); $storedBasename = trim((string) ($options['stored_basename'] ?? '')); $displayName = trim((string) ($options['display_name'] ?? '')); if ($storedBasename !== '') { $sanitizedBase = trim((string) Str::of(pathinfo($storedBasename, PATHINFO_FILENAME))->slug('-'), '-'); $sanitizedBase = $sanitizedBase !== '' ? $sanitizedBase : 'file'; $storedName = $sanitizedBase . ($extension !== '' ? '.' . $extension : ''); $path = $file->storeAs($directory, $storedName, 'public'); } else { $path = $file->store($directory, 'public'); $storedName = basename($path); } if ($displayName === '') { $displayName = $storedName !== '' ? $storedName : $originalName; } $mime = TicketAttachment::normalizeMimeType( method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '', $originalName, $path, ); $metadata = []; if (str_starts_with($mime, 'image/')) { $metadata = $this->extractImageMetadata($path); $this->optimizeImage($path, $mime, $metadata); $mime = TicketAttachment::normalizeMimeType($mime, $originalName, $path); } $size = 0; try { $size = (int) Storage::disk('public')->size($path); } catch (\Throwable) { $size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0); } return [ 'path' => $path, 'mime' => $mime, 'size' => $size, 'original_name' => $displayName, 'source_original_name' => $originalName, 'stored_name' => $storedName, 'metadata' => $metadata, ]; } private function optimizeImage(string $path, string $mime, array $metadata = []): void { $absolutePath = Storage::disk('public')->path($path); if (! is_file($absolutePath) || ! function_exists('getimagesize')) { return; } $source = $this->createImageResource($absolutePath, $mime); if (! $source) { return; } $source = $this->applyOrientation($source, (int) ($metadata['orientation'] ?? 1)); $width = imagesx($source); $height = imagesy($source); if ($width <= 0 || $height <= 0) { imagedestroy($source); return; } $maxWidth = 1600; $maxHeight = 1600; $ratio = min($maxWidth / $width, $maxHeight / $height, 1); $targetW = (int) max(1, round($width * $ratio)); $targetH = (int) max(1, round($height * $ratio)); $target = imagecreatetruecolor($targetW, $targetH); if ($target === false) { imagedestroy($source); return; } if (in_array($mime, ['image/png', 'image/webp'], true)) { imagealphablending($target, false); imagesavealpha($target, true); $transparent = imagecolorallocatealpha($target, 255, 255, 255, 127); imagefilledrectangle($target, 0, 0, $targetW, $targetH, $transparent); } imagecopyresampled($target, $source, 0, 0, 0, 0, $targetW, $targetH, $width, $height); switch ($mime) { case 'image/png': imagepng($target, $absolutePath, 6); break; case 'image/webp': if (function_exists('imagewebp')) { imagewebp($target, $absolutePath, 82); } break; default: imagejpeg($target, $absolutePath, 82); break; } imagedestroy($target); imagedestroy($source); } private function createImageResource(string $absolutePath, string $mime) { return match ($mime) { 'image/png' => function_exists('imagecreatefrompng') ? @imagecreatefrompng($absolutePath) : false, 'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($absolutePath) : false, default => function_exists('imagecreatefromjpeg') ? @imagecreatefromjpeg($absolutePath) : false, }; } private function applyOrientation($source, int $orientation) { $angle = match ($orientation) { 3 => 180, 6 => 270, 8 => 90, default => 0, }; if ($angle === 0 || ! function_exists('imagerotate')) { return $source; } $rotated = @imagerotate($source, $angle, 0); if ($rotated === false) { return $source; } imagedestroy($source); return $rotated; } private function extractImageMetadata(string $path): array { $absolutePath = Storage::disk('public')->path($path); $metadata = []; $imageSize = @getimagesize($absolutePath); if (is_array($imageSize)) { $metadata['width'] = $imageSize[0] ?? null; $metadata['height'] = $imageSize[1] ?? null; $metadata['mime'] = $imageSize['mime'] ?? null; } if (! function_exists('exif_read_data')) { return $metadata; } try { $exif = @exif_read_data($absolutePath); if (! is_array($exif)) { return $metadata; } $metadata['exif_datetime'] = $exif['DateTimeOriginal'] ?? ($exif['DateTime'] ?? null); $metadata['device'] = trim(implode(' ', array_filter([ $exif['Make'] ?? null, $exif['Model'] ?? null, ]))); $metadata['orientation'] = (int) ($exif['Orientation'] ?? 1); $lat = $this->gpsCoordinateToDecimal($exif['GPSLatitude'] ?? null, $exif['GPSLatitudeRef'] ?? null); $lng = $this->gpsCoordinateToDecimal($exif['GPSLongitude'] ?? null, $exif['GPSLongitudeRef'] ?? null); if ($lat !== null && $lng !== null) { $metadata['gps_decimal'] = ['lat' => $lat, 'lng' => $lng]; $metadata['google_maps_url'] = 'https://maps.google.com/?q=' . $lat . ',' . $lng; } } catch (\Throwable) { } return $metadata; } private function gpsCoordinateToDecimal(mixed $coordinate, mixed $reference): ?float { if (! is_array($coordinate) || count($coordinate) < 3) { return null; } $degrees = $this->gpsValueToFloat($coordinate[0] ?? null); $minutes = $this->gpsValueToFloat($coordinate[1] ?? null); $seconds = $this->gpsValueToFloat($coordinate[2] ?? null); if ($degrees === null || $minutes === null || $seconds === null) { return null; } $decimal = $degrees + ($minutes / 60) + ($seconds / 3600); $ref = strtoupper(trim((string) $reference)); if (in_array($ref, ['S', 'W'], true)) { $decimal *= -1; } return round($decimal, 6); } private function gpsValueToFloat(mixed $value): ?float { if (is_numeric($value)) { return (float) $value; } $raw = trim((string) $value); if ($raw === '') { return null; } if (! str_contains($raw, '/')) { return is_numeric($raw) ? (float) $raw : null; } [$numerator, $denominator] = array_pad(explode('/', $raw, 2), 2, null); if (! is_numeric($numerator) || ! is_numeric($denominator) || (float) $denominator === 0.0) { return null; } return (float) $numerator / (float) $denominator; } }