Click here to Skip to main content
15,892,298 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I have an PNG image with a single-color shape on a transparent background. I want to blur the edges of the shape using the alpha channel to control opacity (so the blurred pixels will be the same color but the opacity will gradually diminish to 0).

I've looked around on google (and here on CodeProject), but haven't found anything that is a) fast enough to be used, or b) seems to work on the alpha channel. I found Christian Graus's 2003 article, but I don't think it does alpha channel stuff.

Can anyone help?

EDIT ================================

I'm still interested in something that's faster than the code below. I might try to optimize it later on, but for now, I'm simply too busy (and besides, it works as is).
Posted
Updated 26-Sep-11 8:34am
v2

1 solution

I found some code (but I can't find the source URL again). It was some german guy's code that was answering a similar question on MSDN. I'm using the sigma functions. I reformatted the code a little and added the NormalizeValue method (at the top) as well as the Blur method (at the bottom). I call the Blur method from my own code.

C#
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Collections;
using System.Drawing.Drawing2D;
using System.Collections.Generic;
namespace CamoPicker
{
	public static class ImageBlur
	{
		//--------------------------------------------------------------------------------
		private static int NormalizeValue(int value, int min, int max)
		{
			int result = Math.Min(Math.Max(value, min), max);
			return result;
		}
		//--------------------------------------------------------------------------------
		public static unsafe bool makeEdgesTransparentHorz(Bitmap bmp, int nWidth1, int nWidth2, int nZuwachs1, int nZuwachs2, bool bauto)
		{
			BitmapData bmData     = null;
			Bitmap     bmpOrig    = null;
			BitmapData bmDataOrig = null;
			try
			{
				ArrayList     Al        = new ArrayList();
				bmData                  = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0     = bmData.Scan0;
				byte*         p         = (byte*)(void*)Scan0;
				bmpOrig                 = (Bitmap)bmp.Clone();
				bmDataOrig              = bmpOrig.LockBits(new Rectangle(0, 0, bmpOrig.Width, bmpOrig.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0Orig = bmDataOrig.Scan0;
				byte*         pOrig     = (byte*)(void*)Scan0Orig;
				int           lp        = 0;
				for (int row = 0; row < bmp.Height; row++)
				{
					for (int col = 0; col < bmp.Width; col++)
					{
						p  = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				for (int i = 0; i < Al.Count; i++)
				{
					Point pt1 = (Point)Al[i];
					p = (byte*)(void*)Scan0;
					pOrig = (byte*)(void*)Scan0Orig;
					if (pt1.X <= bmp.Width - nWidth1)
					{
						p += (pt1.Y * bmp.Width * 4) + (pt1.X * 4);
						pOrig += (pt1.Y * bmpOrig.Width * 4) + (pt1.X * 4);
						int start = 255 - (int)(nZuwachs1 * nWidth1);
						int z = nZuwachs1;
						if (bauto)
						{
							z = Math.Max(1, (int)Math.Ceiling(255.0 / nWidth1));
							start = Math.Max(0, (int)(255 - (z * nWidth1)));
						}
						for (int ii = 0; ii < nWidth1; ii++)
						{
							//int val = start;
							//if (val < 0)
							//{
							//    val = 0;
							//}
							//if (val > 255)
							//{
							//    val = 255;
							//}
							int val = NormalizeValue(start, 0, 255);
							p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
							start += z;
							p += 4;
							pOrig += 4;
						}
					}
				}
				Al.Clear();
				lp = 0;
				for (int row = 0; row < bmp.Height; row++)
				{
					for (int col = bmp.Width - 1; col >= 0; col--)
					{
						p = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				for (int i = 0; i < Al.Count; i++)
				{
					Point pt1 = (Point)Al[i];
					p = (byte*)(void*)Scan0;
					pOrig = (byte*)(void*)Scan0Orig;
					if (pt1.X >= (nWidth2 - 1))
					{
						//p += ((nWidth2) * -4) + (pt1.Y * bmp.Width * 4) + (pt1.X * 4);
						int start = 255 - (int)(nZuwachs2 * nWidth2);
						int z = nZuwachs2;
						if (bauto)
						{
							z = Math.Max(1, (int)Math.Ceiling(255.0 / nWidth2));
							start = Math.Max(0, (int)(255 - (z * nWidth2)));
						}
						p = (byte*)(void*)Scan0;
						p += ((nWidth2) * -4) + (pt1.Y * bmp.Width * 4) + ((pt1.X + nWidth2) * 4);
						pOrig = (byte*)(void*)Scan0Orig;
						pOrig += ((nWidth2) * -4) + (pt1.Y * bmpOrig.Width * 4) + ((pt1.X + nWidth2) * 4);
						for (int ii = 0; ii < nWidth2; ii++)
						{
							//int val = start;
							//if (val < 0)
							//    val = 0;
							//if (val > 255)
							//    val = 255;
							int val = NormalizeValue(start, 0, 255);
							p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
							start += z;
							p -= 4;
							pOrig -= 4;
						}
					}
				}
				bmp.UnlockBits(bmData);
				bmpOrig.UnlockBits(bmDataOrig);
				bmpOrig.Dispose();
				bmpOrig = null;
				return true;
			}
			catch
			{
				try
				{
					bmpOrig.UnlockBits(bmDataOrig);
					bmp.UnlockBits(bmData);
				}
				catch
				{
				}
				if (bmpOrig != null)
				{
					bmpOrig.Dispose();
					bmpOrig = null;
				}
			}
			return false;
		}
		//--------------------------------------------------------------------------------
		public static unsafe bool makeEdgesTransparentVert(Bitmap bmp, int nWidth1, int nWidth2, int nZuwachs1, int nZuwachs2, bool bauto)
		{
			BitmapData bmData = null;
			Bitmap bmpOrig = null;
			BitmapData bmDataOrig = null;
			try
			{
				ArrayList Al = new ArrayList();
				bmData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0 = bmData.Scan0;
				byte* p = (byte*)(void*)Scan0;
				bmpOrig = (Bitmap)bmp.Clone();
				bmDataOrig = bmpOrig.LockBits(new Rectangle(0, 0, bmpOrig.Width, bmpOrig.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0Orig = bmDataOrig.Scan0;
				byte* pOrig = (byte*)(void*)Scan0Orig;
				int lp = 0;
				for (int col = 0; col < bmp.Width; col++)
				{
					for (int row = 0; row < bmp.Height; row++)
					{
						p = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				for (int i = 0; i < Al.Count; i++)
				{
					Point pt1 = (Point)Al[i];
					p = (byte*)(void*)Scan0;
					pOrig = (byte*)(void*)Scan0Orig;
					if (pt1.Y <= bmp.Height - nWidth1)
					{
						p += (pt1.Y * bmp.Width * 4) + (pt1.X * 4);
						pOrig += (pt1.Y * bmpOrig.Width * 4) + (pt1.X * 4);
						int start = 255 - (int)(nZuwachs1 * nWidth1);
						int z = nZuwachs1;
						if (bauto)
						{
							z = Math.Max(1, (int)Math.Ceiling(255.0 / nWidth1));
							start = Math.Max(0, (int)(255 - (z * nWidth1)));
						}
						for (int ii = 0; ii < nWidth1; ii++)
						{
							//int val = start;
							//if (val < 0)
							//    val = 0;
							//if (val > 255)
							//    val = 255;
							int val = NormalizeValue(start, 0, 255);
							p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
							start += z;
							p += (bmp.Width * 4);
							pOrig += (bmpOrig.Width * 4);
						}
					}
				}
				Al.Clear();
				lp = 0;
				for (int col = 0; col < bmp.Width; col++)
				{
					for (int row = bmp.Height - 1; row >= 0; row--)
					{
						p = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				for (int i = 0; i < Al.Count; i++)
				{
					Point pt1 = (Point)Al[i];
					p = (byte*)(void*)Scan0;
					pOrig = (byte*)(void*)Scan0Orig;
					if (pt1.Y >= (nWidth2 - 1))
					{
						//p += ((nWidth2) * bmp.Width * -4) + (pt1.Y * bmp.Width * 4) + (pt1.X * 4);
						int start = 255 - (int)(nZuwachs2 * nWidth2);
						int z = nZuwachs2;
						if (bauto)
						{
							z = Math.Max(1, (int)Math.Ceiling(255.0 / nWidth2));
							start = Math.Max(0, (int)(255 - (z * nWidth2)));
						}
						p = (byte*)(void*)Scan0;
						p += ((nWidth2) * bmp.Width * -4) + ((pt1.Y + nWidth2) * bmp.Width * 4) + (pt1.X * 4);
						pOrig = (byte*)(void*)Scan0Orig;
						pOrig += ((nWidth2) * bmpOrig.Width * -4) + ((pt1.Y + nWidth2) * bmpOrig.Width * 4) + (pt1.X * 4);
						for (int ii = 0; ii < nWidth2; ii++)
						{
							//int val = start;
							//if (val < 0)
							//    val = 0;
							//if (val > 255)
							//    val = 255;
							int val = NormalizeValue(start, 0, 255);
							p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
							start += z;
							p -= (bmp.Width * 4);
							pOrig -= (bmpOrig.Width * 4);
						}
					}
				}
				bmp.UnlockBits(bmData);
				bmpOrig.UnlockBits(bmDataOrig);
				bmpOrig.Dispose();
				bmpOrig = null;
				return true;
			}
			catch
			{
				try
				{
					bmpOrig.UnlockBits(bmDataOrig);
					bmp.UnlockBits(bmData);
				}
				catch
				{
				}
				if (bmpOrig != null)
				{
					bmpOrig.Dispose();
					bmpOrig = null;
				}
			}
			return false;
		}
		//--------------------------------------------------------------------------------
		public static double[] getVector(int Length)
		{
			if ((Length & 0x01) != 1)
			{
				MessageBox.Show("Length must be odd", "Hinweis", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
				return new Double[] { };
			}
			double[] KernelVector = new double[Length];
			int Radius = Length / 2;
			double a = -2.0 * Radius * Radius / Math.Log(0.01 /*Weight*/);
			double Sum = 0.0;
			for (int x = 0; x < KernelVector.Length; x++)
			{
				double dist = Math.Abs(x - Radius);
				KernelVector[x] = Math.Exp(-dist * dist / a);
				Sum += KernelVector[x];
			}
			//for (int x = 0; x < KernelVector.Length; x++)
			//{
			//    KernelVector[x] /= Sum;
			//}
			return KernelVector;
		}
		//--------------------------------------------------------------------------------
		public static unsafe bool makeEdgesTransparentHorzSigma(Bitmap bmp, int nWidth1, int nWidth2)
		{
			BitmapData bmData = null;
			Bitmap bmpOrig = null;
			BitmapData bmDataOrig = null;
			try
			{
				ArrayList Al = new ArrayList();
				bmData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0 = bmData.Scan0;
				byte* p = (byte*)(void*)Scan0;
				bmpOrig = (Bitmap)bmp.Clone();
				bmDataOrig = bmpOrig.LockBits(new Rectangle(0, 0, bmpOrig.Width, bmpOrig.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0Orig = bmDataOrig.Scan0;
				byte* pOrig = (byte*)(void*)Scan0Orig;
				int lp = 0;
				for (int row = 0; row < bmp.Height; row++)
				{
					for (int col = 0; col < bmp.Width; col++)
					{
						p = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				double[] Values = getVector((nWidth1 * 2) + 1);
				if (Values.Length / 2 == nWidth1)
				{
					for (int i = 0; i < Al.Count; i++)
					{
						Point pt1 = (Point)Al[i];
						p = (byte*)(void*)Scan0;
						pOrig = (byte*)(void*)Scan0Orig;
						if (pt1.X <= bmp.Width - nWidth1)
						{
							p += (pt1.Y * bmp.Width * 4) + (pt1.X * 4);
							pOrig += (pt1.Y * bmpOrig.Width * 4) + (pt1.X * 4);
							for (int ii = 0; ii < nWidth1; ii++)
							{
								int val = (int)Math.Max(0, (int)(255.0 * Values[ii]));
								//if (val < 0)
								//    val = 0;
								//if (val > 255)
								//    val = 255;
								val = NormalizeValue(val, 0, 255);
								p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
								p += 4;
								pOrig += 4;
							}
						}
					}
				}
				Al.Clear();
				lp = 0;
				for (int row = 0; row < bmp.Height; row++)
				{
					for (int col = bmp.Width - 1; col >= 0; col--)
					{
						p = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				Values = getVector((nWidth2 * 2) + 1);
				if (Values.Length / 2 == nWidth2)
				{
					for (int i = 0; i < Al.Count; i++)
					{
						Point pt1 = (Point)Al[i];
						p = (byte*)(void*)Scan0;
						pOrig = (byte*)(void*)Scan0;
						if (pt1.X >= (nWidth2 - 1))
						{
							p = (byte*)(void*)Scan0;
							p += ((nWidth2) * -4) + (pt1.Y * bmp.Width * 4) + ((pt1.X + nWidth2) * 4);
							pOrig = (byte*)(void*)Scan0Orig;
							pOrig += ((nWidth2) * -4) + (pt1.Y * bmpOrig.Width * 4) + ((pt1.X + nWidth2) * 4);
							for (int ii = 0; ii < nWidth2; ii++)
							{
								int val = (int)Math.Max(0, (int)(255.0 * Values[ii]));
								//if (val < 0)
								//    val = 0;
								//if (val > 255)
								//    val = 255;
								val = NormalizeValue(val, 0, 255);
								p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
								p -= 4;
								pOrig -= 4;
							}
						}
					}
				}
				bmp.UnlockBits(bmData);
				bmpOrig.UnlockBits(bmDataOrig);
				bmpOrig.Dispose();
				bmpOrig = null;
				return true;
			}
			catch
			{
				try
				{
					bmpOrig.UnlockBits(bmDataOrig);
					bmp.UnlockBits(bmData);
				}
				catch
				{
				}
				if (bmpOrig != null)
				{
					bmpOrig.Dispose();
					bmpOrig = null;
				}
			}
			return false;
		}
		//--------------------------------------------------------------------------------
		public static unsafe bool makeEdgesTransparentVertSigma(Bitmap bmp, int nWidth1, int nWidth2)
		{
			BitmapData bmData = null;
			Bitmap bmpOrig = null;
			BitmapData bmDataOrig = null;
			try
			{
				ArrayList Al = new ArrayList();
				bmData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0 = bmData.Scan0;
				byte* p = (byte*)(void*)Scan0;
				bmpOrig = (Bitmap)bmp.Clone();
				bmDataOrig = bmpOrig.LockBits(new Rectangle(0, 0, bmpOrig.Width, bmpOrig.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
				System.IntPtr Scan0Orig = bmDataOrig.Scan0;
				byte* pOrig = (byte*)(void*)Scan0Orig;
				int lp = 0;
				for (int col = 0; col < bmp.Width; col++)
				{
					for (int row = 0; row < bmp.Height; row++)
					{
						p = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				double[] Values = getVector((nWidth1 * 2) + 1);
				if (Values.Length / 2 == nWidth1)
				{
					for (int i = 0; i < Al.Count; i++)
					{
						Point pt1 = (Point)Al[i];
						p = (byte*)(void*)Scan0;
						pOrig = (byte*)(void*)Scan0Orig;
						if (pt1.Y <= bmp.Height - nWidth1)
						{
							p += (pt1.Y * bmp.Width * 4) + (pt1.X * 4);
							pOrig += (pt1.Y * bmpOrig.Width * 4) + (pt1.X * 4);
							for (int ii = 0; ii < nWidth1; ii++)
							{
								int val = (int)Math.Max(0, (int)(255.0 * Values[ii]));
								//if (val < 0)
								//    val = 0;
								//if (val > 255)
								//    val = 255;
								val = NormalizeValue(val, 0, 255);
								p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
								p += (bmp.Width * 4);
								pOrig += (bmpOrig.Width * 4);
							}
						}
					}
				}
				Al.Clear();
				lp = 0;
				for (int col = 0; col < bmp.Width; col++)
				{
					for (int row = bmp.Height - 1; row >= 0; row--)
					{
						p = (byte*)(void*)Scan0;
						p += (row * bmp.Width * 4) + (col * 4);
						if ((p[3] > 0) && (lp == 0))
						{
							Al.Add(new Point(col, row));
						}
						lp = (int)p[3];
					}
					lp = 0;
				}
				Al.TrimToSize();
				Values = getVector((nWidth2 * 2) + 1);
				if (Values.Length / 2 == nWidth2)
				{
					for (int i = 0; i < Al.Count; i++)
					{
						Point pt1 = (Point)Al[i];
						p = (byte*)(void*)Scan0;
						pOrig = (byte*)(void*)Scan0Orig;
						if (pt1.Y >= (nWidth2 - 1))
						{
							p = (byte*)(void*)Scan0;
							p += ((nWidth2) * bmp.Width * -4) + ((pt1.Y + nWidth2) * bmp.Width * 4) + (pt1.X * 4);
							pOrig = (byte*)(void*)Scan0Orig;
							pOrig += ((nWidth2) * bmpOrig.Width * -4) + ((pt1.Y + nWidth2) * bmpOrig.Width * 4) + (pt1.X * 4);
							for (int ii = 0; ii < nWidth2; ii++)
							{
								int val = (int)Math.Max(0, (int)(255.0 * Values[ii]));
								//if (val < 0)
								//    val = 0;
								//if (val > 255)
								//    val = 255;
								val = NormalizeValue(val, 0, 255);
								p[3] = (byte)(((double)pOrig[3] / 255.0) * val);
								p -= (bmp.Width * 4);
								pOrig -= (bmpOrig.Width * 4);
							}
						}
					}
				}
				bmp.UnlockBits(bmData);
				bmpOrig.UnlockBits(bmDataOrig);
				bmpOrig.Dispose();
				bmpOrig = null;
				return true;
			}
			catch
			{
				try
				{
					bmpOrig.UnlockBits(bmDataOrig);
					bmp.UnlockBits(bmData);
				}
				catch
				{
				}
				if (bmpOrig != null)
				{
					bmpOrig.Dispose();
					bmpOrig = null;
				}
			}
			return false;
		}
		//--------------------------------------------------------------------------------
		public static bool MergeBmp(Bitmap f, Bitmap z, Bitmap b)
		{
			BitmapData    bmData  = f.LockBits(new Rectangle(0, 0, f.Width, f.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
			BitmapData    bmData2 = z.LockBits(new Rectangle(0, 0, z.Width, z.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
			BitmapData    bmDataf = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
			int           stride  = bmData.Stride;
			System.IntPtr Scan0   = bmData.Scan0;
			System.IntPtr Scan02  = bmData2.Scan0;
			System.IntPtr Scanf   = bmDataf.Scan0;
			unsafe
			{
				byte* p       = (byte*)(void*)Scan0;
				byte* pf      = (byte*)(void*)Scan02;
				byte* pff     = (byte*)(void*)Scanf;
				int   nOffset = stride - b.Width * 4;
				int   nWidth  = b.Width;
				int   nHeight = b.Height;
				for (int y = 0; y < nHeight; y++)
				{
					for (int x = 0; x < nWidth; x++)
					{
						if (pf[3] <= pff[3])
						{
							p[3] = pf[3];
						}
						else
						{
							p[3] = pff[3];
						}
						p   += 4;
						pf  += 4;
						pff += 4;
					}
					p   += nOffset;
					pf  += nOffset;
					pff += nOffset;
				}
			}
			f.UnlockBits(bmData);
			z.UnlockBits(bmData2);
			b.UnlockBits(bmDataf);
			return true;
		}
		//--------------------------------------------------------------------------------
		public static void Blur(ref Bitmap bmp, bool useSigma, int width1, int width2, bool auto, int growth1, int growth2)
		{
			Bitmap bmp2 = null;
			Bitmap bmp3 = null;
			try
			{
				bmp2 = bmp.Clone() as Bitmap;
				bmp3 = bmp.Clone() as Bitmap;
				if (useSigma)
				{
					makeEdgesTransparentHorzSigma(bmp2, width1, width2);
					makeEdgesTransparentVertSigma(bmp3, width1, width2);
				}
				else
				{
					makeEdgesTransparentHorz(bmp2, width1, width2, growth1, growth2, auto);
					makeEdgesTransparentVert(bmp3, width1, width2, growth1, growth2, auto);
				}
				MergeBmp(bmp, bmp2, bmp3);
			}
			catch (Exception ex)
			{
				if (ex != null) {}
			}
			finally
			{
				if (bmp2 != null)
				{
					bmp2.Dispose();
				}
				if (bmp3 != null)
				{
					bmp3.Dispose();
				}
			}
		}
	}
	
}
 
Share this answer
 

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900