diff --git a/src/pixie.nim b/src/pixie.nim index f4a1004..a3e4fe3 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -18,24 +18,36 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: []. ## Convert a straight alpha RGBA to a premultiplied alpha RGBA. c.rgbx() +proc decodeImageDimensions*( + data: pointer, len: int +): ImageDimensions {.raises: [PixieError].} = + ## Decodes an image's dimensions from memory. + if len > 8 and equalMem(data, pngSignature[0].unsafeAddr, 8): + decodePngDimensions(data, len) + elif len > 2 and equalMem(data, jpegStartOfImage[0].unsafeAddr, 2): + decodeJpegDimensions(data, len) + elif len > 2 and equalMem(data, bmpSignature.cstring, 2): + decodeBmpDimensions(data, len) + elif len > 6 and ( + equalMem(data, gifSignatures[0].cstring, 6) or + equalMem(data, gifSignatures[1].cstring, 6) + ): + decodeGifDimensions(data, len) + elif len > (14 + 8) and equalMem(data, qoiSignature.cstring, 4): + decodeQoiDimensions(data, len) + elif len > 9 and ( + equalMem(data, ppmSignatures[0].cstring, 2) or + equalMem(data, ppmSignatures[1].cstring, 2) + ): + decodePpmDimensions(data, len) + else: + raise newException(PixieError, "Unsupported image file format") + proc decodeImageDimensions*( data: string ): ImageDimensions {.raises: [PixieError].} = ## Decodes an image's dimensions from memory. - if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature): - decodePngDimensions(data) - elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage): - decodeJpegDimensions(data) - elif data.len > 2 and data.readStr(0, 2) == bmpSignature: - decodeBmpDimensions(data) - elif data.len > 6 and data.readStr(0, 6) in gifSignatures: - decodeGifDimensions(data) - elif data.len > (14+8) and data.readStr(0, 4) == qoiSignature: - decodeQoiDimensions(data) - elif data.len > 9 and data.readStr(0, 2) in ppmSignatures: - decodePpmDimensions(data) - else: - raise newException(PixieError, "Unsupported image file format") + decodeImageDimensions(data.cstring, data.len) proc decodeImage*(data: string): Image {.raises: [PixieError].} = ## Loads an image from memory. diff --git a/src/pixie/fileformats/bmp.nim b/src/pixie/fileformats/bmp.nim index ffd1fe1..65d7c05 100644 --- a/src/pixie/fileformats/bmp.nim +++ b/src/pixie/fileformats/bmp.nim @@ -228,19 +228,27 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = decodeDib(data[14].unsafeAddr, data.len - 14) proc decodeBmpDimensions*( - data: string + data: pointer, len: int ): ImageDimensions {.raises: [PixieError].} = ## Decodes the BMP dimensions. - if data.len < 26: + if len < 26: failInvalid() + let data = cast[ptr UncheckedArray[uint8]](data) + # BMP Header - if data[0 .. 1] != "BM": + if data[0].char != 'B' or data[1].char != 'M': # Must start with BM failInvalid() result.width = data.readInt32(18).int result.height = abs(data.readInt32(22)).int +proc decodeBmpDimensions*( + data: string +): ImageDimensions {.raises: [PixieError].} = + ## Decodes the BMP dimensions. + decodeBmpDimensions(data.cstring, data.len) + proc encodeDib*(image: Image): string {.raises: [].} = ## Encodes an image into a DIB. diff --git a/src/pixie/fileformats/gif.nim b/src/pixie/fileformats/gif.nim index 47ef20b..7adfa78 100644 --- a/src/pixie/fileformats/gif.nim +++ b/src/pixie/fileformats/gif.nim @@ -379,18 +379,29 @@ proc decodeGif*(data: string): Gif {.raises: [PixieError].} = result.duration += interval proc decodeGifDimensions*( - data: string + data: pointer, len: int ): ImageDimensions {.raises: [PixieError].} = ## Decodes the GIF dimensions. - if data.len < 10: + if len < 10: failInvalid() - if data[0 .. 5] notin gifSignatures: + let data = cast[ptr UncheckedArray[uint8]](data) + + let startsWithSignature = + equalMem(data, gifSignatures[0].cstring, 6) or + equalMem(data, gifSignatures[1].cstring, 6) + + if not startsWithSignature: raise newException(PixieError, "Invalid GIF file signature") result.width = data.readInt16(6).int result.height = data.readInt16(8).int +proc decodeGifDimensions*( + data: string +): ImageDimensions {.raises: [PixieError].} = + decodeGifDimensions(data.cstring, data.len) + proc newImage*(gif: Gif): Image {.raises: [].} = gif.frames[0].copy() diff --git a/src/pixie/fileformats/jpeg.nim b/src/pixie/fileformats/jpeg.nim index ed0b33c..47e91d1 100644 --- a/src/pixie/fileformats/jpeg.nim +++ b/src/pixie/fileformats/jpeg.nim @@ -24,7 +24,6 @@ import chroma, flatty/binny, ../common, ../images, ../internal, const fastBits = 9 - jpegStartOfImage* = [0xFF.uint8, 0xD8] deZigZag = [ uint8 00, 01, 08, 16, 09, 02, 03, 10, uint8 17, 24, 32, 25, 18, 11, 04, 05, @@ -40,6 +39,9 @@ const 1023, 2047, 4095, 8191, 16383, 32767, 65535 ] +let + jpegStartOfImage* = [0xFF.uint8, 0xD8] + type Huffman = object codes: array[256, uint16] @@ -380,15 +382,16 @@ proc decodeSOF2(state: var DecoderState) = proc decodeExif(state: var DecoderState) = ## Decode Exif header - let - len = state.readUint16be().int - 2 - endOffset = state.pos + len + var len = state.readUint16be().int - 2 let exifHeader = state.readStr(6) + + len -= 6 + if exifHeader != "Exif\0\0": # Happens with progressive images, just ignore instead of error. # Skip to the end. - state.pos = endOffset + state.skipBytes(len) return # Read the endianess of the exif header @@ -402,22 +405,40 @@ proc decodeExif(state: var DecoderState) = else: failInvalid("invalid Tiff header") + len -= 2 + # Verify we got the endianess right. if state.readUint16be().maybeSwap(littleEndian) != 0x002A.uint16: failInvalid("invalid Tiff header endianess") + len -= 2 + # Skip any other tiff header data. let offsetToFirstIFD = state.readUint32be().maybeSwap(littleEndian).int + + len -= 4 + + if offsetToFirstIFD < 8: + failInvalid("invalid Tiff offset") + state.skipBytes(offsetToFirstIFD - 8) + len -= (offsetToFirstIFD - 8) + # Read the IFD0 (main image) tags. let numTags = state.readUint16be().maybeSwap(littleEndian).int + + len -= 2 + for i in 0 ..< numTags: let tagNumber = state.readUint16be().maybeSwap(littleEndian) dataFormat = state.readUint16be().maybeSwap(littleEndian) numberComponents = state.readUint32be().maybeSwap(littleEndian) dataOffset = state.readUint32be().maybeSwap(littleEndian).int + + len -= 12 + # For now we only care about orientation tag. case tagNumber: of 0x0112: # Orientation @@ -426,7 +447,7 @@ proc decodeExif(state: var DecoderState) = discard # Skip all of the data we do not want to read, IFD1, thumbnail, etc. - state.pos = endOffset + state.skipBytes(len) # Skip any remaining len proc reset(state: var DecoderState) = ## Rests the decoder state need for restart markers. @@ -1136,13 +1157,13 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} = state.buildImage() proc decodeJpegDimensions*( - data: string + data: pointer, len: int ): ImageDimensions {.raises: [PixieError].} = ## Decodes the JPEG dimensions. var state = DecoderState() - state.buffer = cast[ptr UncheckedArray[uint8]](data.cstring) - state.len = data.len + state.buffer = cast[ptr UncheckedArray[uint8]](data) + state.len = len while true: if state.readUint8() != 0xFF: @@ -1153,21 +1174,24 @@ proc decodeJpegDimensions*( of 0xD8: # SOI - Start of Image discard - of 0xC0: - # Start Of Frame (Baseline DCT) - state.decodeSOF0() + of 0xC0, 0xC2: + # Start Of Frame (Baseline DCT or Progressive DCT) + discard state.readUint16be().int # Chunk len + discard state.readUint8() # Precision + state.imageHeight = state.readUint16be().int + state.imageWidth = state.readUint16be().int break of 0xC1: - # Start Of Frame (Extended sequential DCT) - state.decodeSOF1() - break - of 0xC2: - # Start Of Frame (Progressive DCT) - state.decodeSOF2() - break + failInvalid("unsupported extended sequential DCT format") + of 0xC4: + # Define Huffman Table + state.decodeDHT() of 0xDB: # Define Quantization Table(s) state.skipChunk() + of 0xDD: + # Define Restart Interval + state.skipChunk() of 0XE0: # Application-specific state.skipChunk() @@ -1193,5 +1217,11 @@ proc decodeJpegDimensions*( else: failInvalid("invalid orientation") +proc decodeJpegDimensions*( + data: string +): ImageDimensions {.raises: [PixieError].} = + ## Decodes the JPEG dimensions. + decodeJpegDimensions(data.cstring, data.len) + when defined(release): {.pop.} diff --git a/src/pixie/fileformats/png.nim b/src/pixie/fileformats/png.nim index 228351b..b24aed4 100644 --- a/src/pixie/fileformats/png.nim +++ b/src/pixie/fileformats/png.nim @@ -3,7 +3,7 @@ import chroma, flatty/binny, math, ../common, ../images, ../internal, # See http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html -const +let pngSignature* = [137.uint8, 80, 78, 71, 13, 10, 26, 10] type @@ -76,14 +76,6 @@ proc decodeHeader(data: pointer): PngHeader = if result.interlaceMethod notin [0.uint8, 1]: raise newException(PixieError, "Invalid PNG interlace method") - # Not yet supported: - - if result.bitDepth == 16: - raise newException(PixieError, "PNG 16 bit depth not supported yet") - - if result.interlaceMethod != 0: - raise newException(PixieError, "Interlaced PNG not supported yet") - proc decodePalette(data: pointer, len: int): seq[ColorRGB] = if len == 0 or len mod 3 != 0: failInvalid() @@ -451,6 +443,12 @@ proc decodePng*(data: pointer, len: int): Png {.raises: [PixieError].} = failCRC() inc(pos, 4) # CRC + # Not yet supported: + if header.bitDepth == 16: + raise newException(PixieError, "PNG 16 bit depth not supported yet") + if header.interlaceMethod != 0: + raise newException(PixieError, "Interlaced PNG not supported yet") + while true: if pos + 8 > len: failInvalid() diff --git a/src/pixie/fileformats/ppm.nim b/src/pixie/fileformats/ppm.nim index 401c218..4bba3d4 100644 --- a/src/pixie/fileformats/ppm.nim +++ b/src/pixie/fileformats/ppm.nim @@ -12,16 +12,17 @@ type template failInvalid() = raise newException(PixieError, "Invalid PPM data") -proc decodeHeader(data: string): PpmHeader {.raises: [PixieError].} = - if data.len <= 10: # Each part + whitespace - raise newException(PixieError, "Invalid PPM file header") - +proc decodeHeader( + data: ptr UncheckedArray[uint8], len: int +): PpmHeader {.raises: [PixieError].} = var commentMode, readWhitespace: bool i, readFields: int field: string while readFields < 4: - let c = readUint8(data, i).char + if i >= len: + raise newException(PixieError, "Invalid PPM file header") + let c = data[i].char if c == '#': commentMode = true elif c == '\n': @@ -120,7 +121,10 @@ proc decodeP3Data(data: string, maxVal: int): seq[ColorRGBX] {.raises: [PixieErr proc decodePpm*(data: string): Image {.raises: [PixieError].} = ## Decodes Portable Pixel Map data into an Image. - let header = decodeHeader(data) + let header = decodeHeader( + cast[ptr UncheckedArray[uint8]](data.cstring), + data.len + ) if not (header.version in ppmSignatures): failInvalid() @@ -135,13 +139,21 @@ proc decodePpm*(data: string): Image {.raises: [PixieError].} = else: decodeP6Data(data[header.dataOffset .. ^1], header.maxVal) +proc decodePpmDimensions*( + data: pointer, len: int +): ImageDimensions {.raises: [PixieError].} = + ## Decodes the PPM dimensions. + let + data = cast[ptr UncheckedArray[uint8]](data) + header = decodeHeader(data, len) + result.width = header.width + result.height = header.height + proc decodePpmDimensions*( data: string ): ImageDimensions {.raises: [PixieError].} = ## Decodes the PPM dimensions. - let header = decodeHeader(data) - result.width = header.width - result.height = header.height + decodePpmDimensions(data.cstring, data.len) proc encodePpm*(image: Image): string {.raises: [].} = ## Encodes an image into the PPM file format (version P6). diff --git a/src/pixie/fileformats/qoi.nim b/src/pixie/fileformats/qoi.nim index 6e97083..c7d65d9 100644 --- a/src/pixie/fileformats/qoi.nim +++ b/src/pixie/fileformats/qoi.nim @@ -136,15 +136,23 @@ proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} = inc(p) proc decodeQoiDimensions*( - data: string + data: pointer, len: int ): ImageDimensions {.raises: [PixieError].} = ## Decodes the QOI dimensions. - if data.len <= 12 or data[0 .. 3] != qoiSignature: + if len <= 12 or not equalMem(data, qoiSignature.cstring, 4): raise newException(PixieError, "Invalid QOI header") + let data = cast[ptr UncheckedArray[uint8]](data) + result.width = data.readUint32(4).swap().int result.height = data.readUint32(8).swap().int +proc decodeQoiDimensions*( + data: string +): ImageDimensions {.raises: [PixieError].} = + ## Decodes the QOI dimensions. + decodeQoiDimensions(data.cstring, data.len) + proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} = ## Encodes raw QOI pixels to the QOI file format.