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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Format | Read | Write |
PNG | ✅ | ✅ |
JPEG | ✅ | |
BMP | ✅ | ✅ |
QOI | ✅ | ✅ |
GIF | ✅ | |
SVG | ✅ | |

Expand Down
3 changes: 1 addition & 2 deletions src/pixie.nim
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import bumpy, chroma, flatty/binny, os, pixie/common, pixie/contexts,
pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpg,
pixie/fileformats/png, pixie/fileformats/qoi, pixie/fileformats/svg,
pixie/fonts, pixie/images, pixie/masks, pixie/paints, pixie/paths,
strutils, vmath
pixie/fonts, pixie/images, pixie/masks, pixie/paints, pixie/paths, strutils, vmath

export bumpy, chroma, common, contexts, fonts, images, masks, paints, paths, vmath

Expand Down
190 changes: 96 additions & 94 deletions src/pixie/fileformats/qoi.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import std/endians, chroma, flatty/binny
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

morepretty does not understand [] imports so we do not use them (support could be added tho, not opposed to them if it is automatic)

import pixie/[common, images, internal]
import chroma, flatty/binny, pixie/common, pixie/images, pixie/internal

# See: https://qoiformat.org/qoi-specification.pdf

Expand All @@ -15,158 +14,150 @@ const
opRun = 0b11000000'u8

type
Colorspace* = enum sRBG = 0, linear = 1
Colorspace* = enum
sRBG = 0
Linear = 1

Qoi* = ref object
## Raw QOI image data.
data*: seq[ColorRGBA]
width*, height*, channels*: int
colorspace*: Colorspace
data*: seq[ColorRGBA]

Index = array[indexLen, ColorRGBA]

func hash(p: ColorRGBA): int =
(p.r.int * 3 + p.g.int * 5 + p.b.int * 7 + p.a.int * 11) mod indexLen

func toImage*(qoi: Qoi): Image =
func newImage*(qoi: Qoi): Image =
## Converts raw QOI data to `Image`.
result = newImage(qoi.width, qoi.height)
copyMem(result.data[0].addr, qoi.data[0].addr, qoi.data.len * 4)
result.data.toPremultipliedAlpha()

func toQoi*(img: Image; channels: range[3..4]): Qoi =
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

be consistent with png's raw api, do not take in channels without tests (this is not run in any tests)

## Converts an `Image` to raw QOI data.
result = Qoi(
data: newSeq[ColorRGBA](img.data.len),
width: img.width,
height: img.height,
channels: channels)
result.data.toStraightAlpha()

proc decompressQoi*(data: string): Qoi {.raises: [PixieError].} =
proc decodeQoiRaw*(data: string): Qoi {.raises: [PixieError].} =
## Decompress QOI file format data.
if data.len <= 14 or data[0 .. 3] != qoiSignature:
raise newException(PixieError, "Invalid QOI header")
var
width, height: uint32
channels, colorspace: uint8
block:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see endian comment below

when cpuEndian == bigEndian:
width = data.readUint32(4)
height = data.readUint32(8)
else:
var (wBe, hBe) = (data.readUint32(4), data.readUint32(8))
swapEndian32(addr width, addr wBe)
swapEndian32(addr height, addr hBe)
channels = data.readUint8(12)
colorspace = data.readUint8(13)

let
width = data.readUint32(4).swap()
height = data.readUint32(8).swap()
channels = data.readUint8(12)
colorspace = data.readUint8(13)

if channels notin {3, 4} or colorspace notin {0, 1}:
raise newException(PixieError, "Invalid QOI header")

if width.int * height.int > uint32.high.int:
raise newException(PixieError, "QOI is too large to decode")

result = Qoi(
data: newSeq[ColorRGBA](int width * height),
width: int width,
Copy link
Copy Markdown
Collaborator Author

@guzba guzba Jan 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we by convention append our type conversions to the end

height: int height,
channels: int channels,
colorspace: Colorspace colorspace)
result = Qoi()
result.width = width.int
result.height = height.int
result.channels = channels.int
result.colorspace = colorspace.Colorspace
result.data.setLen(result.width * result.height)

var
index: Index
p = 14
run: uint8
px = rgba(0, 0, 0, 0xff)

px = rgba(0, 0, 0, 255)
for dst in result.data.mitems:
if p > data.len-8:
if p > data.len - 8:
raise newException(PixieError, "Underrun of QOI decoder")

if run > 0:
dec(run)
dec run
else:
let b0 = data.readUint8(p)
inc(p)
case b0
inc p

case b0:
of opRgb:
px.r = data.readUint8(p+0)
px.g = data.readUint8(p+1)
px.b = data.readUint8(p+2)
inc(p, 3)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of convension we only use inc and dec for +1 and -1, otherwise just use +=

px.r = data.readUint8(p + 0)
px.g = data.readUint8(p + 1)
px.b = data.readUint8(p + 2)
p += 3
of opRgba:
px.r = data.readUint8(p+0)
px.g = data.readUint8(p+1)
px.b = data.readUint8(p+2)
px.a = data.readUint8(p+3)
inc(p, 4)
px.r = data.readUint8(p + 0)
px.g = data.readUint8(p + 1)
px.b = data.readUint8(p + 2)
px.a = data.readUint8(p + 3)
p += 4
else:
case b0 and opMask2
case b0 and opMask2:
of opIndex:
px = index[b0]
of opDiff:
px.r = px.r + uint8((b0 shr 4) and 0x03) - 2
px.g = px.g + uint8((b0 shr 2) and 0x03) - 2
px.b = px.b + uint8((b0 shr 0) and 0x03) - 2
px.r = px.r + ((b0 shr 4) and 0x03).uint8 - 2
px.g = px.g + ((b0 shr 2) and 0x03).uint8 - 2
px.b = px.b + ((b0 shr 0) and 0x03).uint8 - 2
of opLuma:
let b1 = data.readUint8(p)
inc(p)
let vg = (b0.uint8 and 0x3f) - 32
let
b1 = data.readUint8(p)
vg = (b0.uint8 and 0x3f) - 32
px.r = px.r + vg - 8 + ((b1 shr 4) and 0x0f)
px.g = px.g + vg
px.b = px.b + vg - 8 + ((b1 shr 0) and 0x0f)
inc p
of opRun:
run = b0 and 0x3f
else: assert false
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems bad, throw for real?

else:
raise newException(PixieError, "Unexpected QOI op")

index[hash(px)] = px

dst = px

while p < data.len:
case data[p]
of '\0': discard
of '\1': break # ignore trailing data
case data[p]:
of '\0':
discard
of '\1':
break # ignore trailing data
else:
raise newException(PixieError, "Invalid QOI padding")
inc(p)

proc decodeQoi*(data: string): Image {.raises: [PixieError].} =
## Decodes data in the QOI file format to an `Image`.
decompressQoi(data).toImage()

proc decodeQoi*(data: seq[uint8]): Image {.inline, raises: [PixieError].} =
Copy link
Copy Markdown
Collaborator Author

@guzba guzba Jan 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gave up on seq[uint8] a long time ago. nim's native representation for bytes is string, see standard lib readFile

## Decodes data in the QOI file format to an `Image`.
decodeQoi(cast[string](data))
newImage(decodeQoiRaw(data))

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

if qoi.width.int * qoi.height.int > uint32.high.int:
raise newException(PixieError, "QOI is too large to encode")

# Allocate a buffer 3/4 the size of the pathological encoding
result = newStringOfCap(14 + 8 + qoi.data.len * 3)
# allocate a buffer 3/4 the size of the pathological encoding

result.add(qoiSignature)
when cpuEndian == bigEndian:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no tests, untested. we do not support big endian anywhere else and have not / cannot? ensure this actually is all that is needed. rm.

result.addUint32(uint32 qoi.width)
result.addUint32(uint32 qoi.height)
else:
var
(wLe, hLe) = (uint32 qoi.width, uint32 qoi.height)
result.setLen(12)
swapEndian32(addr result[4], addr wLe)
swapEndian32(addr result[8], addr hLe)
result.addUint8(uint8 qoi.channels)
result.addUint8(uint8 qoi.colorspace)
result.addUint32(qoi.width.uint32.swap())
result.addUint32(qoi.height.uint32.swap())
result.addUint8(qoi.channels.uint8)
result.addUint8(qoi.colorspace.uint8)

var
index: Index
run: uint8
pxPrev = rgba(0, 0, 0, 0xff)

pxPrev = rgba(0, 0, 0, 255)
for off, px in qoi.data:
if px == pxPrev:
inc run
if run == 62 or off == qoi.data.high:
result.addUint8(opRun or pred(run))
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to look this up too, not worth it

reset run
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to look this up, not worth it

result.addUint8(opRun or (run - 1))
run = 0
else:
if run > 0:
result.addUint8(opRun or pred(run))
reset run
result.addUint8(opRun or (run - 1))
run = 0

let i = hash(px)
if index[i] == px: result.addUint8(opIndex or uint8(i))
if index[i] == px:
result.addUint8(opIndex or uint8(i))
else:
index[i] = px
if px.a == pxPrev.a:
Expand All @@ -179,16 +170,14 @@ proc compressQoi*(qoi: Qoi): string =
if (vr > -3) and (vr < 2) and
(vg > -3) and (vg < 2) and
(vb > -3) and (vb < 2):
let b = opDiff or uint8(
((vr + 2) shl 4) or
((vg + 2) shl 2) or
((vb + 2) shl 0))
let b = opDiff or
(((vr + 2) shl 4) or ((vg + 2) shl 2) or ((vb + 2) shl 0)).uint8
result.addUint8(b)
elif vgr > -9 and vgr < 8 and
vg > -33 and vg < 32 and
vgb > -9 and vgb < 8:
result.addUint8(opLuma or uint8(vg + 32))
result.addUint8(uint8 ((vgr + 8) shl 4) or (vgb + 8))
result.addUint8(opLuma or (vg + 32).uint8)
result.addUint8((((vgr + 8) shl 4) or (vgb + 8)).uint8)
else:
result.addUint8(opRgb)
result.addUint8(px.r)
Expand All @@ -200,10 +189,23 @@ proc compressQoi*(qoi: Qoi): string =
result.addUint8(px.g)
result.addUint8(px.b)
result.addUint8(px.a)

pxPrev = px
for _ in 0..6: result.addUint8(0x00)

for _ in 0 .. 6:
result.addUint8(0x00)

result.addUint8(0x01)

proc encodeQoi*(img: Image): string {.raises: [].} =
proc encodeQoi*(image: Image): string {.raises: [PixieError].} =
## Encodes an image to the QOI file format.
compressQoi(toQoi(img, 4))
let qoi = Qoi()
qoi.width = image.width
qoi.height = image.height
qoi.channels = 4
qoi.data.setLen(image.data.len)

copyMem(qoi.data[0].addr, image.data[0].addr, image.data.len * 4)
qoi.data.toStraightAlpha()

encodeQoi(qoi)
2 changes: 1 addition & 1 deletion src/pixie/images.nim
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ proc drawUber(
for q in [0, 4, 8, 12]:
let sourceVec = unpackAlphaValues(values)
if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, mm_setzero_si128())) == 0xffff:
mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr, mm_setzero_si128())
mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr, mm_setzero_si128())
elif (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and 0x8888) != 0x8888:
let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x + q, y)].addr)
mm_storeu_si128(
Expand Down
2 changes: 1 addition & 1 deletion tests/benchmark_qoi.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import benchy, pixie/fileformats/qoi
let data = readFile("tests/fileformats/qoi/testcard_rgba.qoi")

timeIt "pixie decode":
keep decodeQOI(data)
keep decodeQoi(data)
2 changes: 1 addition & 1 deletion tests/fuzz_image_draw.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import random, pixie
import pixie, random

randomize()

Expand Down
12 changes: 6 additions & 6 deletions tests/fuzz_qoi.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import std/[random, strformat]
import pixie/[common, fileformats/qoi]
import pixie/common, pixie/fileformats/qoi, std/random, strformat

randomize()

Expand All @@ -9,17 +8,18 @@ for i in 0 ..< 10_000:
var data = original
let
pos = rand(data.len)
value = rand(255).char
data[pos] = value
value = rand(255).uint8
data[pos] = value.char
echo &"{i} {pos} {value}"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important to echo, so if fuzzing crashes we can reproduce the case immediately

try:
let img = decodeQOI(data)
let img = decodeQoi(data)
doAssert img.height > 0 and img.width > 0
except PixieError:
discard

data = data[0 ..< pos]
try:
let img = decodeQOI(data)
let img = decodeQoi(data)
doAssert img.height > 0 and img.width > 0
except PixieError:
discard
20 changes: 11 additions & 9 deletions tests/test_qoi.nim
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import pixie, pixie/fileformats/qoi, pixie/fileformats/png
import pixie, pixie/fileformats/png, pixie/fileformats/qoi

const tests = ["testcard", "testcard_rgba"]

for name in tests:
var input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
var control = decodePng(readFile("tests/fileformats/qoi/" & name & ".png"))
doAssert(input.data == control.data, "input mismatch of " & name)
let
input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
control = decodePng(readFile("tests/fileformats/qoi/" & name & ".png"))
doAssert input.data == control.data, "input mismatch of " & name
discard encodeQoi(control)

for name in tests:
var
input: Qoi = decompressQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
output: Qoi = decompressQoi(compressQoi(input))
doAssert(output.data.len == input.data.len)
doAssert(output.data == input.data)
let
input = decodeQoiRaw(readFile("tests/fileformats/qoi/" & name & ".qoi"))
output = decodeQoiRaw(encodeQoi(input))
doAssert output.data.len == input.data.len
doAssert output.data == input.data