bmp dib decoder
This commit is contained in:
parent
17ad1fbbd1
commit
5282caa056
3 changed files with 117 additions and 89 deletions
|
@ -9,7 +9,7 @@ requires "nim >= 1.4.8"
|
||||||
requires "vmath >= 1.1.0"
|
requires "vmath >= 1.1.0"
|
||||||
requires "chroma >= 0.2.5"
|
requires "chroma >= 0.2.5"
|
||||||
requires "zippy >= 0.8.1"
|
requires "zippy >= 0.8.1"
|
||||||
requires "flatty >= 0.2.2"
|
requires "flatty >= 0.2.4"
|
||||||
requires "nimsimd >= 1.0.0"
|
requires "nimsimd >= 1.0.0"
|
||||||
requires "bumpy >= 1.0.3"
|
requires "bumpy >= 1.0.3"
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@ import bitops, chroma, flatty/binny, pixie/common, pixie/images
|
||||||
|
|
||||||
# See: https://en.wikipedia.org/wiki/BMP_file_format
|
# See: https://en.wikipedia.org/wiki/BMP_file_format
|
||||||
# See: https://bmptestsuite.sourceforge.io/
|
# 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"
|
const bmpSignature* = "BM"
|
||||||
|
|
||||||
|
@ -11,94 +14,105 @@ template failInvalid() =
|
||||||
proc colorMaskShift(color, mask: uint32): uint8 {.inline.} =
|
proc colorMaskShift(color, mask: uint32): uint8 {.inline.} =
|
||||||
((color and mask) shr (mask.firstSetBit() - 1)).uint8
|
((color and mask) shr (mask.firstSetBit() - 1)).uint8
|
||||||
|
|
||||||
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
proc decodeDib*(
|
||||||
## Decodes bitmap data into an Image.
|
data: pointer, len: int, lpBitmapInfo = false
|
||||||
|
): Image {.raises: [PixieError].} =
|
||||||
|
## Decodes DIB data into an Image.
|
||||||
|
|
||||||
if data.len < 48:
|
if len < 40:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
|
|
||||||
# BMP Header
|
let data = cast[ptr UncheckedArray[uint8]](data)
|
||||||
if data[0 .. 1] != "BM":
|
|
||||||
failInvalid()
|
|
||||||
|
|
||||||
let
|
# BITMAPINFOHEADER
|
||||||
bits = data.readUint16(28).int
|
|
||||||
compression = data.readUint32(30).int
|
|
||||||
dibHeader = data.readInt32(14).int
|
|
||||||
var
|
var
|
||||||
numColors = data.readInt32(46).int
|
headerSize = data.readInt32(0).int
|
||||||
width = data.readInt32(18).int
|
width = data.readInt32(4).int
|
||||||
height = data.readInt32(22).int
|
height = data.readInt32(8).int
|
||||||
offset = data.readUInt32(10).int
|
planes = data.readUint16(12).int
|
||||||
# Default channels if header does not contain them:
|
bits = data.readUint16(14).int
|
||||||
redChannel = 0x00FF0000.uint32
|
compression = data.readInt32(16).int
|
||||||
greenChannel = 0x0000FF00.uint32
|
colorPaletteSize = data.readInt32(32).int
|
||||||
blueChannel = 0x000000FF.uint32
|
|
||||||
alphaChannel = 0xFF000000.uint32
|
|
||||||
useAlpha = false
|
|
||||||
flipVertical = false
|
|
||||||
|
|
||||||
if numColors < 0 or numColors > 256:
|
if headerSize notin [40, 108, 124]:
|
||||||
failInvalid()
|
|
||||||
if dibHeader notin [40, 108]:
|
|
||||||
failInvalid()
|
failInvalid()
|
||||||
|
|
||||||
var
|
if planes != 1:
|
||||||
colorTable = newSeq[ColorRGBA](numColors)
|
|
||||||
|
|
||||||
if dibHeader == 108:
|
|
||||||
if data.len < 14 + dibHeader:
|
|
||||||
failInvalid()
|
|
||||||
|
|
||||||
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:
|
|
||||||
failInvalid()
|
|
||||||
|
|
||||||
var colorOffset = dibHeader + 14
|
|
||||||
for i in 0 ..< numColors:
|
|
||||||
var rgba: ColorRGBA
|
|
||||||
if colorOffset + 3 > data.len - 2:
|
|
||||||
failInvalid()
|
|
||||||
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:
|
|
||||||
failInvalid()
|
failInvalid()
|
||||||
|
|
||||||
if bits notin [1, 4, 8, 32, 24]:
|
if bits notin [1, 4, 8, 24, 32]:
|
||||||
raise newException(PixieError, "Unsupported BMP data format")
|
raise newException(PixieError, "Unsupported BMP bit count")
|
||||||
|
|
||||||
if compression notin [0, 3]:
|
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:
|
if height < 0:
|
||||||
height = -height
|
height = -height
|
||||||
flipVertical = true
|
flipVertical = true
|
||||||
|
|
||||||
result = newImage(width, height)
|
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:
|
if bits == 1:
|
||||||
var
|
var
|
||||||
haveBits = 0
|
haveBits = 0
|
||||||
colorBits: uint8 = 0
|
colorBits: uint8 = 0
|
||||||
for y in 0 ..< result.height:
|
for y in 0 ..< result.height:
|
||||||
# pad the row
|
|
||||||
haveBits = 0
|
haveBits = 0
|
||||||
let padding = (offset - startOffset) mod 4
|
let padding = (offset - startOffset) mod 4
|
||||||
if padding > 0:
|
if padding > 0:
|
||||||
|
@ -106,25 +120,24 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
||||||
for x in 0 ..< result.width:
|
for x in 0 ..< result.width:
|
||||||
var rgba: ColorRGBA
|
var rgba: ColorRGBA
|
||||||
if haveBits == 0:
|
if haveBits == 0:
|
||||||
if offset >= data.len:
|
if offset >= len:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
colorBits = data.readUint8(offset)
|
colorBits = data.readUint8(offset)
|
||||||
haveBits = 8
|
haveBits = 8
|
||||||
offset += 1
|
offset += 1
|
||||||
if (colorBits and 0b1000_0000) == 0:
|
if (colorBits and 0b1000_0000) == 0:
|
||||||
rgba = colorTable[0]
|
rgba = colorPalette[0]
|
||||||
else:
|
else:
|
||||||
rgba = colorTable[1]
|
rgba = colorPalette[1]
|
||||||
colorBits = colorBits shl 1
|
colorBits = colorBits shl 1
|
||||||
dec haveBits
|
dec haveBits
|
||||||
result[x, result.height - y - 1] = rgba.rgbx()
|
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
|
||||||
|
|
||||||
elif bits == 4:
|
elif bits == 4:
|
||||||
var
|
var
|
||||||
haveBits = 0
|
haveBits = 0
|
||||||
colorBits: uint8 = 0
|
colorBits: uint8 = 0
|
||||||
for y in 0 ..< result.height:
|
for y in 0 ..< result.height:
|
||||||
# pad the row
|
|
||||||
haveBits = 0
|
haveBits = 0
|
||||||
let padding = (offset - startOffset) mod 4
|
let padding = (offset - startOffset) mod 4
|
||||||
if padding > 0:
|
if padding > 0:
|
||||||
|
@ -132,44 +145,42 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
||||||
for x in 0 ..< result.width:
|
for x in 0 ..< result.width:
|
||||||
var rgba: ColorRGBA
|
var rgba: ColorRGBA
|
||||||
if haveBits == 0:
|
if haveBits == 0:
|
||||||
if offset >= data.len:
|
if offset >= len:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
colorBits = data.readUint8(offset)
|
colorBits = data.readUint8(offset)
|
||||||
haveBits = 8
|
haveBits = 8
|
||||||
offset += 1
|
offset += 1
|
||||||
let index = (colorBits and 0b1111_0000) shr 4
|
let index = (colorBits and 0b1111_0000) shr 4
|
||||||
if index.int >= numColors:
|
if index.int >= colorPaletteSize:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
rgba = colorTable[index]
|
rgba = colorPalette[index]
|
||||||
colorBits = colorBits shl 4
|
colorBits = colorBits shl 4
|
||||||
haveBits -= 4
|
haveBits -= 4
|
||||||
result[x, result.height - y - 1] = rgba.rgbx()
|
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
|
||||||
|
|
||||||
elif bits == 8:
|
elif bits == 8:
|
||||||
for y in 0 ..< result.height:
|
for y in 0 ..< result.height:
|
||||||
# pad the row
|
|
||||||
let padding = (offset - startOffset) mod 4
|
let padding = (offset - startOffset) mod 4
|
||||||
if padding > 0:
|
if padding > 0:
|
||||||
offset += 4 - padding
|
offset += 4 - padding
|
||||||
for x in 0 ..< result.width:
|
for x in 0 ..< result.width:
|
||||||
if offset >= data.len:
|
if offset >= len:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
var rgba: ColorRGBA
|
var rgba: ColorRGBA
|
||||||
let index = data.readUint8(offset)
|
let index = data.readUint8(offset)
|
||||||
offset += 1
|
offset += 1
|
||||||
if index.int >= numColors:
|
if index.int >= colorPaletteSize:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
rgba = colorTable[index]
|
rgba = colorPalette[index]
|
||||||
result[x, result.height - y - 1] = rgba.rgbx()
|
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
|
||||||
|
|
||||||
elif bits == 24:
|
elif bits == 24:
|
||||||
for y in 0 ..< result.height:
|
for y in 0 ..< result.height:
|
||||||
# pad the row
|
|
||||||
let padding = (offset - startOffset) mod 4
|
let padding = (offset - startOffset) mod 4
|
||||||
if padding > 0:
|
if padding > 0:
|
||||||
offset += 4 - padding
|
offset += 4 - padding
|
||||||
for x in 0 ..< result.width:
|
for x in 0 ..< result.width:
|
||||||
if offset + 2 >= data.len:
|
if offset + 2 >= len:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
var rgba: ColorRGBA
|
var rgba: ColorRGBA
|
||||||
rgba.r = data.readUint8(offset + 2)
|
rgba.r = data.readUint8(offset + 2)
|
||||||
|
@ -177,28 +188,45 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
||||||
rgba.b = data.readUint8(offset + 0)
|
rgba.b = data.readUint8(offset + 0)
|
||||||
rgba.a = 255
|
rgba.a = 255
|
||||||
offset += 3
|
offset += 3
|
||||||
result[x, result.height - y - 1] = rgba.rgbx()
|
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
|
||||||
|
|
||||||
elif bits == 32:
|
elif bits == 32:
|
||||||
for y in 0 ..< result.height:
|
for y in 0 ..< result.height:
|
||||||
for x in 0 ..< result.width:
|
for x in 0 ..< result.width:
|
||||||
if offset + 3 >= data.len:
|
if offset + 3 >= len:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
var rgba: ColorRGBA
|
|
||||||
let color = data.readUint32(offset)
|
let color = data.readUint32(offset)
|
||||||
rgba.r = color.colorMaskShift(redChannel)
|
|
||||||
rgba.g = color.colorMaskShift(greenChannel)
|
|
||||||
rgba.b = color.colorMaskShift(blueChannel)
|
|
||||||
if useAlpha:
|
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:
|
else:
|
||||||
|
var rgba: ColorRGBA
|
||||||
|
rgba.r = color.colorMaskShift(redMask)
|
||||||
|
rgba.g = color.colorMaskShift(greenMask)
|
||||||
|
rgba.b = color.colorMaskShift(blueMask)
|
||||||
rgba.a = 255
|
rgba.a = 255
|
||||||
|
result.unsafe[x, result.height - y - 1] = rgba.rgbx()
|
||||||
offset += 4
|
offset += 4
|
||||||
result[x, result.height - y - 1] = rgba.rgbx()
|
|
||||||
|
|
||||||
if flipVertical:
|
if flipVertical:
|
||||||
result.flipVertical()
|
result.flipVertical()
|
||||||
|
|
||||||
|
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
||||||
|
## Decodes bitmap data into an Image.
|
||||||
|
|
||||||
|
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: [].} =
|
proc encodeBmp*(image: Image): string {.raises: [].} =
|
||||||
## Encodes an image into the BMP file format.
|
## Encodes an image into the BMP file format.
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 890 B After Width: | Height: | Size: 890 B |
Loading…
Reference in a new issue