-
Notifications
You must be signed in to change notification settings - Fork 4
feat: optimize image #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: optimize image #504
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||||||||||||||||||||||
| package upbrella.be.config | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import org.springframework.boot.context.properties.ConfigurationProperties | ||||||||||||||||||||||||||||
| import org.springframework.boot.context.properties.ConstructorBinding | ||||||||||||||||||||||||||||
| import org.springframework.context.annotation.Configuration | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @Configuration | ||||||||||||||||||||||||||||
| @ConfigurationProperties(prefix = "image.processing") | ||||||||||||||||||||||||||||
| @ConstructorBinding | ||||||||||||||||||||||||||||
| data class ImageProcessingConfig( | ||||||||||||||||||||||||||||
| val sizes: ImageSizes = ImageSizes(), | ||||||||||||||||||||||||||||
| val quality: ImageQuality = ImageQuality() | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
Comment on lines
+7
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect usage of @ConstructorBinding with @configuration. In Spring Boot 2.7+, 🔎 Recommended fixOption 1 (Preferred): Remove @configuration and enable via @EnableConfigurationProperties Remove -@Configuration
@ConfigurationProperties(prefix = "image.processing")
@ConstructorBinding
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)Then add Option 2: Remove @ConstructorBinding and keep @configuration @Configuration
@ConfigurationProperties(prefix = "image.processing")
-@ConstructorBinding
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)This allows Spring to use setter-based binding instead of constructor binding. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| data class ImageSizes( | ||||||||||||||||||||||||||||
| val thumbnail: Int = 300, | ||||||||||||||||||||||||||||
| val medium: Int = 800, | ||||||||||||||||||||||||||||
| val large: Int = 1200 | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| data class ImageQuality( | ||||||||||||||||||||||||||||
| val webp: Float = 0.85f, | ||||||||||||||||||||||||||||
| val jpeg: Float = 0.90f | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package upbrella.be.store.dto.response | ||
|
|
||
| data class ImageUrlsResponse( | ||
| val id: Long?, | ||
| val webp: ImageSizeUrls?, | ||
| val jpeg: ImageSizeUrls | ||
| ) | ||
|
|
||
| data class ImageSizeUrls( | ||
| val thumb: String, | ||
| val medium: String, | ||
| val large: String | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,9 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package upbrella.be.store.entity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import upbrella.be.store.dto.response.ImageSizeUrls | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import upbrella.be.store.dto.response.ImageUrlsResponse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import javax.persistence.* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Entity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8,17 +12,56 @@ class StoreImage( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @JoinColumn(name = "store_detail_id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val storeDetail: StoreDetail? = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var imageUrl: String? = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Column(name = "image_urls", columnDefinition = "TEXT") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var imageUrls: String? = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val id: Long? = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| companion object { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val objectMapper: ObjectMapper = jacksonObjectMapper() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @JvmStatic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun createStoreImage(storeDetail: StoreDetail, imageUrl: String): StoreImage { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return StoreImage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storeDetail = storeDetail, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageUrl = imageUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @JvmStatic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun createStoreImageWithUrls( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storeDetail: StoreDetail, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageUrlsResponse: ImageUrlsResponse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): StoreImage { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return StoreImage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storeDetail = storeDetail, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageUrl = imageUrlsResponse.jpeg.medium, // 기존 호환성 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageUrls = objectMapper.writeValueAsString(imageUrlsResponse), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 다중 URL 구조 반환 (JSON 파싱 또는 기존 URL fallback) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun getImageUrlsResponse(): ImageUrlsResponse { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return if (imageUrls != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 새 포맷: JSON 파싱 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 기존 이미지: 단일 URL을 모든 크기에 사용 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ImageUrlsResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id = id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| webp = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jpeg = ImageSizeUrls( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thumb = imageUrl ?: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| medium = imageUrl ?: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| large = imageUrl ?: "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for JSON parsing. The 🔎 Recommended fix fun getImageUrlsResponse(): ImageUrlsResponse {
return if (imageUrls != null) {
- // 새 포맷: JSON 파싱
- objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
+ try {
+ // 새 포맷: JSON 파싱
+ objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
+ } catch (e: Exception) {
+ // JSON 파싱 실패 시 fallback
+ ImageUrlsResponse(
+ id = id,
+ webp = null,
+ jpeg = ImageSizeUrls(
+ thumb = imageUrl ?: "",
+ medium = imageUrl ?: "",
+ large = imageUrl ?: ""
+ )
+ )
+ }
} else {
// 기존 이미지: 단일 URL을 모든 크기에 사용
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| package upbrella.be.store.service | ||
|
|
||
| import net.coobird.thumbnailator.Thumbnails | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.web.multipart.MultipartFile | ||
| import upbrella.be.config.ImageProcessingConfig | ||
| import java.awt.image.BufferedImage | ||
| import java.io.ByteArrayOutputStream | ||
| import javax.imageio.ImageIO | ||
|
|
||
| @Service | ||
| class ImageProcessingService( | ||
| private val config: ImageProcessingConfig | ||
| ) { | ||
|
|
||
| enum class ImageSize(val maxSize: Int) { | ||
| THUMBNAIL(300), | ||
| MEDIUM(800), | ||
| LARGE(1200); | ||
|
|
||
| companion object { | ||
| fun from(configValue: Int): ImageSize { | ||
| return when (configValue) { | ||
| 300 -> THUMBNAIL | ||
| 800 -> MEDIUM | ||
| 1200 -> LARGE | ||
| else -> throw IllegalArgumentException("Unknown image size: $configValue") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 이미지를 3가지 크기 x 2가지 포맷(WebP, JPEG)으로 처리 | ||
| */ | ||
| fun processImage(file: MultipartFile): ProcessedImageSet { | ||
| val originalImage = ImageIO.read(file.inputStream) | ||
| ?: throw IllegalArgumentException("Invalid image file") | ||
|
|
||
| // 3가지 크기로 리사이징 | ||
| val thumbnail = processSize(originalImage, config.sizes.thumbnail) | ||
| val medium = processSize(originalImage, config.sizes.medium) | ||
| val large = processSize(originalImage, config.sizes.large) | ||
|
|
||
| return ProcessedImageSet(thumbnail, medium, large) | ||
| } | ||
|
|
||
| /** | ||
| * 특정 크기로 리사이징하고 WebP와 JPEG 포맷으로 변환 | ||
| */ | ||
| private fun processSize(originalImage: BufferedImage, targetSize: Int): ImageVariantData { | ||
| // 리사이징 | ||
| val resized = resizeImage(originalImage, targetSize) | ||
|
|
||
| // WebP 변환 | ||
| val webp = convertToWebP(resized, config.quality.webp) | ||
|
|
||
| // JPEG 변환 | ||
| val jpeg = convertToJpeg(resized, config.quality.jpeg) | ||
|
|
||
| return ImageVariantData(webp, jpeg) | ||
| } | ||
|
|
||
| /** | ||
| * 이미지를 지정된 크기로 리사이징 (비율 유지) | ||
| */ | ||
| private fun resizeImage(image: BufferedImage, maxSize: Int): BufferedImage { | ||
| return Thumbnails.of(image) | ||
| .size(maxSize, maxSize) | ||
| .asBufferedImage() | ||
| } | ||
|
|
||
| /** | ||
| * BufferedImage를 WebP 포맷으로 변환 | ||
| */ | ||
| private fun convertToWebP(image: BufferedImage, quality: Float): ByteArray { | ||
| val outputStream = ByteArrayOutputStream() | ||
|
|
||
| // WebP ImageWriter 설정 | ||
| val writer = ImageIO.getImageWritersByFormatName("webp").next() | ||
| ?: throw IllegalStateException("WebP ImageWriter not found. Make sure webp-imageio is on the classpath") | ||
|
|
||
| val writeParam = writer.defaultWriteParam | ||
| if (writeParam.canWriteCompressed()) { | ||
| writeParam.compressionMode = javax.imageio.ImageWriteParam.MODE_EXPLICIT | ||
| writeParam.compressionQuality = quality | ||
| } | ||
|
|
||
| val output = ImageIO.createImageOutputStream(outputStream) | ||
| writer.output = output | ||
| writer.write(null, javax.imageio.IIOImage(image, null, null), writeParam) | ||
| writer.dispose() | ||
| output.close() | ||
|
|
||
| return outputStream.toByteArray() | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * BufferedImage를 JPEG 포맷으로 변환 | ||
| */ | ||
| private fun convertToJpeg(image: BufferedImage, quality: Float): ByteArray { | ||
| val outputStream = ByteArrayOutputStream() | ||
|
|
||
| // RGB 이미지로 변환 (JPEG는 투명도 미지원) | ||
| val rgbImage = if (image.type == BufferedImage.TYPE_INT_ARGB || | ||
| image.type == BufferedImage.TYPE_4BYTE_ABGR) { | ||
| val newImage = BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB) | ||
| val g = newImage.createGraphics() | ||
| g.drawImage(image, 0, 0, null) | ||
| g.dispose() | ||
| newImage | ||
| } else { | ||
| image | ||
| } | ||
|
|
||
| val writer = ImageIO.getImageWritersByFormatName("jpeg").next() | ||
| val writeParam = writer.defaultWriteParam | ||
| writeParam.compressionMode = javax.imageio.ImageWriteParam.MODE_EXPLICIT | ||
| writeParam.compressionQuality = quality | ||
|
|
||
| val output = ImageIO.createImageOutputStream(outputStream) | ||
| writer.output = output | ||
| writer.write(null, javax.imageio.IIOImage(rgbImage, null, null), writeParam) | ||
| writer.dispose() | ||
| output.close() | ||
|
|
||
| return outputStream.toByteArray() | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * 3가지 크기의 이미지 데이터 세트 | ||
| */ | ||
| data class ProcessedImageSet( | ||
| val thumbnail: ImageVariantData, | ||
| val medium: ImageVariantData, | ||
| val large: ImageVariantData | ||
| ) | ||
|
|
||
| /** | ||
| * 각 크기별 WebP와 JPEG 데이터 | ||
| */ | ||
| data class ImageVariantData( | ||
| val webp: ByteArray, | ||
| val jpeg: ByteArray | ||
| ) { | ||
| override fun equals(other: Any?): Boolean { | ||
| if (this === other) return true | ||
| if (javaClass != other?.javaClass) return false | ||
|
|
||
| other as ImageVariantData | ||
|
|
||
| if (!webp.contentEquals(other.webp)) return false | ||
| if (!jpeg.contentEquals(other.jpeg)) return false | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| override fun hashCode(): Int { | ||
| var result = webp.contentHashCode() | ||
| result = 31 * result + jpeg.contentHashCode() | ||
| return result | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: UPbrella/UPbrella_back
Length of output: 361
🌐 Web query:
Thumbnailator security vulnerabilities CVE 2024 2025💡 Result:
Short answer: I found CVEs affecting "thumbnail" software in 2024–2025 (notably Matrix Media Repo and several WordPress thumbnail plugins), but I found no CVE entries for the Java Thumbnailator library (coobird/thumbnailator).
Notable CVEs (2024–2025)
No CVE for Java Thumbnailator
Sources
If you want, I can:
🌐 Web query:
webp-imageio org.sejda.imageio security advisories vulnerabilities💡 Result:
Summary — webp-imageio (org.sejda.imageio)
Relevant sources
If you want, I can:
Address unmaintained webp-imageio dependency.
Thumbnailator 0.4.20 is the latest version (released June 2023) with no known security advisories. However, webp-imageio 0.1.6 has not been updated since May 2020 and appears unmaintained. Its underlying libwebp dependency is outdated and has known vulnerabilities (e.g., CVE-2023-1999). Consider evaluating alternative WebP processing libraries or submitting upstream patches to the sejda project to address the stale dependencies.
🤖 Prompt for AI Agents