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