Skip to content

Commit e49b814

Browse files
authored
feat: Add toRawPixelData() and toEncodedPixelData() (+ GPU support) (#70)
* feat: Create **raw** PixelData accessors and split to encoded * new specs * Implement iOS * Update HybridImageFactory.swift * Lint * Add a lot more cases to `PixelFormat` * fix format * fix: Use zero copy CGDataProvider! :) * feat: Implement Android * feat: Add GPU Buffer support * Extract a bit * Fix * fix: Make it work on Android * fix: Copy to CPU if needed - proper format now * Lint * Update EmptyTab.tsx
1 parent 9d85efb commit e49b814

65 files changed

Lines changed: 2397 additions & 289 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

example/src/EmptyTab.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { useState } from "react";
2-
import { StyleSheet, Text, TextInput, View } from "react-native";
2+
import { StyleSheet, TextInput, View } from "react-native";
33
import { NitroImage } from "react-native-nitro-image";
44

55
export function EmptyTab() {
6-
const [value, setValue] = useState('https://picsum.photos/seed/123/400')
6+
const [value, setValue] = useState('https://picsum.photos/seed/123/600')
77

88
return (
99
<View style={styles.container}>
10-
<Text style={styles.text}>
11-
<TextInput placeholder="Image URL" value={value} onChangeText={setValue} style={styles.textInput} />
12-
<NitroImage image={{ url: value }} style={styles.image} />
13-
</Text>
10+
<TextInput placeholder="Image URL" value={value} onChangeText={setValue} style={styles.textInput} />
11+
<NitroImage image={{ url: value }} style={styles.image} />
1412
</View>
1513
);
1614
}
@@ -26,13 +24,14 @@ const styles = StyleSheet.create({
2624
fontWeight: "500",
2725
},
2826
textInput: {
29-
height: 50,
30-
width: '100%'
27+
borderWidth: 1,
28+
borderRadius: 5,
29+
paddingHorizontal: 10,
3130
},
3231
image: {
33-
width: 400,
34-
height: 400,
35-
borderColor: 'grey',
36-
borderWidth: StyleSheet.hairlineWidth
32+
width: 350,
33+
height: 350,
34+
backgroundColor: 'grey',
35+
marginTop: 15
3736
}
3837
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.margelo.nitro.image
2+
3+
import com.margelo.nitro.core.ArrayBuffer
4+
5+
fun ArrayBuffer.copyIfNotOwner(): ArrayBuffer {
6+
if (!this.isOwner) {
7+
return ArrayBuffer.copy(this)
8+
}
9+
return this
10+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.margelo.nitro.image
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.ColorSpace
5+
import android.os.Build
6+
import androidx.core.graphics.createBitmap
7+
import java.io.File
8+
import java.io.FileOutputStream
9+
import java.nio.IntBuffer
10+
11+
private data class Swizzle(val r: Int, val g: Int, val b: Int, val a: Int, val bpp: Int)
12+
13+
private val SW = mapOf(
14+
PixelFormat.ARGB to Swizzle(1,2,3,0,4), // A R G B
15+
PixelFormat.BGRA to Swizzle(2,1,0,3,4), // B G R A
16+
PixelFormat.ABGR to Swizzle(3,2,1,0,4), // A B G R
17+
PixelFormat.RGBA to Swizzle(0,1,2,3,4), // R G B A
18+
PixelFormat.XRGB to Swizzle(1,2,3,-1,4),// X R G B
19+
PixelFormat.BGRX to Swizzle(2,1,0,-1,4),// B G R X
20+
PixelFormat.XBGR to Swizzle(3,2,1,-1,4),// X B G R
21+
PixelFormat.RGBX to Swizzle(0,1,2,-1,4),// R G B X
22+
PixelFormat.RGB to Swizzle(0,1,2,-1,3),// R G B
23+
PixelFormat.BGR to Swizzle(2,1,0,-1,3) // B G R
24+
)
25+
26+
fun bitmapFromRawPixelData(data: RawPixelData, allowGpu: Boolean): Bitmap {
27+
if (allowGpu) {
28+
// FAST PATH: Try using GPU Buffer (HardwareBuffer) if it is one. This is zero-copy!
29+
if (data.buffer.isHardwareBuffer && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
30+
val bitmap = Bitmap.wrapHardwareBuffer(data.buffer.getHardwareBuffer(), ColorSpace.get(ColorSpace.Named.SRGB))
31+
if (bitmap != null) {
32+
return bitmap
33+
}
34+
}
35+
}
36+
37+
val w = data.width.toInt()
38+
val h = data.height.toInt()
39+
val totalLength = data.buffer.size
40+
val bytesPerRow = totalLength / h
41+
val bytesPerPixel = bytesPerRow / w
42+
if (data.pixelFormat == PixelFormat.BGRA && bytesPerPixel == 4) {
43+
// FAST PATH: Source came from ARGB_8888 Bitmap bytes -> reinterpret as Ints with little endian
44+
val buffer = data.buffer.getBuffer(false).slice().order(java.nio.ByteOrder.LITTLE_ENDIAN)
45+
val source = buffer.asIntBuffer()
46+
val bitmap = createBitmap(w, h, Bitmap.Config.ARGB_8888)
47+
bitmap.isPremultiplied = true
48+
bitmap.copyPixelsFromBuffer(source)
49+
return bitmap
50+
}
51+
52+
// SLOW PATH: Perform a CPU copy of the Buffer and read byte by byte into a Bitmap
53+
val sw =
54+
SW[data.pixelFormat] ?: throw Error("Unsupported Pixel Format: ${data.pixelFormat}")
55+
val stride = w * sw.bpp
56+
57+
val buffer = data.buffer.getBuffer(false)
58+
buffer.rewind()
59+
if (buffer.remaining() < stride * h) {
60+
throw Error("ByteBuffer is too small! (Remaining: ${buffer.remaining()}/${buffer.capacity()}, Expected Bytes: ${stride * h})")
61+
}
62+
63+
val out = IntArray(w * h)
64+
65+
fun pack(r: Int, g: Int, b_: Int, aIn: Int, srcPremul: Boolean): Int {
66+
val a = if (aIn < 0) 0xFF else aIn
67+
val rr = if (srcPremul || a == 255) r else (r * a + 127) / 255
68+
val gg = if (srcPremul || a == 255) g else (g * a + 127) / 255
69+
val bb = if (srcPremul || a == 255) b_ else (b_ * a + 127) / 255
70+
return (a shl 24) or (rr shl 16) or (gg shl 8) or bb
71+
}
72+
73+
var di = 0
74+
if (sw.bpp == 4) {
75+
for (y in 0 until h) {
76+
val row = y * stride
77+
for (x in 0 until w) {
78+
val p = row + x * 4
79+
val b0 = buffer.get(p).toInt() and 0xFF
80+
val b1 = buffer.get(p + 1).toInt() and 0xFF
81+
val b2 = buffer.get(p + 2).toInt() and 0xFF
82+
val b3 = buffer.get(p + 3).toInt() and 0xFF
83+
val r = when (sw.r) {0->b0;1->b1;2->b2;else->b3}
84+
val g = when (sw.g) {0->b0;1->b1;2->b2;else->b3}
85+
val bl= when (sw.b) {0->b0;1->b1;2->b2;else->b3}
86+
val a = if (sw.a >= 0) when(sw.a){0->b0;1->b1;2->b2;else->b3} else 255
87+
val srcPremul = sw.a >= 0
88+
out[di++] = pack(r, g, bl, a, srcPremul)
89+
}
90+
}
91+
} else {
92+
for (y in 0 until h) {
93+
val row = y * stride
94+
for (x in 0 until w) {
95+
val p = row + x * 3
96+
val b0 = buffer.get(p).toInt() and 0xFF
97+
val b1 = buffer.get(p + 1).toInt() and 0xFF
98+
val b2 = buffer.get(p + 2).toInt() and 0xFF
99+
val r = when (sw.r) {0->b0;1->b1;else->b2}
100+
val g = when (sw.g) {0->b0;1->b1;else->b2}
101+
val bl= when (sw.b) {0->b0;1->b1;else->b2}
102+
out[di++] = pack(r, g, bl, 255, true) // opaque => already "premultiplied"
103+
}
104+
}
105+
}
106+
107+
val bitmap = createBitmap(w, h, Bitmap.Config.ARGB_8888)
108+
bitmap.isPremultiplied = true
109+
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(out))
110+
return bitmap
111+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.margelo.nitro.image
2+
3+
import android.graphics.Bitmap
4+
import android.os.Build
5+
6+
val Bitmap.isGPU: Boolean
7+
get() {
8+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
9+
this.config == Bitmap.Config.HARDWARE
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.margelo.nitro.image
2+
3+
import android.graphics.Bitmap
4+
import android.hardware.HardwareBuffer
5+
import android.os.Build
6+
7+
val Bitmap.pixelFormat: PixelFormat
8+
get() {
9+
when (config) {
10+
Bitmap.Config.ARGB_8888 -> {
11+
// ARGB_8888 is BGRA on Android (because of little endian)
12+
return PixelFormat.BGRA
13+
}
14+
Bitmap.Config.HARDWARE -> {
15+
// Hardware Buffer is either RGBA or RGBX
16+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
17+
when (hardwareBuffer.format) {
18+
HardwareBuffer.RGBA_8888 -> return PixelFormat.RGBA
19+
HardwareBuffer.RGBX_8888 -> return PixelFormat.RGBX
20+
HardwareBuffer.RGB_888 -> return PixelFormat.RGB
21+
}
22+
}
23+
return PixelFormat.UNKNOWN
24+
}
25+
else -> return PixelFormat.UNKNOWN
26+
}
27+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.margelo.nitro.image
2+
3+
import android.graphics.Bitmap
4+
import java.nio.ByteBuffer
5+
6+
fun Bitmap.toByteBuffer(): ByteBuffer {
7+
var bitmap = this
8+
if (isGPU) {
9+
// It's a GPU Bitmap - we need to copy it to CPU memory first.
10+
bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
11+
}
12+
13+
val buffer = ByteBuffer.allocateDirect(bitmap.byteCount)
14+
bitmap.copyPixelsToBuffer(buffer)
15+
buffer.rewind()
16+
return buffer
17+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.margelo.nitro.image
2+
3+
import java.io.OutputStream
4+
import java.nio.ByteBuffer
5+
6+
class FastByteArrayOutputStream(initialSize: Int = 64 * 1024) : OutputStream() {
7+
var bytes = ByteArray(initialSize)
8+
private set
9+
var count = 0
10+
private set
11+
12+
override fun write(b: Int) {
13+
val i = count + 1
14+
ensureCapacity(i)
15+
bytes[count] = b.toByte()
16+
count = i
17+
}
18+
19+
override fun write(b: ByteArray, off: Int, len: Int) {
20+
val i = count + len
21+
ensureCapacity(i)
22+
System.arraycopy(b, off, bytes, count, len)
23+
count = i
24+
}
25+
26+
private fun ensureCapacity(min: Int) {
27+
if (min <= bytes.size) return
28+
var newCap = bytes.size.coerceAtLeast(1)
29+
while (newCap < min) newCap = newCap shl 1
30+
bytes = bytes.copyOf(newCap)
31+
}
32+
33+
fun toByteBuffer(): ByteBuffer = ByteBuffer.wrap(bytes, 0, count)
34+
}

packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImage.kt

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package com.margelo.nitro.image
33
import android.graphics.Bitmap
44
import android.os.Build
55
import androidx.annotation.Keep
6+
import androidx.core.graphics.scale
7+
import com.facebook.common.memory.PooledByteBufferOutputStream
68
import com.facebook.proguard.annotations.DoNotStrip
79
import com.madebyevan.thumbhash.ThumbHash
810
import com.margelo.nitro.core.ArrayBuffer
911
import com.margelo.nitro.core.Promise
12+
import java.io.ByteArrayOutputStream
1013
import java.io.File
1114
import java.io.FileOutputStream
1215
import java.io.IOException
@@ -30,35 +33,49 @@ class HybridImage: HybridImageSpec {
3033
this.bitmap = bitmap
3134
}
3235

33-
private val isGPU: Boolean
34-
get() {
35-
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
36-
bitmap.config == Bitmap.Config.HARDWARE
37-
}
38-
private fun toByteBuffer(): ByteBuffer {
39-
var bitmap = bitmap
40-
if (isGPU) {
41-
// It's a GPU Bitmap - we need to copy it to CPU memory first.
42-
bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
36+
override fun toRawPixelData(allowGpu: Boolean?): RawPixelData {
37+
val allowGpu = allowGpu ?: false
38+
if (allowGpu && bitmap.isGPU && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
39+
// Wrap the existing GPU buffer (HardwareBuffer)
40+
val arrayBuffer = ArrayBuffer.wrap(bitmap.hardwareBuffer)
41+
return RawPixelData(arrayBuffer, width, height, bitmap.pixelFormat)
42+
} else {
43+
// Copy the data into a CPU buffer (ByteBuffer)
44+
var bitmap = bitmap
45+
if (bitmap.isGPU) {
46+
// If this is a GPU-based bitmap (but we cannot use GPU), copy it to a CPU Bitmap first
47+
bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
48+
}
49+
val buffer = bitmap.toByteBuffer()
50+
val arrayBuffer = ArrayBuffer.wrap(buffer)
51+
return RawPixelData(arrayBuffer, width, height, bitmap.pixelFormat)
4352
}
44-
45-
val buffer = ByteBuffer.allocateDirect(bitmap.byteCount)
46-
bitmap.copyPixelsToBuffer(buffer)
47-
buffer.rewind()
48-
return buffer
53+
}
54+
override fun toRawPixelDataAsync(allowGpu: Boolean?): Promise<RawPixelData> {
55+
return Promise.async { toRawPixelData(allowGpu) }
4956
}
5057

51-
override fun toArrayBuffer(): ArrayBuffer {
52-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isGPU) {
53-
return ArrayBuffer.wrap(bitmap.hardwareBuffer)
54-
} else {
55-
val buffer = toByteBuffer()
56-
return ArrayBuffer.wrap(buffer)
58+
override fun toEncodedImageData(format: ImageFormat, quality: Double?): EncodedImageData {
59+
val quality = quality ?: 1.0
60+
val estimatedByteSize = when (format) {
61+
ImageFormat.JPG -> (width * height) / 2
62+
ImageFormat.PNG -> width * height
63+
}
64+
val outputStream = FastByteArrayOutputStream(estimatedByteSize.toInt())
65+
val successful = bitmap.compress(format.toBitmapFormat(), quality.toInt(), outputStream)
66+
if (!successful) {
67+
throw Error("Failed to compress the Bitmap into EncodedImageData! (Format: ${format.name}, " +
68+
"Quality: ${quality}, Written Bytes: ${outputStream.count})")
5769
}
70+
val byteBuffer = outputStream.toByteBuffer()
71+
val arrayBuffer = ArrayBuffer.wrap(byteBuffer)
72+
return EncodedImageData(arrayBuffer, width, height, format)
5873
}
59-
60-
override fun toArrayBufferAsync(): Promise<ArrayBuffer> {
61-
return Promise.async { toArrayBuffer() }
74+
override fun toEncodedImageDataAsync(
75+
format: ImageFormat,
76+
quality: Double?
77+
): Promise<EncodedImageData> {
78+
return Promise.async { toEncodedImageData(format, quality) }
6279
}
6380

6481
override fun resize(width: Double, height: Double): HybridImageSpec {
@@ -68,7 +85,7 @@ class HybridImage: HybridImageSpec {
6885
if (height < 0) {
6986
throw Error("Height cannot be less than 0! (height: $height)")
7087
}
71-
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, width.toInt(), height.toInt(), true)
88+
val resizedBitmap = bitmap.scale(width.toInt(), height.toInt(), true)
7289
return HybridImage(resizedBitmap)
7390
}
7491
override fun resizeAsync(width: Double, height: Double): Promise<HybridImageSpec> {
@@ -84,7 +101,13 @@ class HybridImage: HybridImageSpec {
84101
if (height < 0) {
85102
throw Error("Height cannot be less than 0! (startY: $startY - endY: $endY = $height)")
86103
}
87-
val croppedBitmap = Bitmap.createBitmap(bitmap, startX.toInt(), startY.toInt(), width.toInt(), height.toInt())
104+
val croppedBitmap = Bitmap.createBitmap(
105+
bitmap,
106+
startX.toInt(),
107+
startY.toInt(),
108+
width.toInt(),
109+
height.toInt()
110+
)
88111
return HybridImage(croppedBitmap)
89112
}
90113

@@ -100,14 +123,15 @@ class HybridImage: HybridImageSpec {
100123
override fun saveToFileAsync(
101124
path: String,
102125
format: ImageFormat,
103-
quality: Double
126+
quality: Double?
104127
): Promise<Unit> {
128+
val quality = quality ?: 1.0
105129
return Promise.async {
106130
bitmap.saveToFile(path, format, quality.toInt())
107131
}
108132
}
109133

110-
override fun saveToTemporaryFileAsync(format: ImageFormat, quality: Double): Promise<String> {
134+
override fun saveToTemporaryFileAsync(format: ImageFormat, quality: Double?): Promise<String> {
111135
return Promise.async {
112136
val tempFile = File.createTempFile("nitro_image_", format.name)
113137
this.saveToFileAsync(tempFile.path, format, quality)
@@ -121,7 +145,7 @@ class HybridImage: HybridImageSpec {
121145
"Resize the image to <100 pixels in width and height first, then try again!")
122146
}
123147

124-
val bitmapBuffer = toByteBuffer()
148+
val bitmapBuffer = bitmap.toByteBuffer()
125149

126150
val thumbHash = ThumbHash.rgbaToThumbHash(bitmap.width, bitmap.height, bitmapBuffer.array())
127151
val buffer = ByteBuffer.wrap(thumbHash)

0 commit comments

Comments
 (0)