-
Notifications
You must be signed in to change notification settings - Fork 32
qoi consistent style pass #356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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].} = | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ## 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: | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. had to look this up too, not worth it |
||
| reset run | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
|
@@ -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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import random, pixie | ||
| import pixie, random | ||
|
|
||
| randomize() | ||
|
|
||
|
|
||
| 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() | ||
|
|
||
|
|
@@ -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}" | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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 |
There was a problem hiding this comment.
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)