1// tImageBMP.cpp 
2// 
3// This class knows how to load and save windows bitmap (.bmp) files into tPixel4b arrays. These tPixels may be 'stolen' by the 
4// tPicture's constructor if a targa file is specified. After the array is stolen the tImageBMP is invalid. This is 
5// purely for performance. 
6// 
7// The code in this module is a modification of code from https://github.com/phm97/bmp under the BSD 2-Clause License: 
8// 
9// Copyright (c) 2019, phm97 
10// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 
11// following conditions are met: 
12// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 
13// disclaimer. 
14// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 
15// following disclaimer in the documentation and/or other materials provided with the distribution. 
16// 
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
18// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
19// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
20// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
21// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
22// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
23// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24// 
25// The modifications to use Tacent datatypes and conversion to C++ are under the ISC licence: 
26// 
27// Copyright (c) 2020, 2022-2024 Tristan Grimmer. 
28// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
29// granted, provided that the above copyright notice and this permission notice appear in all copies. 
30// 
31// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
32// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
33// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
34// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
35// PERFORMANCE OF THIS SOFTWARE. 
36 
37#include <System/tFile.h> 
38#include <Foundation/tArray.h> 
39#include "Image/tImageBMP.h" 
40#include "Image/tPicture.h" 
41using namespace tSystem
42namespace tImage 
43
44 
45 
46bool tImageBMP::Load(const tString& bmpFile
47
48 Clear(); 
49 
50 if ((tSystem::tGetFileType(file: bmpFile) != tSystem::tFileType::BMP) || !tFileExists(file: bmpFile)) 
51 return false
52 
53 tFileHandle file = tOpenFile(file: bmpFile.Chars(), mode: "rb"); 
54 if (!file
55 return false
56  
57 Header bmpHeader
58 tStaticAssert(sizeof(bmpHeader) == 14); 
59 tReadFile(handle: file, buffer: &bmpHeader, sizeBytes: 14); 
60 if (bmpHeader.FourCC != FourCC
61
62 tCloseFile(f: file); 
63 return false
64
65 
66 InfoHeader infoHeader
67 tStaticAssert(sizeof(infoHeader) == 40); 
68 tReadFile(handle: file, buffer: &infoHeader, sizeBytes: 40); 
69 
70 // Some sanity-checking. 
71 // @todo Support jpeg and png compression. 
72 // @todo Support more than one plane. 
73 if 
74
75 ((infoHeader.HeaderSize != 40) && (infoHeader.HeaderSize != 108) && (infoHeader.HeaderSize != 124)) || 
76 (infoHeader.NumPlanes != 1) || 
77 ((infoHeader.Compression == 4) || (infoHeader.Compression == 5)) 
78
79
80 tCloseFile(f: file); 
81 return false
82
83 
84 Width = infoHeader.Width
85 Height = infoHeader.Height
86 
87 bool flipped = false
88 if (Height < 0
89
90 flipped = true
91 Height = -Height
92
93 
94 // Is this bmp indexed (using a palette)? 
95 PaletteColour* palette = nullptr
96 if (infoHeader.BPP <= 8
97
98 tFileSeek(file, offsetBytes: 14 + infoHeader.HeaderSize, tSeekOrigin::Set); 
99 if (infoHeader.ColoursUsed == 0
100
101 // Only 1, 4, and 8 bit indexes allowed. 
102 if ((infoHeader.BPP != 1) && (infoHeader.BPP != 4) && (infoHeader.BPP != 8)) 
103
104 tCloseFile(f: file); 
105 Clear(); // Clears Width and Height. 
106 return false
107
108 infoHeader.ColoursUsed = 1 << infoHeader.BPP
109
110 palette = new PaletteColour[infoHeader.ColoursUsed]; 
111 tReadFile(handle: file, buffer: palette, sizeBytes: sizeof(PaletteColour)*infoHeader.ColoursUsed); 
112
113 
114 uint8* buf = new uint8[Width * Height * 4]; 
115 tStd::tMemset(dest: buf, val: 0x00, numBytes: Width*Height * 4); 
116 tFileSeek(file, offsetBytes: bmpHeader.Offset, tSeekOrigin::Set); 
117 PixelFormatSrc = tPixelFormat::R8G8B8A8
118 
119 switch (infoHeader.BPP
120
121 case 32
122 ReadRow_Pixels32(file, dest: buf); 
123 PixelFormatSrc = tPixelFormat::R8G8B8A8
124 break
125 
126 case 24
127 ReadRow_Pixels24(file, dest: buf); 
128 PixelFormatSrc = tPixelFormat::R8G8B8
129 break
130 
131 case 16:  
132 ReadRow_Pixels16(file, dest: buf); 
133 PixelFormatSrc = tPixelFormat::G3B5A1R5G2
134 break
135 
136 case 8
137 tAssert(palette); 
138 if (infoHeader.Compression == 1
139 ReadRow_IndexedRLE8(file, dest: buf, palette); 
140 else 
141 ReadRow_Indexed8(file, dest: buf, palette); 
142 PixelFormatSrc = tPixelFormat::PAL8BIT
143 break
144 
145 case 4
146 tAssert(palette); 
147 if (infoHeader.Compression == 2
148 ReadRow_IndexedRLE4(file, dest: buf, palette); 
149 else 
150 ReadRow_Indexed4(file, dest: buf, palette); 
151 PixelFormatSrc = tPixelFormat::PAL4BIT
152 break
153 
154 case 1
155 tAssert(palette); 
156 ReadRow_Indexed1(file, dest: buf, palette); 
157 PixelFormatSrc = tPixelFormat::PAL1BIT
158 break
159
160 
161 Pixels = (tPixel4b*)buf
162 delete[] palette
163 tCloseFile(f: file); 
164 
165 if (flipped
166
167 tPixel4b* newPixels = new tPixel4b[Width * Height]; 
168 for (int y = 0; y < Height; y++) 
169 for (int x = 0; x < Width; x++) 
170 newPixels[ y*Width + x ] = Pixels[ (Height-1-y)*Width + x ]; 
171 delete[] Pixels
172 Pixels = newPixels
173
174 
175 PixelFormat = tPixelFormat::R8G8B8A8
176 ColourProfileSrc = tColourProfile::sRGB
177 ColourProfile = tColourProfile::sRGB
178  
179 return true
180
181 
182 
183void tImageBMP::ReadRow_Pixels32(tFileHandle file, uint8* dest
184
185 for (int p = 0; p < Width*Height; p++) 
186
187 uint8 pixel[4]; 
188 tReadFile(handle: file, buffer: pixel, sizeBytes: 4); 
189 *(dest++) = pixel[2]; 
190 *(dest++) = pixel[1]; 
191 *(dest++) = pixel[0]; 
192 *(dest++) = pixel[3]; 
193
194
195 
196 
197void tImageBMP::ReadRow_Pixels24(tFileHandle file, uint8* dest
198
199 int rowPad = (Width*3 % 4) ? 4 - (Width*3 % 4) : 0
200 for (int y = 0; y < Height; y++) 
201
202 for (int x = 0; x < Width; x++) 
203
204 uint8 pixel[3]; 
205 tReadFile(handle: file, buffer: pixel, sizeBytes: 3); 
206 *(dest++) = pixel[2]; 
207 *(dest++) = pixel[1]; 
208 *(dest++) = pixel[0]; 
209 *(dest++) = 0xFF
210
211 
212 if (rowPad
213 tFileSeek(file, offsetBytes: rowPad, tSeekOrigin::Current); 
214
215
216 
217 
218void tImageBMP::ReadRow_Pixels16(tFileHandle file, uint8* dest
219
220 int rowPad = (Width*2 % 4) ? 4 - (Width*2 % 4) : 0
221 for (int y = 0; y < Height; y++) 
222
223 for (int x = 0; x < Width; x++) 
224
225 uint16 pixel
226 tReadFile(handle: file, buffer: &pixel, sizeBytes: 2); 
227 uint16 b = (pixel >> 10) & 0x1F
228 uint16 g = (pixel >> 5) & 0x1F
229 uint16 r = (pixel >> 0) & 0x1F
230 *(dest++) = r*8
231 *(dest++) = g*8
232 *(dest++) = b*8
233 *(dest++) = 0xFF; // @todo Correct? Should we not read the 1-bit alpha? 
234
235 
236 if (rowPad
237 tFileSeek(file, offsetBytes: rowPad, tSeekOrigin::Current); 
238
239
240 
241 
242void tImageBMP::ReadRow_Indexed8(tFileHandle file, uint8* dest, PaletteColour* palette
243
244 int rowPad = (Width % 4) ? 4 - (Width % 4) : 0
245 for (int y = 0; y < Height; y++) 
246
247 for (int x = 0; x < Width; x++) 
248
249 uint8 index
250 tReadFile(handle: file, buffer: &index, sizeBytes: 1); 
251 *(dest++) = palette[index].R
252 *(dest++) = palette[index].G
253 *(dest++) = palette[index].B
254 *(dest++) = 0xFF
255
256 if (rowPad
257 tFileSeek(file, offsetBytes: rowPad, tSeekOrigin::Current); 
258
259
260 
261 
262void tImageBMP::ReadRow_Indexed4(tFileHandle file, uint8* dest, PaletteColour* palette
263
264 int size = (Width+1) / 2
265 int rowPad = (size % 4) ? 4 - (size % 4) : 0
266 tArray<uint8> rowStride(size); 
267 
268 for (int y = 0; y < Height; y++) 
269
270 tReadFile(handle: file, buffer: (uint8*)rowStride, sizeBytes: size); 
271 for (int x = 0; x < Width; x++) 
272
273 uint8 byte = rowStride[x/2]; 
274 uint8 index = (x % 2) ? 0x0F & byte : byte >> 4
275 *(dest++) = palette[index].R
276 *(dest++) = palette[index].G
277 *(dest++) = palette[index].B
278 *(dest++) = 0xFF
279
280 if (rowPad
281 tFileSeek(file, offsetBytes: rowPad, tSeekOrigin::Current); 
282
283
284 
285 
286void tImageBMP::ReadRow_Indexed1(tFileHandle file, uint8* dest, PaletteColour* palette
287
288 int size = (Width + 7) / 8
289 int rowPad = (size % 4) ? 4 - (size % 4) : 0
290 tArray<uint8> rowStride(size); 
291  
292 for (int y = 0; y < Height; y++) 
293
294 tReadFile(handle: file, buffer: (uint8*)rowStride, sizeBytes: size); 
295 for (int x = 0; x < Width; x++) 
296
297 int bit = (x % 8) + 1
298 uint8 byte = rowStride[x/8]; 
299 uint8 index = byte >> (8-bit); 
300 index &= 0x01
301 *(dest++) = palette[index].R
302 *(dest++) = palette[index].G
303 *(dest++) = palette[index].B
304 *(dest++) = 0xFF
305
306 if (rowPad
307 tFileSeek(file, offsetBytes: rowPad, tSeekOrigin::Current); 
308
309
310 
311 
312void tImageBMP::ReadRow_IndexedRLE8(tFileHandle file, uint8* dest, PaletteColour* palette
313
314 uint8 byte = 0
315 int currentLine = 0
316 uint8* d = dest
317 
318 bool keepReading = true
319 while (keepReading
320
321 byte = tGetc(file); 
322 if (byte
323
324 int index = tGetc(file); 
325 for (int i = 0; i < byte; i++) 
326
327 *d++ = palette[index].R
328 *d++ = palette[index].G
329 *d++ = palette[index].B
330 *d++ = 0xFF
331
332
333 else 
334
335 byte = tGetc(file); 
336 switch (byte)  
337
338 case 0
339 currentLine++; 
340 d = dest + currentLine*Width*4
341 break
342 
343 case 1
344 keepReading = false
345 break
346 
347 case 2
348
349 int xoffset = tGetc(file); 
350 int yoffset = tGetc(file); 
351 currentLine += yoffset
352 d += yoffset*Width*4 + xoffset*4
353 break
354
355 
356 default
357
358 for (int i = 0; i < byte; i++) 
359
360 int index = tGetc(file); 
361 *d++ = palette[index].R
362 *d++ = palette[index].G
363 *d++ = palette[index].B
364 *d++ = 0xFF
365
366 if (byte % 2
367 tFileSeek(file, offsetBytes: 1, tSeekOrigin::Current); 
368 break
369
370
371
372 if (d >= dest + Width*Height*4
373 keepReading = false
374
375
376 
377 
378void tImageBMP::ReadRow_IndexedRLE4(tFileHandle file, uint8* dest, PaletteColour* palette
379
380 int currentLine = 0
381 uint8* d = dest
382 bool keepReading = true
383 while (keepReading
384
385 uint8 byte1 = tGetc(file); 
386 if (byte1
387
388 uint8 byte2 = tGetc(file); 
389 uint8 index1 = byte2 >> 4
390 uint8 index2 = byte2 & 0x0F
391 for (int i = 0; i < (byte1 / 2); i++) 
392
393 *(d++) = palette[index1].R
394 *(d++) = palette[index1].G
395 *(d++) = palette[index1].B
396 *(d++) = 0x0F
397 
398 *(d++) = palette[index2].R
399 *(d++) = palette[index2].G
400 *(d++) = palette[index2].B
401 *(d++) = 0x0F
402
403 if (byte1 % 2
404
405 *(d++) = palette[index1].R
406 *(d++) = palette[index1].G
407 *(d++) = palette[index1].B
408 *(d++) = 0x0F
409
410
411 else // Absolute mode. 
412 {  
413 uint8 byte2 = tGetc(file); 
414 switch (byte2
415
416 case 0
417 currentLine++; 
418 d = dest + currentLine*Width*4
419 break
420 
421 case 1
422 keepReading = false
423 break
424 
425 case 2
426
427 int xoffset = tGetc(file); 
428 int yoffset = tGetc(file); 
429 currentLine += yoffset
430 d += yoffset*Width*4 + xoffset*4
431 break
432
433 
434 default
435 for (int i = 0; i < (byte2/2); i++) 
436
437 byte1 = tGetc(file); 
438 uint8 index1 = byte1 >> 4
439 *(d++) = palette[index1].R
440 *(d++) = palette[index1].G
441 *(d++) = palette[index1].B
442 *(d++) = 0x0F
443 
444 uint8 index2 = byte1 & 0x0F
445 *(d++) = palette[index2].R
446 *(d++) = palette[index2].G
447 *(d++) = palette[index2].B
448 *(d++) = 0x0F
449
450 if (byte2 % 2
451
452 byte1 = tGetc(file); 
453 uint8 index = byte1 >> 4
454 *(d++) = palette[index].R
455 *(d++) = palette[index].G
456 *(d++) = palette[index].B
457 *(d++) = 0x0F
458
459 if (((byte2 + 1)/2) % 2
460 tFileSeek(file, offsetBytes: 1, tSeekOrigin::Current); 
461 break
462
463
464 if (d >= dest + Width*Height*4
465 keepReading = false
466
467
468 
469 
470bool tImageBMP::Set(tPixel4b* pixels, int width, int height, bool steal
471
472 Clear(); 
473 if (!pixels || (width <= 0) || (height <= 0)) 
474 return false
475 
476 Width = width
477 Height = height
478 
479 if (steal
480
481 Pixels = pixels
482
483 else 
484
485 Pixels = new tPixel4b[Width*Height]; 
486 tStd::tMemcpy(dest: Pixels, src: pixels, numBytes: Width*Height*sizeof(tPixel4b)); 
487
488 
489 PixelFormatSrc = tPixelFormat::R8G8B8A8
490 PixelFormat = tPixelFormat::R8G8B8A8
491 ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB. 
492 ColourProfile = tColourProfile::sRGB
493 
494 return true
495
496 
497 
498bool tImageBMP::Set(tFrame* frame, bool steal
499
500 Clear(); 
501 if (!frame || !frame->IsValid()) 
502 return false
503 
504 PixelFormatSrc = frame->PixelFormatSrc
505 PixelFormat = tPixelFormat::R8G8B8A8
506 ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB. 
507 ColourProfile = tColourProfile::sRGB
508 
509 Set(pixels: frame->GetPixels(steal), width: frame->Width, height: frame->Height, steal); 
510 if (steal
511 delete frame
512 
513 return true
514
515 
516 
517bool tImageBMP::Set(tPicture& picture, bool steal
518
519 Clear(); 
520 if (!picture.IsValid()) 
521 return false
522 
523 PixelFormatSrc = picture.PixelFormatSrc
524 PixelFormat = tPixelFormat::R8G8B8A8
525 // We don't know colour profile of tPicture. 
526 
527 // This is worth some explanation. If steal is true the picture becomes invalid and the 
528 // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the 
529 // 'set' call will memcpy them out... which makes sure the picture is still valid after and 
530 // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must 
531 // succeed if picture was valid. 
532 tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels(); 
533 bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal); 
534 tAssert(success); 
535 return true
536
537 
538 
539tFrame* tImageBMP::GetFrame(bool steal
540
541 if (!IsValid()) 
542 return nullptr
543 
544 tFrame* frame = new tFrame(); 
545 frame->PixelFormatSrc = PixelFormatSrc
546 
547 if (steal
548
549 frame->StealFrom(src: Pixels, width: Width, height: Height); 
550 Pixels = nullptr
551
552 else 
553
554 frame->Set(srcPixels: Pixels, width: Width, height: Height); 
555
556 return frame
557
558 
559 
560tImageBMP::tFormat tImageBMP::Save(const tString& bmpFile, tFormat format) const 
561
562 SaveParams params
563 params.Format = format
564 return Save(bmpFile, params); 
565
566 
567 
568tImageBMP::tFormat tImageBMP::Save(const tString& bmpFile, const SaveParams& params) const 
569
570 tFormat format = params.Format
571 if (!IsValid() || (format == tFormat::Invalid)) 
572 return tFormat::Invalid
573 
574 if (tSystem::tGetFileType(file: bmpFile) != tSystem::tFileType::BMP
575 return tFormat::Invalid
576 
577 if (format == tFormat::Auto
578 format = IsOpaque() ? tFormat::BPP24 : tFormat::BPP32
579 
580 tFileHandle file = tOpenFile(file: bmpFile.Chars(), mode: "wb"); 
581 if (!file
582 return tFormat::Invalid
583 
584 int bytesPerPixel = (format == tFormat::BPP24) ? 3 : 4
585 
586 Header bmpHeader
587 bmpHeader.FourCC = FourCC
588 bmpHeader.Size = Width*Height*bytesPerPixel + 54
589 bmpHeader.AppId = 0
590 bmpHeader.Offset = 54
591 tWriteFile(handle: file, buffer: &bmpHeader, sizeBytes: sizeof(Header)); 
592  
593 InfoHeader infoHeader
594 infoHeader.HeaderSize = 40
595 infoHeader.Width = Width
596 infoHeader.Height = Height
597 infoHeader.NumPlanes = 1
598 infoHeader.BPP = bytesPerPixel*8
599 infoHeader.Compression = 0
600 infoHeader.ImageSize = Width * Height * bytesPerPixel
601 infoHeader.HorizontalResolution = 0
602 infoHeader.VerticalResolution = 0
603 infoHeader.ColoursUsed = 0
604 infoHeader.ColoursImportant = 0
605 tWriteFile(handle: file, buffer: &infoHeader, sizeBytes: sizeof(InfoHeader)); 
606  
607 uint8* p = (uint8*)Pixels
608 int rowPad = (Width*bytesPerPixel % 4) ? 4 - (Width*bytesPerPixel % 4) : 0
609 uint8 zeroPad[4]; 
610 tStd::tMemset(dest: zeroPad, val: 0, numBytes: 4); 
611 for (int y = 0; y < Height; y++) 
612
613 for (int x = 0; x < Width; x++) 
614
615 uint8 sample[4]; 
616 sample[2] = *p++; 
617 sample[1] = *p++; 
618 sample[0] = *p++; 
619 sample[3] = *p++; 
620 tWriteFile(handle: file, buffer: sample, sizeBytes: bytesPerPixel); 
621
622 if (rowPad
623 tWriteFile(handle: file, buffer: zeroPad, sizeBytes: rowPad); 
624
625  
626 tCloseFile(f: file); 
627 return format
628
629 
630 
631bool tImageBMP::IsOpaque() const 
632
633 for (int p = 0; p < (Width*Height); p++) 
634
635 if (Pixels[p].A < 255
636 return false
637
638 
639 return true
640
641 
642 
643tPixel4b* tImageBMP::StealPixels() 
644
645 tPixel4b* pixels = Pixels
646 Pixels = nullptr
647 Width = 0
648 Height = 0
649 return pixels
650
651 
652 
653
654