Merge pull request #385 from guzba/master

bmp dib decoder
This commit is contained in:
treeform 2022-02-13 19:28:49 -08:00 committed by GitHub
commit 2c38804022
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 105 deletions

View file

@ -9,7 +9,7 @@ requires "nim >= 1.4.8"
requires "vmath >= 1.1.0"
requires "chroma >= 0.2.5"
requires "zippy >= 0.8.1"
requires "flatty >= 0.2.2"
requires "flatty >= 0.2.4"
requires "nimsimd >= 1.0.0"
requires "bumpy >= 1.0.3"

View file

@ -18,7 +18,7 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: [].
## Convert a straight alpha RGBA to a premultiplied alpha RGBA.
c.rgbx()
proc decodeImage*(data: string | seq[uint8]): Image {.raises: [PixieError].} =
proc decodeImage*(data: string): Image {.raises: [PixieError].} =
## Loads an image from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePng(data)
@ -38,7 +38,7 @@ proc decodeImage*(data: string | seq[uint8]): Image {.raises: [PixieError].} =
else:
raise newException(PixieError, "Unsupported image file format")
proc decodeMask*(data: string | seq[uint8]): Mask {.raises: [PixieError].} =
proc decodeMask*(data: string): Mask {.raises: [PixieError].} =
## Loads a mask from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
newMask(decodePng(data))

View file

@ -2,100 +2,117 @@ import bitops, chroma, flatty/binny, pixie/common, pixie/images
# See: https://en.wikipedia.org/wiki/BMP_file_format
# See: https://bmptestsuite.sourceforge.io/
# https://docs.microsoft.com/en-us/windows/win32/gdi/bitmap-header-types
# https://stackoverflow.com/questions/61788908/windows-clipboard-getclipboarddata-for-cf-dibv5-causes-the-image-on-the-clip
# https://stackoverflow.com/questions/44177115/copying-from-and-to-clipboard-loses-image-transparency/46424800#46424800
const bmpSignature* = "BM"
proc colorMaskShift(color: uint32, mask: uint32): uint8 =
template failInvalid() =
raise newException(PixieError, "Invalid BMP buffer, unable to load")
proc colorMaskShift(color, mask: uint32): uint8 {.inline.} =
((color and mask) shr (mask.firstSetBit() - 1)).uint8
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
## Decodes bitmap data into an Image.
proc decodeDib*(
data: pointer, len: int, lpBitmapInfo = false
): Image {.raises: [PixieError].} =
## Decodes DIB data into an Image.
if data.len < 48:
raise newException(PixieError, "Invalid BMP data")
if len < 40:
failInvalid()
# BMP Header
if data[0 .. 1] != "BM":
raise newException(PixieError, "Invalid BMP data")
let data = cast[ptr UncheckedArray[uint8]](data)
let
bits = data.readUint16(28).int
compression = data.readUint32(30).int
dibHeader = data.readInt32(14).int
# BITMAPINFOHEADER
var
numColors = data.readInt32(46).int
width = data.readInt32(18).int
height = data.readInt32(22).int
offset = data.readUInt32(10).int
# Default channels if header does not contain them:
redChannel = 0x00FF0000.uint32
greenChannel = 0x0000FF00.uint32
blueChannel = 0x000000FF.uint32
alphaChannel = 0xFF000000.uint32
useAlpha = false
flipVertical = false
headerSize = data.readInt32(0).int
width = data.readInt32(4).int
height = data.readInt32(8).int
planes = data.readUint16(12).int
bits = data.readUint16(14).int
compression = data.readInt32(16).int
colorPaletteSize = data.readInt32(32).int
if numColors < 0 or numColors > 256:
raise newException(PixieError, "Invalid number of colors")
if dibHeader notin [40, 108]:
raise newException(PixieError, "Invalid BMP data")
if headerSize notin [40, 108, 124]:
failInvalid()
var
colorTable = newSeq[ColorRGBA](numColors)
if planes != 1:
failInvalid()
if dibHeader == 108:
if data.len < 14 + dibHeader:
raise newException(PixieError, "Invalid BMP data")
redChannel = data.readUInt32(54)
greenChannel = data.readUInt32(58)
blueChannel = data.readUInt32(62)
alphaChannel = data.readUInt32(66)
useAlpha = true
if bits == 8 and numColors == 0:
numColors = 256
colorTable = newSeq[ColorRGBA](numColors)
if numColors > 0:
if data.len < 14 + dibHeader + numColors * 4:
raise newException(PixieError, "Invalid BMP data")
var colorOffset = dibHeader + 14
for i in 0 ..< numColors:
var rgba: ColorRGBA
if colorOffset + 3 > data.len - 2:
raise newException(PixieError, "Truncated BMP data")
rgba.r = data.readUint8(colorOffset + 2)
rgba.g = data.readUint8(colorOffset + 1)
rgba.b = data.readUint8(colorOffset + 0)
rgba.a = 255
colorOffset += 4
colorTable[i] = rgba
if redChannel == 0 or greenChannel == 0 or
blueChannel == 0 or alphaChannel == 0:
raise newException(PixieError, "Unsupported 0 channel mask.")
if bits notin [1, 4, 8, 32, 24]:
raise newException(PixieError, "Unsupported BMP data format")
if bits notin [1, 4, 8, 24, 32]:
raise newException(PixieError, "Unsupported BMP bit count")
if compression notin [0, 3]:
raise newException(PixieError, "Unsupported BMP data format")
raise newException(PixieError, "Unsupported BMP compression format")
var
redMask = 0x00FF0000.uint32
greenMask = 0x0000FF00.uint32
blueMask = 0x000000FF.uint32
alphaMask = 0xFF000000.uint32
flipVertical: bool
useAlpha: bool
if compression == 3:
if len < 52:
failInvalid()
redMask = data.readUInt32(40)
greenMask = data.readUInt32(44)
blueMask = data.readUInt32(48)
if redMask == 0 or blueMask == 0 or greenMask == 0:
failInvalid()
if headerSize > 40:
if len < 56:
failInvalid()
alphaMask = data.readUInt32(52)
useAlpha = alphaMask != 0
if colorPaletteSize < 0 or colorPaletteSize > 256:
failInvalid()
if bits == 8 and colorPaletteSize == 0:
colorPaletteSize = 256
var colorPalette = newSeq[ColorRGBA](colorPaletteSize)
if colorPaletteSize > 0:
if len < headerSize + colorPaletteSize * 4:
failInvalid()
var offset = headerSize
for i in 0 ..< colorPaletteSize:
var rgba: ColorRGBA
if offset + 3 > len - 2:
failInvalid()
rgba.r = data.readUint8(offset + 2)
rgba.g = data.readUint8(offset + 1)
rgba.b = data.readUint8(offset + 0)
rgba.a = 255
offset += 4
colorPalette[i] = rgba
if height < 0:
height = -height
flipVertical = true
result = newImage(width, height)
let startOffset = offset
var startOffset = headerSize + colorPaletteSize * 4
if compression == 3 and (headerSize == 40 or lpBitmapInfo):
startOffset += 12
var offset = startOffset
if bits == 1:
var
haveBits = 0
colorBits: uint8 = 0
for y in 0 ..< result.height:
# pad the row
haveBits = 0
let padding = (offset - startOffset) mod 4
if padding > 0:
@ -103,25 +120,24 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
for x in 0 ..< result.width:
var rgba: ColorRGBA
if haveBits == 0:
if offset >= data.len:
raise newException(PixieError, "Truncated BMP data")
if offset >= len:
failInvalid()
colorBits = data.readUint8(offset)
haveBits = 8
offset += 1
if (colorBits and 0b1000_0000) == 0:
rgba = colorTable[0]
rgba = colorPalette[0]
else:
rgba = colorTable[1]
rgba = colorPalette[1]
colorBits = colorBits shl 1
dec haveBits
result[x, result.height - y - 1] = rgba.rgbx()
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
elif bits == 4:
var
haveBits = 0
colorBits: uint8 = 0
for y in 0 ..< result.height:
# pad the row
haveBits = 0
let padding = (offset - startOffset) mod 4
if padding > 0:
@ -129,76 +145,87 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
for x in 0 ..< result.width:
var rgba: ColorRGBA
if haveBits == 0:
if offset >= data.len:
raise newException(PixieError, "Truncated BMP data")
if offset >= len:
failInvalid()
colorBits = data.readUint8(offset)
haveBits = 8
offset += 1
let index = (colorBits and 0b1111_0000) shr 4
if index.int >= numColors:
raise newException(PixieError, "Invalid BMP index")
rgba = colorTable[index]
if index.int >= colorPaletteSize:
failInvalid()
rgba = colorPalette[index]
colorBits = colorBits shl 4
haveBits -= 4
result[x, result.height - y - 1] = rgba.rgbx()
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
elif bits == 8:
for y in 0 ..< result.height:
# pad the row
let padding = (offset - startOffset) mod 4
if padding > 0:
offset += 4 - padding
for x in 0 ..< result.width:
if offset >= data.len:
raise newException(PixieError, "Truncated BMP data")
if offset >= len:
failInvalid()
var rgba: ColorRGBA
let index = data.readUint8(offset)
offset += 1
if index.int >= numColors:
raise newException(PixieError, "Invalid BMP index")
rgba = colorTable[index]
result[x, result.height - y - 1] = rgba.rgbx()
if index.int >= colorPaletteSize:
failInvalid()
rgba = colorPalette[index]
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
elif bits == 24:
for y in 0 ..< result.height:
# pad the row
let padding = (offset - startOffset) mod 4
if padding > 0:
offset += 4 - padding
for x in 0 ..< result.width:
if offset + 2 >= data.len:
raise newException(PixieError, "Truncated BMP data")
if offset + 2 >= len:
failInvalid()
var rgba: ColorRGBA
rgba.r = data.readUint8(offset + 2)
rgba.g = data.readUint8(offset + 1)
rgba.b = data.readUint8(offset + 0)
rgba.a = 255
offset += 3
result[x, result.height - y - 1] = rgba.rgbx()
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
elif bits == 32:
for y in 0 ..< result.height:
for x in 0 ..< result.width:
if offset + 3 >= data.len:
raise newException(PixieError, "Truncated BMP data")
var rgba: ColorRGBA
if offset + 3 >= len:
failInvalid()
let color = data.readUint32(offset)
rgba.r = color.colorMaskShift(redChannel)
rgba.g = color.colorMaskShift(greenChannel)
rgba.b = color.colorMaskShift(blueChannel)
if useAlpha:
rgba.a = color.colorMaskShift(alphaChannel)
var rgbx: ColorRGBX
rgbx.r = color.colorMaskShift(redMask)
rgbx.g = color.colorMaskShift(greenMask)
rgbx.b = color.colorMaskShift(blueMask)
rgbx.a = color.colorMaskShift(alphaMask)
result.unsafe[x, result.height - y - 1] = rgbx
else:
var rgba: ColorRGBA
rgba.r = color.colorMaskShift(redMask)
rgba.g = color.colorMaskShift(greenMask)
rgba.b = color.colorMaskShift(blueMask)
rgba.a = 255
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
offset += 4
result[x, result.height - y - 1] = rgba.rgbx()
if flipVertical:
result.flipVertical()
proc decodeBmp*(data: seq[uint8]): Image {.inline, raises: [PixieError].} =
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
## Decodes bitmap data into an Image.
decodeBmp(cast[string](data))
if data.len < 14:
failInvalid()
# BMP Header
if data[0 .. 1] != "BM":
failInvalid()
decodeDib(data[14].unsafeAddr, data.len - 14)
proc encodeBmp*(image: Image): string {.raises: [].} =
## Encodes an image into the BMP file format.

View file

@ -3,4 +3,4 @@ import benchy, pixie/fileformats/jpg
let data = readFile("tests/fileformats/jpg/jpeg420exif.jpg")
timeIt "pixie decode":
discard decodeJpg(cast[seq[uint8]](data))
discard decodeJpg(data)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 B

After

Width:  |  Height:  |  Size: 890 B