Quick OK Image format

Everything else that doesn't fall into one of the other PB categories.
User avatar
Tenaja
Addict
Addict
Posts: 1948
Joined: Tue Nov 09, 2010 10:15 pm

Quick OK Image format

Post 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
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Quick OK Image format

Post by wilbert »

Thanks Tenaja.
That's a very simple and clever way to encode / decode a 24 or 32 bit image. :)
Windows (x64)
Raspberry Pi OS (Arm64)
Fred
Administrator
Administrator
Posts: 16617
Joined: Fri May 17, 2002 4:39 pm
Location: France
Contact:

Re: Quick OK Image format

Post by Fred »

Interesting read
#NULL
Addict
Addict
Posts: 1440
Joined: Thu Aug 30, 2007 11:54 pm
Location: right here

Re: Quick OK Image format

Post by #NULL »

The thread title says 'quick' instead of 'quite'.
User avatar
Tenaja
Addict
Addict
Posts: 1948
Joined: Tue Nov 09, 2010 10:15 pm

Re: Quick OK Image format

Post by Tenaja »

He titled it quite ok, and I read in his benchmark that it was quick...
User avatar
NicTheQuick
Addict
Addict
Posts: 1224
Joined: Sun Jun 22, 2003 7:43 pm
Location: Germany, Saarbrücken
Contact:

Re: Quick OK Image format

Post 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.
The english grammar is freeware, you can use it freely - But it's not Open Source, i.e. you can not change it or publish it in altered way.
User avatar
Tenaja
Addict
Addict
Posts: 1948
Joined: Tue Nov 09, 2010 10:15 pm

Re: Quick OK Image format

Post 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.
ebs
Enthusiast
Enthusiast
Posts: 530
Joined: Fri Apr 25, 2003 11:08 pm

Re: Quick OK Image format

Post 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.
User avatar
Tenaja
Addict
Addict
Posts: 1948
Joined: Tue Nov 09, 2010 10:15 pm

Re: Quick OK Image format

Post 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!
acreis
Enthusiast
Enthusiast
Posts: 182
Joined: Fri Jun 01, 2012 12:20 am

Re: Quick OK Image format

Post 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!
GoodNPlenty
Enthusiast
Enthusiast
Posts: 107
Joined: Wed May 13, 2009 8:38 am
Location: Arizona, USA

Re: Quick OK Image format

Post 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!
BarryG
Addict
Addict
Posts: 3292
Joined: Thu Apr 18, 2019 8:17 am

Re: Quick OK Image format

Post by BarryG »

Is there a binary to test the compression of our own images? I can only see source code at that site?
ebs
Enthusiast
Enthusiast
Posts: 530
Joined: Fri Apr 25, 2003 11:08 pm

Re: Quick OK Image format

Post 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
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Quick OK Image format

Post 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.
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
Tenaja
Addict
Addict
Posts: 1948
Joined: Tue Nov 09, 2010 10:15 pm

Re: Quick OK Image format

Post 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".
Post Reply