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