Bitmap class as part of the bitmap library
authorBudi Kurniawan <budi2020@gmail.com>
Sun, 19 Apr 2015 14:11:25 +0000 (10:11 -0400)
committerBudi Kurniawan <budi2020@gmail.com>
Mon, 11 May 2015 22:42:31 +0000 (18:42 -0400)
Signed-off-by: Budi Kurniawan <budi2020@gmail.com>

lib/bitmap/bitmap.nit [new file with mode: 0644]
lib/bitmap/test_bitmap.nit [new file with mode: 0644]

diff --git a/lib/bitmap/bitmap.nit b/lib/bitmap/bitmap.nit
new file mode 100644 (file)
index 0000000..0343b8e
--- /dev/null
@@ -0,0 +1,261 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2015 Budi Kurniawan <budi2020@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# The Bitmap class represents a 24-bit bitmap image. An instance can be constructed
+# by calling load or with_size:
+#
+#      new Bitmap.load(path_to_a_bmp_file)
+#      new Bitmap.with_size(400, 300)
+#
+# The width and height attributes contain the image's width and height,
+# respectively.
+#
+# Individual pixels can be manipulated by calling the set_pixel function:
+#
+#      set_pixel(x, y, color)
+#
+# The no-argument grayscale function converts the bitmap to grayscale and is an
+# implementation of this Rossetacode task:
+# http://rosettacode.org/wiki/Grayscale_image
+#
+# Finally, the bitmap can be saved to a file by calling save:
+#
+#      save(path_to_a_file)
+#
+# For information on the bitmap format, see
+# http://en.wikipedia.org/wiki/BMP_file_format
+#
+# A module containing classes for manipulating bitmap images
+module bitmap
+#
+# A class that represents a Bitmap image
+class Bitmap
+       # The file path if this Bitmap is created by loading a bitmap file
+       var file_path: String is noinit
+
+       # The file extension if this Bitmap is created by loading a bitmap file
+       var file_extension: String is noinit
+
+       # The file size if this Bitmap is created by loading a bitmap file
+       var file_size: Int is noinit
+
+       # The image width in pixels
+       var width: Int is noinit
+
+       # The image height in pixels
+       var height: Int is noinit
+
+       # The offset of image data. Typically, 54
+       private var data_offset: Int is noinit
+
+       # The number of bits representing a pixel. Currently only 24-bit bitmaps are supported
+       private var bits_per_pixel: Int is noinit
+
+       # The size of images. For a 24-bit bitmap, this is equal to (3*width*height)
+       private var image_size: Int is noinit
+
+       # 14-byte bitmap header
+       private var bitmap_header: Array[Int] = [66, 77, 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0]
+
+       # 40-byte dib header
+       private var dib_header: Array[Int] = [40, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                       0, 0, 1, 0, 24, 0, 0, 0, 0, 0,
+                       0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                       0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+
+       # The image data, each cell contains a pixel.
+       # The first 8 least-significant bits represent the red component,
+       # the next 8 least-significant bits represent the green component,
+       # and the next 8 bit represent the blue component.
+       # The last 8 most-significant bits are not used
+       private var data = new Array[Array[Int]]
+
+       # Constructs a Bitmap object by passing the image's width and height (in pixels)
+       init with_size(width: Int, height: Int)
+       do
+               self.width = width
+               self.height = height
+               self.bits_per_pixel = 24
+               self.data_offset = 54
+               #set width and height
+
+               self.set_value(dib_header, 4, width)
+               self.set_value(dib_header, 8, height)
+               #set file size
+               var file_size = 3 * width * height + 54
+               self.set_value(bitmap_header, 2, file_size)
+
+               #init pixel data
+               self.data = new Array[Array[Int]].with_capacity(height)
+               for x in [0..height[ do
+                       var row = new Array[Int].with_capacity(width)
+                       for y in [0..width[ do
+                               row.add(0x00FFFFFF)
+                       end
+                       self.data[x] = row
+               end
+       end
+
+       # Creates an instance of Bitmap by loading an existing image file
+       init load(path: String)
+       do
+               self.file_path = path
+               var temp = path.file_extension
+               if temp != null then
+                       self.file_extension = temp
+               else
+                       self.file_extension = ""
+               end
+               var fileReader = new FileReader.open(path)
+
+               # =============== Bitmap header ================
+               for x in [0..13] do
+                       bitmap_header[x] = fileReader.read(1)[0].ascii
+               end
+               self.file_size = get_value(bitmap_header.subarray(2, 4))
+               self.data_offset = get_value(bitmap_header.subarray(10, 4))
+
+               # =============== DIB header ================
+               for x in [0..39] do
+                       dib_header[x] = fileReader.read(1)[0].ascii
+               end
+               var dib_size = get_value(dib_header.subarray(0, 4))
+               # only support BITMAPINFOHEADER
+               if dib_size != 40 then
+                       print "This type of bitmap is not supported"
+                       fileReader.close
+                       return
+               end
+
+               self.width = get_value(dib_header.subarray(4, 4))
+               self.height = get_value(dib_header.subarray(8, 4))
+               self.bits_per_pixel = get_value(dib_header.subarray(14, 2))
+               self.image_size = get_value(dib_header.subarray(20, 4))
+
+               if self.bits_per_pixel != 24 then
+                       print "Only full color bitmaps are supported"
+                       fileReader.close
+                       return
+               end
+
+               if self.bits_per_pixel == 24 then
+                       # assert self.image_size + 54 <= self.file_size
+                       # start loading image data, for now assume no padding
+                       for x in [0..self.height[
+                       do
+                               var row = new Array[Int].with_capacity(self.width)
+                               for y in [0..self.width[
+                               do
+                                       var red = fileReader.read(1)[0].ascii * 256 * 256
+                                       var green = fileReader.read(1)[0].ascii * 256
+                                       var blue = fileReader.read(1)[0].ascii
+                                       row.add(red + green + blue)
+                               end
+                               self.data.add(row)
+                       end
+               end
+               fileReader.close
+       end #end of load_from_file method
+
+       # Reads in a series of bytes from the FileReader when loading a Bitmap from a file
+       # FileReader.read(1) is used due to https://github.com/privat/nit/issues/1264
+       private fun read_chars(fr: FileReader, howMany: Int): String
+       do
+               var s = ""
+               for x in [1..howMany]
+               do
+                       s += fr.read(1)
+               end
+               return s
+       end
+
+       # Converts the value contained in two or four bytes into an Int. Since Nit
+       # does not have a byte type, Int is used
+       private fun get_value(array: Array[Int]): Int
+       do
+               var value = 0
+               for x in [0..array.length[ do
+                       value += array[x] * 256.to_f.pow(x.to_f).to_i
+               end
+               return value
+       end
+
+       # Converts the value in an Int to four bytes. Since Nit does not have a byte
+       # type, Int is used.
+       private fun set_value(array: Array[Int], start_index: Int, value: Int)
+       do
+               array[start_index] = value.bin_and(0x000000FF)
+               array[start_index + 1] = value.rshift(8).bin_and(0x000000FF)
+               array[start_index + 2] = value.rshift(16).bin_and(0x000000FF)
+               array[start_index + 3] = value.rshift(24).bin_and(0x000000FF)
+       end
+
+       # Saves the bitmap into a file
+       fun save(path: String)
+       do
+               var fw = new FileWriter.open(path)
+               # Write bitmap header
+               for x in [0..self.bitmap_header.length[ do
+                       fw.write(self.bitmap_header[x].ascii.to_s)
+               end
+               # Write dib header
+               for x in [0..self.dib_header.length[ do
+                       fw.write(self.dib_header[x].ascii.to_s)
+               end
+               # Write color table (if any)
+               # Write data (no padding for now)
+               for x in [0..self.height[ do
+                       var row = self.data[x]
+                       for y in [0..self.width[ do
+                               var pixel = row[y]
+                               var red = pixel.rshift(16)
+                               var green = pixel.bin_and(0x00FF00).rshift(8)
+                               var blue = pixel.bin_and(0x000000FF)
+                               fw.write(red.ascii.to_s)
+                               fw.write(green.ascii.to_s)
+                               fw.write(blue.ascii.to_s)
+                       end
+               end
+               fw.close
+       end #end of save
+
+       # Converts the bitmap to grayscale by manipulating each individual pixel
+       fun grayscale
+       do
+               for x in [0..self.height[ do
+                       var row = self.data[x]
+                       for y in [0..self.width[ do
+                               var pixel = row[y]
+                               var red = pixel.rshift(16)
+                               var green = pixel.bin_and(0x00FF00).rshift(8)
+                               var blue = pixel.bin_and(0x000000FF)
+                               var lum = (0.2126 * red.to_f + 0.7152 * green.to_f + 0.0722 * blue.to_f).to_i
+                               pixel = lum * 256 * 256 + lum * 256 + lum
+                               self.data[x][y] = pixel
+                       end
+               end
+       end
+
+       # Sets an individual pixel, where position (0, 0) represents the top-left pixel.
+       fun set_pixel(x: Int, y: Int, color: Int)
+       do
+               if x >= 0 and y >= 0 and x < self.width and y < self.height then
+                       # Since a bitmap stores its rows of pixels upside-down, y is mapped to
+                       # height - 1 - y to make (0, 0) the top-left pixel
+                       self.data[self.height - 1 - y][x] = color
+               end
+       end
+end #end of class Bitmap
diff --git a/lib/bitmap/test_bitmap.nit b/lib/bitmap/test_bitmap.nit
new file mode 100644 (file)
index 0000000..4475689
--- /dev/null
@@ -0,0 +1,44 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2015 Budi Kurniawan <budi2020@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This file tests the Bitmap class in the bitmap module
+# It has to be called by passing two arguments:
+#
+#   - An input bitmap file
+#   - An output bitmap file
+#
+# It converts the input bitmap to grayscale and save it to the specified
+# output file. In addition, it creates a blank.bmp file in the current directory.
+#
+#
+# A module for testing the classes in the bitmap module
+module test_bitmap is test_suite
+
+import bitmap
+import test_suite
+
+class TestBitmap
+       super TestSuite
+
+       fun test_grayscale do
+               var bitmap = new Bitmap.with_size(400, 300)
+               for y in [0..300] do
+                       for x in [0..200] do bitmap.set_pixel(x, y, 0x0077AAAA)
+               end
+               bitmap.grayscale
+               bitmap.save("./blank.bmp")
+       end
+end