diff --git a/pixie.nimble b/pixie.nimble index 3a9c108..dc17c54 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -9,7 +9,7 @@ requires "nim >= 1.4.8" requires "vmath >= 1.1.4" requires "chroma >= 0.2.5" requires "zippy >= 0.9.7" -requires "flatty >= 0.3.0" +requires "flatty >= 0.3.3" requires "nimsimd >= 1.0.0" requires "bumpy >= 1.1.1" diff --git a/src/pixie/fileformats/jpeg.nim b/src/pixie/fileformats/jpeg.nim index a4d684a..d5c8e65 100644 --- a/src/pixie/fileformats/jpeg.nim +++ b/src/pixie/fileformats/jpeg.nim @@ -1,4 +1,5 @@ -import pixie/common, pixie/images, pixie/masks, sequtils, strutils, chroma, std/decls +import pixie/common, pixie/images, pixie/masks, sequtils, strutils, chroma, + std/decls, flatty/binny # This JPEG decoder is loosely based on stb_image which is public domain. @@ -8,6 +9,7 @@ import pixie/common, pixie/images, pixie/masks, sequtils, strutils, chroma, std/ # * 4:4:4, 4:2:2, 4:1:1, 4:2:0 resampling modes # * progressive # * restart markers +# * Exif orientation # * https://github.com/daviddrysdale/libjpeg # * https://www.youtube.com/watch?v=Kv1Hiv3ox8I @@ -81,6 +83,7 @@ type restartInterval: int todoBeforeRestart: int eobRun: int + orientation: int when defined(release): {.push checks: off.} @@ -108,6 +111,21 @@ proc readUint16be(state: var DecoderState): uint16 = ## Reads uint16 big-endian from the input stream. (state.readUint8().uint16 shl 8) or state.readUint8() +proc readUint32be(state: var DecoderState): uint32 = + ## Reads uint32 big-endian from the input stream. + return + (state.readUint8().uint32 shl 24) or + (state.readUint8().uint32 shl 16) or + (state.readUint8().uint32 shl 8) or + state.readUint8().uint32 + +proc readStr(state: var DecoderState, n: int): string = + ## Reads n number of bytes as a string. + if state.pos + n > state.buffer.len: + failInvalid() + result = state.buffer[state.pos ..< state.pos + n] + state.pos += n + proc skipBytes(state: var DecoderState, n: int) = ## Skips a number of bytes. if state.pos + n > state.buffer.len: @@ -312,6 +330,56 @@ proc decodeSOF2(state: var DecoderState) = state.decodeSOF0() state.progressive = true +proc decodeExif(state: var DecoderState) = + ## Decode Exif header + let + len = state.readUint16be().int - 2 + endOffset = state.pos + len + + let exifHeader = state.readStr(6) + if exifHeader != "Exif\0\0": + # Happens with progressive images, just ignore instead of error. + # Skip to the end. + state.pos = endOffset + return + + # Read the endianess of the exif header + let + tiffHeader = state.readUint16be().int + littleEndian = + if tiffHeader == 0x4D4D: + false + elif tiffHeader == 0x4949: + true + else: + failInvalid("invalid Tiff header") + + # Verify we got the endianess right. + if state.readUint16be().maybeSwap(littleEndian) != 0x002A.uint16: + failInvalid("invalid Tiff header endianess") + + # Skip any other tiff header data. + let offsetToFirstIFD = state.readUint32be().maybeSwap(littleEndian).int + state.skipBytes(offsetToFirstIFD - 8) + + # Read the IFD0 (main image) tags. + let numTags = state.readUint16be().maybeSwap(littleEndian).int + 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 + # For now we only care about orientation tag. + case tagNumber: + of 0x0112: # Orientation + state.orientation = dataOffset shr 16 + else: + discard + + # Skip all of the data we do not want to read, IFD1, thumbnail, etc. + state.pos = endOffset + proc reset(state: var DecoderState) = ## Rests the decoder state need for restart markers. state.bitBuffer = 0 @@ -457,7 +525,8 @@ proc getBit(state: var DecoderState): int = proc getBitsAsSignedInt(state: var DecoderState, n: int): int = ## Get n number of bits as a signed integer. - if n notin 0 .. 16: + # TODO: Investigate why 15 not 16? + if n notin 0 .. 15: failInvalid() if state.bitsBuffered < n: state.fillBitBuffer() @@ -913,6 +982,32 @@ proc buildImage(state: var DecoderState): Image = else: failInvalid() + # Do any of the orientation flips from the Exif header. + case state.orientation: + of 0, 1: + discard + of 2: + result.flipHorizontal() + of 3: + result.flipVertical() + result.flipHorizontal() + of 4: + result.flipVertical() + of 5: + result.rotate90() + result.flipHorizontal() + of 6: + result.rotate90() + of 7: + result.rotate90() + result.flipVertical() + of 8: + result.rotate90() + result.flipVertical() + result.flipHorizontal() + else: + failInvalid("invalid orientation") + proc decodeJpeg*(data: string): Image {.raises: [PixieError].} = ## Decodes the JPEG into an Image. @@ -962,9 +1057,8 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} = # state.decodeAPP0(data, at) state.skipChunk() of 0xE1: - # Exif - # state.decodeExif(data, at) - state.skipChunk() + # Exif/APP1 + state.decodeExif() of 0xE2..0xEF: # Application-specific state.skipChunk() diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 0368570..a5fb534 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -170,6 +170,17 @@ proc flipVertical*(image: Image) {.raises: [].} = image.data[image.dataIndex(x, image.height - y - 1)] ) +proc rotate90*(image: Image) {.raises: [PixieError].} = + ## Rotates the image 90 degrees clockwise. + let copy = newImage(image.height, image.width) + for y in 0 ..< copy.height: + for x in 0 ..< copy.width: + copy.data[copy.dataIndex(x, y)] = + image.data[image.dataIndex(y, image.height - x - 1)] + image.width = copy.width + image.height = copy.height + image.data = copy.data + proc subImage*(image: Image, x, y, w, h: int): Image {.raises: [PixieError].} = ## Gets a sub image from this image. if x < 0 or x + w > image.width: diff --git a/tests/fileformats/jpeg/masters/f1-exif.jpg b/tests/fileformats/jpeg/masters/f1-exif.jpg new file mode 100644 index 0000000..ff003e3 Binary files /dev/null and b/tests/fileformats/jpeg/masters/f1-exif.jpg differ diff --git a/tests/fileformats/jpeg/masters/f2-exif.jpg b/tests/fileformats/jpeg/masters/f2-exif.jpg new file mode 100644 index 0000000..7e0f170 Binary files /dev/null and b/tests/fileformats/jpeg/masters/f2-exif.jpg differ diff --git a/tests/fileformats/jpeg/masters/f3-exif.jpg b/tests/fileformats/jpeg/masters/f3-exif.jpg new file mode 100644 index 0000000..3ed7b16 Binary files /dev/null and b/tests/fileformats/jpeg/masters/f3-exif.jpg differ diff --git a/tests/fileformats/jpeg/masters/f4-exif.jpg b/tests/fileformats/jpeg/masters/f4-exif.jpg new file mode 100644 index 0000000..0e081f9 Binary files /dev/null and b/tests/fileformats/jpeg/masters/f4-exif.jpg differ diff --git a/tests/fileformats/jpeg/masters/f5-exif.jpg b/tests/fileformats/jpeg/masters/f5-exif.jpg new file mode 100644 index 0000000..e8d8754 Binary files /dev/null and b/tests/fileformats/jpeg/masters/f5-exif.jpg differ diff --git a/tests/fileformats/jpeg/masters/f6-exif.jpg b/tests/fileformats/jpeg/masters/f6-exif.jpg new file mode 100644 index 0000000..4e2c864 Binary files /dev/null and b/tests/fileformats/jpeg/masters/f6-exif.jpg differ diff --git a/tests/fileformats/jpeg/masters/f7-exif.jpg b/tests/fileformats/jpeg/masters/f7-exif.jpg new file mode 100644 index 0000000..b5dddea Binary files /dev/null and b/tests/fileformats/jpeg/masters/f7-exif.jpg differ diff --git a/tests/fileformats/jpeg/masters/f8-exif.jpg b/tests/fileformats/jpeg/masters/f8-exif.jpg new file mode 100644 index 0000000..fb050fc Binary files /dev/null and b/tests/fileformats/jpeg/masters/f8-exif.jpg differ diff --git a/tests/jpegsuite.nim b/tests/jpegsuite.nim index 3bb5650..c073954 100644 --- a/tests/jpegsuite.nim +++ b/tests/jpegsuite.nim @@ -36,4 +36,13 @@ const jpegSuiteFiles* = [ "tests/fileformats/jpeg/masters/testimgp.jpg", "tests/fileformats/jpeg/masters/testorig.jpg", "tests/fileformats/jpeg/masters/testprog.jpg", + + "tests/fileformats/jpeg/masters/f1-exif.jpg", + "tests/fileformats/jpeg/masters/f2-exif.jpg", + "tests/fileformats/jpeg/masters/f3-exif.jpg", + "tests/fileformats/jpeg/masters/f4-exif.jpg", + "tests/fileformats/jpeg/masters/f5-exif.jpg", + "tests/fileformats/jpeg/masters/f6-exif.jpg", + "tests/fileformats/jpeg/masters/f7-exif.jpg", + "tests/fileformats/jpeg/masters/f8-exif.jpg", ]