From 8035bfa0470166f74143e169cdd4c31afd09895c Mon Sep 17 00:00:00 2001 From: GeonHwan Cha Date: Thu, 19 Feb 2026 02:28:48 +0900 Subject: [PATCH 1/7] feat(kotlin): add InputStream and ByteBuffer overloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decode, optimize, formatFromMagicBytes 함수에 InputStream 및 ByteBuffer 오버로드를 추가하여 WebFlux DataBuffer 등과의 통합을 용이하게 함 --- .../kotlin/io/clroot/slimg/SlimgStreams.kt | 70 +++++++++ .../io/clroot/slimg/SlimgStreamsTest.kt | 147 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgStreams.kt create mode 100644 bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgStreamsTest.kt diff --git a/bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgStreams.kt b/bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgStreams.kt new file mode 100644 index 0000000..b94b8dc --- /dev/null +++ b/bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgStreams.kt @@ -0,0 +1,70 @@ +package io.clroot.slimg + +import java.io.InputStream +import java.nio.ByteBuffer + +// ── InputStream overloads ─────────────────────── + +/** + * Decode an image from an [InputStream]. + * + * Reads all bytes from the stream and delegates to [decode]. + * The caller is responsible for closing the stream. + */ +@Throws(SlimgException::class) +fun decode(stream: InputStream): DecodeResult = + decode(stream.readBytes()) + +/** + * Re-encode an image read from an [InputStream] at the given [quality]. + * + * Reads all bytes from the stream and delegates to [optimize]. + * The caller is responsible for closing the stream. + */ +@Throws(SlimgException::class) +fun optimize(stream: InputStream, quality: UByte): PipelineResult = + optimize(stream.readBytes(), quality) + +/** + * Detect the image format of data provided by an [InputStream] using magic bytes. + * + * Reads all bytes from the stream and delegates to [formatFromMagicBytes]. + * The caller is responsible for closing the stream. + */ +fun formatFromMagicBytes(stream: InputStream): Format? = + formatFromMagicBytes(stream.readBytes()) + +// ── ByteBuffer overloads ──────────────────────── + +/** + * Decode an image from a [ByteBuffer]. + * + * Reads the remaining bytes (position to limit) without modifying the buffer's state. + */ +@Throws(SlimgException::class) +fun decode(buffer: ByteBuffer): DecodeResult = + decode(buffer.toByteArray()) + +/** + * Re-encode an image from a [ByteBuffer] at the given [quality]. + * + * Reads the remaining bytes (position to limit) without modifying the buffer's state. + */ +@Throws(SlimgException::class) +fun optimize(buffer: ByteBuffer, quality: UByte): PipelineResult = + optimize(buffer.toByteArray(), quality) + +/** + * Detect the image format of data in a [ByteBuffer] using magic bytes. + * + * Reads the remaining bytes (position to limit) without modifying the buffer's state. + */ +fun formatFromMagicBytes(buffer: ByteBuffer): Format? = + formatFromMagicBytes(buffer.toByteArray()) + +private fun ByteBuffer.toByteArray(): ByteArray { + val buf = duplicate() + val bytes = ByteArray(buf.remaining()) + buf.get(bytes) + return bytes +} diff --git a/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgStreamsTest.kt b/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgStreamsTest.kt new file mode 100644 index 0000000..c6e3da5 --- /dev/null +++ b/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgStreamsTest.kt @@ -0,0 +1,147 @@ +package io.clroot.slimg + +import java.io.ByteArrayInputStream +import java.nio.ByteBuffer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNull + +class SlimgStreamsTest { + + private fun createTestImage(width: UInt, height: UInt): ImageData { + val data = ByteArray(width.toInt() * height.toInt() * 4) + for (row in 0 until height.toInt()) { + for (col in 0 until width.toInt()) { + val offset = (row * width.toInt() + col) * 4 + data[offset] = row.toByte() + data[offset + 1] = col.toByte() + data[offset + 2] = 0xFF.toByte() + data[offset + 3] = 0xFF.toByte() + } + } + return ImageData(width, height, data) + } + + private fun encodePng(image: ImageData): ByteArray = + convert(image, PipelineOptions( + format = Format.PNG, + quality = 100u.toUByte(), + resize = null, + crop = null, + extend = null, + fillColor = null, + )).data + + // ── decode(InputStream) ───────────────────────────── + + @Test + fun `decode InputStream returns same result as decode ByteArray`() { + val image = createTestImage(6u, 4u) + val pngBytes = encodePng(image) + + val fromBytes = decode(pngBytes) + val fromStream = decode(ByteArrayInputStream(pngBytes)) + + assertEquals(fromBytes.format, fromStream.format) + assertEquals(fromBytes.image.width, fromStream.image.width) + assertEquals(fromBytes.image.height, fromStream.image.height) + } + + // ── optimize(InputStream) ─────────────────────────── + + @Test + fun `optimize InputStream returns same result as optimize ByteArray`() { + val image = createTestImage(8u, 8u) + val pngBytes = encodePng(image) + + val fromBytes = optimize(pngBytes, 60u.toUByte()) + val fromStream = optimize(ByteArrayInputStream(pngBytes), 60u.toUByte()) + + assertEquals(fromBytes.format, fromStream.format) + assertTrue(fromStream.data.isNotEmpty()) + } + + // ── formatFromMagicBytes(InputStream) ─────────────── + + @Test + fun `formatFromMagicBytes InputStream detects PNG`() { + val image = createTestImage(4u, 4u) + val pngBytes = encodePng(image) + + val format = formatFromMagicBytes(ByteArrayInputStream(pngBytes)) + assertEquals(Format.PNG, format) + } + + @Test + fun `formatFromMagicBytes InputStream returns null for unknown data`() { + val unknownBytes = byteArrayOf(0x00, 0x01, 0x02, 0x03) + val format = formatFromMagicBytes(ByteArrayInputStream(unknownBytes)) + assertNull(format) + } + + // ── decode(ByteBuffer) ────────────────────────────── + + @Test + fun `decode ByteBuffer returns same result as decode ByteArray`() { + val image = createTestImage(6u, 4u) + val pngBytes = encodePng(image) + + val fromBytes = decode(pngBytes) + val fromBuffer = decode(ByteBuffer.wrap(pngBytes)) + + assertEquals(fromBytes.format, fromBuffer.format) + assertEquals(fromBytes.image.width, fromBuffer.image.width) + assertEquals(fromBytes.image.height, fromBuffer.image.height) + } + + @Test + fun `decode ByteBuffer does not modify buffer position`() { + val pngBytes = encodePng(createTestImage(4u, 4u)) + val buffer = ByteBuffer.wrap(pngBytes) + val positionBefore = buffer.position() + + decode(buffer) + + assertEquals(positionBefore, buffer.position()) + } + + @Test + fun `decode direct ByteBuffer works`() { + val pngBytes = encodePng(createTestImage(4u, 4u)) + val directBuffer = ByteBuffer.allocateDirect(pngBytes.size) + directBuffer.put(pngBytes) + directBuffer.flip() + + val result = decode(directBuffer) + assertEquals(Format.PNG, result.format) + assertEquals(4u, result.image.width) + } + + // ── optimize(ByteBuffer) ──────────────────────────── + + @Test + fun `optimize ByteBuffer returns valid result`() { + val pngBytes = encodePng(createTestImage(8u, 8u)) + + val result = optimize(ByteBuffer.wrap(pngBytes), 60u.toUByte()) + + assertEquals(Format.PNG, result.format) + assertTrue(result.data.isNotEmpty()) + } + + // ── formatFromMagicBytes(ByteBuffer) ──────────────── + + @Test + fun `formatFromMagicBytes ByteBuffer detects PNG`() { + val pngBytes = encodePng(createTestImage(4u, 4u)) + val format = formatFromMagicBytes(ByteBuffer.wrap(pngBytes)) + assertEquals(Format.PNG, format) + } + + @Test + fun `formatFromMagicBytes ByteBuffer returns null for unknown data`() { + val format = formatFromMagicBytes(ByteBuffer.wrap(byteArrayOf(0x00, 0x01, 0x02, 0x03))) + assertNull(format) + } +} From 4a42109541d98dbad503d6f742755098e9359471 Mon Sep 17 00:00:00 2001 From: GeonHwan Cha Date: Thu, 19 Feb 2026 02:29:54 +0900 Subject: [PATCH 2/7] feat(kotlin): add Slimg wrapper object for ergonomic API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UByte/UInt 변환 없이 Int로 quality를 전달하고, PipelineOptions 대신 개별 파라미터로 convert를 호출할 수 있는 래퍼 object 추가. solidColor 헬퍼 포함. --- .../src/main/kotlin/io/clroot/slimg/Slimg.kt | 123 ++++++++++++ .../kotlin/io/clroot/slimg/SlimgObjectTest.kt | 181 ++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 bindings/kotlin/src/main/kotlin/io/clroot/slimg/Slimg.kt create mode 100644 bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgObjectTest.kt diff --git a/bindings/kotlin/src/main/kotlin/io/clroot/slimg/Slimg.kt b/bindings/kotlin/src/main/kotlin/io/clroot/slimg/Slimg.kt new file mode 100644 index 0000000..376a360 --- /dev/null +++ b/bindings/kotlin/src/main/kotlin/io/clroot/slimg/Slimg.kt @@ -0,0 +1,123 @@ +package io.clroot.slimg + +import java.io.InputStream +import java.nio.ByteBuffer + +/** + * Ergonomic wrapper around the slimg FFI functions. + * + * Eliminates [UByte]/[UInt] boilerplate and provides sensible defaults. + * + * ```kotlin + * val result = Slimg.decode(imageBytes) + * val optimized = Slimg.optimize(imageBytes, quality = 80) + * val converted = Slimg.convert(result.image, Format.WEB_P, quality = 85) + * ``` + */ +object Slimg { + + // ── Decode ────────────────────────────────────────── + + @Throws(SlimgException::class) + fun decode(data: ByteArray): DecodeResult = + io.clroot.slimg.decode(data) + + @Throws(SlimgException::class) + fun decode(stream: InputStream): DecodeResult = + io.clroot.slimg.decode(stream) + + @Throws(SlimgException::class) + fun decode(buffer: ByteBuffer): DecodeResult = + io.clroot.slimg.decode(buffer) + + @Throws(SlimgException::class) + fun decodeFile(path: String): DecodeResult = + io.clroot.slimg.decodeFile(path) + + // ── Optimize ──────────────────────────────────────── + + @Throws(SlimgException::class) + fun optimize(data: ByteArray, quality: Int = 80): PipelineResult = + io.clroot.slimg.optimize(data, quality.toQuality()) + + @Throws(SlimgException::class) + fun optimize(stream: InputStream, quality: Int = 80): PipelineResult = + io.clroot.slimg.optimize(stream, quality.toQuality()) + + @Throws(SlimgException::class) + fun optimize(buffer: ByteBuffer, quality: Int = 80): PipelineResult = + io.clroot.slimg.optimize(buffer, quality.toQuality()) + + // ── Convert ───────────────────────────────────────── + + /** + * Encode [image] into [format] with optional transformations. + * + * ```kotlin + * Slimg.convert(image, Format.WEB_P, quality = 85, resize = ResizeMode.Width(800u)) + * ``` + */ + @Throws(SlimgException::class) + fun convert( + image: ImageData, + format: Format, + quality: Int = 80, + resize: ResizeMode? = null, + crop: CropMode? = null, + extend: ExtendMode? = null, + fillColor: FillColor? = null, + ): PipelineResult = io.clroot.slimg.convert( + image, + PipelineOptions(format, quality.toQuality(), resize, crop, extend, fillColor), + ) + + // ── Image Operations ──────────────────────────────── + + @Throws(SlimgException::class) + fun crop(image: ImageData, mode: CropMode): ImageData = + io.clroot.slimg.crop(image, mode) + + @Throws(SlimgException::class) + fun resize(image: ImageData, mode: ResizeMode): ImageData = + io.clroot.slimg.resize(image, mode) + + @Throws(SlimgException::class) + fun extend( + image: ImageData, + mode: ExtendMode, + fill: FillColor = FillColor.Transparent, + ): ImageData = io.clroot.slimg.extend(image, mode, fill) + + // ── Format Utilities ──────────────────────────────── + + fun formatExtension(format: Format): String = + io.clroot.slimg.formatExtension(format) + + fun formatCanEncode(format: Format): Boolean = + io.clroot.slimg.formatCanEncode(format) + + fun formatFromExtension(path: String): Format? = + io.clroot.slimg.formatFromExtension(path) + + fun formatFromMagicBytes(data: ByteArray): Format? = + io.clroot.slimg.formatFromMagicBytes(data) + + fun formatFromMagicBytes(stream: InputStream): Format? = + io.clroot.slimg.formatFromMagicBytes(stream) + + fun formatFromMagicBytes(buffer: ByteBuffer): Format? = + io.clroot.slimg.formatFromMagicBytes(buffer) + + fun outputPath(input: String, format: Format, output: String? = null): String = + io.clroot.slimg.outputPath(input, format, output) + + // ── Helpers ───────────────────────────────────────── + + /** + * Create a solid [FillColor] from integer RGBA values (0–255). + */ + fun solidColor(r: Int, g: Int, b: Int, a: Int = 255): FillColor = + FillColor.Solid(r.toUByte(), g.toUByte(), b.toUByte(), a.toUByte()) +} + +private fun Int.toQuality(): UByte = coerceIn(0, 100).toUByte() diff --git a/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgObjectTest.kt b/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgObjectTest.kt new file mode 100644 index 0000000..9df83ce --- /dev/null +++ b/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgObjectTest.kt @@ -0,0 +1,181 @@ +package io.clroot.slimg + +import java.io.ByteArrayInputStream +import java.nio.ByteBuffer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SlimgObjectTest { + + private fun testImage(width: UInt = 8u, height: UInt = 8u): ImageData { + val data = ByteArray(width.toInt() * height.toInt() * 4) { 0xFF.toByte() } + return ImageData(width, height, data) + } + + private fun pngBytes(image: ImageData = testImage()): ByteArray = + Slimg.convert(image, Format.PNG).data + + // ── Decode ────────────────────────────────────────── + + @Test + fun `decode ByteArray`() { + val bytes = pngBytes() + val result = Slimg.decode(bytes) + assertEquals(Format.PNG, result.format) + assertEquals(8u, result.image.width) + } + + @Test + fun `decode InputStream`() { + val bytes = pngBytes() + val result = Slimg.decode(ByteArrayInputStream(bytes)) + assertEquals(Format.PNG, result.format) + } + + @Test + fun `decode ByteBuffer`() { + val bytes = pngBytes() + val result = Slimg.decode(ByteBuffer.wrap(bytes)) + assertEquals(Format.PNG, result.format) + } + + // ── Optimize ──────────────────────────────────────── + + @Test + fun `optimize with Int quality`() { + val bytes = pngBytes() + val result = Slimg.optimize(bytes, quality = 60) + assertEquals(Format.PNG, result.format) + assertTrue(result.data.isNotEmpty()) + } + + @Test + fun `optimize default quality`() { + val bytes = pngBytes() + val result = Slimg.optimize(bytes) + assertTrue(result.data.isNotEmpty()) + } + + @Test + fun `optimize InputStream with Int quality`() { + val bytes = pngBytes() + val result = Slimg.optimize(ByteArrayInputStream(bytes), quality = 60) + assertTrue(result.data.isNotEmpty()) + } + + @Test + fun `optimize ByteBuffer with Int quality`() { + val bytes = pngBytes() + val result = Slimg.optimize(ByteBuffer.wrap(bytes), quality = 60) + assertTrue(result.data.isNotEmpty()) + } + + @Test + fun `optimize clamps out-of-range quality`() { + val bytes = pngBytes() + // Should not throw - values clamped to 0..100 + Slimg.optimize(bytes, quality = -10) + Slimg.optimize(bytes, quality = 200) + } + + // ── Convert ───────────────────────────────────────── + + @Test + fun `convert with defaults`() { + val image = testImage() + val result = Slimg.convert(image, Format.WEB_P) + assertEquals(Format.WEB_P, result.format) + assertTrue(result.data.isNotEmpty()) + } + + @Test + fun `convert with resize`() { + val image = testImage(16u, 16u) + val result = Slimg.convert(image, Format.PNG, resize = ResizeMode.Width(8u)) + assertEquals(Format.PNG, result.format) + } + + @Test + fun `convert with crop and quality`() { + val image = testImage(16u, 16u) + val result = Slimg.convert( + image, + Format.PNG, + quality = 90, + crop = CropMode.AspectRatio(1u, 1u), + ) + val decoded = Slimg.decode(result.data) + assertEquals(decoded.image.width, decoded.image.height) + } + + // ── Image Operations ──────────────────────────────── + + @Test + fun `crop delegates correctly`() { + val image = testImage(10u, 10u) + val cropped = Slimg.crop(image, CropMode.Region(0u, 0u, 5u, 5u)) + assertEquals(5u, cropped.width) + assertEquals(5u, cropped.height) + } + + @Test + fun `resize delegates correctly`() { + val image = testImage(10u, 10u) + val resized = Slimg.resize(image, ResizeMode.Width(5u)) + assertEquals(5u, resized.width) + } + + @Test + fun `extend with default fill`() { + val image = testImage(8u, 8u) + val extended = Slimg.extend(image, ExtendMode.Size(16u, 16u)) + assertEquals(16u, extended.width) + assertEquals(16u, extended.height) + } + + // ── solidColor ────────────────────────────────────── + + @Test + fun `solidColor creates FillColor with Int values`() { + val fill = Slimg.solidColor(255, 128, 0) + assertEquals(FillColor.Solid(255u.toUByte(), 128u.toUByte(), 0u.toUByte(), 255u.toUByte()), fill) + } + + @Test + fun `solidColor with custom alpha`() { + val fill = Slimg.solidColor(0, 0, 0, 128) + assertEquals(FillColor.Solid(0u.toUByte(), 0u.toUByte(), 0u.toUByte(), 128u.toUByte()), fill) + } + + @Test + fun `extend with solidColor`() { + val image = testImage(4u, 4u) + val extended = Slimg.extend( + image, + ExtendMode.Size(8u, 8u), + Slimg.solidColor(255, 0, 0), + ) + assertEquals(8u, extended.width) + } + + // ── Format Utilities ──────────────────────────────── + + @Test + fun `formatFromExtension via wrapper`() { + assertEquals(Format.PNG, Slimg.formatFromExtension("image.png")) + assertNull(Slimg.formatFromExtension("file.xyz")) + } + + @Test + fun `formatCanEncode via wrapper`() { + assertTrue(Slimg.formatCanEncode(Format.PNG)) + } + + @Test + fun `formatFromMagicBytes ByteBuffer via wrapper`() { + val bytes = pngBytes() + assertEquals(Format.PNG, Slimg.formatFromMagicBytes(ByteBuffer.wrap(bytes))) + } +} From 8e1fc15a1a147e66dbfcf0e30031e60f2f5241f0 Mon Sep 17 00:00:00 2001 From: GeonHwan Cha Date: Thu, 19 Feb 2026 02:35:36 +0900 Subject: [PATCH 3/7] feat(kotlin): add SlimgImage fluent API for chaining operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decode → resize/crop/extend → encode 흐름을 체이닝으로 연결하는 SlimgImage 래퍼 클래스 추가. Int 파라미터로 UInt/UByte 변환 불필요. --- .../main/kotlin/io/clroot/slimg/SlimgImage.kt | 167 ++++++++++++++ .../kotlin/io/clroot/slimg/SlimgImageTest.kt | 213 ++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgImage.kt create mode 100644 bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgImageTest.kt diff --git a/bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgImage.kt b/bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgImage.kt new file mode 100644 index 0000000..30de7f1 --- /dev/null +++ b/bindings/kotlin/src/main/kotlin/io/clroot/slimg/SlimgImage.kt @@ -0,0 +1,167 @@ +package io.clroot.slimg + +import java.io.InputStream +import java.nio.ByteBuffer + +/** + * Fluent wrapper around [ImageData] for chaining image operations. + * + * ```kotlin + * val result = SlimgImage.decode(bytes) + * .resize(width = 800) + * .crop(aspectRatio = 16 to 9) + * .encode(Format.WEB_P, quality = 85) + * ``` + * + * Each transformation returns a new [SlimgImage]; the original is never mutated. + */ +class SlimgImage private constructor( + val imageData: ImageData, + val format: Format?, +) { + val width: Int get() = imageData.width.toInt() + val height: Int get() = imageData.height.toInt() + + // ── Factory ───────────────────────────────────────── + + companion object { + @Throws(SlimgException::class) + fun decode(data: ByteArray): SlimgImage = + io.clroot.slimg.decode(data).toSlimgImage() + + @Throws(SlimgException::class) + fun decode(stream: InputStream): SlimgImage = + io.clroot.slimg.decode(stream).toSlimgImage() + + @Throws(SlimgException::class) + fun decode(buffer: ByteBuffer): SlimgImage = + io.clroot.slimg.decode(buffer).toSlimgImage() + + @Throws(SlimgException::class) + fun decodeFile(path: String): SlimgImage = + io.clroot.slimg.decodeFile(path).toSlimgImage() + + fun from(imageData: ImageData, format: Format? = null): SlimgImage = + SlimgImage(imageData, format) + + private fun DecodeResult.toSlimgImage() = SlimgImage(image, format) + } + + // ── Resize ────────────────────────────────────────── + + /** + * Resize the image. + * + * - `resize(width = 800)` — scale to width, preserve aspect ratio + * - `resize(height = 600)` — scale to height, preserve aspect ratio + * - `resize(width = 800, height = 600)` — exact dimensions (may distort) + */ + @Throws(SlimgException::class) + fun resize(width: Int? = null, height: Int? = null): SlimgImage { + val mode = when { + width != null && height != null -> ResizeMode.Exact(width.toUInt(), height.toUInt()) + width != null -> ResizeMode.Width(width.toUInt()) + height != null -> ResizeMode.Height(height.toUInt()) + else -> throw IllegalArgumentException("At least one of width or height must be specified") + } + return transformed(io.clroot.slimg.resize(imageData, mode)) + } + + /** + * Fit within bounds, preserving aspect ratio. + * + * ```kotlin + * image.resize(fit = 1200 to 1200) + * ``` + */ + @Throws(SlimgException::class) + fun resize(fit: Pair): SlimgImage { + val mode = ResizeMode.Fit(fit.first.toUInt(), fit.second.toUInt()) + return transformed(io.clroot.slimg.resize(imageData, mode)) + } + + /** + * Scale by a factor (e.g. `0.5` = half size). + */ + @Throws(SlimgException::class) + fun resize(scale: Double): SlimgImage = + transformed(io.clroot.slimg.resize(imageData, ResizeMode.Scale(scale))) + + // ── Crop ──────────────────────────────────────────── + + /** + * Extract a specific region. + */ + @Throws(SlimgException::class) + fun crop(x: Int, y: Int, width: Int, height: Int): SlimgImage { + val mode = CropMode.Region(x.toUInt(), y.toUInt(), width.toUInt(), height.toUInt()) + return transformed(io.clroot.slimg.crop(imageData, mode)) + } + + /** + * Crop to an aspect ratio (centered). + * + * ```kotlin + * image.crop(aspectRatio = 16 to 9) + * ``` + */ + @Throws(SlimgException::class) + fun crop(aspectRatio: Pair): SlimgImage { + val mode = CropMode.AspectRatio(aspectRatio.first.toUInt(), aspectRatio.second.toUInt()) + return transformed(io.clroot.slimg.crop(imageData, mode)) + } + + // ── Extend ────────────────────────────────────────── + + /** + * Extend the canvas to exact pixel dimensions (centered). + */ + @Throws(SlimgException::class) + fun extend( + width: Int, + height: Int, + fill: FillColor = FillColor.Transparent, + ): SlimgImage { + val mode = ExtendMode.Size(width.toUInt(), height.toUInt()) + return transformed(io.clroot.slimg.extend(imageData, mode, fill)) + } + + /** + * Extend the canvas to fit an aspect ratio (centered). + * + * ```kotlin + * image.extend(aspectRatio = 16 to 9, fill = Slimg.solidColor(255, 255, 255)) + * ``` + */ + @Throws(SlimgException::class) + fun extend( + aspectRatio: Pair, + fill: FillColor = FillColor.Transparent, + ): SlimgImage { + val mode = ExtendMode.AspectRatio(aspectRatio.first.toUInt(), aspectRatio.second.toUInt()) + return transformed(io.clroot.slimg.extend(imageData, mode, fill)) + } + + // ── Encode ────────────────────────────────────────── + + /** + * Encode the current image into the given [format]. + */ + @Throws(SlimgException::class) + fun encode(format: Format, quality: Int = 80): PipelineResult = + Slimg.convert(imageData, format, quality) + + /** + * Re-encode in the original format detected during [decode]. + * + * @throws IllegalStateException if the source format is unknown. + */ + @Throws(SlimgException::class) + fun optimize(quality: Int = 80): PipelineResult { + val fmt = format + ?: throw IllegalStateException("Source format unknown. Use encode(format, quality) instead.") + return encode(fmt, quality) + } + + private fun transformed(newData: ImageData) = SlimgImage(newData, format) +} diff --git a/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgImageTest.kt b/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgImageTest.kt new file mode 100644 index 0000000..fa89cee --- /dev/null +++ b/bindings/kotlin/src/test/kotlin/io/clroot/slimg/SlimgImageTest.kt @@ -0,0 +1,213 @@ +package io.clroot.slimg + +import java.io.ByteArrayInputStream +import java.nio.ByteBuffer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class SlimgImageTest { + + private fun testImage(width: Int = 8, height: Int = 8): ImageData { + val data = ByteArray(width * height * 4) { 0xFF.toByte() } + return ImageData(width.toUInt(), height.toUInt(), data) + } + + private fun pngBytes(width: Int = 8, height: Int = 8): ByteArray = + Slimg.convert(testImage(width, height), Format.PNG).data + + // ── Factory ───────────────────────────────────────── + + @Test + fun `decode ByteArray preserves format`() { + val img = SlimgImage.decode(pngBytes()) + assertEquals(Format.PNG, img.format) + assertEquals(8, img.width) + assertEquals(8, img.height) + } + + @Test + fun `decode InputStream`() { + val img = SlimgImage.decode(ByteArrayInputStream(pngBytes())) + assertEquals(Format.PNG, img.format) + } + + @Test + fun `decode ByteBuffer`() { + val img = SlimgImage.decode(ByteBuffer.wrap(pngBytes())) + assertEquals(Format.PNG, img.format) + } + + @Test + fun `from wraps existing ImageData`() { + val data = testImage() + val img = SlimgImage.from(data, Format.WEB_P) + assertEquals(Format.WEB_P, img.format) + assertEquals(8, img.width) + } + + @Test + fun `from without format`() { + val img = SlimgImage.from(testImage()) + assertEquals(null, img.format) + } + + // ── Resize ────────────────────────────────────────── + + @Test + fun `resize by width`() { + val img = SlimgImage.decode(pngBytes(16, 16)) + .resize(width = 8) + assertEquals(8, img.width) + assertEquals(8, img.height) + } + + @Test + fun `resize by height`() { + val img = SlimgImage.decode(pngBytes(16, 16)) + .resize(height = 8) + assertEquals(8, img.width) + assertEquals(8, img.height) + } + + @Test + fun `resize exact`() { + val img = SlimgImage.decode(pngBytes(16, 16)) + .resize(width = 12, height = 8) + assertEquals(12, img.width) + assertEquals(8, img.height) + } + + @Test + fun `resize fit`() { + val img = SlimgImage.decode(pngBytes(16, 8)) + .resize(fit = 10 to 10) + assertTrue(img.width <= 10) + assertTrue(img.height <= 10) + } + + @Test + fun `resize scale`() { + val img = SlimgImage.decode(pngBytes(16, 16)) + .resize(scale = 0.5) + assertEquals(8, img.width) + assertEquals(8, img.height) + } + + @Test + fun `resize no args throws`() { + val img = SlimgImage.decode(pngBytes()) + assertFailsWith { + img.resize() + } + } + + // ── Crop ──────────────────────────────────────────── + + @Test + fun `crop region`() { + val img = SlimgImage.decode(pngBytes(16, 16)) + .crop(x = 0, y = 0, width = 8, height = 4) + assertEquals(8, img.width) + assertEquals(4, img.height) + } + + @Test + fun `crop aspect ratio`() { + val img = SlimgImage.decode(pngBytes(16, 8)) + .crop(aspectRatio = 1 to 1) + assertEquals(img.width, img.height) + } + + // ── Extend ────────────────────────────────────────── + + @Test + fun `extend to size`() { + val img = SlimgImage.decode(pngBytes(8, 8)) + .extend(16, 16) + assertEquals(16, img.width) + assertEquals(16, img.height) + } + + @Test + fun `extend to aspect ratio`() { + val img = SlimgImage.decode(pngBytes(8, 8)) + .extend(aspectRatio = 2 to 1) + assertEquals(2.0, img.width.toDouble() / img.height, 0.01) + } + + @Test + fun `extend with solid color`() { + val img = SlimgImage.decode(pngBytes(4, 4)) + .extend(8, 8, fill = Slimg.solidColor(255, 0, 0)) + assertEquals(8, img.width) + } + + // ── Encode ────────────────────────────────────────── + + @Test + fun `encode to format`() { + val result = SlimgImage.decode(pngBytes()) + .encode(Format.WEB_P, quality = 85) + assertEquals(Format.WEB_P, result.format) + assertTrue(result.data.isNotEmpty()) + } + + @Test + fun `optimize re-encodes in source format`() { + val result = SlimgImage.decode(pngBytes()) + .optimize(quality = 60) + assertEquals(Format.PNG, result.format) + } + + @Test + fun `optimize throws when format unknown`() { + val img = SlimgImage.from(testImage()) + assertFailsWith { + img.optimize() + } + } + + // ── Chaining ──────────────────────────────────────── + + @Test + fun `full pipeline chain`() { + val result = SlimgImage.decode(pngBytes(16, 16)) + .resize(width = 12) + .crop(aspectRatio = 1 to 1) + .encode(Format.WEB_P, quality = 85) + + assertEquals(Format.WEB_P, result.format) + assertTrue(result.data.isNotEmpty()) + } + + @Test + fun `chain preserves source format`() { + val img = SlimgImage.decode(pngBytes(16, 16)) + .resize(width = 8) + .crop(aspectRatio = 1 to 1) + + assertEquals(Format.PNG, img.format) + } + + @Test + fun `chain does not mutate original`() { + val original = SlimgImage.decode(pngBytes(16, 16)) + val resized = original.resize(width = 8) + + assertEquals(16, original.width) + assertEquals(8, resized.width) + } + + @Test + fun `decode stream resize extend encode`() { + val result = SlimgImage.decode(ByteArrayInputStream(pngBytes(10, 10))) + .resize(width = 8) + .extend(aspectRatio = 16 to 9, fill = Slimg.solidColor(0, 0, 0)) + .encode(Format.PNG, quality = 90) + + val decoded = Slimg.decode(result.data) + assertEquals(Format.PNG, decoded.format) + } +} From bf4b64742e6a5e52b71c83492732f49883e36c79 Mon Sep 17 00:00:00 2001 From: GeonHwan Cha Date: Thu, 19 Feb 2026 02:37:58 +0900 Subject: [PATCH 4/7] docs(kotlin): update README with SlimgImage fluent API and Slimg object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quick Start, 3단계 API 레이어 설명, InputStream/ByteBuffer 예시, SlimgImage 체이닝 패턴 등 새 API 기준으로 전면 재작성 --- bindings/kotlin/README.md | 185 +++++++++++++++++++++++++++++--------- 1 file changed, 143 insertions(+), 42 deletions(-) diff --git a/bindings/kotlin/README.md b/bindings/kotlin/README.md index d5d4dcc..3fdae44 100644 --- a/bindings/kotlin/README.md +++ b/bindings/kotlin/README.md @@ -32,54 +32,156 @@ dependencies { ``` -## Usage +## Quick Start ```kotlin import io.clroot.slimg.* -// Decode an image file -val result = decodeFile("photo.jpg") -println("${result.image.width}x${result.image.height} ${result.format}") - -// Convert to WebP -val webp = convert(result.image, PipelineOptions( - format = Format.WEB_P, - quality = 80u, - resize = null, -)) -File("photo.webp").writeBytes(webp.data) - -// Optimize in the same format -val fileBytes = File("photo.png").readBytes() -val optimized = optimize(fileBytes, 80u) -File("photo-optimized.png").writeBytes(optimized.data) - -// Resize and convert -val resized = convert(result.image, PipelineOptions( - format = Format.WEB_P, - quality = 80u, +// Decode → transform → encode in a single chain +val result = SlimgImage.decodeFile("photo.jpg") + .resize(width = 800) + .crop(aspectRatio = 16 to 9) + .encode(Format.WEB_P, quality = 85) + +File("photo.webp").writeBytes(result.data) +``` + +## Usage + +### SlimgImage (Fluent API) + +Chain decode → transform → encode operations fluently. All parameters use `Int` — no `UInt`/`UByte` conversions needed. + +```kotlin +// Decode from various sources +val img = SlimgImage.decode(byteArray) +val img = SlimgImage.decode(inputStream) +val img = SlimgImage.decode(byteBuffer) +val img = SlimgImage.decodeFile("photo.jpg") + +// Resize +img.resize(width = 800) // by width (preserve aspect ratio) +img.resize(height = 600) // by height +img.resize(width = 800, height = 600) // exact dimensions +img.resize(fit = 1200 to 1200) // fit within bounds +img.resize(scale = 0.5) // scale factor + +// Crop +img.crop(aspectRatio = 16 to 9) // center-anchored +img.crop(x = 100, y = 50, width = 800, height = 600) // region + +// Extend (add padding) +img.extend(1920, 1080) // to exact size +img.extend(aspectRatio = 1 to 1) // to aspect ratio +img.extend(1920, 1080, fill = Slimg.solidColor(255, 0, 0)) // with color + +// Encode +img.encode(Format.WEB_P, quality = 85) // to specific format +img.optimize(quality = 70) // re-encode in source format +``` + +#### Chaining + +Each operation returns a new `SlimgImage`, so you can chain freely: + +```kotlin +val result = SlimgImage.decodeFile("photo.jpg") + .resize(fit = 1200 to 1200) + .crop(aspectRatio = 1 to 1) + .extend(aspectRatio = 16 to 9, fill = Slimg.solidColor(0, 0, 0)) + .encode(Format.WEB_P, quality = 80) + +File("output.webp").writeBytes(result.data) +``` + +#### With InputStream / ByteBuffer + +Works with any byte source — file streams, network responses, WebFlux `DataBuffer`, etc. + +```kotlin +// From InputStream +FileInputStream("photo.jpg").use { stream -> + val result = SlimgImage.decode(stream) + .resize(width = 800) + .encode(Format.WEB_P) + File("photo.webp").writeBytes(result.data) +} + +// From ByteBuffer (e.g. WebFlux DataBuffer) +val result = SlimgImage.decode(dataBuffer.asByteBuffer()) + .resize(fit = 1200 to 1200) + .encode(Format.WEB_P, quality = 85) +``` + +### Slimg Object + +For one-off operations without chaining: + +```kotlin +// Decode +val decoded = Slimg.decode(byteArray) // or InputStream, ByteBuffer +val decoded = Slimg.decodeFile("photo.jpg") + +// Convert with simplified parameters +val result = Slimg.convert( + decoded.image, + Format.WEB_P, + quality = 85, // Int, not UByte resize = ResizeMode.Width(800u), -)) + crop = CropMode.AspectRatio(16u, 9u), +) + +// Optimize +val optimized = Slimg.optimize(fileBytes, quality = 70) // Int quality + +// Image operations +val resized = Slimg.resize(image, ResizeMode.Width(800u)) +val cropped = Slimg.crop(image, CropMode.AspectRatio(16u, 9u)) +val extended = Slimg.extend(image, ExtendMode.Size(1920u, 1080u)) + +// FillColor helper +val red = Slimg.solidColor(255, 0, 0) // Int RGBA, alpha defaults to 255 ``` +### Low-level Functions + +Direct FFI bindings are also available as top-level functions: + +```kotlin +val decoded = decode(byteArray) +val result = convert(image, PipelineOptions(Format.WEB_P, 80u.toUByte(), null, null, null, null)) +val optimized = optimize(byteArray, 80u.toUByte()) +``` + +## API Layers + +| Layer | Use When | Example | +|-------|----------|---------| +| `SlimgImage` | Chaining multiple operations | `SlimgImage.decode(bytes).resize(width = 800).encode(Format.WEB_P)` | +| `Slimg` object | One-off operations with `Int` params | `Slimg.optimize(bytes, quality = 80)` | +| Top-level functions | Direct FFI access needed | `decode(bytes)`, `convert(image, options)` | + ## API Reference -### Functions - -| Function | Description | -|----------|-------------| -| `decode(data: ByteArray)` | Decode image from bytes | -| `decodeFile(path: String)` | Decode image from file path | -| `convert(image, options)` | Convert image to a different format | -| `crop(image, mode)` | Crop an image by region or aspect ratio | -| `extend(image, mode, fill)` | Extend (pad) image canvas | -| `resize(image, mode)` | Resize an image | -| `optimize(data: ByteArray, quality: UByte)` | Re-encode to reduce file size | -| `outputPath(input, format, output?)` | Generate output file path | -| `formatExtension(format)` | Get file extension for a format | -| `formatCanEncode(format)` | Check if format supports encoding | -| `formatFromExtension(path)` | Detect format from file extension | -| `formatFromMagicBytes(data)` | Detect format from file header | +### SlimgImage + +| Method | Description | +|--------|-------------| +| `SlimgImage.decode(ByteArray / InputStream / ByteBuffer)` | Decode from bytes | +| `SlimgImage.decodeFile(path)` | Decode from file path | +| `SlimgImage.from(ImageData, format?)` | Wrap existing ImageData | +| `.resize(width?, height?)` | Resize by width/height/exact | +| `.resize(fit: Pair)` | Fit within bounds | +| `.resize(scale: Double)` | Scale by factor | +| `.crop(x, y, width, height)` | Crop region | +| `.crop(aspectRatio: Pair)` | Crop to aspect ratio | +| `.extend(width, height, fill?)` | Extend to exact size | +| `.extend(aspectRatio: Pair, fill?)` | Extend to aspect ratio | +| `.encode(format, quality?)` | Encode to format | +| `.optimize(quality?)` | Re-encode in source format | +| `.width` / `.height` | Current dimensions | +| `.format` | Source format (from decode) | +| `.imageData` | Underlying ImageData | ### Types @@ -90,11 +192,10 @@ val resized = convert(result.image, PipelineOptions( | `CropMode` | `Region`, `AspectRatio` | | `ExtendMode` | `AspectRatio`, `Size` | | `FillColor` | `Transparent`, `Solid(r, g, b, a)` | -| `PipelineOptions` | `format`, `quality`, `resize`, `crop`, `extend`, `fillColor` | | `PipelineResult` | `data` (ByteArray), `format` | | `DecodeResult` | `image` (ImageData), `format` | -| `ImageData` | `width`, `height`, `data` (raw pixels) | -| `SlimgException` | Error with subclasses: `UnsupportedFormat`, `UnknownFormat`, `EncodingNotSupported`, `Decode`, `Encode`, `Resize`, `Crop`, `Extend`, `Io`, `Image` | +| `ImageData` | `width`, `height`, `data` (raw RGBA pixels) | +| `SlimgException` | `UnsupportedFormat`, `UnknownFormat`, `EncodingNotSupported`, `Decode`, `Encode`, `Resize`, `Crop`, `Extend`, `Io`, `Image` | ## Supported Platforms From 84c1f0066716893783dafa08f8b1494e54f0e2c2 Mon Sep 17 00:00:00 2001 From: GeonHwan Cha Date: Thu, 19 Feb 2026 02:39:42 +0900 Subject: [PATCH 5/7] docs(kotlin): replace hardcoded version with variable and Maven Central badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 릴리스마다 문서 버전 수정 누락 방지 --- bindings/kotlin/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bindings/kotlin/README.md b/bindings/kotlin/README.md index 3fdae44..2f5d046 100644 --- a/bindings/kotlin/README.md +++ b/bindings/kotlin/README.md @@ -6,11 +6,13 @@ Supports macOS (Apple Silicon, Intel), Linux (x86_64, ARM64), and Windows (x86_6 ## Installation +[![Maven Central](https://img.shields.io/maven-central/v/io.clroot.slimg/slimg-kotlin)](https://central.sonatype.com/artifact/io.clroot.slimg/slimg-kotlin) + ### Gradle (Kotlin DSL) ```kotlin dependencies { - implementation("io.clroot.slimg:slimg-kotlin:0.3.1") + implementation("io.clroot.slimg:slimg-kotlin:$slimgVersion") } ``` @@ -18,7 +20,7 @@ dependencies { ```groovy dependencies { - implementation 'io.clroot.slimg:slimg-kotlin:0.3.1' + implementation 'io.clroot.slimg:slimg-kotlin:$slimgVersion' } ``` @@ -28,7 +30,7 @@ dependencies { io.clroot.slimg slimg-kotlin - 0.3.1 + ${slimgVersion} ``` From 7cf4c90450aa691029b918cf05742b96f2d25daf Mon Sep 17 00:00:00 2001 From: GeonHwan Cha Date: Thu, 19 Feb 2026 02:42:45 +0900 Subject: [PATCH 6/7] chore: bump version to 0.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kotlin ergonomic API (SlimgImage fluent API, Slimg wrapper object, InputStream/ByteBuffer overloads) 추가에 따른 minor version bump --- Cargo.lock | 6 +++--- bindings/kotlin/gradle.properties | 2 +- bindings/python/pyproject.toml | 2 +- cli/Cargo.toml | 4 ++-- crates/slimg-core/Cargo.toml | 2 +- crates/slimg-ffi/Cargo.toml | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8f3a05..8ba773f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1777,7 +1777,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slimg" -version = "0.3.1" +version = "0.4.0" dependencies = [ "anyhow", "clap", @@ -1790,7 +1790,7 @@ dependencies = [ [[package]] name = "slimg-core" -version = "0.3.1" +version = "0.4.0" dependencies = [ "criterion", "image", @@ -1806,7 +1806,7 @@ dependencies = [ [[package]] name = "slimg-ffi" -version = "0.3.1" +version = "0.4.0" dependencies = [ "slimg-core", "thiserror", diff --git a/bindings/kotlin/gradle.properties b/bindings/kotlin/gradle.properties index 45471e0..e6b14db 100644 --- a/bindings/kotlin/gradle.properties +++ b/bindings/kotlin/gradle.properties @@ -1,3 +1,3 @@ group=io.clroot.slimg -version=0.3.1 +version=0.4.0 kotlin.code.style=official diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index f881480..a823693 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "slimg" -version = "0.3.1" +version = "0.4.0" requires-python = ">=3.9" description = "Fast image optimization library powered by Rust" readme = "README.md" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d29ba7a..8787da4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slimg" -version = "0.3.1" +version = "0.4.0" edition = "2024" license = "MIT OR Apache-2.0" description = "Image optimization CLI — convert, compress, and resize images using MozJPEG, OxiPNG, WebP, AVIF, and QOI" @@ -11,7 +11,7 @@ keywords = ["image", "optimization", "cli", "compression", "webp"] categories = ["multimedia::images", "command-line-utilities"] [dependencies] -slimg-core = { version = "0.3.1", path = "../crates/slimg-core" } +slimg-core = { version = "0.4.0", path = "../crates/slimg-core" } clap = { version = "4.5", features = ["derive"] } clap_complete = "4.5" anyhow = "1" diff --git a/crates/slimg-core/Cargo.toml b/crates/slimg-core/Cargo.toml index 965293e..698fdbe 100644 --- a/crates/slimg-core/Cargo.toml +++ b/crates/slimg-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slimg-core" -version = "0.3.1" +version = "0.4.0" edition = "2024" license = "MIT OR Apache-2.0" description = "Image optimization library — encode, decode, convert, resize with MozJPEG, OxiPNG, WebP, AVIF, and QOI" diff --git a/crates/slimg-ffi/Cargo.toml b/crates/slimg-ffi/Cargo.toml index 6e8ac14..7ad265d 100644 --- a/crates/slimg-ffi/Cargo.toml +++ b/crates/slimg-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slimg-ffi" -version = "0.3.1" +version = "0.4.0" edition = "2024" license = "MIT OR Apache-2.0" description = "UniFFI bindings for slimg-core image optimization library" From 419d865c3abf7da41c4bc709d611f91c10e3419c Mon Sep 17 00:00:00 2001 From: GeonHwan Cha Date: Thu, 19 Feb 2026 02:53:12 +0900 Subject: [PATCH 7/7] chore: change license from MIT OR Apache-2.0 to MIT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LICENSE 파일 생성 및 모든 매니페스트/문서에서 라이센스 참조 변경 --- LICENSE | 21 +++++++++++++++++++++ README.ko.md | 2 +- README.md | 2 +- bindings/kotlin/README.md | 2 +- bindings/kotlin/build.gradle.kts | 2 +- bindings/python/README.md | 2 +- bindings/python/pyproject.toml | 3 +-- cli/Cargo.toml | 2 +- crates/slimg-core/Cargo.toml | 2 +- crates/slimg-core/README.md | 2 +- crates/slimg-ffi/Cargo.toml | 2 +- 11 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..021f521 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 clroot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.ko.md b/README.ko.md index fe8b404..ec8306e 100644 --- a/README.ko.md +++ b/README.ko.md @@ -123,4 +123,4 @@ result.save(Path::new("photo.webp"))?; ## 라이선스 -MIT OR Apache-2.0 +MIT diff --git a/README.md b/README.md index 0d5bca5..66fd514 100644 --- a/README.md +++ b/README.md @@ -124,4 +124,4 @@ result.save(Path::new("photo.webp"))?; ## License -MIT OR Apache-2.0 +MIT diff --git a/bindings/kotlin/README.md b/bindings/kotlin/README.md index 2f5d046..e92dca5 100644 --- a/bindings/kotlin/README.md +++ b/bindings/kotlin/README.md @@ -216,4 +216,4 @@ val optimized = optimize(byteArray, 80u.toUByte()) ## License -MIT OR Apache-2.0 +MIT diff --git a/bindings/kotlin/build.gradle.kts b/bindings/kotlin/build.gradle.kts index 3fc808d..96e765b 100644 --- a/bindings/kotlin/build.gradle.kts +++ b/bindings/kotlin/build.gradle.kts @@ -42,7 +42,7 @@ mavenPublishing { licenses { license { - name.set("MIT OR Apache-2.0") + name.set("MIT") url.set("https://github.com/clroot/slimg/blob/main/LICENSE") } } diff --git a/bindings/python/README.md b/bindings/python/README.md index 1b595c0..3cf57b8 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -99,4 +99,4 @@ extended = slimg.extend(image, aspect_ratio=(1, 1)) ## License -MIT OR Apache-2.0 +MIT diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index a823693..1daea50 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -8,12 +8,11 @@ version = "0.4.0" requires-python = ">=3.9" description = "Fast image optimization library powered by Rust" readme = "README.md" -license = { text = "MIT OR Apache-2.0" } +license = { text = "MIT" } keywords = ["image", "optimization", "compression", "webp", "avif", "jpeg", "png"] classifiers = [ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", - "License :: OSI Approved :: Apache Software License", "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: 3", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8787da4..6e67018 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,7 +2,7 @@ name = "slimg" version = "0.4.0" edition = "2024" -license = "MIT OR Apache-2.0" +license = "MIT" description = "Image optimization CLI — convert, compress, and resize images using MozJPEG, OxiPNG, WebP, AVIF, and QOI" repository = "https://github.com/clroot/slimg" homepage = "https://github.com/clroot/slimg" diff --git a/crates/slimg-core/Cargo.toml b/crates/slimg-core/Cargo.toml index 698fdbe..ae5b23b 100644 --- a/crates/slimg-core/Cargo.toml +++ b/crates/slimg-core/Cargo.toml @@ -2,7 +2,7 @@ name = "slimg-core" version = "0.4.0" edition = "2024" -license = "MIT OR Apache-2.0" +license = "MIT" description = "Image optimization library — encode, decode, convert, resize with MozJPEG, OxiPNG, WebP, AVIF, and QOI" repository = "https://github.com/clroot/slimg" homepage = "https://github.com/clroot/slimg" diff --git a/crates/slimg-core/README.md b/crates/slimg-core/README.md index bb761e2..0390f1b 100644 --- a/crates/slimg-core/README.md +++ b/crates/slimg-core/README.md @@ -59,4 +59,4 @@ For batch processing and command-line usage, see [slimg](https://crates.io/crate ## License -MIT OR Apache-2.0 +MIT diff --git a/crates/slimg-ffi/Cargo.toml b/crates/slimg-ffi/Cargo.toml index 7ad265d..101e123 100644 --- a/crates/slimg-ffi/Cargo.toml +++ b/crates/slimg-ffi/Cargo.toml @@ -2,7 +2,7 @@ name = "slimg-ffi" version = "0.4.0" edition = "2024" -license = "MIT OR Apache-2.0" +license = "MIT" description = "UniFFI bindings for slimg-core image optimization library" publish = false