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