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