Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bindings/bindings.nim
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ exportObject ColorStop:
exportObject TextMetrics:
discard

exportObject ImageDimensions:
discard

exportSeq seq[float32]:
discard

Expand Down Expand Up @@ -310,7 +313,10 @@ exportRefObject Context:
isPointInStroke(Context, Path, float32, float32)

exportProcs:
decodeImage
decodeImageDimensions
readImage
readImageDimensions
readmask
readTypeface
readFont
Expand Down
2 changes: 1 addition & 1 deletion pixie.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ srcDir = "src"
requires "nim >= 1.4.8"
requires "vmath >= 1.1.4"
requires "chroma >= 0.2.5"
requires "zippy >= 0.9.9"
requires "zippy >= 0.9.11"
requires "flatty >= 0.3.3"
requires "nimsimd >= 1.0.0"
requires "bumpy >= 1.1.1"
Expand Down
28 changes: 28 additions & 0 deletions src/pixie.nim
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: [].
## Convert a straight alpha RGBA to a premultiplied alpha RGBA.
c.rgbx()

proc decodeImageDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes an image's dimensions from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePngDimensions(data)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
decodeJpegDimensions(data)
elif data.len > 2 and data.readStr(0, 2) == bmpSignature:
decodeBmpDimensions(data)
elif data.len > 6 and data.readStr(0, 6) in gifSignatures:
decodeGifDimensions(data)
elif data.len > (14+8) and data.readStr(0, 4) == qoiSignature:
decodeQoiDimensions(data)
elif data.len > 9 and data.readStr(0, 2) in ppmSignatures:
decodePpmDimensions(data)
else:
raise newException(PixieError, "Unsupported image file format")

proc decodeImage*(data: string): Image {.raises: [PixieError].} =
## Loads an image from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
Expand Down Expand Up @@ -45,6 +64,15 @@ proc decodeMask*(data: string): Mask {.raises: [PixieError].} =
else:
raise newException(PixieError, "Unsupported mask file format")

proc readImageDimensions*(
filePath: string
): ImageDimensions {.inline, raises: [PixieError].} =
## Loads an image from a file.
try:
decodeImageDimensions(readFile(filePath))
except IOError as e:
raise newException(PixieError, e.msg, e)

proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} =
## Loads an image from a file.
try:
Expand Down
3 changes: 3 additions & 0 deletions src/pixie/common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type
SubtractMaskBlend ## Inverse mask
ExcludeMaskBlend

ImageDimensions* = object
width*, height*: int

proc mix*(a, b: uint8, t: float32): uint8 {.inline, raises: [].} =
## Linearly interpolate between a and b using t.
let t = round(t * 255).uint32
Expand Down
15 changes: 15 additions & 0 deletions src/pixie/fileformats/bmp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,21 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =

decodeDib(data[14].unsafeAddr, data.len - 14)

proc decodeBmpDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the BMP dimensions.

if data.len < 26:
failInvalid()

# BMP Header
if data[0 .. 1] != "BM":
failInvalid()

result.width = data.readInt32(18).int
result.height = abs(data.readInt32(22)).int

proc encodeBmp*(image: Image): string {.raises: [].} =
## Encodes an image into the BMP file format.

Expand Down
13 changes: 13 additions & 0 deletions src/pixie/fileformats/gif.nim
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,18 @@ proc decodeGif*(data: string): Image {.raises: [PixieError].} =
else:
raise newException(PixieError, "Invalid GIF block type")

proc decodeGifDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the GIF dimensions.
if data.len < 10:
failInvalid()

if data[0 .. 5] notin gifSignatures:
raise newException(PixieError, "Invalid GIF file signature")

result.width = data.readInt16(6).int
result.height = data.readInt16(8).int

when defined(release):
{.pop.}
65 changes: 64 additions & 1 deletion src/pixie/fileformats/jpeg.nim
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type
len, pos: int
bitsBuffered: int
bitBuffer: uint32
foundSOF: bool
imageHeight, imageWidth: int
progressive: bool
quantizationTables: array[4, array[64, uint8]]
Expand Down Expand Up @@ -243,6 +244,10 @@ proc decodeDHT(state: var DecoderState) =

proc decodeSOF0(state: var DecoderState) =
## Decode start of Frame
if state.foundSOF:
failInvalid()
state.foundSOF = true

var len = state.readUint16be().int - 2

let precision = state.readUint8()
Expand Down Expand Up @@ -1056,7 +1061,7 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
state.decodeDHT()
of 0xD8:
# SOI - Start of Image
continue
discard
of 0xD9:
# EOI - End of Image
break
Expand Down Expand Up @@ -1094,5 +1099,63 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =

state.buildImage()

proc decodeJpegDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the JPEG dimensions.

var state = DecoderState()
state.buffer = cast[ptr UncheckedArray[uint8]](data.cstring)
state.len = data.len

while true:
if state.readUint8() != 0xFF:
failInvalid("invalid chunk marker")

let chunkId = state.readUint8()
case chunkId:
of 0xD8:
# SOI - Start of Image
discard
of 0xC0:
# Start Of Frame (Baseline DCT)
state.decodeSOF0()
break
of 0xC1:
# Start Of Frame (Extended sequential DCT)
state.decodeSOF1()
break
of 0xC2:
# Start Of Frame (Progressive DCT)
state.decodeSOF2()
break
of 0xDB:
# Define Quantization Table(s)
state.skipChunk()
of 0XE0:
# Application-specific
state.skipChunk()
of 0xE1:
# Exif/APP1
state.decodeExif()
of 0xE2..0xEF:
# Application-specific
state.skipChunk()
of 0xFE:
# Comment
state.skipChunk()
else:
failInvalid("invalid chunk " & chunkId.toHex())

case state.orientation:
of 0, 1, 2, 3, 4:
result.width = state.imageWidth
result.height = state.imageHeight
of 5, 6, 7, 8:
result.width = state.imageHeight
result.height = state.imageWidth
else:
failInvalid("invalid orientation")

when defined(release):
{.pop.}
43 changes: 31 additions & 12 deletions src/pixie/fileformats/png.nim
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ proc decodePalette(data: string): seq[ColorRGB] =
result.setLen(data.len div 3)

for i in 0 ..< data.len div 3:
result[i] = cast[ptr ColorRGB](data[i * 3].unsafeAddr)[]
copyMem(result[i].addr, data[i * 3].unsafeAddr, 3)

proc unfilter(
uncompressed: string, height, rowBytes, bpp: int
Expand Down Expand Up @@ -262,21 +262,20 @@ proc decodeImageData(

# 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
copyMem(result[i].addr, unfiltered[i * 3].unsafeAddr, 4)
result[i].a = 255
if result[i] == special:
result[i].a = 0
else:
# While we can read an extra byte safely, do so. Much faster.
# var rgba: ColorRGBA
for i in 0 ..< header.height * header.width - 1:
var rgba = cast[ptr ColorRGBA](unfiltered[i * 3].unsafeAddr)[]
rgba.a = 255
result[i] = rgba
copyMem(result[i].addr, unfiltered[i * 3].unsafeAddr, 4)
result[i].a = 255

let
lastOffset = header.height * header.width - 1
rgb = cast[ptr array[3, uint8]](unfiltered[lastOffset * 3].unsafeAddr)[]
let lastOffset = header.height * header.width - 1
var rgb: array[3, uint8]
copyMem(rgb.addr, unfiltered[lastOffset * 3].unsafeAddr, 3)
var rgba = ColorRGBA(r: rgb[0], g: rgb[1], b: rgb[2], a: 255)
if rgba == special:
rgba.a = 0
Expand Down Expand Up @@ -342,6 +341,26 @@ proc newImage*(png: Png): Image {.raises: [PixieError].} =
copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4)
result.data.toPremultipliedAlpha()

proc decodePngDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PNG dimensions.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
failInvalid()

# PNG file signature
let signature = cast[array[8, uint8]](data.readUint64(0))
if signature != pngSignature:
failInvalid()

# First chunk must be IHDR
if data.readUint32(8).swap() != 13 or data.readStr(12, 4) != "IHDR":
failInvalid()

let header = decodeHeader(data[16 ..< 16 + 13])
result.width = header.width
result.height = header.height

proc decodePng*(data: string): Png {.raises: [PixieError].} =
## Decodes the PNG data.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
Expand Down
8 changes: 8 additions & 0 deletions src/pixie/fileformats/ppm.nim
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ proc decodePpm*(data: string): Image {.raises: [PixieError].} =
else:
decodeP6Data(data[header.dataOffset .. ^1], header.maxVal)

proc decodePpmDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PPM dimensions.
let header = decodeHeader(data)
result.width = header.width
result.height = header.height

proc encodePpm*(image: Image): string {.raises: [].} =
## Encodes an image into the PPM file format (version P6).

Expand Down
10 changes: 10 additions & 0 deletions src/pixie/fileformats/qoi.nim
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} =
raise newException(PixieError, "Invalid QOI padding")
inc(p)

proc decodeQoiDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the QOI dimensions.
if data.len <= 12 or data[0 .. 3] != qoiSignature:
raise newException(PixieError, "Invalid QOI header")

result.width = data.readUint32(4).swap().int
result.height = data.readUint32(8).swap().int

proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} =
## Encodes raw QOI pixels to the QOI file format.

Expand Down
19 changes: 10 additions & 9 deletions src/pixie/images.nim
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,13 @@ proc minifyBy2*(image: Image, power = 1): Image {.raises: [PixieError].} =
b = src.unsafe[x * 2 + 1, y * 2 + 0]
c = src.unsafe[x * 2 + 1, y * 2 + 1]
d = src.unsafe[x * 2 + 0, y * 2 + 1]
rgba = rgbx(
mixed = rgbx(
((a.r.uint32 + b.r + c.r + d.r) div 4).uint8,
((a.g.uint32 + b.g + c.g + d.g) div 4).uint8,
((a.b.uint32 + b.b + c.b + d.b) div 4).uint8,
((a.a.uint32 + b.a + c.a + d.a) div 4).uint8
)

result.unsafe[x, y] = rgba
result.unsafe[x, y] = mixed

if srcWidthIsOdd:
let rgbx = mix(
Expand Down Expand Up @@ -382,6 +381,8 @@ proc magnifyBy2*(image: Image, power = 1): Image {.raises: [PixieError].} =
proc applyOpacity*(target: Image | Mask, opacity: float32) {.raises: [].} =
## Multiplies alpha of the image by opacity.
let opacity = round(255 * opacity).uint16
if opacity == 255:
return

if opacity == 0:
when type(target) is Image:
Expand Down Expand Up @@ -434,12 +435,12 @@ proc applyOpacity*(target: Image | Mask, opacity: float32) {.raises: [].} =

when type(target) is Image:
for j in i div 4 ..< target.data.len:
var rgba = target.data[j]
rgba.r = ((rgba.r * opacity) div 255).uint8
rgba.g = ((rgba.g * opacity) div 255).uint8
rgba.b = ((rgba.b * opacity) div 255).uint8
rgba.a = ((rgba.a * opacity) div 255).uint8
target.data[j] = rgba
var rgbx = target.data[j]
rgbx.r = ((rgbx.r * opacity) div 255).uint8
rgbx.g = ((rgbx.g * opacity) div 255).uint8
rgbx.b = ((rgbx.b * opacity) div 255).uint8
rgbx.a = ((rgbx.a * opacity) div 255).uint8
target.data[j] = rgbx
else:
for j in i ..< target.data.len:
target.data[j] = ((target.data[j] * opacity) div 255).uint8
Expand Down
4 changes: 2 additions & 2 deletions src/pixie/internal.nim
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ proc fillUnsafe*(
else:
when sizeof(int) == 8:
# Fill 8 bytes at a time when possible
let
var
u32 = cast[uint32](rgbx)
u64 = cast[uint64]([u32, u32])
for _ in 0 ..< len div 2:
cast[ptr uint64](data[i].addr)[] = u64
copyMem(data[i].addr, u64.addr, 8)
i += 2
# Fill whatever is left the slow way
for j in i ..< start + len:
Expand Down
4 changes: 2 additions & 2 deletions tests/benchmark_gif.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import benchy, pixie/fileformats/gif

let data = readFile("tests/fileformats/gif/audrey.gif")

timeIt "pixie decode":
keep decodeGif(data)
timeIt "gif decode":
discard decodeGif(data)
Loading