Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ const result = await MediaToolkit.compressImage(imageUri, {
});
```

### Split image into grid

```typescript
const parts = await MediaToolkit.splitImage(imageUri, {
rows: 3,
columns: 4,
quality: 100, // optional, defaults to 100 for lossy formats
});

console.log(parts.length); // 12
console.log(parts[0].uri, parts[0].width, parts[0].height);
```

### Flip image

```typescript
Expand Down Expand Up @@ -296,6 +309,19 @@ All options are optional. Pass an empty object `{}` to use all defaults.
| `format` | `string` | `'jpeg'` | Output format: `'jpeg'` \| `'png'` \| `'webp'` |
| `outputPath` | `string` | temp file | Absolute path for the output file |

### `splitImage(uri, options): Promise<MediaResult[]>`

Splits the source image into a `rows x columns` grid using original pixel dimensions. Tiles are not resized. When format is omitted, the native layer keeps the source format when supported; iOS falls back to PNG for unsupported source encoders such as WebP.

| Option | Type | Required | Description |
|---|---|---|---|
| `rows` | `number` | **Required** | Number of output rows |
| `columns` | `number` | **Required** | Number of output columns |
| `format` | `string` | Optional | Force output format: `'jpeg'` \| `'png'` \| `'webp'` |
| `quality` | `number` | Optional | Lossy encode quality (0–100). Defaults to `100` |
| `outputDir` | `string` | Optional | Absolute output directory for generated tiles |
| `prefix` | `string` | Optional | Filename prefix for generated tiles |

### `flipImage(uri, options): Promise<MediaResult>`
### `flipVideo(uri, options): Promise<MediaResult>`

Expand Down
26 changes: 26 additions & 0 deletions README.vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ const result = await MediaToolkit.compressImage(imageUri, {
});
```

### Chia ảnh theo lưới

```typescript
const parts = await MediaToolkit.splitImage(imageUri, {
rows: 3,
columns: 4,
quality: 100, // tuỳ chọn, mặc định 100 với format lossy
});

console.log(parts.length); // 12
console.log(parts[0].uri, parts[0].width, parts[0].height);
```

### Lật ảnh

```typescript
Expand Down Expand Up @@ -296,6 +309,19 @@ Tất cả options đều là tuỳ chọn. Có thể truyền object rỗng `{}
| `format` | `string` | `'jpeg'` | Định dạng output: `'jpeg'` \| `'png'` \| `'webp'` |
| `outputPath` | `string` | file tạm | Đường dẫn tuyệt đối file output |

### `splitImage(uri, options): Promise<MediaResult[]>`

Chia ảnh nguồn thành lưới `rows x columns` theo đúng pixel gốc, không resize từng mảnh. Nếu không truyền `format`, native sẽ giữ format nguồn khi hỗ trợ; trên iOS sẽ fallback sang PNG nếu format nguồn không encode lại được như WebP.

| Option | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `rows` | `number` | **Bắt buộc** | Số hàng đầu ra |
| `columns` | `number` | **Bắt buộc** | Số cột đầu ra |
| `format` | `string` | Tuỳ chọn | Ép định dạng output: `'jpeg'` \| `'png'` \| `'webp'` |
| `quality` | `number` | Tuỳ chọn | Chất lượng encode lossy (0–100). Mặc định `100` |
| `outputDir` | `string` | Tuỳ chọn | Thư mục output tuyệt đối cho các tile |
| `prefix` | `string` | Tuỳ chọn | Tiền tố tên file cho các tile |

### `flipImage(uri, options): Promise<MediaResult>`
### `flipVideo(uri, options): Promise<MediaResult>`

Expand Down
15 changes: 15 additions & 0 deletions android/src/main/java/com/mediatoolkit/HybridMediaToolkit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.margelo.nitro.com.mediatoolkit.RotateOptions
import com.margelo.nitro.com.mediatoolkit.SpeedOptions
import com.margelo.nitro.com.mediatoolkit.ExtractAudioOptions
import com.margelo.nitro.com.mediatoolkit.GeneratePreviewOptions
import com.margelo.nitro.com.mediatoolkit.SplitImageOptions
import com.margelo.nitro.com.mediatoolkit.ThumbnailOptions
import com.margelo.nitro.com.mediatoolkit.ThumbnailResult
import com.margelo.nitro.com.mediatoolkit.TrimAndCropOptions
Expand Down Expand Up @@ -74,6 +75,20 @@ class HybridMediaToolkit : HybridMediaToolkitSpec() {
}
}

override fun splitImage(uri: String, options: SplitImageOptions): Promise<Array<MediaResult>> {
return Promise.async(scope) {
ImageProcessor.splitImage(
uri,
options.rows.toInt(),
options.columns.toInt(),
options.format,
(options.quality ?: 100.0).toInt(),
options.outputDir,
options.prefix
).map { it.toMediaResult() }.toTypedArray()
}
}

override fun flipImage(uri: String, options: FlipOptions): Promise<MediaResult> {
return Promise.async(scope) {
val raw = ImageProcessor.flipImage(
Expand Down
110 changes: 110 additions & 0 deletions android/src/main/java/com/mediatoolkit/ImageProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.exifinterface.media.ExifInterface
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
import java.util.Locale

/**
* Image crop and compress using Android Bitmap API.
Expand Down Expand Up @@ -237,6 +238,56 @@ internal object ImageProcessor {
return buildResult(out, bmp, mime, 0)
}

fun splitImage(
uri: String,
rows: Int,
columns: Int,
format: String?,
quality: Int,
outputDir: String?,
prefix: String?
): List<Map<String, Any>> {
if (rows <= 0 || columns <= 0) {
throw MediaToolkitException.InvalidInput("rows and columns must be greater than 0")
}

val path = uriToPath(uri)
var bmp = BitmapFactory.decodeFile(path)
?: throw MediaToolkitException.InvalidInput("Cannot decode image: $uri")

bmp = fixExifOrientation(bmp, path)

try {
val resolvedFormat = resolveImageFormat(format, path)
val targetDir = resolveOutputDirectory(outputDir)
val filePrefix = prefix?.takeIf { it.isNotBlank() } ?: "split_${UUID.randomUUID()}"
val qualityValue = quality.coerceIn(0, 100)
val results = mutableListOf<Map<String, Any>>()

for (row in 0 until rows) {
val top = bmp.height * row / rows
val bottom = bmp.height * (row + 1) / rows
val tileHeight = (bottom - top).coerceAtLeast(1)

for (column in 0 until columns) {
val left = bmp.width * column / columns
val right = bmp.width * (column + 1) / columns
val tileWidth = (right - left).coerceAtLeast(1)
val tile = Bitmap.createBitmap(bmp, left, top, tileWidth, tileHeight)
val out = File(targetDir, "${filePrefix}_r${row + 1}_c${column + 1}.${resolvedFormat.ext}").absolutePath

writeBitmap(tile, out, resolvedFormat, qualityValue)
results.add(buildResult(out, tile, resolvedFormat.mime, 0))
tile.recycle()
}
}

return results
} finally {
bmp.recycle()
}
}

// ─── Helpers ─────────────────────────────────────────────────────────────

fun uriToPath(uri: String): String =
Expand All @@ -248,6 +299,65 @@ internal object ImageProcessor {
return "$dir/${UUID.randomUUID()}.$ext"
}

private data class EncodedImageFormat(
val ext: String,
val mime: String,
val compressFormat: Bitmap.CompressFormat
)

private fun resolveImageFormat(requestedFormat: String?, path: String): EncodedImageFormat {
return when ((requestedFormat?.lowercase(Locale.US) ?: inferSourceFormat(path))) {
"png" -> EncodedImageFormat("png", "image/png", Bitmap.CompressFormat.PNG)
"webp" -> EncodedImageFormat(
"webp",
"image/webp",
if (android.os.Build.VERSION.SDK_INT >= 30) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
Bitmap.CompressFormat.WEBP
}
)
"jpg", "jpeg" -> EncodedImageFormat("jpg", "image/jpeg", Bitmap.CompressFormat.JPEG)
else -> EncodedImageFormat("jpg", "image/jpeg", Bitmap.CompressFormat.JPEG)
}
}

private fun inferSourceFormat(path: String): String {
val ext = path.substringAfterLast('.', "").lowercase(Locale.US)
return when (ext) {
"png" -> "png"
"webp" -> "webp"
"jpg", "jpeg" -> "jpeg"
else -> "jpeg"
}
}

private fun resolveOutputDirectory(outputDir: String?): File {
val dir = if (outputDir.isNullOrBlank()) {
File(System.getProperty("java.io.tmpdir") ?: "/data/local/tmp")
} else {
File(outputDir)
}
if (!dir.exists()) {
dir.mkdirs()
}
return dir
}

private fun writeBitmap(
bmp: Bitmap,
path: String,
format: EncodedImageFormat,
quality: Int
) {
val written = FileOutputStream(path).use { fos ->
bmp.compress(format.compressFormat, quality, fos)
}
if (!written) {
throw MediaToolkitException.ProcessingFailed("Could not encode split image")
}
}

private fun fixExifOrientation(bmp: Bitmap, path: String): Bitmap {
return try {
val exif = ExifInterface(path)
Expand Down
Loading
Loading