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