Page 1 of 3

Quick OK Image format

Posted: Wed Dec 01, 2021 5:07 am
by Tenaja
This looks like an excellent image format that some of you might enjoy. It boasts 20-50x faster compression than PNG, and 3-4x faster decompression. Worth looking into, if you need faster images.

https://phoboslab.org/log/2021/11/qoi-f ... ompression

Re: Quick OK Image format

Posted: Wed Dec 01, 2021 7:09 am
by wilbert
Thanks Tenaja.
That's a very simple and clever way to encode / decode a 24 or 32 bit image. :)

Re: Quick OK Image format

Posted: Wed Dec 01, 2021 5:46 pm
by Fred
Interesting read

Re: Quick OK Image format

Posted: Wed Dec 01, 2021 5:53 pm
by #NULL
The thread title says 'quick' instead of 'quite'.

Re: Quick OK Image format

Posted: Wed Dec 01, 2021 6:19 pm
by Tenaja
He titled it quite ok, and I read in his benchmark that it was quick...

Re: Quick OK Image format

Posted: Wed Dec 01, 2021 6:33 pm
by NicTheQuick
There is also Lossless JPEG which is quite easy to understand too. Would be interesting to also check how fast it is against QOI.

Re: Quick OK Image format

Posted: Wed Dec 01, 2021 9:14 pm
by Tenaja
One of the points of the blog post is to bring attention to the bloat that comes along with a committee-driven format.

Re: Quick OK Image format

Posted: Wed Dec 08, 2021 6:05 pm
by ebs
I'm working on a PureBasic implementation of the QOI algorithm.
Is anybody interested? If so, I'll post it when I'm done.

Re: Quick OK Image format

Posted: Wed Dec 08, 2021 6:58 pm
by Tenaja
ebs wrote: Wed Dec 08, 2021 6:05 pm I'm working on a PureBasic implementation of the QOI algorithm.
Is anybody interested? If so, I'll post it when I'm done.
I would appreciate adding it to my library--thanks in advance!

Re: Quick OK Image format

Posted: Wed Dec 08, 2021 7:03 pm
by acreis
I'm working on a PureBasic implementation of the QOI algorithm.
Is anybody interested? If so, I'll post it when I'm done.
Would be great!

Re: Quick OK Image format

Posted: Wed Dec 08, 2021 11:20 pm
by GoodNPlenty
I'm working on a PureBasic implementation of the QOI algorithm.
Is anybody interested? If so, I'll post it when I'm done.
That would be helpful to have in my library. Thank You!

Re: Quick OK Image format

Posted: Thu Dec 09, 2021 1:01 am
by BarryG
Is there a binary to test the compression of our own images? I can only see source code at that site?

Re: Quick OK Image format

Posted: Thu Dec 09, 2021 2:58 pm
by ebs
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 :wink: ).

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

Re: Quick OK Image format

Posted: Thu Dec 09, 2021 6:20 pm
by wilbert
A little warning ...
The website mentions
the file format is not yet finalized. We're still working to fix some smaller issues. The final specification will be announced on 2021.12.20.

Re: Quick OK Image format

Posted: Thu Dec 09, 2021 6:43 pm
by Tenaja
Yes, but more importantly... Does it work for your project. The whole point of it is to free your images from a bloated & slow "standardized file format".