Read more bmp formats.
This commit is contained in:
parent
ffc1b90c5f
commit
4dc50d9778
|
@ -1,6 +1,9 @@
|
||||||
import bitops, chroma, flatty/binny, pixie/common, pixie/images
|
import bitops, chroma, flatty/binny, pixie/common, pixie/images
|
||||||
|
|
||||||
|
import print, strutils
|
||||||
|
|
||||||
# See: https://en.wikipedia.org/wiki/BMP_file_format
|
# See: https://en.wikipedia.org/wiki/BMP_file_format
|
||||||
|
# See: https://bmptestsuite.sourceforge.io/
|
||||||
|
|
||||||
const bmpSignature* = "BM"
|
const bmpSignature* = "BM"
|
||||||
|
|
||||||
|
@ -10,64 +13,192 @@ proc colorMaskShift(color: uint32, mask: uint32): uint8 =
|
||||||
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
|
||||||
## Decodes bitmap data into an Image.
|
## Decodes bitmap data into an Image.
|
||||||
|
|
||||||
|
if data.len < 48:
|
||||||
|
raise newException(PixieError, "Invalid BMP data")
|
||||||
|
|
||||||
# BMP Header
|
# BMP Header
|
||||||
if data[0 .. 1] != "BM":
|
if data[0 .. 1] != "BM":
|
||||||
raise newException(PixieError, "Invalid BMP data")
|
raise newException(PixieError, "Invalid BMP data")
|
||||||
|
|
||||||
let
|
let
|
||||||
width = data.readInt32(18).int
|
|
||||||
height = data.readInt32(22).int
|
|
||||||
bits = data.readUint16(28).int
|
bits = data.readUint16(28).int
|
||||||
compression = data.readUint32(30).int
|
compression = data.readUint32(30).int
|
||||||
dibHeader = data.readInt32(14).int
|
dibHeader = data.readInt32(14).int
|
||||||
|
|
||||||
var
|
var
|
||||||
|
numColors = data.readInt32(46).int
|
||||||
|
width = data.readInt32(18).int
|
||||||
|
height = data.readInt32(22).int
|
||||||
offset = data.readUInt32(10).int
|
offset = data.readUInt32(10).int
|
||||||
# Default channels if header does not contain them:
|
# Default channels if header does not contain them:
|
||||||
redChannel = 0x00FF0000.uint32
|
redChannel = 0x00FF0000.uint32
|
||||||
greenChannel = 0x0000FF00.uint32
|
greenChannel = 0x0000FF00.uint32
|
||||||
blueChannel = 0x000000FF.uint32
|
blueChannel = 0x000000FF.uint32
|
||||||
alphaChannel = 0xFF000000.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 dibHeader == 108:
|
||||||
|
if data.len < 14 + dibHeader:
|
||||||
|
raise newException(PixieError, "Invalid BMP data")
|
||||||
|
|
||||||
redChannel = data.readUInt32(54)
|
redChannel = data.readUInt32(54)
|
||||||
greenChannel = data.readUInt32(58)
|
greenChannel = data.readUInt32(58)
|
||||||
blueChannel = data.readUInt32(62)
|
blueChannel = data.readUInt32(62)
|
||||||
alphaChannel = data.readUInt32(66)
|
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
|
if redChannel == 0 or greenChannel == 0 or
|
||||||
blueChannel == 0 or alphaChannel == 0:
|
blueChannel == 0 or alphaChannel == 0:
|
||||||
raise newException(PixieError, "Unsupported 0 channel mask.")
|
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")
|
raise newException(PixieError, "Unsupported BMP data format")
|
||||||
|
|
||||||
if compression notin [0, 3]:
|
if compression notin [0, 3]:
|
||||||
raise newException(PixieError, "Unsupported BMP data format")
|
raise newException(PixieError, "Unsupported BMP data format")
|
||||||
|
|
||||||
let channels = if bits == 32: 4 else: 3
|
if height < 0:
|
||||||
if width * height * channels + offset > data.len:
|
height = -height
|
||||||
raise newException(PixieError, "Invalid BMP data size")
|
flipVertical = true
|
||||||
|
|
||||||
result = newImage(width, height)
|
result = newImage(width, height)
|
||||||
|
let startOffset = offset
|
||||||
|
|
||||||
for y in 0 ..< result.height:
|
if bits == 1:
|
||||||
for x in 0 ..< result.width:
|
var
|
||||||
var rgba: ColorRGBA
|
haveBits = 0
|
||||||
if bits == 32:
|
colorBits: uint8 = 0
|
||||||
let color = data.readUint32(offset)
|
for y in 0 ..< result.height:
|
||||||
rgba.r = color.colorMaskShift(redChannel)
|
# pad the row
|
||||||
rgba.g = color.colorMaskShift(greenChannel)
|
haveBits = 0
|
||||||
rgba.b = color.colorMaskShift(blueChannel)
|
let padding = (offset - startOffset) mod 4
|
||||||
rgba.a = color.colorMaskShift(alphaChannel)
|
if padding > 0:
|
||||||
offset += 4
|
offset += 4 - padding
|
||||||
elif bits == 24:
|
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.r = data.readUint8(offset + 2)
|
||||||
rgba.g = data.readUint8(offset + 1)
|
rgba.g = data.readUint8(offset + 1)
|
||||||
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[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].} =
|
proc decodeBmp*(data: seq[uint8]): Image {.inline, raises: [PixieError].} =
|
||||||
## Decodes bitmap data into an Image.
|
## 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:
|
# block:
|
||||||
# var image = newImage(4, 2)
|
# var image = newImage(4, 2)
|
||||||
|
@ -42,3 +42,8 @@ block:
|
||||||
"tests/fileformats/bmp/rgb.24.master.bmp"
|
"tests/fileformats/bmp/rgb.24.master.bmp"
|
||||||
))
|
))
|
||||||
writeFile("tests/fileformats/bmp/rgb.24.bmp", encodeBmp(image))
|
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