📘 Backend/Kotlin

이미지 압축 & 회전 & 변환

신건우 2023. 5. 23. 10:11

이미지 압축 & 회전

fun rotateCompressImage(orientation : Int, width : Double, height : Double, file : File) : File {  
    val atf = AffineTransform()  
    var width = width  
    var height = height  

    when (orientation) {  
        1 -> {}  
        2 -> {  
            atf.scale(-1.0, 1.0)  
            atf.translate(-width, 0.0)  
        }  
        3 -> {  
            atf.translate(width, height)  
            atf.rotate(Math.PI)  
        }  
        4 -> {  
            atf.scale(1.0, -1.0)  
            atf.translate(0.0, -height)  
        }  
        5 -> {  
            atf.rotate(-Math.PI / 2)  
            atf.scale(-1.0, 1.0)  
        }  
        6 -> {  
            atf.translate(height, 0.0)  
            atf.rotate(Math.PI / 2)  
        }  
        7 -> {  
            atf.scale(-1.0, 1.0)  
            atf.translate(-height, 0.0)  
            atf.translate(0.0, width)  
            atf.rotate(3 * Math.PI / 2)  
        }  
        8 -> {  
            atf.translate(0.0, width)  
            atf.rotate(3 * Math.PI / 2)  
        }  
    }  

    when (orientation) {  
        5, 6, 7, 8 -> {  
            var tempWidth = width  
            width = height  
            height = tempWidth  
        }  
    }  

    val image: BufferedImage = ImageIO.read(file)  
    val afterImage = BufferedImage(width.toInt(), height.toInt(), image.type)  
    val rotateOp = AffineTransformOp(atf, AffineTransformOp.TYPE_BILINEAR)  
    val rotatedImage = rotateOp.filter(image, afterImage)  
    val iter = ImageIO.getImageWritersByFormatName("jpg")  
    val writer = iter.next()  
    val iwp = writer.defaultWriteParam  
    iwp.compressionMode = ImageWriteParam.MODE_EXPLICIT  
    iwp.compressionQuality = CalculateImageCompressionRatio(file.length())  

    val result = File.createTempFile("compress_image.jpg", "")  
    val fios = FileImageOutputStream(result)  

    writer.output = fios  
    writer.write(null, IIOImage(rotatedImage, null, null), iwp)  
    fios.close()  
    writer.dispose()  

    return result  
}

createTempFile()

createTempFile() 메소드를 사용하여 임시 파일을 생성합니다.

이 파일은 java.io.tmpdir 시스템 프로퍼티에 정의된 디렉토리에서 생성됩니다.

System.getProperty("file.separator")는 파일 경로의 구분자를 의미합니다.

files[0].originalFilename은 업로드된 파일의 원래 파일 이름을 의미합니다.

여기서 files는 클라이언트에서 전송된 파일 목록입니다.

따라서, java.io.tmpdir 디렉토리에 원래 파일 이름과 확장자를 가진 임시 파일이 생성됩니다.

마지막으로, transferTo() 메소드를 사용하여 업로드된 파일을 생성된 임시 파일로 전송합니다.

이 메소드는 업로드된 파일을 디스크에 저장합니다. 이후 임시 파일은 다른 곳에서 사용될 수 있습니다.


transferTo()

files[0]는 클라이언트에서 전송된 첫 번째 파일을 나타냅니다.

만약 다중 파일 업로드를 지원한다면 files는 파일의 목록이 될 것입니다.

transferTo() 메소드는 FileSystemResource 또는 MultipartFile 객체에서 서버의 파일 시스템 경로로 파일을 전송합니다.

이 경우, files[0]에서 transferTo() 메소드를 사용하여, 서버의 임시 파일인 file로 전송합니다.

따라서, 이 코드를 실행하면 클라이언트에서 전송된 첫 번째 파일이 서버의 임시 디렉토리에 files[0].originalFilename과 동일한 이름을 가진 파일로 저장됩니다.

저장된 파일을 이후에 다른 곳에서 사용할 수 있습니다.


FileTypeDetector

MIME 타입을 식별하기 위해 FileTypeDetector 클래스를 사용하는 코드입니다.

먼저, BufferedInputStream을 사용하여 파일의 바이트 스트림을 읽어들입니다.

file.inputStream()은 파일의 바이트 스트림을 생성하는 메소드입니다.

이를 BufferedInputStream으로 감싸는 이유는 파일의 읽기 속도를 향상시키기 위함입니다.

그런 다음 FileTypeDetector 클래스의 detectFileType() 메소드를 사용하여 파일의 MIME 타입을 식별합니다. 이 메소드는 BufferedInputStream을 인자로 받습니다.

detectFileType() 메소드는 식별된 MIME 타입을 반환합니다.

FileTypeDetector 클래스는 자바에서 기본으로 제공되는 클래스가 아니므로, 클래스의 패키지 이름과 함께 import 되어 있어야 합니다.


MIME 타입을 식별하는 데 사용되는 방식은 파일 시그니처라는 것을 이용합니다.

파일 시그니처는 파일의 첫 번째 몇 바이트를 검사하여 파일 유형을 식별합니다.

이를 통해 파일의 확장자가 바뀌어도 MIME 타입을 정확하게 식별할 수 있습니다.


파일 압축

업로드된 이미지 파일이 2MB를 초과할 경우, 이미지를 압축하고 회전시킨 후, Amazon S3에 업로드하는 기능을 구현한 부분입니다.

먼저, 업로드된 파일의 크기가 2MB를 초과하는지를 확인합니다.

파일 크기를 확인한 후, ImageMetadataReader 라이브러리를 사용하여 이미지의 메타데이터를 읽습니다.

이 메타데이터에는 이미지의 방향, 가로 세로 크기 등의 정보가 포함됩니다.


try-catch 블록 내부에서는 ImageMetadataReader를 사용하여 파일의 Exif 메타데이터를 읽어오고, ExifIFD0Directory 및 JpegDirectory 클래스를 사용하여 이미지의 방향, 가로 및 세로 크기 등의 정보를 가져옵니다.

그 다음으로는 rotateCompressImage() 함수를 호출하여 이미지를 압축하고 회전합니다.


압축 및 회전 작업이 성공적으로 수행되면, 새로운 압축 파일을 compressFile 변수에 할당합니다.

compressFile이 null이 아닌 경우에는 UUID를 사용하여 랜덤한 파일 이름을 생성하고, S3 버킷에 업로드합니다.


이때, ObjectMetadata 클래스를 사용하여 압축 파일의 크기와 MIME 타입 정보를 설정합니다.

putObject() 함수를 사용하여 S3에 파일을 업로드하고, 업로드가 성공하면 결과 목록(result)에 파일 경로를 추가합니다.

그리고 마지막으로, 압축 썸네일 업로드가 완료되면 로그를 출력합니다.


Exif Metadata를 이용해 사진을 회전시키는 이유는 뭘까?

이미지의 회전은 Exif 메타데이터 정보 때문입니다. Exif(Exchangeable image file format)는 디지털 카메라에서 촬영된 이미지 파일에 대한 추가 정보를 포함하는 표준 포맷입니다.

카메라에서 이미지를 촬영할 때, 카메라의 방향에 따라서 이미지가 회전되어 저장되는 경우가 있습니다. 이 때 Exif 메타데이터 정보에 회전 정보가 저장되는데, 이 정보를 이용해서 이미지를 보정해야 합니다.

예를 들어, 스마트폰으로 세로 방향으로 촬영한 사진을 업로드하면, 이미지가 90도 회전된 상태로 업로드될 수 있습니다. 이 때 Exif 메타데이터 정보를 이용해서 이미지를 90도 회전시켜서 원래대로 보정해야 합니다.

따라서, 스프링부트에서 썸네일을 구현할 때는 이미지 파일의 Exif 메타데이터 정보를 읽어와서 회전 정보를 파악한 후, 이미지를 회전시켜서 보정해야 합니다. 이를 위해서 ImageMetadataReader와 같은 라이브러리를 사용할 수 있습니다.


S3 파일 업로드

  1. 첫 번째 파일을 S3에 업로드합니다.
    • UUID.randomUUID().toString() 함수를 사용하여 고유한 파일명을 생성합니다.
    • fileLocation() 함수를 사용하여 파일의 S3 저장 위치를 지정합니다.
    • ObjectMetadata() 객체를 생성하여 파일의 크기와 MIME 타입을 설정합니다.
    • ncp.assetNcpS3Client().putObject() 함수를 사용하여 파일을 업로드합니다.
  2. 두 번째부터 마지막 파일까지의 파일을 S3에 업로드합니다.
    • File.createTempFile() 함수를 사용하여 임시 파일을 생성합니다.
    • files[index].transferTo() 함수를 사용하여 원본 파일을 임시 파일에 복사합니다.
    • UUID.randomUUID().toString() 함수를 사용하여 고유한 파일명을 생성합니다.
    • fileLocation() 함수를 사용하여 파일의 S3 저장 위치를 지정합니다.
    • ObjectMetadata() 객체를 생성하여 파일의 크기와 MIME 타입을 설정합니다.
    • ncp.assetNcpS3Client().putObject() 함수를 사용하여 파일을 업로드합니다.
  3. 업로드한 파일의 S3 저장 위치를 result 리스트에 추가합니다.

코드에서는 FileTypeDetector.detectFileType() 함수를 사용하여 파일의 MIME 타입을 추론하고 있습니다.

이 함수는 파일의 내용을 분석하여 파일의 MIME 타입을 추측합니다.

추측한 MIME 타입을 기반으로 ObjectMetadata() 객체의 contentType 속성을 설정합니다.

추가로, bufferedStream 객체는 try 블록 안에서 생성되었지만, catch 블록 안에서만 닫히고 있습니다.

bufferedStream 객체를 닫지 않은 경우에는 메모리 누수가 발생할 수 있으므로, try 블록이 끝날 때 반드시 bufferedStream.close() 함수를 호출해야 합니다.


이미지 압축, 회전하기 전 이미지 데이터 만들기

for (file in files) {

  1. val tempFile = File.createTempFile(System.getProperty("java.io.tmpdir") + System.getProperty("file.separator") + files[0].originalFilename, "")

  2. file.transferTo(tempFile)

  3. var bufferedStream = BufferedInputStream(tempFile.inputStream())
    파일의 내용을 읽기 위해 버퍼링된 입력 스트림 생성

  4. var extension = FileTypeDetector.detectFileType(bufferedStream) , 타입 검증
    파일의 MIME타입 식별, 파일에 실제 유형에 대한 정보를 얻을 수 있음

  5. var fileName = UUID + extension.commonExtention
    파일 확장자를 파일 이름에 추가

  6. var metaData = ObjectMetadata = ObjectMetadata()
    ObjectMetadata 객체 생성, 이 객체는 S3에 업로드되는 파일의 메타데이터를 나타냄

  7. metaData.contentLength = tempFile.length()
    metaData 객체의 contentLength 속성에 tempFile의 길이를 할당함, 업로드되는 파일의 크기를 나타낸다

  8. metaData.contentType = extension.mimeType
    metaData 객체의 contentType 속성에 extension의 MIME 타입을 할당함, 업로드되는 파일의 컨텐츠 타입을 나타냄

}


정리

  • ImageMetaDataReader - 이미지 메타데이터 추출 (JPEG 전용, 외부 라이브러리)
    • readMetadata()
    • getFirstDirectoryOfType()
    • getImageWidth()
    • getImageHeight()
  • ExifIFD0Directory - IFD0 디렉터리의 Exif 태그 설정 (이미지 회전)
  • JpegDirectory - JPG 이미지 정보를 읽기 위한 객체

  • AffineTransform() - 2D좌표로의 선형 매핑을 수행하는 2D 아핀 변환 클래스

    • 아핀 변환은 변환, 크기조정, 뒤집기, 회전 시퀀스를 사용하여 구성할 수 있음
    • scale()
    • translate()
    • AffineTransformOp
  • ImageIO - 이미지 데이터 I/O를 할 수 있는 Python 라이브러리 (크로스 플랫폼)

    • read()
  • ImageWriteParam - 스트림 인코딩 관련 클래스, 특정 이미지 형식을 위한 플러그인

    • MODE_EXPLICIT
      • 향후의 출력 기능을 사용 가능하게 하기 위해서 setTilingMode 또는 setCompressionMode와 같은 메서드에 건네줄 수 있는 정수값
    • dispose()
      • 이미지 해제 메서드
  • IIOImage - 썸네일, 이미지의 메타데이터를 보관 & 유지

  • ObjectMetadata - 유저 제공 메타데이터 + HTTP 헤더 포함