1 /// this file imports all available image decoders, and provides convenient functions to load image regardless of it's format.
2 module arsd.image;
3 
4 public import arsd.color;
5 public import arsd.png;
6 public import arsd.jpeg;
7 public import arsd.bmp;
8 public import arsd.targa;
9 public import arsd.pcx;
10 public import arsd.dds;
11 
12 import core.memory;
13 
14 static if (__traits(compiles, { import iv.vfs; })) enum ArsdImageHasIVVFS = true; else enum ArsdImageHasIVVFS = false;
15 
16 
17 private bool strEquCI (const(char)[] s0, const(char)[] s1) pure nothrow @trusted @nogc {
18   if (s0.length != s1.length) return false;
19   foreach (immutable idx, char ch; s0) {
20     if (ch >= 'A' && ch <= 'Z') ch += 32; // poor man's tolower()
21     char c1 = s1.ptr[idx];
22     if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
23     if (ch != c1) return false;
24   }
25   return true;
26 }
27 
28 
29 /// Image formats `arsd.image` can load (except `Unknown`, of course).
30 enum ImageFileFormat {
31   Unknown, ///
32   Png, ///
33   Bmp, ///
34   Jpeg, ///
35   Tga, ///
36   Gif, /// we can't load it yet, but we can at least detect it
37   Pcx, /// can load 8BPP and 24BPP pcx images
38   Dds, /// can load ARGB8888, DXT1, DXT3, DXT5 dds images (without mipmaps)
39 }
40 
41 
42 /// Try to guess image format from file extension.
43 public ImageFileFormat guessImageFormatFromExtension (const(char)[] filename) {
44   if (filename.length < 2) return ImageFileFormat.Unknown;
45   size_t extpos = filename.length;
46   version(Windows) {
47     while (extpos > 0 && filename.ptr[extpos-1] != '.' && filename.ptr[extpos-1] != '/' && filename.ptr[extpos-1] != '\\' && filename.ptr[extpos-1] != ':') --extpos;
48   } else {
49     while (extpos > 0 && filename.ptr[extpos-1] != '.' && filename.ptr[extpos-1] != '/') --extpos;
50   }
51   if (extpos == 0 || filename.ptr[extpos-1] != '.') return ImageFileFormat.Unknown;
52   auto ext = filename[extpos..$];
53   if (strEquCI(ext, "png")) return ImageFileFormat.Png;
54   if (strEquCI(ext, "bmp")) return ImageFileFormat.Bmp;
55   if (strEquCI(ext, "jpg") || strEquCI(ext, "jpeg")) return ImageFileFormat.Jpeg;
56   if (strEquCI(ext, "gif")) return ImageFileFormat.Gif;
57   if (strEquCI(ext, "tga")) return ImageFileFormat.Tga;
58   if (strEquCI(ext, "pcx")) return ImageFileFormat.Pcx;
59   if (strEquCI(ext, "dds")) return ImageFileFormat.Dds;
60   return ImageFileFormat.Unknown;
61 }
62 
63 
64 /// Try to guess image format by first data bytes.
65 public ImageFileFormat guessImageFormatFromMemory (const(void)[] membuf) {
66   enum TargaSign = "TRUEVISION-XFILE.\x00";
67   auto buf = cast(const(ubyte)[])membuf;
68   if (buf.length == 0) return ImageFileFormat.Unknown;
69   // detect file format
70   // png
71   if (buf.length > 7 && buf.ptr[0] == 0x89 && buf.ptr[1] == 0x50 && buf.ptr[2] == 0x4E &&
72       buf.ptr[3] == 0x47 && buf.ptr[4] == 0x0D && buf.ptr[5] == 0x0A && buf.ptr[6] == 0x1A)
73   {
74     return ImageFileFormat.Png;
75   }
76   // bmp
77   if (buf.length > 6 && buf.ptr[0] == 'B' && buf.ptr[1] == 'M') {
78     uint datasize = buf.ptr[2]|(buf.ptr[3]<<8)|(buf.ptr[4]<<16)|(buf.ptr[5]<<24);
79     if (datasize > 6 && datasize <= buf.length) return ImageFileFormat.Bmp;
80   }
81   // gif
82   if (buf.length > 5 && buf.ptr[0] == 'G' && buf.ptr[1] == 'I' && buf.ptr[2] == 'F' &&
83       buf.ptr[3] == '8' && (buf.ptr[4] == '7' || buf.ptr[4] == '9'))
84   {
85     return ImageFileFormat.Gif;
86   }
87   // dds
88   if (ddsDetect(membuf)) return ImageFileFormat.Dds;
89   // jpg
90   try {
91     int width, height, components;
92     if (detect_jpeg_image_from_memory(buf, width, height, components)) return ImageFileFormat.Jpeg;
93   } catch (Exception e) {} // sorry
94   // tga (sorry, targas without footer, i don't love you)
95   if (buf.length > TargaSign.length+4*2 && cast(const(char)[])(buf[$-TargaSign.length..$]) == TargaSign) {
96     // more guesswork
97     switch (buf.ptr[2]) {
98       case 1: case 2: case 3: case 9: case 10: case 11: return ImageFileFormat.Tga;
99       default:
100     }
101   }
102   // ok, try to guess targa by validating some header fields
103   bool guessTarga () nothrow @trusted @nogc {
104     if (buf.length < 45) return false; // minimal 1x1 tga
105     immutable ubyte idlength = buf.ptr[0];
106     immutable ubyte bColorMapType = buf.ptr[1];
107     immutable ubyte type = buf.ptr[2];
108     immutable ushort wColorMapFirstEntryIndex = cast(ushort)(buf.ptr[3]|(buf.ptr[4]<<8));
109     immutable ushort wColorMapLength = cast(ushort)(buf.ptr[5]|(buf.ptr[6]<<8));
110     immutable ubyte bColorMapEntrySize = buf.ptr[7];
111     immutable ushort wOriginX = cast(ushort)(buf.ptr[8]|(buf.ptr[9]<<8));
112     immutable ushort wOriginY = cast(ushort)(buf.ptr[10]|(buf.ptr[11]<<8));
113     immutable ushort wImageWidth = cast(ushort)(buf.ptr[12]|(buf.ptr[13]<<8));
114     immutable ushort wImageHeight = cast(ushort)(buf.ptr[14]|(buf.ptr[15]<<8));
115     immutable ubyte bPixelDepth = buf.ptr[16];
116     immutable ubyte bImageDescriptor = buf.ptr[17];
117     if (wImageWidth < 1 || wImageHeight < 1 || wImageWidth > 32000 || wImageHeight > 32000) return false; // arbitrary limit
118     immutable uint pixelsize = (bPixelDepth>>3);
119     switch (type) {
120       case 2: // truecolor, raw
121       case 10: // truecolor, rle
122         switch (pixelsize) {
123           case 2: case 3: case 4: break;
124           default: return false;
125         }
126         break;
127       case 1: // paletted, raw
128       case 9: // paletted, rle
129         if (pixelsize != 1) return false;
130         break;
131       case 3: // b/w, raw
132       case 11: // b/w, rle
133         if (pixelsize != 1 && pixelsize != 2) return false;
134         break;
135       default: // invalid type
136         return false;
137     }
138     // check for valid colormap
139     switch (bColorMapType) {
140       case 0:
141         if (wColorMapFirstEntryIndex != 0 || wColorMapLength != 0) return 0;
142         break;
143       case 1:
144         if (bColorMapEntrySize != 15 && bColorMapEntrySize != 16 && bColorMapEntrySize != 24 && bColorMapEntrySize != 32) return false;
145         if (wColorMapLength == 0) return false;
146         break;
147       default: // invalid colormap type
148         return false;
149     }
150     if (((bImageDescriptor>>6)&3) != 0) return false;
151     // this *looks* like a tga
152     return true;
153   }
154   if (guessTarga()) return ImageFileFormat.Tga;
155 
156   bool guessPcx() nothrow @trusted @nogc {
157     if (buf.length < 129) return false; // we should have at least header
158 
159     ubyte manufacturer = buf.ptr[0];
160     ubyte ver = buf.ptr[1];
161     ubyte encoding = buf.ptr[2];
162     ubyte bitsperpixel = buf.ptr[3];
163     ushort xmin = cast(ushort)(buf.ptr[4]+256*buf.ptr[5]);
164     ushort ymin = cast(ushort)(buf.ptr[6]+256*buf.ptr[7]);
165     ushort xmax = cast(ushort)(buf.ptr[8]+256*buf.ptr[9]);
166     ushort ymax = cast(ushort)(buf.ptr[10]+256*buf.ptr[11]);
167     ubyte reserved = buf.ptr[64];
168     ubyte colorplanes = buf.ptr[65];
169     ushort bytesperline = cast(ushort)(buf.ptr[66]+256*buf.ptr[67]);
170     //ushort palettetype = cast(ushort)(buf.ptr[68]+256*buf.ptr[69]);
171 
172     // check some header fields
173     if (manufacturer != 0x0a) return false;
174     if (/*ver != 0 && ver != 2 && ver != 3 &&*/ ver != 5) return false;
175     if (encoding != 0 && encoding != 1) return false;
176 
177     int wdt = xmax-xmin+1;
178     int hgt = ymax-ymin+1;
179 
180     // arbitrary size limits
181     if (wdt < 1 || wdt > 32000) return false;
182     if (hgt < 1 || hgt > 32000) return false;
183 
184     if (bytesperline < wdt) return false;
185 
186     // if it's not a 256-color PCX file, and not 24-bit PCX file, gtfo
187     bool bpp24 = false;
188     if (colorplanes == 1) {
189       if (bitsperpixel != 8 && bitsperpixel != 24 && bitsperpixel != 32) return false;
190       bpp24 = (bitsperpixel == 24);
191     } else if (colorplanes == 3 || colorplanes == 4) {
192       if (bitsperpixel != 8) return false;
193       bpp24 = true;
194     }
195 
196     // additional checks
197     if (reserved != 0) return false;
198 
199     // 8bpp files MUST have palette
200     if (!bpp24 && buf.length < 129+769) return false;
201 
202     // it can be pcx
203     return true;
204   }
205   if (guessPcx()) return ImageFileFormat.Pcx;
206 
207   // dunno
208   return ImageFileFormat.Unknown;
209 }
210 
211 
212 /// Try to guess image format from file name and load that image.
213 public MemoryImage loadImageFromFile(T:const(char)[]) (T filename) {
214   static if (is(T == typeof(null))) {
215     throw new Exception("cannot load image from unnamed file");
216   } else {
217     final switch (guessImageFormatFromExtension(filename)) {
218       case ImageFileFormat.Unknown:
219         //throw new Exception("cannot determine file format from extension");
220         static if (ArsdImageHasIVVFS) {
221           auto fl = VFile(filename);
222         } else {
223           import std.stdio;
224           static if (is(T == string)) {
225             auto fl = File(filename);
226           } else {
227             auto fl = File(filename.idup);
228           }
229         }
230         auto fsz = fl.size-fl.tell;
231         if (fsz < 4) throw new Exception("cannot determine file format");
232         if (fsz > int.max/8) throw new Exception("image data too big");
233         auto data = new ubyte[](cast(uint)fsz);
234         scope(exit) { import core.memory : GC; GC.free(data.ptr); } // this should be safe, as image will copy data to it's internal storage
235         fl.rawReadExact(data);
236         return loadImageFromMemory(data);
237       case ImageFileFormat.Png: static if (is(T == string)) return readPng(filename); else return readPng(filename.idup);
238       case ImageFileFormat.Bmp: static if (is(T == string)) return readBmp(filename); else return readBmp(filename.idup);
239       case ImageFileFormat.Jpeg: return readJpeg(filename);
240       case ImageFileFormat.Gif: throw new Exception("arsd has no GIF loader yet");
241       case ImageFileFormat.Tga: return loadTga(filename);
242       case ImageFileFormat.Pcx: return loadPcx(filename);
243       case ImageFileFormat.Dds:
244         static if (ArsdImageHasIVVFS) {
245           auto fl = VFile(filename);
246         } else {
247           import std.stdio;
248           static if (is(T == string)) {
249             auto fl = File(filename);
250           } else {
251             auto fl = File(filename.idup);
252           }
253         }
254         return ddsLoadFromFile(fl);
255     }
256   }
257 }
258 
259 
260 /// Try to guess image format from data and load that image.
261 public MemoryImage loadImageFromMemory (const(void)[] membuf) {
262   final switch (guessImageFormatFromMemory(membuf)) {
263     case ImageFileFormat.Unknown: throw new Exception("cannot determine file format");
264     case ImageFileFormat.Png: return imageFromPng(readPng(cast(const(ubyte)[])membuf));
265     case ImageFileFormat.Bmp: return readBmp(cast(const(ubyte)[])membuf);
266     case ImageFileFormat.Jpeg: return readJpegFromMemory(cast(const(ubyte)[])membuf);
267     case ImageFileFormat.Gif: throw new Exception("arsd has no GIF loader yet");
268     case ImageFileFormat.Tga: return loadTgaMem(membuf);
269     case ImageFileFormat.Pcx: return loadPcxMem(membuf);
270     case ImageFileFormat.Dds: return ddsLoadFromMemory(membuf);
271   }
272 }
273 
274 
275 static if (ArsdImageHasIVVFS) {
276 import iv.vfs;
277 public MemoryImage loadImageFromFile (VFile fl) {
278   auto fsz = fl.size-fl.tell;
279   if (fsz < 4) throw new Exception("cannot determine file format");
280   if (fsz > int.max/8) throw new Exception("image data too big");
281   auto data = new ubyte[](cast(uint)fsz);
282   scope(exit) { import core.memory : GC; GC.free(data.ptr); } // this should be safe, as image will copy data to it's internal storage
283   fl.rawReadExact(data);
284   return loadImageFromMemory(data);
285 }
286 }
287 
288 
289 // ////////////////////////////////////////////////////////////////////////// //
290 // Separable filtering image rescaler v2.21, Rich Geldreich - richgel99@gmail.com
291 //
292 // This is free and unencumbered software released into the public domain.
293 //
294 // Anyone is free to copy, modify, publish, use, compile, sell, or
295 // distribute this software, either in source code form or as a compiled
296 // binary, for any purpose, commercial or non-commercial, and by any
297 // means.
298 //
299 // In jurisdictions that recognize copyright laws, the author or authors
300 // of this software dedicate any and all copyright interest in the
301 // software to the public domain. We make this dedication for the benefit
302 // of the public at large and to the detriment of our heirs and
303 // successors. We intend this dedication to be an overt act of
304 // relinquishment in perpetuity of all present and future rights to this
305 // software under copyright law.
306 //
307 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
308 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
309 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
310 // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
311 // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
312 // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
313 // OTHER DEALINGS IN THE SOFTWARE.
314 //
315 // For more information, please refer to <http://unlicense.org/>
316 //
317 // Feb. 1996: Creation, losely based on a heavily bugfixed version of Schumacher's resampler in Graphics Gems 3.
318 // Oct. 2000: Ported to C++, tweaks.
319 // May 2001: Continous to discrete mapping, box filter tweaks.
320 // March 9, 2002: Kaiser filter grabbed from Jonathan Blow's GD magazine mipmap sample code.
321 // Sept. 8, 2002: Comments cleaned up a bit.
322 // Dec. 31, 2008: v2.2: Bit more cleanup, released as public domain.
323 // June 4, 2012: v2.21: Switched to unlicense.org, integrated GCC fixes supplied by Peter Nagy <petern@crytek.com>, Anteru at anteru.net, and clay@coge.net,
324 // added Codeblocks project (for testing with MinGW and GCC), VS2008 static code analysis pass.
325 // float or double
326 private:
327 
328 //version = iresample_debug;
329 
330 
331 // ////////////////////////////////////////////////////////////////////////// //
332 public enum ImageResizeDefaultFilter = "lanczos4"; /// Default filter for image resampler.
333 public enum ImageResizeMaxDimension = 65536; /// Maximum image width/height for image resampler.
334 
335 
336 // ////////////////////////////////////////////////////////////////////////// //
337 /// Number of known image resizer filters.
338 public @property int imageResizeFilterCount () { pragma(inline, true); return NumFilters; }
339 
340 /// Get filter name. Will return `null` for invalid index.
341 public string imageResizeFilterName (long idx) { pragma(inline, true); return (idx >= 0 && idx < NumFilters ? gFilters.ptr[cast(uint)idx].name : null); }
342 
343 /// Find filter index by name. Will use default filter for invalid names.
344 public int imageResizeFindFilter (const(char)[] name, const(char)[] defaultFilter=ImageResizeDefaultFilter) {
345   int res = resamplerFindFilterInternal(name);
346   if (res >= 0) return res;
347   res = resamplerFindFilterInternal(defaultFilter);
348   if (res >= 0) return res;
349   res = resamplerFindFilterInternal("lanczos4");
350   assert(res >= 0);
351   return res;
352 }
353 
354 
355 // ////////////////////////////////////////////////////////////////////////// //
356 /// Resize image.
357 public TrueColorImage imageResize(int Components=4) (MemoryImage msrcimg, int dstwdt, int dsthgt, const(char)[] filter=null, float gamma=1.0f, float filterScale=1.0f) {
358   static assert(Components == 1 || Components == 3 || Components == 4, "invalid number of components in color");
359   return imageResize!Components(msrcimg, dstwdt, dsthgt, imageResizeFindFilter(filter), gamma, filterScale);
360 }
361 
362 /// Ditto.
363 public TrueColorImage imageResize(int Components=4) (MemoryImage msrcimg, int dstwdt, int dsthgt, int filter, float gamma=1.0f, float filterScale=1.0f) {
364   static assert(Components == 1 || Components == 3 || Components == 4, "invalid number of components in color");
365   if (msrcimg is null || msrcimg.width < 1 || msrcimg.height < 1 || msrcimg.width > ImageResizeMaxDimension || msrcimg.height > ImageResizeMaxDimension) {
366     throw new Exception("invalid source image");
367   }
368   if (dstwdt < 1 || dsthgt < 1 || dstwdt > ImageResizeMaxDimension || dsthgt > ImageResizeMaxDimension) throw new Exception("invalid destination image size");
369   auto resimg = new TrueColorImage(dstwdt, dsthgt);
370   scope(failure) .destroy(resimg);
371   if (auto tc = cast(TrueColorImage)msrcimg) {
372     imageResize!Components(
373       delegate (Color[] destrow, int y) { destrow[] = tc.imageData.colors[y*tc.width..(y+1)*tc.width]; },
374       delegate (int y, const(Color)[] row) { resimg.imageData.colors[y*resimg.width..(y+1)*resimg.width] = row[]; },
375       msrcimg.width, msrcimg.height, dstwdt, dsthgt, filter, gamma, filterScale
376     );
377   } else {
378     imageResize!Components(
379       delegate (Color[] destrow, int y) { foreach (immutable x, ref c; destrow) c = msrcimg.getPixel(cast(int)x, y); },
380       delegate (int y, const(Color)[] row) { resimg.imageData.colors[y*resimg.width..(y+1)*resimg.width] = row[]; },
381       msrcimg.width, msrcimg.height, dstwdt, dsthgt, filter, gamma, filterScale
382     );
383   }
384   return resimg;
385 }
386 
387 
388 private {
389   enum Linear2srgbTableSize = 4096;
390   enum InvLinear2srgbTableSize = cast(float)(1.0f/Linear2srgbTableSize);
391   float[256] srgb2linear = void;
392   ubyte[Linear2srgbTableSize] linear2srgb = void;
393   float lastGamma = float.nan;
394 }
395 
396 /// Resize image.
397 /// Partial gamma correction looks better on mips; set to 1.0 to disable gamma correction.
398 /// Filter scale: values < 1.0 cause aliasing, but create sharper looking mips (0.75f, for example).
399 public void imageResize(int Components=4) (
400   scope void delegate (Color[] destrow, int y) srcGetRow,
401   scope void delegate (int y, const(Color)[] row) dstPutRow,
402   int srcwdt, int srchgt, int dstwdt, int dsthgt,
403   int filter=-1, float gamma=1.0f, float filterScale=1.0f
404 ) {
405   static assert(Components == 1 || Components == 3 || Components == 4, "invalid number of components in color");
406   assert(srcGetRow !is null);
407   assert(dstPutRow !is null);
408 
409   if (srcwdt < 1 || srchgt < 1 || dstwdt < 1 || dsthgt < 1 ||
410       srcwdt > ImageResizeMaxDimension || srchgt > ImageResizeMaxDimension ||
411       dstwdt > ImageResizeMaxDimension || dsthgt > ImageResizeMaxDimension) throw new Exception("invalid image size");
412 
413   if (filter < 0 || filter >= NumFilters) {
414     filter = resamplerFindFilterInternal(ImageResizeDefaultFilter);
415     if (filter < 0) {
416       filter = resamplerFindFilterInternal("lanczos4");
417     }
418   }
419   assert(filter >= 0 && filter < NumFilters);
420 
421 
422   if (lastGamma != gamma) {
423     version(iresample_debug) { import core.stdc.stdio; stderr.fprintf("creating translation tables for gamma %f (previous gamma is %f)\n", gamma, lastGamma); }
424     foreach (immutable i, ref v; srgb2linear[]) {
425       import std.math : pow;
426       v = cast(float)pow(cast(int)i*1.0f/255.0f, gamma);
427     }
428     immutable float invSourceGamma = 1.0f/gamma;
429     foreach (immutable i, ref v; linear2srgb[]) {
430       import std.math : pow;
431       int k = cast(int)(255.0f*pow(cast(int)i*InvLinear2srgbTableSize, invSourceGamma)+0.5f);
432       if (k < 0) k = 0; else if (k > 255) k = 255;
433       v = cast(ubyte)k;
434     }
435     lastGamma = gamma;
436   }
437   version(iresample_debug) { import core.stdc.stdio; stderr.fprintf("filter is %d\n", filter); }
438 
439   ImageResampleWorker[Components] resamplers;
440   float[][Components] samples;
441   Color[] srcrow, dstrow;
442   scope(exit) {
443     foreach (ref rsm; resamplers[]) .destroy(rsm);
444     foreach (ref smr; samples[]) .destroy(smr);
445   }
446 
447   // now create a ImageResampleWorker instance for each component to process
448   // the first instance will create new contributor tables, which are shared by the resamplers
449   // used for the other components (a memory and slight cache efficiency optimization).
450   resamplers[0] = new ImageResampleWorker(srcwdt, srchgt, dstwdt, dsthgt, ImageResampleWorker.BoundaryClamp, 0.0f, 1.0f, filter, null, null, filterScale, filterScale);
451   samples[0].length = srcwdt;
452   srcrow.length = srcwdt;
453   dstrow.length = dstwdt;
454   foreach (immutable i; 1..Components) {
455     resamplers[i] = new ImageResampleWorker(srcwdt, srchgt, dstwdt, dsthgt, ImageResampleWorker.BoundaryClamp, 0.0f, 1.0f, filter, resamplers[0].getClistX(), resamplers[0].getClistY(), filterScale, filterScale);
456     samples[i].length = srcwdt;
457   }
458 
459   int dsty = 0;
460   foreach (immutable int srcy; 0..srchgt) {
461     // get row components
462     srcGetRow(srcrow, srcy);
463     {
464       auto scp = srcrow.ptr;
465       foreach (immutable x; 0..srcwdt) {
466         auto sc = *scp++;
467         samples.ptr[0].ptr[x] = srgb2linear.ptr[sc.r]; // first component
468         static if (Components > 1) samples.ptr[1].ptr[x] = srgb2linear.ptr[sc.g]; // second component
469         static if (Components > 2) samples.ptr[2].ptr[x] = srgb2linear.ptr[sc.b]; // thirs component
470         static if (Components == 4) samples.ptr[3].ptr[x] = sc.a*(1.0f/255.0f); // fourth component is alpha, and it is already linear
471       }
472     }
473 
474     foreach (immutable c; 0..Components) if (!resamplers.ptr[c].putLine(samples.ptr[c].ptr)) assert(0, "out of memory");
475 
476     for (;;) {
477       int compIdx = 0;
478       for (; compIdx < Components; ++compIdx) {
479         const(float)* outsmp = resamplers.ptr[compIdx].getLine();
480         if (outsmp is null) break;
481         auto dsc = dstrow.ptr;
482         // alpha?
483         static if (Components == 4) {
484           if (compIdx == 3) {
485             foreach (immutable x; 0..dstwdt) {
486               dsc.a = Color.clampToByte(cast(int)(255.0f*(*outsmp++)+0.5f));
487               ++dsc;
488             }
489             continue;
490           }
491         }
492         // color
493         auto dsb = (cast(ubyte*)dsc)+compIdx;
494         foreach (immutable x; 0..dstwdt) {
495           int j = cast(int)(Linear2srgbTableSize*(*outsmp++)+0.5f);
496           if (j < 0) j = 0; else if (j >= Linear2srgbTableSize) j = Linear2srgbTableSize-1;
497           *dsb = linear2srgb.ptr[j];
498           dsb += 4;
499         }
500       }
501       if (compIdx < Components) break;
502       // fill destination line
503       assert(dsty < dsthgt);
504       static if (Components != 4) {
505         auto dsc = dstrow.ptr;
506         foreach (immutable x; 0..dstwdt) {
507           static if (Components == 1) dsc.g = dsc.b = dsc.r;
508           dsc.a = 255;
509           ++dsc;
510         }
511       }
512       //version(iresample_debug) { import core.stdc.stdio; stderr.fprintf("writing dest row %d with %u components\n", dsty, Components); }
513       dstPutRow(dsty, dstrow);
514       ++dsty;
515     }
516   }
517 }
518 
519 
520 // ////////////////////////////////////////////////////////////////////////// //
521 public final class ImageResampleWorker {
522 nothrow @trusted @nogc:
523 public:
524   alias ResampleReal = float;
525   alias Sample = ResampleReal;
526 
527   static struct Contrib {
528     ResampleReal weight;
529     ushort pixel;
530   }
531 
532   static struct ContribList {
533     ushort n;
534     Contrib* p;
535   }
536 
537   alias BoundaryOp = int;
538   enum /*Boundary_Op*/ {
539     BoundaryWrap = 0,
540     BoundaryReflect = 1,
541     BoundaryClamp = 2,
542   }
543 
544   alias Status = int;
545   enum /*Status*/ {
546     StatusOkay = 0,
547     StatusOutOfMemory = 1,
548     StatusBadFilterName = 2,
549     StatusScanBufferFull = 3,
550   }
551 
552 private:
553   alias FilterFunc = ResampleReal function (ResampleReal) nothrow @trusted @nogc;
554 
555   int mIntermediateX;
556 
557   int mResampleSrcX;
558   int mResampleSrcY;
559   int mResampleDstX;
560   int mResampleDstY;
561 
562   BoundaryOp mBoundaryOp;
563 
564   Sample* mPdstBuf;
565   Sample* mPtmpBuf;
566 
567   ContribList* mPclistX;
568   ContribList* mPclistY;
569 
570   bool mClistXForced;
571   bool mClistYForced;
572 
573   bool mDelayXResample;
574 
575   int* mPsrcYCount;
576   ubyte* mPsrcYFlag;
577 
578   // The maximum number of scanlines that can be buffered at one time.
579   enum MaxScanBufSize = ImageResizeMaxDimension;
580 
581   static struct ScanBuf {
582     int[MaxScanBufSize] scanBufY;
583     Sample*[MaxScanBufSize] scanBufL;
584   }
585 
586   ScanBuf* mPscanBuf;
587 
588   int mCurSrcY;
589   int mCurDstY;
590 
591   Status mStatus;
592 
593   // The make_clist() method generates, for all destination samples,
594   // the list of all source samples with non-zero weighted contributions.
595   ContribList* makeClist(
596     int srcX, int dstX, BoundaryOp boundaryOp,
597     FilterFunc Pfilter,
598     ResampleReal filterSupport,
599     ResampleReal filterScale,
600     ResampleReal srcOfs)
601   {
602     import core.stdc.stdlib : calloc, free;
603     import std.math : floor, ceil;
604 
605     static struct ContribBounds {
606       // The center of the range in DISCRETE coordinates (pixel center = 0.0f).
607       ResampleReal center;
608       int left, right;
609     }
610 
611     ContribList* Pcontrib, PcontribRes;
612     Contrib* Pcpool;
613     Contrib* PcpoolNext;
614     ContribBounds* PcontribBounds;
615 
616     if ((Pcontrib = cast(ContribList*)calloc(dstX, ContribList.sizeof)) is null) return null;
617     scope(exit) if (Pcontrib !is null) free(Pcontrib);
618 
619     PcontribBounds = cast(ContribBounds*)calloc(dstX, ContribBounds.sizeof);
620     if (PcontribBounds is null) return null;
621     scope(exit) free(PcontribBounds);
622 
623     enum ResampleReal NUDGE = 0.5f;
624     immutable ResampleReal ooFilterScale = 1.0f/filterScale;
625     immutable ResampleReal xscale = dstX/cast(ResampleReal)srcX;
626 
627     if (xscale < 1.0f) {
628       int total = 0;
629       // Handle case when there are fewer destination samples than source samples (downsampling/minification).
630       // stretched half width of filter
631       immutable ResampleReal halfWidth = (filterSupport/xscale)*filterScale;
632       // Find the range of source sample(s) that will contribute to each destination sample.
633       foreach (immutable i; 0..dstX) {
634         // Convert from discrete to continuous coordinates, scale, then convert back to discrete.
635         ResampleReal center = (cast(ResampleReal)i+NUDGE)/xscale;
636         center -= NUDGE;
637         center += srcOfs;
638         immutable int left = castToInt(cast(ResampleReal)floor(center-halfWidth));
639         immutable int right = castToInt(cast(ResampleReal)ceil(center+halfWidth));
640         PcontribBounds[i].center = center;
641         PcontribBounds[i].left = left;
642         PcontribBounds[i].right = right;
643         total += (right-left+1);
644       }
645 
646       // Allocate memory for contributors.
647       if (total == 0 || ((Pcpool = cast(Contrib*)calloc(total, Contrib.sizeof)) is null)) return null;
648       //scope(failure) free(Pcpool);
649       //immutable int total = n;
650 
651       PcpoolNext = Pcpool;
652 
653       // Create the list of source samples which contribute to each destination sample.
654       foreach (immutable i; 0..dstX) {
655         int maxK = -1;
656         ResampleReal maxW = -1e+20f;
657 
658         ResampleReal center = PcontribBounds[i].center;
659         immutable int left = PcontribBounds[i].left;
660         immutable int right = PcontribBounds[i].right;
661 
662         Pcontrib[i].n = 0;
663         Pcontrib[i].p = PcpoolNext;
664         PcpoolNext += (right-left+1);
665         assert(PcpoolNext-Pcpool <= total);
666 
667         ResampleReal totalWeight0 = 0;
668         foreach (immutable j; left..right+1) totalWeight0 += Pfilter((center-cast(ResampleReal)j)*xscale*ooFilterScale);
669         immutable ResampleReal norm = cast(ResampleReal)(1.0f/totalWeight0);
670 
671         ResampleReal totalWeight1 = 0;
672         foreach (immutable j; left..right+1) {
673           immutable ResampleReal weight = Pfilter((center-cast(ResampleReal)j)*xscale*ooFilterScale)*norm;
674           if (weight == 0.0f) continue;
675           immutable int n = reflect(j, srcX, boundaryOp);
676           // Increment the number of source samples which contribute to the current destination sample.
677           immutable int k = Pcontrib[i].n++;
678           Pcontrib[i].p[k].pixel = cast(ushort)(n); // store src sample number
679           Pcontrib[i].p[k].weight = weight; // store src sample weight
680           totalWeight1 += weight; // total weight of all contributors
681           if (weight > maxW) {
682             maxW = weight;
683             maxK = k;
684           }
685         }
686         //assert(Pcontrib[i].n);
687         //assert(max_k != -1);
688         if (maxK == -1 || Pcontrib[i].n == 0) return null;
689         if (totalWeight1 != 1.0f) Pcontrib[i].p[maxK].weight += 1.0f-totalWeight1;
690       }
691     } else {
692       int total = 0;
693       // Handle case when there are more destination samples than source samples (upsampling).
694       immutable ResampleReal halfWidth = filterSupport*filterScale;
695       // Find the source sample(s) that contribute to each destination sample.
696       foreach (immutable i; 0..dstX) {
697         // Convert from discrete to continuous coordinates, scale, then convert back to discrete.
698         ResampleReal center = (cast(ResampleReal)i+NUDGE)/xscale;
699         center -= NUDGE;
700         center += srcOfs;
701         immutable int left = castToInt(cast(ResampleReal)floor(center-halfWidth));
702         immutable int right = castToInt(cast(ResampleReal)ceil(center+halfWidth));
703         PcontribBounds[i].center = center;
704         PcontribBounds[i].left = left;
705         PcontribBounds[i].right = right;
706         total += (right-left+1);
707       }
708 
709       // Allocate memory for contributors.
710       if (total == 0 || ((Pcpool = cast(Contrib*)calloc(total, Contrib.sizeof)) is null)) return null;
711       //scope(failure) free(Pcpool);
712 
713       PcpoolNext = Pcpool;
714 
715       // Create the list of source samples which contribute to each destination sample.
716       foreach (immutable i; 0..dstX) {
717         int maxK = -1;
718         ResampleReal maxW = -1e+20f;
719 
720         ResampleReal center = PcontribBounds[i].center;
721         immutable int left = PcontribBounds[i].left;
722         immutable int right = PcontribBounds[i].right;
723 
724         Pcontrib[i].n = 0;
725         Pcontrib[i].p = PcpoolNext;
726         PcpoolNext += (right-left+1);
727         assert(PcpoolNext-Pcpool <= total);
728 
729         ResampleReal totalWeight0 = 0;
730         foreach (immutable j; left..right+1) totalWeight0 += Pfilter((center-cast(ResampleReal)j)*ooFilterScale);
731         immutable ResampleReal norm = cast(ResampleReal)(1.0f/totalWeight0);
732 
733         ResampleReal totalWeight1 = 0;
734         foreach (immutable j; left..right+1) {
735           immutable ResampleReal weight = Pfilter((center-cast(ResampleReal)j)*ooFilterScale)*norm;
736           if (weight == 0.0f) continue;
737           immutable int n = reflect(j, srcX, boundaryOp);
738           // Increment the number of source samples which contribute to the current destination sample.
739           immutable int k = Pcontrib[i].n++;
740           Pcontrib[i].p[k].pixel = cast(ushort)(n); // store src sample number
741           Pcontrib[i].p[k].weight = weight; // store src sample weight
742           totalWeight1 += weight; // total weight of all contributors
743           if (weight > maxW) {
744             maxW = weight;
745             maxK = k;
746           }
747         }
748         //assert(Pcontrib[i].n);
749         //assert(max_k != -1);
750         if (maxK == -1 || Pcontrib[i].n == 0) return null;
751         if (totalWeight1 != 1.0f) Pcontrib[i].p[maxK].weight += 1.0f-totalWeight1;
752       }
753     }
754     // don't free return value
755     PcontribRes = Pcontrib;
756     Pcontrib = null;
757     return PcontribRes;
758   }
759 
760   static int countOps (const(ContribList)* Pclist, int k) {
761     int t = 0;
762     foreach (immutable i; 0..k) t += Pclist[i].n;
763     return t;
764   }
765 
766   private ResampleReal mLo;
767   private ResampleReal mHi;
768 
769   ResampleReal clampSample (ResampleReal f) const {
770     pragma(inline, true);
771     if (f < mLo) f = mLo; else if (f > mHi) f = mHi;
772     return f;
773   }
774 
775 public:
776   // src_x/src_y - Input dimensions
777   // dst_x/dst_y - Output dimensions
778   // boundary_op - How to sample pixels near the image boundaries
779   // sample_low/sample_high - Clamp output samples to specified range, or disable clamping if sample_low >= sample_high
780   // Pclist_x/Pclist_y - Optional pointers to contributor lists from another instance of a ImageResampleWorker
781   // src_x_ofs/src_y_ofs - Offset input image by specified amount (fractional values okay)
782   this(
783     int srcX, int srcY,
784     int dstX, int dstY,
785     BoundaryOp boundaryOp=BoundaryClamp,
786     ResampleReal sampleLow=0.0f, ResampleReal sampleHigh=0.0f,
787     int PfilterIndex=-1,
788     ContribList* PclistX=null,
789     ContribList* PclistY=null,
790     ResampleReal filterXScale=1.0f,
791     ResampleReal filterYScale=1.0f,
792     ResampleReal srcXOfs=0.0f,
793     ResampleReal srcYOfs=0.0f)
794   {
795     import core.stdc.stdlib : calloc, malloc;
796 
797     int i, j;
798     ResampleReal support;
799     FilterFunc func;
800 
801     assert(srcX > 0);
802     assert(srcY > 0);
803     assert(dstX > 0);
804     assert(dstY > 0);
805 
806     mLo = sampleLow;
807     mHi = sampleHigh;
808 
809     mDelayXResample = false;
810     mIntermediateX = 0;
811     mPdstBuf = null;
812     mPtmpBuf = null;
813     mClistXForced = false;
814     mPclistX = null;
815     mClistYForced = false;
816     mPclistY = null;
817     mPsrcYCount = null;
818     mPsrcYFlag = null;
819     mPscanBuf = null;
820     mStatus = StatusOkay;
821 
822     mResampleSrcX = srcX;
823     mResampleSrcY = srcY;
824     mResampleDstX = dstX;
825     mResampleDstY = dstY;
826 
827     mBoundaryOp = boundaryOp;
828 
829     if ((mPdstBuf = cast(Sample*)malloc(mResampleDstX*Sample.sizeof)) is null) {
830       mStatus = StatusOutOfMemory;
831       return;
832     }
833 
834     if (PfilterIndex < 0 || PfilterIndex >= NumFilters) {
835       PfilterIndex = resamplerFindFilterInternal(ImageResizeDefaultFilter);
836       if (PfilterIndex < 0 || PfilterIndex >= NumFilters) {
837         mStatus = StatusBadFilterName;
838         return;
839       }
840     }
841 
842     func = gFilters[PfilterIndex].func;
843     support = gFilters[PfilterIndex].support;
844 
845     // Create contributor lists, unless the user supplied custom lists.
846     if (PclistX is null) {
847       mPclistX = makeClist(mResampleSrcX, mResampleDstX, mBoundaryOp, func, support, filterXScale, srcXOfs);
848       if (mPclistX is null) {
849         mStatus = StatusOutOfMemory;
850         return;
851       }
852     } else {
853       mPclistX = PclistX;
854       mClistXForced = true;
855     }
856 
857     if (PclistY is null) {
858       mPclistY = makeClist(mResampleSrcY, mResampleDstY, mBoundaryOp, func, support, filterYScale, srcYOfs);
859       if (mPclistY is null) {
860         mStatus = StatusOutOfMemory;
861         return;
862       }
863     } else {
864       mPclistY = PclistY;
865       mClistYForced = true;
866     }
867 
868     if ((mPsrcYCount = cast(int*)calloc(mResampleSrcY, int.sizeof)) is null) {
869       mStatus = StatusOutOfMemory;
870       return;
871     }
872 
873     if ((mPsrcYFlag = cast(ubyte*)calloc(mResampleSrcY, ubyte.sizeof)) is null) {
874       mStatus = StatusOutOfMemory;
875       return;
876     }
877 
878     // Count how many times each source line contributes to a destination line.
879     for (i = 0; i < mResampleDstY; ++i) {
880       for (j = 0; j < mPclistY[i].n; ++j) {
881         ++mPsrcYCount[resamplerRangeCheck(mPclistY[i].p[j].pixel, mResampleSrcY)];
882       }
883     }
884 
885     if ((mPscanBuf = cast(ScanBuf*)malloc(ScanBuf.sizeof)) is null) {
886       mStatus = StatusOutOfMemory;
887       return;
888     }
889 
890     for (i = 0; i < MaxScanBufSize; ++i) {
891       mPscanBuf.scanBufY.ptr[i] = -1;
892       mPscanBuf.scanBufL.ptr[i] = null;
893     }
894 
895     mCurSrcY = mCurDstY = 0;
896     {
897       // Determine which axis to resample first by comparing the number of multiplies required
898       // for each possibility.
899       int xOps = countOps(mPclistX, mResampleDstX);
900       int yOps = countOps(mPclistY, mResampleDstY);
901 
902       // Hack 10/2000: Weight Y axis ops a little more than X axis ops.
903       // (Y axis ops use more cache resources.)
904       int xyOps = xOps*mResampleSrcY+(4*yOps*mResampleDstX)/3;
905       int yxOps = (4*yOps*mResampleSrcX)/3+xOps*mResampleDstY;
906 
907       // Now check which resample order is better. In case of a tie, choose the order
908       // which buffers the least amount of data.
909       if (xyOps > yxOps || (xyOps == yxOps && mResampleSrcX < mResampleDstX)) {
910         mDelayXResample = true;
911         mIntermediateX = mResampleSrcX;
912       } else {
913         mDelayXResample = false;
914         mIntermediateX = mResampleDstX;
915       }
916     }
917 
918     if (mDelayXResample) {
919       if ((mPtmpBuf = cast(Sample*)malloc(mIntermediateX*Sample.sizeof)) is null) {
920         mStatus = StatusOutOfMemory;
921         return;
922       }
923     }
924   }
925 
926   ~this () {
927      import core.stdc.stdlib : free;
928 
929      if (mPdstBuf !is null) {
930        free(mPdstBuf);
931        mPdstBuf = null;
932      }
933 
934      if (mPtmpBuf !is null) {
935        free(mPtmpBuf);
936        mPtmpBuf = null;
937      }
938 
939      // Don't deallocate a contibutor list if the user passed us one of their own.
940      if (mPclistX !is null && !mClistXForced) {
941        free(mPclistX.p);
942        free(mPclistX);
943        mPclistX = null;
944      }
945      if (mPclistY !is null && !mClistYForced) {
946        free(mPclistY.p);
947        free(mPclistY);
948        mPclistY = null;
949      }
950 
951      if (mPsrcYCount !is null) {
952        free(mPsrcYCount);
953        mPsrcYCount = null;
954      }
955 
956      if (mPsrcYFlag !is null) {
957        free(mPsrcYFlag);
958        mPsrcYFlag = null;
959      }
960 
961      if (mPscanBuf !is null) {
962        foreach (immutable i; 0..MaxScanBufSize) if (mPscanBuf.scanBufL.ptr[i] !is null) free(mPscanBuf.scanBufL.ptr[i]);
963        free(mPscanBuf);
964        mPscanBuf = null;
965      }
966   }
967 
968   // Reinits resampler so it can handle another frame.
969   void restart () {
970     import core.stdc.stdlib : free;
971     if (StatusOkay != mStatus) return;
972     mCurSrcY = mCurDstY = 0;
973     foreach (immutable i; 0..mResampleSrcY) {
974       mPsrcYCount[i] = 0;
975       mPsrcYFlag[i] = false;
976     }
977     foreach (immutable i; 0..mResampleDstY) {
978       foreach (immutable j; 0..mPclistY[i].n) {
979         ++mPsrcYCount[resamplerRangeCheck(mPclistY[i].p[j].pixel, mResampleSrcY)];
980       }
981     }
982     foreach (immutable i; 0..MaxScanBufSize) {
983       mPscanBuf.scanBufY.ptr[i] = -1;
984       free(mPscanBuf.scanBufL.ptr[i]);
985       mPscanBuf.scanBufL.ptr[i] = null;
986     }
987   }
988 
989   // false on out of memory.
990   bool putLine (const(Sample)* Psrc) {
991     int i;
992 
993     if (mCurSrcY >= mResampleSrcY) return false;
994 
995     // Does this source line contribute to any destination line? if not, exit now.
996     if (!mPsrcYCount[resamplerRangeCheck(mCurSrcY, mResampleSrcY)]) {
997       ++mCurSrcY;
998       return true;
999     }
1000 
1001     // Find an empty slot in the scanline buffer. (FIXME: Perf. is terrible here with extreme scaling ratios.)
1002     for (i = 0; i < MaxScanBufSize; ++i) if (mPscanBuf.scanBufY.ptr[i] == -1) break;
1003 
1004     // If the buffer is full, exit with an error.
1005     if (i == MaxScanBufSize) {
1006       mStatus = StatusScanBufferFull;
1007       return false;
1008     }
1009 
1010     mPsrcYFlag[resamplerRangeCheck(mCurSrcY, mResampleSrcY)] = true;
1011     mPscanBuf.scanBufY.ptr[i] = mCurSrcY;
1012 
1013     // Does this slot have any memory allocated to it?
1014     if (!mPscanBuf.scanBufL.ptr[i]) {
1015       import core.stdc.stdlib : malloc;
1016       if ((mPscanBuf.scanBufL.ptr[i] = cast(Sample*)malloc(mIntermediateX*Sample.sizeof)) is null) {
1017         mStatus = StatusOutOfMemory;
1018         return false;
1019       }
1020     }
1021 
1022     // Resampling on the X axis first?
1023     if (mDelayXResample) {
1024       import core.stdc.string : memcpy;
1025       assert(mIntermediateX == mResampleSrcX);
1026       // Y-X resampling order
1027       memcpy(mPscanBuf.scanBufL.ptr[i], Psrc, mIntermediateX*Sample.sizeof);
1028     } else {
1029       assert(mIntermediateX == mResampleDstX);
1030       // X-Y resampling order
1031       resampleX(mPscanBuf.scanBufL.ptr[i], Psrc);
1032     }
1033 
1034     ++mCurSrcY;
1035 
1036     return true;
1037   }
1038 
1039   // null if no scanlines are currently available (give the resampler more scanlines!)
1040   const(Sample)* getLine () {
1041     // if all the destination lines have been generated, then always return null
1042     if (mCurDstY == mResampleDstY) return null;
1043     // check to see if all the required contributors are present, if not, return null
1044     foreach (immutable i; 0..mPclistY[mCurDstY].n) {
1045       if (!mPsrcYFlag[resamplerRangeCheck(mPclistY[mCurDstY].p[i].pixel, mResampleSrcY)]) return null;
1046     }
1047     resampleY(mPdstBuf);
1048     ++mCurDstY;
1049     return mPdstBuf;
1050   }
1051 
1052   @property Status status () const { pragma(inline, true); return mStatus; }
1053 
1054   // returned contributor lists can be shared with another ImageResampleWorker
1055   void getClists (ContribList** ptrClistX, ContribList** ptrClistY) {
1056     if (ptrClistX !is null) *ptrClistX = mPclistX;
1057     if (ptrClistY !is null) *ptrClistY = mPclistY;
1058   }
1059 
1060   @property ContribList* getClistX () { pragma(inline, true); return mPclistX; }
1061   @property ContribList* getClistY () { pragma(inline, true); return mPclistY; }
1062 
1063   // filter accessors
1064   static @property auto filters () {
1065     static struct FilterRange {
1066     pure nothrow @trusted @nogc:
1067       int idx;
1068       @property bool empty () const { pragma(inline, true); return (idx >= NumFilters); }
1069       @property string front () const { pragma(inline, true); return (idx < NumFilters ? gFilters[idx].name : null); }
1070       void popFront () { if (idx < NumFilters) ++idx; }
1071       int length () const { return cast(int)NumFilters; }
1072       alias opDollar = length;
1073     }
1074     return FilterRange();
1075   }
1076 
1077 private:
1078   /* Ensure that the contributing source sample is
1079   * within bounds. If not, reflect, clamp, or wrap.
1080   */
1081   int reflect (in int j, in int srcX, in BoundaryOp boundaryOp) {
1082     int n;
1083     if (j < 0) {
1084       if (boundaryOp == BoundaryReflect) {
1085         n = -j;
1086         if (n >= srcX) n = srcX-1;
1087       } else if (boundaryOp == BoundaryWrap) {
1088         n = posmod(j, srcX);
1089       } else {
1090         n = 0;
1091       }
1092     } else if (j >= srcX) {
1093       if (boundaryOp == BoundaryReflect) {
1094         n = (srcX-j)+(srcX-1);
1095         if (n < 0) n = 0;
1096       } else if (boundaryOp == BoundaryWrap) {
1097         n = posmod(j, srcX);
1098       } else {
1099         n = srcX-1;
1100       }
1101     } else {
1102       n = j;
1103     }
1104     return n;
1105   }
1106 
1107   void resampleX (Sample* Pdst, const(Sample)* Psrc) {
1108     assert(Pdst);
1109     assert(Psrc);
1110 
1111     Sample total;
1112     ContribList *Pclist = mPclistX;
1113     Contrib *p;
1114 
1115     for (int i = mResampleDstX; i > 0; --i, ++Pclist) {
1116       int j = void;
1117       for (j = Pclist.n, p = Pclist.p, total = 0; j > 0; --j, ++p) total += Psrc[p.pixel]*p.weight;
1118       *Pdst++ = total;
1119     }
1120   }
1121 
1122   void scaleYMov (Sample* Ptmp, const(Sample)* Psrc, ResampleReal weight, int dstX) {
1123     // Not += because temp buf wasn't cleared.
1124     for (int i = dstX; i > 0; --i) *Ptmp++ = *Psrc++*weight;
1125   }
1126 
1127   void scaleYAdd (Sample* Ptmp, const(Sample)* Psrc, ResampleReal weight, int dstX) {
1128     for (int i = dstX; i > 0; --i) (*Ptmp++) += *Psrc++*weight;
1129   }
1130 
1131   void clamp (Sample* Pdst, int n) {
1132     while (n > 0) {
1133       *Pdst = clampSample(*Pdst);
1134       ++Pdst;
1135       --n;
1136     }
1137   }
1138 
1139   void resampleY (Sample* Pdst) {
1140     Sample* Psrc;
1141     ContribList* Pclist = &mPclistY[mCurDstY];
1142 
1143     Sample* Ptmp = mDelayXResample ? mPtmpBuf : Pdst;
1144     assert(Ptmp);
1145 
1146     // process each contributor
1147     foreach (immutable i; 0..Pclist.n) {
1148       // locate the contributor's location in the scan buffer -- the contributor must always be found!
1149       int j = void;
1150       for (j = 0; j < MaxScanBufSize; ++j) if (mPscanBuf.scanBufY.ptr[j] == Pclist.p[i].pixel) break;
1151       assert(j < MaxScanBufSize);
1152       Psrc = mPscanBuf.scanBufL.ptr[j];
1153       if (!i) {
1154         scaleYMov(Ptmp, Psrc, Pclist.p[i].weight, mIntermediateX);
1155       } else {
1156         scaleYAdd(Ptmp, Psrc, Pclist.p[i].weight, mIntermediateX);
1157       }
1158 
1159       /* If this source line doesn't contribute to any
1160        * more destination lines then mark the scanline buffer slot
1161        * which holds this source line as free.
1162        * (The max. number of slots used depends on the Y
1163        * axis sampling factor and the scaled filter width.)
1164        */
1165 
1166       if (--mPsrcYCount[resamplerRangeCheck(Pclist.p[i].pixel, mResampleSrcY)] == 0) {
1167         mPsrcYFlag[resamplerRangeCheck(Pclist.p[i].pixel, mResampleSrcY)] = false;
1168         mPscanBuf.scanBufY.ptr[j] = -1;
1169       }
1170     }
1171 
1172     // now generate the destination line
1173     if (mDelayXResample) {
1174       // X was resampling delayed until after Y resampling
1175       assert(Pdst != Ptmp);
1176       resampleX(Pdst, Ptmp);
1177     } else {
1178       assert(Pdst == Ptmp);
1179     }
1180 
1181     if (mLo < mHi) clamp(Pdst, mResampleDstX);
1182   }
1183 }
1184 
1185 
1186 // ////////////////////////////////////////////////////////////////////////// //
1187 private nothrow @trusted @nogc:
1188 int resamplerRangeCheck (int v, int h) {
1189   version(assert) {
1190     //import std.conv : to;
1191     //assert(v >= 0 && v < h, "invalid v ("~to!string(v)~"), should be in [0.."~to!string(h)~")");
1192     assert(v >= 0 && v < h); // alas, @nogc
1193     return v;
1194   } else {
1195     pragma(inline, true);
1196     return v;
1197   }
1198 }
1199 
1200 enum M_PI = 3.14159265358979323846;
1201 
1202 // Float to int cast with truncation.
1203 int castToInt (ImageResampleWorker.ResampleReal i) { pragma(inline, true); return cast(int)i; }
1204 
1205 // (x mod y) with special handling for negative x values.
1206 int posmod (int x, int y) {
1207   pragma(inline, true);
1208   if (x >= 0) {
1209     return (x%y);
1210   } else {
1211     int m = (-x)%y;
1212     if (m != 0) m = y-m;
1213     return m;
1214   }
1215 }
1216 
1217 // To add your own filter, insert the new function below and update the filter table.
1218 // There is no need to make the filter function particularly fast, because it's
1219 // only called during initializing to create the X and Y axis contributor tables.
1220 
1221 /* pulse/Fourier window */
1222 enum BoxFilterSupport = 0.5f;
1223 ImageResampleWorker.ResampleReal boxFilter (ImageResampleWorker.ResampleReal t) {
1224   // make_clist() calls the filter function with t inverted (pos = left, neg = right)
1225   if (t >= -0.5f && t < 0.5f) return 1.0f; else return 0.0f;
1226 }
1227 
1228 /* box (*) box, bilinear/triangle */
1229 enum TentFilterSupport = 1.0f;
1230 ImageResampleWorker.ResampleReal tentFilter (ImageResampleWorker.ResampleReal t) {
1231   if (t < 0.0f) t = -t;
1232   if (t < 1.0f) return 1.0f-t; else return 0.0f;
1233 }
1234 
1235 /* box (*) box (*) box */
1236 enum BellSupport = 1.5f;
1237 ImageResampleWorker.ResampleReal bellFilter (ImageResampleWorker.ResampleReal t) {
1238   if (t < 0.0f) t = -t;
1239   if (t < 0.5f) return (0.75f-(t*t));
1240   if (t < 1.5f) { t = (t-1.5f); return (0.5f*(t*t)); }
1241   return (0.0f);
1242 }
1243 
1244 /* box (*) box (*) box (*) box */
1245 enum BSplineSupport = 2.0f;
1246 ImageResampleWorker.ResampleReal BSplineFilter (ImageResampleWorker.ResampleReal t) {
1247   if (t < 0.0f) t = -t;
1248   if (t < 1.0f) { immutable ImageResampleWorker.ResampleReal tt = t*t; return ((0.5f*tt*t)-tt+(2.0f/3.0f)); }
1249   if (t < 2.0f) { t = 2.0f-t; return ((1.0f/6.0f)*(t*t*t)); }
1250   return 0.0f;
1251 }
1252 
1253 // Dodgson, N., "Quadratic Interpolation for Image Resampling"
1254 enum QuadraticSupport = 1.5f;
1255 ImageResampleWorker.ResampleReal quadratic (ImageResampleWorker.ResampleReal t, in ImageResampleWorker.ResampleReal R) {
1256   pragma(inline, true);
1257   if (t < 0.0f) t = -t;
1258   if (t < QuadraticSupport) {
1259     immutable ImageResampleWorker.ResampleReal tt = t*t;
1260     if (t <= 0.5f) return (-2.0f*R)*tt+0.5f*(R+1.0f);
1261     return (R*tt)+(-2.0f*R-0.5f)*t+(3.0f/4.0f)*(R+1.0f);
1262   }
1263   return 0.0f;
1264 }
1265 
1266 ImageResampleWorker.ResampleReal quadraticInterpFilter (ImageResampleWorker.ResampleReal t) {
1267   return quadratic(t, 1.0f);
1268 }
1269 
1270 ImageResampleWorker.ResampleReal quadraticApproxFilter (ImageResampleWorker.ResampleReal t) {
1271   return quadratic(t, 0.5f);
1272 }
1273 
1274 ImageResampleWorker.ResampleReal quadraticMixFilter (ImageResampleWorker.ResampleReal t) {
1275   return quadratic(t, 0.8f);
1276 }
1277 
1278 // Mitchell, D. and A. Netravali, "Reconstruction Filters in Computer Graphics."
1279 // Computer Graphics, Vol. 22, No. 4, pp. 221-228.
1280 // (B, C)
1281 // (1/3, 1/3)  - Defaults recommended by Mitchell and Netravali
1282 // (1, 0)    - Equivalent to the Cubic B-Spline
1283 // (0, 0.5)   - Equivalent to the Catmull-Rom Spline
1284 // (0, C)   - The family of Cardinal Cubic Splines
1285 // (B, 0)   - Duff's tensioned B-Splines.
1286 ImageResampleWorker.ResampleReal mitchell (ImageResampleWorker.ResampleReal t, in ImageResampleWorker.ResampleReal B, in ImageResampleWorker.ResampleReal C) {
1287   ImageResampleWorker.ResampleReal tt = t*t;
1288   if (t < 0.0f) t = -t;
1289   if (t < 1.0f) {
1290     t = (((12.0f-9.0f*B-6.0f*C)*(t*tt))+
1291          ((-18.0f+12.0f*B+6.0f*C)*tt)+
1292          (6.0f-2.0f*B));
1293     return (t/6.0f);
1294   }
1295   if (t < 2.0f) {
1296     t = (((-1.0f*B-6.0f*C)*(t*tt))+
1297          ((6.0f*B+30.0f*C)*tt)+
1298          ((-12.0f*B-48.0f*C)*t)+
1299          (8.0f*B+24.0f*C));
1300     return (t/6.0f);
1301   }
1302   return 0.0f;
1303 }
1304 
1305 enum MitchellSupport = 2.0f;
1306 ImageResampleWorker.ResampleReal mitchellFilter (ImageResampleWorker.ResampleReal t) {
1307   return mitchell(t, 1.0f/3.0f, 1.0f/3.0f);
1308 }
1309 
1310 enum CatmullRomSupport = 2.0f;
1311 ImageResampleWorker.ResampleReal catmullRomFilter (ImageResampleWorker.ResampleReal t) {
1312   return mitchell(t, 0.0f, 0.5f);
1313 }
1314 
1315 double sinc (double x) {
1316   pragma(inline, true);
1317   import std.math : sin;
1318   x *= M_PI;
1319   if (x < 0.01f && x > -0.01f) return 1.0f+x*x*(-1.0f/6.0f+x*x*1.0f/120.0f);
1320   return sin(x)/x;
1321 }
1322 
1323 ImageResampleWorker.ResampleReal clean (double t) {
1324   pragma(inline, true);
1325   import std.math : abs;
1326   enum EPSILON = cast(ImageResampleWorker.ResampleReal)0.0000125f;
1327   if (abs(t) < EPSILON) return 0.0f;
1328   return cast(ImageResampleWorker.ResampleReal)t;
1329 }
1330 
1331 //static double blackman_window(double x)
1332 //{
1333 //  return 0.42f+0.50f*cos(M_PI*x)+0.08f*cos(2.0f*M_PI*x);
1334 //}
1335 
1336 double blackmanExactWindow (double x) {
1337   pragma(inline, true);
1338   import std.math : cos;
1339   return 0.42659071f+0.49656062f*cos(M_PI*x)+0.07684867f*cos(2.0f*M_PI*x);
1340 }
1341 
1342 enum BlackmanSupport = 3.0f;
1343 ImageResampleWorker.ResampleReal blackmanFilter (ImageResampleWorker.ResampleReal t) {
1344   if (t < 0.0f) t = -t;
1345   if (t < 3.0f) {
1346     //return clean(sinc(t)*blackman_window(t/3.0f));
1347     return clean(sinc(t)*blackmanExactWindow(t/3.0f));
1348   }
1349   return (0.0f);
1350 }
1351 
1352 // with blackman window
1353 enum GaussianSupport = 1.25f;
1354 ImageResampleWorker.ResampleReal gaussianFilter (ImageResampleWorker.ResampleReal t) {
1355   import std.math : exp, sqrt;
1356   if (t < 0) t = -t;
1357   if (t < GaussianSupport) return clean(exp(-2.0f*t*t)*sqrt(2.0f/M_PI)*blackmanExactWindow(t/GaussianSupport));
1358   return 0.0f;
1359 }
1360 
1361 // Windowed sinc -- see "Jimm Blinn's Corner: Dirty Pixels" pg. 26.
1362 enum Lanczos3Support = 3.0f;
1363 ImageResampleWorker.ResampleReal lanczos3Filter (ImageResampleWorker.ResampleReal t) {
1364   if (t < 0.0f) t = -t;
1365   if (t < 3.0f) return clean(sinc(t)*sinc(t/3.0f));
1366   return (0.0f);
1367 }
1368 
1369 enum Lanczos4Support = 4.0f;
1370 ImageResampleWorker.ResampleReal lanczos4Filter (ImageResampleWorker.ResampleReal t) {
1371   if (t < 0.0f) t = -t;
1372   if (t < 4.0f) return clean(sinc(t)*sinc(t/4.0f));
1373   return (0.0f);
1374 }
1375 
1376 enum Lanczos6Support = 6.0f;
1377 ImageResampleWorker.ResampleReal lanczos6Filter (ImageResampleWorker.ResampleReal t) {
1378   if (t < 0.0f) t = -t;
1379   if (t < 6.0f) return clean(sinc(t)*sinc(t/6.0f));
1380   return (0.0f);
1381 }
1382 
1383 enum Lanczos12Support = 12.0f;
1384 ImageResampleWorker.ResampleReal lanczos12Filter (ImageResampleWorker.ResampleReal t) {
1385   if (t < 0.0f) t = -t;
1386   if (t < 12.0f) return clean(sinc(t)*sinc(t/12.0f));
1387   return (0.0f);
1388 }
1389 
1390 double bessel0 (double x) {
1391   enum EpsilonRatio = cast(double)1E-16;
1392   double xh = 0.5*x;
1393   double sum = 1.0;
1394   double pow = 1.0;
1395   int k = 0;
1396   double ds = 1.0;
1397   // FIXME: Shouldn't this stop after X iterations for max. safety?
1398   while (ds > sum*EpsilonRatio) {
1399     ++k;
1400     pow = pow*(xh/k);
1401     ds = pow*pow;
1402     sum = sum+ds;
1403   }
1404   return sum;
1405 }
1406 
1407 enum KaiserAlpha = cast(ImageResampleWorker.ResampleReal)4.0;
1408 double kaiser (double alpha, double halfWidth, double x) {
1409   pragma(inline, true);
1410   import std.math : sqrt;
1411   immutable double ratio = (x/halfWidth);
1412   return bessel0(alpha*sqrt(1-ratio*ratio))/bessel0(alpha);
1413 }
1414 
1415 enum KaiserSupport = 3;
1416 static ImageResampleWorker.ResampleReal kaiserFilter (ImageResampleWorker.ResampleReal t) {
1417   if (t < 0.0f) t = -t;
1418   if (t < KaiserSupport) {
1419     import std.math : exp, log;
1420     // db atten
1421     immutable ImageResampleWorker.ResampleReal att = 40.0f;
1422     immutable ImageResampleWorker.ResampleReal alpha = cast(ImageResampleWorker.ResampleReal)(exp(log(cast(double)0.58417*(att-20.96))*0.4)+0.07886*(att-20.96));
1423     //const ImageResampleWorker.Resample_Real alpha = KAISER_ALPHA;
1424     return cast(ImageResampleWorker.ResampleReal)clean(sinc(t)*kaiser(alpha, KaiserSupport, t));
1425   }
1426   return 0.0f;
1427 }
1428 
1429 // filters[] is a list of all the available filter functions.
1430 struct FilterInfo {
1431   string name;
1432   ImageResampleWorker.FilterFunc func;
1433   ImageResampleWorker.ResampleReal support;
1434 }
1435 
1436 static immutable FilterInfo[16] gFilters = [
1437    FilterInfo("box",              &boxFilter,             BoxFilterSupport),
1438    FilterInfo("tent",             &tentFilter,            TentFilterSupport),
1439    FilterInfo("bell",             &bellFilter,            BellSupport),
1440    FilterInfo("bspline",          &BSplineFilter,         BSplineSupport),
1441    FilterInfo("mitchell",         &mitchellFilter,        MitchellSupport),
1442    FilterInfo("lanczos3",         &lanczos3Filter,        Lanczos3Support),
1443    FilterInfo("blackman",         &blackmanFilter,        BlackmanSupport),
1444    FilterInfo("lanczos4",         &lanczos4Filter,        Lanczos4Support),
1445    FilterInfo("lanczos6",         &lanczos6Filter,        Lanczos6Support),
1446    FilterInfo("lanczos12",        &lanczos12Filter,       Lanczos12Support),
1447    FilterInfo("kaiser",           &kaiserFilter,          KaiserSupport),
1448    FilterInfo("gaussian",         &gaussianFilter,        GaussianSupport),
1449    FilterInfo("catmullrom",       &catmullRomFilter,      CatmullRomSupport),
1450    FilterInfo("quadratic_interp", &quadraticInterpFilter, QuadraticSupport),
1451    FilterInfo("quadratic_approx", &quadraticApproxFilter, QuadraticSupport),
1452    FilterInfo("quadratic_mix",    &quadraticMixFilter,    QuadraticSupport),
1453 ];
1454 
1455 enum NumFilters = cast(int)gFilters.length;
1456 
1457 
1458 bool rsmStringEqu (const(char)[] s0, const(char)[] s1) {
1459   for (;;) {
1460     if (s0.length && (s0.ptr[0] <= ' ' || s0.ptr[0] == '_')) { s0 = s0[1..$]; continue; }
1461     if (s1.length && (s1.ptr[0] <= ' ' || s1.ptr[0] == '_')) { s1 = s1[1..$]; continue; }
1462     if (s0.length == 0) {
1463       while (s1.length && (s1.ptr[0] <= ' ' || s1.ptr[0] == '_')) s1 = s1[1..$];
1464       return (s1.length == 0);
1465     }
1466     if (s1.length == 0) {
1467       while (s0.length && (s0.ptr[0] <= ' ' || s0.ptr[0] == '_')) s0 = s0[1..$];
1468       return (s0.length == 0);
1469     }
1470     assert(s0.length && s1.length);
1471     char c0 = s0.ptr[0];
1472     char c1 = s1.ptr[0];
1473     if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower
1474     if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower
1475     if (c0 != c1) return false;
1476     s0 = s0[1..$];
1477     s1 = s1[1..$];
1478   }
1479 }
1480 
1481 
1482 int resamplerFindFilterInternal (const(char)[] name) {
1483   if (name.length) {
1484     foreach (immutable idx, const ref fi; gFilters[]) if (rsmStringEqu(name, fi.name)) return cast(int)idx;
1485   }
1486   return -1;
1487 }