1 // FYI: There used to be image resize code in here directly, but I moved it to `imageresize.d`. 2 /++ 3 This file imports all available image decoders in the arsd library, and provides convenient functions to load image regardless of it's format. Main functions: [loadImageFromFile] and [loadImageFromMemory]. 4 5 6 $(WARNING 7 This module is exempt from my usual build-compatibility guarantees. I may add new built-time dependency modules to it at any time without notice. 8 9 You should either copy the `image.d` module and the pieces you use to your own project, or always use it along with the rest of the repo and `dmd -i`, or the dub `arsd-official:image_files` subpackage, which both will include new files automatically and avoid breaking your build. 10 ) 11 12 History: 13 The image resize code used to live directly in here, but has now moved to a new module, [arsd.imageresize]. It is public imported here for compatibility, but the build has changed as of December 25, 2020. 14 +/ 15 module arsd.image; 16 17 public import arsd.color; 18 public import arsd.png; 19 public import arsd.jpeg; 20 public import arsd.bmp; 21 public import arsd.targa; 22 public import arsd.pcx; 23 public import arsd.dds; 24 public import arsd.svg; 25 26 public import arsd.imageresize; 27 28 import core.memory; 29 30 static if (__traits(compiles, { import iv.vfs; })) enum ArsdImageHasIVVFS = true; else enum ArsdImageHasIVVFS = false; 31 32 MemoryImage readSvg(string filename) { 33 import std.file; 34 return readSvg(cast(const(ubyte)[]) readText(filename)); 35 } 36 37 MemoryImage readSvg(const(ubyte)[] rawData) { 38 // Load 39 NSVG* image = nsvgParse(cast(const(char)[]) rawData); 40 41 if(image is null) 42 return null; 43 44 int w = cast(int) image.width + 1; 45 int h = cast(int) image.height + 1; 46 47 NSVGrasterizer rast = nsvgCreateRasterizer(); 48 auto img = new TrueColorImage(w, h); 49 rasterize(rast, image, 0, 0, 1, img.imageData.bytes.ptr, w, h, w*4); 50 image.kill(); 51 52 return img; 53 } 54 55 56 private bool strEquCI (const(char)[] s0, const(char)[] s1) pure nothrow @trusted @nogc { 57 if (s0.length != s1.length) return false; 58 foreach (immutable idx, char ch; s0) { 59 if (ch >= 'A' && ch <= 'Z') ch += 32; // poor man's tolower() 60 char c1 = s1.ptr[idx]; 61 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower() 62 if (ch != c1) return false; 63 } 64 return true; 65 } 66 67 68 /// Image formats `arsd.image` can load (except `Unknown`, of course). 69 enum ImageFileFormat { 70 Unknown, /// 71 Png, /// 72 Bmp, /// 73 Jpeg, /// 74 Tga, /// 75 Gif, /// we can't load it yet, but we can at least detect it 76 Pcx, /// can load 8BPP and 24BPP pcx images 77 Dds, /// can load ARGB8888, DXT1, DXT3, DXT5 dds images (without mipmaps) 78 Svg, /// will rasterize simple svg images 79 } 80 81 82 /// Try to guess image format from file extension. 83 public ImageFileFormat guessImageFormatFromExtension (const(char)[] filename) { 84 if (filename.length < 2) return ImageFileFormat.Unknown; 85 size_t extpos = filename.length; 86 version(Windows) { 87 while (extpos > 0 && filename.ptr[extpos-1] != '.' && filename.ptr[extpos-1] != '/' && filename.ptr[extpos-1] != '\\' && filename.ptr[extpos-1] != ':') --extpos; 88 } else { 89 while (extpos > 0 && filename.ptr[extpos-1] != '.' && filename.ptr[extpos-1] != '/') --extpos; 90 } 91 if (extpos == 0 || filename.ptr[extpos-1] != '.') return ImageFileFormat.Unknown; 92 auto ext = filename[extpos..$]; 93 if (strEquCI(ext, "png")) return ImageFileFormat.Png; 94 if (strEquCI(ext, "bmp")) return ImageFileFormat.Bmp; 95 if (strEquCI(ext, "jpg") || strEquCI(ext, "jpeg")) return ImageFileFormat.Jpeg; 96 if (strEquCI(ext, "gif")) return ImageFileFormat.Gif; 97 if (strEquCI(ext, "tga")) return ImageFileFormat.Tga; 98 if (strEquCI(ext, "pcx")) return ImageFileFormat.Pcx; 99 if (strEquCI(ext, "dds")) return ImageFileFormat.Dds; 100 if (strEquCI(ext, "svg")) return ImageFileFormat.Svg; 101 return ImageFileFormat.Unknown; 102 } 103 104 105 /// Try to guess image format by first data bytes. 106 public ImageFileFormat guessImageFormatFromMemory (const(void)[] membuf) { 107 enum TargaSign = "TRUEVISION-XFILE.\x00"; 108 auto buf = cast(const(ubyte)[])membuf; 109 if (buf.length == 0) return ImageFileFormat.Unknown; 110 // detect file format 111 // png 112 if (buf.length > 7 && buf.ptr[0] == 0x89 && buf.ptr[1] == 0x50 && buf.ptr[2] == 0x4E && 113 buf.ptr[3] == 0x47 && buf.ptr[4] == 0x0D && buf.ptr[5] == 0x0A && buf.ptr[6] == 0x1A) 114 { 115 return ImageFileFormat.Png; 116 } 117 // bmp 118 if (buf.length > 6 && buf.ptr[0] == 'B' && buf.ptr[1] == 'M') { 119 uint datasize = buf.ptr[2]|(buf.ptr[3]<<8)|(buf.ptr[4]<<16)|(buf.ptr[5]<<24); 120 if (datasize > 6 && datasize <= buf.length) return ImageFileFormat.Bmp; 121 } 122 // gif 123 if (buf.length > 5 && buf.ptr[0] == 'G' && buf.ptr[1] == 'I' && buf.ptr[2] == 'F' && 124 buf.ptr[3] == '8' && (buf.ptr[4] == '7' || buf.ptr[4] == '9')) 125 { 126 return ImageFileFormat.Gif; 127 } 128 // dds 129 if (ddsDetect(membuf)) return ImageFileFormat.Dds; 130 // jpg 131 try { 132 int width, height, components; 133 if (detect_jpeg_image_from_memory(buf, width, height, components)) return ImageFileFormat.Jpeg; 134 } catch (Exception e) {} // sorry 135 // tga (sorry, targas without footer, i don't love you) 136 if (buf.length > TargaSign.length+4*2 && cast(const(char)[])(buf[$-TargaSign.length..$]) == TargaSign) { 137 // more guesswork 138 switch (buf.ptr[2]) { 139 case 1: case 2: case 3: case 9: case 10: case 11: return ImageFileFormat.Tga; 140 default: 141 } 142 } 143 // ok, try to guess targa by validating some header fields 144 bool guessTarga () nothrow @trusted @nogc { 145 if (buf.length < 45) return false; // minimal 1x1 tga 146 immutable ubyte idlength = buf.ptr[0]; 147 immutable ubyte bColorMapType = buf.ptr[1]; 148 immutable ubyte type = buf.ptr[2]; 149 immutable ushort wColorMapFirstEntryIndex = cast(ushort)(buf.ptr[3]|(buf.ptr[4]<<8)); 150 immutable ushort wColorMapLength = cast(ushort)(buf.ptr[5]|(buf.ptr[6]<<8)); 151 immutable ubyte bColorMapEntrySize = buf.ptr[7]; 152 immutable ushort wOriginX = cast(ushort)(buf.ptr[8]|(buf.ptr[9]<<8)); 153 immutable ushort wOriginY = cast(ushort)(buf.ptr[10]|(buf.ptr[11]<<8)); 154 immutable ushort wImageWidth = cast(ushort)(buf.ptr[12]|(buf.ptr[13]<<8)); 155 immutable ushort wImageHeight = cast(ushort)(buf.ptr[14]|(buf.ptr[15]<<8)); 156 immutable ubyte bPixelDepth = buf.ptr[16]; 157 immutable ubyte bImageDescriptor = buf.ptr[17]; 158 if (wImageWidth < 1 || wImageHeight < 1 || wImageWidth > 32000 || wImageHeight > 32000) return false; // arbitrary limit 159 immutable uint pixelsize = (bPixelDepth>>3); 160 switch (type) { 161 case 2: // truecolor, raw 162 case 10: // truecolor, rle 163 switch (pixelsize) { 164 case 2: case 3: case 4: break; 165 default: return false; 166 } 167 break; 168 case 1: // paletted, raw 169 case 9: // paletted, rle 170 if (pixelsize != 1) return false; 171 break; 172 case 3: // b/w, raw 173 case 11: // b/w, rle 174 if (pixelsize != 1 && pixelsize != 2) return false; 175 break; 176 default: // invalid type 177 return false; 178 } 179 // check for valid colormap 180 switch (bColorMapType) { 181 case 0: 182 if (wColorMapFirstEntryIndex != 0 || wColorMapLength != 0) return 0; 183 break; 184 case 1: 185 if (bColorMapEntrySize != 15 && bColorMapEntrySize != 16 && bColorMapEntrySize != 24 && bColorMapEntrySize != 32) return false; 186 if (wColorMapLength == 0) return false; 187 break; 188 default: // invalid colormap type 189 return false; 190 } 191 if (((bImageDescriptor>>6)&3) != 0) return false; 192 // this *looks* like a tga 193 return true; 194 } 195 if (guessTarga()) return ImageFileFormat.Tga; 196 197 bool guessPcx() nothrow @trusted @nogc { 198 if (buf.length < 129) return false; // we should have at least header 199 200 ubyte manufacturer = buf.ptr[0]; 201 ubyte ver = buf.ptr[1]; 202 ubyte encoding = buf.ptr[2]; 203 ubyte bitsperpixel = buf.ptr[3]; 204 ushort xmin = cast(ushort)(buf.ptr[4]+256*buf.ptr[5]); 205 ushort ymin = cast(ushort)(buf.ptr[6]+256*buf.ptr[7]); 206 ushort xmax = cast(ushort)(buf.ptr[8]+256*buf.ptr[9]); 207 ushort ymax = cast(ushort)(buf.ptr[10]+256*buf.ptr[11]); 208 ubyte reserved = buf.ptr[64]; 209 ubyte colorplanes = buf.ptr[65]; 210 ushort bytesperline = cast(ushort)(buf.ptr[66]+256*buf.ptr[67]); 211 //ushort palettetype = cast(ushort)(buf.ptr[68]+256*buf.ptr[69]); 212 213 // check some header fields 214 if (manufacturer != 0x0a) return false; 215 if (/*ver != 0 && ver != 2 && ver != 3 &&*/ ver != 5) return false; 216 if (encoding != 0 && encoding != 1) return false; 217 218 int wdt = xmax-xmin+1; 219 int hgt = ymax-ymin+1; 220 221 // arbitrary size limits 222 if (wdt < 1 || wdt > 32000) return false; 223 if (hgt < 1 || hgt > 32000) return false; 224 225 if (bytesperline < wdt) return false; 226 227 // if it's not a 256-color PCX file, and not 24-bit PCX file, gtfo 228 bool bpp24 = false; 229 if (colorplanes == 1) { 230 if (bitsperpixel != 8 && bitsperpixel != 24 && bitsperpixel != 32) return false; 231 bpp24 = (bitsperpixel == 24); 232 } else if (colorplanes == 3 || colorplanes == 4) { 233 if (bitsperpixel != 8) return false; 234 bpp24 = true; 235 } 236 237 // additional checks 238 if (reserved != 0) return false; 239 240 // 8bpp files MUST have palette 241 if (!bpp24 && buf.length < 129+769) return false; 242 243 // it can be pcx 244 return true; 245 } 246 if (guessPcx()) return ImageFileFormat.Pcx; 247 248 // kinda lame svg detection but don't want to parse too much of it here 249 if (buf.length > 6 && buf.ptr[0] == '<') { 250 return ImageFileFormat.Svg; 251 } 252 253 // dunno 254 return ImageFileFormat.Unknown; 255 } 256 257 258 /// Try to guess image format from file name and load that image. 259 public MemoryImage loadImageFromFile(T:const(char)[]) (T filename) { 260 static if (is(T == typeof(null))) { 261 throw new Exception("cannot load image from unnamed file"); 262 } else { 263 final switch (guessImageFormatFromExtension(filename)) { 264 case ImageFileFormat.Unknown: 265 //throw new Exception("cannot determine file format from extension"); 266 static if (ArsdImageHasIVVFS) { 267 auto fl = VFile(filename); 268 } else { 269 import std.stdio; 270 static if (is(T == string)) { 271 auto fl = File(filename); 272 } else { 273 auto fl = File(filename.idup); 274 } 275 } 276 auto fsz = fl.size-fl.tell; 277 if (fsz < 4) throw new Exception("cannot determine file format"); 278 if (fsz > int.max/8) throw new Exception("image data too big"); 279 auto data = new ubyte[](cast(uint)fsz); 280 scope(exit) { import core.memory : GC; GC.free(data.ptr); } // this should be safe, as image will copy data to it's internal storage 281 fl.rawReadExact(data); 282 return loadImageFromMemory(data); 283 case ImageFileFormat.Png: static if (is(T == string)) return readPng(filename); else return readPng(filename.idup); 284 case ImageFileFormat.Bmp: static if (is(T == string)) return readBmp(filename); else return readBmp(filename.idup); 285 case ImageFileFormat.Jpeg: return readJpeg(filename); 286 case ImageFileFormat.Gif: throw new Exception("arsd has no GIF loader yet"); 287 case ImageFileFormat.Tga: return loadTga(filename); 288 case ImageFileFormat.Pcx: return loadPcx(filename); 289 case ImageFileFormat.Svg: static if (is(T == string)) return readSvg(filename); else return readSvg(filename.idup); 290 case ImageFileFormat.Dds: 291 static if (ArsdImageHasIVVFS) { 292 auto fl = VFile(filename); 293 } else { 294 import std.stdio; 295 static if (is(T == string)) { 296 auto fl = File(filename); 297 } else { 298 auto fl = File(filename.idup); 299 } 300 } 301 return ddsLoadFromFile(fl); 302 } 303 } 304 } 305 306 307 /// Try to guess image format from data and load that image. 308 public MemoryImage loadImageFromMemory (const(void)[] membuf) { 309 final switch (guessImageFormatFromMemory(membuf)) { 310 case ImageFileFormat.Unknown: throw new Exception("cannot determine file format"); 311 case ImageFileFormat.Png: return imageFromPng(readPng(cast(const(ubyte)[])membuf)); 312 case ImageFileFormat.Bmp: return readBmp(cast(const(ubyte)[])membuf); 313 case ImageFileFormat.Jpeg: return readJpegFromMemory(cast(const(ubyte)[])membuf); 314 case ImageFileFormat.Gif: throw new Exception("arsd has no GIF loader yet"); 315 case ImageFileFormat.Tga: return loadTgaMem(membuf); 316 case ImageFileFormat.Pcx: return loadPcxMem(membuf); 317 case ImageFileFormat.Svg: return readSvg(cast(const(ubyte)[]) membuf); 318 case ImageFileFormat.Dds: return ddsLoadFromMemory(membuf); 319 } 320 } 321 322 323 static if (ArsdImageHasIVVFS) { 324 import iv.vfs; 325 public MemoryImage loadImageFromFile (VFile fl) { 326 auto fsz = fl.size-fl.tell; 327 if (fsz < 4) throw new Exception("cannot determine file format"); 328 if (fsz > int.max/8) throw new Exception("image data too big"); 329 auto data = new ubyte[](cast(uint)fsz); 330 scope(exit) { import core.memory : GC; GC.free(data.ptr); } // this should be safe, as image will copy data to it's internal storage 331 fl.rawReadExact(data); 332 return loadImageFromMemory(data); 333 } 334 } 335 336 // FYI: There used to be image resize code in here directly, but I moved it to `imageresize.d`.