aws s3에서 가져온 webp를 jpeg로 변환하기

effiRin·2023년 1월 4일
0
  • 문제
    AWS S3에서 가져온 WEBP 파일을 JPEG로 변환해서 다시 S3에 업로드하기.

  • 해결 과정

STEP 1. AWS S3에서 WEBP 파일 다운받기

  1. 우선 AmazonS3 Client의 메소드 'getObjectContent'를 이용해서 S3에 저장된 파일을 다운로드 받는다. 이때 성공적이면 해당 메소드는 S3ObjectInputStream을 반환한다.

S3ObjectInputStream getObjectContent()
Gets the input stream containing the contents of this object.

   public File getFileFromBucket(String fileName) {
        GetObjectRequest getObjectRequest = new GetObjectRequest(aWSBucketName, fileName);
        S3Object s3Object = aWSS3client.getObject(getObjectRequest);
        File s3File = new File(fileName);
        try (FileOutputStream fos = new FileOutputStream(s3File)) { //throws Exception
            IOUtils.copy(s3Object.getObjectContent(), fos); 
        } catch (IOException e) {
            log.debug("IOException Occurred while fetching file {}", fileName);
            e.printStacktrace();
        }
        return s3File;
    }


2. getObjectContent를 통해 받은 S3ObjectInputStream은 InputStream을 상속받는다. 따라서 스택오버플로우에 있는 아래 코드를 참고 했다.

How to convert InputStream to virtual File

  public static File stream2file (InputStream in) throws IOException {
        final File tempFile = File.createTempFile(PREFIX, SUFFIX);
        tempFile.deleteOnExit();
        try (FileOutputStream out = new FileOutputStream(tempFile)) {
            IOUtils.copy(in, out);
        }
        return tempFile;

(1) createTempFile()로 임시 파일을 만들어준다.
일단 나의 경우엔 변환을 위해 잠시 만들 파일이라서 tempFile을 선택했다.
참고로 이 메소드는 로컬이 아닌 캐시에 있는 임시 폴더에 임시 파일을 만든다.

(2) deleteOnExit()를 해준다.
이 메소드를 해주면 JVM이 종료됨과 동시에 해당 파일이 삭제가 된다.
어차피 변환하면서 계속 저장해줄 필요도 없었기 때문에 이 설정을 해주었다.
(안 해주면 변환하면서 파일이 계속 쌓임)



(3) 이제 IOUtils.copy를 이용해서 S3ObjectInputstream을 FileOutputStream으로 바꾸려고 했는데...!!!
내가 원하는 것은 s3에서 다운받은 webp 파일을 JPEG로 변환한 후, 파일로 저장이였다.
다운 받은 원본 파일을 그대로 저장할 거였으면 이걸 사용해도 되겠지만, 나의 경우엔 FileOutputStream으로 나오기 전에 jpeg 변환 과정을 한번 거쳐야 했다.


그래서 JPEG 변환을 위해 여기서부터 코드 수정 시작...



STEP 2. JPEG로 변환하기

(4) JPEG 변환을 위한 설정값을 줄 때 아래 코드를 참고하려고 했다.

Setting jpg compression level with ImageIO in Java

  ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam();
jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
jpgWriteParam.setCompressionQuality(0.7f);


  • 그러려면 ImageWriter와 ImageWriteParam을 쓰는 것 같은데 이게 뭘까...?

    오라클 공식 문서 - ImageWriter 를 보았다.

    An abstract superclass for encoding and writing images. This class must be subclassed by classes that write out images in the context of the Java Image I/O framework.

    대충 번역하면...
    "이미지 인코딩과 쓰기에 사용되는 클래스다. 이 클래스는 Java Image I/O 프레임워크의 context에서 이미지가 쓰여지는 클래스의 subclass(구현 상속)가 되어야만 한다."

이게 무슨 소릴까...?

그래서 아래에 있는 코드를 보았다.

Mimetypes for ImageIO read() and write()

   int width = 200, height = 200;
    BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

    Graphics2D ig2 = bi.createGraphics();
    ig2.fillRect(0, 0, width - 1, height - 1);

    Iterator imageWriters = getImageWritersByMIMEType("image/gif");
    ImageWriter imageWriter = (ImageWriter) imageWriters.next();
    File file = new File("filename.gif");
    ImageOutputStream ios = ImageIO.createImageOutputStream(file);
    imageWriter.setOutput(ios);
    imageWriter.write(bi);  

ImageWriter가 사용되는 또 다른 코드인데 잘 보면 맨 마지막줄
imageWriter.write(bi)
즉, ImageWriter로 어떤 이미지 클래스에 이미지를 write해주었는데, 그 클래스가 바로 BufferedImage인 것을 볼 수 있다.


The BufferedImage subclass describes an Image with an accessible buffer of image data. A BufferedImage is comprised of a ColorModel and a Raster of image data. The number and types of bands in the SampleModel of the Raster must match the number and types required by the ColorModel to represent its color and alpha components

대충 번역하면
"BufferedImage는 이미지 데이터의 buffer에 접근 가능한 이미지로 설명되는 하위 클래스다..."


여튼 지금까지의 코드를 보면 저 ImageWriterParam을 통해서 jpeg 변환 설정값을 주고, ImageWriter를 통해서 jpeg로 변환된 이미지를 BufferedImage에 써주면 될 것 같았다.


  • ImageIO.read

그러기 위해선 우선 s3ObjectInputStream을 읽어줘야 했는데, ImageIO.read(inputStream) 로 읽어줬다. 이때 반환값은 BufferedImage다.

static BufferedImage read(InputStream input)

ImageIO 찾아보니까 이런 말도 써있다.

A class containing static convenience methods for locating ImageReaders and ImageWriters, and performing simple encoding and decoding.

오라클 공식 문서 - ImageIO

즉, 정리하자면 원본(webp) BufferedImage → 변환(jpeg) BufferedImage로 바꿔주는데,
이 사이에 ImageWriter(이미지 write 해주는 클래스), ImageWriteParam(jpeg 설정)을 통해 변환이 이루어지는 것이다.



STEP 3. 최종 정리

(1) s3Client.getObjectContent()으로 s3에 있는 webp 파일 다운-> 이때 S3ObjectInputStream 반환
(2) s3ObjectInputStream를 ImageIO.read()로 읽기 -> BufferedImage로 반환
(3) 해당 BufferedImage를 ImageWriter의 setOutput() 메소드를 통해 FileOutputStream을 설정해준다. 이때 FileOutputStream에 의해 만들어질 File, 즉 결과물(jpeg)을 createTempFile로 임시 File이다.
(4) ImageWriteParam으로 jpeg 변환 설정해주고, ImageWriter로 BufferedImage인 webp를 jpeg로 변환해서 write
(5) write된 임시 파일(File 객체)은 이제 jpeg 파일이다. 이 파일을 s3 업로드 해주면 끝



📌 ImageIO.read() 할 때 못 읽고 null을 뱉는 에러

처음에 webp 파일을 직접 넣어서 읽으려고 했다. 그러다보니 자꾸 null을 뱉는 에러가 발생함.

java8 ImageIO does not support webp image format
converting .webp to .jpeg using Java

왜냐하면 ImageIO는 webp 파일을 지원하지 않기 때문

It seems that ImageIO  is not able to read webp images. As you can read in the docs, the method read returns null in this case. I think that you have to use an additional library to read and write webp images.

스택오버플로우 말씀 따라 공식문서를 확인해보니 정말로 ImageIO가 지원하는 파일 형식은 GIF, PNG, JPEG, BMP, and WBMP였다.

https://docs.oracle.com/javase/tutorial/2d/images/loadimage.html

Image I/O has built-in support for GIF, PNG, JPEG, BMP, and WBMP. Image I/O is also extensible so that developers or administrators can "plug-in" support for additional formats. For example, plug-ins for TIFF and JPEG 2000 are separately available.

플러그인을 쓰면 포맷을 추가적으로 확장할 수 있다고 하는데, 플러그인을 쓰고 싶지 않아서 S3ObjectInputStream을 바로 넣는 방법을 선택했음. (그리고 굳이 File로 넣을 필요 없었고 inputStream 넣는게 더 효율적인데 삽질했다)


고민할 부분 : flush(), close() 에 대해


  1. [Java] getS3Object 후 스트림을 닫아야하는 이유

S3 object 를 조회해오는 코드를 살펴보다가 낯선 코드가 보여서 한 번 살펴봤습니다.

public S3Object getObject(String filePath) {
	...
   try (S3Object s3Object = amazonS3Client.getObject(bucket, filePath)) {
	...
  } catch (Exception e) {
  ...
  }
}

일단 try() {} 구문 자체도 눈에 익지 않아서 찾아보니 try-with-resources-Statement 라는 try 리소스 문 이더라고요.

Java 7 부터 추가된 문법인데 () 안에 java.io.Closeable을 구현하는 리소스들은 Statement 가 끝날 때 리소스가 닫히는걸 보장한다고 해요.

이전에 닫는걸 보장해줬던 것들은 BufferedReader나 DB 커넥션 같은 것들이었는데, S3Object 는 받아오면 끝일거 같은데 왜 닫아줘야하지? 라는 생각을 했는데요.

Closeable S3Objects - AWS에 따르면 2013년부터 S3Object 클래스는 Closeable 인터페이스를 구현하고 있고, S3Object는 HTTP connection으로부터 데이터를 스트리밍할 수 있는 S3ObjectInputStream을 포함하고 있습니다.

S3Object를 조회하기 위해 getObject 를 호출하면 HTTP Connection이 계속해서 열려있고 대기중이므로 스트림을 빠르게 읽고 HTTP 연결이 제대로 해제될 수 있도록 스트림을 닫아야한다고 안내하고 있습니다.

그러므로 getObject를 한 후에도 스트림을 닫아줘야합니다.


닫아주는 방식은 위의 코드처럼 try 리소스 문을 활용해 저절로 닫히게 할 수도 있고, 아니면 s3Object.close()를 직접 호출해서 닫아줘도 됩니다.



  1. [자바][IO] 텍스트 파일 생성하고 파일에 텍스트 쓰기 (3) - FileOutputStream 활용

그리고 flush() 메소드까지 호출한 후에 close() 메소드를 통해 FileOutputStream의 인스턴스를 닫아주어야 한다는 점도 다른점입니다.

try {
            fos = new FileOutputStream(file);            
            // FileOutputStream 클래스가 파일에 바이트를 내보내는 역할을 하는 클래스이므로
            // 내보낼 내용을 바이트로 변환을 하는 작업이 필요합니다.
            byte[] content = message.getBytes();
            fos.write(content);
            fos.flush();
            fos.close();

→ 아마도... flush랑 close 해줘야 할 것 같다...


  1. File.createTempFileFiles.createTempFile 의 차이
    나는 전자를 쓰긴 했는데 이 후자도 있는 것을 발견. 무슨 차이일까? 나중에 알아보자.


  2. jpg와 jpeg
    ImageIO: getImageWritersByMIMEType(String MIMEType) : ImageIO " javax.imageio " Java by API

import java.io.File;
import java.util.Iterator;

import javax.imageio.ImageIO;

public class Main {
  public static void main(String[] argv) throws Exception {
    boolean b;

    b = canWriteMimeType("image/jpg"); // false
    b = canWriteMimeType("image/jpeg"); // true
  }
  // Returns true if the specified mime type can be written
  public static boolean canWriteMimeType(String mimeType) {
    Iterator iter = ImageIO.getImageWritersByMIMEType(mimeType);
    return iter.hasNext();
  }

jpg와 jpeg가 차이가 없는 걸로 알고 있는데, 이런 메소드의 경우엔 차이가 발생하는 것 같기도....


profile
모종삽에서 포크레인까지

0개의 댓글