From ccccd883bb7a74bd76db55dd8f7f84714b48a669 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Sat, 12 Feb 2022 18:59:13 -0600 Subject: [PATCH 1/4] rm seq[uint8] --- src/pixie.nim | 4 ++-- src/pixie/fileformats/bmp.nim | 4 ---- tests/benchmark_jpg.nim | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pixie.nim b/src/pixie.nim index 28e4094..cd0cc33 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -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)) diff --git a/src/pixie/fileformats/bmp.nim b/src/pixie/fileformats/bmp.nim index 6a69efe..4f0d245 100644 --- a/src/pixie/fileformats/bmp.nim +++ b/src/pixie/fileformats/bmp.nim @@ -196,10 +196,6 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = if flipVertical: result.flipVertical() -proc decodeBmp*(data: seq[uint8]): Image {.inline, raises: [PixieError].} = - ## Decodes bitmap data into an Image. - decodeBmp(cast[string](data)) - proc encodeBmp*(image: Image): string {.raises: [].} = ## Encodes an image into the BMP file format. diff --git a/tests/benchmark_jpg.nim b/tests/benchmark_jpg.nim index 4a68227..71d484a 100644 --- a/tests/benchmark_jpg.nim +++ b/tests/benchmark_jpg.nim @@ -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) From 97515a3849acf848308ff91c593cda2c6cb4577e Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Sat, 12 Feb 2022 19:03:34 -0600 Subject: [PATCH 2/4] invalid --- src/pixie/fileformats/bmp.nim | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/pixie/fileformats/bmp.nim b/src/pixie/fileformats/bmp.nim index 4f0d245..1a75845 100644 --- a/src/pixie/fileformats/bmp.nim +++ b/src/pixie/fileformats/bmp.nim @@ -5,6 +5,9 @@ import bitops, chroma, flatty/binny, pixie/common, pixie/images const bmpSignature* = "BM" +template failInvalid() = + raise newException(PixieError, "Invalid BMP buffer, unable to load") + proc colorMaskShift(color: uint32, mask: uint32): uint8 = ((color and mask) shr (mask.firstSetBit() - 1)).uint8 @@ -12,11 +15,11 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = ## Decodes bitmap data into an Image. if data.len < 48: - raise newException(PixieError, "Invalid BMP data") + failInvalid() # BMP Header if data[0 .. 1] != "BM": - raise newException(PixieError, "Invalid BMP data") + failInvalid() let bits = data.readUint16(28).int @@ -36,16 +39,16 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = flipVertical = false if numColors < 0 or numColors > 256: - raise newException(PixieError, "Invalid number of colors") + failInvalid() if dibHeader notin [40, 108]: - raise newException(PixieError, "Invalid BMP data") + failInvalid() var colorTable = newSeq[ColorRGBA](numColors) if dibHeader == 108: if data.len < 14 + dibHeader: - raise newException(PixieError, "Invalid BMP data") + failInvalid() redChannel = data.readUInt32(54) greenChannel = data.readUInt32(58) @@ -59,13 +62,13 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = if numColors > 0: if data.len < 14 + dibHeader + numColors * 4: - raise newException(PixieError, "Invalid BMP data") + failInvalid() var colorOffset = dibHeader + 14 for i in 0 ..< numColors: var rgba: ColorRGBA if colorOffset + 3 > data.len - 2: - raise newException(PixieError, "Truncated BMP data") + failInvalid() rgba.r = data.readUint8(colorOffset + 2) rgba.g = data.readUint8(colorOffset + 1) rgba.b = data.readUint8(colorOffset + 0) @@ -75,7 +78,7 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = if redChannel == 0 or greenChannel == 0 or blueChannel == 0 or alphaChannel == 0: - raise newException(PixieError, "Unsupported 0 channel mask.") + failInvalid() if bits notin [1, 4, 8, 32, 24]: raise newException(PixieError, "Unsupported BMP data format") @@ -104,7 +107,7 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = var rgba: ColorRGBA if haveBits == 0: if offset >= data.len: - raise newException(PixieError, "Truncated BMP data") + failInvalid() colorBits = data.readUint8(offset) haveBits = 8 offset += 1 @@ -130,13 +133,13 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = var rgba: ColorRGBA if haveBits == 0: if offset >= data.len: - raise newException(PixieError, "Truncated BMP data") + 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") + failInvalid() rgba = colorTable[index] colorBits = colorBits shl 4 haveBits -= 4 @@ -150,12 +153,12 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = offset += 4 - padding for x in 0 ..< result.width: if offset >= data.len: - raise newException(PixieError, "Truncated BMP data") + failInvalid() var rgba: ColorRGBA let index = data.readUint8(offset) offset += 1 if index.int >= numColors: - raise newException(PixieError, "Invalid BMP index") + failInvalid() rgba = colorTable[index] result[x, result.height - y - 1] = rgba.rgbx() @@ -167,7 +170,7 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = offset += 4 - padding for x in 0 ..< result.width: if offset + 2 >= data.len: - raise newException(PixieError, "Truncated BMP data") + failInvalid() var rgba: ColorRGBA rgba.r = data.readUint8(offset + 2) rgba.g = data.readUint8(offset + 1) @@ -180,7 +183,7 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = for y in 0 ..< result.height: for x in 0 ..< result.width: if offset + 3 >= data.len: - raise newException(PixieError, "Truncated BMP data") + failInvalid() var rgba: ColorRGBA let color = data.readUint32(offset) rgba.r = color.colorMaskShift(redChannel) From 17ad1fbbd184f01bc327ac504a7c65fdaa3f43f2 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Sat, 12 Feb 2022 19:07:19 -0600 Subject: [PATCH 3/4] inline --- src/pixie/fileformats/bmp.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pixie/fileformats/bmp.nim b/src/pixie/fileformats/bmp.nim index 1a75845..f80a49c 100644 --- a/src/pixie/fileformats/bmp.nim +++ b/src/pixie/fileformats/bmp.nim @@ -8,7 +8,7 @@ const bmpSignature* = "BM" template failInvalid() = raise newException(PixieError, "Invalid BMP buffer, unable to load") -proc colorMaskShift(color: uint32, mask: uint32): uint8 = +proc colorMaskShift(color, mask: uint32): uint8 {.inline.} = ((color and mask) shr (mask.firstSetBit() - 1)).uint8 proc decodeBmp*(data: string): Image {.raises: [PixieError].} = From 5282caa0562a6ceb8863072362ef61967263029e Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Sun, 13 Feb 2022 14:54:30 -0600 Subject: [PATCH 4/4] bmp dib decoder --- pixie.nimble | 2 +- src/pixie/fileformats/bmp.nim | 204 ++++++++++++++++------------ tests/fileformats/bmp/knight.32.bmp | Bin 890 -> 890 bytes 3 files changed, 117 insertions(+), 89 deletions(-) diff --git a/pixie.nimble b/pixie.nimble index 9d12f81..6cb2f7c 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -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" diff --git a/src/pixie/fileformats/bmp.nim b/src/pixie/fileformats/bmp.nim index f80a49c..9f7cc52 100644 --- a/src/pixie/fileformats/bmp.nim +++ b/src/pixie/fileformats/bmp.nim @@ -2,6 +2,9 @@ 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" @@ -11,94 +14,105 @@ template failInvalid() = 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: + if len < 40: failInvalid() - # BMP Header - if data[0 .. 1] != "BM": - failInvalid() + 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: - failInvalid() - if dibHeader notin [40, 108]: + if headerSize notin [40, 108, 124]: failInvalid() - var - 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: + if planes != 1: failInvalid() - 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: @@ -106,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: + 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: @@ -132,44 +145,42 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = for x in 0 ..< result.width: var rgba: ColorRGBA if haveBits == 0: - if offset >= data.len: + if offset >= len: failInvalid() colorBits = data.readUint8(offset) haveBits = 8 offset += 1 let index = (colorBits and 0b1111_0000) shr 4 - if index.int >= numColors: + if index.int >= colorPaletteSize: failInvalid() - rgba = colorTable[index] + 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: + if offset >= len: failInvalid() var rgba: ColorRGBA let index = data.readUint8(offset) offset += 1 - if index.int >= numColors: + if index.int >= colorPaletteSize: failInvalid() - rgba = colorTable[index] - result[x, result.height - y - 1] = rgba.rgbx() + 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: + if offset + 2 >= len: failInvalid() var rgba: ColorRGBA rgba.r = data.readUint8(offset + 2) @@ -177,28 +188,45 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = 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: + if offset + 3 >= len: failInvalid() - 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) + 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: 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: [].} = ## Encodes an image into the BMP file format. diff --git a/tests/fileformats/bmp/knight.32.bmp b/tests/fileformats/bmp/knight.32.bmp index a698d6f24fc9db1fc4e310d515b34acead857b3a..ebce0d18b205344051665f5613b9d690da4751a9 100644 GIT binary patch literal 890 zcmb7=v5EpQ6h%k2)I!)Fn9|P9!bY&)!bSzL7D2Yx4-jOl#n02y|A;wRjy#7nJ8*Mf za?g7qGygh$H03+x^^i5l8fNvHwjpY>KKp;lW%&|oSXTS*wgmrJQc8{5{a#W{=F#D1 zlVy=TaW6aH9A6AMd-+=C3^U!{=(0L&P3HJ{do7H$d-|vmnS0)G|5=waGHN+%c1R-NRGUgU8vvRgamh|3RkaF!YjfPh`lt XGh}dUdf*