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`.