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