Using raw DrawingBuffer() as a multi-OS image file format

Share your advanced PureBasic knowledge/code with the community.
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Using raw DrawingBuffer() as a multi-OS image file format

Post by Keya »

To give another option to multi-OS development, this tool takes any LoadImage'd image (png, jpg etc) and saves its DrawingBuffer() as three 'dump' image files, each converted where necessary to that specific OS format - Win/OSX/Linux.

For example you can give it world.png, and it generates world.win.pbimg, world.lnx.pbimg, and world.osx.pbimg.
Each .pbimg file is slightly converted so that it should appear exactly as if you had dumped it from that OS. (RGB vs BGR, reverse row order, and 24/32 bit are the differences - see very end of this post). Each .pbimg file simply has a 16-byte metadata header followed by the raw image data.

This way we have:
- a very simple, portable image format (not a public format as such, just for PB)
- that works across all three OS, yet only requires image conversions from one
- doesn't require a codec
- doesn't require any OS API like gdk
- supports 24+32bit (gives us another option for showing transparent images)
- hardly requires any supporting code (essentially just a CopyMemory) - no 'decoding'
- allows for a single format across all three (dont need ICO for Windows, ICNS for Mac, PNG for Linux etc)
- easily supports any compression algorithm we want so it can still compete with PNG/JPG/GIF, but I've left that out for now.

Then we can simply CompilerIf-IncludeBinary the right .pbimg depending on OS, example usage:

Code: Select all

Structure PBIMG_Hdr  ;Size=16 bytes, followed immediately by raw image data.
  Width.l            ;Width (pixels)
  Height.l           ;Height (pixels).  Total image bytes after the header = dwHeight * dwPitch
  Pitch.l            ;DrawingBufferPitch()  (bytes per row)
  PixFmt.w           ;DrawingBufferPixelFormat()
  Depth.a            ;24 or 32
  Packer.a           ;0=Original data, otherwise #PB_PackerPlugin_BriefLZ, #PB_PackerPlugin_Lzma, or #PB_PackerPlugin_Zip
EndStructure

Procedure LoadPBImg(ImgGadget, *PBImg.PBIMG_Hdr)
  hImg = CreateImage(#PB_Any, *PBImg\Width, *PBImg\Height, *PBImg\Depth) 
  If hImg And StartDrawing(ImageOutput(hImg))
    CopyMemory(*PBImg+SizeOf(PBIMG_Hdr), DrawingBuffer(), *PBImg\Height * *PBImg\Pitch)
    StopDrawing()
    SetGadgetState(ImgGadget, ImageID(hImg))
  EndIf  
EndProcedure

#Image1=1
If OpenWindow(0, 0, 0, 200, 200, "PBImage", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ImageGadget(#Image1, 0, 0, 200, 200, 0)
  LoadPBImg(#Image1, ?Image1)
EndIf  
Repeat:  Until WaitWindowEvent() = #PB_Event_CloseWindow

DataSection
  Image1:
  CompilerIf #PB_Compiler_OS = #PB_OS_Windows
    IncludeBinary(#PB_Compiler_Home + "/examples/sources/Data/world.png.win.pbimg")
  CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS
    IncludeBinary(#PB_Compiler_Home + "/examples/sources/Data/world.png.osx.pbimg")
  CompilerElseIf #PB_Compiler_OS = #PB_OS_Linux
    IncludeBinary(#PB_Compiler_Home + "/examples/sources/Data/world.png.lnx.pbimg")
  CompilerEndIf
EndDataSection
Here's the image conversion tool (supports Win+OSX+Linux 32+64):

Code: Select all

EnableExplicit

Structure PBIMG_Hdr  ;Size=16 bytes, followed immediately by raw image data.
  Width.l             ;Width (pixels)
  Height.l            ;Height (pixels).  Total image bytes after the header = dwHeight * dwPitch
  Pitch.l             ;DrawingBufferPitch()  (bytes per row)
  PixFmt.w            ;DrawingBufferPixelFormat()
  Depth.a             ;24 or 32
  Packer.a            ;(COMING SOON) 0=Original data, or #PB_PackerPlugin_BriefLZ, #PB_PackerPlugin_Lzma, or #PB_PackerPlugin_Zip
EndStructure

Structure RGB
  r.a
  g.a
  b.a
EndStructure


Procedure FlipImageRows(hImg)  ;Applies or unapplies #PB_PixelFormat_ReversedY
  Protected *pbuf, *psrc, *ptmp, *pdst, width, height, i 
  If StartDrawing(ImageOutput(hImg)) 
    *pbuf = DrawingBuffer()
    width = DrawingBufferPitch()
    height = ImageHeight(hImg) 
    *psrc = *pbuf
    *ptmp = AllocateMemory(width*height)
    If *ptmp
      *pdst = *ptmp + (width*height)-width
      For i = 0 To height-1
        CopyMemory(*psrc, *pdst, width)
        *pdst-width
        *psrc+width
      Next i
      CopyMemory(*ptmp, *pbuf, width*height)
      FreeMemory(*ptmp)                     
    EndIf
    StopDrawing()
  EndIf
EndProcedure


Procedure.i ConvertImageDepth(hImg, newfmt)
  Protected width,height, hImgNew
  width = ImageWidth(hImg)
  height = ImageHeight(hImg)
  hImgNew = CreateImage(#PB_Any, width, height, newfmt)
  If hImgNew = 0: ProcedureReturn 0: EndIf  
  If StartDrawing(ImageOutput(hImgNew)) = 0
    FreeImage(hImgNew)
    ProcedureReturn 0
  EndIf
  DrawingMode(#PB_2DDrawing_Default)
  DrawImage(ImageID(hImg),0,0,width,height)
  StopDrawing()
  FreeImage(hImg)
  ProcedureReturn hImgNew
EndProcedure


Procedure FlipEndianRGB(hImg)
  Protected *pbuf, width,height,pitch,  x,y, depthstep, *nextRGB.RGB
  If StartDrawing(ImageOutput(hImg)) 
    *pbuf = DrawingBuffer()
    pitch = DrawingBufferPitch()
    width = ImageWidth(hImg)
    height = ImageHeight(hImg)         
    If ImageDepth(hImg) = 24: depthstep = 3: Else: depthstep = 4: EndIf
    For y = 0 To height-1
      *nextRGB.RGB = *pbuf + (y * pitch)
      For x = 0 To width-1
        Swap *nextRGB\b, *nextRGB\r
        *nextRGB + depthstep
      Next x
    Next y    
    StopDrawing()
  EndIf
EndProcedure


Procedure SavePBImg(hImg, fmt, filename.s)
  Protected PBImg.PBIMG_Hdr, hFile, bytes, *buf
  If StartDrawing(ImageOutput(hImg))
    PBImg\Width = ImageWidth(hImg)
    PBImg\Height = ImageHeight(hImg)
    PBImg\Pitch = DrawingBufferPitch()
    PBImg\PixFmt = fmt
    PBImg\Depth = ImageDepth(hImg)
    PBImg\Packer = 0
    bytes = PBImg\Pitch * PBImg\Height
    *buf = AllocateMemory(bytes + SizeOf(PBIMG_Hdr))
    If *buf
      CopyMemory(DrawingBuffer(), *buf+SizeOf(PBIMG_Hdr), bytes)
      CopyMemory(@PBImg, *buf, SizeOf(PBIMG_Hdr))
      hFile = CreateFile(#PB_Any, filename)
      WriteData(hFile, *buf, MemorySize(*buf))
      CloseFile(hFile)
    EndIf
  EndIf
  StopDrawing()    
EndProcedure


Procedure ConvertImageToRaw(imgfile.s)
  Protected *buf, hImg = LoadImage(#PB_Any, imgfile)
  If hImg = 0
    MessageRequester("Error","Couldnt load file " + imgfile)
    ProcedureReturn 0
  EndIf
  
  Select ImageDepth(hImg)
    Case 24:
      CompilerIf #PB_Compiler_OS = #PB_OS_Windows
        SavePBImg(hImg, #PB_PixelFormat_24Bits_BGR + #PB_PixelFormat_ReversedY, imgfile+".win.pbimg")
        FlipEndianRGB(hImg)
        FlipImageRows(hImg)
        SavePBImg(hImg, #PB_PixelFormat_24Bits_RGB, imgfile+".lnx.pbimg")
        hImg = ConvertImageDepth(hImg,32)
        SavePBImg(hImg, #PB_PixelFormat_32Bits_RGB, imgfile+".osx.pbimg")        
      CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS
        SavePBImg(hImg, #PB_PixelFormat_32Bits_RGB, imgfile+".osx.pbimg")        
        hImg = ConvertImageDepth(hImg,24)
        SavePBImg(hImg, #PB_PixelFormat_24Bits_RGB, imgfile+".lnx.pbimg")
        FlipEndianRGB(hImg)
        FlipImageRows(hImg)
        SavePBImg(hImg, #PB_PixelFormat_24Bits_BGR + #PB_PixelFormat_ReversedY, imgfile+".win.pbimg")        
      CompilerElseIf #PB_Compiler_OS = #PB_OS_Linux
        SavePBImg(hImg, #PB_PixelFormat_24Bits_RGB, imgfile+".lnx.pbimg")
        FlipEndianRGB(hImg)
        FlipImageRows(hImg)
        SavePBImg(hImg, #PB_PixelFormat_24Bits_BGR + #PB_PixelFormat_ReversedY, imgfile+".win.pbimg")        
        FlipEndianRGB(hImg)
        FlipImageRows(hImg)        
        hImg = ConvertImageDepth(hImg,32)
        SavePBImg(hImg, #PB_PixelFormat_32Bits_RGB, imgfile+".osx.pbimg")               
      CompilerEndIf      
    Case 32:
      CompilerIf #PB_Compiler_OS = #PB_OS_Windows
        SavePBImg(hImg, #PB_PixelFormat_32Bits_BGR + #PB_PixelFormat_ReversedY, imgfile+".win.pbimg")
        FlipEndianRGB(hImg)
        FlipImageRows(hImg)
        SavePBImg(hImg, #PB_PixelFormat_32Bits_RGB, imgfile+".lnxosx.pbimg")
      CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS Or #PB_Compiler_OS = #PB_OS_Linux
        SavePBImg(hImg, #PB_PixelFormat_32Bits_RGB, imgfile+".lnxosx.pbimg")
        FlipEndianRGB(hImg)
        FlipImageRows(hImg)
        SavePBImg(hImg, #PB_PixelFormat_32Bits_BGR + #PB_PixelFormat_ReversedY, imgfile+".win.pbimg")
      CompilerEndIf      
  EndSelect  
  If StartDrawing(ImageOutput(hImg))
  Else
    MessageRequester("Error","StartDrawing failed")
  EndIf
EndProcedure


;---
UsePNGImageDecoder(): UseJPEGImageDecoder(): UseJPEG2000ImageDecoder(): UseTGAImageDecoder(): UseTIFFImageDecoder()
Define TargetImage.s = #PB_Compiler_Home + "/examples/sources/Data/world.png"   ;Image to convert to .pbimg's
ConvertImageToRaw(TargetImage)
MessageRequester("OK","Converted")
For reference, OS native pixel formats (24 & 32-bit images):

Code: Select all

; Win-24 (BGR):  $8010 = #PB_PixelFormat_24Bits_BGR + #PB_PixelFormat_ReversedY
; Win-32 (BGRA): $8040 = #PB_PixelFormat_32Bits_BGR + #PB_PixelFormat_ReversedY
; Lnx-24 (RGB):  $8    = #PB_PixelFormat_24Bits_RGB
; Lnx-32 (RGBA): $20   = #PB_PixelFormat_32Bits_RGB ;shared
; OSX-24 (RGBA): $20   = #PB_PixelFormat_32Bits_RGB ;shared
; OSX-32 (RGBA): $20   = #PB_PixelFormat_32Bits_RGB ;shared
Last edited by Keya on Wed Jun 29, 2016 1:14 am, edited 3 times in total.
freak
PureBasic Team
PureBasic Team
Posts: 5940
Joined: Fri Apr 25, 2003 5:21 pm
Location: Germany

Re: Using raw DrawingBuffer() as a multi-OS image file forma

Post by freak »

The actual pixel format used on each OS is not a guarantee though. It could change in a future PB version which breaks your code.

Why not just use BMP? It is essentially the same thing (raw pixel data + a header). Also the decoder for it is pretty light-weight and available in all PB versions by default. I think you are making this more complex than it needs to be.
quidquid Latine dictum sit altum videtur
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Re: Using raw DrawingBuffer() as a multi-OS image file forma

Post by Keya »

PB's BMP decoder doesn't properly support 32bit alpha channel (one of the reasons i was looking into using native/gdk API with Linux). If you meant make my own BMP decoder, i could, but it would still be decoding into the same RGB or BGR formats that these dumps are being made in, so whatever "break" you're refering to would happen there too? and to just about every other PB code using DrawingBuffer()! - virtually every post here using DrawingBuffer seems to make these assumptions about the OS's! (not saying rightly though lol) Is there some system check that LoadImage makes to figure out what format to use, or does it also make these assumptions? looking at it in Windows there is a GetPixelFormat function but my test PB app with LoadImage+StartDrawing+DrawImage isnt importing it, perhaps another way, or perhaps just not needed due to API doing it such as GetDIBits.

If I just check what the pixel format is after CreateImage i'll be able to modify it if required simply using FlipEndianRGB and FlipImageRows, so that should prevent any such break, and with minimal overhead of just those two procs. problem solved? a bmp decoder would have to do the same thing

but yes if the BMP decoder had 32bit alpha channel support none of this would be necessary, but there are bigger problems in life :)
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Re: Using raw DrawingBuffer() as a multi-OS image file forma

Post by Keya »

I think i have the solution to your break idea...
Because we know the pixel format each .pbimg is in (which is the one that should be correct for that OS 99.9% of the time), if the CreateImage() pixel format for any reason is different from expected it's simply a matter of either reversing the Y order and/or changing RGB<>BGR, because we can tell by the flag comparison exactly what changes are required, and they're basically the only two changes there could really be for 24+32? So in the first example in my first post it would simply mean including the small FlipImageRows + FlipEndianRGB functions, and changing the LoadPBImg function accordingly ... what do you think? :)

Code: Select all

Procedure LoadPBImg(ImgGadget, *PBImg.PBIMG_Hdr)
  hImg = CreateImage(#PB_Any, *PBImg\Width, *PBImg\Height, *PBImg\Depth) 
  If hImg And StartDrawing(ImageOutput(hImg))
    Protected ImgFmt = DrawingBufferPixelFormat()
    CopyMemory(*PBImg+SizeOf(PBIMG_Hdr), DrawingBuffer(), *PBImg\Height * *PBImg\Pitch)
    If *PBImg\PixFmt <> ImgFmt  ;Is the CreateImage pixfmt different to the .pbimg pixfmt? Rarely would be, but accomodate anyway...
      If (*PBImg\PixFmt & #PB_PixelFormat_ReversedY) <> (ImgFmt & #PB_PixelFormat_ReversedY) ;check if row order needs flipping
        FlipImageRows(hImg)
      EndIf
      If *PBImg\Depth = 24        ;check if RGB<>BGR needs endian flipping
        If (*PBImg\PixFmt & #PB_PixelFormat_24Bits_BGR) <> (ImgFmt & #PB_PixelFormat_24Bits_BGR) Or
           (*PBImg\PixFmt & #PB_PixelFormat_24Bits_RGB) <> (ImgFmt & #PB_PixelFormat_24Bits_RGB)         
          FlipEndianRGB(hImg)
        EndIf
      ElseIf *PBImg\Depth = 32
        If (*PBImg\PixFmt & #PB_PixelFormat_32Bits_BGR) <> (ImgFmt & #PB_PixelFormat_32Bits_BGR) Or
           (*PBImg\PixFmt & #PB_PixelFormat_32Bits_RGB) <> (ImgFmt & #PB_PixelFormat_32Bits_RGB)         
          FlipEndianRGB(hImg)
        EndIf
      EndIf      
    EndIf
    StopDrawing()
    SetGadgetState(ImgGadget, ImageID(hImg))
  EndIf  
EndProcedure
Post Reply