|
@ -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"
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
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/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",
|
||||
]
|
||||
|
|