example: add 24 game task of Rosetta code
[nit.git] / lib / bitmap / bitmap.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2015 Budi Kurniawan <budi2020@gmail.com>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # The Bitmap class represents a 24-bit bitmap image. An instance can be constructed
18 # by calling load or with_size:
19 #
20 # new Bitmap.load(path_to_a_bmp_file)
21 # new Bitmap.with_size(400, 300)
22 #
23 # The width and height attributes contain the image's width and height,
24 # respectively.
25 #
26 # Individual pixels can be manipulated by calling the set_pixel function:
27 #
28 # set_pixel(x, y, color)
29 #
30 # The no-argument grayscale function converts the bitmap to grayscale and is an
31 # implementation of this Rossetacode task:
32 # http://rosettacode.org/wiki/Grayscale_image
33 #
34 # Finally, the bitmap can be saved to a file by calling save:
35 #
36 # save(path_to_a_file)
37 #
38 # For information on the bitmap format, see
39 # http://en.wikipedia.org/wiki/BMP_file_format
40 #
41 # A module containing classes for manipulating bitmap images
42 module bitmap
43 #
44 # A class that represents a Bitmap image
45 class Bitmap
46 # The file path if this Bitmap is created by loading a bitmap file
47 var file_path: String is noinit
48
49 # The file extension if this Bitmap is created by loading a bitmap file
50 var file_extension: String is noinit
51
52 # The file size if this Bitmap is created by loading a bitmap file
53 var file_size: Int is noinit
54
55 # The image width in pixels
56 var width: Int is noinit
57
58 # The image height in pixels
59 var height: Int is noinit
60
61 # The offset of image data. Typically, 54
62 private var data_offset: Int is noinit
63
64 # The number of bits representing a pixel. Currently only 24-bit bitmaps are supported
65 private var bits_per_pixel: Int is noinit
66
67 # The size of images. For a 24-bit bitmap, this is equal to (3*width*height)
68 private var image_size: Int is noinit
69
70 # 14-byte bitmap header
71 private var bitmap_header: Array[Int] = [66, 77, 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0]
72
73 # 40-byte dib header
74 private var dib_header: Array[Int] = [40, 0, 0, 0, 0, 0, 0, 0, 0, 0,
75 0, 0, 1, 0, 24, 0, 0, 0, 0, 0,
76 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
77 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
78
79 # The image data, each cell contains a pixel.
80 # The first 8 least-significant bits represent the red component,
81 # the next 8 least-significant bits represent the green component,
82 # and the next 8 bit represent the blue component.
83 # The last 8 most-significant bits are not used
84 private var data = new Array[Array[Int]]
85
86 # Constructs a Bitmap object by passing the image's width and height (in pixels)
87 init with_size(width: Int, height: Int)
88 do
89 self.width = width
90 self.height = height
91 self.bits_per_pixel = 24
92 self.data_offset = 54
93 #set width and height
94
95 self.set_value(dib_header, 4, width)
96 self.set_value(dib_header, 8, height)
97 #set file size
98 var file_size = 3 * width * height + 54
99 self.set_value(bitmap_header, 2, file_size)
100
101 #init pixel data
102 self.data = new Array[Array[Int]].with_capacity(height)
103 for x in [0..height[ do
104 var row = new Array[Int].with_capacity(width)
105 for y in [0..width[ do
106 row.add(0x00FFFFFF)
107 end
108 self.data[x] = row
109 end
110 end
111
112 # Creates an instance of Bitmap by loading an existing image file
113 init load(path: String)
114 do
115 self.file_path = path
116 var temp = path.file_extension
117 if temp != null then
118 self.file_extension = temp
119 else
120 self.file_extension = ""
121 end
122 var fileReader = new FileReader.open(path)
123
124 # =============== Bitmap header ================
125 for x in [0..13] do
126 bitmap_header[x] = fileReader.read(1)[0].ascii
127 end
128 self.file_size = get_value(bitmap_header.subarray(2, 4))
129 self.data_offset = get_value(bitmap_header.subarray(10, 4))
130
131 # =============== DIB header ================
132 for x in [0..39] do
133 dib_header[x] = fileReader.read(1)[0].ascii
134 end
135 var dib_size = get_value(dib_header.subarray(0, 4))
136 # only support BITMAPINFOHEADER
137 if dib_size != 40 then
138 print "This type of bitmap is not supported"
139 fileReader.close
140 return
141 end
142
143 self.width = get_value(dib_header.subarray(4, 4))
144 self.height = get_value(dib_header.subarray(8, 4))
145 self.bits_per_pixel = get_value(dib_header.subarray(14, 2))
146 self.image_size = get_value(dib_header.subarray(20, 4))
147
148 if self.bits_per_pixel != 24 then
149 print "Only full color bitmaps are supported"
150 fileReader.close
151 return
152 end
153
154 if self.bits_per_pixel == 24 then
155 # assert self.image_size + 54 <= self.file_size
156 # start loading image data, for now assume no padding
157 for x in [0..self.height[
158 do
159 var row = new Array[Int].with_capacity(self.width)
160 for y in [0..self.width[
161 do
162 var red = fileReader.read(1)[0].ascii * 256 * 256
163 var green = fileReader.read(1)[0].ascii * 256
164 var blue = fileReader.read(1)[0].ascii
165 row.add(red + green + blue)
166 end
167 self.data.add(row)
168 end
169 end
170 fileReader.close
171 end #end of load_from_file method
172
173 # Reads in a series of bytes from the FileReader when loading a Bitmap from a file
174 # FileReader.read(1) is used due to https://github.com/privat/nit/issues/1264
175 private fun read_chars(fr: FileReader, howMany: Int): String
176 do
177 var s = ""
178 for x in [1..howMany]
179 do
180 s += fr.read(1)
181 end
182 return s
183 end
184
185 # Converts the value contained in two or four bytes into an Int. Since Nit
186 # does not have a byte type, Int is used
187 private fun get_value(array: Array[Int]): Int
188 do
189 var value = 0
190 for x in [0..array.length[ do
191 value += array[x] * 256.to_f.pow(x.to_f).to_i
192 end
193 return value
194 end
195
196 # Converts the value in an Int to four bytes. Since Nit does not have a byte
197 # type, Int is used.
198 private fun set_value(array: Array[Int], start_index: Int, value: Int)
199 do
200 array[start_index] = value.bin_and(0x000000FF)
201 array[start_index + 1] = value.rshift(8).bin_and(0x000000FF)
202 array[start_index + 2] = value.rshift(16).bin_and(0x000000FF)
203 array[start_index + 3] = value.rshift(24).bin_and(0x000000FF)
204 end
205
206 # Saves the bitmap into a file
207 fun save(path: String)
208 do
209 var fw = new FileWriter.open(path)
210 # Write bitmap header
211 for x in [0..self.bitmap_header.length[ do
212 fw.write(self.bitmap_header[x].ascii.to_s)
213 end
214 # Write dib header
215 for x in [0..self.dib_header.length[ do
216 fw.write(self.dib_header[x].ascii.to_s)
217 end
218 # Write color table (if any)
219 # Write data (no padding for now)
220 for x in [0..self.height[ do
221 var row = self.data[x]
222 for y in [0..self.width[ do
223 var pixel = row[y]
224 var red = pixel.rshift(16)
225 var green = pixel.bin_and(0x00FF00).rshift(8)
226 var blue = pixel.bin_and(0x000000FF)
227 fw.write(red.ascii.to_s)
228 fw.write(green.ascii.to_s)
229 fw.write(blue.ascii.to_s)
230 end
231 end
232 fw.close
233 end #end of save
234
235 # Converts the bitmap to grayscale by manipulating each individual pixel
236 fun grayscale
237 do
238 for x in [0..self.height[ do
239 var row = self.data[x]
240 for y in [0..self.width[ do
241 var pixel = row[y]
242 var red = pixel.rshift(16)
243 var green = pixel.bin_and(0x00FF00).rshift(8)
244 var blue = pixel.bin_and(0x000000FF)
245 var lum = (0.2126 * red.to_f + 0.7152 * green.to_f + 0.0722 * blue.to_f).to_i
246 pixel = lum * 256 * 256 + lum * 256 + lum
247 self.data[x][y] = pixel
248 end
249 end
250 end
251
252 # Sets an individual pixel, where position (0, 0) represents the top-left pixel.
253 fun set_pixel(x: Int, y: Int, color: Int)
254 do
255 if x >= 0 and y >= 0 and x < self.width and y < self.height then
256 # Since a bitmap stores its rows of pixels upside-down, y is mapped to
257 # height - 1 - y to make (0, 0) the top-left pixel
258 self.data[self.height - 1 - y][x] = color
259 end
260 end
261 end #end of class Bitmap