/src/FreeRDP/winpr/libwinpr/utils/image.c
Line | Count | Source (jump to first uncovered line) |
1 | | /** |
2 | | * WinPR: Windows Portable Runtime |
3 | | * Image Utils |
4 | | * |
5 | | * Copyright 2014 Marc-Andre Moreau <marcandre.moreau@gmail.com> |
6 | | * Copyright 2016 Inuvika Inc. |
7 | | * Copyright 2016 David PHAM-VAN <d.phamvan@inuvika.com> |
8 | | * |
9 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
10 | | * you may not use this file except in compliance with the License. |
11 | | * You may obtain a copy of the License at |
12 | | * |
13 | | * http://www.apache.org/licenses/LICENSE-2.0 |
14 | | * |
15 | | * Unless required by applicable law or agreed to in writing, software |
16 | | * distributed under the License is distributed on an "AS IS" BASIS, |
17 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
18 | | * See the License for the specific language governing permissions and |
19 | | * limitations under the License. |
20 | | */ |
21 | | |
22 | | #include <winpr/config.h> |
23 | | |
24 | | #include <winpr/wtypes.h> |
25 | | #include <winpr/crt.h> |
26 | | #include <winpr/file.h> |
27 | | |
28 | | #include <winpr/image.h> |
29 | | |
30 | | #if defined(WITH_LODEPNG) |
31 | | #include <lodepng.h> |
32 | | #endif |
33 | | #include <winpr/stream.h> |
34 | | |
35 | | #include "../log.h" |
36 | | #define TAG WINPR_TAG("utils.image") |
37 | | |
38 | | static BOOL writeBitmapFileHeader(wStream* s, const WINPR_BITMAP_FILE_HEADER* bf) |
39 | 0 | { |
40 | 0 | if (!Stream_EnsureRemainingCapacity(s, sizeof(WINPR_BITMAP_FILE_HEADER))) |
41 | 0 | return FALSE; |
42 | | |
43 | 0 | Stream_Write_UINT8(s, bf->bfType[0]); |
44 | 0 | Stream_Write_UINT8(s, bf->bfType[1]); |
45 | 0 | Stream_Write_UINT32(s, bf->bfSize); |
46 | 0 | Stream_Write_UINT16(s, bf->bfReserved1); |
47 | 0 | Stream_Write_UINT16(s, bf->bfReserved2); |
48 | 0 | Stream_Write_UINT32(s, bf->bfOffBits); |
49 | 0 | return TRUE; |
50 | 0 | } |
51 | | |
52 | | static BOOL readBitmapFileHeader(wStream* s, WINPR_BITMAP_FILE_HEADER* bf) |
53 | 0 | { |
54 | 0 | if (!s || !bf || (!Stream_CheckAndLogRequiredLength(TAG, s, sizeof(WINPR_BITMAP_FILE_HEADER)))) |
55 | 0 | return FALSE; |
56 | | |
57 | 0 | Stream_Read_UINT8(s, bf->bfType[0]); |
58 | 0 | Stream_Read_UINT8(s, bf->bfType[1]); |
59 | 0 | Stream_Read_UINT32(s, bf->bfSize); |
60 | 0 | Stream_Read_UINT16(s, bf->bfReserved1); |
61 | 0 | Stream_Read_UINT16(s, bf->bfReserved2); |
62 | 0 | Stream_Read_UINT32(s, bf->bfOffBits); |
63 | 0 | return TRUE; |
64 | 0 | } |
65 | | |
66 | | static BOOL writeBitmapInfoHeader(wStream* s, const WINPR_BITMAP_INFO_HEADER* bi) |
67 | 0 | { |
68 | 0 | if (!Stream_EnsureRemainingCapacity(s, sizeof(WINPR_BITMAP_INFO_HEADER))) |
69 | 0 | return FALSE; |
70 | | |
71 | 0 | Stream_Write_UINT32(s, bi->biSize); |
72 | 0 | Stream_Write_INT32(s, bi->biWidth); |
73 | 0 | Stream_Write_INT32(s, bi->biHeight); |
74 | 0 | Stream_Write_UINT16(s, bi->biPlanes); |
75 | 0 | Stream_Write_UINT16(s, bi->biBitCount); |
76 | 0 | Stream_Write_UINT32(s, bi->biCompression); |
77 | 0 | Stream_Write_UINT32(s, bi->biSizeImage); |
78 | 0 | Stream_Write_INT32(s, bi->biXPelsPerMeter); |
79 | 0 | Stream_Write_INT32(s, bi->biYPelsPerMeter); |
80 | 0 | Stream_Write_UINT32(s, bi->biClrUsed); |
81 | 0 | Stream_Write_UINT32(s, bi->biClrImportant); |
82 | 0 | return TRUE; |
83 | 0 | } |
84 | | |
85 | | static BOOL readBitmapInfoHeader(wStream* s, WINPR_BITMAP_INFO_HEADER* bi) |
86 | 0 | { |
87 | 0 | if (!s || !bi || (!Stream_CheckAndLogRequiredLength(TAG, s, sizeof(WINPR_BITMAP_INFO_HEADER)))) |
88 | 0 | return FALSE; |
89 | | |
90 | 0 | Stream_Read_UINT32(s, bi->biSize); |
91 | 0 | Stream_Read_INT32(s, bi->biWidth); |
92 | 0 | Stream_Read_INT32(s, bi->biHeight); |
93 | 0 | Stream_Read_UINT16(s, bi->biPlanes); |
94 | 0 | Stream_Read_UINT16(s, bi->biBitCount); |
95 | 0 | Stream_Read_UINT32(s, bi->biCompression); |
96 | 0 | Stream_Read_UINT32(s, bi->biSizeImage); |
97 | 0 | Stream_Read_INT32(s, bi->biXPelsPerMeter); |
98 | 0 | Stream_Read_INT32(s, bi->biYPelsPerMeter); |
99 | 0 | Stream_Read_UINT32(s, bi->biClrUsed); |
100 | 0 | Stream_Read_UINT32(s, bi->biClrImportant); |
101 | 0 | return TRUE; |
102 | 0 | } |
103 | | |
104 | | BYTE* winpr_bitmap_construct_header(size_t width, size_t height, size_t bpp) |
105 | 0 | { |
106 | 0 | BYTE* result = NULL; |
107 | 0 | WINPR_BITMAP_FILE_HEADER bf = { 0 }; |
108 | 0 | WINPR_BITMAP_INFO_HEADER bi = { 0 }; |
109 | 0 | wStream* s; |
110 | 0 | size_t imgSize; |
111 | |
|
112 | 0 | imgSize = width * height * (bpp / 8); |
113 | 0 | if ((width > INT32_MAX) || (height > INT32_MAX) || (bpp > UINT16_MAX) || (imgSize > UINT32_MAX)) |
114 | 0 | return NULL; |
115 | | |
116 | 0 | s = Stream_New(NULL, WINPR_IMAGE_BMP_HEADER_LEN); |
117 | 0 | if (!s) |
118 | 0 | return NULL; |
119 | | |
120 | 0 | bf.bfType[0] = 'B'; |
121 | 0 | bf.bfType[1] = 'M'; |
122 | 0 | bf.bfReserved1 = 0; |
123 | 0 | bf.bfReserved2 = 0; |
124 | 0 | bf.bfOffBits = (UINT32)sizeof(WINPR_BITMAP_FILE_HEADER) + sizeof(WINPR_BITMAP_INFO_HEADER); |
125 | 0 | bi.biSizeImage = (UINT32)imgSize; |
126 | 0 | bf.bfSize = bf.bfOffBits + bi.biSizeImage; |
127 | 0 | bi.biWidth = (INT32)width; |
128 | 0 | bi.biHeight = -1 * (INT32)height; |
129 | 0 | bi.biPlanes = 1; |
130 | 0 | bi.biBitCount = (UINT16)bpp; |
131 | 0 | bi.biCompression = 0; |
132 | 0 | bi.biXPelsPerMeter = (INT32)width; |
133 | 0 | bi.biYPelsPerMeter = (INT32)height; |
134 | 0 | bi.biClrUsed = 0; |
135 | 0 | bi.biClrImportant = 0; |
136 | 0 | bi.biSize = (UINT32)sizeof(WINPR_BITMAP_INFO_HEADER); |
137 | |
|
138 | 0 | if (!writeBitmapFileHeader(s, &bf)) |
139 | 0 | goto fail; |
140 | | |
141 | 0 | if (!writeBitmapInfoHeader(s, &bi)) |
142 | 0 | goto fail; |
143 | | |
144 | 0 | result = Stream_Buffer(s); |
145 | 0 | fail: |
146 | 0 | Stream_Free(s, result == 0); |
147 | 0 | return result; |
148 | 0 | } |
149 | | |
150 | | /** |
151 | | * Refer to "Compressed Image File Formats: JPEG, PNG, GIF, XBM, BMP" book |
152 | | */ |
153 | | |
154 | | int winpr_bitmap_write(const char* filename, const BYTE* data, size_t width, size_t height, |
155 | | size_t bpp) |
156 | 0 | { |
157 | 0 | return winpr_bitmap_write_ex(filename, data, 0, width, height, bpp); |
158 | 0 | } |
159 | | |
160 | | int winpr_bitmap_write_ex(const char* filename, const BYTE* data, size_t stride, size_t width, |
161 | | size_t height, size_t bpp) |
162 | 0 | { |
163 | 0 | FILE* fp = NULL; |
164 | 0 | BYTE* bmp_header = NULL; |
165 | 0 | const size_t bpp_stride = width * (bpp / 8); |
166 | |
|
167 | 0 | if (stride == 0) |
168 | 0 | stride = bpp_stride; |
169 | |
|
170 | 0 | int ret = -1; |
171 | 0 | fp = winpr_fopen(filename, "w+b"); |
172 | |
|
173 | 0 | if (!fp) |
174 | 0 | { |
175 | 0 | WLog_ERR(TAG, "failed to open file %s", filename); |
176 | 0 | return -1; |
177 | 0 | } |
178 | | |
179 | 0 | bmp_header = winpr_bitmap_construct_header(width, height, bpp); |
180 | 0 | if (!bmp_header) |
181 | 0 | goto fail; |
182 | | |
183 | 0 | if (fwrite(bmp_header, WINPR_IMAGE_BMP_HEADER_LEN, 1, fp) != 1) |
184 | 0 | goto fail; |
185 | | |
186 | 0 | for (size_t y = 0; y < height; y++) |
187 | 0 | { |
188 | 0 | const void* line = &data[stride * y]; |
189 | 0 | if (fwrite(line, bpp_stride, 1, fp) != 1) |
190 | 0 | goto fail; |
191 | 0 | } |
192 | | |
193 | 0 | ret = 1; |
194 | 0 | fail: |
195 | 0 | if (fp) |
196 | 0 | fclose(fp); |
197 | 0 | free(bmp_header); |
198 | 0 | return ret; |
199 | 0 | } |
200 | | |
201 | | int winpr_image_write(wImage* image, const char* filename) |
202 | 0 | { |
203 | 0 | int status = -1; |
204 | |
|
205 | 0 | if (image->type == WINPR_IMAGE_BITMAP) |
206 | 0 | { |
207 | 0 | status = winpr_bitmap_write(filename, image->data, image->width, image->height, |
208 | 0 | image->bitsPerPixel); |
209 | 0 | } |
210 | | #if defined(WITH_LODEPNG) |
211 | | else |
212 | | { |
213 | | unsigned lodepng_status; |
214 | | lodepng_status = lodepng_encode32_file(filename, image->data, image->width, image->height); |
215 | | status = (lodepng_status) ? -1 : 1; |
216 | | } |
217 | | #endif |
218 | 0 | return status; |
219 | 0 | } |
220 | | |
221 | | #if defined(WITH_LODEPNG) |
222 | | static int winpr_image_png_read_fp(wImage* image, FILE* fp) |
223 | | { |
224 | | INT64 size; |
225 | | BYTE* data; |
226 | | UINT32 width; |
227 | | UINT32 height; |
228 | | unsigned lodepng_status; |
229 | | _fseeki64(fp, 0, SEEK_END); |
230 | | size = _ftelli64(fp); |
231 | | _fseeki64(fp, 0, SEEK_SET); |
232 | | if (size < 0) |
233 | | return -1; |
234 | | |
235 | | data = (BYTE*)malloc((size_t)size); |
236 | | |
237 | | if (!data) |
238 | | return -1; |
239 | | |
240 | | if (fread((void*)data, (size_t)size, 1, fp) != 1) |
241 | | { |
242 | | free(data); |
243 | | return -1; |
244 | | } |
245 | | |
246 | | lodepng_status = lodepng_decode32(&(image->data), &width, &height, data, (size_t)size); |
247 | | free(data); |
248 | | |
249 | | if (lodepng_status) |
250 | | return -1; |
251 | | |
252 | | image->width = width; |
253 | | image->height = height; |
254 | | image->bitsPerPixel = 32; |
255 | | image->bytesPerPixel = 4; |
256 | | image->scanline = image->bytesPerPixel * image->width; |
257 | | return 1; |
258 | | } |
259 | | |
260 | | static int winpr_image_png_read_buffer(wImage* image, const BYTE* buffer, size_t size) |
261 | | { |
262 | | UINT32 width; |
263 | | UINT32 height; |
264 | | unsigned lodepng_status = lodepng_decode32(&(image->data), &width, &height, buffer, size); |
265 | | |
266 | | if (lodepng_status) |
267 | | return -1; |
268 | | |
269 | | image->width = width; |
270 | | image->height = height; |
271 | | image->bitsPerPixel = 32; |
272 | | image->bytesPerPixel = 4; |
273 | | image->scanline = image->bytesPerPixel * image->width; |
274 | | return 1; |
275 | | } |
276 | | #endif |
277 | | |
278 | | static int winpr_image_bitmap_read_fp(wImage* image, FILE* fp) |
279 | 0 | { |
280 | 0 | int rc = -1; |
281 | 0 | UINT32 index; |
282 | 0 | BOOL vFlip; |
283 | 0 | BYTE* pDstData; |
284 | 0 | wStream* s; |
285 | 0 | wStream sbuffer = { 0 }; |
286 | 0 | BYTE buffer[sizeof(WINPR_BITMAP_FILE_HEADER) + sizeof(WINPR_BITMAP_INFO_HEADER)] = { 0 }; |
287 | 0 | WINPR_BITMAP_FILE_HEADER bf = { 0 }; |
288 | 0 | WINPR_BITMAP_INFO_HEADER bi = { 0 }; |
289 | |
|
290 | 0 | if (!image || !fp) |
291 | 0 | return -1; |
292 | | |
293 | 0 | image->data = NULL; |
294 | |
|
295 | 0 | s = Stream_StaticInit(&sbuffer, buffer, sizeof(buffer)); |
296 | |
|
297 | 0 | if (!s) |
298 | 0 | return -1; |
299 | | |
300 | 0 | if (fread(Stream_Buffer(s), Stream_Capacity(s), 1, fp) != 1) |
301 | 0 | goto fail; |
302 | | |
303 | 0 | if (!readBitmapFileHeader(s, &bf) || !readBitmapInfoHeader(s, &bi)) |
304 | 0 | goto fail; |
305 | | |
306 | 0 | if ((bf.bfType[0] != 'B') || (bf.bfType[1] != 'M')) |
307 | 0 | goto fail; |
308 | | |
309 | 0 | image->type = WINPR_IMAGE_BITMAP; |
310 | |
|
311 | 0 | if (_ftelli64(fp) != bf.bfOffBits) |
312 | 0 | _fseeki64(fp, bf.bfOffBits, SEEK_SET); |
313 | |
|
314 | 0 | if (bi.biWidth < 0) |
315 | 0 | goto fail; |
316 | | |
317 | 0 | image->width = (UINT32)bi.biWidth; |
318 | |
|
319 | 0 | if (bi.biHeight < 0) |
320 | 0 | { |
321 | 0 | vFlip = FALSE; |
322 | 0 | image->height = (UINT32)(-1 * bi.biHeight); |
323 | 0 | } |
324 | 0 | else |
325 | 0 | { |
326 | 0 | vFlip = TRUE; |
327 | 0 | image->height = (UINT32)bi.biHeight; |
328 | 0 | } |
329 | |
|
330 | 0 | image->bitsPerPixel = bi.biBitCount; |
331 | 0 | image->bytesPerPixel = (image->bitsPerPixel / 8); |
332 | 0 | image->scanline = (bi.biSizeImage / image->height); |
333 | 0 | image->data = (BYTE*)malloc(bi.biSizeImage); |
334 | |
|
335 | 0 | if (!image->data) |
336 | 0 | goto fail; |
337 | | |
338 | 0 | if (!vFlip) |
339 | 0 | { |
340 | 0 | if (fread((void*)image->data, bi.biSizeImage, 1, fp) != 1) |
341 | 0 | goto fail; |
342 | 0 | } |
343 | 0 | else |
344 | 0 | { |
345 | 0 | pDstData = &(image->data[(image->height - 1) * image->scanline]); |
346 | |
|
347 | 0 | for (index = 0; index < image->height; index++) |
348 | 0 | { |
349 | 0 | if (fread((void*)pDstData, image->scanline, 1, fp) != 1) |
350 | 0 | goto fail; |
351 | | |
352 | 0 | pDstData -= image->scanline; |
353 | 0 | } |
354 | 0 | } |
355 | | |
356 | 0 | rc = 1; |
357 | 0 | fail: |
358 | |
|
359 | 0 | if (rc < 0) |
360 | 0 | { |
361 | 0 | free(image->data); |
362 | 0 | image->data = NULL; |
363 | 0 | } |
364 | |
|
365 | 0 | return 1; |
366 | 0 | } |
367 | | |
368 | | static int winpr_image_bitmap_read_buffer(wImage* image, const BYTE* buffer, size_t size) |
369 | 0 | { |
370 | 0 | int rc = -1; |
371 | 0 | UINT32 index; |
372 | 0 | BOOL vFlip; |
373 | 0 | BYTE* pDstData; |
374 | 0 | WINPR_BITMAP_FILE_HEADER bf; |
375 | 0 | WINPR_BITMAP_INFO_HEADER bi; |
376 | 0 | wStream sbuffer = { 0 }; |
377 | 0 | wStream* s = Stream_StaticConstInit(&sbuffer, buffer, size); |
378 | |
|
379 | 0 | if (!s) |
380 | 0 | return -1; |
381 | | |
382 | 0 | if (!readBitmapFileHeader(s, &bf) || !readBitmapInfoHeader(s, &bi)) |
383 | 0 | goto fail; |
384 | | |
385 | 0 | if ((bf.bfType[0] != 'B') || (bf.bfType[1] != 'M')) |
386 | 0 | goto fail; |
387 | | |
388 | 0 | image->type = WINPR_IMAGE_BITMAP; |
389 | |
|
390 | 0 | if (Stream_GetPosition(s) > bf.bfOffBits) |
391 | 0 | goto fail; |
392 | 0 | if (!Stream_SafeSeek(s, bf.bfOffBits - Stream_GetPosition(s))) |
393 | 0 | goto fail; |
394 | 0 | if (!Stream_CheckAndLogRequiredCapacity(TAG, s, bi.biSizeImage)) |
395 | 0 | goto fail; |
396 | | |
397 | 0 | if (bi.biWidth < 0) |
398 | 0 | goto fail; |
399 | | |
400 | 0 | image->width = (UINT32)bi.biWidth; |
401 | |
|
402 | 0 | if (bi.biHeight < 0) |
403 | 0 | { |
404 | 0 | vFlip = FALSE; |
405 | 0 | image->height = (UINT32)(-1 * bi.biHeight); |
406 | 0 | } |
407 | 0 | else |
408 | 0 | { |
409 | 0 | vFlip = TRUE; |
410 | 0 | image->height = (UINT32)bi.biHeight; |
411 | 0 | } |
412 | |
|
413 | 0 | image->bitsPerPixel = bi.biBitCount; |
414 | 0 | image->bytesPerPixel = (image->bitsPerPixel / 8); |
415 | 0 | image->scanline = (bi.biSizeImage / image->height); |
416 | 0 | image->data = (BYTE*)malloc(bi.biSizeImage); |
417 | |
|
418 | 0 | if (!image->data) |
419 | 0 | goto fail; |
420 | | |
421 | 0 | if (!vFlip) |
422 | 0 | Stream_Read(s, image->data, bi.biSizeImage); |
423 | 0 | else |
424 | 0 | { |
425 | 0 | pDstData = &(image->data[(image->height - 1) * image->scanline]); |
426 | |
|
427 | 0 | for (index = 0; index < image->height; index++) |
428 | 0 | { |
429 | 0 | Stream_Read(s, pDstData, image->scanline); |
430 | 0 | pDstData -= image->scanline; |
431 | 0 | } |
432 | 0 | } |
433 | |
|
434 | 0 | rc = 1; |
435 | 0 | fail: |
436 | |
|
437 | 0 | if (rc < 0) |
438 | 0 | { |
439 | 0 | free(image->data); |
440 | 0 | image->data = NULL; |
441 | 0 | } |
442 | |
|
443 | 0 | return rc; |
444 | 0 | } |
445 | | |
446 | | int winpr_image_read(wImage* image, const char* filename) |
447 | 0 | { |
448 | 0 | FILE* fp; |
449 | 0 | BYTE sig[8]; |
450 | 0 | int status = -1; |
451 | |
|
452 | 0 | fp = winpr_fopen(filename, "rb"); |
453 | |
|
454 | 0 | if (!fp) |
455 | 0 | { |
456 | 0 | WLog_ERR(TAG, "failed to open file %s", filename); |
457 | 0 | return -1; |
458 | 0 | } |
459 | | |
460 | 0 | if (fread((void*)&sig, sizeof(sig), 1, fp) != 1 || _fseeki64(fp, 0, SEEK_SET) < 0) |
461 | 0 | { |
462 | 0 | fclose(fp); |
463 | 0 | return -1; |
464 | 0 | } |
465 | | |
466 | 0 | if ((sig[0] == 'B') && (sig[1] == 'M')) |
467 | 0 | { |
468 | 0 | image->type = WINPR_IMAGE_BITMAP; |
469 | 0 | status = winpr_image_bitmap_read_fp(image, fp); |
470 | 0 | } |
471 | | #if defined(WITH_LODEPNG) |
472 | | else if ((sig[0] == 0x89) && (sig[1] == 'P') && (sig[2] == 'N') && (sig[3] == 'G') && |
473 | | (sig[4] == '\r') && (sig[5] == '\n') && (sig[6] == 0x1A) && (sig[7] == '\n')) |
474 | | { |
475 | | image->type = WINPR_IMAGE_PNG; |
476 | | status = winpr_image_png_read_fp(image, fp); |
477 | | } |
478 | | #endif |
479 | |
|
480 | 0 | fclose(fp); |
481 | 0 | return status; |
482 | 0 | } |
483 | | |
484 | | int winpr_image_read_buffer(wImage* image, const BYTE* buffer, size_t size) |
485 | 0 | { |
486 | 0 | BYTE sig[8]; |
487 | 0 | int status = -1; |
488 | |
|
489 | 0 | if (size < 8) |
490 | 0 | return -1; |
491 | | |
492 | 0 | CopyMemory(sig, buffer, 8); |
493 | |
|
494 | 0 | if ((sig[0] == 'B') && (sig[1] == 'M')) |
495 | 0 | { |
496 | 0 | image->type = WINPR_IMAGE_BITMAP; |
497 | 0 | status = winpr_image_bitmap_read_buffer(image, buffer, size); |
498 | 0 | } |
499 | | #if defined(WITH_LODEPNG) |
500 | | else if ((sig[0] == 0x89) && (sig[1] == 'P') && (sig[2] == 'N') && (sig[3] == 'G') && |
501 | | (sig[4] == '\r') && (sig[5] == '\n') && (sig[6] == 0x1A) && (sig[7] == '\n')) |
502 | | { |
503 | | image->type = WINPR_IMAGE_PNG; |
504 | | status = winpr_image_png_read_buffer(image, buffer, size); |
505 | | } |
506 | | #endif |
507 | |
|
508 | 0 | return status; |
509 | 0 | } |
510 | | |
511 | | wImage* winpr_image_new(void) |
512 | 0 | { |
513 | 0 | wImage* image; |
514 | 0 | image = (wImage*)calloc(1, sizeof(wImage)); |
515 | |
|
516 | 0 | if (!image) |
517 | 0 | return NULL; |
518 | | |
519 | 0 | return image; |
520 | 0 | } |
521 | | |
522 | | void winpr_image_free(wImage* image, BOOL bFreeBuffer) |
523 | 0 | { |
524 | 0 | if (!image) |
525 | 0 | return; |
526 | | |
527 | 0 | if (bFreeBuffer) |
528 | 0 | free(image->data); |
529 | |
|
530 | 0 | free(image); |
531 | 0 | } |