Merge pull request #424 from treeform/dev

Add JPEG exif orientation.
This commit is contained in:
treeform 2022-05-22 11:35:59 -07:00 committed by GitHub
commit 82263888a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 120 additions and 6 deletions

View file

@ -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"

View file

@ -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()

View file

@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

View file

@ -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",
]