1 // Copyright (c) 2014-2018 Tero Hänninen
2 // Boost Software License - Version 1.0 - August 17th, 2003
3 module imageformats;
4 
5 import std.stdio  : File, SEEK_SET, SEEK_CUR, SEEK_END;
6 import std.string : toLower, lastIndexOf;
7 import std.typecons : scoped;
8 public import imageformats.png;
9 public import imageformats.tga;
10 public import imageformats.bmp;
11 public import imageformats.jpeg;
12 
13 /// Image with 8-bit channels. Top-left corner at (0, 0).
14 struct IFImage {
15     /// width
16     int         w;
17     /// height
18     int         h;
19     /// channels
20     ColFmt      c;
21     /// buffer
22     ubyte[]     pixels;
23 }
24 
25 /// Image with 16-bit channels. Top-left corner at (0, 0).
26 struct IFImage16 {
27     /// width
28     int         w;
29     /// height
30     int         h;
31     /// channels
32     ColFmt      c;
33     /// buffer
34     ushort[]    pixels;
35 }
36 
37 /// Color format which you can pass to the read and write functions.
38 enum ColFmt {
39     Y = 1,      /// Gray
40     YA = 2,     /// Gray + Alpha
41     RGB = 3,    /// Truecolor
42     RGBA = 4,   /// Truecolor + Alpha
43 }
44 
45 /// Reads an image from file. req_chans defines the format of returned image
46 /// (you can use ColFmt here).
47 IFImage read_image(in char[] file, long req_chans = 0) {
48     auto reader = scoped!FileReader(file);
49     return read_image_from_reader(reader, req_chans);
50 }
51 
52 /// Reads an image from a buffer. req_chans defines the format of returned
53 /// image (you can use ColFmt here).
54 IFImage read_image_from_mem(in ubyte[] source, long req_chans = 0) {
55     auto reader = scoped!MemReader(source);
56     return read_image_from_reader(reader, req_chans);
57 }
58 
59 /// Writes an image to file. req_chans defines the format the image is saved in
60 /// (you can use ColFmt here).
61 void write_image(in char[] file, long w, long h, in ubyte[] data, long req_chans = 0) {
62     const char[] ext = extract_extension_lowercase(file);
63 
64     void function(Writer, long, long, in ubyte[], long) write_image;
65     switch (ext) {
66         case "png": write_image = &write_png; break;
67         case "tga": write_image = &write_tga; break;
68         case "bmp": write_image = &write_bmp; break;
69         default: throw new ImageIOException("unknown image extension/type");
70     }
71     auto writer = scoped!FileWriter(file);
72     write_image(writer, w, h, data, req_chans);
73 }
74 
75 /// Returns width, height and color format information via w, h and chans.
76 /// If number of channels is unknown chans is set to zero, otherwise chans
77 /// values map to those of ColFmt.
78 void read_image_info(in char[] file, out int w, out int h, out int chans) {
79     auto reader = scoped!FileReader(file);
80     try {
81         return read_png_info(reader, w, h, chans);
82     } catch (Throwable) {
83         reader.seek(0, SEEK_SET);
84     }
85     try {
86         return read_jpeg_info(reader, w, h, chans);
87     } catch (Throwable) {
88         reader.seek(0, SEEK_SET);
89     }
90     try {
91         return read_bmp_info(reader, w, h, chans);
92     } catch (Throwable) {
93         reader.seek(0, SEEK_SET);
94     }
95     try {
96         return read_tga_info(reader, w, h, chans);
97     } catch (Throwable) {
98         reader.seek(0, SEEK_SET);
99     }
100     throw new ImageIOException("unknown image type");
101 }
102 
103 /// Thrown from all the functions...
104 class ImageIOException : Exception {
105    @safe pure const
106    this(string msg, string file = __FILE__, size_t line = __LINE__) {
107        super(msg, file, line);
108    }
109 }
110 
111 private:
112 
113 IFImage read_image_from_reader(Reader reader, long req_chans) {
114     if (detect_png(reader)) return read_png(reader, req_chans);
115     if (detect_jpeg(reader)) return read_jpeg(reader, req_chans);
116     if (detect_bmp(reader)) return read_bmp(reader, req_chans);
117     if (detect_tga(reader)) return read_tga(reader, req_chans);
118     throw new ImageIOException("unknown image type");
119 }
120 
121 // --------------------------------------------------------------------------------
122 // Conversions
123 
124 package enum _ColFmt : int {
125     Unknown = 0,
126     Y = 1,
127     YA,
128     RGB,
129     RGBA,
130     BGR,
131     BGRA,
132 }
133 
134 package alias LineConv(T) = void function(in T[] src, T[] tgt);
135 
136 package LineConv!T get_converter(T)(long src_chans, long tgt_chans) pure {
137     long combo(long a, long b) pure nothrow { return a*16 + b; }
138 
139     if (src_chans == tgt_chans)
140         return &copy_line!T;
141 
142     switch (combo(src_chans, tgt_chans)) with (_ColFmt) {
143         case combo(Y, YA)      : return &Y_to_YA!T;
144         case combo(Y, RGB)     : return &Y_to_RGB!T;
145         case combo(Y, RGBA)    : return &Y_to_RGBA!T;
146         case combo(Y, BGR)     : return &Y_to_BGR!T;
147         case combo(Y, BGRA)    : return &Y_to_BGRA!T;
148         case combo(YA, Y)      : return &YA_to_Y!T;
149         case combo(YA, RGB)    : return &YA_to_RGB!T;
150         case combo(YA, RGBA)   : return &YA_to_RGBA!T;
151         case combo(YA, BGR)    : return &YA_to_BGR!T;
152         case combo(YA, BGRA)   : return &YA_to_BGRA!T;
153         case combo(RGB, Y)     : return &RGB_to_Y!T;
154         case combo(RGB, YA)    : return &RGB_to_YA!T;
155         case combo(RGB, RGBA)  : return &RGB_to_RGBA!T;
156         case combo(RGB, BGR)   : return &RGB_to_BGR!T;
157         case combo(RGB, BGRA)  : return &RGB_to_BGRA!T;
158         case combo(RGBA, Y)    : return &RGBA_to_Y!T;
159         case combo(RGBA, YA)   : return &RGBA_to_YA!T;
160         case combo(RGBA, RGB)  : return &RGBA_to_RGB!T;
161         case combo(RGBA, BGR)  : return &RGBA_to_BGR!T;
162         case combo(RGBA, BGRA) : return &RGBA_to_BGRA!T;
163         case combo(BGR, Y)     : return &BGR_to_Y!T;
164         case combo(BGR, YA)    : return &BGR_to_YA!T;
165         case combo(BGR, RGB)   : return &BGR_to_RGB!T;
166         case combo(BGR, RGBA)  : return &BGR_to_RGBA!T;
167         case combo(BGRA, Y)    : return &BGRA_to_Y!T;
168         case combo(BGRA, YA)   : return &BGRA_to_YA!T;
169         case combo(BGRA, RGB)  : return &BGRA_to_RGB!T;
170         case combo(BGRA, RGBA) : return &BGRA_to_RGBA!T;
171         default                : throw new ImageIOException("internal error");
172     }
173 }
174 
175 void copy_line(T)(in T[] src, T[] tgt) pure nothrow {
176     tgt[0..$] = src[0..$];
177 }
178 
179 T luminance(T)(T r, T g, T b) pure nothrow {
180     return cast(T) (0.21*r + 0.64*g + 0.15*b); // somewhat arbitrary weights
181 }
182 
183 void Y_to_YA(T)(in T[] src, T[] tgt) pure nothrow {
184     for (size_t k, t;   k < src.length;   k+=1, t+=2) {
185         tgt[t] = src[k];
186         tgt[t+1] = T.max;
187     }
188 }
189 
190 alias Y_to_BGR = Y_to_RGB;
191 void Y_to_RGB(T)(in T[] src, T[] tgt) pure nothrow {
192     for (size_t k, t;   k < src.length;   k+=1, t+=3)
193         tgt[t .. t+3] = src[k];
194 }
195 
196 alias Y_to_BGRA = Y_to_RGBA;
197 void Y_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow {
198     for (size_t k, t;   k < src.length;   k+=1, t+=4) {
199         tgt[t .. t+3] = src[k];
200         tgt[t+3] = T.max;
201     }
202 }
203 
204 void YA_to_Y(T)(in T[] src, T[] tgt) pure nothrow {
205     for (size_t k, t;   k < src.length;   k+=2, t+=1)
206         tgt[t] = src[k];
207 }
208 
209 alias YA_to_BGR = YA_to_RGB;
210 void YA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow {
211     for (size_t k, t;   k < src.length;   k+=2, t+=3)
212         tgt[t .. t+3] = src[k];
213 }
214 
215 alias YA_to_BGRA = YA_to_RGBA;
216 void YA_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow {
217     for (size_t k, t;   k < src.length;   k+=2, t+=4) {
218         tgt[t .. t+3] = src[k];
219         tgt[t+3] = src[k+1];
220     }
221 }
222 
223 void RGB_to_Y(T)(in T[] src, T[] tgt) pure nothrow {
224     for (size_t k, t;   k < src.length;   k+=3, t+=1)
225         tgt[t] = luminance(src[k], src[k+1], src[k+2]);
226 }
227 
228 void RGB_to_YA(T)(in T[] src, T[] tgt) pure nothrow {
229     for (size_t k, t;   k < src.length;   k+=3, t+=2) {
230         tgt[t] = luminance(src[k], src[k+1], src[k+2]);
231         tgt[t+1] = T.max;
232     }
233 }
234 
235 void RGB_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow {
236     for (size_t k, t;   k < src.length;   k+=3, t+=4) {
237         tgt[t .. t+3] = src[k .. k+3];
238         tgt[t+3] = T.max;
239     }
240 }
241 
242 void RGBA_to_Y(T)(in T[] src, T[] tgt) pure nothrow {
243     for (size_t k, t;   k < src.length;   k+=4, t+=1)
244         tgt[t] = luminance(src[k], src[k+1], src[k+2]);
245 }
246 
247 void RGBA_to_YA(T)(in T[] src, T[] tgt) pure nothrow {
248     for (size_t k, t;   k < src.length;   k+=4, t+=2) {
249         tgt[t] = luminance(src[k], src[k+1], src[k+2]);
250         tgt[t+1] = src[k+3];
251     }
252 }
253 
254 void RGBA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow {
255     for (size_t k, t;   k < src.length;   k+=4, t+=3)
256         tgt[t .. t+3] = src[k .. k+3];
257 }
258 
259 void BGR_to_Y(T)(in T[] src, T[] tgt) pure nothrow {
260     for (size_t k, t;   k < src.length;   k+=3, t+=1)
261         tgt[t] = luminance(src[k+2], src[k+1], src[k+1]);
262 }
263 
264 void BGR_to_YA(T)(in T[] src, T[] tgt) pure nothrow {
265     for (size_t k, t;   k < src.length;   k+=3, t+=2) {
266         tgt[t] = luminance(src[k+2], src[k+1], src[k+1]);
267         tgt[t+1] = T.max;
268     }
269 }
270 
271 alias RGB_to_BGR = BGR_to_RGB;
272 void BGR_to_RGB(T)(in T[] src, T[] tgt) pure nothrow {
273     for (size_t k;   k < src.length;   k+=3) {
274         tgt[k  ] = src[k+2];
275         tgt[k+1] = src[k+1];
276         tgt[k+2] = src[k  ];
277     }
278 }
279 
280 alias RGB_to_BGRA = BGR_to_RGBA;
281 void BGR_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow {
282     for (size_t k, t;   k < src.length;   k+=3, t+=4) {
283         tgt[t  ] = src[k+2];
284         tgt[t+1] = src[k+1];
285         tgt[t+2] = src[k  ];
286         tgt[t+3] = T.max;
287     }
288 }
289 
290 void BGRA_to_Y(T)(in T[] src, T[] tgt) pure nothrow {
291     for (size_t k, t;   k < src.length;   k+=4, t+=1)
292         tgt[t] = luminance(src[k+2], src[k+1], src[k]);
293 }
294 
295 void BGRA_to_YA(T)(in T[] src, T[] tgt) pure nothrow {
296     for (size_t k, t;   k < src.length;   k+=4, t+=2) {
297         tgt[t] = luminance(src[k+2], src[k+1], src[k]);
298         tgt[t+1] = T.max;
299     }
300 }
301 
302 alias RGBA_to_BGR = BGRA_to_RGB;
303 void BGRA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow {
304     for (size_t k, t;   k < src.length;   k+=4, t+=3) {
305         tgt[t  ] = src[k+2];
306         tgt[t+1] = src[k+1];
307         tgt[t+2] = src[k  ];
308     }
309 }
310 
311 alias RGBA_to_BGRA = BGRA_to_RGBA;
312 void BGRA_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow {
313     for (size_t k, t;   k < src.length;   k+=4, t+=4) {
314         tgt[t  ] = src[k+2];
315         tgt[t+1] = src[k+1];
316         tgt[t+2] = src[k  ];
317         tgt[t+3] = src[k+3];
318     }
319 }
320 
321 // --------------------------------------------------------------------------------
322 
323 package interface Reader {
324     void readExact(ubyte[], size_t);
325     void seek(ptrdiff_t, int);
326 }
327 
328 package interface Writer {
329     void rawWrite(in ubyte[]);
330     void flush();
331 }
332 
333 package class FileReader : Reader {
334     this(in char[] filename) {
335         this(File(filename.idup, "rb"));
336     }
337 
338     this(File f) {
339         if (!f.isOpen) throw new ImageIOException("File not open");
340         this.f = f;
341     }
342 
343     void readExact(ubyte[] buffer, size_t bytes) {
344         auto slice = this.f.rawRead(buffer[0..bytes]);
345         if (slice.length != bytes)
346             throw new Exception("not enough data");
347     }
348 
349     void seek(ptrdiff_t offset, int origin) { this.f.seek(offset, origin); }
350 
351     private File f;
352 }
353 
354 package class MemReader : Reader {
355     this(in ubyte[] source) {
356         this.source = source;
357     }
358 
359     void readExact(ubyte[] buffer, size_t bytes) {
360         if (source.length - cursor < bytes)
361             throw new Exception("not enough data");
362         buffer[0..bytes] = source[cursor .. cursor+bytes];
363         cursor += bytes;
364     }
365 
366     void seek(ptrdiff_t offset, int origin) {
367         switch (origin) {
368             case SEEK_SET:
369                 if (offset < 0 || source.length <= offset)
370                     throw new Exception("seek error");
371                 cursor = offset;
372                 break;
373             case SEEK_CUR:
374                 ptrdiff_t dst = cursor + offset;
375                 if (dst < 0 || source.length <= dst)
376                     throw new Exception("seek error");
377                 cursor = dst;
378                 break;
379             case SEEK_END:
380                 if (0 <= offset || source.length < -offset)
381                     throw new Exception("seek error");
382                 cursor = cast(ptrdiff_t) source.length + offset;
383                 break;
384             default: assert(0);
385         }
386     }
387 
388     private const ubyte[] source;
389     private ptrdiff_t cursor;
390 }
391 
392 package class FileWriter : Writer {
393     this(in char[] filename) {
394         this(File(filename.idup, "wb"));
395     }
396 
397     this(File f) {
398         if (!f.isOpen) throw new ImageIOException("File not open");
399         this.f = f;
400     }
401 
402     void rawWrite(in ubyte[] block) { this.f.rawWrite(block); }
403     void flush() { this.f.flush(); }
404 
405     private File f;
406 }
407 
408 package class MemWriter : Writer {
409     this() { }
410 
411     ubyte[] result() { return buffer; }
412 
413     void rawWrite(in ubyte[] block) { this.buffer ~= block; }
414     void flush() { }
415 
416     private ubyte[] buffer;
417 }
418 
419 const(char)[] extract_extension_lowercase(in char[] filename) {
420     ptrdiff_t di = filename.lastIndexOf('.');
421     return (0 < di && di+1 < filename.length) ? filename[di+1..$].toLower() : "";
422 }
423 
424 unittest {
425     // The TGA and BMP files are not as varied in format as the PNG files, so
426     // not as well tested.
427     string png_path = "tests/pngsuite/";
428     string tga_path = "tests/pngsuite-tga/";
429     string bmp_path = "tests/pngsuite-bmp/";
430 
431     auto files = [
432         "basi0g08",    // PNG image data, 32 x 32, 8-bit grayscale, interlaced
433         "basi2c08",    // PNG image data, 32 x 32, 8-bit/color RGB, interlaced
434         "basi3p08",    // PNG image data, 32 x 32, 8-bit colormap, interlaced
435         "basi4a08",    // PNG image data, 32 x 32, 8-bit gray+alpha, interlaced
436         "basi6a08",    // PNG image data, 32 x 32, 8-bit/color RGBA, interlaced
437         "basn0g08",    // PNG image data, 32 x 32, 8-bit grayscale, non-interlaced
438         "basn2c08",    // PNG image data, 32 x 32, 8-bit/color RGB, non-interlaced
439         "basn3p08",    // PNG image data, 32 x 32, 8-bit colormap, non-interlaced
440         "basn4a08",    // PNG image data, 32 x 32, 8-bit gray+alpha, non-interlaced
441         "basn6a08",    // PNG image data, 32 x 32, 8-bit/color RGBA, non-interlaced
442     ];
443 
444     foreach (file; files) {
445         //writefln("%s", file);
446         auto a = read_image(png_path ~ file ~ ".png", ColFmt.RGBA);
447         auto b = read_image(tga_path ~ file ~ ".tga", ColFmt.RGBA);
448         auto c = read_image(bmp_path ~ file ~ ".bmp", ColFmt.RGBA);
449         assert(a.w == b.w && a.w == c.w);
450         assert(a.h == b.h && a.h == c.h);
451         assert(a.pixels.length == b.pixels.length && a.pixels.length == c.pixels.length);
452         assert(a.pixels[0..$] == b.pixels[0..$], "png/tga");
453         assert(a.pixels[0..$] == c.pixels[0..$], "png/bmp");
454     }
455 }