1 /*
2 Copyright (c) 2014-2021 Timur Gafarov, Roman Chistokhodov
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 
6 Permission is hereby granted, free of charge, to any person or organization
7 obtaining a copy of the software and accompanying documentation covered by
8 this license (the "Software") to use, reproduce, display, distribute,
9 execute, and transmit the Software, and to prepare derivative works of the
10 Software, and to permit third-parties to whom the Software is furnished to
11 do so, all subject to the following:
12 
13 The copyright notices in the Software and this entire statement, including
14 the above license grant, this restriction and the following disclaimer,
15 must be included in all copies of the Software, in whole or in part, and
16 all derivative works of the Software, unless such copies or derivative
17 works are solely in the form of machine-executable object code generated by
18 a source language processor.
19 
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 DEALINGS IN THE SOFTWARE.
27 */
28 
29 /**
30  * Decode JPEG images
31  *
32  * Copyright: Timur Gafarov, Roman Chistokhodov 2014-2021.
33  * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0).
34  * Authors: Timur Gafarov, Roman Chistokhodov
35  */
36 module dlib.image.io.tga;
37 
38 import std.stdio;
39 import std.file;
40 import std.conv;
41 import dlib.core.memory;
42 import dlib.core.stream;
43 import dlib.core.compound;
44 import dlib.image.color;
45 import dlib.image.image;
46 import dlib.image.io;
47 import dlib.image.io.utils;
48 import dlib.filesystem.local;
49 
50 // uncomment this to see debug messages:
51 //version = TGADebug;
52 
53 struct TGAHeader
54 {
55     ubyte idLength;
56     ubyte type;
57     ubyte encoding;
58     short colmapStart;
59     short colmapLen;
60     ubyte colmapBits;
61     short xstart;
62     short ystart;
63     short width;
64     short height;
65     ubyte bpp;
66     ubyte descriptor;
67 }
68 
69 enum TGAEncoding : ubyte {
70     Indexed = 1,
71     RGB = 2,
72     Grey = 3,
73     RLE_Indexed = 9,
74     RLE_RGB = 10,
75     RLE_Grey = 11
76 };
77 
78 enum TgaOrigin : ubyte
79 {
80     Left = 0x00,
81     Right = 0x10,
82     Lower = 0x00,
83     Upper = 0x20
84 }
85 
86 class TGALoadException: ImageLoadException
87 {
88     this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
89     {
90         super(msg, file, line, next);
91     }
92 }
93 
94 /**
95  * Load PNG from file using local FileSystem.
96  * Causes GC allocation
97  */
98 SuperImage loadTGA(string filename)
99 {
100     InputStream input = openForInput(filename);
101 
102     try
103     {
104         return loadTGA(input);
105     }
106     catch (TGALoadException ex)
107     {
108         throw new Exception("'" ~ filename ~ "' :" ~ ex.msg, ex.file, ex.line, ex.next);
109     }
110     finally
111     {
112         input.close();
113     }
114 }
115 
116 /**
117  * Load TGA from stream using default image factory.
118  * Causes GC allocation
119  */
120 SuperImage loadTGA(InputStream istrm)
121 {
122     Compound!(SuperImage, string) res =
123         loadTGA(istrm, defaultImageFactory);
124     if (res[0] is null)
125         throw new TGALoadException(res[1]);
126     else
127         return res[0];
128 }
129 
130 /**
131  * Load TGA from stream using specified image factory.
132  * GC-free
133  */
134 Compound!(SuperImage, string) loadTGA(
135     InputStream istrm,
136     SuperImageFactory imgFac)
137 {
138     SuperImage img = null;
139 
140     Compound!(SuperImage, string) error(string errorMsg)
141     {
142         if (img)
143         {
144             img.free();
145             img = null;
146         }
147         return compound(img, errorMsg);
148     }
149 
150     TGAHeader readHeader()
151     {
152         TGAHeader hdr = readStruct!TGAHeader(istrm);
153         version(TGADebug)
154         {
155             writefln("idLength = %s", hdr.idLength);
156             writefln("type = %s", hdr.type);
157            /*
158             * Encoding flag:
159             * 1 = Raw indexed image
160             * 2 = Raw RGB
161             * 3 = Raw greyscale
162             * 9 = RLE indexed
163             * 10 = RLE RGB
164             * 11 = RLE greyscale
165             * 32 & 33 = Other compression, indexed
166             */
167             writefln("encoding = %s", hdr.encoding);
168 
169             writefln("colmapStart = %s", hdr.colmapStart);
170             writefln("colmapLen = %s", hdr.colmapLen);
171             writefln("colmapBits = %s", hdr.colmapBits);
172             writefln("xstart = %s", hdr.xstart);
173             writefln("ystart = %s", hdr.ystart);
174             writefln("width = %s", hdr.width);
175             writefln("height = %s", hdr.height);
176             writefln("bpp = %s", hdr.bpp);
177             writefln("descriptor = %s", hdr.descriptor);
178             writeln("-------------------");
179         }
180         return hdr;
181     }
182 
183     SuperImage readRawRGB(ref TGAHeader hdr)
184     {
185         uint channels = hdr.bpp / 8;
186         SuperImage res = imgFac.createImage(hdr.width, hdr.height, channels, 8);
187 
188         if (hdr.descriptor & TgaOrigin.Upper) {
189             istrm.fillArray(res.data);
190         } else {
191             foreach(i; 0..hdr.height) {
192                 istrm.fillArray(res.data[channels * hdr.width * (hdr.height-i-1)..channels * hdr.width * (hdr.height-i)]);
193             }
194         }
195 
196         const ubyte alphaBits = cast(ubyte)(hdr.descriptor & 0xf);
197         version(TGADebug) {
198             writefln("Alpha bits: %s", alphaBits);
199         }
200 
201         if (channels == 4) {
202             for (size_t i=0; i<res.data.length; i += channels) {
203                 auto alphaIndex = i+3;
204                 res.data[alphaIndex] = cast(ubyte)((res.data[alphaIndex] & ((1 << alphaBits)-1 )) << (8 - alphaBits));
205             }
206         }
207 
208         return res;
209     }
210 
211     SuperImage readRLERGB(ref TGAHeader hdr)
212     {
213         uint channels = hdr.bpp / 8;
214         uint imageSize = hdr.width * hdr.height * channels;
215         SuperImage res = imgFac.createImage(hdr.width, hdr.height, channels, 8);
216 
217         // Calculate offset to image data
218         uint dataOffset = 18 + hdr.idLength;
219 
220         // Add palette offset for indexed images
221         if (hdr.type == 1)
222             dataOffset += 768;
223 
224         // Read compressed data
225         // TODO: take scanline order into account (bottom-up or top-down)
226         ubyte[] data = New!(ubyte[])(cast(uint)istrm.size - dataOffset);
227         istrm.fillArray(data);
228 
229         uint ii = 0;
230         uint i = 0;
231         while (ii < imageSize)
232         {
233             ubyte b = data[i];
234             i++;
235 
236             if (b & 0x80) // Run length chunk
237             {
238                 // Get run length
239                 uint runLength = b - 127;
240 
241                 // Repeat the next pixel runLength times
242                 for (uint j = 0; j < runLength; j++)
243                 {
244                     foreach(pIndex; 0..channels)
245                         res.data[ii + pIndex] = data[i + pIndex];
246 
247                     ii += channels;
248                 }
249 
250                 i += channels;
251             }
252             else // Raw chunk
253             {
254                 // Get run length
255                 uint runLength = b + 1;
256 
257                 // Write the next runLength pixels directly
258                 for (uint j = 0; j < runLength; j++)
259                 {
260                     foreach(pIndex; 0..channels)
261                         res.data[ii + pIndex] = data[i + pIndex];
262 
263                     ii += channels;
264                     i += channels;
265                 }
266             }
267         }
268 
269         Delete(data);
270 
271         return res;
272     }
273 
274     auto hdr = readHeader();
275 
276     if (hdr.idLength)
277     {
278         char[] id = New!(char[])(hdr.idLength);
279         istrm.fillArray(id);
280 
281         version(TGADebug)
282         {
283             writefln("id = %s", id);
284         }
285 
286         Delete(id);
287     }
288 
289     if (hdr.encoding == TGAEncoding.RGB) {
290         img = readRawRGB(hdr);
291     } else if (hdr.encoding == TGAEncoding.RLE_RGB) {
292         img = readRLERGB(hdr);
293     } else {
294         return error("loadTGA error: only RGB images are supported");
295     }
296 
297     img.swapRGB();
298 
299     return compound(img, "");
300 }
301 
302 void swapRGB(SuperImage img)
303 {
304     foreach(x; 0..img.width) {
305         foreach(y; 0..img.height)
306         {
307             img[x, y] = Color4f(img[x, y].bgra);
308         }
309     }
310 }
311 
312 /**
313  * Save TGA to file using local FileSystem.
314  * Causes GC allocation
315  */
316 void saveTGA(SuperImage img, string filename)
317 {
318     OutputStream output = openForOutput(filename);
319     Compound!(bool, string) res = saveTGA(img, output);
320     output.close();
321 
322     if (!res[0])
323         throw new TGALoadException(res[1]);
324 }
325 
326 /**
327  * Save TGA to stream.
328  * GC-free
329  */
330 Compound!(bool, string) saveTGA(SuperImage img, OutputStream output)
331 {
332     Compound!(bool, string) error(string errorMsg)
333     {
334         return compound(false, errorMsg);
335     }
336 
337     enum ubyte[12] tgaStart = [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0];
338     output.writeArray(tgaStart);
339     output.writeLE(cast(ushort)img.width);
340     output.writeLE(cast(ushort)img.height);
341 
342     const bool hasAlpha = img.channels == 4;
343 
344     if (img.channels == 3) {
345         output.writeLE(cast(ubyte)24);
346         output.writeLE(cast(ubyte)TgaOrigin.Upper);
347     } else if (img.channels == 4) {
348         output.writeLE(cast(ubyte)32);
349         output.writeLE(cast(ubyte)0x28);
350     } else {
351         return error("saveTGA error: unsupported number of channels");
352     }
353 
354     foreach(y; 0..img.height) {
355         foreach(x; 0..img.width) {
356             ubyte[4] rgb;
357             ColorRGBA color = img[x, y].convert(8);
358             rgb[0] = cast(ubyte)color[2];
359             rgb[1] = cast(ubyte)color[1];
360             rgb[2] = cast(ubyte)color[0];
361 
362             if (hasAlpha) {
363                 rgb[3] = cast(ubyte)(color[3]);
364                 output.writeArray(rgb[]);
365             } else {
366                 output.writeArray(rgb[0..3]);
367             }
368         }
369     }
370 
371     return compound(true, "");
372 }