pnm.f90 Source File


This file depends on

sourcefile~~pnm.f90~~EfferentGraph sourcefile~pnm.f90 pnm.f90 sourcefile~forimage_parameters.f90 forimage_parameters.f90 sourcefile~pnm.f90->sourcefile~forimage_parameters.f90

Files dependent on this one

sourcefile~~pnm.f90~~AfferentGraph sourcefile~pnm.f90 pnm.f90 sourcefile~forcolor.f90 forcolor.f90 sourcefile~forcolor.f90->sourcefile~pnm.f90 sourcefile~forimage.f90 forimage.f90 sourcefile~forimage.f90->sourcefile~pnm.f90 sourcefile~forimage.f90->sourcefile~forcolor.f90 sourcefile~demo_color.f90 demo_color.f90 sourcefile~demo_color.f90->sourcefile~forimage.f90 sourcefile~demo_ppm.f90 demo_ppm.f90 sourcefile~demo_ppm.f90->sourcefile~forimage.f90 sourcefile~test1.f90 test1.f90 sourcefile~test1.f90->sourcefile~forimage.f90 sourcefile~test10.f90 test10.f90 sourcefile~test10.f90->sourcefile~forimage.f90 sourcefile~test11.f90 test11.f90 sourcefile~test11.f90->sourcefile~forimage.f90 sourcefile~test12.f90 test12.f90 sourcefile~test12.f90->sourcefile~forimage.f90 sourcefile~test13.f90 test13.f90 sourcefile~test13.f90->sourcefile~forimage.f90 sourcefile~test14.f90 test14.f90 sourcefile~test14.f90->sourcefile~forimage.f90 sourcefile~test15.f90 test15.f90 sourcefile~test15.f90->sourcefile~forimage.f90 sourcefile~test16.f90 test16.f90 sourcefile~test16.f90->sourcefile~forimage.f90 sourcefile~test17.f90 test17.f90 sourcefile~test17.f90->sourcefile~forimage.f90 sourcefile~test18.f90 test18.f90 sourcefile~test18.f90->sourcefile~forimage.f90 sourcefile~test19.f90 test19.f90 sourcefile~test19.f90->sourcefile~forimage.f90 sourcefile~test2.f90 test2.f90 sourcefile~test2.f90->sourcefile~forimage.f90 sourcefile~test20.f90 test20.f90 sourcefile~test20.f90->sourcefile~forimage.f90 sourcefile~test21.f90 test21.f90 sourcefile~test21.f90->sourcefile~forimage.f90 sourcefile~test22.f90 test22.f90 sourcefile~test22.f90->sourcefile~forimage.f90 sourcefile~test23.f90 test23.f90 sourcefile~test23.f90->sourcefile~forimage.f90 sourcefile~test24.f90 test24.f90 sourcefile~test24.f90->sourcefile~forimage.f90 sourcefile~test25.f90 test25.f90 sourcefile~test25.f90->sourcefile~forimage.f90 sourcefile~test26.f90 test26.f90 sourcefile~test26.f90->sourcefile~forimage.f90 sourcefile~test27.f90 test27.f90 sourcefile~test27.f90->sourcefile~forimage.f90 sourcefile~test28.f90 test28.f90 sourcefile~test28.f90->sourcefile~forimage.f90 sourcefile~test29.f90 test29.f90 sourcefile~test29.f90->sourcefile~forimage.f90 sourcefile~test3.f90 test3.f90 sourcefile~test3.f90->sourcefile~forimage.f90 sourcefile~test4.f90 test4.f90 sourcefile~test4.f90->sourcefile~forimage.f90 sourcefile~test5.f90 test5.f90 sourcefile~test5.f90->sourcefile~forimage.f90 sourcefile~test6.f90 test6.f90 sourcefile~test6.f90->sourcefile~forimage.f90 sourcefile~test7.f90 test7.f90 sourcefile~test7.f90->sourcefile~forimage.f90 sourcefile~test8.f90 test8.f90 sourcefile~test8.f90->sourcefile~forimage.f90 sourcefile~test9.f90 test9.f90 sourcefile~test9.f90->sourcefile~forimage.f90

Source Code

!> author: Seyed Ali Ghasemi
!> license: BSD 3-Clause
!> This module defines the `format_pnm` type for handling PNM (Portable Any Map) image files.
!> PNM files include both ASCII and binary representations for various image types, such as PBM, PGM, and PPM.
!> The module offers functionalities to manipulate, import, and export PNM images, along with image processing.

module pnm
   use forimage_parameters, only: rk, ik
   implicit none

   private
   public format_pnm

   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> This type is designed to store and manipulate PNM (Portable Any Map) image files.
   type format_pnm
      character(2)                             , private :: magic_number !! Magic number representing the PNM image type (`P1`, `P2`, `P3`, `P4`, `P5`, `P6`).
      integer                                  , private :: width        !! Width (number of columns) of the image.
      integer                                  , private :: height       !! Height (number of rows) of the image.
      character(:), allocatable                , private :: comment      !! Optional comment associated with the image.
      integer                                  , private :: max_color    !! Maximum color value of the image. Used for PGM and PPM images.
      integer(ik), dimension(:,:), allocatable :: pixels                 !! Pixel values of the image.
      character(3)                             , private :: file_format  !! File format of the PNM image (`pbm`, `pgm`, `ppm`).
      character(6)                             , private :: encoding     !! Encoding of the PNM image (`ascii` or `plain`, `binary` or `raw`).
   contains
      ! Procedures for setting individual attributes
      procedure :: set_format                 !!> Set the encoding of the PNM image.
      procedure, private :: set_file_format   !!> Set the file format of the PNM image.
      procedure, private :: set_magicnumber   !!> Set the magic number of the PNM image.
      procedure, private :: set_width         !!> Set the width of the PNM image.
      procedure, private :: set_height        !!> Set the height of the PNM image.
      procedure, private :: set_comment       !!> Set a comment for the PNM image.
      procedure, private :: set_max_color     !!> Set the maximum color value of the PNM image.
      procedure, private :: set_header        !!> Set the header of the PNM image.
      procedure, private :: allocate_pixels   !!> Allocate memory for the pixels of the PNM image.
      procedure, private :: check_pixel_range !!> Check if the pixel values are within the valid range.
      procedure, private :: set_pixels        !!> Set the pixel values of the PNM image.
      
      ! Procedures for setting individual attributes
      procedure :: get_format                 !!> Get the encoding of the PNM image.

      ! High-level procedures for working with PNM images
      procedure :: set_pnm                    !!> Set the attributes of the PNM image.
      procedure :: print_info                 !!> Display information about the image (dimensions, aspect ratio, etc.).
      procedure :: import_pnm                 !!> Read an image from a file.
      procedure :: export_pnm                 !!> Write an image to a file.
      procedure :: finalize => deallocate_pnm !!> Clean up allocated memory for the PNM image.

      ! Image manipulation procedures
      procedure :: negative                   !!> Invert the colors of the image.             
      procedure :: brighten                   !!> Adjust the brightness and darkness of the image.
      procedure :: swap_channels              !!> Swap the RGB channels of the image.
      procedure :: remove_channels            !!> Remove one or more RGB channels from the image.
      procedure :: greyscale                  !!> Convert a color image to greyscale.
      procedure :: rotate                     !!> Rotate the image by a specified angle.
      procedure :: flip_horizontal            !!> Flip the image horizontally.
      procedure :: flip_vertical              !!> Flip the image vertically.
      procedure :: crop                       !!> Crop the image to a specified region.
      procedure :: resize                     !!> Resize the image to a specified size.
   end type format_pnm
   !===============================================================================

contains

   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Displays information about the image e.g. dimensions, aspect ratio, etc.
   elemental impure subroutine print_info(this)
      class(format_pnm), intent(in) :: this
      real(rk) :: avg, avg_red, avg_green, avg_blue
      real(rk) :: asp_ratio
      real(rk) :: pixel_size_kb, pixel_size_mb

      select case (this%file_format)
       case ('pbm', 'pgm')
         call average_colors(this, avg)
       case ('ppm')
         call average_colors(this, avg, avg_red, avg_green, avg_blue)
      end select
      call aspect_ratio(this, asp_ratio)
      call pixel_size(this, pixel_size_kb, pixel_size_mb)

      print '(a)'                           , 'Image Information:'
      print '(a)'                           , '-------------------------------------------'
      print '(a, g0)'                       , 'Magic Number: ', this%magic_number
      print '(a, a)'                        , 'File Format : ', this%file_format
      print '(a, a)'                        , 'Encoding    : ', this%encoding
      print '(a, a)'                        , 'Comment     : ', trim(this%comment)
      print '(a, a, g0, a, g0)'             , 'Dimensions  : ', 'Height: ', this%height, ' Width: ', this%width
      print '(a, g0)'                       , 'Total Pixels: ', this%width * this%height
      print '(a, f6.2)'                     , 'Aspect Ratio: ', asp_ratio
      print '(a, f8.2, a, f8.2, a)'         , 'Pixel Size  : ', pixel_size_kb, ' KB ', pixel_size_mb, ' MB'
      select case (this%file_format)
       case ('pbm', 'pgm')
         print '(a, g0)'                     , 'Average     : ', avg
       case ('ppm')
         print '(a, g0)'                       , 'Max Color   : ', this%max_color
         print '(a, a, f6.2, a, f6.2, a, f6.2)', 'Average RGB : ', 'R:', avg_red, ' G:', avg_green, ' B:', avg_blue
      end select
      print '(a)'                           , '-------------------------------------------'
   end subroutine print_info
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Calculates imgae size in KB and MB. Required for `print_info` method.
   elemental pure subroutine pixel_size(this, pixel_size_kb, pixel_size_mb)
      class(format_pnm), intent(in) :: this
      real(rk), intent(out) :: pixel_size_kb, pixel_size_mb
      integer :: bits_per_channel, bytes_per_pixel

      bits_per_channel = 8

      select case (this%file_format)
       case ('pbm', 'pgm')
         bytes_per_pixel = bits_per_channel
       case ('ppm')
         bytes_per_pixel = bits_per_channel*3
      end select

      pixel_size_kb = real(this%width*this%height * bytes_per_pixel, kind=rk) / 1024.0_rk
      pixel_size_mb = pixel_size_kb / 1024.0_rk
   end subroutine pixel_size
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Calculates the average color values of the image. Required for `print_info` method.
   elemental pure subroutine average_colors(this, avg, avg_red, avg_green, avg_blue)
      class(format_pnm), intent(in) :: this
      real(rk), intent(out), optional :: avg_red, avg_green, avg_blue, avg

      select case (this%file_format)
       case ('pbm', 'pgm')

         avg = sum(this%pixels) / real(this%width*this%height, kind=rk)

       case ('ppm')

         avg_red   = sum(this%pixels(:, 1:this%width:3)) / real(this%width*this%height, kind=rk)
         avg_green = sum(this%pixels(:, 2:this%width:3)) / real(this%width*this%height, kind=rk)
         avg_blue  = sum(this%pixels(:, 3:this%width:3)) / real(this%width*this%height, kind=rk)

      end select

   end subroutine average_colors
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Calculates the aspect ratio of the image. Required for `print_info` method.
   elemental pure subroutine aspect_ratio(this, ratio)
      class(format_pnm), intent(in) :: this
      real(rk), intent(out) :: ratio
      ratio = real(this%width, kind=rk) / real(this%height, kind=rk)
   end subroutine aspect_ratio
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Resizes the image to specified dimensions.
   elemental pure subroutine resize(this, new_height, new_width)
      class(format_pnm), intent(inout)         :: this
      integer,           intent(in)            :: new_width, new_height
      integer(ik), dimension(:,:), allocatable :: resized_pixels
      integer                                  :: i_out, j_out, i_in, j_in, channel
      real(rk)                                 :: width_scale, height_scale

      select case (this%file_format)
       case ('pbm', 'pgm')

         allocate(resized_pixels(new_height, new_width))

         width_scale  = real(this%width, kind=rk) / real(new_width, kind=rk)
         height_scale = real(this%height, kind=rk) / real(new_height, kind=rk)

         do i_out = 1, new_height
            do j_out = 1, new_width
               i_in = min(this%height, max(1, int((real(i_out, kind=rk) - 0.5_rk) * height_scale) + 1))
               j_in = min(this%width,  max(1, int((real(j_out, kind=rk) - 0.5_rk) * width_scale)  + 1))

               resized_pixels(i_out, j_out) = this%pixels(i_in, j_in)
            end do
         end do

       case ('ppm')

         allocate(resized_pixels(new_height, 3*new_width))

         width_scale  = real(this%width, kind=rk) / real(new_width, kind=rk)
         height_scale = real(this%height, kind=rk) / real(new_height, kind=rk)

         do i_out = 1, new_height
            do j_out = 1, new_width
               i_in = min(this%height,  max(1, int((real(i_out, kind=rk) - 0.5_rk) * height_scale) + 1))
               j_in = min(3*this%width, max(1, int((real(j_out, kind=rk) - 0.5_rk) * width_scale)  + 1))

               do channel = 1, 3
                  resized_pixels(i_out, 3*(j_out-1) + channel) = this%pixels(i_in, 3*(j_in-1)+channel)
               end do
            end do
         end do

      end select

      call this%set_height(new_height)
      call this%set_width(new_width)
      deallocate(this%pixels)
      call this%allocate_pixels()
      call this%set_pixels(resized_pixels)

      deallocate(resized_pixels)
   end subroutine resize
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Crops the image to a specified region.
   elemental pure subroutine crop(this, start_row, end_row, start_col, end_col)
      class(format_pnm), intent(inout)         :: this
      integer,           intent(in)            :: start_row, end_row, start_col, end_col
      integer                                  :: cropped_start_row, cropped_end_row, cropped_start_col, cropped_end_col
      integer(ik), dimension(:,:), allocatable :: cropped_pixels
      integer                                  :: i, j, cropped_height, cropped_width

      ! Check if the cropping coordinates are within the image boundaries
      cropped_start_row = max(1, start_row)
      cropped_end_row   = min(this%height, end_row)
      cropped_start_col = max(1, start_col)
      cropped_end_col   = min(this%width, end_col)

      ! Calculate the dimensions of the cropped image
      cropped_height = cropped_end_row - cropped_start_row + 1
      cropped_width  = cropped_end_col - cropped_start_col + 1

      select case (this%file_format)
       case ('pbm', 'pgm')

         ! Allocate memory for cropped image pixels
         allocate(cropped_pixels(cropped_height, cropped_width))

         ! Copy the cropped pixels to the new array
         do i = 1, cropped_height
            do j = 1, cropped_width
               cropped_pixels(i, j) = this%pixels(cropped_start_row-1+i, (cropped_start_col-1)+j)
            end do
         end do

       case ('ppm')

         ! Allocate memory for cropped image pixels
         allocate(cropped_pixels(cropped_height, 3*cropped_width))

         ! Copy the cropped pixels to the new array
         do i = 1, cropped_height
            do j = 1, 3*cropped_width
               cropped_pixels(i, j) = this%pixels(cropped_start_row-1+i, (cropped_start_col-1)*3+j)
            end do
         end do

      end select

      ! Update image dimensions and pixels
      call this%set_height(cropped_height)
      call this%set_width(cropped_width)
      deallocate(this%pixels)
      call this%allocate_pixels()

      call this%set_pixels(cropped_pixels)

      ! Deallocate temporary array
      deallocate(cropped_pixels)
   end subroutine crop
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Flips the image vertically.
   elemental pure subroutine flip_vertical(this)
      class(format_pnm), intent(inout) :: this

      this%pixels(:,:) = this%pixels(size(this%pixels,1):1:-1, :)
      call this%check_pixel_range(this%pixels)

      call this%set_height(size(this%pixels,1))

      select case (this%file_format)
       case ('pbm', 'pgm')
         call this%set_width(size(this%pixels,2))
       case ('ppm')
         call this%set_width(size(this%pixels,2)/3)
      end select
   end subroutine flip_vertical
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Flips the image horizontally.
   elemental pure subroutine flip_horizontal(this)
      class(format_pnm), intent(inout) :: this

      select case (this%file_format)
       case ('pbm', 'pgm')

         this%pixels(:,:) = this%pixels(:, this%width:1:-1)

         call this%check_pixel_range(this%pixels)

       case ('ppm')

         this%pixels(:, 1:size(this%pixels, 2):3) = this%pixels(:, size(this%pixels, 2)-2:1:-3)
         this%pixels(:, 2:size(this%pixels, 2):3) = this%pixels(:, size(this%pixels, 2)-1:2:-3)
         this%pixels(:, 3:size(this%pixels, 2):3) = this%pixels(:, size(this%pixels, 2)  :3:-3)

         call this%check_pixel_range(this%pixels)

      end select

      call this%set_height(size(this%pixels,1))

      select case (this%file_format)
       case ('pbm', 'pgm')
         call this%set_width(size(this%pixels,2))
       case ('ppm')
         call this%set_width(size(this%pixels,2)/3)
      end select

   end subroutine flip_horizontal
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Rotates the image by a specified angle. Supported angles are 90, 180, 270, -90, -180, -270.
   elemental pure subroutine rotate(this, angle)
      class(format_pnm), intent(inout)         :: this
      integer,           intent(in)            :: angle
      integer(ik), dimension(:,:), allocatable :: rotated_pixels
      integer                                  :: target_height, target_width
      integer                                  :: i, j

      ! Determine the target height and width based on the rotation angle
      select case (angle)
       case (90, -90, 270, -270)
         target_height = this%width
         target_width  = this%height
       case (180, -180)
         target_height = this%height
         target_width  = this%width
       case default
         error stop "Invalid rotation angle. Valid angles are 90, 180, 270, -90, -180, -270."
      end select

      select case (this%file_format)
       case ('pbm', 'pgm')

         ! Allocate memory for rotated_pixels array
         allocate(rotated_pixels(target_height, target_width))

         ! Rotate pixels based on the specified angle
         select case (angle)
          case (90, -270)
            do i = 1, this%height
               do j = 1, this%width
                  rotated_pixels(j, this%height-i+1) = this%pixels(i, j)
               end do
            end do
          case (180, -180)
            do i = 1, this%height
               do j = 1, this%width
                  rotated_pixels(this%height-i+1, this%width-j+1) = this%pixels(i, j)
               end do
            end do
          case (270, -90)
            do i = 1, this%height
               do j = 1, this%width
                  rotated_pixels(this%width-j+1, i) = this%pixels(i, j)
               end do
            end do
         end select

       case ('ppm')

         ! Allocate memory for rotated_pixels array
         allocate(rotated_pixels(target_height, 3*target_width))

         ! Rotate pixels based on the specified angle
         select case (angle)
          case (90, -270)
            do i = 1, this%height
               do j = 1, this%width
                  rotated_pixels(j, 3*(this%height-i+1)-2:3*(this%height-i+1)) = this%pixels(i, 3*j-2:3*j)
               end do
            end do
          case (180, -180)
            do i = 1, this%height
               do j = 1, this%width
                  rotated_pixels(this%height-i+1, 3*(this%width-j+1)-2:3*(this%width-j+1)) = this%pixels(i, 3*j-2:3*j)
               end do
            end do
          case (270, -90)
            do i = 1, this%height
               do j = 1, this%width
                  rotated_pixels(this%width-j+1, 3*i-2:3*i) = this%pixels(i, 3*j-2:3*j)
               end do
            end do
         end select

      end select

      ! Update height and width of the image
      call this%set_height(target_height)
      call this%set_width(target_width)

      deallocate(this%pixels)
      call this%allocate_pixels()

      ! Update the original pixels with rotated pixels
      call this%set_pixels(rotated_pixels)

      ! Deallocate rotated_pixels array
      deallocate(rotated_pixels)
   end subroutine rotate
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Converts a color image to greyscale. Only supported for PPM images.
   elemental pure subroutine greyscale(this)
      class(format_pnm), intent(inout) :: this
      integer                          :: i, j

      ! Check if the file is ppm
      if (this%file_format /= 'ppm') error stop 'greyscale: This function is only for ppm files.'

      do i = 1, this%height
         do j = 1, this%width
            ! Calculate the ITU Rec.709 weighted average of RGB channels to derive a greyscale value and assign it as an integer to all RGB channels.
            this%pixels(i, 3*j-2:3*j) = int(0.2126_rk * real(this%pixels(i, 3*j-2), kind=rk) + &
                                            0.7152_rk * real(this%pixels(i, 3*j-1), kind=rk) + &
                                            0.0722_rk * real(this%pixels(i, 3*j-0), kind=rk), kind=ik)
         end do
      end do

      call this%check_pixel_range(this%pixels)

   end subroutine greyscale
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Removes one or more RGB channels from the image. Only supported for PPM images.
   elemental pure subroutine remove_channels(this, remove_r, remove_g, remove_b)
      class(format_pnm), intent(inout) :: this
      logical, optional, intent(in)    :: remove_r, remove_g, remove_b

      ! Check if the file is ppm
      if (this%file_format /= 'ppm') error stop 'remove_channels: This function is only for ppm files.'

      ! Remove R channel
      if (present(remove_r)) then
         if (remove_r) then
            this%pixels(:,1:size(this%pixels,2):3) = 0_ik
         end if
      end if

      ! Remove G channel
      if (present(remove_g)) then
         if (remove_g) then
            this%pixels(:,2:size(this%pixels,2):3) = 0_ik
         end if
      end if

      ! Remove B channel
      if (present(remove_b)) then
         if (remove_b) then
            this%pixels(:,3:size(this%pixels,2):3) = 0_ik
         end if
      end if

      call this%check_pixel_range(this%pixels)

   end subroutine remove_channels
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Swaps the RGB channels of the image. Only supported for PPM images.
   elemental pure subroutine swap_channels(this, swap)
      class(format_pnm), intent(inout) :: this
      character(*),      intent(in)    :: swap
      integer(ik)                      :: temp
      integer                          :: i, j

      ! Check if the file is ppm
      if (this%file_format /= 'ppm') error stop 'swap_channels: This function is only for ppm files.'

      ! Swap R and G channels
      if (swap=='rg' .or. swap=='gr' .or. swap=='RG' .or. swap=='GR') then
         do i = 1, this%height
            do j = 1, this%width
               temp = this%pixels(i, 3*j-2)
               this%pixels(i, 3*j-2) = this%pixels(i, 3*j-1)
               this%pixels(i, 3*j-1) = temp
            end do
         end do
      end if

      ! Swap G and B channels
      if (swap=='gb' .or. swap=='bg' .or. swap=='GB' .or. swap=='BG') then
         do i = 1, this%height
            do j = 1, this%width
               temp = this%pixels(i, 3*j-1)
               this%pixels(i, 3*j-1) = this%pixels(i, 3*j-0)
               this%pixels(i, 3*j-0) = temp
            end do
         end do
      end if

      ! Swap R and B channels
      if (swap=='rb' .or. swap=='br' .or. swap=='RB' .or. swap=='BR') then
         do i = 1, this%height
            do j = 1, this%width
               temp = this%pixels(i, 3*j-2)
               this%pixels(i, 3*j-2) = this%pixels(i, 3*j-0)
               this%pixels(i, 3*j-0) = temp
            end do
         end do
      end if

      call this%check_pixel_range(this%pixels)
   end subroutine swap_channels
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Brightens or darkens the image. Only supported for PGM and PPM images.
   elemental pure subroutine brighten(this, factor)
      class(format_pnm), intent(inout) :: this
      integer,           intent(in)    :: factor

      select case (this%file_format)
       case ('pbm')
         error stop 'brighten: This function is not supported for pbm files.'
       case ('pgm', 'ppm')
         call this%set_pixels(min(this%max_color, max(0, this%pixels + factor)))
      end select
   end subroutine brighten
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Inverts the colors of the image.
   elemental pure subroutine negative(this)
      class(format_pnm), intent(inout) :: this

      call this%set_pixels(this%max_color - this%pixels)
   end subroutine negative
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the file format of the PNM image. Supported values are `pbm`, `pgm`, and `ppm`.
   elemental pure subroutine set_file_format(this, file_format)
      class(format_pnm), intent(inout) :: this
      character(3), intent(in)         :: file_format

      this%file_format = file_format
   end subroutine set_file_format
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the encoding of the PNM image. Supported values are `ascii` or `plain` and `binary` or `raw`.
   elemental pure subroutine set_format(this, encoding)
      class(format_pnm), intent(inout) :: this
      character(*), intent(in)         :: encoding

      this%encoding = trim(encoding)
   end subroutine set_format
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Gets the encoding of the PNM image.
   pure function get_format(this) result(encoding)
      class(format_pnm), intent(in) :: this
      character(:), allocatable        :: encoding

      encoding = trim(this%encoding)
   end function get_format
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Deallocates memory for the PNM image.
   elemental pure subroutine deallocate_pnm(this)
      class(format_pnm), intent(inout)    :: this
      if (allocated(this%pixels)) deallocate(this%pixels)
      if (allocated(this%comment)) deallocate(this%comment)
   end subroutine deallocate_pnm
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Reads a PNM image from a file.
   impure subroutine import_pnm(this, file_name, file_format, encoding)
      class(format_pnm), intent(inout)       :: this
      character(*),      intent(in)          :: file_name, encoding
      character(3),      intent(in)          :: file_format
      integer                                :: nunit, iostat
      character, dimension(:), allocatable   :: buffer_ch
      integer(ik), dimension(:), allocatable :: buffer_int
      logical                                :: file_exists
      integer                                :: pos

      inquire(file=file_name//'.'//file_format, exist=file_exists)
      if (file_exists) then

         call this%set_file_format(file_format)
         call this%set_format(encoding)

         select case (this%encoding)
          case ('binary','raw')

            select case (file_format)
             case ('pbm')
               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, form='formatted', access='stream', action='read', status='old')
               if (iostat /= 0) error stop 'Error opening the file.'
               call read_header(this, nunit, pos)
               close(nunit)
               allocate(buffer_ch(this%height*this%width))
               buffer_ch = achar(0_ik)

               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, access='stream', form='unformatted', action='read', status='old', position='append')
               if (iostat /= 0) error stop 'Error opening the file.'
               read(nunit, iostat=iostat, pos=pos) buffer_ch
               if (iostat /= 0) error stop 'Error reading the file.'
               call this%allocate_pixels()
               this%pixels = iachar(transpose(reshape(buffer_ch, [this%width, this%height])), kind=ik)
               close(nunit)
             case ('pgm')
               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, form='formatted', access='stream', action='read', status='old')
               if (iostat /= 0) error stop 'Error opening the file.'
               call read_header(this, nunit, pos)
               close(nunit)
               allocate(buffer_ch(this%height*this%width))
               buffer_ch = achar(0_ik)

               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, access='stream', form='unformatted', action='read', status='old', position='append')
               if (iostat /= 0) error stop 'Error opening the file.'
               read(nunit, iostat=iostat, pos=pos) buffer_ch
               if (iostat /= 0) error stop 'Error reading the file.'
               call this%allocate_pixels()
               this%pixels = iachar(transpose(reshape(buffer_ch, [this%width, this%height])), kind=ik)
               close(nunit)
             case ('ppm')
               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, form='formatted', access='stream', action='read', status='old')
               if (iostat /= 0) error stop 'Error opening the file.'
               call read_header(this, nunit, pos)
               close(nunit)
               allocate(buffer_ch(this%height*3*this%width))
               buffer_ch = achar(0_ik)

               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, access='stream', form='unformatted', action='read', status='old', position='append')
               if (iostat /= 0) error stop 'Error opening the file.'
               read(nunit,iostat=iostat, pos=pos) buffer_ch
               call this%allocate_pixels()
               this%pixels = iachar(transpose(reshape(buffer_ch, [this%width*3, this%height])), kind=ik)
               close(nunit)
            end select

          case ('ascii','plain')

            select case (file_format)
             case ('pbm')
               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, access='stream', form='formatted', action='read', status='old')
               if (iostat /= 0) error stop 'Error opening the file.'
               call read_header(this, nunit, pos)
               allocate(buffer_int(this%height*this%width))
               buffer_int = 0_ik
               read(nunit, *) buffer_int
               call this%allocate_pixels()
               this%pixels = transpose(reshape(buffer_int, [this%width, this%height]))
               close(nunit)
               call this%check_pixel_range(this%pixels)
             case ('pgm')
               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, access='stream', form='formatted', action='read', status='old')
               if (iostat /= 0) error stop 'Error opening the file.'
               call read_header(this, nunit, pos)
               allocate(buffer_int(this%height*this%width))
               buffer_int = 0_ik
               read(nunit, *) buffer_int
               call this%allocate_pixels()
               this%pixels = transpose(reshape(buffer_int, [this%width, this%height]))
               call this%check_pixel_range(this%pixels)
               close(nunit)
             case ('ppm')
               open (newunit = nunit, file = file_name//'.'//file_format,&
               iostat=iostat, access='stream', form='formatted', action='read', status='old')
               if (iostat /= 0) error stop 'Error opening the file.'
               call read_header(this, nunit, pos)
               allocate(buffer_int(this%height*3*this%width))
               buffer_int = 0_ik
               read(nunit, *) buffer_int
               call this%allocate_pixels()
               this%pixels = transpose(reshape(buffer_int, [this%width*3, this%height]))
               call this%check_pixel_range(this%pixels)
               close(nunit)
            end select

         end select

      else
         error stop 'Error: File does not exist.'
      end if
   end subroutine import_pnm
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the attributes of the PNM image.
   pure subroutine set_pnm(this, encoding, file_format,width,height,max_color,comment,pixels)
      class(format_pnm),           intent(inout) :: this
      integer,                     intent(in)    :: width
      integer,                     intent(in)    :: height
      character(*),                intent(in)    :: comment
      integer, optional,           intent(in)    :: max_color
      integer(ik), dimension(:,:), intent(in)    :: pixels
      character(*),                intent(in)    :: encoding
      character(3),                intent(in)    :: file_format
      character(2)                               :: magic_number

      call this%set_format(encoding)
      call this%set_file_format(file_format)

      select case (this%encoding)
       case ('ascii','plain')
         select case (this%file_format)
          case ('pbm')
            magic_number = 'P1'
          case ('pgm')
            magic_number = 'P2'
          case ('ppm')
            magic_number = 'P3'
         end select
       case ('binary','raw')
         select case (this%file_format)
          case ('pbm')
            magic_number = 'P4'
          case ('pgm')
            magic_number = 'P5'
          case ('ppm')
            magic_number = 'P6'
         end select
      end select

      call this%set_header(magic_number,width,height,comment,max_color)
      call this%allocate_pixels()
      call this%set_pixels(pixels)
   end subroutine set_pnm
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Allocates memory for the pixels of the PNM image.
   elemental pure subroutine allocate_pixels(this)
      class(format_pnm), intent(inout) :: this
      select case(this%magic_number)
       case('P1')
         if (.not.allocated(this%pixels)) allocate(this%pixels(this%height, this%width))
       case('P2')
         if (.not.allocated(this%pixels)) allocate(this%pixels(this%height, this%width))
       case('P3')
         if (.not.allocated(this%pixels)) allocate(this%pixels(this%height, 3*this%width))
       case('P4')
         if (.not.allocated(this%pixels)) allocate(this%pixels(this%height, this%width))
       case('P5')
         if (.not.allocated(this%pixels)) allocate(this%pixels(this%height, this%width))
       case('P6')
         if (.not.allocated(this%pixels)) allocate(this%pixels(this%height, 3*this%width))
      end select
   end subroutine allocate_pixels
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the magic number of the PNM image. Supported values are `P1`, `P2`, `P3`, `P4`, `P5`, and `P6`.
   elemental pure subroutine set_magicnumber(this, magic_number)
      class(format_pnm), intent(inout) :: this
      character(*), intent(in)         :: magic_number
      this%magic_number = magic_number
   end subroutine set_magicnumber
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the width of the PNM image.
   elemental pure subroutine set_width(this, width)
      class(format_pnm), intent(inout) :: this
      integer, intent(in)              :: width
      this%width = width
   end subroutine set_width
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the height of the PNM image.
   elemental pure subroutine set_height(this, height)
      class(format_pnm), intent(inout) :: this
      integer, intent(in)              :: height
      this%height = height
   end subroutine set_height
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets a comment for the PNM image.
   elemental pure subroutine set_comment(this, comment)
      class(format_pnm), intent(inout) :: this
      character(*), intent(in)         :: comment
      this%comment = comment
   end subroutine set_comment
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the maximum color value of the PNM image. Only required for PGM and PPM images
   elemental pure subroutine set_max_color(this, max_color)
      class(format_pnm), intent(inout) :: this
      integer, intent(in)              :: max_color
      this%max_color = max_color
   end subroutine set_max_color
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the header of the PNM image. The header includes the magic number, width, height, comment, and max_color.
   elemental pure subroutine set_header(this, magic_number, width, height, comment, max_color)
      class(format_pnm), intent(inout) :: this
      character(*), intent(in)         :: magic_number
      integer, intent(in)              :: width
      integer, intent(in)              :: height
      character(*), intent(in)         :: comment
      integer, optional, intent(in)    :: max_color

      call this%set_magicnumber(magic_number)
      call this%set_width(width)
      call this%set_height(height)
      call this%set_comment(comment)
      if (this%file_format /= 'pbm') call this%set_max_color(max_color)
   end subroutine set_header
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Checks if the pixel values are within the valid range.
   pure subroutine check_pixel_range(this, pixels)
      class(format_pnm),           intent(inout) :: this
      integer(ik), dimension(:,:), intent(in)    :: pixels

      ! Check if the pixel values are within the valid range
      select case (this%file_format)
       case ('pbm')
         if (maxval(pixels) > 1 .or. minval(pixels) < 0)&
         error stop 'set_pixels: Invalid pixel values. Valid values are 0 and 1.'
       case ('pgm')
         if (maxval(pixels) > this%max_color .or. minval(pixels) < 0)&
         error stop 'set_pixels: Invalid pixel values. Valid values are between 0 and max_color.'
       case ('ppm')
         if (maxval(pixels) > this%max_color .or. minval(pixels) < 0)&
         error stop 'set_pixels: Invalid pixel values. Valid values are between 0 and max_color.'
      end select
   end subroutine check_pixel_range
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the pixel values of the PNM image.
   pure subroutine set_pixels(this, pixels)
      class(format_pnm), intent(inout) :: this
      integer(ik), dimension(:,:), intent(in) :: pixels

      call this%check_pixel_range(pixels)

      this%pixels = pixels
   end subroutine set_pixels
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Sets the pixel values of the PNM image.
   elemental pure subroutine set_pixel(this, grey,r,g,b, i, j)
      class(format_pnm), intent(inout) :: this
      integer, intent(in), optional    :: grey
      integer, intent(in), optional    :: r, g, b
      integer, intent(in)              :: i, j
      select case(this%magic_number)
       case('P1')
         this%pixels(i,j) = grey
       case('P2')
         this%pixels(i,j) = grey
       case('P3')
         this%pixels(i,3*j-2) = r
         this%pixels(i,3*j-1) = g
         this%pixels(i,3*j-0) = b
      end select
   end subroutine set_pixel
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Writes the PNM image to a file.
   impure subroutine export_pnm(this, file_name, encoding)
      class(format_pnm), intent(inout)            :: this
      character(*),      intent(in)               :: file_name
      character(*),      intent(in), optional     :: encoding
      integer                                     :: nunit
      integer                                     :: iostat

      if (present(encoding)) then
         call this%set_format(encoding)

         select case (this%encoding)
          case ('ascii','plain')
            select case (this%file_format)
             case ('pbm')
               this%magic_number = 'P1'
             case ('pgm')
               this%magic_number = 'P2'
             case ('ppm')
               this%magic_number = 'P3'
            end select
          case ('binary','raw')
            select case (this%file_format)
             case ('pbm')
               this%magic_number = 'P4'
             case ('pgm')
               this%magic_number = 'P5'
             case ('ppm')
               this%magic_number = 'P6'
            end select
         end select
      end if

      select case (this%magic_number)
       case ('P1', 'P2', 'P3')
         open (newunit = nunit, file = file_name//'.'//this%file_format,&
         status='replace', iostat=iostat, form='formatted', action='write')
         if (iostat /= 0) error stop 'Error opening the file.'
         call write_header(this, nunit)
         close(nunit)

         open (newunit = nunit, file = file_name//'.'//this%file_format,&
         status='old', iostat=iostat, form='formatted', action='write', position='append')
         if (iostat /= 0) error stop 'Error opening the file.'
         write(nunit, '(*(g0,1x))', advance='no') transpose(this%pixels)
         close(nunit)
       case ('P4', 'P5', 'P6')
         open (newunit = nunit, file = file_name//'.'//this%file_format,&
         status='replace', iostat=iostat, form='formatted', action='write')
         if (iostat /= 0) error stop 'Error opening the file.'
         call write_header(this, nunit)
         close(nunit)

         open (newunit = nunit, file = file_name//'.'//this%file_format,&
         status='old', iostat=iostat, access='stream', form='unformatted', action='write', position='append')
         if (iostat /= 0) error stop 'Error opening the file.'
         write(nunit) transpose(achar(this%pixels))
         close(nunit)
      end select
   end subroutine export_pnm
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Writes the header of the PNM image to a file. Required before writing the pixels to the file.
   subroutine write_header(this, nunit)
      type(format_pnm), intent(in) :: this
      integer, intent(in)           :: nunit
      integer :: i, k

      ! Write magic number
      write(nunit,'(a)') this%magic_number

      ! Write comments
      k = ceiling(real(len(adjustl(this%comment)))/70.0)
      if (len(adjustl(this%comment)) /=0 .and. len(adjustl(this%comment)) <= 70) then
         write(nunit,'(a,a)') '# ',trim(adjustl(this%comment))
      else if (len(adjustl(this%comment)) /=0 .and. len(adjustl(this%comment)) > 70 ) then
         do i = 1, k-1
            write(nunit,'(a,a)') '# ',adjustl(this%comment(70*(i-1)+1:70*(i-1)+70))
         end do
         write(nunit,'(a,a)') '# ',trim(adjustl(this%comment(70*(k-1)+1:)))
      end if

      ! Write width, height and max_color
      write(nunit, '(g0,1x,g0)') this%width, this%height
      if (this%file_format /= 'pbm') write(nunit,'(g0)') this%max_color
   end subroutine write_header
   !===============================================================================


   !===============================================================================
   !> author: Seyed Ali Ghasemi
   !> license: BSD 3-Clause
   !> Reads the header of the PNM image from a file. Required before reading the pixels from the file.
   subroutine read_header(this, nunit, pos)
      class(format_pnm), intent(inout) :: this
      integer, intent(in)           :: nunit
      integer, intent(out)          :: pos
      character(len=70) :: comment
      character :: temp
      integer :: i, k

      read(nunit,*)
      k = 0
      do
         read(nunit,'(a)') temp
         if (temp /= '#') exit
         k = k + 1
      end do
      inquire(nunit, pos=pos)
      
      rewind(nunit)
      read(nunit,*) this%magic_number
      this%comment = ''
      do i = 1, k
         read(nunit,'(a,a,a)') temp, temp, comment
         this%comment = this%comment//comment
      end do
      read(nunit,*) this%width, this%height
      inquire(nunit, pos=pos)

      if (this%file_format == 'pgm' .or. this%file_format == 'ppm') then
         read(nunit,*) this%max_color
         inquire(nunit, pos=pos)
      end if
   end subroutine read_header
   !===============================================================================

end module pnm