commit
eb6d47c560
5 changed files with 238 additions and 55 deletions
|
@ -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()
|
||||
|
||||
|
@ -50,7 +54,10 @@ proc writeFile*(image: Image, filePath: string, fileFormat: FileFormat) =
|
|||
proc writeFile*(image: Image, filePath: string) =
|
||||
## Writes an image to a file.
|
||||
let fileFormat = case splitFile(filePath).ext:
|
||||
of "png": ffPng
|
||||
of "bmp": ffBmp
|
||||
else: ffPng
|
||||
writeFile(filePath, image.encodeImage(fileFormat))
|
||||
of ".png": ffPng
|
||||
of ".bmp": ffBmp
|
||||
of ".jpg": ffJpg
|
||||
of ".jpeg": ffJpg
|
||||
else:
|
||||
raise newException(PixieError, "Unsupported image file extension")
|
||||
image.writeFile(filePath, fileformat)
|
||||
|
|
169
src/pixie/fileformats/jpg.nim
Normal file
169
src/pixie/fileformats/jpg.nim
Normal file
|
@ -0,0 +1,169 @@
|
|||
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]
|
||||
|
||||
type
|
||||
Component = object
|
||||
id, samplingFactors, quantizationTable: uint8
|
||||
|
||||
Jpg = object
|
||||
width, height: int
|
||||
components: array[3, Component]
|
||||
|
||||
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(jpg: var Jpg, 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 width <= 0:
|
||||
raise newException(PixieError, "Invalid JPG width")
|
||||
|
||||
if height <= 0:
|
||||
raise newException(PixieError, "Invalid JPG height")
|
||||
|
||||
if precision != 8:
|
||||
raise newException(PixieError, "Unsupported JPG bit depth")
|
||||
|
||||
if components != 3:
|
||||
raise newException(PixieError, "Unsupported JPG channel count")
|
||||
|
||||
jpg.width = width
|
||||
jpg.height = height
|
||||
|
||||
if 8 + components * 3 != segmentLen:
|
||||
failInvalid()
|
||||
|
||||
for i in 0 ..< 3:
|
||||
jpg.components[i] = Component(
|
||||
id: data[pos],
|
||||
samplingFactors: data[pos + 1],
|
||||
quantizationTable: data[pos + 2]
|
||||
)
|
||||
pos += 3
|
||||
|
||||
proc decodeDHT(data: seq[uint8], pos: var int) =
|
||||
skipSegment(data, pos)
|
||||
|
||||
proc decodeDQT(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:
|
||||
if pos + 1 == data.len:
|
||||
failInvalid()
|
||||
if data[pos + 1] == 0xD9: # End of Image:
|
||||
pos += 2
|
||||
break
|
||||
elif data[pos + 1] == 0x00:
|
||||
discard # Skip the 0x00 byte
|
||||
else:
|
||||
failInvalid()
|
||||
else:
|
||||
discard
|
||||
|
||||
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
|
||||
jpg: Jpg
|
||||
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
|
||||
jpg.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
|
||||
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 segment")
|
||||
|
||||
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")
|
|
@ -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]
|
||||
|
@ -217,22 +217,34 @@ proc parseImageData(
|
|||
inc bytePos
|
||||
bitPos = 0
|
||||
of 2:
|
||||
let special =
|
||||
if transparency.len == 6:
|
||||
ColorRGBA(
|
||||
r: transparency[1], g: transparency[3], b: transparency[5], a: 255
|
||||
)
|
||||
else:
|
||||
ColorRGBA()
|
||||
var bytePos: int
|
||||
for y in 0 ..< header.height:
|
||||
for x in 0 ..< header.width:
|
||||
let rgb = cast[ptr array[3, uint8]](unfiltered[bytePos].unsafeAddr)[]
|
||||
var rgba = ColorRGBA(r: rgb[0], g: rgb[1], b: rgb[2], a: 255)
|
||||
var special: ColorRGBA
|
||||
if transparency.len == 6: # Need to apply transparency check, slower.
|
||||
special.r = transparency[1]
|
||||
special.g = transparency[3]
|
||||
special.b = transparency[5]
|
||||
special.a = 255
|
||||
|
||||
# While we can read an extra byte safely, do so. Much faster.
|
||||
for i in 0 ..< header.height * header.width - 1:
|
||||
var rgba = cast[ptr ColorRGBA](unfiltered[i * 3].unsafeAddr)[]
|
||||
rgba.a = 255
|
||||
if rgba == special:
|
||||
rgba.a = 0
|
||||
result[x + y * header.width] = rgba
|
||||
bytePos += 3
|
||||
result[i] = rgba
|
||||
else:
|
||||
# While we can read an extra byte safely, do so. Much faster.
|
||||
for i in 0 ..< header.height * header.width - 1:
|
||||
var rgba = cast[ptr ColorRGBA](unfiltered[i * 3].unsafeAddr)[]
|
||||
rgba.a = 255
|
||||
result[i] = rgba
|
||||
|
||||
let
|
||||
lastOffset = header.height * header.width - 1
|
||||
rgb = cast[ptr array[3, uint8]](unfiltered[lastOffset * 3].unsafeAddr)[]
|
||||
var rgba = ColorRGBA(r: rgb[0], g: rgb[1], b: rgb[2], a: 255)
|
||||
if rgba == special:
|
||||
rgba.a = 0
|
||||
result[header.height * header.width - 1] = cast[ColorRGBA](rgba)
|
||||
of 3:
|
||||
var bytePos, bitPos: int
|
||||
for y in 0 ..< header.height:
|
||||
|
@ -275,32 +287,22 @@ proc parseImageData(
|
|||
inc bytePos
|
||||
bitPos = 0
|
||||
of 4:
|
||||
var bytePos: int
|
||||
for y in 0 ..< header.height:
|
||||
for x in 0 ..< header.width:
|
||||
result[x + y * header.width] = ColorRGBA(
|
||||
r: unfiltered[bytePos],
|
||||
g: unfiltered[bytePos],
|
||||
b: unfiltered[bytePos],
|
||||
a: unfiltered[bytePos + 1]
|
||||
)
|
||||
bytePos += 2
|
||||
for i in 0 ..< header.height * header.width:
|
||||
let bytePos = i * 2
|
||||
result[i] = ColorRGBA(
|
||||
r: unfiltered[bytePos],
|
||||
g: unfiltered[bytePos],
|
||||
b: unfiltered[bytePos],
|
||||
a: unfiltered[bytePos + 1]
|
||||
)
|
||||
of 6:
|
||||
var bytePos: int
|
||||
for y in 0 ..< header.height:
|
||||
for x in 0 ..< header.width:
|
||||
result[x + y * header.width] = cast[ColorRGBA](
|
||||
unfiltered.readUint32(bytePos)
|
||||
)
|
||||
bytePos += 4
|
||||
for i in 0 ..< header.height * header.width:
|
||||
result[i] = cast[ColorRGBA](unfiltered.readUint32(i * 4))
|
||||
else:
|
||||
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 +325,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 +355,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 +409,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
BIN
tests/data/jpeg420exif.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 751 KiB |
5
tests/test_jpg.nim
Normal file
5
tests/test_jpg.nim
Normal file
|
@ -0,0 +1,5 @@
|
|||
import pixie/fileformats/jpg
|
||||
|
||||
let original = readFile("tests/data/jpeg420exif.jpg")
|
||||
|
||||
# discard decodeJpg(original)
|
Loading…
Reference in a new issue