Downloads containing Resize v10.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Nyan Rush sAlAmAnDeR Mutator N/A Download file
JJ2+ Only: Foo Single Player 2/14:...Featured Download Violet CLM Single player 10 Download file
TSF with JJ2+ Only: Operation Cleanup: Turtle...Featured Download happygreenfrog Single player 8.2 Download file

File preview

//Resize v10.asc: version 1.0 (2018/02/08)
#pragma require "Resize v10.asc"
#include "TrueColor v12.asc"

/*
Resize v10.asc lets scriptwriters resize any image, animframe, animation, or animset by any positive multipliers, choosing from a number of different algorithms to see what looks best on any given image (sequence), unlike JJ2+'s normal jjDrawResizedSprite function which only uses nearest neighbor resizing.

SAMPLE SCRIPT:
	void onLevelLoad() {
		Resize::Resize(jjAnimSets[ANIM::RAVEN], 3, 3.0, Resize::Method::Scale2xSAI);
	}

	
API:
	jjPIXELMAP@ Resize::Resize(const jjPIXELMAP &in input, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f)
		Takes an existing jjPIXELMAP object and returns a new one. The returned jjPIXELMAP's width will be equal to scaleX * input.width and its height scaleY * input.height, and the image will be resized accordingly. If scaleY is left at its default value, it will be set to the same value as scaleX instead, meaning the aspect ratio will stay constant. The effects of negative scale values are undefined.
		
		For the method argument, you are encouraged to pass a member of the Resize::Method enum. There are two categories of these:
			Algorithms which do not introduce new colors: NearestNeighbor, Scale2x, AdvMAME2x, Eagle
			Algorithms which blend adjacent colors and thereby introduce new colors: BilinearInterpolation, Regular2xSAI, Scale2xSAI
		For more information about how these different algorithms differ you are referred to the internet.
		
		There are also two values in the Resize::Flags enum you may find useful for the method argument. These should be bitwise-OR'd together, e.g. "Resize::Method::Scale2x | Resize::Flags::EdgesRepeat".
			EdgesRepeat: Toggles whether the resizing algorithms should consider the image to repeat indefinitely outside the boundaries of the pixelmap, or to be surrounded by transparent pixels.
			OnlySpriteColors: Has an effect only for the algorithms that introduce new colors, restricting them to colors found in the standard sprite palette. You should use this flag if you anticipate calling jjPAL::apply after resizing any sprites.
			
			
	TrueColor::Bitmap@ Resize(const TrueColor::Bitmap &in input, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f)
		Same as the above function, but instead of jjPIXELMAP objects, it works on Bitmap objects from the TrueColor library. The Resize::Flags::OnlySpriteColors flag is ignored when working with TrueColor images, because it would be meaningless.
			
			
	void Resize::Resize(jjANIMFRAME & frame, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f)
		Resizes the image used by a jjANIMFRAME, and also multiplies that frame's hotspot, gunspot, and coldspot properties by the scale arguments. Note that unlike the above two functions, which return new objects, this one modifies the jjANIMFRAME in place.
	
	
	void Resize::Resize(const jjANIMATION & anim, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f)
	void Resize::Resize(jjANIMSET & set, uint animCount, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f)
		Convenience functions for resizing multiple jjANIMFRAMEs at once while keeping the method and scale values constant. Note that the latter function requires you to state explicitly how many animations are used by the jjANIMSET (e.g. 2 for ANIM::FISH), because JJ2 does not store that number anywhere.
		
		
	void Resize::AllocateAndResizeTrueColorSpriteSheet(const ANIM::Set setID, const TrueColor::Bitmap& bitmap, const array<array<TrueColor::Coordinates>>& setCoordinates, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f)
		Works the same (and has roughly the same signature) as TrueColor::AllocateSpriteSheet, but resizes each image (in accordance with scaleX/scaleY/method) from the sprite sheet before saving it. The coordinates in the setCoordinates array should refer to the coordinates within the image BEFORE they are resized.
			
WARNING: All internal code details are subject to change, and any properties, methods, or functions NOT described above should not be used by external scripts for risk of failing to work in later updates to this library.

*/






























namespace Resize {
	enum Method {
		NearestNeighbor, Scale2x, AdvMAME2x, Eagle, _firstInvolvingFindNearestColor, BilinearInterpolation, Regular2xSAI, Scale2xSAI, Super2xSAI, SuperEagle, HQ2x, xBR, //some of these don't work
		//http://supercomputingblog.com/graphics/coding-bilinear-interpolation/
		//http://www.scale2x.it/algorithm
		//https://github.com/Treeki/libxbr-standalone
		//https://vdnoort.home.xs4all.nl/emulation/2xsai
		//https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms
	};
	enum Flags {
		None = 0, _firstFlag = 1<<5, OnlySpriteColors = _firstFlag, EdgesRepeat = 1<<6
	};
	bool _edgesRepeat;
	
	class _Color {
		uint8 index = 0;
		jjPALCOLOR color;
		bool isPaletted = true;
		_Color(uint8 i) { index = i; color = jjPalette.color[i]; }
		_Color(const jjPALCOLOR &in c) { color = c; isPaletted = false; }
		bool opEquals(const _Color &in other) const {
			if (!isPaletted) return color == other.color;
			return index == other.index;
		}
	}
	
	class _Image {
		jjPIXELMAP@ pixelmap = null;
		TrueColor::Bitmap@ bitmap = null;
		uint width, height;
		_Image(const jjPIXELMAP &in pm) {
			@pixelmap = jjPIXELMAP(width = pm.width, height = pm.height);
			for (int x = width - 1; x >= 0; --x)
				for (int y = height - 1; y >= 0; --y)
					pixelmap[x,y] = pm[x,y]; //no native copy constructor available
		}
		_Image(const TrueColor::Bitmap &in bm) {
			@bitmap = TrueColor::Bitmap(bm, 0, 0, width = bm.width, height = bm.height); //copy constructor
		}
		_Image(const _Image &in i, uint w, uint h) {
			width = w; height = h;
			if (i.bitmap is null)
				@pixelmap = jjPIXELMAP(w, h);
			else
				@bitmap = TrueColor::Bitmap(w, h);
		}
		
		_Color@ Get(int x, int y) const {
			if (!_edgesRepeat) {
				if (x < 0 ||
				y < 0 ||
				uint(x) >= width ||
				uint(y) >= height)
					return (bitmap is null) ? _Color(0) : _Color(jjPALCOLOR());
			} else {
				if (x < 0) x = 0;
				else if (uint(x) >= width) x = width - 1;
				if (y < 0) y = 0;
				else if (uint(y) >= height) y = height - 1;
			}
			
			return (bitmap is null) ? _Color(pixelmap[x,y]) : _Color(bitmap.pixels[x][y]);
		}
		void Set(int x, int y, const _Color &in color) {
			if (x < 0 ||
			y < 0 ||
			uint(x) >= width ||
			uint(y) >= height)
				return;
			
			if (bitmap is null) {
				if (color.isPaletted)
					pixelmap[x,y] = color.index;
				else {
					const uint8 index = _palette.findNearestColor(color.color);
					const jjPALCOLOR foundColor = _palette.color[index];
					if (foundColor.red == 0 && foundColor.green == 0 && foundColor.blue == 0) pixelmap[x,y] = 0; //black = transparent
					else pixelmap[x,y] = index;
				}
			} else
				bitmap.pixels[x][y] = color.color;
		}
		void Set(int x, int y, const jjPALCOLOR &in color) {
			this.Set(x, y, _Color(color));
		}
	}
	
	jjPAL _palette;
	
	_Image@ Resize(_Image@ input, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f) {
		scaleX = abs(scaleX); //undefined?
		if (scaleY == 0.f) scaleY = scaleX;
		else scaleY = abs(scaleY);
				
		if (scaleX == 1.f && scaleY == 1.f)
			return @input;
		
		const auto width = uint(input.width * scaleX), height = uint(input.height * scaleY);
		
		_palette = jjPalette;
		const uint flags = method & ~(Flags::_firstFlag - 1);
		method ^= flags;
		if ((flags & Flags::OnlySpriteColors) != 0 && input.bitmap is null && method >= _firstInvolvingFindNearestColor) {
			jjPALCOLOR black;
			_palette.fill(black, 0, 15); //start
			_palette.fill(black, 56, 3); //yellow gap
			_palette.fill(black, 96, 159); //remainder + finish
		}
		_edgesRepeat = (flags & Flags::EdgesRepeat) != 0;
			
		//non-doubling-based algorithms which work at any size
		if (method == Method::BilinearInterpolation)
			return _bilinearInterpolation(input, width,height);
		if (method == Method::Scale2xSAI)
			return _scale_2xSaI(input, width,height);
		
		if ((scaleX > 1.f || scaleY > 1.f) && method != Method::NearestNeighbor) {
			float progressiveScale = scaleX;
			if (scaleY > progressiveScale) progressiveScale = scaleY; //max of two
			while (progressiveScale > 1.f) {
				@input = @_double(input, method);
				progressiveScale /= 2;
			}
			if (progressiveScale == 1.f && scaleX == scaleY)
				return @input;
		}
		
		return _nearestNeighbor(input, width,height);  //default, or simply last step in case of downscaling (after upscaling or otherwise)
	}
	jjPIXELMAP@ Resize(const jjPIXELMAP &in input, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f) {
		return Resize(_Image(input), scaleX, method, scaleY).pixelmap;
	}
	TrueColor::Bitmap@ Resize(const TrueColor::Bitmap &in input, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f) {
		return Resize(_Image(input), scaleX, method, scaleY).bitmap;
	}
	
	void Resize(jjANIMFRAME & frame, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f) {
		if (scaleY == 0.f) scaleY = scaleX;
		else scaleY = abs(scaleY);
		const uint oldWidth = frame.width;
		const uint oldHeight = frame.height;
		Resize(jjPIXELMAP(frame), scaleX, method, scaleY).save(frame);
		frame.hotSpotX = int(frame.hotSpotX * scaleX);
		frame.hotSpotY = int(frame.hotSpotY * scaleY);
		frame.gunSpotX = int(frame.gunSpotX * scaleX);
		frame.gunSpotY = int(frame.gunSpotY * scaleY);
		frame.coldSpotX = int(frame.coldSpotX * scaleX); //not that this accomplishes anything
		frame.coldSpotY = int(frame.coldSpotY * scaleY);
	}
	
	void Resize(const jjANIMATION & anim, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f) {
		const uint numFrames = anim.frameCount;
		for (uint i = 0; i < numFrames; ++i)
			Resize(jjAnimFrames[anim + i], scaleX, method, scaleY);
	}
	void Resize(jjANIMSET & set, uint animCount, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f) {
		if (set == 0)
			set.load();
		for (uint i = 0; i < animCount; ++i)
			Resize(jjAnimations[set + i], scaleX, method, scaleY);
	}
	
	_Image@ _double(const _Image &in input, uint method) {
		switch (method) {
			case Method::Scale2x:
				return _scale2x(input);
			case Method::AdvMAME2x:
				return _advMAME2x(input);
			case Method::Eagle:
				return _eagle(input);
			case Method::Regular2xSAI:
				return _2xSAI(input);
			/*case Method::xBR:
				return _xBR(input);
			case Method::HQ2x:
				return _HQ2x(input);
			case Method::Super2xSAI:
				return _super2xSAI(input);
			case Method::SuperEagle:
				return _superEagle(input);*/
		}
		//default: not yet implemented, etc.
		return _nearestNeighbor(input, input.width*2, input.height*2);
	}
	
	
	
	void AllocateAndResizeTrueColorSpriteSheet(const ANIM::Set setID, const TrueColor::Bitmap& bitmap, const array<array<TrueColor::Coordinates>>& setCoordinates, float scaleX, uint method = Resize::Method::NearestNeighbor, float scaleY = 0.f) {
		if (scaleX == 1.f && scaleY == 1.f) {
			TrueColor::AllocateSpriteSheet(setID, bitmap, setCoordinates);
			return;
		}
		scaleX = abs(scaleX); //undefined?
		if (scaleY == 0.f) scaleY = scaleX;
		else scaleY = abs(scaleY);
		
		array<uint> animSizes(setCoordinates.length);
		for (uint i = 0; i < animSizes.length; ++i) {
			animSizes[i] = setCoordinates[i].length * TrueColor::NumberOfFramesPerImage;
		}
		jjAnimSets[setID].allocate(animSizes);
		for (uint animID = 0; animID < animSizes.length; ++animID) {
			const array<TrueColor::Coordinates>@ animCoordinates = @setCoordinates[animID];
			for (uint frameID = 0; frameID < animCoordinates.length; ++frameID) {
				const TrueColor::Coordinates@ coordinates = animCoordinates[frameID];
				const TrueColor::Bitmap@ sprite = TrueColor::Bitmap(bitmap, coordinates.left, coordinates.top, coordinates.width, coordinates.height);
				@sprite = Resize::Resize(sprite, scaleX, method, scaleY);
				sprite.saveToAnimFrames(
					jjAnimations[jjAnimSets[setID].firstAnim + animID].firstFrame + frameID * TrueColor::NumberOfFramesPerImage,
					TrueColor::Coordinates(
						0, 0,
						sprite.width, sprite.height,
						int(coordinates.hotSpotX * scaleX), int(coordinates.hotSpotY * scaleY),
						int(coordinates.gunSpotX * scaleX), int(coordinates.gunSpotY * scaleY),
						int(coordinates.coldSpotX * scaleX), int(coordinates.coldSpotY * scaleY),
						coordinates.transparent,
						coordinates.swingingVine
					)
				);
			}
		}
	}
	
	
	
	_Image@ _nearestNeighbor(const _Image &in input, uint width, uint height) {
		_Image output(input, width, height);
		for (int x = width - 1; x >= 0; --x)
			for (int y = height - 1; y >= 0; --y)
				output.Set(x, y, input.Get(x * input.width / width, y * input.height / height));
		return output;
	}
	
	_Image@ _bilinearInterpolation(const _Image &in input, uint width, uint height) {
		_Image output(input, width, height);
		for (int x = width - 1; x >= 0; --x) {
			const float
				xx = float(x) * input.width / width,
				x1 = floor(xx),
				x2 = x1+1,
				RX = (x2 - xx);
			for (int y = height - 1; y >= 0; --y) {
				const float
					yy = float(y) * input.height / height,
					y1 = floor(yy),
					y2 = y1+1,
					RY = (y2 - yy);
				const jjPALCOLOR
					TopLeft = input.Get(int(x1), int(y1)).color,
					TopRight = input.Get(int(x2), int(y1)).color,
					BottomLeft = input.Get(int(x1), int(y2)).color,
					BottomRight = input.Get(int(x2), int(y2)).color;
					
				output.Set(x,y, _blendColors(_blendColors(TopLeft,TopRight,RX), _blendColors(BottomLeft,BottomRight,RX), RY));
			}
		}
		return output;
	}
	
	_Image@ _scale2x(const _Image &in input) {
		_Image output(input, input.width * 2, input.height * 2);
		for (int x = output.width - 2, xx = x>>1; x >= 0; x -= 2, --xx)
			for (int y = output.height - 2, yy = y>>1; y >= 0; y -= 2, --yy) {
				const _Color@
					B = input.Get(xx, yy-1),
					D = input.Get(xx-1, yy),
					E = input.Get(xx,yy),
					F = input.Get(xx+1, yy),
					H = input.Get(xx, yy+1);
				if (B != H && D != F) {
					const _Color@ result; //needs to be a separate variable to fix an angelscript bug
					@result = (D == B) ? D : E; output.Set(x,y, result);
					@result = (B == F) ? F : E; output.Set(x+1,y, result);
					@result = (D == H) ? D : E; output.Set(x,y+1, result);
					@result = (H == F) ? F : E; output.Set(x+1,y+1, result);
				} else {
					output.Set(x,y, E);
					output.Set(x+1,y, E);
					output.Set(x,y+1, E);
					output.Set(x+1,y+1, E);
				}
			}
		return output;
	}
	
	_Image@ _advMAME2x(const _Image &in input) {
		_Image output(input, input.width * 2, input.height * 2);
		for (int x = output.width - 2, xx = x>>1; x >= 0; x -= 2, --xx)
			for (int y = output.height - 2, yy = y>>1; y >= 0; y -= 2, --yy) {
				const _Color@
					A = input.Get(xx, yy-1),
					C = input.Get(xx-1, yy),
					P = input.Get(xx,yy),
					B = input.Get(xx+1, yy),
					D = input.Get(xx, yy+1);
				const _Color@ result; //needs to be a separate variable to fix an angelscript bug
				@result = (C==A && C!=D && A!=B) ? A : P; output.Set(x,y, result);
				@result = (A==B && A!=C && B!=D) ? B : P; output.Set(x+1,y, result);
				@result = (D==C && D!=B && C!=A) ? C : P; output.Set(x,y+1, result);
				@result = (B==D && B!=A && D!=C) ? D : P; output.Set(x+1,y+1, result);
			}
		return output;
	}
	
	_Image@ _eagle(const _Image &in input) {
		_Image output(input, input.width * 2, input.height * 2);
		for (int x = output.width - 2, xx = x>>1; x >= 0; x -= 2, --xx)
			for (int y = output.height - 2, yy = y>>1; y >= 0; y -= 2, --yy) {
				const _Color@
					S = input.Get(xx-1, yy-1),
					T = input.Get(xx, yy-1),
					U = input.Get(xx+1, yy-1),
					V = input.Get(xx-1, yy),
					C = input.Get(xx,yy),
					W = input.Get(xx+1, yy),
					X = input.Get(xx-1, yy+1),
					Y = input.Get(xx, yy+1),
					Z = input.Get(xx+1, yy+1);
				const _Color@ result; //needs to be a separate variable to fix an angelscript bug
				@result = (V==S&&S==T) ? S : C; output.Set(x,y, result);
				@result = (T==U&&U==W) ? U : C; output.Set(x+1,y, result);
				@result = (V==X&&X==Y) ? X : C; output.Set(x,y+1, result);
				@result = (W==Z&&Z==Y) ? Z : C; output.Set(x+1,y+1, result);
			}
		return output;
	}
	
	jjPALCOLOR _blendColors(const jjPALCOLOR &in a, const jjPALCOLOR &in b, const float balance = 0.5f) {
		if (a == b)
			return a;
		if (balance == 0.5f) {
			return jjPALCOLOR(
				(uint(a.red) + uint(b.red)) >> 1,
				(uint(a.green) + uint(b.green)) >> 1,
				(uint(a.blue) + uint(b.blue)) >> 1
			);
		} else {
			const float opposite = 1.f - balance;
			return jjPALCOLOR(
				uint8(balance * a.red + opposite * b.red),
				uint8(balance * a.green + opposite * b.green),
				uint8(balance * a.blue + opposite * b.blue)
			);
		}
	}

	int _saiGetResult1(const jjPALCOLOR &in A, const jjPALCOLOR &in B, const jjPALCOLOR &in C, const jjPALCOLOR &in D, const jjPALCOLOR &in E)
	{
		int x = 0;
		int y = 0;
		int r = 0;
		if (A == C) x+=1; else if (B == C) y+=1;
		if (A == D) x+=1; else if (B == D) y+=1;
		if (x <= 1) r+=1; 
		if (y <= 1) r-=1;
		return r;
	}

	int _saiGetResult2(const jjPALCOLOR &in A, const jjPALCOLOR &in B, const jjPALCOLOR &in C, const jjPALCOLOR &in D, const jjPALCOLOR &in E) 
	{
		int x = 0; 
		int y = 0;
		int r = 0;
		if (A == C) x+=1; else if (B == C) y+=1;
		if (A == D) x+=1; else if (B == D) y+=1;
		if (x <= 1) r-=1; 
		if (y <= 1) r+=1;
		return r;
	}


	int _saiGetResult(const jjPALCOLOR &in A, const jjPALCOLOR &in B, const jjPALCOLOR &in C, const jjPALCOLOR &in D)
	{
		int x = 0; 
		int y = 0;
		int r = 0;
		if (A == C) x+=1; else if (B == C) y+=1;
		if (A == D) x+=1; else if (B == D) y+=1;
		if (x <= 1) r+=1; 
		if (y <= 1) r-=1;
		return r;
	}
	
	_Image@ _2xSAI(const _Image &in input) {
		const _Color black(0);
		_Image@ output = _nearestNeighbor(input, input.width*2, input.height*2);
		for (int x = output.width - 2, xx = x>>1; x >= 0; x -= 2, --xx)
			for (int y = output.height - 2, yy = y>>1; y >= 0; y -= 2, --yy) {
				//---------------------------------------
// Map of the pixels:                    I|E F|J
//                                       G|A B|K
//                                       H|C D|L
//                                       M|N O|P
				const jjPALCOLOR
					I = input.Get(xx-1, yy-1).color,
					E = input.Get(xx+0, yy-1).color,
					F = input.Get(xx+1, yy-1).color,
					J = input.Get(xx+2, yy-1).color,
					G = input.Get(xx-1, yy+0).color,
					A = input.Get(xx+0, yy+0).color,
					B = input.Get(xx+1, yy+0).color,
					K = input.Get(xx+2, yy+0).color,
					H = input.Get(xx-1, yy+1).color,
					C = input.Get(xx+0, yy+1).color,
					D = input.Get(xx+1, yy+1).color,
					L = input.Get(xx+2, yy+1).color,
					M = input.Get(xx-1, yy+2).color,
					N = input.Get(xx+0, yy+2).color,
					O = input.Get(xx+1, yy+2).color,
					P = input.Get(xx+2, yy+2).color;
				jjPALCOLOR product, product1, product2;
				
				if ((A == D) && (B != C)) {
				   if ( ((A == E) && (B == L)) ||
						((A == C) && (A == F) && (B != E) && (B == J)) )
					  product = A;
				   else
					  product = _blendColors(A, B);

				   if (((A == G) && (C == O)) ||
					   ((A == B) && (A == H) && (G != C) && (C == M)) )
					  product1 = A;
				   else
					  product1 = _blendColors(A, C);
				   product2 = A;
				} else if ((B == C) && (A != D)) {
				   if (((B == F) && (A == H)) ||
					   ((B == E) && (B == D) && (A != F) && (A == I)) )
					  product = B;
				   else
					  product = _blendColors(A, B);

				   if (((C == H) && (A == F)) ||
					   ((C == G) && (C == D) && (A != H) && (A == I)) )
					  product1 = C;
				   else
					  product1 = _blendColors(A, C);
				   product2 = B;
				} else if ((A == D) && (B == C)) {
				   if (A == B)
				   {
					  product = A;
					  product1 = A;
					  product2 = A;
				   } else {
					  int r = 0;
					  product1 = _blendColors(A, C);
					  product = _blendColors(A, B);

					  r += _saiGetResult1(A, B, G, E, I);
					  r += _saiGetResult2(B, A, K, F, J);
					  r += _saiGetResult2(B, A, H, N, M);
					  r += _saiGetResult1(A, B, L, O, P);

					  if (r > 0)
						  product2 = A;
					  else if (r < 0)
						  product2 = B;
					  else
						  product2 = _blendColors(_blendColors(A, B), _blendColors(C, D));
				   }
				} else {
				   product2 = _blendColors(_blendColors(A, B), _blendColors(C, D));

				   if ((A == C) && (A == F) && (B != E) && (B == J))
					  product = A;
				   else
				   if ((B == E) && (B == D) && (A != F) && (A == I))
					  product = B;
				   else
					  product = _blendColors(A, B);

				   if ((A == B) && (A == H) && (G != C) && (C == M))
					  product1 = A;
				   else
				   if ((C == G) && (C == D) && (A != H) && (A == I))
					  product1 = C;
				   else
					  product1 = _blendColors(A, C);
				}
					
				output.Set(x,y, A);
				output.Set(x+1,y, product);
				output.Set(x,y+1, product1);
				output.Set(x+1,y+1, product2);
			}
		return output;
	}
	
	_Image@ _scale_2xSaI(const _Image &in input, uint width, uint height) {
		_Image output(input, width, height);
		for (int x = width - 1; x >= 0; --x) {
			const float
				xx = float(x) * input.width / width,
				RX = 1 - (floor(xx) + 1 - xx);
			for (int y = height - 1; y >= 0; --y) {
				const float
					yy = float(y) * input.height / height,
					RY = 1 - (floor(yy) + 1 - yy);
				const jjPALCOLOR
					A = input.Get(int(xx + 0), int(yy + 0)).color,
					B = input.Get(int(xx + 1), int(yy + 0)).color,
					C = input.Get(int(xx + 0), int(yy + 1)).color,
					D = input.Get(int(xx + 1), int(yy + 1)).color,
					E = input.Get(int(xx + 0), int(yy - 1)).color,
					F = input.Get(int(xx + 1), int(yy - 1)).color,
					G = input.Get(int(xx - 1), int(yy + 0)).color,
					H = input.Get(int(xx - 1), int(yy + 1)).color,
					I = input.Get(int(xx + 2), int(yy + 0)).color,
					J = input.Get(int(xx + 2), int(yy + 1)).color,
					K = input.Get(int(xx + 0), int(yy + 2)).color,
					L = input.Get(int(xx + 1), int(yy + 2)).color;
				
				jjPALCOLOR result;
/*0*/			if (A == B && C == D && A == C)
					result = A;
				else {
					const float
						FX = (RX / 2) + 0.25f,
						FY = (RY / 2) + 0.25f;
/*1*/				if (A == D && B != C) {
						if (RY <= FX && A == J && A != E) // close to B
							result = _blendColors(B, A, FX - RY);
						else if (RY >= FX && A == G && A != L) // close to C
							result = _blendColors(C, A, RY - FX);
						else if (RX >= FY && A == E && A != J) // close to 
							result = _blendColors(B, A, RX - FY);
						else if (RX <= FY && A == L && A != G) // close to C
							result = _blendColors(C, A, FY - RX);
						else if (RY >= RX) // close to C
							result = _blendColors(C, A, RY - RX);
						else //if (RY <= RX) // close to B
							result = _blendColors(B, A, RX - RY);
/*2*/				} else if (B == C && A != D) {
						const float
							RXO = (1.f - RX),
							RYO = (1.f - RY);
						if (RYO >= FX && B == H && B != F) // close to A
							result = _blendColors(A, B, RYO - FX);
						else if (RYO <= FX && B == I && B != K) // close to D
							result = _blendColors(D, B, FX - RYO);
						else if (RXO >= FY && B == F && B != H) // close to A
							result = _blendColors(A, B, RXO - FY);
						else if (RXO <= FY && B == K && B != I) // close to D
							result = _blendColors(D, B, FY - RXO);
						else if (RYO >= RX) // close to A
							result = _blendColors(A, B, RYO - RX);
						else //if (RYO <= RX) // close to D
							result = _blendColors(D, B, RX - RYO);
/*3*/				} else
						result = _blendColors(_blendColors(D, C, RX), _blendColors(B, A, RX), RY); //just use basic bilinear interpolation
				}
				output.Set(x,y, result);
			}
		}
		return output;
	}
}