๊ฐœ๋ฐœ/BACKEND

java์—์„œ ์ด๋ฏธ์ง€ ์ž‘์—… ๋ฐ ์ตœ์ ํ™”ํ•˜๊ธฐ

uhanuu 2024. 7. 10. 10:57

html์„ ํŒŒ์‹ฑํ•ด์„œ ์ธ๋„ค์ผ ์ด๋ฏธ์ง€๋ฅผ ์ถ”์ถœํ•ด Amazon S3์— ์ €์žฅํ•ด์•ผ ํ•˜๋Š” ์ผ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ ์ธ ๋งŒํผ ์ด๋ฏธ์ง€ ์šฉ๋Ÿ‰์„ ์ค„์ผ ์ˆ˜ ์žˆ์œผ๋ฉด S3 ๋น„์šฉ๋„ ์•„๋ผ๊ณ  ํ”„๋ก ํŠธ ๋ Œ๋”๋ง ์†๋„ ์—ญ์‹œ ๊ฐœ์„ ๋˜๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐ์ด ๋“ค์—ˆ๊ณ  resize์™€ webp ์ด๋ฏธ์ง€ ํฌ๋งท์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์ฝ๊ธฐ

URL ์ •๋ณด๋ฅผ ํ†ตํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ ์ฝ์–ด์™€์•ผ ํ–ˆ์—ˆ๋Š”๋ฐ Java์—์„œ javax.imageio.ImageIO ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ์ด๋ฏธ์ง€๋ฅผ ์ฝ๊ณ  ์“ธ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

URL ํ˜น์€ ํŒŒ์ผ์„ ์ฝ์–ด์„œ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

BufferedImage image = ImageIO.read(new URL(์ด๋ฏธ์ง€ ์ฃผ์†Œ));
BufferedImage image = ImageIO.read(new File(์ด๋ฏธ์ง€ ์ €์žฅ ์œ„์น˜));
  • java.awt.image.BufferedImage๋ฅผ ํ†ตํ•ด์„œ ์ด๋ฏธ์ง€์˜ width, height, RGB๋“ฑ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

**scrimage ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(์ž๋ฐ”, ์ฝ”ํ‹€๋ฆฐ, ์Šค์นผ๋ผ)๋ฅผ ํ†ตํ•ด์„œ๋„ ์ด๋ฏธ์ง€๋ฅผ ์ฝ์–ด์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.**

ImmutableImage image = ImmutableImage.loader().fromFile(file);

๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋„ ์‰ฝ๊ฒŒ ์–ป์–ด์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ImageMetadata meta = ImageMetadata.fromStream(stream);
Arrays.stream(meta.tags()).forEach(tag -> System.out.println(tag));

์ถœ๋ ฅ

...
Tag{name='Compression Type', type=-3, rawValue='0', value='Baseline'}
Tag{name='Data Precision', type=0, rawValue='8', value='8 bits'}
Tag{name='Image Height', type=1, rawValue='405', value='405 pixels'}
Tag{name='Image Width', type=3, rawValue='594', value='594 pixels'}
Tag{name='Resolution Units', type=7, rawValue='1', value='inch'}
Tag{name='X Resolution', type=8, rawValue='300', value='300 dots'}
Tag{name='Y Resolution', type=10, rawValue='300', value='300 dots'}
Tag{name='Thumbnail Width Pixels', type=12, rawValue='0', value='0'}
Tag{name='Thumbnail Height Pixels', type=13, rawValue='0', value='0'}
Tag{name='Image Width', type=256, rawValue='4928', value='4928 pixels'}
Tag{name='Image Height', type=257, rawValue='3280', value='3280 pixels'}
Tag{name='Bits Per Sample', type=258, rawValue='8 8 8', value='8 8 8 bits/component/pixel'}
Tag{name='Photometric Interpretation', type=262, rawValue='2', value='RGB'}
Tag{name='Image Description', type=270, rawValue='during the Sky Bet Championship match between Middlesbrough and Wolverhampton Wanderers at Riverside Stadium on April 14, 2015 in Middlesbrough, England.', value='during the Sky Bet Championship match between Middlesbrough and Wolverhampton Wanderers at Riverside Stadium on April 14, 2015 in Middlesbrough, England.'}
Tag{name='Make', type=271, rawValue='NIKON CORPORATION', value='NIKON CORPORATION'}
Tag{name='Model', type=272, rawValue='NIKON D4S', value='NIKON D4S'}
Tag{name='Orientation', type=274, rawValue='1', value='Top, left side (Horizontal / normal)'}
Tag{name='Samples Per Pixel', type=277, rawValue='3', value='3 samples/pixel'}
Tag{name='X Resolution', type=282, rawValue='72', value='72 dots per inch'}
Tag{name='Y Resolution', type=283, rawValue='72', value='72 dots per inch'}
...

์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ ์กฐ์ •ํ•˜๊ธฐ

BufferedImage (PNG)

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

public class ReadWriteImage {

    public static void main(String[] args) {

        try {
            URL url = new URL(์ด๋ฏธ์ง€ URL ์ฃผ์†Œ);

// read an image from url
            BufferedImage image = ImageIO.read(url);

// resize image to 300x150
            Image scaledImage = image.getScaledInstance(300, 150, Image.SCALE_DEFAULT);

// save the resize image aka thumbnail
            ImageIO.write(
                    convertToBufferedImage(scaledImage),
                    "png",
                    new File(ํŒŒ์ผ ์ €์žฅ ์ฃผ์†Œ));

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Done");

    }

// convert Image to BufferedImagepublic static BufferedImage convertToBufferedImage(Image img) {

        if (img instanceof BufferedImage) {
            return (BufferedImage) img;
        }

// Create a buffered image with transparency
        BufferedImage bi = new BufferedImage(
                img.getWidth(null), img.getHeight(null),
                BufferedImage.TYPE_INT_ARGB);

        Graphics2D graphics2D = bi.createGraphics();
        graphics2D.drawImage(img, 0, 0, null);
        graphics2D.dispose();

        return bi;
    }

}

JPEG, JPG๋ฅผ ์œ„์— ์ฝ”๋“œ๋กœ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ ค๊ณ  ์˜ˆ์™ธ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋ฐ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • PNG๋Š” ์•ŒํŒŒ ์ฑ„๋„์„ ์ง€์›ํ•˜์ง€๋งŒ JPEG ํŒŒ์ผ์€ ์•ŒํŒŒ ์ฑ„๋„์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์ธ์ง€ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์•ŒํŒŒ์ฑ„๋„์€ RGB 3๊ฐœ์˜ ์ฑ„๋„ ์™ธ์— ํŽธ์ง‘์šฉ ์ •๋ณด๋ฅผ ์ทจ๊ธ‰ํ•˜๋Š” ๋ณด์กฐ์ฑ„๋„์„ ๋งํ•ฉ๋‹ˆ๋‹ค.
BufferedImage.TYPE_INT_ARGB// ์•ŒํŒŒ ์ฑ„๋„ O
BufferedImage.TYPE_3BYTE_BGR// ์•ŒํŒŒ ์ฑ„๋„ X
  • TYPE_3BYTE_BGR๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ์‹œ๋ฉด ์ •์ƒ์ ์œผ๋กœ JEPG, JPG ์ด๋ฏธ์ง€ ํŒŒ์ผ์ด ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

scrimage ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  • resize ๋ฉ”์„œ๋“œ๋„ ์กด์žฌํ•˜์ง€๋งŒ ์ด๋ฏธ์ง€๋ฅผ ์ถ•์†Œํ•˜์‹œ๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•˜์‹œ๋Š” ๋ถ„๋“ค์€ scale ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
image.scaleToWidth(400)// keeps aspect ratio
image.scaleToHeight(200)// keeps aspect ratio
image.scaleTo(400, 400)

WebP

WebP ํŒŒ์ผ์„ ์ €์žฅํ•  ๋•Œ ์••์ถ• ๋ฐฉ์‹(๋ฌด์†์‹ค ์••์ถ•, ์†์‹ค ์••์ถ•)์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ฐ์ดํ„ฐ ์†์‹ค ์—†์ด ๋˜๋Š” ์ค‘์š”ํ•œ ์ •๋ณด๋ฅผ ์†์‹คํ•˜์ง€ ์•Š๊ณ  ์ด๋ฏธ์ง€๋ฅผ ์••์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ฐธ์กฐ)

  • WebP ๋ฌด์†์‹ค ์ด๋ฏธ์ง€๋Š” PNG์— ๋น„ํ•ด ํฌ๊ธฐ๊ฐ€ 26% ๋” ์ž‘์œผ๋ฉฐ, JPEG ์ด๋ฏธ์ง€๋ณด๋‹ค 25~34% ๋” ์ž‘์Šต๋‹ˆ๋‹ค.
  • ์†์‹ค RGB ์••์ถ•์ด ํ—ˆ์šฉ๋˜๋Š” ๊ฒฝ์šฐ ์†์‹ค์ด ์žˆ๋Š” WebP๋Š” ํˆฌ๋ช…๋„๋„ ์ง€์›ํ•˜์—ฌ ์ผ๋ฐ˜์ ์œผ๋กœ PNG์— ๋น„ํ•ด 3๋ฐฐ ์ž‘์€ ํŒŒ์ผ ํฌ๊ธฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

scrimage ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด์„œ WebP๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ

// ๋ฒ„์ „ ์ •๋ณด: <https://sksamuel.github.io/scrimage/changelog/>
implementation("com.sksamuel.scrimage:scrimage-core:4.1.3")
implementation("com.sksamuel.scrimage:scrimage-webp:4.1.3")// WebP๋ฅผ ์œ„ํ•ด ์ถ”๊ฐ€

๊ฐ„๋‹จํ•œ ์˜ˆ์ œ

// ์ฝ๊ธฐ
ImmutableImage.loader().fromFile(new File("someimage.webp"))
// ์“ฐ๊ธฐ
myimage.output(WebpWriter.MAX_LOSSLESS_COMPRESSION,"output.webp");

Gif๋„ ์ฝ๊ณ  ์“ธ ์ˆ˜ ์žˆ๋‹ค.

// ์ฝ๊ธฐ
AnimatedGifReader.read(ImageSource.of(File("animated.gif"));
// ์“ฐ๊ธฐ
animatedGif.bytes(Gif2WebpWriter.DEFAULT);
animatedGif.output(Gif2WebpWriter.DEFAULT, "output.webp");

ํ”„๋กœ์ ํŠธ ์ ์šฉ ์ฝ”๋“œ (์ฝ”ํ‹€๋ฆฐ)

URL ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์™€์•ผ ํ–ˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ImageIO๋ฅผ ํ†ตํ•ด์„œ BufferedImage๋ฅผ ๊ฐ€์ ธ์˜จ ๋‹ค์Œ์— width๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์‚ฌ์ด์ฆˆ๋ฅผ ์กฐ์ •ํ•˜๊ณ  .webp๋กœ ๋ณ€ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

fun imageResizeAndConvertWebp(image: BufferedImage): String {
        val immutableImage = getImmutableImage(image)
                .scaleToWidth(TARGET_WIDTH)
                .output(WebpWriter.DEFAULT, File("$path/${UUID.randomUUID()}$WEBP_SUFFIX"))
}

private fun getImmutableImage(image: BufferedImage): ImmutableImage =
            ImmutableImage.create(
                    image.width,
                    image.height,
                    PixelFactory.getPixelArrayFromImage(image),
                    BufferedImage.TYPE_3BYTE_BGR
            )

ImmutableImage.create ๋ฉ”์„œ๋“œ์—์„œ Pixel ํƒ€์ž…์˜ ๋ฐฐ์—ด์„ ๋ฐ›๊ฒŒ ๋˜๋Š”๋ฐ stackoverflow ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ œ๊ณตํ•ด์ฃผ์…”์„œ ์ž‘์„ฑํ•œ๊ฑธ ์ด์šฉํ•ด์„œ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

   private static int[][] convertTo2DWithoutUsingGetRGB(BufferedImage image) {

      final byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
      final int width = image.getWidth();
      final int height = image.getHeight();
      final boolean hasAlphaChannel = image.getAlphaRaster() != null;

      int[][] result = new int[height][width];
      if (hasAlphaChannel) {
         final int pixelLength = 4;
         for (int pixel = 0, row = 0, col = 0; pixel + 3 < pixels.length; pixel += pixelLength) {
            int argb = 0;
            argb += (((int) pixels[pixel] & 0xff) << 24);// alpha
            argb += ((int) pixels[pixel + 1] & 0xff);// blue
            argb += (((int) pixels[pixel + 2] & 0xff) << 8);// green
            argb += (((int) pixels[pixel + 3] & 0xff) << 16);// red
            result[row][col] = argb;
            col++;
            if (col == width) {
               col = 0;
               row++;
            }
         }
      } else {
         final int pixelLength = 3;
         for (int pixel = 0, row = 0, col = 0; pixel + 2 < pixels.length; pixel += pixelLength) {
            int argb = 0;
            argb += -16777216;// 255 alpha
            argb += ((int) pixels[pixel] & 0xff);// blue
            argb += (((int) pixels[pixel + 1] & 0xff) << 8);// green
            argb += (((int) pixels[pixel + 2] & 0xff) << 16);// red
            result[row][col] = argb;
            col++;
            if (col == width) {
               col = 0;
               row++;
            }
         }
      }

      return result;
   }
  • ๊ธฐ์กด์˜ ์ค‘์ฒฉ ๋ฐ˜๋ณต๋ฌธ ๋ณด๋‹ค 10๋ฐฐ ์ด์ƒ ๋น ๋ฅธ ์ฝ”๋“œ

๊ฒฐ๊ณผ

JPEG

 

84.36kB -> 39.26kB(Resize Image) -> 23.02kB (Convert WebP) 72.71% ๊ฐœ์„ 

 

PNG (์†์‹ค ์••์ถ•)

920.3 kB -> 366.94kB(Resize Image) -> 22.4kB(Convert WebP) 97.6% ๊ฐœ์„ 

๐Ÿ“š reference