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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ const imageData = await image.toEncodedImageData('jpg', 90)
const sameImageCopied = await Images.loadFromEncodedImageData(imageData)
```

#### Base64 (`string`)

The `Image` type can be encoded to a standard Base64 string using a container format like `jpg`, `png` or `heic`:

```ts
const image = ...
const base64 = await image.toBase64Async('jpg', 90)
const dataUrl = `data:image/jpeg;base64,${base64}`
```

`toBase64Async(...)` returns a padded Base64 payload without line breaks or a data URL prefix.

> [!NOTE]
> Base64 is larger than binary image data. Prefer `toEncodedImageDataAsync(...)` unless the destination specifically requires a string.

#### Resizing

An `Image` can be resized entirely in-memory, without ever writing to- or reading from- a file:
Expand Down Expand Up @@ -226,6 +241,7 @@ Images can be compressed using the `jpg` container format - either in-memory or
const image = ...
const path = await image.saveToTemporaryFileAsync('jpg', 50) // 50% compression
const compressed = await image.toEncodedImageData('jpg', 50) // 50% compression
const base64 = await image.toBase64Async('jpg', 50) // 50% compression
```

#### HEIC/HEIF
Expand Down
69 changes: 68 additions & 1 deletion example/__tests__/image-encoding.harness.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'react-native-harness'
import { Images } from 'react-native-nitro-image'
import {
Images,
thumbHashFromBase64String,
thumbHashToBase64String,
} from 'react-native-nitro-image'

const makeImage = () =>
Images.createBlankImage(16, 16, false, { r: 0, g: 0, b: 1, a: 1 })
Expand Down Expand Up @@ -110,6 +114,69 @@ describe('Image - toEncodedImageData', () => {
})
})

describe('Image - toBase64Async', () => {
it('encodes PNG to a bare standard Base64 string', async () => {
const image = makeImage()
const base64 = await image.toBase64Async('png')

expect(base64.length).toBeGreaterThan(0)
expect(base64.startsWith('data:')).toBe(false)
expect(base64.length % 4).toBe(0)
expect(base64).toMatch(/^[A-Za-z0-9+/]*={0,2}$/)
expect(base64.startsWith('iVBORw0KGgo')).toBe(true)
})

it('matches toEncodedImageDataAsync for the same format', async () => {
const image = makeImage()
const base64 = await image.toBase64Async('png')
const encoded = await image.toEncodedImageDataAsync('png')

expect(base64).toBe(thumbHashToBase64String(encoded.buffer))
})

it('decodes to valid encoded image bytes', async () => {
const image = makeImage()
const base64 = await image.toBase64Async('png')
const buffer = thumbHashFromBase64String(base64)
const decoded = Images.loadFromEncodedImageData({
buffer,
width: image.width,
height: image.height,
imageFormat: 'png',
})

expect(decoded.width).toBe(image.width)
expect(decoded.height).toBe(image.height)
})

it('uses a 0...100 JPEG quality range with 100 as the default', async () => {
const image = makeDetailedImage()
const lowest = await image.toBase64Async('jpg', 0)
const highest = await image.toBase64Async('jpg', 100)
const defaultQuality = await image.toBase64Async('jpg')

expect(lowest.length).toBeLessThan(highest.length)
expect(defaultQuality).toBe(highest)
})

it('rounds fractional JPEG quality to the nearest integer', async () => {
const image = makeDetailedImage()
const quality0 = await image.toBase64Async('jpg', 0)
const quality0Point4 = await image.toBase64Async('jpg', 0.4)
const quality99Point5 = await image.toBase64Async('jpg', 99.5)
const quality100 = await image.toBase64Async('jpg', 100)

expect(quality0Point4).toBe(quality0)
expect(quality99Point5).toBe(quality100)
})

it('rejects quality values outside 0...100', async () => {
const image = makeImage()
await expect(image.toBase64Async('jpg', -0.5)).rejects.toBeDefined()
await expect(image.toBase64Async('jpg', 100.5)).rejects.toBeDefined()
})
})

describe('Images - loadFromEncodedImageData', () => {
it('round-trips through PNG encoding', () => {
const image = makeImage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.margelo.nitro.image.extensions.pixelFormat
import com.margelo.nitro.image.extensions.saveToFile
import com.margelo.nitro.image.extensions.toFilePath
import com.margelo.nitro.image.extensions.toByteBuffer
import com.margelo.nitro.image.extensions.toBase64
import com.margelo.nitro.image.extensions.toCpuAccessible
import com.margelo.nitro.image.extensions.toMutable
import java.io.File
Expand Down Expand Up @@ -92,6 +93,13 @@ class HybridImage: HybridImageSpec {
return Promise.async { toEncodedImageData(format, quality) }
}

override fun toBase64Async(format: ImageFormat, quality: Double?): Promise<String> {
return Promise.parallel {
val resolvedQuality = resolveImageQuality(quality)
bitmap.toBase64(format, resolvedQuality)
}
}

override fun rotate(degrees: Double, allowFastFlagRotation: Boolean?): HybridImageSpec {
// 1. Make sure the Bitmap we want to draw is drawable (HARDWARE isn't)
val source = bitmap.toCpuAccessible()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.margelo.nitro.image.extensions

import android.graphics.Bitmap
import com.margelo.nitro.image.ImageFormat
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

@OptIn(ExperimentalEncodingApi::class)
internal fun Bitmap.toBase64(format: ImageFormat, quality: Int): String {
val buffer = compressInMemory(format, quality)
val startIndex = buffer.arrayOffset() + buffer.position()
val endIndex = buffer.arrayOffset() + buffer.limit()
return Base64.Default.encode(buffer.array(), startIndex, endIndex)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// UIImage+toBase64.swift
// react-native-nitro-image
//
// Created by Marc Rousavy on 25.08.26.
//

import UIKit

extension UIImage {
/**
* Encodes this Image in the given `format` and returns its Base64 string.
*/
func toBase64(format: ImageFormat, quality: Double) throws -> String {
let data = try getData(in: format, quality: quality)
return data.base64EncodedString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ public extension NativeImage {
}
}

func toBase64Async(format: ImageFormat, quality: Double?) -> Promise<String> {
let image = uiImage
let resolvedQuality = quality ?? 100
return Promise.parallel(.global(qos: .userInitiated)) {
return try image.toBase64(format: format, quality: resolvedQuality)
}
}

func rotate(degrees: Double, allowFastFlagRotation: Bool?) -> any HybridImageSpec {
if allowFastFlagRotation == true,
degrees.truncatingRemainder(dividingBy: 90) == 0,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading