Add JPEG exif orientation.
|
@ -9,7 +9,7 @@ requires "nim >= 1.4.8"
|
||||||
requires "vmath >= 1.1.4"
|
requires "vmath >= 1.1.4"
|
||||||
requires "chroma >= 0.2.5"
|
requires "chroma >= 0.2.5"
|
||||||
requires "zippy >= 0.9.7"
|
requires "zippy >= 0.9.7"
|
||||||
requires "flatty >= 0.3.0"
|
requires "flatty >= 0.3.2"
|
||||||
requires "nimsimd >= 1.0.0"
|
requires "nimsimd >= 1.0.0"
|
||||||
requires "bumpy >= 1.1.1"
|
requires "bumpy >= 1.1.1"
|
||||||
|
|
||||||
|
|
|
@ -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.
|
# This JPEG decoder is loosely based on stb_image which is public domain.
|
||||||
|
|
||||||
|
@ -81,6 +82,7 @@ type
|
||||||
restartInterval: int
|
restartInterval: int
|
||||||
todoBeforeRestart: int
|
todoBeforeRestart: int
|
||||||
eobRun: int
|
eobRun: int
|
||||||
|
orientation: int
|
||||||
|
|
||||||
when defined(release):
|
when defined(release):
|
||||||
{.push checks: off.}
|
{.push checks: off.}
|
||||||
|
@ -108,6 +110,14 @@ proc readUint16be(state: var DecoderState): uint16 =
|
||||||
## Reads uint16 big-endian from the input stream.
|
## Reads uint16 big-endian from the input stream.
|
||||||
(state.readUint8().uint16 shl 8) or state.readUint8()
|
(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 skipBytes(state: var DecoderState, n: int) =
|
proc skipBytes(state: var DecoderState, n: int) =
|
||||||
## Skips a number of bytes.
|
## Skips a number of bytes.
|
||||||
if state.pos + n > state.buffer.len:
|
if state.pos + n > state.buffer.len:
|
||||||
|
@ -312,6 +322,57 @@ proc decodeSOF2(state: var DecoderState) =
|
||||||
state.decodeSOF0()
|
state.decodeSOF0()
|
||||||
state.progressive = true
|
state.progressive = true
|
||||||
|
|
||||||
|
proc decodeExif(state: var DecoderState) =
|
||||||
|
## Decode Exif header
|
||||||
|
let
|
||||||
|
len = state.readUint16be().int - 2
|
||||||
|
endOffset = state.pos + len
|
||||||
|
|
||||||
|
let exifHeader = state.buffer[state.pos ..< state.pos + 6]
|
||||||
|
state.skipBytes(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) =
|
proc reset(state: var DecoderState) =
|
||||||
## Rests the decoder state need for restart markers.
|
## Rests the decoder state need for restart markers.
|
||||||
state.bitBuffer = 0
|
state.bitBuffer = 0
|
||||||
|
@ -913,6 +974,32 @@ proc buildImage(state: var DecoderState): Image =
|
||||||
else:
|
else:
|
||||||
failInvalid()
|
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()
|
||||||
|
of 6:
|
||||||
|
result.rotate90()
|
||||||
|
result.flipHorizontal()
|
||||||
|
of 7:
|
||||||
|
result.rotate90()
|
||||||
|
result.flipVertical()
|
||||||
|
result.flipHorizontal()
|
||||||
|
of 8:
|
||||||
|
result.rotate90()
|
||||||
|
result.flipVertical()
|
||||||
|
else:
|
||||||
|
failInvalid("invalid orientation")
|
||||||
|
|
||||||
proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
|
proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
|
||||||
## Decodes the JPEG into an Image.
|
## Decodes the JPEG into an Image.
|
||||||
|
|
||||||
|
@ -962,9 +1049,8 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
|
||||||
# state.decodeAPP0(data, at)
|
# state.decodeAPP0(data, at)
|
||||||
state.skipChunk()
|
state.skipChunk()
|
||||||
of 0xE1:
|
of 0xE1:
|
||||||
# Exif
|
# Exif/APP1
|
||||||
# state.decodeExif(data, at)
|
state.decodeExif()
|
||||||
state.skipChunk()
|
|
||||||
of 0xE2..0xEF:
|
of 0xE2..0xEF:
|
||||||
# Application-specific
|
# Application-specific
|
||||||
state.skipChunk()
|
state.skipChunk()
|
||||||
|
|
|
@ -170,6 +170,16 @@ proc flipVertical*(image: Image) {.raises: [].} =
|
||||||
image.data[image.dataIndex(x, image.height - y - 1)]
|
image.data[image.dataIndex(x, image.height - y - 1)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
proc rotate90*(image: Image) {.raises: [PixieError].} =
|
||||||
|
## Rotates the image 90 degrees.
|
||||||
|
var 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, x)]
|
||||||
|
image.width = copy.width
|
||||||
|
image.height = copy.height
|
||||||
|
image.data = copy.data
|
||||||
|
|
||||||
proc subImage*(image: Image, x, y, w, h: int): Image {.raises: [PixieError].} =
|
proc subImage*(image: Image, x, y, w, h: int): Image {.raises: [PixieError].} =
|
||||||
## Gets a sub image from this image.
|
## Gets a sub image from this image.
|
||||||
if x < 0 or x + w > image.width:
|
if x < 0 or x + w > image.width:
|
||||||
|
|
BIN
tests/fileformats/jpeg/masters/f1-exif.jpg
Normal file
After Width: | Height: | Size: 992 B |
BIN
tests/fileformats/jpeg/masters/f2-exif.jpg
Normal file
After Width: | Height: | Size: 994 B |
BIN
tests/fileformats/jpeg/masters/f3-exif.jpg
Normal file
After Width: | Height: | Size: 992 B |
BIN
tests/fileformats/jpeg/masters/f4-exif.jpg
Normal file
After Width: | Height: | Size: 994 B |
BIN
tests/fileformats/jpeg/masters/f5-exif.jpg
Normal file
After Width: | Height: | Size: 980 B |
BIN
tests/fileformats/jpeg/masters/f6-exif.jpg
Normal file
After Width: | Height: | Size: 982 B |
BIN
tests/fileformats/jpeg/masters/f7-exif.jpg
Normal file
After Width: | Height: | Size: 980 B |
BIN
tests/fileformats/jpeg/masters/f8-exif.jpg
Normal file
After Width: | Height: | Size: 982 B |
|
@ -36,4 +36,13 @@ const jpegSuiteFiles* = [
|
||||||
"tests/fileformats/jpeg/masters/testimgp.jpg",
|
"tests/fileformats/jpeg/masters/testimgp.jpg",
|
||||||
"tests/fileformats/jpeg/masters/testorig.jpg",
|
"tests/fileformats/jpeg/masters/testorig.jpg",
|
||||||
"tests/fileformats/jpeg/masters/testprog.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",
|
||||||
]
|
]
|
||||||
|
|