Merge pull request #7 from guzba/master

stuff
This commit is contained in:
treeform 2020-11-21 19:10:31 -08:00 committed by GitHub
commit eb6d47c560
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 238 additions and 55 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()
@ -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)

View 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")

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

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)