1 module imageformats.tga; 2 3 import std.algorithm : min; 4 import std.bitmanip : littleEndianToNative, nativeToLittleEndian; 5 import std.stdio : File, SEEK_SET, SEEK_CUR; 6 import std.typecons : scoped; 7 import imageformats; 8 9 private: 10 11 /// Header of a TGA file. 12 public struct TGA_Header { 13 ubyte id_length; 14 ubyte palette_type; 15 ubyte data_type; 16 ushort palette_start; 17 ushort palette_length; 18 ubyte palette_bits; 19 ushort x_origin; 20 ushort y_origin; 21 ushort width; 22 ushort height; 23 ubyte bits_pp; 24 ubyte flags; 25 } 26 27 /// Returns the header of a TGA file. 28 public TGA_Header read_tga_header(in char[] filename) { 29 auto reader = scoped!FileReader(filename); 30 return read_tga_header(reader); 31 } 32 33 /// Reads the image header from a buffer containing a TGA image. 34 public TGA_Header read_tga_header_from_mem(in ubyte[] source) { 35 auto reader = scoped!MemReader(source); 36 return read_tga_header(reader); 37 } 38 39 /// Reads a TGA image. req_chans defines the format of returned image 40 /// (you can use ColFmt here). 41 public IFImage read_tga(in char[] filename, long req_chans = 0) { 42 auto reader = scoped!FileReader(filename); 43 return read_tga(reader, req_chans); 44 } 45 46 /// Reads an image from a buffer containing a TGA image. req_chans defines the 47 /// format of returned image (you can use ColFmt here). 48 public IFImage read_tga_from_mem(in ubyte[] source, long req_chans = 0) { 49 auto reader = scoped!MemReader(source); 50 return read_tga(reader, req_chans); 51 } 52 53 /// Writes a TGA image into a file. 54 public void write_tga(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0) 55 { 56 auto writer = scoped!FileWriter(file); 57 write_tga(writer, w, h, data, tgt_chans); 58 } 59 60 /// Writes a TGA image into a buffer. 61 public ubyte[] write_tga_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) { 62 auto writer = scoped!MemWriter(); 63 write_tga(writer, w, h, data, tgt_chans); 64 return writer.result; 65 } 66 67 /// Returns width, height and color format information via w, h and chans. 68 public void read_tga_info(in char[] filename, out int w, out int h, out int chans) { 69 auto reader = scoped!FileReader(filename); 70 return read_tga_info(reader, w, h, chans); 71 } 72 73 /// Returns width, height and color format information via w, h and chans. 74 public void read_tga_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { 75 auto reader = scoped!MemReader(source); 76 return read_tga_info(reader, w, h, chans); 77 } 78 79 // Detects whether a TGA image is readable from stream. 80 package bool detect_tga(Reader stream) { 81 try { 82 auto hdr = read_tga_header(stream); 83 return true; 84 } catch (Throwable) { 85 return false; 86 } finally { 87 stream.seek(0, SEEK_SET); 88 } 89 } 90 91 TGA_Header read_tga_header(Reader stream) { 92 ubyte[18] tmp = void; 93 stream.readExact(tmp, tmp.length); 94 95 TGA_Header hdr = { 96 id_length : tmp[0], 97 palette_type : tmp[1], 98 data_type : tmp[2], 99 palette_start : littleEndianToNative!ushort(tmp[3..5]), 100 palette_length : littleEndianToNative!ushort(tmp[5..7]), 101 palette_bits : tmp[7], 102 x_origin : littleEndianToNative!ushort(tmp[8..10]), 103 y_origin : littleEndianToNative!ushort(tmp[10..12]), 104 width : littleEndianToNative!ushort(tmp[12..14]), 105 height : littleEndianToNative!ushort(tmp[14..16]), 106 bits_pp : tmp[16], 107 flags : tmp[17], 108 }; 109 110 if (hdr.width < 1 || hdr.height < 1 || hdr.palette_type > 1 111 || (hdr.palette_type == 0 && (hdr.palette_start 112 || hdr.palette_length 113 || hdr.palette_bits)) 114 || (4 <= hdr.data_type && hdr.data_type <= 8) || 12 <= hdr.data_type) 115 throw new ImageIOException("corrupt TGA header"); 116 117 return hdr; 118 } 119 120 package IFImage read_tga(Reader stream, long req_chans = 0) { 121 if (req_chans < 0 || 4 < req_chans) 122 throw new ImageIOException("come on..."); 123 124 TGA_Header hdr = read_tga_header(stream); 125 126 if (hdr.width < 1 || hdr.height < 1) 127 throw new ImageIOException("invalid dimensions"); 128 if (hdr.flags & 0xc0) // two bits 129 throw new ImageIOException("interlaced TGAs not supported"); 130 if (hdr.flags & 0x10) 131 throw new ImageIOException("right-to-left TGAs not supported"); 132 ubyte attr_bits_pp = (hdr.flags & 0xf); 133 if (! (attr_bits_pp == 0 || attr_bits_pp == 8)) // some set it 0 although data has 8 134 throw new ImageIOException("only 8-bit alpha/attribute(s) supported"); 135 if (hdr.palette_type) 136 throw new ImageIOException("paletted TGAs not supported"); 137 138 const bool rle = hdr.data_type == TGA_DataType.TrueColor_RLE // Idx_RLE 139 || hdr.data_type == TGA_DataType.Gray_RLE; // not supported 140 141 switch (hdr.data_type) with (TGA_DataType) { 142 case TrueColor: 143 case TrueColor_RLE: 144 if (hdr.bits_pp != 24 && hdr.bits_pp != 32) 145 throw new ImageIOException("not supported"); 146 break; 147 case Gray: 148 case Gray_RLE: 149 if (hdr.bits_pp != 8 && !(hdr.bits_pp == 16 && attr_bits_pp == 8)) 150 throw new ImageIOException("not supported"); 151 break; 152 default: 153 throw new ImageIOException("not supported"); 154 } 155 156 int src_chans = hdr.bits_pp / 8; 157 158 if (hdr.id_length) 159 stream.seek(hdr.id_length, SEEK_CUR); 160 161 TGA_Decoder dc = { 162 stream : stream, 163 w : hdr.width, 164 h : hdr.height, 165 origin_at_top : cast(bool) (hdr.flags & 0x20), 166 bytes_pp : hdr.bits_pp / 8, 167 rle : rle, 168 tgt_chans : (req_chans == 0) ? src_chans : cast(int) req_chans, 169 }; 170 171 switch (dc.bytes_pp) { 172 case 1: dc.src_fmt = _ColFmt.Y; break; 173 case 2: dc.src_fmt = _ColFmt.YA; break; 174 case 3: dc.src_fmt = _ColFmt.BGR; break; 175 case 4: dc.src_fmt = _ColFmt.BGRA; break; 176 default: throw new ImageIOException("TGA: format not supported"); 177 } 178 179 IFImage result = { 180 w : dc.w, 181 h : dc.h, 182 c : cast(ColFmt) dc.tgt_chans, 183 pixels : decode_tga(dc), 184 }; 185 return result; 186 } 187 188 void write_tga(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) { 189 if (w < 1 || h < 1 || ushort.max < w || ushort.max < h) 190 throw new ImageIOException("invalid dimensions"); 191 ulong src_chans = data.length / w / h; 192 if (src_chans < 1 || 4 < src_chans || tgt_chans < 0 || 4 < tgt_chans) 193 throw new ImageIOException("invalid channel count"); 194 if (src_chans * w * h != data.length) 195 throw new ImageIOException("mismatching dimensions and length"); 196 197 TGA_Encoder ec = { 198 stream : stream, 199 w : cast(ushort) w, 200 h : cast(ushort) h, 201 src_chans : cast(int) src_chans, 202 tgt_chans : cast(int) ((tgt_chans) ? tgt_chans : src_chans), 203 rle : true, 204 data : data, 205 }; 206 207 write_tga(ec); 208 stream.flush(); 209 } 210 211 struct TGA_Decoder { 212 Reader stream; 213 int w, h; 214 bool origin_at_top; // src 215 uint bytes_pp; 216 bool rle; // run length compressed 217 _ColFmt src_fmt; 218 uint tgt_chans; 219 } 220 221 ubyte[] decode_tga(ref TGA_Decoder dc) { 222 auto result = new ubyte[dc.w * dc.h * dc.tgt_chans]; 223 224 const size_t tgt_linesize = dc.w * dc.tgt_chans; 225 const size_t src_linesize = dc.w * dc.bytes_pp; 226 auto src_line = new ubyte[src_linesize]; 227 228 const ptrdiff_t tgt_stride = (dc.origin_at_top) ? tgt_linesize : -tgt_linesize; 229 ptrdiff_t ti = (dc.origin_at_top) ? 0 : (dc.h-1) * tgt_linesize; 230 231 const LineConv!ubyte convert = get_converter!ubyte(dc.src_fmt, dc.tgt_chans); 232 233 if (!dc.rle) { 234 foreach (_j; 0 .. dc.h) { 235 dc.stream.readExact(src_line, src_linesize); 236 convert(src_line, result[ti .. ti + tgt_linesize]); 237 ti += tgt_stride; 238 } 239 return result; 240 } 241 242 // ----- RLE ----- 243 244 ubyte[4] rbuf; 245 size_t plen = 0; // packet length 246 bool its_rle = false; 247 248 foreach (_j; 0 .. dc.h) { 249 // fill src_line with uncompressed data (this works like a stream) 250 size_t wanted = src_linesize; 251 while (wanted) { 252 if (plen == 0) { 253 dc.stream.readExact(rbuf, 1); 254 its_rle = cast(bool) (rbuf[0] & 0x80); 255 plen = ((rbuf[0] & 0x7f) + 1) * dc.bytes_pp; // length in bytes 256 } 257 const size_t gotten = src_linesize - wanted; 258 const size_t copysize = min(plen, wanted); 259 if (its_rle) { 260 dc.stream.readExact(rbuf, dc.bytes_pp); 261 for (size_t p = gotten; p < gotten+copysize; p += dc.bytes_pp) 262 src_line[p .. p+dc.bytes_pp] = rbuf[0 .. dc.bytes_pp]; 263 } else { // it's raw 264 auto slice = src_line[gotten .. gotten+copysize]; 265 dc.stream.readExact(slice, copysize); 266 } 267 wanted -= copysize; 268 plen -= copysize; 269 } 270 271 convert(src_line, result[ti .. ti + tgt_linesize]); 272 ti += tgt_stride; 273 } 274 275 return result; 276 } 277 278 // ---------------------------------------------------------------------- 279 // TGA encoder 280 281 immutable ubyte[18] tga_footer_sig = 282 cast(immutable(ubyte)[18]) "TRUEVISION-XFILE.\0"; 283 284 struct TGA_Encoder { 285 Writer stream; 286 ushort w, h; 287 int src_chans; 288 int tgt_chans; 289 bool rle; // run length compression 290 const(ubyte)[] data; 291 } 292 293 void write_tga(ref TGA_Encoder ec) { 294 ubyte data_type; 295 bool has_alpha = false; 296 switch (ec.tgt_chans) with (TGA_DataType) { 297 case 1: data_type = ec.rle ? Gray_RLE : Gray; break; 298 case 2: data_type = ec.rle ? Gray_RLE : Gray; has_alpha = true; break; 299 case 3: data_type = ec.rle ? TrueColor_RLE : TrueColor; break; 300 case 4: data_type = ec.rle ? TrueColor_RLE : TrueColor; has_alpha = true; break; 301 default: throw new ImageIOException("internal error"); 302 } 303 304 ubyte[18] hdr = void; 305 hdr[0] = 0; // id length 306 hdr[1] = 0; // palette type 307 hdr[2] = data_type; 308 hdr[3..8] = 0; // palette start (2), len (2), bits per palette entry (1) 309 hdr[8..12] = 0; // x origin (2), y origin (2) 310 hdr[12..14] = nativeToLittleEndian(ec.w); 311 hdr[14..16] = nativeToLittleEndian(ec.h); 312 hdr[16] = cast(ubyte) (ec.tgt_chans * 8); // bits per pixel 313 hdr[17] = (has_alpha) ? 0x8 : 0x0; // flags: attr_bits_pp = 8 314 ec.stream.rawWrite(hdr); 315 316 write_image_data(ec); 317 318 ubyte[26] ftr = void; 319 ftr[0..4] = 0; // extension area offset 320 ftr[4..8] = 0; // developer directory offset 321 ftr[8..26] = tga_footer_sig; 322 ec.stream.rawWrite(ftr); 323 } 324 325 void write_image_data(ref TGA_Encoder ec) { 326 _ColFmt tgt_fmt; 327 switch (ec.tgt_chans) { 328 case 1: tgt_fmt = _ColFmt.Y; break; 329 case 2: tgt_fmt = _ColFmt.YA; break; 330 case 3: tgt_fmt = _ColFmt.BGR; break; 331 case 4: tgt_fmt = _ColFmt.BGRA; break; 332 default: throw new ImageIOException("internal error"); 333 } 334 335 const LineConv!ubyte convert = get_converter!ubyte(ec.src_chans, tgt_fmt); 336 337 const size_t src_linesize = ec.w * ec.src_chans; 338 const size_t tgt_linesize = ec.w * ec.tgt_chans; 339 auto tgt_line = new ubyte[tgt_linesize]; 340 341 ptrdiff_t si = (ec.h-1) * src_linesize; // origin at bottom 342 343 if (!ec.rle) { 344 foreach (_; 0 .. ec.h) { 345 convert(ec.data[si .. si + src_linesize], tgt_line); 346 ec.stream.rawWrite(tgt_line); 347 si -= src_linesize; // origin at bottom 348 } 349 return; 350 } 351 352 // ----- RLE ----- 353 354 const bytes_pp = ec.tgt_chans; 355 const size_t max_packets_per_line = (tgt_linesize+127) / 128; 356 auto tgt_cmp = new ubyte[tgt_linesize + max_packets_per_line]; // compressed line 357 foreach (_; 0 .. ec.h) { 358 convert(ec.data[si .. si + src_linesize], tgt_line); 359 ubyte[] compressed_line = rle_compress(tgt_line, tgt_cmp, ec.w, bytes_pp); 360 ec.stream.rawWrite(compressed_line); 361 si -= src_linesize; // origin at bottom 362 } 363 } 364 365 ubyte[] rle_compress(in ubyte[] line, ubyte[] tgt_cmp, in size_t w, in int bytes_pp) 366 { 367 const int rle_limit = (1 < bytes_pp) ? 2 : 3; // run len worth an RLE packet 368 size_t runlen = 0; 369 size_t rawlen = 0; 370 size_t raw_i = 0; // start of raw packet data in line 371 size_t cmp_i = 0; 372 size_t pixels_left = w; 373 const(ubyte)[] px; 374 for (size_t i = bytes_pp; pixels_left; i += bytes_pp) { 375 runlen = 1; 376 px = line[i-bytes_pp .. i]; 377 while (i < line.length && line[i .. i+bytes_pp] == px[0..$] && runlen < 128) { 378 ++runlen; 379 i += bytes_pp; 380 } 381 pixels_left -= runlen; 382 383 if (runlen < rle_limit) { 384 // data goes to raw packet 385 rawlen += runlen; 386 if (128 <= rawlen) { // full packet, need to store it 387 size_t copysize = 128 * bytes_pp; 388 tgt_cmp[cmp_i++] = 0x7f; // raw packet header 389 tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; 390 cmp_i += copysize; 391 raw_i += copysize; 392 rawlen -= 128; 393 } 394 } else { 395 // RLE packet is worth it 396 397 // store raw packet first, if any 398 if (rawlen) { 399 assert(rawlen < 128); 400 size_t copysize = rawlen * bytes_pp; 401 tgt_cmp[cmp_i++] = cast(ubyte) (rawlen-1); // raw packet header 402 tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; 403 cmp_i += copysize; 404 rawlen = 0; 405 } 406 407 // store RLE packet 408 tgt_cmp[cmp_i++] = cast(ubyte) (0x80 | (runlen-1)); // packet header 409 tgt_cmp[cmp_i .. cmp_i+bytes_pp] = px[0..$]; // packet data 410 cmp_i += bytes_pp; 411 raw_i = i; 412 } 413 } // for 414 415 if (rawlen) { // last packet of the line 416 size_t copysize = rawlen * bytes_pp; 417 tgt_cmp[cmp_i++] = cast(ubyte) (rawlen-1); // raw packet header 418 tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; 419 cmp_i += copysize; 420 } 421 return tgt_cmp[0 .. cmp_i]; 422 } 423 424 enum TGA_DataType : ubyte { 425 Idx = 1, 426 TrueColor = 2, 427 Gray = 3, 428 Idx_RLE = 9, 429 TrueColor_RLE = 10, 430 Gray_RLE = 11, 431 } 432 433 package void read_tga_info(Reader stream, out int w, out int h, out int chans) { 434 TGA_Header hdr = read_tga_header(stream); 435 w = hdr.width; 436 h = hdr.height; 437 438 // TGA is awkward... 439 auto dt = hdr.data_type; 440 if ((dt == TGA_DataType.TrueColor || dt == TGA_DataType.Gray || 441 dt == TGA_DataType.TrueColor_RLE || dt == TGA_DataType.Gray_RLE) 442 && (hdr.bits_pp % 8) == 0) 443 { 444 chans = hdr.bits_pp / 8; 445 return; 446 } else if (dt == TGA_DataType.Idx || dt == TGA_DataType.Idx_RLE) { 447 switch (hdr.palette_bits) { 448 case 15: chans = 3; return; 449 case 16: chans = 3; return; // one bit could be for some "interrupt control" 450 case 24: chans = 3; return; 451 case 32: chans = 4; return; 452 default: 453 } 454 } 455 chans = 0; // unknown 456 }