jpg checkpoint

This commit is contained in:
Ryan Oldenburg 2020-11-21 17:45:02 -06:00
parent cd612f9a6c
commit dbf6a481c4
5 changed files with 182 additions and 22 deletions

View file

@ -1,13 +1,14 @@
## Public interface to you library.
import pixie/images, pixie/masks, pixie/paths, pixie/common, pixie/blends,
pixie/fileformats/bmp, pixie/fileformats/png, flatty/binny, os
pixie/fileformats/bmp, pixie/fileformats/png, pixie/fileformats/jpg,
flatty/binny, os
export images, masks, paths, PixieError, blends
type
FileFormat* = enum
ffPng, ffBmp
ffPng, ffBmp, ffJpg
proc toMask*(image: Image): Mask =
## Converts an Image to a Mask.
@ -23,23 +24,26 @@ proc toImage*(mask: Mask): Image =
proc decodeImage*(data: string | seq[uint8]): Image =
## Loads an image from a memory.
if data.len > 8 and cast[array[8, uint8]](data.readUint64(0)) == pngSignature:
return decodePng(data)
if data.len > 2 and data.readStr(0, 2) == "BM":
return decodeBmp(data)
raise newException(PixieError, "Unsupported image file format")
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePng(data)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpgStartOfImage):
decodeJpg(data)
elif data.len > 2 and data.readStr(0, 2) == "BM":
decodeBmp(data)
else:
raise newException(PixieError, "Unsupported image file format")
proc readImage*(filePath: string): Image =
## Loads an image from a file.
decodeImage(readFile(filePath))
proc encodeImage*(image: Image, fileFormat: FileFormat): string =
## Encodes an image into a memory.
## Encodes an image into memory.
case fileFormat:
of ffPng:
image.encodePng()
of ffJpg:
image.encodeJpg()
of ffBmp:
image.encodeBmp()
@ -52,5 +56,7 @@ proc writeFile*(image: Image, filePath: string) =
let fileFormat = case splitFile(filePath).ext:
of "png": ffPng
of "bmp": ffBmp
else: ffPng
writeFile(filePath, image.encodeImage(fileFormat))
of "jpg": ffJpg
else:
raise newException(PixieError, "Unrecognized file extension")
image.writeFile(filePath, fileformat)

View file

@ -0,0 +1,152 @@
import flatty/binny, pixie/common, pixie/images
# See http://www.vip.sugovica.hu/Sardi/kepnezo/JPEG%20File%20Layout%20and%20Format.htm
const
jpgStartOfImage* = [0xFF.uint8, 0xD8]
template failInvalid() =
raise newException(PixieError, "Invalid JPG buffer, unable to load")
proc readSegmentLen(data: seq[uint8], pos: int): int =
if pos + 2 > data.len:
failInvalid()
let segmentLen = data.readUint16(pos).swap().int
if pos + segmentLen > data.len:
failInvalid()
segmentLen
proc skipSegment(data: seq[uint8], pos: var int) {.inline.} =
pos += readSegmentLen(data, pos)
proc decodeSOF(data: seq[uint8], pos: var int) =
let segmentLen = readSegmentLen(data, pos)
pos += 2
if pos + 6 > data.len:
failInvalid()
let
precision = data[pos].int
height = data.readUint16(pos + 1).swap().int
width = data.readUint16(pos + 3).swap().int
components = data[pos + 5].int
pos += 6
if precision != 8:
raise newException(PixieError, "Unsupported JPG bit depth")
if components != 3:
raise newException(PixieError, "Unsupported JPG channel count")
debugEcho width, " x ", height
if 8 + components * 3 != segmentLen:
failInvalid()
for i in 0 ..< 3:
discard
pos += components * 3
proc decodeDHT(data: seq[uint8], pos: var int) =
skipSegment(data, pos)
proc decodeDQT(data: seq[uint8], pos: var int) =
skipSegment(data, pos)
proc decodeDRI(data: seq[uint8], pos: var int) =
skipSegment(data, pos)
proc decodeSOS(data: seq[uint8], pos: var int) =
let segmentLen = readSegmentLen(data, pos)
pos += 2
if segmentLen != 12:
failInvalid()
let components = data[pos]
if components != 3:
raise newException(PixieError, "Unsupported JPG channel count")
for i in 0 ..< 3:
discard
pos += 10
pos += 3 # Skip 3 more bytes
while true:
if pos >= data.len:
failInvalid()
if data[pos] == 0xFF:
inc pos
if pos == data.len:
failInvalid()
if data[pos] == 0xD9: # End of Image:
inc pos
break
elif data[pos] == 0x00:
discard # Skip this byte
else:
failInvalid()
inc pos
proc decodeJpg*(data: seq[uint8]): Image =
## Decodes the JPEG into an Image.
if data.len < 4:
failInvalid()
if data.readUint16(0) != cast[uint16](jpgStartOfImage):
failInvalid()
var pos: int
while true:
if pos + 2 > data.len:
failInvalid()
let marker = [data[pos], data[pos + 1]]
pos += 2
if marker[0] != 0xFF:
failInvalid()
case marker[1]:
of 0xD8: # Start of Image
discard
of 0xC0: # Start of Frame
decodeSOF(data, pos)
of 0xC2: # Start of Frame
raise newException(PixieError, "Progressive JPG not supported")
of 0xC4: # Define Huffman Tables
decodeDHT(data, pos)
of 0xDB: # Define Quantanization Table(s)
decodeDQT(data, pos)
of 0xDD: # Define Restart Interval
decodeDRI(data, pos)
of 0xDA: # Start of Scan
decodeSOS(data, pos)
break
of 0xFE: # Comment
skipSegment(data, pos)
of 0xD9: # End of Image
failInvalid() # Not expected here
else:
if (marker[1] and 0xF0) == 0xE0:
# Skip APPn segments
skipSegment(data, pos)
else:
raise newException(PixieError, "Unsupported JPG segemnt")
raise newException(PixieError, "Decoding JPG not supported yet")
proc decodeJpg*(data: string): Image {.inline.} =
decodeJpg(cast[seq[uint8]](data))
proc encodeJpg*(image: Image): string =
raise newException(PixieError, "Encoding JPG not supported yet")

View file

@ -22,7 +22,7 @@ template failInvalid() =
when defined(release):
{.push checks: off.}
proc parseHeader(data: seq[uint8]): PngHeader =
proc decodeHeader(data: seq[uint8]): PngHeader =
result.width = data.readUint32(0).swap().int
result.height = data.readUint32(4).swap().int
result.bitDepth = data[8]
@ -78,7 +78,7 @@ proc parseHeader(data: seq[uint8]): PngHeader =
if result.interlaceMethod != 0:
raise newException(PixieError, "Interlaced PNG not yet supported")
proc parsePalette(data: seq[uint8]): seq[array[3, uint8]] =
proc decodePalette(data: seq[uint8]): seq[array[3, uint8]] =
if data.len == 0 or data.len mod 3 != 0:
failInvalid()
@ -145,7 +145,7 @@ proc unfilter(
result[unfiteredIdx(x, y)] = value
proc parseImageData(
proc decodeImageData(
header: PngHeader,
palette: seq[array[3, uint8]],
transparency, data: seq[uint8]
@ -297,10 +297,7 @@ proc parseImageData(
discard # Not possible, parseHeader validates
proc decodePng*(data: seq[uint8]): Image =
## Decodes the PNG from the parameter buffer. Check png.channels and
## png.bitDepth to see how the png.data is formatted.
## The returned png.data is currently always RGBA (4 channels)
## with bitDepth of 8.
## Decodes the PNG into an Image.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
failInvalid()
@ -323,7 +320,7 @@ proc decodePng*(data: seq[uint8]): Image =
data.readStr(pos + 4, 4) != "IHDR":
failInvalid()
inc(pos, 8)
header = parseHeader(data[pos ..< pos + 13])
header = decodeHeader(data[pos ..< pos + 13])
prevChunkType = "IHDR"
inc(pos, 13)
@ -353,7 +350,7 @@ proc decodePng*(data: seq[uint8]): Image =
inc counts.PLTE
if counts.PLTE > 1 or counts.IDAT > 0 or counts.tRNS > 0:
failInvalid()
palette = parsePalette(data[pos ..< pos + chunkLen])
palette = decodePalette(data[pos ..< pos + chunkLen])
of "tRNS":
inc counts.tRNS
if counts.tRNS > 1 or counts.IDAT > 0:
@ -407,7 +404,7 @@ proc decodePng*(data: seq[uint8]): Image =
result = Image()
result.width = header.width
result.height = header.height
result.data = parseImageData(header, palette, transparency, imageData)
result.data = decodeImageData(header, palette, transparency, imageData)
proc decodePng*(data: string): Image {.inline.} =
decodePng(cast[seq[uint8]](data))

BIN
tests/data/jpeg420exif.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

5
tests/test_jpg.nim Normal file
View file

@ -0,0 +1,5 @@
import pixie/fileformats/jpg
let original = readFile("tests/data/jpeg420exif.jpg")
discard decodeJpg(original)