Read/load & write/save PPM & PGM portable bitmap formats

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

Read/load & write/save PPM & PGM portable bitmap formats

Post by Keya »

... the what formats? I hadn't heard of PPM or PGM either! but I came across a program that used them because it's a really simple format as it turns out.

Photoshop and GIMP can read and write them.

In Photoshop it will save as PGM format in Mode->Grayscale, and PPM format in Mode->RGB (but not Indexed), which you can access simply with File -> Save As.

It's an 80's format and only appears to have semi-official specs - this page has some really good info:
http://paulbourke.net/dataformats/ppm/
and apart from that i just used a hex editor to look at Photoshop saves

Here's a hex edit of a full PPM file (the header component highlighted, the rest is raw RGB data), from an image 5px width by 3px height, where the first row of pixels are all nearly-red (FF0101), the 2nd row is nearly-green (02FF02), and the 3rd row is nearly-blue (0303FF), stored in RGB order (not BGR):
Image
The header "P6" means binary RGB format, 5 and 3 the dimensions, 255 the maximum value, each separated by a $0A line-feed. Pretty simple! Then the raw data, which is 3 bytes for PPM/P6 format (R,G,B), or 1 byte for PGM/P5 grayscale format.

Here in my implementation i've added Read & Write support, but only for BINARY versions of the format, not the ASCII text versions. For saving it supports 24bit and 32bit images as input, and can save as either 8bit grayscale (PGM) or 24bit RGB (PPM). It can also read both PGM and PPM formats. When saving using the grayscale PGM format it simply saves the first channel (blue) value; it assumes you've already converted the image to grayscale (so R,G,B would all be the same). It also correctly draws up or down depending which OS you're using, and I believe i've added all necessary error checks. All tests of my output were byte-for-byte identical to Photoshop's output.

So it should be a fairly complete and multiOS implementation, at least as far as the binary versions of PGM and PPM go which are the two main ones. It does not support the PBM (1bit monochrome) or PNM (content-independent weirdness) formats.

Sample PPM & PGM images
http://www.sendspace.com/file/4jk38a
(167kb, one of each format, saved using Photoshop)

Read PPM/PGM:

Code: Select all

EnableExplicit

Structure structRGB
  B.a
  G.a
  R.a
EndStructure

Procedure.i ReadPortableMapImage(ImgFile.s)  ;Returns image handle or 0. Supports PPM & PGM (binary only, not ascii)
  Protected Pfmt, hFile, hImg, width, height, flen, pitch, startaddr, endaddr, elements, x, y, find
  Protected *draw, *next.Ascii, *buf, *row.Ascii, *char.Ascii, Magic.w, *Magic.Word = @Magic
  Protected *RGBin.structRGB, *RGBout.structRGB, *bytein.Ascii  
  Dim element.s(2)
  hFile = ReadFile(#PB_Any, ImgFile, #PB_File_SharedRead |  #PB_File_SharedWrite)
  If hFile = 0
    ProcedureReturn 0
  EndIf
  flen = Lof(hFile)
  If flen < 12
    ErrReturn:
    If *buf: FreeMemory(*buf): EndIf
    If IsImage(hImg): FreeImage(hImg): EndIf
    CloseFile(hFile)
    ProcedureReturn 0
  EndIf
  *buf = AllocateMemory(flen) 
  If *buf = 0: Goto ErrReturn: EndIf    
  ReadData(hFile, @Magic, 2)  ;Read first 2 bytes to check Magic to ensure file is a supported format
  Select *Magic\w
    Case $3550: Pfmt=1  ;"P5" PGM Grayscale
    Case $3650: Pfmt=3  ;"P6" PPM Bitmap
    Default: 
      Goto ErrReturn
  EndSelect  
  ReadData(hFile, *buf, flen-2)
  *char = *buf
  Repeat
    Select *char\a
      Case $0D, $0A, $20, $09:  ;Skip chars CR, LF, SPC, TAB
        If startaddr <> 0
          endaddr = *char
          element(elements) = PeekS(startaddr, endaddr-startaddr, #PB_Ascii)
          find = FindString(element(elements), "#")
          If find
            element(elements) = Left(element(elements), find-1)
          EndIf            
          element(elements) = Trim(element(elements))
          If Left(element(elements),1) <> "#":  elements+1: EndIf  ;ignore comments          
          If elements = 3
            startaddr = *char+1
            Break
          EndIf
          startaddr = 0
        EndIf          
      Default:
        If startaddr = 0
          startaddr = *char
        EndIf
    EndSelect      
    *char+1
  Until *char = *buf+flen
  
  If Val(element(2)) <> 255: Goto ErrReturn: EndIf  ;only the full/normal range (0-255) supported in this implementation
  
  width = Val(element(0))
  height = Val(element(1))    
  If width <= 0 Or height <= 0: Goto ErrReturn: EndIf
  
  If Pfmt = 3
    If (flen - (startaddr - *buf)) < ((width*height)*3): Goto ErrReturn: EndIf ;incomplete data
  Else                                                                         ;Pfmt = 1
    If (flen - (startaddr - *buf)) < (width*height): Goto ErrReturn: EndIf     ;incomplete data    
  EndIf
  
  hImg = CreateImage(#PB_Any, width, height, 24)
  If hImg = 0: Goto ErrReturn: EndIf
  If StartDrawing(ImageOutput(hImg)) = 0: Goto ErrReturn: EndIf
  
  pitch = DrawingBufferPitch() 
  *RGBin = startaddr
  *bytein = *RGBin  
  startaddr = DrawingBuffer()
  
  For y = 0 To height-1
    CompilerIf #PB_Compiler_OS = #PB_OS_Windows   ;draw rows bottom-to-top      
      *RGBout = startaddr + (pitch*height) - (pitch*(y+1))      
    CompilerElse                                  ;draw rows top-to-bottom      
      *RGBout = startaddr + (y * pitch)
    CompilerEndIf    
    If Pfmt = 3  ;RGB Bitmap
      For x = 1 To width
        CompilerIf #PB_Compiler_OS = #PB_OS_Windows ;BGRA
          *RGBout\B = *RGBin\R
          *RGBout\G = *RGBin\G
          *RGBout\R = *RGBin\B
        CompilerElse ;RGBA
          *RGBout\B = *RGBin\B
          *RGBout\G = *RGBin\G
          *RGBout\R = *RGBin\R
        CompilerEndIf 
        *RGBout+3: *RGBin+3
      Next x      
    Else ;Pfmt = 1  ;Grayscale
      For x = 1 To width
        *RGBout\R = *bytein\a
        *RGBout\G = *bytein\a
        *RGBout\B = *bytein\a
        *RGBout+3: *bytein+1
      Next x      
    EndIf  
  Next y
  
  StopDrawing()
  FreeMemory(*buf)
  ProcedureReturn hImg
EndProcedure


Define hImg = ReadPortableMapImage("c:\temp\input.ppm")
;Define hImg = ReadPortableMapImage("c:\temp\input.pgm")
If hImg = 0
  MessageRequester("Error","Couldnt load file"):  End
EndIf

If OpenWindow(0, 0, 0, ImageWidth(hImg), ImageHeight(hImg), "PPM + PGM Image Reader", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ImageGadget(0, 0, 0, ImageWidth(hImg), ImageHeight(hImg), ImageID(hImg))
  Repeat
    Define Event = WaitWindowEvent()
  Until Event = #PB_Event_CloseWindow
EndIf
Save PPM / PGM:

Code: Select all

EnableExplicit

Structure structRGB
  B.a
  G.a
  R.a
EndStructure

Procedure.i SavePortableMapImage(hImg, OutFile.s, Grayscale.i)  ;hImg=24 or 32bit image.  Grayscale = 1 (8bit) or 0 (24bit RGB)
  Protected hFile, width, height, depth, pitch, hdr.s, drawbuf, *buf, *RGBin.structRGB, *RGBout.structRGB, *byteout.Ascii, x,y
  If Not IsImage(hImg): ProcedureReturn 0: EndIf
  depth = ImageDepth(hImg)
  If depth = 24: depth = 3: ElseIf depth = 32: depth = 4: Else: ProcedureReturn 0: EndIf
  hFile = CreateFile(#PB_Any, OutFile)
  If hFile = 0: ProcedureReturn 0: EndIf 
  If Not StartDrawing(ImageOutput(hImg))
    ErrReturn:
    CloseFile(hFile)
    ProcedureReturn 0
  EndIf
  width = ImageWidth(hImg): height = ImageHeight(hImg)
  pitch = DrawingBufferPitch()
  drawbuf = DrawingBuffer()
  If Grayscale = 1: hdr = "P5": Else: hdr = "P6": EndIf          
  hdr + Chr($0A)+Str(width)+Chr($0A)+Str(height)+Chr($0A)+"255"+Chr($0A)
  *buf = AllocateMemory(Len(hdr) + (width*height)*3)
  If Not *buf: Goto ErrReturn: EndIf
  PokeS(*buf, hdr, -1, #PB_Ascii)   
  *RGBout = *buf + Len(hdr)
  *byteout = *RGBout
  *RGBin = drawbuf
  For y = 0 To height-1
    CompilerIf #PB_Compiler_OS = #PB_OS_Windows   ;draw bottom-to-top      
      *RGBin = drawbuf + (pitch*height) - (pitch*(y+1))      
    CompilerElse                      ;draw top-to-bottom      
      *RGBin = drawbuf + (y * pitch)
    CompilerEndIf
    If Grayscale = 0
      For x = 0 To width-1
        CompilerIf #PB_Compiler_OS = #PB_OS_Windows ;BGRA     
          *RGBout\R = *RGBin\B
          *RGBout\G = *RGBin\G
          *RGBout\B = *RGBin\R
        CompilerElse ;Linux/OSX=RGBA
          *RGBout\R = *RGBin\R
          *RGBout\G = *RGBin\G
          *RGBout\B = *RGBin\B
        CompilerEndIf
        *RGBin+depth: *RGBout+3
      Next x
    Else
      For x = 0 To width-1
        *byteout\a = *RGBin\B
        *RGBin+depth: *byteout+1
      Next x
    EndIf
  Next y      
  StopDrawing()  
  WriteData(hFile, *buf, MemorySize(*buf))
  CloseFile(hFile)
  ProcedureReturn 1  
EndProcedure


Define hImg = LoadImage(#PB_Any, "c:\temp\input.bmp")  ;can be 24bit or 32bit
SavePortableMapImage(hImg, "c:\temp\output.ppm", 0)     ;save as 24bit RGB
SavePortableMapImage(hImg, "c:\temp\output.pgm", 1)     ;save as 8bit grayscale
FreeImage(hImg)
Last edited by Keya on Thu May 26, 2016 11:13 pm, edited 8 times in total.
User avatar
Demivec
Addict
Addict
Posts: 4089
Joined: Mon Jul 25, 2005 3:51 pm
Location: Utah, USA

Re: Read/load & write/save PPM & PGM portable bitmap formats

Post by Demivec »

I don't want to detract from your posting but wanted to share some additonal examples of reading and writing the PPM format that are also posted on Rosetta Code.

Read_a_PPM_file
Write_a_PPM_file
User avatar
Keya
Addict
Addict
Posts: 1891
Joined: Thu Jun 04, 2015 7:10 am

Re: Read/load & write/save PPM & PGM portable bitmap formats

Post by Keya »

thanks for sharing :) i wasnt aware of that - i use Google to search for Purebasic code but i guess my "inurl:purebasic" wont find anything posted at Rosetta :(
i didnt think there was toooo much PB there though, but actually it seems there's quite a lot of good stuff http://rosettacode.org/wiki/Category:PureBasic

I can already spot a few problems with their code though (and it crashes when trying to load the sample images i made in Photoshop):
- it assumes only spaces are used whereas actually "any whitespace" can be used (tabs, spaces, CRs, LFs - Photoshop uses $0A line-feed and no spaces)
- it assumes for some reason the 2nd line is a comment, but comments are optional - Photoshop doesnt save any
- it only supports 24bit images and doesnt check if ImageDepth = 32 (or other)
- it doesnt account for incomplete files; it will corrupt buffer or crash
- it doesn't account for Windows images writing bottom-to-top vs top-to-bottom on Linux/OSX so they'll be upside down
- it doesnt account for OSX/Linux being RGBA vs Windows BGRA
- it doesn't account for pitch/stride so it wont work with all image widths
- it doesnt support .PGM format (exactly the same as .PPM but data is 8bit-grayscale instead of 24bit-RGB)
- its also very slow, using Plot() to draw instead of buffer, and saving one byte at a time with WriteByte instead of WriteData, and uses regular expressions to parse the header

not to sound negative! :( but those are all issues i had to deal with too when writing my implementation.
Theirs supports the Ascii text format though (i only support binary) which is cool! :)

ps. i believe the FreeImage library also supports this format family, but as its such a simple format i was interested in standalone support. I think it makes a nice addition (along with BMP and TGA) for image support without large codecs, especially seeing as Photoshop and GIMP have full read+write support for it
Post Reply