Here is my PureBasic implementation of the QOI format encode/decode algorithm.
I wrote it in a 32-bit version of PB, but I don't think there will be any problem running it in 64-bit, as long as you don't use image files with more than 2GB of data.
Initially, I wrote PB code to match the C code as closely as possible. Then I went back and did some optimization where I thought it would make the code faster. Let me know If you have any suggestions (be kind please
).
The test stub at the end of the code converts between BMP and QOI format image files.
Code: Select all
; Structure qoi_header_t
; Magic.s{4} ; magic bytes "qoif"
; Width.l ; image width in pixels (BE)
; Height.l ; image height in pixels (BE)
; channels.a ; must be 3 (RGB) or 4 (RGBA)
; colorspace.a ; a bitmap 0000rgba where
; ; a zero bit indicates sRGBA,
; ; a one bit indicates linear (user interpreted)
; ; colorspace for each channel
; EndStructure
#QOI_SRGB = $00
#QOI_SRGB_LINEAR_ALPHA = $01
#QOI_LINEAR = $0F
Structure qoi_desc
Width.l
Height.l
channels.a
colorspace.a
EndStructure
Structure rgba
r.a
g.a
B.a
a.a
EndStructure
Structure qoi_rgba_t
StructureUnion
_rgba.rgba
v.l
EndStructureUnion
EndStructure
Macro QOI_MALLOC(sz)
AllocateMemory(sz)
EndMacro
Macro QOI_FREE(p)
FreeMemory(p)
EndMacro
Macro QOI_COLOR_HASH(c)
(Red(c) ! Green(c) ! Blue(c) ! Alpha(c))
EndMacro
#QOI_INDEX = %00000000
#QOI_RUN_8 = %01000000
#QOI_RUN_16 = %01100000
#QOI_DIFF_8 = %10000000
#QOI_DIFF_16 = %11000000
#QOI_DIFF_24 = %11100000
#QOI_COLOR = %11110000
#QOI_MASK_2 = %11000000
#QOI_MASK_3 = %11100000
#QOI_MASK_4 = %11110000
#QOI_MAGIC = 'q' << 24 | 'o' << 16 | 'i' << 8 | 'f'
#QOI_HEADER_SIZE = 14
#QOI_PADDING = 4
Procedure qoi_write_32(*Bytes, *p, v.l)
; retrieve/update pointer
p = PeekL(*p)
PokeL(*p, p + 4)
p + *Bytes
PokeB(p + 0, ($FF000000 & v) >> 24)
PokeB(p + 1, ($00FF0000 & v) >> 16)
PokeB(p + 2, ($0000FF00 & v) >> 8)
PokeB(p + 3, ($000000FF & v))
EndProcedure
Procedure qoi_read_32(*Bytes, *p)
; retrieve/update pointer
p = PeekL(*p)
PokeL(*p, p + 4)
p + *Bytes
a.a = PeekB(p + 0)
B.a = PeekB(p + 1)
c.a = PeekB(p + 2)
d.a = PeekB(p + 3)
ProcedureReturn (a << 24) | (B << 16) | (c << 8) | d
EndProcedure
Procedure.l qoi_decode(*Data, Size.l, *desc.qoi_desc, channels.l)
If *Data = 0 Or *desc = 0 Or (channels <> 0 And channels <> 3 And channels <> 4) Or Size < #QOI_HEADER_SIZE + #QOI_PADDING
ProcedureReturn 0
EndIf
p.l = 0
header_magic.l = qoi_read_32(*Data, @p)
*desc\Width = qoi_read_32(*Data, @p)
*desc\Height = qoi_read_32(*Data, @p)
*desc\channels = PeekB(*Data + p)
p + 1
*desc\colorspace = PeekB(*Data + p)
p + 1
If *desc\Width = 0 Or *desc\Height = 0 Or *desc\channels < 3 Or *desc\channels > 4 Or header_magic <> #QOI_MAGIC
ProcedureReturn 0
EndIf
If channels = 0
channels = *desc\channels
EndIf
px_len.l = *desc\Width * *desc\Height * channels
*pixels = QOI_MALLOC(px_len)
If *pixels = 0
ProcedureReturn 0
EndIf
px.qoi_rgba_t
px\_rgba\a = 255
Dim index.qoi_rgba_t(63)
run.l = 0
chunks_len.l = Size - #QOI_PADDING
px_pos.l = 0
While px_pos < px_len
If run > 0
run - 1
ElseIf p < chunks_len
b1.a = PeekB(*Data + p)
p + 1
If (b1 & #QOI_MASK_2) = #QOI_INDEX
px\v = index(b1 ! #QOI_INDEX)\v
ElseIf (b1 & #QOI_MASK_3) = #QOI_RUN_8
run = b1 & $1F
ElseIf (b1 & #QOI_MASK_3) = #QOI_RUN_16
b2.a = PeekB(*Data + p)
p + 1
run = ((b1 & $1F) << 8 | b2) + 32
ElseIf (b1 & #QOI_MASK_2) = #QOI_DIFF_8
px\_rgba\r + ((b1 >> 4) & $03) - 2
px\_rgba\g + ((b1 >> 2) & $03) - 2
px\_rgba\B + ( b1 & $03) - 2
ElseIf (b1 & #QOI_MASK_3) = #QOI_DIFF_16
b2.a = PeekB(*Data + p)
p + 1
px\_rgba\r + (b1 & $1F) - 16
px\_rgba\g + (b2 >> 4) - 8
px\_rgba\B + (b2 & $0F) - 8
ElseIf (b1 & #QOI_MASK_4) = #QOI_DIFF_24
b2.a = PeekB(*Data + p)
p + 1
b3.a = PeekB(*Data + p)
p + 1
px\_rgba\r + (((b1 & $0F) << 1) | (b2 >> 7)) - 16
px\_rgba\g + ((b2 & $7C) >> 2) - 16
px\_rgba\B + (((b2 & $03) << 3) | ((b3 & $E0) >> 5)) - 16
px\_rgba\a + (b3 & $1F) - 16
ElseIf (b1 & #QOI_MASK_4) = #QOI_COLOR
If b1 & 8
px\_rgba\r = PeekB(*Data + p)
p + 1
EndIf
If b1 & 4
px\_rgba\g = PeekB(*Data + p)
p + 1
EndIf
If b1 & 2
px\_rgba\B = PeekB(*Data + p)
p + 1
EndIf
If b1 & 1
px\_rgba\a = PeekB(*Data + p)
p + 1
EndIf
EndIf
index(QOI_COLOR_HASH(px) % 64)\v = px\v
EndIf
PokeB(*pixels + px_pos, px\_rgba\r)
PokeB(*pixels + px_pos + 1, px\_rgba\g)
PokeB(*pixels + px_pos + 2, px\_rgba\B)
If channels = 4
PokeB(*pixels + px_pos + 3, px\_rgba\a)
EndIf
px_pos + channels
Wend
ProcedureReturn *pixels
EndProcedure
Procedure.l qoi_encode(*Data, *desc.qoi_desc, *out_len)
If *Data = 0 Or *out_len = 0 Or *desc = 0 Or *desc\Width = 0 Or *desc\Height = 0 Or *desc\channels < 3 Or *desc\channels > 4 Or (*desc\colorspace & $F0) <> 0
ProcedureReturn 0
EndIf
max_size.l = *desc\Width * *desc\Height * (*desc\channels + 1) + #QOI_HEADER_SIZE + #QOI_PADDING
p.l = 0
*Bytes = QOI_MALLOC(max_size)
If *Bytes = 0
ProcedureReturn 0
EndIf
qoi_write_32(*Bytes, @p, #QOI_MAGIC)
qoi_write_32(*Bytes, @p, *desc\Width)
qoi_write_32(*Bytes, @p, *desc\Height)
PokeB(*Bytes + p, *desc\channels)
p + 1
PokeB(*Bytes + p, *desc\colorspace)
p + 1
Dim index.qoi_rgba_t(63)
run.l = 0
px_prev.qoi_rgba_t
px_prev\_rgba\a = 255
px.qoi_rgba_t = px_prev
px_len.l = *desc\Width * *desc\Height * *desc\channels
px_end.l = px_len - *desc\channels
channels.l = *desc\channels
px_pos.l = 0
While px_pos < px_len
Data_px.l = *Data + px_pos
With px
\_rgba\r = PeekB(Data_px + 0)
\_rgba\g = PeekB(Data_px + 1)
\_rgba\B = PeekB(Data_px + 2)
If channels = 4
\_rgba\a = PeekB(Data_px + 3)
EndIf
EndWith
If px\v = px_prev\v
run + 1
EndIf
If run > 0 And (run = $2020 Or px\v <> px_prev\v Or px_pos = px_end)
If run < 33
run - 1
PokeB(*Bytes + p, #QOI_RUN_8 | run)
p + 1
Else
run - 33
PokeB(*Bytes + p, #QOI_RUN_16 | run >> 8)
p + 1
PokeB(*Bytes + p, run)
p + 1
EndIf
run = 0
EndIf
If px\v <> px_prev\v
index_pos.l = QOI_COLOR_HASH(px) % 64
If index(index_pos)\v = px\v
PokeB(*Bytes + p, #QOI_INDEX | index_pos)
p + 1
Else
index(index_pos)\v = px\v
vr.l = px\_rgba\r - px_prev\_rgba\r
vg.l = px\_rgba\g - px_prev\_rgba\g
vb.l = px\_rgba\B - px_prev\_rgba\B
va.l = px\_rgba\a - px_prev\_rgba\a
If vr > -17 And vr < 16 And vg > -17 And vg < 16 And vb > -17 And vb < 16 And va > -17 And va < 16
If va = 0 And vr > -3 And vr < 2 And vg > -3 And vg < 2 And vb > -3 And vb < 2
PokeB(*Bytes + p, #QOI_DIFF_8 | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2))
p + 1
ElseIf va = 0 And vr > -17 And vr < 16 And vg > -9 And vg < 8 And vb > -9 And vb < 8
PokeB(*Bytes + p, #QOI_DIFF_16 | (vr + 16))
p + 1
PokeB(*Bytes + p, (vg + 8) << 4 | (vb + 8))
p + 1
Else
PokeB(*Bytes + p, #QOI_DIFF_24 | (vr + 16) >> 1)
p + 1
PokeB(*Bytes + p, (vr + 16) << 7 | (vg + 16) << 2 | (vb + 16) >> 3)
p + 1
PokeB(*Bytes + p, (vb + 16) << 5 | (va + 16))
p + 1
EndIf
Else
vv.l = 0
; save pointer for color byte
p_color.l = p
p + 1
If vr
vv | 8
PokeB(*Bytes + p, px\_rgba\r)
p + 1
EndIf
If vg
vv | 4
PokeB(*Bytes + p, px\_rgba\g)
p + 1
EndIf
If vb
vv | 2
PokeB(*Bytes + p, px\_rgba\B)
p + 1
EndIf
If va
vv | 1
PokeB(*Bytes + p, px\_rgba\a)
p + 1
EndIf
PokeB(*Bytes + p_color, #QOI_COLOR | vv)
EndIf
EndIf
EndIf
px_prev\v = px\v
px_pos + channels
Wend
For i.l = 0 To #QOI_PADDING - 1
PokeB(*Bytes + p, 0)
p + 1
Next
PokeL(*out_len, p)
ProcedureReturn *Bytes
EndProcedure
Procedure.l qoi_write(Filename.s, *Data, *desc.qoi_desc)
hFile.l = CreateFile(#PB_Any, Filename)
If hFile = 0
ProcedureReturn 0
EndIf
Size.l
*encoded = qoi_encode(*Data, *desc, @Size)
If *encoded = 0
CloseFile(hFile)
ProcedureReturn 0
EndIf
WriteData(hFile, *encoded, Size)
CloseFile(hFile)
QOI_FREE(*encoded)
ProcedureReturn Size
EndProcedure
Procedure.l qoi_read(Filename.s, *desc.qoi_desc, channels.l)
hFile.l = OpenFile(#PB_Any, Filename)
If hFile = 0
ProcedureReturn 0
EndIf
Size.l = Lof(hFile)
*Data = QOI_MALLOC(Size)
If *Data = 0
ProcedureReturn 0
EndIf
bytes_read.l = ReadData(hFile, *Data, Size)
CloseFile(hFile)
*pixels = qoi_decode(*Data, bytes_read, *desc, channels)
QOI_FREE(*Data)
ProcedureReturn *pixels
EndProcedure
;- test stub to convert between BMP and QOI format
CompilerIf #PB_Compiler_IsMainFile
Filename.s = OpenFileRequester("Select Image File", "C:\", "Bitmap Files (*.bmp)|*.bmp|QOI Files (*.qoi)|*.qoi", 0)
If Filename
Select UCase(GetExtensionPart(Filename))
Case "BMP"
hImage.l = LoadImage(#PB_Any, Filename)
If hImage
StartDrawing(ImageOutput(hImage))
*ImageAddress = DrawingBuffer()
desc.qoi_desc
With desc
\Width = ImageWidth(hImage)
\Height = ImageHeight(hImage)
Select ImageDepth(hImage)
Case 24
\channels = 3
Case 32
\channels = 4
EndSelect
\colorspace = #QOI_SRGB
EndWith
Size.l = qoi_write(Left(Filename, Len(Filename) - 4) + "_Q.qoi", *ImageAddress, @desc)
StopDrawing()
If Size = 0
MessageRequester("Error!", "Couldn't encode/write!", #MB_ICONERROR)
EndIf
EndIf
Case "QOI"
*pixels = qoi_read(Filename, @desc.qoi_desc, 0)
If *pixels = 0
MessageRequester("Error!", "Couldn't read/decode!", #MB_ICONERROR)
Else
channels.l = desc\channels
Select channels
Case 3
depth.l = 24
Case 4
depth.l = 32
EndSelect
W.l = desc\Width
H.l = desc\Height
hImage.l = CreateImage(#PB_Any, W, H, depth)
If hImage
StartDrawing(ImageOutput(hImage))
*ImageAddress = DrawingBuffer()
CopyMemory(*pixels, *ImageAddress, W * H * channels)
StopDrawing()
SaveImage(hImage, Left(Filename, Len(Filename) - 3) + "bmp")
EndIf
QOI_FREE(*pixels)
EndIf
EndSelect
EndIf
CompilerEndIf