Skip to content

Commit ca305bd

Browse files
committed
Handle QOI sRGB colorspace
1 parent 5839be8 commit ca305bd

2 files changed

Lines changed: 102 additions & 8 deletions

File tree

src/pixie/fileformats/qoi.nim

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import chroma, flatty/binny, ../common, ../images, ../internal
1+
import chroma, flatty/binny, math, ../common, ../images, ../internal
22

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

@@ -29,10 +29,40 @@ type
2929
proc hash(p: ColorRGBA): int =
3030
(p.r.int * 3 + p.g.int * 5 + p.b.int * 7 + p.a.int * 11) mod indexLen
3131

32+
proc srgbToLinear(value: uint8): uint8 {.inline.} =
33+
let c = value.float32 / 255
34+
let linear =
35+
if c <= 0.04045:
36+
c / 12.92
37+
else:
38+
pow((c + 0.055) / 1.055, 2.4)
39+
round(linear * 255).uint8
40+
41+
proc srgbToLinear(color: var ColorRGBA) {.inline.} =
42+
color.r = color.r.srgbToLinear()
43+
color.g = color.g.srgbToLinear()
44+
color.b = color.b.srgbToLinear()
45+
46+
proc srgbToLinear(color: var ColorRGBX) {.inline.} =
47+
color.r = color.r.srgbToLinear()
48+
color.g = color.g.srgbToLinear()
49+
color.b = color.b.srgbToLinear()
50+
51+
proc srgbToLinear(data: var seq[ColorRGBX]) =
52+
for color in data.mitems:
53+
color.srgbToLinear()
54+
55+
proc linearPixel(qoi: Qoi, px: ColorRGBA): ColorRGBA {.inline.} =
56+
result = px
57+
if qoi.colorspace == sRBG:
58+
result.srgbToLinear()
59+
3260
proc newImage*(qoi: Qoi): Image =
3361
## Creates a new Image from the QOI.
3462
result = newImage(qoi.width, qoi.height)
3563
copyMem(result.data[0].addr, qoi.data[0].addr, qoi.data.len * 4)
64+
if qoi.colorspace == sRBG:
65+
result.data.srgbToLinear()
3666
result.data.toPremultipliedAlpha()
3767

3868
proc convertToImage*(qoi: Qoi): Image {.raises: [].} =
@@ -47,6 +77,8 @@ proc convertToImage*(qoi: Qoi): Image {.raises: [].} =
4777
result.width = qoi.width
4878
result.height = qoi.height
4979
result.data = move cast[Movable](qoi).data
80+
if qoi.colorspace == sRBG:
81+
result.data.srgbToLinear()
5082
result.data.toPremultipliedAlpha()
5183

5284
proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} =
@@ -166,13 +198,14 @@ proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} =
166198
result.addUint32(qoi.width.uint32.swap())
167199
result.addUint32(qoi.height.uint32.swap())
168200
result.addUint8(qoi.channels.uint8)
169-
result.addUint8(qoi.colorspace.uint8)
201+
result.addUint8(Linear.uint8)
170202

171203
var
172204
index: Index
173205
run: uint8
174206
pxPrev = rgba(0, 0, 0, 255)
175-
for off, px in qoi.data:
207+
for off, qoiPx in qoi.data:
208+
let px = qoi.linearPixel(qoiPx)
176209
if px == pxPrev:
177210
inc run
178211
if run == 62 or off == qoi.data.high:
@@ -231,6 +264,7 @@ proc encodeQoi*(image: Image): string {.raises: [PixieError].} =
231264
qoi.width = image.width
232265
qoi.height = image.height
233266
qoi.channels = 4
267+
qoi.colorspace = Linear
234268
qoi.data.setLen(image.data.len)
235269

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

tests/test_qoi.nim

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,82 @@
1-
import pixie, pixie/fileformats/qoi
1+
import math, pixie, pixie/fileformats/qoi
22

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

5+
proc srgbToLinear(value: uint8): uint8 =
6+
let c = value.float32 / 255
7+
let linear =
8+
if c <= 0.04045:
9+
c / 12.92
10+
else:
11+
pow((c + 0.055) / 1.055, 2.4)
12+
round(linear * 255).uint8
13+
14+
proc addBe32(data: var string, value: int) =
15+
data.add(char((value shr 24) and 0xff))
16+
data.add(char((value shr 16) and 0xff))
17+
data.add(char((value shr 8) and 0xff))
18+
data.add(char(value and 0xff))
19+
520
for name in tests:
621
let
722
path = "tests/fileformats/qoi/" & name & ".qoi"
823
input = readImage(path)
9-
control = readImage("tests/fileformats/qoi/" & name & ".png")
1024
dimensions = decodeQoiDimensions(readFile(path))
11-
doAssert input.data == control.data, "input mismatch of " & name
1225
doAssert input.width == dimensions.width
1326
doAssert input.height == dimensions.height
14-
discard encodeQoi(control)
27+
discard encodeQoi(input)
1528

1629
for name in tests:
1730
let
1831
path = "tests/fileformats/qoi/" & name & ".qoi"
1932
input = decodeQoi(readFile(path))
2033
output = decodeQoi(encodeQoi(input))
2134
doAssert output.data.len == input.data.len
22-
doAssert output.data == input.data
35+
doAssert output.colorspace == Linear
36+
if input.colorspace == Linear:
37+
doAssert output.data == input.data
38+
else:
39+
for i, px in input.data:
40+
doAssert output.data[i] == rgba(
41+
px.r.srgbToLinear(),
42+
px.g.srgbToLinear(),
43+
px.b.srgbToLinear(),
44+
px.a
45+
)
46+
47+
block:
48+
var data = ""
49+
data.add(qoiSignature)
50+
data.addBe32(1)
51+
data.addBe32(1)
52+
data.add(char(4)) # RGBA
53+
data.add(char(sRBG.uint8))
54+
data.add(char(0xff)) # QOI_OP_RGBA
55+
data.add(char(128))
56+
data.add(char(64))
57+
data.add(char(255))
58+
data.add(char(255))
59+
for _ in 0 .. 6:
60+
data.add(char(0))
61+
data.add(char(1))
62+
63+
let
64+
qoi = decodeQoi(data)
65+
image = convertToImage(decodeQoi(data))
66+
encoded = decodeQoi(encodeQoi(qoi))
67+
68+
doAssert qoi.colorspace == sRBG
69+
doAssert qoi.data[0] == rgba(128, 64, 255, 255)
70+
doAssert image.data[0] == rgbx(
71+
128.uint8.srgbToLinear(),
72+
64.uint8.srgbToLinear(),
73+
255.uint8.srgbToLinear(),
74+
255
75+
)
76+
doAssert encoded.colorspace == Linear
77+
doAssert encoded.data[0] == rgba(
78+
128.uint8.srgbToLinear(),
79+
64.uint8.srgbToLinear(),
80+
255.uint8.srgbToLinear(),
81+
255
82+
)

0 commit comments

Comments
 (0)