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 }