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