qoi consistent style pass

This commit is contained in:
Ryan Oldenburg 2022-01-03 17:41:36 -06:00
parent a8cd3bfd3b
commit 91abc684ad
8 changed files with 140 additions and 124 deletions

View file

@ -34,6 +34,7 @@ Format | Read | Write |
PNG | ✅ | ✅ | PNG | ✅ | ✅ |
JPEG | ✅ | | JPEG | ✅ | |
BMP | ✅ | ✅ | BMP | ✅ | ✅ |
QOI | ✅ | ✅ |
GIF | ✅ | | GIF | ✅ | |
SVG | ✅ | | SVG | ✅ | |

View file

@ -1,8 +1,7 @@
import bumpy, chroma, flatty/binny, os, pixie/common, pixie/contexts, import bumpy, chroma, flatty/binny, os, pixie/common, pixie/contexts,
pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpg, pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpg,
pixie/fileformats/png, pixie/fileformats/qoi, pixie/fileformats/svg, pixie/fileformats/png, pixie/fileformats/qoi, pixie/fileformats/svg,
pixie/fonts, pixie/images, pixie/masks, pixie/paints, pixie/paths, pixie/fonts, pixie/images, pixie/masks, pixie/paints, pixie/paths, strutils, vmath
strutils, vmath
export bumpy, chroma, common, contexts, fonts, images, masks, paints, paths, vmath export bumpy, chroma, common, contexts, fonts, images, masks, paints, paths, vmath

View file

@ -1,5 +1,4 @@
import std/endians, chroma, flatty/binny import chroma, flatty/binny, pixie/common, pixie/images, pixie/internal
import pixie/[common, images, internal]
# See: https://qoiformat.org/qoi-specification.pdf # See: https://qoiformat.org/qoi-specification.pdf
@ -15,158 +14,150 @@ const
opRun = 0b11000000'u8 opRun = 0b11000000'u8
type type
Colorspace* = enum sRBG = 0, linear = 1 Colorspace* = enum
sRBG = 0
Linear = 1
Qoi* = ref object Qoi* = ref object
## Raw QOI image data. ## Raw QOI image data.
data*: seq[ColorRGBA]
width*, height*, channels*: int width*, height*, channels*: int
colorspace*: Colorspace colorspace*: Colorspace
data*: seq[ColorRGBA]
Index = array[indexLen, ColorRGBA] Index = array[indexLen, ColorRGBA]
func hash(p: ColorRGBA): int = func hash(p: ColorRGBA): int =
(p.r.int * 3 + p.g.int * 5 + p.b.int * 7 + p.a.int * 11) mod indexLen (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`. ## Converts raw QOI data to `Image`.
result = newImage(qoi.width, qoi.height) result = newImage(qoi.width, qoi.height)
copyMem(result.data[0].addr, qoi.data[0].addr, qoi.data.len * 4) copyMem(result.data[0].addr, qoi.data[0].addr, qoi.data.len * 4)
result.data.toPremultipliedAlpha() result.data.toPremultipliedAlpha()
func toQoi*(img: Image; channels: range[3..4]): Qoi = proc decodeQoiRaw*(data: string): Qoi {.raises: [PixieError].} =
## 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].} =
## Decompress QOI file format data. ## Decompress QOI file format data.
if data.len <= 14 or data[0 .. 3] != qoiSignature: if data.len <= 14 or data[0 .. 3] != qoiSignature:
raise newException(PixieError, "Invalid QOI header") raise newException(PixieError, "Invalid QOI header")
var
width, height: uint32 let
channels, colorspace: uint8 width = data.readUint32(4).swap()
block: height = data.readUint32(8).swap()
when cpuEndian == bigEndian: channels = data.readUint8(12)
width = data.readUint32(4) colorspace = data.readUint8(13)
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)
if channels notin {3, 4} or colorspace notin {0, 1}: if channels notin {3, 4} or colorspace notin {0, 1}:
raise newException(PixieError, "Invalid QOI header") raise newException(PixieError, "Invalid QOI header")
if width.int * height.int > uint32.high.int: if width.int * height.int > uint32.high.int:
raise newException(PixieError, "QOI is too large to decode") raise newException(PixieError, "QOI is too large to decode")
result = Qoi( result = Qoi()
data: newSeq[ColorRGBA](int width * height), result.width = width.int
width: int width, result.height = height.int
height: int height, result.channels = channels.int
channels: int channels, result.colorspace = colorspace.Colorspace
colorspace: Colorspace colorspace) result.data.setLen(result.width * result.height)
var var
index: Index index: Index
p = 14 p = 14
run: uint8 run: uint8
px = rgba(0, 0, 0, 0xff) px = rgba(0, 0, 0, 255)
for dst in result.data.mitems: for dst in result.data.mitems:
if p > data.len-8: if p > data.len - 8:
raise newException(PixieError, "Underrun of QOI decoder") raise newException(PixieError, "Underrun of QOI decoder")
if run > 0: if run > 0:
dec(run) dec run
else: else:
let b0 = data.readUint8(p) let b0 = data.readUint8(p)
inc(p) inc p
case b0
case b0:
of opRgb: of opRgb:
px.r = data.readUint8(p+0) px.r = data.readUint8(p + 0)
px.g = data.readUint8(p+1) px.g = data.readUint8(p + 1)
px.b = data.readUint8(p+2) px.b = data.readUint8(p + 2)
inc(p, 3) p += 3
of opRgba: of opRgba:
px.r = data.readUint8(p+0) px.r = data.readUint8(p + 0)
px.g = data.readUint8(p+1) px.g = data.readUint8(p + 1)
px.b = data.readUint8(p+2) px.b = data.readUint8(p + 2)
px.a = data.readUint8(p+3) px.a = data.readUint8(p + 3)
inc(p, 4) p += 4
else: else:
case b0 and opMask2 case b0 and opMask2:
of opIndex: of opIndex:
px = index[b0] px = index[b0]
of opDiff: of opDiff:
px.r = px.r + uint8((b0 shr 4) and 0x03) - 2 px.r = px.r + ((b0 shr 4) and 0x03).uint8 - 2
px.g = px.g + uint8((b0 shr 2) and 0x03) - 2 px.g = px.g + ((b0 shr 2) and 0x03).uint8 - 2
px.b = px.b + uint8((b0 shr 0) and 0x03) - 2 px.b = px.b + ((b0 shr 0) and 0x03).uint8 - 2
of opLuma: of opLuma:
let b1 = data.readUint8(p) let
inc(p) b1 = data.readUint8(p)
let vg = (b0.uint8 and 0x3f) - 32 vg = (b0.uint8 and 0x3f) - 32
px.r = px.r + vg - 8 + ((b1 shr 4) and 0x0f) px.r = px.r + vg - 8 + ((b1 shr 4) and 0x0f)
px.g = px.g + vg px.g = px.g + vg
px.b = px.b + vg - 8 + ((b1 shr 0) and 0x0f) px.b = px.b + vg - 8 + ((b1 shr 0) and 0x0f)
inc p
of opRun: of opRun:
run = b0 and 0x3f run = b0 and 0x3f
else: assert false else:
raise newException(PixieError, "Unexpected QOI op")
index[hash(px)] = px index[hash(px)] = px
dst = px dst = px
while p < data.len: while p < data.len:
case data[p] case data[p]:
of '\0': discard of '\0':
of '\1': break # ignore trailing data discard
of '\1':
break # ignore trailing data
else: else:
raise newException(PixieError, "Invalid QOI padding") raise newException(PixieError, "Invalid QOI padding")
inc(p) inc(p)
proc decodeQoi*(data: string): Image {.raises: [PixieError].} = proc decodeQoi*(data: string): Image {.raises: [PixieError].} =
## Decodes data in the QOI file format to an `Image`. ## Decodes data in the QOI file format to an `Image`.
decompressQoi(data).toImage() newImage(decodeQoiRaw(data))
proc decodeQoi*(data: seq[uint8]): Image {.inline, raises: [PixieError].} = proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} =
## Decodes data in the QOI file format to an `Image`.
decodeQoi(cast[string](data))
proc compressQoi*(qoi: Qoi): string =
## Encodes raw QOI pixels to the QOI file format. ## 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) result = newStringOfCap(14 + 8 + qoi.data.len * 3)
# allocate a buffer 3/4 the size of the pathological encoding
result.add(qoiSignature) result.add(qoiSignature)
when cpuEndian == bigEndian: result.addUint32(qoi.width.uint32.swap())
result.addUint32(uint32 qoi.width) result.addUint32(qoi.height.uint32.swap())
result.addUint32(uint32 qoi.height) result.addUint8(qoi.channels.uint8)
else: result.addUint8(qoi.colorspace.uint8)
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)
var var
index: Index index: Index
run: uint8 run: uint8
pxPrev = rgba(0, 0, 0, 0xff) pxPrev = rgba(0, 0, 0, 255)
for off, px in qoi.data: for off, px in qoi.data:
if px == pxPrev: if px == pxPrev:
inc run inc run
if run == 62 or off == qoi.data.high: if run == 62 or off == qoi.data.high:
result.addUint8(opRun or pred(run)) result.addUint8(opRun or (run - 1))
reset run run = 0
else: else:
if run > 0: if run > 0:
result.addUint8(opRun or pred(run)) result.addUint8(opRun or (run - 1))
reset run run = 0
let i = hash(px) 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: else:
index[i] = px index[i] = px
if px.a == pxPrev.a: if px.a == pxPrev.a:
@ -179,16 +170,14 @@ proc compressQoi*(qoi: Qoi): string =
if (vr > -3) and (vr < 2) and if (vr > -3) and (vr < 2) and
(vg > -3) and (vg < 2) and (vg > -3) and (vg < 2) and
(vb > -3) and (vb < 2): (vb > -3) and (vb < 2):
let b = opDiff or uint8( let b = opDiff or
((vr + 2) shl 4) or (((vr + 2) shl 4) or ((vg + 2) shl 2) or ((vb + 2) shl 0)).uint8
((vg + 2) shl 2) or
((vb + 2) shl 0))
result.addUint8(b) result.addUint8(b)
elif vgr > -9 and vgr < 8 and elif vgr > -9 and vgr < 8 and
vg > -33 and vg < 32 and vg > -33 and vg < 32 and
vgb > -9 and vgb < 8: vgb > -9 and vgb < 8:
result.addUint8(opLuma or uint8(vg + 32)) result.addUint8(opLuma or (vg + 32).uint8)
result.addUint8(uint8 ((vgr + 8) shl 4) or (vgb + 8)) result.addUint8((((vgr + 8) shl 4) or (vgb + 8)).uint8)
else: else:
result.addUint8(opRgb) result.addUint8(opRgb)
result.addUint8(px.r) result.addUint8(px.r)
@ -200,10 +189,23 @@ proc compressQoi*(qoi: Qoi): string =
result.addUint8(px.g) result.addUint8(px.g)
result.addUint8(px.b) result.addUint8(px.b)
result.addUint8(px.a) result.addUint8(px.a)
pxPrev = px pxPrev = px
for _ in 0..6: result.addUint8(0x00)
for _ in 0 .. 6:
result.addUint8(0x00)
result.addUint8(0x01) result.addUint8(0x01)
proc encodeQoi*(img: Image): string {.raises: [].} = proc encodeQoi*(image: Image): string {.raises: [PixieError].} =
## Encodes an image to the QOI file format. ## 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)

View file

@ -838,11 +838,14 @@ proc drawUber(
when type(b) is Image: when type(b) is Image:
for q in [0, 4, 8, 12]: for q in [0, 4, 8, 12]:
let sourceVec = mm_loadu_si128(b.data[b.dataIndex(sx + q, sy)].addr) let sourceVec = mm_loadu_si128(b.data[b.dataIndex(sx + q, sy)].addr)
if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, mm_setzero_si128())) != 0xffff: if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec,
if (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and 0x8888) == 0x8888: mm_setzero_si128())) != 0xffff:
if (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and
0x8888) == 0x8888:
mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr, sourceVec) mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr, sourceVec)
else: else:
let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x + q, y)].addr) let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x +
q, y)].addr)
mm_storeu_si128( mm_storeu_si128(
a.data[a.dataIndex(x + q, y)].addr, a.data[a.dataIndex(x + q, y)].addr,
blendNormalInlineSimd(backdropVec, sourceVec) blendNormalInlineSimd(backdropVec, sourceVec)
@ -851,11 +854,14 @@ proc drawUber(
var values = mm_loadu_si128(b.data[b.dataIndex(sx, sy)].addr) var values = mm_loadu_si128(b.data[b.dataIndex(sx, sy)].addr)
for q in [0, 4, 8, 12]: for q in [0, 4, 8, 12]:
let sourceVec = unpackAlphaValues(values) let sourceVec = unpackAlphaValues(values)
if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, mm_setzero_si128())) != 0xffff: if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec,
if (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and 0x8888) == 0x8888: mm_setzero_si128())) != 0xffff:
if (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and
0x8888) == 0x8888:
discard discard
else: else:
let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x + q, y)].addr) let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x +
q, y)].addr)
mm_storeu_si128( mm_storeu_si128(
a.data[a.dataIndex(x + q, y)].addr, a.data[a.dataIndex(x + q, y)].addr,
blendNormalInlineSimd(backdropVec, sourceVec) blendNormalInlineSimd(backdropVec, sourceVec)
@ -886,8 +892,10 @@ proc drawUber(
when type(b) is Image: when type(b) is Image:
for q in [0, 4, 8, 12]: for q in [0, 4, 8, 12]:
let sourceVec = mm_loadu_si128(b.data[b.dataIndex(sx + q, sy)].addr) let sourceVec = mm_loadu_si128(b.data[b.dataIndex(sx + q, sy)].addr)
if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, mm_setzero_si128())) == 0xffff: if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec,
mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr, mm_setzero_si128()) mm_setzero_si128())) == 0xffff:
mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr,
mm_setzero_si128())
elif mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) != 0xffff: elif mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) != 0xffff:
let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x + q, y)].addr) let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x + q, y)].addr)
mm_storeu_si128( mm_storeu_si128(
@ -898,9 +906,12 @@ proc drawUber(
var values = mm_loadu_si128(b.data[b.dataIndex(sx, sy)].addr) var values = mm_loadu_si128(b.data[b.dataIndex(sx, sy)].addr)
for q in [0, 4, 8, 12]: for q in [0, 4, 8, 12]:
let sourceVec = unpackAlphaValues(values) let sourceVec = unpackAlphaValues(values)
if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, mm_setzero_si128())) == 0xffff: if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec,
mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr, mm_setzero_si128()) mm_setzero_si128())) == 0xffff:
elif (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and 0x8888) != 0x8888: 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) let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x + q, y)].addr)
mm_storeu_si128( mm_storeu_si128(
a.data[a.dataIndex(x + q, y)].addr, a.data[a.dataIndex(x + q, y)].addr,

View file

@ -3,4 +3,4 @@ import benchy, pixie/fileformats/qoi
let data = readFile("tests/fileformats/qoi/testcard_rgba.qoi") let data = readFile("tests/fileformats/qoi/testcard_rgba.qoi")
timeIt "pixie decode": timeIt "pixie decode":
keep decodeQOI(data) keep decodeQoi(data)

View file

@ -1,4 +1,4 @@
import random, pixie import pixie, random
randomize() randomize()

View file

@ -1,5 +1,4 @@
import std/[random, strformat] import pixie/common, pixie/fileformats/qoi, std/random, strformat
import pixie/[common, fileformats/qoi]
randomize() randomize()
@ -9,17 +8,18 @@ for i in 0 ..< 10_000:
var data = original var data = original
let let
pos = rand(data.len) pos = rand(data.len)
value = rand(255).char value = rand(255).uint8
data[pos] = value data[pos] = value.char
echo &"{i} {pos} {value}"
try: try:
let img = decodeQOI(data) let img = decodeQoi(data)
doAssert img.height > 0 and img.width > 0 doAssert img.height > 0 and img.width > 0
except PixieError: except PixieError:
discard discard
data = data[0 ..< pos] data = data[0 ..< pos]
try: try:
let img = decodeQOI(data) let img = decodeQoi(data)
doAssert img.height > 0 and img.width > 0 doAssert img.height > 0 and img.width > 0
except PixieError: except PixieError:
discard discard

View file

@ -1,15 +1,18 @@
import pixie, pixie/fileformats/qoi, pixie/fileformats/png import pixie, pixie/fileformats/png, pixie/fileformats/qoi
const tests = ["testcard", "testcard_rgba"] const tests = ["testcard", "testcard_rgba"]
for name in tests: for name in tests:
var input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi")) let
var control = decodePng(readFile("tests/fileformats/qoi/" & name & ".png")) input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
doAssert(input.data == control.data, "input mismatch of " & name) control = decodePng(readFile("tests/fileformats/qoi/" & name & ".png"))
doAssert input.data == control.data, "input mismatch of " & name
decodeQoi(control.encodeQoi()).writeFile("tmp.png")
for name in tests: for name in tests:
var let
input: Qoi = decompressQoi(readFile("tests/fileformats/qoi/" & name & ".qoi")) input = decodeQoiRaw(readFile("tests/fileformats/qoi/" & name & ".qoi"))
output: Qoi = decompressQoi(compressQoi(input)) output = decodeQoiRaw(encodeQoi(input))
doAssert(output.data.len == input.data.len) doAssert output.data.len == input.data.len
doAssert(output.data == input.data) doAssert output.data == input.data