Skip to content

Fix R/B channel swap in Android raw pixel data - #158

Merged
mrousavy merged 2 commits into
mainfrom
fix/android-raw-pixel-rb-swap
Aug 6, 2026
Merged

Fix R/B channel swap in Android raw pixel data#158
mrousavy merged 2 commits into
mainfrom
fix/android-raw-pixel-rb-swap

Conversation

@mrousavy

Copy link
Copy Markdown
Owner

What

On Android, an ARGB_8888 Bitmap's memory is physically [R, G, B, A] bytes. The ARGB in the config name only describes the getPixel()/setPixel() ColorInt packing (0xAARRGGBB), not the layout that copyPixelsFromBuffer/copyPixelsToBuffer (raw memory copies) use.

Two coupled bugs made every raw pixel roundtrip swap red and blue on little-endian devices:

  • loadFromRawPixelData (slow path) packed ColorInt-ordered words and wrote them with copyPixelsFromBuffer, so on little-endian they landed as B, G, R, A bytes.
  • toRawPixelData labeled ARGB_8888 exports as BGRA, but the raw-copied bytes are physically R, G, B, A.

Fix

  • Pack the physical pixel word (0xAABBGGRR) in the slow path.
  • Label ARGB_8888 exports as RGBA.
  • Re-key the byte-identical raw-copy fast path from BGRA to RGBA, so BGRA input now goes through the corrected swizzle path.
  • Export PixelFormat, RawPixelData, EncodedImageData and ImageFormat from the entrypoint.

Note for consumers

This is a behavior fix. Code that worked around the swap by mislabeling RGBA bytes as 'BGRA' on Android must drop that workaround (for example react-native-vision-camera's harness tests).

Regression tests covering this land separately so they can validate all of the raw pixel data fixes together.

🤖 Generated with Claude Code

An ARGB_8888 Bitmap stores each pixel as a 32-bit word with R in the
least significant byte (physically [R, G, B, A] bytes). The "ARGB" in the
config name only refers to the getPixel/setPixel ColorInt packing
(0xAARRGGBB), not the memory layout that copyPixelsFromBuffer and
copyPixelsToBuffer read and write.

The slow load path packed ColorInt-ordered words, and pixelFormat
labeled ARGB_8888 exports as BGRA, so raw pixel data roundtrips swapped
red and blue on little-endian devices. Pack the physical word instead,
label exports as RGBA, and re-key the byte-identical raw-copy fast path
to RGBA so BGRA input now goes through the corrected swizzle path.

Also expose PixelFormat, RawPixelData, EncodedImageData and ImageFormat
from the package entrypoint so consumers can read the reported format.
@patrickkabwe

Copy link
Copy Markdown

@mrousavy ran into this issue on android toRawPixelData() reports BGRA for ARGB_8888 bitmaps, but the bytes are actually RGBA so red and blue come out swapped if you read the buffer using the format it hands you. Here's a snippet that shows it

const image = await Images.loadFromFileAsync(anyColourfulPhoto)
const raw = await image.toRawPixelDataAsync()
const bytes = new Uint8Array(raw.buffer)
// Mean of each byte position across the image.
const means = [0, 1, 2, 3].map((k) => {
  let sum = 0
  for (let i = k; i < bytes.length; i += 4) sum += bytes[i]
  return sum / (bytes.length / 4)
})
console.log(raw.pixelFormat, means)
// Android: 'BGRA' [122.9, 124.7, 102.8, 255]  <- byte 0 is red, not blue
// iOS:     'BGRA' [103.2, 125.5, 123.3, 255]  <- byte 0 is blue, as reported

I tested this PR in both debug and release build it fixes the issue.

@mrousavy

mrousavy commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

thanks for your feedback! lgtm then

@mrousavy
mrousavy merged commit dad68b3 into main Aug 6, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants