관리 메뉴

투덜이 개발자

이미지 변환 Thumbnail 클래스 본문

Program Language/PHP

이미지 변환 Thumbnail 클래스

엠투 2025. 6. 2. 10:31
반응형

이미지 변환 Thumbnail 클래스

<?php
	
class Thumbnail {

    /**
     * HEIC 파일을 JPEG로 변환
     *
     * ※ 주의: 서버에 ImageMagick 및 libheif 라이브러리가 설치되어 있어야 함
     *
     * @param string $heicFile 원본 HEIC 파일 경로
     * @return string|false 변환된 JPEG 파일 경로 또는 실패 시 false
     */
    public static function convertHeicToJpeg($heicFile) {
        $jpgFile = preg_replace('/\.heic$/i', '.jpg', $heicFile);

        $cmd = "convert " . escapeshellarg($heicFile) . " " . escapeshellarg($jpgFile);
        exec($cmd, $output, $return_var);

        if ($return_var === 0 && file_exists($jpgFile)) {
            return $jpgFile;
        } else {
            error_log("[HEIC 변환 실패] 파일: $heicFile");
            return false;
        }
    }


    /**
     * 썸네일 이미지 생성
     *
     * @param string $srcFile 원본 이미지 경로
     * @param string $destFile 썸네일 저장 경로 (비우면 자동 생성)
     * @param int $thumbWidth 썸네일 너비
     * @param int|null $thumbHeight 썸네일 높이 (null이면 비율 유지)
     * @param int $quality 저장 품질 (기본 JPEG용)
     * @param bool $cropSquare 정사각형으로 크롭 여부
     * @param array $watermark 워터마크 설정 [file, text, font_path, font_size, text_color, position]
     * @return array|false 썸네일 정보 배열 또는 실패 시 false
     */
    public static function generate(
        $srcFile,
        $destFile = '',
        $thumbWidth = 600,
        $thumbHeight = null,
        $quality = 90,
        $cropSquare = false,
        $watermark = array()
    ) {
        if (!file_exists($srcFile)) {
            error_log("[Thumbnail Error] Source file not found: $srcFile");
            error_log("[Thumbnail Error] Failed to create thumbnail for: $srcFile");
            return false;
        }

        $size = @getimagesize($srcFile);
        // HEIC 확장자 또는 MIME 검사 시 변환 시도
        $ext = strtolower(pathinfo($srcFile, PATHINFO_EXTENSION));
        $mime = function_exists('mime_content_type') ? mime_content_type($srcFile) : '';
        if ($ext === 'heic' || $mime === 'image/heic') {
            $converted = self::convertHeicToJpeg($srcFile);
            if (!$converted) {
                error_log("[Thumbnail Error] HEIC 변환 실패: $srcFile");
                return false;
            }
            $srcFile = $converted;
        }

        $size = @getimagesize($srcFile);
        if ($size === false) {
            error_log("[Thumbnail Error] Invalid image size for: $srcFile");
            return false;
        }

        $imageType = $size[2];
        $orientation = 1;
        $srcImage = false;

        switch ($imageType) {
            case IMAGETYPE_JPEG:
                $srcImage = @imagecreatefromjpeg($srcFile);
                if (function_exists('exif_read_data')) {
                    $exif = @exif_read_data($srcFile);
                    if (!empty($exif['Orientation'])) {
                        $orientation = $exif['Orientation'];
                    }
                }
                break;
            case IMAGETYPE_PNG:
                $srcImage = @imagecreatefrompng($srcFile);
                break;
            case IMAGETYPE_GIF:
                $srcImage = @imagecreatefromgif($srcFile);
                break;
            default:
                error_log("[Thumbnail Error] Unsupported image type for: $srcFile");
                return false;
        }

        if (!$srcImage) {
            error_log("[Thumbnail Error] Failed to create image resource from: $srcFile");
            return false;
        }

        switch ($orientation) {
            case 3: $srcImage = imagerotate($srcImage, 180, 0); break;
            case 6: $srcImage = imagerotate($srcImage, -90, 0); break;
            case 8: $srcImage = imagerotate($srcImage, 90, 0); break;
        }

        $width  = imagesx($srcImage);
        $height = imagesy($srcImage);

        $targetWidth  = $thumbWidth;
        $targetHeight = $thumbHeight !== null ? $thumbHeight : $thumbWidth / ($width / $height);

        $hasWatermark = !empty($watermark['file']) || !empty($watermark['text']);
        $isRotated    = ($orientation !== 1);

        if ($width <= $targetWidth && $height <= $targetHeight && !$hasWatermark && !$isRotated) {
            return array(
                'path'   => $srcFile,
                'size'   => filesize($srcFile),
                'width'  => $width,
                'height' => $height
            );
        }

        if ($cropSquare) {
            $cropSize = min($width, $height);
            $srcX = ($width - $cropSize) / 2;
            $srcY = ($height - $cropSize) / 2;
            $newWidth = $newHeight = $thumbWidth;
        } else {
            $srcX = 0; $srcY = 0; $cropSize = 0;
            if ($width <= $targetWidth && $height <= $targetHeight) {
                $newWidth  = $width;
                $newHeight = $height;
            } else if ($thumbHeight === null) {
                $newWidth = $thumbWidth;
                $newHeight = $thumbWidth / ($width / $height);
            } else {
                $ratio = $width / $height;
                if ($thumbWidth / $thumbHeight > $ratio) {
                    $newHeight = $thumbHeight;
                    $newWidth  = $thumbHeight * $ratio;
                } else {
                    $newWidth  = $thumbWidth;
                    $newHeight = $thumbWidth / $ratio;
                }
            }
        }

        $srcW = $cropSize ? $cropSize : $width;
        $srcH = $cropSize ? $cropSize : $height;

        $dstImg = imagecreatetruecolor($newWidth, $newHeight);
        if ($imageType == IMAGETYPE_PNG || $imageType == IMAGETYPE_GIF) {
            imagecolortransparent($dstImg, imagecolorallocatealpha($dstImg, 0, 0, 0, 127));
            imagealphablending($dstImg, false);
            imagesavealpha($dstImg, true);
        }

        imagecopyresampled($dstImg, $srcImage, 0, 0, $srcX, $srcY, $newWidth, $newHeight, $srcW, $srcH);
        self::applyWatermark($dstImg, $newWidth, $newHeight, $watermark);

        if (empty($destFile)) {
            $destFile = self::getDestFile($srcFile, $newWidth, $newHeight);
        }

        $success = false;
        switch ($imageType) {
            case IMAGETYPE_JPEG: $success = imagejpeg($dstImg, $destFile, $quality); break;
            case IMAGETYPE_PNG:  $success = imagepng($dstImg, $destFile); break;
            case IMAGETYPE_GIF:  $success = imagegif($dstImg, $destFile); break;
        }

        if (is_resource($srcImage)) imagedestroy($srcImage);
        if (is_resource($dstImg)) imagedestroy($dstImg);

        if ($success && file_exists($destFile)) {
            $info = @getimagesize($destFile);
            return array(
                'path'   => $destFile,
                'size'   => filesize($destFile),
                'width'  => $info[0],
                'height' => $info[1]
            );
        }

        return false;
    }

    // 워터마크 이미지 및 텍스트를 지정된 위치에 적용
    /**
     * 이미지에 워터마크 이미지 또는 텍스트 삽입
     *
     * @param resource $img GD 이미지 리소스
     * @param int $imgW 이미지 너비
     * @param int $imgH 이미지 높이
     * @param array $watermark 워터마크 설정값
     */
    private static function applyWatermark(&$img, $imgW, $imgH, $watermark) {
        $position = isset($watermark['position']) ? $watermark['position'] : 'bottom-right';
        $fontSize = isset($watermark['font_size']) ? $watermark['font_size'] : 12;
        $textColor = isset($watermark['text_color']) ? $watermark['text_color'] : array(255,255,255);

        if (!empty($watermark['file']) && file_exists($watermark['file'])) {
            $wmImg = @imagecreatefrompng($watermark['file']);
            if ($wmImg) {
                $wmW = imagesx($wmImg);
                $wmH = imagesy($wmImg);
                list($x, $y) = self::getWatermarkPosition($position, $imgW, $imgH, $wmW, $wmH);
                imagecopy($img, $wmImg, $x, $y, 0, 0, $wmW, $wmH);
                imagedestroy($wmImg);
            }
        }

        if (!empty($watermark['text']) && !empty($watermark['font_path']) && file_exists($watermark['font_path'])) {
            $color = imagecolorallocate($img, $textColor[0], $textColor[1], $textColor[2]);
            $bbox = imagettfbbox($fontSize, 0, $watermark['font_path'], $watermark['text']);
            $textW = abs($bbox[2] - $bbox[0]);
            $textH = abs($bbox[7] - $bbox[1]);
            list($x, $y) = self::getWatermarkPosition($position, $imgW, $imgH, $textW, $textH);
            $y += $textH;
            imagettftext($img, $fontSize, 0, $x, $y, $color, $watermark['font_path'], $watermark['text']);
        }
    }

    // 워터마크 위치 계산
    /**
     * 워터마크 삽입 위치 계산
     *
     * @param string $position 위치 (top-left, top-right, center, bottom-left, bottom-right)
     * @param int $imgW 이미지 너비
     * @param int $imgH 이미지 높이
     * @param int $markW 워터마크 너비
     * @param int $markH 워터마크 높이
     * @return array [x, y] 좌표값
     */
    private static function getWatermarkPosition($position, $imgW, $imgH, $markW, $markH) {
        switch (strtolower($position)) {
            case 'top-left':     return array(10, 10);
            case 'top-right':    return array($imgW - $markW - 10, 10);
            case 'center':       return array(($imgW - $markW) / 2, ($imgH - $markH) / 2);
            case 'bottom-left':  return array(10, $imgH - $markH - 10);
            case 'bottom-right':
            default:             return array($imgW - $markW - 10, $imgH - $markH - 10);
        }
    }

    // 썸네일 저장 경로 자동 생성
    /**
     * 썸네일 저장 파일 경로 자동 생성
     *
     * @param string $srcFile 원본 이미지 경로
     * @param int $w 최종 저장될 이미지 너비
     * @param int $h 최종 저장될 이미지 높이
     * @return string 저장될 썸네일 경로
     */
    private static function getDestFile($srcFile, $w, $h) {
        $srcDir   = dirname($srcFile);
        $srcName  = pathinfo($srcFile, PATHINFO_FILENAME);
        $srcExt   = pathinfo($srcFile, PATHINFO_EXTENSION);
        $thumbDir = $srcDir . '/thumb';
        if (!is_dir($thumbDir)) @mkdir($thumbDir, 0755, true);
        return "$thumbDir/{$srcName}_" . intval($w) . "x" . intval($h) . ".{$srcExt}";
    }
}

/*
사용 예시:

1. 기본 썸네일 생성 (600px 너비 기준)
$result = Thumbnail::generate('/upload/photo.jpg');

2. 썸네일 너비 지정 + 크롭
$result = Thumbnail::generate('/upload/photo.jpg', '', 300, null, 90, true);

3. 워터마크 이미지 또는 텍스트 삽입
$result = Thumbnail::generate('/upload/photo.jpg', '', 300, null, 90, false, array(
    'file'       => '/upload/logo.png',
    'text'       => '엠투',
    'font_path'  => '/fonts/NotoSansKR-Regular.ttf',
    'font_size'  => 14,
    'text_color' => array(255,255,255),
    'position'   => 'bottom-right'
));

리턴값 예시:
Array
(
    [path] => /upload/thumb/photo_300x200.jpg
    [size] => 21503
    [width] => 300
    [height] => 200
)
*/
?>
반응형