From 91abc684ad30bba0e22dd84ce2b2cb64d91145a1 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Mon, 3 Jan 2022 17:41:36 -0600 Subject: [PATCH] qoi consistent style pass --- README.md | 1 + src/pixie.nim | 3 +- src/pixie/fileformats/qoi.nim | 190 +++++++++++++++++----------------- src/pixie/images.nim | 33 ++++-- tests/benchmark_qoi.nim | 2 +- tests/fuzz_image_draw.nim | 2 +- tests/fuzz_qoi.nim | 12 +-- tests/test_qoi.nim | 21 ++-- 8 files changed, 140 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 9968626..32b57ce 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Format | Read | Write | PNG | ✅ | ✅ | JPEG | ✅ | | BMP | ✅ | ✅ | +QOI | ✅ | ✅ | GIF | ✅ | | SVG | ✅ | | diff --git a/src/pixie.nim b/src/pixie.nim index 7afede4..6af2bc6 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -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 diff --git a/src/pixie/fileformats/qoi.nim b/src/pixie/fileformats/qoi.nim index 12a6498..6a7d24d 100644 --- a/src/pixie/fileformats/qoi.nim +++ b/src/pixie/fileformats/qoi.nim @@ -1,5 +1,4 @@ -import std/endians, chroma, flatty/binny -import pixie/[common, images, internal] +import chroma, flatty/binny, pixie/common, pixie/images, pixie/internal # See: https://qoiformat.org/qoi-specification.pdf @@ -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 = - ## 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: - 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, - 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) + 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 + 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() + newImage(decodeQoiRaw(data)) -proc decodeQoi*(data: seq[uint8]): Image {.inline, raises: [PixieError].} = - ## Decodes data in the QOI file format to an `Image`. - decodeQoi(cast[string](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: - 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)) - reset run + 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: @@ -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) @@ -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) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index af5972a..d76c1b3 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -838,11 +838,14 @@ proc drawUber( when type(b) is Image: for q in [0, 4, 8, 12]: 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, vec255)) and 0x8888) == 0x8888: + if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, + 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) 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( a.data[a.dataIndex(x + q, y)].addr, blendNormalInlineSimd(backdropVec, sourceVec) @@ -851,11 +854,14 @@ proc drawUber( var values = mm_loadu_si128(b.data[b.dataIndex(sx, sy)].addr) for q in [0, 4, 8, 12]: let sourceVec = unpackAlphaValues(values) - if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, mm_setzero_si128())) != 0xffff: - if (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and 0x8888) == 0x8888: + if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, + mm_setzero_si128())) != 0xffff: + if (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and + 0x8888) == 0x8888: discard 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( a.data[a.dataIndex(x + q, y)].addr, blendNormalInlineSimd(backdropVec, sourceVec) @@ -886,8 +892,10 @@ proc drawUber( when type(b) is Image: for q in [0, 4, 8, 12]: 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: - mm_storeu_si128(a.data[a.dataIndex(x + q, y)].addr, mm_setzero_si128()) + 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()) elif mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) != 0xffff: let backdropVec = mm_loadu_si128(a.data[a.dataIndex(x + q, y)].addr) mm_storeu_si128( @@ -898,9 +906,12 @@ proc drawUber( var values = mm_loadu_si128(b.data[b.dataIndex(sx, sy)].addr) 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()) - elif (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and 0x8888) != 0x8888: + 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()) + 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( a.data[a.dataIndex(x + q, y)].addr, diff --git a/tests/benchmark_qoi.nim b/tests/benchmark_qoi.nim index 616f317..105addd 100644 --- a/tests/benchmark_qoi.nim +++ b/tests/benchmark_qoi.nim @@ -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) diff --git a/tests/fuzz_image_draw.nim b/tests/fuzz_image_draw.nim index 7cbc422..0a80cd8 100644 --- a/tests/fuzz_image_draw.nim +++ b/tests/fuzz_image_draw.nim @@ -1,4 +1,4 @@ -import random, pixie +import pixie, random randomize() diff --git a/tests/fuzz_qoi.nim b/tests/fuzz_qoi.nim index 9149822..6f5ad16 100644 --- a/tests/fuzz_qoi.nim +++ b/tests/fuzz_qoi.nim @@ -1,5 +1,4 @@ -import std/[random, strformat] -import pixie/[common, fileformats/qoi] +import pixie/common, pixie/fileformats/qoi, std/random, strformat randomize() @@ -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}" 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 diff --git a/tests/test_qoi.nim b/tests/test_qoi.nim index 90ab472..b5f9f39 100644 --- a/tests/test_qoi.nim +++ b/tests/test_qoi.nim @@ -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"] 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 + + decodeQoi(control.encodeQoi()).writeFile("tmp.png") 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