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.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..9f7cc52 100644 --- a/src/pixie/fileformats/bmp.nim +++ b/src/pixie/fileformats/bmp.nim @@ -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. 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) diff --git a/tests/fileformats/bmp/knight.32.bmp b/tests/fileformats/bmp/knight.32.bmp index a698d6f..ebce0d1 100644 Binary files a/tests/fileformats/bmp/knight.32.bmp and b/tests/fileformats/bmp/knight.32.bmp differ