commit
eb6d47c560
5 changed files with 238 additions and 55 deletions
|
@ -1,13 +1,14 @@
|
||||||
## Public interface to you library.
|
## Public interface to you library.
|
||||||
|
|
||||||
import pixie/images, pixie/masks, pixie/paths, pixie/common, pixie/blends,
|
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
|
export images, masks, paths, PixieError, blends
|
||||||
|
|
||||||
type
|
type
|
||||||
FileFormat* = enum
|
FileFormat* = enum
|
||||||
ffPng, ffBmp
|
ffPng, ffBmp, ffJpg
|
||||||
|
|
||||||
proc toMask*(image: Image): Mask =
|
proc toMask*(image: Image): Mask =
|
||||||
## Converts an Image to a Mask.
|
## Converts an Image to a Mask.
|
||||||
|
@ -23,12 +24,13 @@ proc toImage*(mask: Mask): Image =
|
||||||
|
|
||||||
proc decodeImage*(data: string | seq[uint8]): Image =
|
proc decodeImage*(data: string | seq[uint8]): Image =
|
||||||
## Loads an image from a memory.
|
## Loads an image from a memory.
|
||||||
if data.len > 8 and cast[array[8, uint8]](data.readUint64(0)) == pngSignature:
|
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
|
||||||
return decodePng(data)
|
decodePng(data)
|
||||||
|
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpgStartOfImage):
|
||||||
if data.len > 2 and data.readStr(0, 2) == "BM":
|
decodeJpg(data)
|
||||||
return decodeBmp(data)
|
elif data.len > 2 and data.readStr(0, 2) == "BM":
|
||||||
|
decodeBmp(data)
|
||||||
|
else:
|
||||||
raise newException(PixieError, "Unsupported image file format")
|
raise newException(PixieError, "Unsupported image file format")
|
||||||
|
|
||||||
proc readImage*(filePath: string): Image =
|
proc readImage*(filePath: string): Image =
|
||||||
|
@ -36,10 +38,12 @@ proc readImage*(filePath: string): Image =
|
||||||
decodeImage(readFile(filePath))
|
decodeImage(readFile(filePath))
|
||||||
|
|
||||||
proc encodeImage*(image: Image, fileFormat: FileFormat): string =
|
proc encodeImage*(image: Image, fileFormat: FileFormat): string =
|
||||||
## Encodes an image into a memory.
|
## Encodes an image into memory.
|
||||||
case fileFormat:
|
case fileFormat:
|
||||||
of ffPng:
|
of ffPng:
|
||||||
image.encodePng()
|
image.encodePng()
|
||||||
|
of ffJpg:
|
||||||
|
image.encodeJpg()
|
||||||
of ffBmp:
|
of ffBmp:
|
||||||
image.encodeBmp()
|
image.encodeBmp()
|
||||||
|
|
||||||
|
@ -50,7 +54,10 @@ proc writeFile*(image: Image, filePath: string, fileFormat: FileFormat) =
|
||||||
proc writeFile*(image: Image, filePath: string) =
|
proc writeFile*(image: Image, filePath: string) =
|
||||||
## Writes an image to a file.
|
## Writes an image to a file.
|
||||||
let fileFormat = case splitFile(filePath).ext:
|
let fileFormat = case splitFile(filePath).ext:
|
||||||
of "png": ffPng
|
of ".png": ffPng
|
||||||
of "bmp": ffBmp
|
of ".bmp": ffBmp
|
||||||
else: ffPng
|
of ".jpg": ffJpg
|
||||||
writeFile(filePath, image.encodeImage(fileFormat))
|
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):
|
when defined(release):
|
||||||
{.push checks: off.}
|
{.push checks: off.}
|
||||||
|
|
||||||
proc parseHeader(data: seq[uint8]): PngHeader =
|
proc decodeHeader(data: seq[uint8]): PngHeader =
|
||||||
result.width = data.readUint32(0).swap().int
|
result.width = data.readUint32(0).swap().int
|
||||||
result.height = data.readUint32(4).swap().int
|
result.height = data.readUint32(4).swap().int
|
||||||
result.bitDepth = data[8]
|
result.bitDepth = data[8]
|
||||||
|
@ -78,7 +78,7 @@ proc parseHeader(data: seq[uint8]): PngHeader =
|
||||||
if result.interlaceMethod != 0:
|
if result.interlaceMethod != 0:
|
||||||
raise newException(PixieError, "Interlaced PNG not yet supported")
|
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:
|
if data.len == 0 or data.len mod 3 != 0:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ proc unfilter(
|
||||||
|
|
||||||
result[unfiteredIdx(x, y)] = value
|
result[unfiteredIdx(x, y)] = value
|
||||||
|
|
||||||
proc parseImageData(
|
proc decodeImageData(
|
||||||
header: PngHeader,
|
header: PngHeader,
|
||||||
palette: seq[array[3, uint8]],
|
palette: seq[array[3, uint8]],
|
||||||
transparency, data: seq[uint8]
|
transparency, data: seq[uint8]
|
||||||
|
@ -217,22 +217,34 @@ proc parseImageData(
|
||||||
inc bytePos
|
inc bytePos
|
||||||
bitPos = 0
|
bitPos = 0
|
||||||
of 2:
|
of 2:
|
||||||
let special =
|
var special: ColorRGBA
|
||||||
if transparency.len == 6:
|
if transparency.len == 6: # Need to apply transparency check, slower.
|
||||||
ColorRGBA(
|
special.r = transparency[1]
|
||||||
r: transparency[1], g: transparency[3], b: transparency[5], a: 255
|
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[i] = rgba
|
||||||
else:
|
else:
|
||||||
ColorRGBA()
|
# While we can read an extra byte safely, do so. Much faster.
|
||||||
var bytePos: int
|
for i in 0 ..< header.height * header.width - 1:
|
||||||
for y in 0 ..< header.height:
|
var rgba = cast[ptr ColorRGBA](unfiltered[i * 3].unsafeAddr)[]
|
||||||
for x in 0 ..< header.width:
|
rgba.a = 255
|
||||||
let rgb = cast[ptr array[3, uint8]](unfiltered[bytePos].unsafeAddr)[]
|
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)
|
var rgba = ColorRGBA(r: rgb[0], g: rgb[1], b: rgb[2], a: 255)
|
||||||
if rgba == special:
|
if rgba == special:
|
||||||
rgba.a = 0
|
rgba.a = 0
|
||||||
result[x + y * header.width] = rgba
|
result[header.height * header.width - 1] = cast[ColorRGBA](rgba)
|
||||||
bytePos += 3
|
|
||||||
of 3:
|
of 3:
|
||||||
var bytePos, bitPos: int
|
var bytePos, bitPos: int
|
||||||
for y in 0 ..< header.height:
|
for y in 0 ..< header.height:
|
||||||
|
@ -275,32 +287,22 @@ proc parseImageData(
|
||||||
inc bytePos
|
inc bytePos
|
||||||
bitPos = 0
|
bitPos = 0
|
||||||
of 4:
|
of 4:
|
||||||
var bytePos: int
|
for i in 0 ..< header.height * header.width:
|
||||||
for y in 0 ..< header.height:
|
let bytePos = i * 2
|
||||||
for x in 0 ..< header.width:
|
result[i] = ColorRGBA(
|
||||||
result[x + y * header.width] = ColorRGBA(
|
|
||||||
r: unfiltered[bytePos],
|
r: unfiltered[bytePos],
|
||||||
g: unfiltered[bytePos],
|
g: unfiltered[bytePos],
|
||||||
b: unfiltered[bytePos],
|
b: unfiltered[bytePos],
|
||||||
a: unfiltered[bytePos + 1]
|
a: unfiltered[bytePos + 1]
|
||||||
)
|
)
|
||||||
bytePos += 2
|
|
||||||
of 6:
|
of 6:
|
||||||
var bytePos: int
|
for i in 0 ..< header.height * header.width:
|
||||||
for y in 0 ..< header.height:
|
result[i] = cast[ColorRGBA](unfiltered.readUint32(i * 4))
|
||||||
for x in 0 ..< header.width:
|
|
||||||
result[x + y * header.width] = cast[ColorRGBA](
|
|
||||||
unfiltered.readUint32(bytePos)
|
|
||||||
)
|
|
||||||
bytePos += 4
|
|
||||||
else:
|
else:
|
||||||
discard # Not possible, parseHeader validates
|
discard # Not possible, parseHeader validates
|
||||||
|
|
||||||
proc decodePng*(data: seq[uint8]): Image =
|
proc decodePng*(data: seq[uint8]): Image =
|
||||||
## Decodes the PNG from the parameter buffer. Check png.channels and
|
## Decodes the PNG into an Image.
|
||||||
## png.bitDepth to see how the png.data is formatted.
|
|
||||||
## The returned png.data is currently always RGBA (4 channels)
|
|
||||||
## with bitDepth of 8.
|
|
||||||
|
|
||||||
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
|
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
|
||||||
failInvalid()
|
failInvalid()
|
||||||
|
@ -323,7 +325,7 @@ proc decodePng*(data: seq[uint8]): Image =
|
||||||
data.readStr(pos + 4, 4) != "IHDR":
|
data.readStr(pos + 4, 4) != "IHDR":
|
||||||
failInvalid()
|
failInvalid()
|
||||||
inc(pos, 8)
|
inc(pos, 8)
|
||||||
header = parseHeader(data[pos ..< pos + 13])
|
header = decodeHeader(data[pos ..< pos + 13])
|
||||||
prevChunkType = "IHDR"
|
prevChunkType = "IHDR"
|
||||||
inc(pos, 13)
|
inc(pos, 13)
|
||||||
|
|
||||||
|
@ -353,7 +355,7 @@ proc decodePng*(data: seq[uint8]): Image =
|
||||||
inc counts.PLTE
|
inc counts.PLTE
|
||||||
if counts.PLTE > 1 or counts.IDAT > 0 or counts.tRNS > 0:
|
if counts.PLTE > 1 or counts.IDAT > 0 or counts.tRNS > 0:
|
||||||
failInvalid()
|
failInvalid()
|
||||||
palette = parsePalette(data[pos ..< pos + chunkLen])
|
palette = decodePalette(data[pos ..< pos + chunkLen])
|
||||||
of "tRNS":
|
of "tRNS":
|
||||||
inc counts.tRNS
|
inc counts.tRNS
|
||||||
if counts.tRNS > 1 or counts.IDAT > 0:
|
if counts.tRNS > 1 or counts.IDAT > 0:
|
||||||
|
@ -407,7 +409,7 @@ proc decodePng*(data: seq[uint8]): Image =
|
||||||
result = Image()
|
result = Image()
|
||||||
result.width = header.width
|
result.width = header.width
|
||||||
result.height = header.height
|
result.height = header.height
|
||||||
result.data = parseImageData(header, palette, transparency, imageData)
|
result.data = decodeImageData(header, palette, transparency, imageData)
|
||||||
|
|
||||||
proc decodePng*(data: string): Image {.inline.} =
|
proc decodePng*(data: string): Image {.inline.} =
|
||||||
decodePng(cast[seq[uint8]](data))
|
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