Read more bmp formats.
This commit is contained in:
parent
ffc1b90c5f
commit
4dc50d9778
3 changed files with 194 additions and 20 deletions
|
@ -1,6 +1,9 @@
|
|||
import bitops, chroma, flatty/binny, pixie/common, pixie/images
|
||||
|
||||
import print, strutils
|
||||
|
||||
# See: https://en.wikipedia.org/wiki/BMP_file_format
|
||||
# See: https://bmptestsuite.sourceforge.io/
|
||||
|
||||
const bmpSignature* = "BM"
|
||||
|
||||
|
@ -10,64 +13,192 @@ proc colorMaskShift(color: uint32, mask: uint32): uint8 =
|
|||
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
||||
## Decodes bitmap data into an Image.
|
||||
|
||||
if data.len < 48:
|
||||
raise newException(PixieError, "Invalid BMP data")
|
||||
|
||||
# BMP Header
|
||||
if data[0 .. 1] != "BM":
|
||||
raise newException(PixieError, "Invalid BMP data")
|
||||
|
||||
let
|
||||
width = data.readInt32(18).int
|
||||
height = data.readInt32(22).int
|
||||
bits = data.readUint16(28).int
|
||||
compression = data.readUint32(30).int
|
||||
dibHeader = data.readInt32(14).int
|
||||
|
||||
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
|
||||
|
||||
if numColors < 0:
|
||||
raise newException(PixieError, "Invalid BMP data")
|
||||
if dibHeader notin [40, 108]:
|
||||
raise newException(PixieError, "Invalid BMP data")
|
||||
|
||||
var
|
||||
colorTable = newSeq[ColorRGBA](numColors)
|
||||
|
||||
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 [32, 24]:
|
||||
if bits notin [1, 4, 8, 32, 24]:
|
||||
raise newException(PixieError, "Unsupported BMP data format")
|
||||
|
||||
if compression notin [0, 3]:
|
||||
raise newException(PixieError, "Unsupported BMP data format")
|
||||
|
||||
let channels = if bits == 32: 4 else: 3
|
||||
if width * height * channels + offset > data.len:
|
||||
raise newException(PixieError, "Invalid BMP data size")
|
||||
if height < 0:
|
||||
height = -height
|
||||
flipVertical = true
|
||||
|
||||
result = newImage(width, height)
|
||||
let startOffset = offset
|
||||
|
||||
for y in 0 ..< result.height:
|
||||
for x in 0 ..< result.width:
|
||||
var rgba: ColorRGBA
|
||||
if bits == 32:
|
||||
let color = data.readUint32(offset)
|
||||
rgba.r = color.colorMaskShift(redChannel)
|
||||
rgba.g = color.colorMaskShift(greenChannel)
|
||||
rgba.b = color.colorMaskShift(blueChannel)
|
||||
rgba.a = color.colorMaskShift(alphaChannel)
|
||||
offset += 4
|
||||
elif bits == 24:
|
||||
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:
|
||||
offset += 4 - padding
|
||||
for x in 0 ..< result.width:
|
||||
if offset >= data.len:
|
||||
raise newException(PixieError, "Truncated BMP data")
|
||||
var rgba: ColorRGBA
|
||||
if haveBits == 0:
|
||||
colorBits = data.readUint8(offset)
|
||||
haveBits = 8
|
||||
offset += 1
|
||||
if (colorBits and 0b1000_0000) == 0:
|
||||
rgba = colorTable[0]
|
||||
else:
|
||||
rgba = colorTable[1]
|
||||
colorBits = colorBits shl 1
|
||||
dec haveBits
|
||||
result[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:
|
||||
offset += 4 - padding
|
||||
for x in 0 ..< result.width:
|
||||
if offset >= data.len:
|
||||
raise newException(PixieError, "Truncated BMP data")
|
||||
var rgba: ColorRGBA
|
||||
if haveBits == 0:
|
||||
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]
|
||||
colorBits = colorBits shl 4
|
||||
haveBits -= 4
|
||||
result[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")
|
||||
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()
|
||||
|
||||
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 + 3 > data.len:
|
||||
raise newException(PixieError, "Truncated BMP data")
|
||||
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[x, result.height - y - 1] = rgba.rgbx()
|
||||
|
||||
|
||||
if bits == 32:
|
||||
|
||||
for y in 0 ..< result.height:
|
||||
for x in 0 ..< result.width:
|
||||
if offset >= data.len - 2:
|
||||
raise newException(PixieError, "Truncated BMP data")
|
||||
var rgba: ColorRGBA
|
||||
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)
|
||||
else:
|
||||
rgba.a = 255
|
||||
offset += 4
|
||||
result[x, result.height - y - 1] = rgba.rgbx()
|
||||
|
||||
if flipVertical:
|
||||
result.flipVertical()
|
||||
|
||||
proc decodeBmp*(data: seq[uint8]): Image {.inline, raises: [PixieError].} =
|
||||
## Decodes bitmap data into an Image.
|
||||
|
|
38
tests/fuzz_bmp.nim
Normal file
38
tests/fuzz_bmp.nim
Normal file
|
@ -0,0 +1,38 @@
|
|||
import pixie/common, pixie/fileformats/bmp, random, strformat, flatty/binny, os
|
||||
|
||||
randomize()
|
||||
|
||||
var originals = @[readFile("tests/fileformats/bmp/knight.32.bmp")]
|
||||
for file in walkFiles("tests/fileformats/bmp/bmpsuite/*"):
|
||||
originals.add(readFile(file))
|
||||
|
||||
for i in 0 ..< 100_000:
|
||||
var data = originals[rand(originals.len-1)]
|
||||
let
|
||||
pos = rand(data.len-1)
|
||||
value = rand(255).char
|
||||
# pos = 27355
|
||||
# value = '&'
|
||||
data[pos] = value
|
||||
|
||||
let
|
||||
width = data.readInt32(18).int
|
||||
height = data.readInt32(22).int
|
||||
numColors = data.readInt32(46).int
|
||||
if abs(width) > 1000 or abs(height) > 1000 or numColors > 1000:
|
||||
echo "too big"
|
||||
continue
|
||||
|
||||
echo &"{i} {pos} {repr(value)}"
|
||||
try:
|
||||
let img = decodeBmp(data)
|
||||
doAssert img.height > 0 and img.width > 0
|
||||
except PixieError:
|
||||
discard
|
||||
|
||||
data = data[0 ..< pos]
|
||||
try:
|
||||
let img = decodeBmp(data)
|
||||
doAssert img.height > 0 and img.width > 0
|
||||
except PixieError:
|
||||
discard
|
|
@ -1,4 +1,4 @@
|
|||
import chroma, pixie, pixie/fileformats/bmp
|
||||
import chroma, pixie, pixie/fileformats/bmp, os, strutils
|
||||
|
||||
# block:
|
||||
# var image = newImage(4, 2)
|
||||
|
@ -42,3 +42,8 @@ block:
|
|||
"tests/fileformats/bmp/rgb.24.master.bmp"
|
||||
))
|
||||
writeFile("tests/fileformats/bmp/rgb.24.bmp", encodeBmp(image))
|
||||
|
||||
block:
|
||||
for file in walkFiles("tests/fileformats/bmp/bmpsuite/*"):
|
||||
let image = decodeBmp(readFile(file))
|
||||
image.writeFile(file.replace("bmpsuite", "output") & ".png")
|
||||
|
|
Loading…
Reference in a new issue