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 ©_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 }