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